3. Стандартные (базовые) типы данных, операции и выражения

advertisement
C++
Конспект лекций
(часть 1)
1
Оглавление
Оглавление .....................................................................................................................................2
Введение .........................................................................................................................................4
1. Этапы и проблемы решения задач с использованием компьютера ......................................4
2. Общие сведения о языке программирования C++ и системе программирования ..............4
2.1. История создания языка C++ ............................................................................................4
2.2. Краткая характеристика языка C++ ..................................................................................5
Алфавит языка.......................................................................................................................5
Ключевые слова ......................................................................................................................6
Идентификаторы .................................................................................................................6
Знаки операций.......................................................................................................................7
Константы ............................................................................................................................7
Комментарии .........................................................................................................................7
2.3. Структура и основные элементы программы ..................................................................7
2.4. Трансляция программ и их выполнение ..........................................................................9
3. Стандартные (базовые) типы данных, операции и выражения ..........................................11
3.1. Типы данных, переменные и константы ........................................................................12
Понятие типов данных .......................................................................................................12
Классификация простых предопределенных типов данных ..........................................13
Переменные, константы ...................................................................................................13
3.2. Целочисленные типы данных..........................................................................................15
3.3. Вещественные типы данных ...........................................................................................16
3.4. Логический тип данных ...................................................................................................17
3.5. Символьный тип данных .................................................................................................17
3.6. Операции и выражения ....................................................................................................19
Преобразования типов данных ..........................................................................................21
Операция присваивания.......................................................................................................24
Арифметические операции .................................................................................................26
Операции отношения ..........................................................................................................30
Логические операции ...........................................................................................................30
Поразрядные (битовые) операции .....................................................................................31
Операции составного присваивания ..................................................................................35
Условная операция ...............................................................................................................36
Операция sizeof.....................................................................................................................36
Приоритеты рассмотренных операций ...........................................................................37
3.7. Ввод и вывод простых типов данных .............................................................................38
Вывод текстовых строк ....................................................................................................39
Ввод/вывод арифметических типов данных ....................................................................41
Форматирование ввода / вывода .......................................................................................42
4. Основные управляющие структуры программирования и управляющие конструкции в
языке С++ .....................................................................................................................................49
4.1. Идеи структурного программирования ..........................................................................49
2
4.2. Управляющие структуры и инструкции языка C++ .....................................................52
Условная инструкция (if) ....................................................................................................52
Инструкция множественного выбора (switch) ................................................................56
Цикл с предусловием (while) ...............................................................................................57
Цикл с постусловием (do while)..........................................................................................60
Итерационный цикл (for) ....................................................................................................61
Инструкции перехода ..........................................................................................................63
5. Приемы программирования циклов ......................................................................................65
5.1. Рекуррентные вычисления...............................................................................................65
5.2. Инвариант цикла ...............................................................................................................69
6. Массивы и указатели ...............................................................................................................71
6.1. Понятие массива ...............................................................................................................71
6.2. Объявление массивов .......................................................................................................72
Объявление одномерных массивов .....................................................................................72
Объявление многомерных массивов ..................................................................................73
6.3. Ввод-вывод массивов .......................................................................................................74
6.4. Текстовые строки как массивы символов ......................................................................75
6.5. Массивы и указатели ........................................................................................................75
7. Разработка программ при работе с массивами .....................................................................75
8. Функции и структура программы ..........................................................................................75
9. Организация ввода/вывода и работа с файлами ...................................................................76
Заключение...................................................................................................................................76
Приложение. Некоторые полезные примеры и иллюстрации к разделам конспекта ...........77
Примеры к разделу 5 ...............................................................................................................77
Вычисление факториала числа ..........................................................................................77
Быстрое возведение чисел в целую степень .....................................................................78
Нахождение наибольшего общего делителя (алгоритм Евклида) .................................79
3
Введение
Предмет дисциплины и ее задачи. Содержание и форма проведения
занятий. Связь с другими дисциплинами учебного плана.
1. Этапы и проблемы решения задач с использованием
компьютера
Решение задач на компьютере. Уровни вычислительных систем:
архитектура компьютера, операционные системы, методы и технологии
программирования, прикладные технологии, информационные системы.
Виды программного обеспечения (ПО): программа, программный комплекс,
программный продукт (изделие), программная система. Основные
показатели качества ПО. Жизненный цикл ПО: основные этапы и
процессы, их соотношение с содержанием и видами занятий учебной
дисциплины.
2. Общие сведения о языке программирования C++ и
системе программирования
Языки программирования и системы программирования. История
создания языков C и C++. Краткая характеристика языка C++.
Структура и основные элементы программы. Классификация действий и
данных. Пример программы на языке C++. Система программирования.
Трансляция программ и выполнение программы. Стандарты C и C++.
Системы программирования C/C++ для различных вычислительных
платформ и операционных сред.
2.1. История создания языка C++
Язык C++ создавался на основе языка C и является его расширенной и улучшенной
версией,
в
которой
реализованы
принципы
объектно-ориентированного
программирования. С++ также включает ряд других усовершенствований языка C,
4
например расширенный набор библиотечных функций. Поэтому историю его создания
следует начать с его предка.
Язык C был разработан Дэнисом Ритчи как "надстройка" над ассемблером в начале
70-х годов прошлого столетия. Язык C был предназначен для поддержки технологии
структурного программирования. В 1983 году был учрежден комитет по созданию ANSIстандарта, для обеспечения единства в различных реализациях этого языка. Конечная
версия этого стандарта стала доступной для желающих в начале 1990 годов. Эта версия
языка C получила название С89, и именно она явилась фундаментом, на котором был
построен язык C++. Язык C многие относят к языкам "среднего" уровня, который
позволяет программисту достаточно просто делать практически все, что он хочет, но за
последствия этих действий в большинстве случаев ответственность ложится именно на
программиста, а не на язык программирования.
Усложнение программ в конце 70-х годов привело к появлению новых технологий
программирования,
одной
из
которых
является
объектно-ориентированное
программирование. Язык C не поддерживал эту технологию, что, в конечном итоге, и
привело к разработке языка C++.
Дату рождения языка C++ относят к 1979 году, когда Бьерн Страуструп создал язык
"С c классам". Свое современное название (C++) он получил в 1983 году. Язык C++
полностью включает в себя все элементы языка C, то есть программы, написанные на
языке C, практически без изменений могут быть откомпилированы в системах C++.
Основные новшества языка C++ связаны с поддержкой технологий объектноориентированного программирования.
В 1994 году был предложен стандарт языка C++. Однако вскоре, в связи с созданием
Александром Степановым стандартной библиотеки шаблонов (STL), стандарт был
пересмотрен в сторону существенного расширения и усложнения. Этот стандарт появился
в свет в 1998 году и основным стандартом языка C++ до настоящего времени и
поддерживается всеми основными современными C++ - компиляторами.
C++ является родительским по отношению к таким языкам, как Java и C#. Эти языки
очень похожи, хотя и предназначены для решения задач различных категорий. Языки Java
и C# предназначены, в первую очередь, для решения задач ориентированных на сильно
распределенные сетевые среды. Но благодаря своей способности поддерживать
многоплатформные среды эти языки теряют в своей эффективности (например, в
быстродействии) по сравнению с языком C++.
2.2. Краткая характеристика языка C++
Алфавит языка
Алфавит C++ включает:
 прописные и строчные латинские буквы и знак подчеркивания;
 арабские цифры от 0 до 9;
 специальные знаки:
“{ } , | [ ] ( ) + - / % * . \ ‘ : ? < = > ! & # _ ; ^
 пробельные символы: пробел, символы табуляции, символы перехода на новую
строку.
Из символов алфавита формируются лексемы (лексема или
элементарная
конструкция - минимальная единица языка, имеющая самостоятельный смысл):
 ключевые (зарезервированные) слова;
 идентификаторы;
 знаки операций;
 константы;
5
 разделители (скобки, точка, запятая, пробельные символы);
 комментарии.
Границы лексем определяются другими лексемами, такими, как разделители или
знаки операций.
Ключевые слова
Всего в C++ 63 ключевых слова:
asm
auto
bool
break
case
catch
char
class
const
const_cast
continue
default
delete
do
double
dynamic_cast
else
enum
explicit
export
extern
false
float
for
friend
goto
if
inline
int
long
mutable
namespace
new
operator
private
protected
public
register
reinterpret_cast
return
short
signed
sizeof
static
static_cast
struct
switch
template
this
throw
true
try
typedef
typeid
typename
union
unsigned
using
virtual
void
volatile
wchar_t
while
Ключевые слова предопределены в языке программирования и имеют вполне
определенный смысл для компилятора. Использовать эти слова по какому-либо другому
назначению нельзя.
Идентификаторы
Идентификаторы – это имена различных программных объектов (имена
переменных, констант, функций и т.д.).
Синтаксически правильный идентификатор – это последовательность латинских букв,
цифр и символов «_» - нижнее подчеркивание, начинающаяся с буквы или символа «_».
Важно помнить, что в C++ различается строчное и прописное написание букв. То
есть, например, идентификаторы ABC, Abc и abc представляют в программе не один и
тот же объект, а три разных объекта. Некоторые компиляторы позволяют отключить
такую чувствительность к написанию букв, но делать этого не следует, так как это может
помешать переносимости программ с одного компилятора на другой.
Максимальная длина идентификатора в стандарте языка не установлена, однако, в
некоторых компиляторах это значение ограничено 32 символами или может
регулироваться с помощью настроек компилятора.
Использование в собственных идентификаторах в качестве первого символа «_»
нежелательно, так как так обычно именуются различные системные объекты, и в ряде
случаев может помешать переносимости программ с одного компилятора на другой.
Обычно этот символ используется для выделения отдельных частей идентификаторов с
целью обеспечения более удобного их восприятия.
Пользовательские идентификаторы не должны совпадать с ключевыми словами
языка. При таком совпадении компилятор выдает сообщение о синтаксической ошибке.
Также нежелательно совпадение пользовательских идентификаторов с именами
стандартных функций или переменных, так как при этом становится невозможным
6
использование соответствующих стандартных функций и переменных. В этом случае
ошибки при компиляции программы не возникает.
Имена программных объектов (идентификаторов) должны отражать назначение
именуемых объектов. Простые (обезличенные) идентификаторы следует использовать для
различных вспомогательных объектов, когда их область действия ограничена.
Знаки операций
Служат для указания действий над операндами. В зависимости от количества
операндов в C++ имеются унарные, бинарные и одна тернарная операции. Знаки операций
могут изображаться одним или несколькими символами. Если операция содержит в своем
изображении несколько символов, то между символами не должно быть пробелов.
Некоторые операции в C++ в зависимости от контекста могут выполнять разные действия.
Большинство стандартных операций можно переопределять (перегружать).
Константы
Константы – это данные, значения которых не могут меняться в процессе работы
программы. Подробно синтаксические правила записи различных констант будут
рассмотрены при изучении соответствующих типов данных. Сейчас только одно
замечание по поводу использования алфавита языка при формировании строковых
(текстовых) констант: в них можно использовать не только символы алфавита языка C++,
но и все другие символы, имеющиеся в используемой таблице символов (символы
национальных алфавитов, символы псевдографики и т.д.).
Комментарии
Комментарии – это фрагменты текста, игнорирующиеся компилятором при обработке
текста программы. Комментарии в текстах программ используются для различных
пояснений к тексту программы, а также для исключения временно не нужных фрагментов
текста программы (например, отладочных кодов или вариантов реализации).
В C++ имеются комментарии двух видов: однострочные и многострочные.
Однострочный комментарий начинается двумя символами // (прямой косой черты) и
заканчивается в конце строки текста программы.
Многострочный комментарий – начинается символами /* и заканчивается */ и
может содержать множество строк. Многострочные комментарии не могут вкладываться
друг в друга. Однострочные комментарии могут находиться внутри многострочных
комментариев.
2.3. Структура и основные элементы программы
Любая C++ программа представляет собой одну или несколько функций. Вот пример
простой программы с одной функцией, обеспечивающей вывод некоторого текста в
консольное окно:
#include <iostream>
using namespace std;
int main()
{
cout << "Это пример программы на языке C++" << endl;
return 0;
}
7
Это исходный код программы (исходный код – это текст, написанный на одном из
языков высокого уровня – в данном случае – на языке C++). Его нельзя выполнить. Для
выполнения программы с помощью компилятора надо получить результирующую
программу (объектный код).
Таким образом, необходимо выполнить следующие действия:
1. написать текст программы (создать исходный код);
2. откомпилировать этот текст (получить объектный код результирующей
программы);
3. выполнить результирующую программу.
Особенности выполнения этих действий существенно зависят от используемой
системы программирования.
Программа, приведенная выше, имеет несколько недостатков:
1. При ее выполнении в большинстве случаев (в зависимости от используемой
среды программирования) невозможно увидеть результат ее работы;
2. Выведенный в консольное окно текст на русском языке будет нечитабельным.
Вот второй вариант этой же программы, лишенный этих недостатков:
#include <iostream> /* Директива препроцессору включить в текст заголовочный файл
библиотеки классов C++ для управления вводом – выводом */
using namespace std; /* Директива компилятору использовать пространство имен std
стандартной библиотеки С++ */
int main() // Основная функция программы – начало выполнения программы
{
setlocale(0, ""); // Установка локальных настроек на вывод русского текста
cout << "Это пример программы на языке С++" << endl; // Вывод на экран
system("Pause"); // Приостановка выполнения программы
return 0; // Выход из функции и из программы
}
А вот пример более сложной программы, содержащей две функции:
#include <iostream>
using namespace std;
int sum(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
setlocale(0, "");
cout << "5 + 7 = " << sum(5, 7) << endl;
system("Pause");
return 0;
}
Более интересный вариант этой же программы, выполняющий сложение любых целых
чисел, введенных с клавиатуры:
8
#include <iostream>
using namespace std;
int sum(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int s1, s2;
setlocale(0, "");
cout << "Введите первое слагаемое: ";
cin >> s1;
cout << "Введите второе слагаемое: ";
cin >> s2;
cout << s1 << " + " << s2 << " = " << sum(s1, s2) << endl;
system("Pause");
return 0;
}
2.4. Трансляция программ и их выполнение
При желании исходный текст программ можно вводить с помощью любого
текстового редактора (например, с помощью Блокнота или WordPad) и обязательно
сохранять его именно в текстовом формате без информации форматирования. Дело в том,
что информация о форматировании помешает работе C++ - компилятора.
Имя файла, который будет содержать исходный код программы, формально может
быть любым. Но С++ - программы обычно хранятся в файлах с расширением .срр.
Поэтому называть исходные тексты С++ - программ можно любыми именами, но в
качестве расширения следует использовать .срр. Например, назвать предыдущую
программу можно, например, MyProg.срр.
Что необходимо сделать для того, чтобы выполнить трансляцию программы и
выполнить ее? Конкретные действия зависят от используемой системы
программирования.
Способ компиляции программ зависит от используемого компилятора и выбранных
опций. Более того, многие компиляторы, например Visual С++ (Microsoft) и С++ Builder
(Borland), предоставляют два различных способа компиляции программ: с помощью
компилятора командной строки и интегрированной среды разработки (Integrated
Development Environment — IDE). Поэтому для компилирования С++ - программ
невозможно дать универсальные инструкции, которые подойдут для всех компиляторов.
Поскольку самыми популярными компиляторами являются Visual С++ и С++ Builder,
здесь приведены инструкции по компиляции программ, соответствующие этим
компиляторам.
Чтобы скомпилировать программу MyProg.срр, используя Visual С++ компилятор
командной строки, необходимо ввести следующую командную строку:
C:\...>cl -GX MyProg.срр
9
Чтобы скомпилировать программу MyProg.срр, используя С++ Builder, необходимо
ввести такую командную строку:
С: \...>bcc32 MyProg.срр
В результате работы С++-компилятора получается выполняемый объектный код. Для
Windows-среды выполняемый файл будет иметь то же имя, что и исходный, но другое
расширение, а именно расширение .exe. Итак, выполняемая версия программы
MyProg.срр будет храниться в файле MyProg.ехе.
При использовании интегрированных сред разработки (IDE) соответствующие
действия выполняются с помощью соответствующих команд меню.
Скомпилированная программа готова к выполнению. Поскольку результатом работы
C++ компилятора является выполняемый объектный код, то для запуска программы
MyProg.ехе необходимо выполнить командную строку:
С:\...>MyProg.exe
Если используется интегрированная среда разработки, то выполнить программу
можно путем выбора из меню команды Run (Выполнить). При выполнении этой команды
осуществляется автоматическая компиляция программы и, при отсутствии ошибок,
программа запускается на выполнение.
Основным назначением компилятора (транслятора) является перевод исходного
текста программы, написанного на языке программирования понятного человеку, на
язык, понятный процессору — в машинные коды. Этот процесс состоит из нескольких
этапов. Рисунок иллюстрирует эти этапы для языка С++.
Сначала программа передается препроцессору, который выполняет директивы,
содержащиеся в ее тексте (например, включение в текст так называемых заголовочных
файлов — текстовых файлов, в которых содержатся описания используемых в программе
элементов).
Получившийся полный текст программы поступает на вход компилятора, который
выделяет лексемы, а затем на основе грамматики языка распознает выражения и
операторы, построенные из этих лексем. При этом компилятор выявляет синтаксические
ошибки и в случае их отсутствия строит объектный модуль.
Компоновщик, или редактор связей, формирует исполняемый модуль программы,
подключая к объектному модулю другие объектные модули, в том числе содержащие
функции библиотек, обращение к которым содержится в любой программе (например, для
осуществления вывода информации на экран). Если программа состоит из нескольких
исходных файлов, они компилируются по отдельности и объединяются на этапе
компоновки. Исполняемый модуль имеет расширение .exe и может быть запущен на
выполнение обычным образом.
При разработке программ возникают ошибки трех видов:
1. Синтаксические ошибки - нарушения синтаксиса (то есть грамматических
правил) языка программирования, например, пропущена точка с запятой;
2. Ошибки периода выполнения – возникающие только при работе программы
(например, деление на 0);
3. Логические ошибки – ошибки исходного алгоритма (например, вместо
операции + ошибочно использована операция *).
10
Исходный текст
программы (.cpp)
Включаемые файлы
(.h)
Препроцессор
Полный текст (.cpp)
Компилятор
Объектный код (.obj)
Библиотечные
модули
Компоновщик
Исполняемая
программа (.exe)
При компиляции программ, содержащих синтаксические ошибки, компилятор все их
обнаружит и выдаст соответствующие сообщения об ошибках и покажет приблизительное
место каждой из них. Однако эти сообщения не всегда точно отражают смысл и
положение синтаксической ошибки в исходном тексте.
Ошибки периода выполнения обнаруживаются только при выполнении программы –
эти ошибки, как правило, приводят к аварийному завершению работы программы с
выдачей соответствующего сообщения.
Логические ошибки компилятором и исполняющей системой компьютера не
обнаруживаются и могут привести к непредсказуемому поведению программы.
Обнаружить такие ошибки можно только путем анализа результатов работы программы
на различных наборах тестовых данных, но и в этом случае 100% уверенности в
правильности работы программы никогда нельзя дать.
3. Стандартные (базовые) типы данных, операции и
выражения
Простые стандартные типы данных (схема рассмотрения: множество
значений, переменные, константы, набор операций, выражения, битовое
представление,
оператор
присваивания).
Целочисленные
типы,
особенности представления и вычислений. Вещественные типы с
плавающей точкой. Особенности машинного представления вещественных
11
чисел. Свойства машинной арифметики. Машинное эпсилон. Булевский
(логический) тип, особенности логических выражений. Сокращенное
(неполное) вычисление логических выражений. Побитовые логические
операции. Символьный тип. Особенности
реализаций. Способы
использования символьного типа в программах. Преобразования типов.
Ввод и вывод значений стандартных типов с использованием стандартных
файлов.
3.1. Типы данных, переменные и константы
Понятие типов данных
Любая программа предназначена для обработки некоторых данных. Данные
представляют некоторую информацию. Информация многообразна – это числовая
информация, текстовая информация, аудио и видеоинформация и т.д. Однако, несмотря на
многообразие видов информации, внутреннее машинное представление ее едино. Любые
данные хранятся в памяти компьютера в виде двоичных кодов.
Память компьютера можно представить в виде непрерывной последовательности
двоичных ячеек, каждая из которых может находиться в двух состояниях условно
обозначаемых 0 и 1. Каждая такая двоичная ячейка называется битом. Вся эта
последовательность ячеек условно разбита на порции из 8 бит, называемые байтами.
Таким образом, 1 байт = 8 битам. Байт является основной единицей измерения объема
памяти.
С каждым байтом памяти связано понятие адреса, который, по сути, является
номером байта в непрерывной последовательности байтов памяти компьютера. То есть
каждый байт памяти имеет свой адрес. По этому адресу и осуществляется доступ к
данным, хранящимся в памяти.
Пусть, например, программе необходимо вывести на экран данные, хранящиеся в
байте с адресом 1. Но как это сделать? Ведь двоичный код, содержащийся в этом байте,
можно трактовать по-разному: это может быть число, а может быть это некоторая буква.
Таким образом, программе для правильной обработки этого байта необходимо «знать» что
это – число или буква. Другими словами, программе необходимо точно представлять
какие данные хранятся в этом байте памяти.
Для разрешения подобных коллизий в языках программирования введено понятие
типов данных.
Тип данных для каждого программного объекта, представляющего данные,
определяет:
 характер данных (число, со знаком или без знака, целое или с дробной частью,
одиночный символ или текст, представляющий последовательность символов и
т.д.);
 объем памяти, который занимают в памяти эти данные;
 диапазон или множество возможных значений;
 правила обработки этих данных (например, допустимые операции).
12
В разных языках программирования определены разные наборы типов данных, но, в
целом, типы данных можно разделить на две группы: простые и структурированные
типы. Простые типы данных представляют неразделимые данные, не имеющие
внутренней структуры (это, например, числа, символы и т.д.). Структурированные типы
данных, как это вытекает из их названия, имеют внутреннюю структуру (иногда
достаточно сложную). Структурированные типы строятся на основе простых типов
данных.
Другой уровень классификации разделяет все типы данных на предопределенные
(изначально встроенные в язык программирования) и пользовательские (типы данных,
определяемые программистом) типы данных.
Классификация простых предопределенных типов данных
Основные (предопределенные) типы данных часто называют арифметическими,
поскольку их можно использовать в арифметических операциях. Для описания основных
типов определены следующие ключевые слова:
 int (целый);
 float (вещественный);
 double (вещественный тип с двойной точностью);
 bool (логический);
 char (символьный).
Типы int, bool и char относят к группе целочисленных (целых) типов, а float и double
- к группе вещественных типов - типов с плавающей точкой. Код, который формирует
компилятор для обработки целых величин, отличается от кода для величин с плавающей
точкой.
Существует четыре спецификатора типа, уточняющих внутреннее представление и
диапазон значений стандартных типов:
 short (короткий);
 long (длинный);
 signed (знаковый);
 unsigned (без знаковый).
Спецификаторы добавляются слева к названию типа, например, так:
short int – короткое целое;
unsigned short int - короткое целое без знака.
Спецификаторы могут в произвольном порядке. Например: unsigned short int
эквивалентно short unsigned int.
Допустимы не все сочетания спецификаторов и типов данных. Например: unsigned
double является недопустимым сочетанием. Есть и другие варианты. Допустимые
сочетания спецификаторов и типов данных будут приведены при рассмотрении
конкретных типов данных.
Переменные, константы
В программах данные представлены константами и переменными.
Переменная — это именованная область памяти, в которой хранятся данные
определенного типа. Каждая переменная имеет имя и значение. Именем переменной
является идентификатор, придуманный программистом, и служит для обращения к
области памяти, в которой хранится значение этой переменной. Идентификатор
переменной преобразуется в адрес памяти, где хранится переменная, в процессе
компиляции программы. Перед использованием любая переменная должна быть описана.
Общее правил определения переменной можно сформулировать так:
13
[класс памяти] <тип данных> <идентификатор - имя> [инициализатор];
Понятие класс памяти определяет такие важные характеристики как время жизни и
область видимости переменных. Эти понятия будет рассмотрено позднее, и в следующих
примерах они пока не используется.
Примеры описания переменных:
int а; // переменная a типа int (целого типа)
double х; // переменная х типа double (вещественного типа с двойной точностью)
unsigned short int d; // переменная d – короткое целое без знака
Однотипные переменные можно определять в одной строке:
int i, j, k;
Описание переменной можно совместить с ее инициализацией:
int а = 1213; // переменная a инициализирована значением 1213
double х = 0.003; // переменная х инициализирована значением 0.003
unsigned short int d = 13; // переменная d инициализирована значением 13
Существует альтернативный способ
примеры эквивалентны предыдущим:
инициализации
переменных. Следующие
int а (1213); // переменная a инициализирована значением 1213
double х (0.003); // переменная х инициализирована значением 0.003
unsigned short int d (13); // переменная d инициализирована значением 13
Определения
неинициализированных
и
переменных можно совмещать в одной строке:
инициализированных
однотипных
int i = 0, j, k (10);
Инициализировать переменные можно не только конкретными значениями, как в
предыдущих примерах. В качестве инициализирующего значения можно использовать
любые допустимые выражения. Например:
double y = a * x; /* переменная y инициализирована значением равным произведению
значений переменных a и x */
При инициализации переменной присваивается конкретное значение. Однако, если
переменная не инициализирована, это не означает, что она не имеет значения. На самом
деле она хранит некоторое значение, которое находилось в этой области памяти до
определения переменной. Это значение невозможно предсказать. Такие случайные данные
обычно называют “мусором”.
Во время выполнения программы значение переменной можно изменять.
Константа – это величина, значение которой в процессе работы программы не
изменяется.
Константы бывают двух видов: константы – литералы и именованные константы.
Константы – литералы представляют собой сами значения. Например:
14
123
-245
0.003 -12.45 ’R’
”Это текст”
Тип данных, которому принадлежит констант – литера, определяется компилятором
автоматически по виду самого значения. Способы записи констант – литералов разных
типов будет рассмотрены позже при изучении соответствующих типов данных.
Именованные константы задаются с помощью ключевого слова const:
const double Pi = 3.14;
const int c1 = 1000, c2 = 2000;
const char point = ’.’;
Далее в программе можно использовать имена этих констант, а не сами значения.
Удобство использования именованных констант обусловлено возможностью изменения
значения константы (например, при модификации программы) только в одном месте
программы – в определении константы. При этом во всех местах программы, где
используется имя этой константы, будет применено ее новое значение.
3.2. Целочисленные типы данных
Размер типа int не определяется стандартом, а зависит от компьютера и компилятора.
Для 16-разрядного процессора под величины этого типа отводится 2 байта – в этом случае
диапазон возможных значений составляет -32 768 ... 32 767 (2 в степени 16 различных
значений). Для 32-разрядного - 4 байта – диапазон значений -2 147 483 648 ... 2 147
483 647 (2 в степени 32 различных значений).
Спецификатор short перед именем типа указывает компилятору, что под число
требуется отвести 2 байта независимо от разрядности процессора.
Спецификатор long означает, что целая величина будет занимать 4 байта. Таким
образом, на 16-разрядном компьютере эквиваленты int и short int, а на 32-разрядном —
int и long int.
При определении переменных вместо short int или long int можно использовать более
короткие обозначения: short или long соответственно:
short a;
long b;
Использование типов int, short int (short), long int (long) подразумевает
представление целых чисел со знаком, поэтому спецификатор signed можно не указывать.
Внутреннее представление величины целого типа — целое число в двоичном коде.
Например, число +22 типа short int (short) представляются в памяти так:
Номера разрядов: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Значения разрядов: 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 0
,
а отрицательное число -22 выглядит следующим образом:
Номера разрядов: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Значения разрядов: 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0
При использовании спецификатора signed (или при его отсутствии) старший бит
числа (в данном случае – разряд с номером 15) интерпретируется как знаковый (О —
положительное число, 1 — отрицательное).
15
Спецификатор unsigned позволяет представлять только положительные числа, при
этом старший разряд рассматривается как часть кода числа. Таким образом, диапазон
значений типа int зависит от спецификаторов. Диапазоны значений величин целого типа
со спецификатором unsigned выглядят так:
unsigned int (16-разрядного процессор): от 0 до 65 535;
unsigned int (32-разрядного процессор): от 0 до 4 294 967 295;
unsigned short int или unsigned short (не зависимо от процессора): от 0 до 65 535.
По умолчанию все целочисленные типы считаются знаковыми, то есть спецификатор
signed можно опускать.
Целые константы литералы можно задавать в трех форматах: в десятичном,
восьмеричном и в шестнадцатеричном.
В десятичном формате целые значения записываются в обычном виде:
1345 +34 -245
В восьмеричном формате сначала записывается 0 (нуль), а за ним восьмеричные
разряды самого числа:
011
07345
-0456
В шестнадцатеричном формате значащим разрядам числа должны предшествовать
символы 0x или 0X:
0x12B5 -0xAF2B
0X1FF02
Как уже говорилось ранее, константам, встречающимся в программе, приписывается
тот или иной тип в соответствии с их видом. Если этот тип по каким-либо причинам не
устраивает программиста, он может явно указать требуемый тип с помощью суффиксов L,
l (long) и u, U (unsigned). Например, константа 32L будет иметь тип long и занимать 4
байта. Можно использовать суффиксы L и U одновременно, например, Ox22UL или
05LU.
3.3. Вещественные типы данных
Стандарт C++ определяет три типа данных для хранения вещественных значений:
float, double и long double. Все эти типы предназначены для представления
отрицательных и положительных значений (спецификатор unsigned к ним не применим) в
разных диапазонах:
 тип float занимает в памяти 4 байта с диапазоном абсолютных значений от 3.4е-38
до 3.4е+38;
 тип double занимает в памяти 8 байт с диапазоном абсолютных значений от 1.7е308 до 1.7е+308;
 тип long double занимает в памяти 10 байт с диапазоном абсолютных значений от
3.4e-4932 до 3.4e+4932.
.
Замечание. В консольных приложениях Windows тип данных long double занимает в
памяти 8 байт, то есть ничем не отличается от типа double.
Константы вещественных типов задаются двумя способами:
 нормальный формат: 123.456 или -3.14;
 экспоненциальный формат: 1.23456e2 (1.23456е+2). Привести другие примеры.
16
Дробная часть отделяется от целой части точкой, а не запятой.
По умолчанию вещественные константы трактуются как константы типа double. Для
явного указания другого типа вещественной константы можно использовать суффиксы F
(f) - тип float или L (l) - тип long double:
3.14F - константа типа float,
3.14L- константа типа long double.
Вещественные типы данных (типы данных с плавающей точкой) хранятся в памяти
компьютера иначе, чем целочисленные. Внутреннее представление вещественного числа
состоит из двух частей — мантиссы и порядка:
-1.2345e+2
|
|
мантисса порядок
Тип float занимает 4 байта, из которых один двоичный разряд отводится под знак
мантиссы, 8 разрядов под порядок и 23 под мантиссу.
Для величин типа double, занимающих 8 байт, под порядок и мантиссу отводится 11 и
52 разряда соответственно. Длина мантиссы определяет точность числа, а длина порядка
— его диапазон.
Все вычисления с вещественными значениями осуществляются приближенно, при
этом, ошибки вычислений могут достигать весьма существенных значений. Это
объясняется дискретностью внутреннего (машинного) представления непрерывного
диапазона вещественных значений. Точность представления значений вещественных
типов зависит от размера мантиссы. Относительная точность представления
вещественных значений остается постоянной при различных значениях порядка. Однако,
абсолютная точность существенно зависит от значения порядка (с уменьшением порядка
абсолютная точность возрастает).
Дать приближенную оценку точности на примерах.
Пример неточности вычислений:
float a = 1e30f, b;
b = a + 1e10f;
cout << b - a << endl; // На экран выведено 0
3.4. Логический тип данных
Величины логического типа могут принимать только значения true и false,
являющиеся зарезервированными словами. Внутренняя форма представления значения
false - О (нуль). Любое другое значение интерпретируется как true. При преобразовании к
целому типу true имеет значение 1 (единица).
В памяти переменные этого типа занимают 1 байт.
Определения переменных этого типа выглядят, например, так:
bool b1, b2 = true, b3 (false), b4 = 1, b5 = 0;
Константы – литералы задаются ключевыми словами true и false.
Именованные константы этого типа особого смысла не имеют, но имеют право на
существование.
3.5. Символьный тип данных
Для обозначения этого типа используется ключевое слово char.
17
Под величину символьного типа отводится количество байт, достаточное для
размещения любого символа из набора символов для данного компьютера, что и
обусловило название типа. Как правило, это 1 байт. Тип char, как и другие целые типы,
может быть со знаком или без знака. В величинах со знаком можно хранить значения в
диапазоне от -128 до 127. По умолчанию тип char являемся знаковым, то есть
спецификатор signed использовать не обязательно. При использовании спецификатора
unsigned значения могут находиться в пределах от 0 до 255. Этого достаточно для
хранения любого символа из 256-символьного набора ASCII. Величины типа char могут
применяться и для хранения целых чисел, не выходящих за границы указанных
диапазонов.
Спецификаторы short и long к этому типу данных не применяются.
Константы - литералы символьного типа представляют собой символы, заключенные
в апострофы. Например:
’A’ ’!’ ’#’ ’f’ ’ш’ ’я’
В языке C++ существует понятие управляющих или ESCAPE –
последовательностей.
Управляющие последовательности начинаются символом обратной косой черты и
служат:
 Для представления символов, не имеющих графического изображения.
Например: ‘\n’ – перевод экранного курсора в начало следующей строки; ‘\t’
символ табуляции и т.д.
 Для представления некоторых специальных символов, а именно: знака косой
черты, апострофа, знака вопроса и кавычки – ‘\\’, ‘\’’, ‘\?’, ‘\”’.
 Для представления любого из 256 символов таблицы ASCII с помощью его
восьмеричного или шестнадцатеричного номера. Например: ‘\054’, ‘\x4A’.
Управляющая последовательность воспринимается компилятором как одиночный
символ.
Если непосредственно за обратной косой чертой следует не предусмотренный символ,
результат интерпретации не определен.
Если в последовательности цифр встречается недопустимая, она считается концом
цифрового кода.
Допустимые варианты управляющих последовательностей приведены в следующей
таблице:
Изображение
Шестнадцатеричный
код
Назначение
\а
7
Звуковой сигнал
\b
8
Возврат на шаг
\f
С
Перевод страницы (формата)
\n
A
Перевод строки
\г
D
Возврат каретки
\t
9
Горизонтальная табуляция
\v
8
Вертикальная табуляция
18
\\
5C
Обратная косая черта
\’
27
Апостроф
\"
22
Кавычка
\?
3F
Вопросительный знак
\0oo
—
0oo - восьмеричный код символа
\xdd
dd
xdd - шестнадцатеричный код символа
3.6. Операции и выражения
Обработка данных выполняется с помощью операций.
Операция – это действие, осуществляемое над операндами. Например:
2+3
Здесь операция сложения (+) выполняется над двумя операндами (2 и 3).
В зависимости от количества операндов в языке C++ имеются унарные (с одним
операндом), бинарные (с двумя операндами) и одна тернарная (с тремя операндами)
операция.
Из знаков операций, операндов и круглых скобок строятся выражения. В качестве
операндов могут использоваться константы, переменные, функции и другие выражения
(константы и переменные считаются частными случаями выражений). В результате
вычисления выражения получается некоторое значение определенного типа. Тип данных
значения выражения зависит от выполняемой операции (операций) и типов данных
операндов.
Особенностью языка C++ является то, что некоторые операции в зависимости от
контекста могут иметь разное назначение.
Ниже приведены операции характерные для рассмотренных выше арифметических
типов данных. Другие операции будут рассмотрены по мере изучения соответствующих
типов данных.
Унарные операции
Операция
Описание
++
увеличение на 1 (инкремент)
--
уменьшение на 1 (декремент)
sizeof
размер объекта или тапа данных в байтах
~
поразрядное отрицание
!
логическое отрицание
-
арифметическое отрицание (унарный минус)
+
унарный плюс
19
(тип)
явное преобразование типов
Бинарные операции
Операция
Описание
*
умножение
/
деление
%
остаток от деления
+
сложение
-
вычитание
<<
сдвиг влево
>>
сдвиг вправо
<
меньше
<=
меньше или равно
>
больше
>=
больше или равно
==
равно
!=
не равно
&
поразрядная конъюнкция (И)
^
поразрядное исключающее ИЛИ
|
поразрядная дизъюнкция (ИЛИ)
&&
||
логическое И
логическое ИЛИ
Особое значение имеет операция присвоения и ее модификации:
Операция
Краткое описание
=
присваивание
*=
умножение с присваиванием
/=
деление с присваиванием
%=
остаток отделения с присваиванием
+=
сложение с присваиванием
-=
вычитание с присваиванием
<<=
сдвиг влево с присваиванием
20
>>=
сдвиг вправо с присваиванием
&=
поразрядное И с присваиванием
|=
поразрядное ИЛИ с присваиванием
^=
поразрядное исключающее ИЛИ с присваиванием
Тернарная операция
Операция
?:
Описание
условная операция
При записи в тексте программы обозначений операций из двух и более символов,
между этими символами не должно быть никаких пробельных символов (пробелов,
символов табуляции, конца строки).
Преобразования типов данных
Рассмотрим пример:
Определены переменные
int a = 5;
double b = 7.6;
В программе необходимо подсчитать их сумму a + b.
Внутреннее (машинное) представление типа int и типа double существенно
различаются. Существенно различаются и процедуры сложения целых значений и
процедуры сложения вещественных значений. Как же тогда сложить целое и
вещественное? Выход – преобразовать оба значения к одному и тому же типу данных, а
затем выполнить соответствующую операцию. Но если преобразовать значение
переменной b к целому типу данных (отбросить дробную часть или округлить до
ближайшего целого) результат будет равен либо 12, либо 13, то есть произошла потеря
точности. А вот если сначала преобразовать значение a к типу double и сложить их как
вещественные значения, тогда точность потеряна не будет (результат будет равен 12.6 и
будет вещественного типа). На самом деле так и происходит.
Следовательно, при выполнении различных операций над разнотипными данными
необходимы преобразования одних типов данных к другим.
В языке C++ различают неявное (автоматическое) и явное преобразование типов
данных.
Неявное преобразование типов данных при выполнении операций, подобной
рассмотренной выше (и в ряде других случаев), выполняется компилятором по
определенным правилам автоматически. В чем же состоят эти правила?
Схема преобразования, используемая при выполнении арифметических операций,
называется обычными арифметическими преобразованиями. Эта схема может быть
описана следующими правилами:
1. Все данные типов char и short int преобразуются к типу int.
2. Если хотя бы один из операндов имеет тип double, то и другой операнд
преобразуется к типу double (если он другого типа); результат вычисления
имеет тип double.
21
3. Если хотя бы один из операндов имеет тип float, то и другой операнд
преобразуется к типу float (если он другого типа); результат вычисления имеет
тип float.
4. Если хотя бы один операнд имеет тип long, то и другой операнд преобразуется
к типу long (если он другого типа); результат имеет тип long.
5. Если хотя бы один из операндов имеет тип unsigned, то и другой операнд
преобразуется к типу unsigned (если его тип не unsigned); результат имеет тип
unsigned.
Если ни один из случаев 1-5 не имеет места, то оба операнда должны иметь тип int;
такой же тип будет и у результата.
Следует отметить, что компиляторы языка C++ достаточно свободно выполняют
подобные преобразования, что может в ряде случаев привести к неожиданным
результатам. Например:
#include <iostream>
using namespace std;
int main()
{
unsigned a = 5;
int b = 10;
cout << a << " - " << b << " = " << a - b << endl;
system("Pause");
return 0;
}
Результат работы программы:
5 – 10 = 4294967291
Таким образом, несмотря на то, что язык C++ достаточно «снисходителен» к
действиям программиста, это требует от программиста еще большей дисциплины в его
действиях и четких знаний нюансов языка программирования.
Для исправления ошибки в работе предыдущей программы можно, например,
изменить вычисление выражения a – b следующим образом: (int) a – b или int(a) – b. В
этом случае мы получим правильный результат:
5 – 10 = -5.
Здесь было использовано явное преобразование типов данных.
Явное преобразование типов данных осуществляется с помощью соответствующей
операции преобразования типов данных, которая имеет один из двух следующих
форматов:
(<тип данных>) <выражение> или <тип данных> (<выражение>)
Например:
(int) 3.14
(double) a
(long) (a + 1e5f)
или
int (3.14)
double (a)
long (a + 1e5f)
Подобные преобразования имеют своим исходом три ситуации:
 преобразование без потерь;
 с потерей точности;
 с потерей данных.
22
Преобразование происходит без потерь, если преобразуемое значение принадлежит
множеству значений типа, к которому осуществляется преобразование. Например:
short a = 100;
cout << (int) a << endl; // На экран выведено 100
cout << (char) a << endl; // Выведена буква d (ее десятичный эквивалент - 100)
cout << (float) a << endl; // На экран выведено 100
cout << (double) a << endl; // На экран выведено 100
float b = 3.14f;
cout << (double) b << endl; // На экран выведено 3.14
double d = 3.14;
cout << (float) d << endl; // На экран выведено 3.14
Преобразование любого вещественного типа к целому осуществляется путем
отбрасывания дробной части вещественного значения, поэтому практически всегда такие
преобразования приводят к потере точности (осуществляются приближенно). Например:
double d = 3.74;
cout << (int) d << endl; // На экран выведено 3
А вот попытки преобразования значений выходящих за пределы диапазона типа
данных, к которому осуществляется преобразование, приводят к полному искажению
данных. Например:
int a = -100;
cout << (unsigned) a << endl; // На экран выведено 4294967196
int a = 50000;
cout << (short) a << endl; // На экран выведено -15536
float b = 3e+9f;
cout << (int) b << endl; // На экран выведено -2147483648
double d = 3e+9;
cout << (int) d << endl; // На экран выведено -2147483648
double d = 3e+40;
cout << (float) d << endl; // На экран выведено 1.#INF - переполнение
double d = -3e+40;
cout << (float) d << endl; // На экран выведено -1.#INF - переполнение
Рассмотренная операция преобразования типов перешла в C++ из C. В C++ имеются
свои операции преобразования типов данных. Например, рассмотренные выше
преобразования в C++ можно было бы выполнить с помощью операции static_cast,
имеющей следующий формат:
static_cast <тип данных> (выражение)
Например:
static_cast <double> (a + 2e+40f)
Пользоваться явными преобразованиями типов следует очень аккуратно и только там,
где это действительно необходимо.
23
При явном преобразовании типов значения преобразуемых величин на самом деле не
изменяются – изменяется только представление этих значений при выполнении действий
над ними.
Операция присваивания
Одна из наиболее часто используемых операций. Формат операции:
<Переменная> = <Выражение>
|
|
Операнд 1
Операнд 2
Например: a = b; b = 3 * a; c = 1.234;
Сначала вычисляется значение выражения с правой стороны, а затем полученное
значение присваивается переменной в левой части операции (значение выражения
записывается в область памяти переменной). Старое значение переменной при этом
безвозвратно теряется.
При выполнении операции присваивания тип значения выражения
автоматически преобразуется к типу левого операнда (к типу данных переменной в
левой части). При этом возможны потери данных или точности (см. явное
преобразование типов). Например:
#include <iostream>
using namespace std;
int main()
{
int i = 100000, k;
short j = 10000, m;
k = j; // Короткое целое преобразуется к целому без потерь
m = i; // Преобразование целого к короткому целому приводит к искажению данных
cout << k << "
" << m << endl; // На экран будет выведено: 10000 -31072
k = 12500;
m = k; // Потери данных нет
cout << k << "
" << m << endl; // На экран будет выведено: 12500 12500
double d = 1.8234, n1, n2;
n1 = i; // Без ошибок
n2 = j; // Без ошибок
k = d; // С потерей точности
j = d; // С потерей точности
/* На экран будет выведено: 100000 10000 1
cout << n1 << "
" << n2 << "
" << k << "
1 */
" << j << endl;
d = 1e+100;
k = d; // С потерей данных
m = d; // С потерей данных
/* На экран будет выведено: 1е+100 -2147483648
cout << d << "
" << k << "
" << m << endl;
0 */
system("Pause");
return 0;
24
}
Операция присваивания в свою очередь является выражением, значением которого
является значение переменной в левой части после присваивания (эту переменную часто
называют L-значением (от слова left – левая сторона)). Например:
#include <iostream>
using namespace std;
int main()
{
int i = 7, j = 30, k;
cout << i * j << endl;
cout << (k = i * j) << endl;
cout << k << endl;
system("Pause");
return 0;
}
Результат работы программы:
210
210
210
Благодаря тому, что операция присваивания является выражением, в языке C++
возможно множественное присваивание:
<Переменная1> = <Переменная2> = … = <ПеременнаяN> = <Выражение>
Например:
#include <iostream>
using namespace std;
int main()
{
int i = 7, j = 30, k, l, m, n;
k = l = m = n = i + j;
cout << k << "
" << l << "
" << m << "
" << n << "
" << endl;
system("Pause");
return 0;
}
Результат работы программы:
37
37
37
37
Операция присваивания имеет самый низкий приоритет.
25
Арифметические операции
Унарный плюс (+) и унарный минус (-) Единственный операнд любого
арифметического типа. Формат записи:
+ < Выражение >
- < Выражение >
Унарный плюс возвращает значение операнда без изменений.
Унарный минус (его иногда называют арифметическим отрицанием) меняет знак
операнда на обратный.
Умножение - * - бинарная операция. Примеры записи:
a*b
2*3
a * 0.56
Операнды могут быть любого арифметического типа данных. Тип данных результата
операции определяется правилами неявного преобразования типов.
При выполнении возможен выход реального значения результата за допустимый
диапазон значений типа данных – при этом значение результата операции трудно
предсказать. Например:
cout << 1000000 * 1000000; // Результат: -727379968
cout << 1е20f * 1e20f; // Результат: 1.#INF – переполнение (+ бесконечность)
cout << 1е20f * -1e20f; // Результат: -1.#INF – переполнение (- бесконечность)
Деление - / - бинарная операция. Примеры записи:
a/b
2/3
a / 0.56
Если оба операнда являются целыми, то результат деления будет целым. В этом
случае целый результат получается отбрасыванием дробной части от полученного
реального значения (не округление). Например:
5/3
- результат равен 1.
Если все же в результате выполнения этой операции требуется получить значение с
дробной частью, необходимо чтобы хотя бы один из операндов был вещественного типа.
Например:
5.0 / 3 или 5 / 3. или 5.0 / 3.0 или 5 / 3f или 5f / 3 или 5f / 3f или
float (5) / 3 или double (5) / 3
Результатом вычисления всех этих выражений будет значение 1.6666… одного из
вещественных типов.
Однако, например, выражение double (5 / 3) хотя и будет вещественного типа, но его
значение все равно будет равно 1, поскольку сначала будет вычислено выражение в
скобках, результат которого будет целого типа и будет равен 1, а затем это значение будет
приведено к вещественному типу.
При выполнении операции возможен выход реального значения результата за
допустимый диапазон значений типа данных – при этом значение результата операции
трудно предсказать. Например:
26
cout << 1е35f / 0.0001f; // Результат: 1.#INF – переполнение (+ бесконечность)
cout << 1е20f / -0.0001f; // Результат: -1.#INF – переполнение (- бесконечность)
cout << 1е200 / 1e-200; // Результат: 1.#INF – переполнение (бесконечность)
Если первый операнд вещественного типа, то деление на 0 дает значение 1.#INF или 1.#INF. Если же он целого типа, возникает ошибка режима исполнения (деление на 0).
Остаток от деления - % - бинарная операция. Операнды только целого типа.
Результат операции целого типа. Например:
5%1
5%2
5%3
5%4
5%5
5%6
…..
- результат 0
- результат 1
- результат 2
- результат 1
- результат 0
- результат 5
Если второй операнд равен 0, возникает ошибка режима исполнения (деление на 0).
Сложение (+) и вычитание (-) – бинарные операции. Операнды могут быть любых
арифметических типов данных. Примеры записи:
a+b
a–b
Тип результата операций определяется правилами неявного преобразования типов.
При выполнении операций возможны ошибки переполнения и некорректного
преобразования типов данных операндов. Например:
unsigned short n = 0, m;
m = n - 1;
cout << m << endl; // На экран будет выведено 65535
n = m + 1;
cout << n << endl; // На экран будет выведено 0
Инкремент (++) и декремент (--) – унарные операции увеличения и уменьшения
операнда на 1 соответственно. Операнд может быть любого арифметического типа
данных.
Операции имеют две формы – префиксную (++a, --a) и постфиксную (a++, a--).
Независимо от формы операция инкремента эквивалентна следующему оператору:
a = a + 1;
а операция декремента следующему:
a = a - 1;
Например:
#include <iostream>
using namespace std;
27
int main()
{
int i = 7, j = 10;
++ i;
j ++;
cout << i << "\t" << j << endl; // На экран выведено 8
-- i;
j --;
cout << i << "\t" << j << endl; // На экран выведено 7
11
10
system("Pause");
return 0;
}
Из примера видно, что разница между префиксной и постфиксной формами этих
операций не чувствуется. Действительно в этом примере эти две формы работают
одинаково.
Немного изменим текст программы:
#include <iostream>
using namespace std;
int main()
{
int i = 7, j = 10;
cout << ++ i << "\t " << j ++ << endl; // На экран выведено 8 10
cout << i << "\t " << j << endl; // На экран выведено 8 11
cout << -- i << "\t " << j -- << endl; // На экран выведено 7 11
cout << i << "\t " << j << endl; // На экран выведено 7 10
system("Pause");
return 0;
}
Разница между префиксной и постфиксной формами этих операций заключается в
том, что при префиксной форме, переменная сначала меняет свое значение, а потом это
измененное значение обрабатывается. В постфиксной форме значение переменной
сначала обрабатывается и только потом ее значение изменяется на 1.
В первом примере операторы ++ i; j ++; просто изменяют значения переменных i и j
без какой-либо другой обработки и только после окончания работы этих операторов на
экран выводятся эти измененные значения. Поэтому различие между префиксной и
постфиксной формами не чувствуется. То же самое происходит и при выполнении
операторов -- i; j--;.
Во втором примере префиксные и постфиксные формы операций инкремента и
декремента участвуют в выполнении оператора вывода данных на экран. Поэтому при
выполнении префиксных операций сначала происходит изменение значений переменной i,
потом эти измененные значения выводятся на экран, а при выполнении постфиксных
операций сначала выводятся неизмененные значения переменной j, а уже после этого
производятся изменения значения этой переменной.
Еще пример:
#include <iostream>
28
using namespace std;
int main()
{
int i = 7, j = 10, k;
k = (++ i) + (j --); // Или k = ++ i + j --;
cout << "k = " << k << endl; // k = 18
cout << "i = " << i << endl; // i = 8
cout << "j = " << j << endl; // j = 9
system("Pause");
return 0;
}
А чему будет равно значение переменной k в этих случаях:
int i = 7, k;
k = (i ++) + i;
Здесь k = 14.
int i = 7, k;
k = (++ i) + i;
Здесь k = 16.
int i = 7, k;
k = i + (++ i);
Здесь k = 16.
int i = 7, k;
k = (++ i) + (++ i);
Здесь k = 18.
То есть сначала просматривается все выражение, при каждой встрече операции
инкремента (декремента) в префиксной форме выполняется изменение на единицу
переменной, а затем вычисляется выражение.
А как интерпретировать такие выражения:
a+++b
a---b
Эти выражения следует интерпретировать так:
(a++) + b
(a--) - b
Но не так:
a + (++b)
29
a - (--b)
Для более понятной записи текста программы в подобных случаях лучше
использовать скобки.
Операции отношения
Операции этой группы служат для сравнения значений. Сюда входят следующие
операции:
 == - равно;
 != - не равно;
 > - больше;
 >= - больше или равно;
 < - меньше;
 <= - меньше или равно.
Все эти операции бинарные. В качестве операндов могут быть использованы
выражения любых арифметических типов данных. Результат этих операций всегда
логического типа (bool).
Примеры:
a == b,
a != b,
a > 10,
(a - 3) >= (b + 10).
Логические операции
Эти операции используются при построении сложных логических выражений. В эту
группу входят 3 операции:
 !
- логическое отрицание (логическое НЕ);
 && - конъюнкция (логическое И);
 ||
- дизъюнкция (логическое ИЛИ).
Первая операция унарная, две остальные – бинарные. Операнды – выражения любого
арифметического типа данных, значения которых интерпретируются как значения
логического типа (отличное от 0 значение – true; 0 - false) . Результат этих операций логического типа.
Правила записи и результаты выполнения логических операций приведены в
следующей таблице:
a
0
0
1
1
b
0
1
0
1
!a
1
1
0
0
a && b
0
0
0
1
a || b
0
1
1
1
Пусть, например, имеется математическое неравенство: 0 < x < 10. На языке C++ это
неравенство следует записывать так: (0 < x) && (10 > x) или (х > 0) && (x < 10). А
математическое неравенство 0 > x > 10 должно выглядеть следующим образом: (0 > x) ||
(10 < x) или (х < 0) || (x > 10).
Особенностью выполнения операций && и || является то, что второй операнд (в
правой части операций) вычисляется не всегда. Он вычисляется только в том случае, если
значения первого операнда недостаточно для получения результата операций && или ||.
Например. Если в выражении (a + 10) && (b – 1) значение первого (левого) операнда
a + 10 равно 0 (false) (это будет при значении a = -10), то вычисление второго (правого)
30
операнда b – 1 не выполняется, так как и без его вычисления, значение результата
операции && уже известно – это false. А в выражении (a + 10) || (b – 1) второй операнд
не будет вычисляться в том случае, если первый операнд не равен 0 – в этом случае
результат операции || и так уже известен – он равен true.
Поразрядные (битовые) операции
Побитовые операции рассматривают операнды как упорядоченные наборы битов,
каждый бит может иметь одно из двух значений - 0 или 1 (наборы двоичных значений).
Такие операции позволяют программисту манипулировать значениями отдельных битов.
Объект, содержащий набор битов, иногда называют битовым вектором. Он позволяет
компактно хранить набор флагов - переменных, принимающих значение "да" "нет".
Операции сдвига - << и >> - бинарные операции. Операнды целого типа. Результат
также целого типа. Формат записи:
< Операнд 1 > << < Операнд 2 > - сдвиг влево
< Операнд 1 > >> < Операнд 2 > - сдвиг вправо
Операции выполняют копирование битов двоичного представления первого операнда
с сдвигом на количество разрядов, указанное во втором операнду, в соответствующем
направлении.
Значение второго операнда должно быть больше или равно 0 и меньше количества
двоичных разрядов первого операнда, иначе результат выполнения операций не
гарантирован (зависит от реализации, но обычно равен 0).
Примеры:
unsigned a = 20, n = 3, r;
r = a << n;
cout << r << endl; // На экран выведено 160
r = a >> n;
cout << r << endl; // На экран выведено 2
Иллюстрация:
Номер разряда:
Значение a:
Операция:
Значение r:
Операция:
Значение r:
31 30 … 8 7 6 5
0 0 … 0 0 0 0
a << n
0 0 … 0 1 0 1
a >> n
0 0 … 0 0 0 0
4 3 2 1 0
1 0 1 0 0
= 20
0
0
0
0
0
= 160
0
0
0
1
0
=2
Операция сдвига влево осуществляет перемещение битов левого операнда a в сторону
больших разрядов на количество разрядов, равное значению правого операнда n. Это
эквивалентно умножению значения a на 2 в степени n (20 * 8 = 160).
Операция сдвига вправо осуществляет перемещение битов левого операнда a в
сторону меньших разрядов на количество разрядов, равное значению правого операнда n.
Это эквивалентно делению значения a на 2 в степени n (целочисленное деление 20 / 8 = 2).
Используя операцию сдвига влево очень просто получить любую целую степень
двойки в диапазоне степеней равной количеству двоичных разрядов правого операнда без
1. Например, так:
1U << 20 - равно 2 в степени 20, то есть 1048576
31
При сдвиге влево (в сторону старших разрядов), освобождающиеся младшие разряды
замещаются 0 (нулями). При сдвиге вправо возможны две ситуации: если первый операнд
беззнаковый (unsigned), то освобождающиеся старшие разряды замещаются 0; если же
первый операнд знаковый, то освобождающиеся старшие разряды замещаются либо
знаковым разрядом, либо 0 (нет гарантии - зависит от реализации).
Поразрядные логические операции
К этой группе операций относятся:
 ~ - побитовое отрицание (побитовое НЕ) - унарная операция;
 & - побитовая конъюнкция (побитовое И) - бинарная операция;
 | - побитовая дизъюнкция (побитовое ИЛИ) - бинарная операция;
 ^ - побитовое исключающее ИЛИ - бинарная операция.
Операндами этих операций целочисленных типов данных. Результат также
целочисленный.
Операция побитовое отрицание (~) осуществляет инвертирование всех байтов
двоичного представления своего операнда. Например:
int a = 14, r;
r = ~a;
cout << r << endl; // На экран выведено -15
Иллюстрация:
Номер разряда:
Значение a:
Значение r = ~a:
31 30 … 8 7 6 5 4 3 2 1 0
0 0 … 0 0 0 0 0 1 1 1 0
1 1 … 1 1 1 1 1 0 0 0 1
= 14
= -15
Остальные операции выполняют соответствующую логическую операцию над каждой
парой соответствующих разрядов первого и второго операндов, интерпретируя значения
двоичных разрядов как логические значения (1 - true; 0 - false). Например:
int a = 14, b = 7, r;
r = a & b;
cout << r << endl; // На экран выведено 6
r = a | b;
cout << r << endl; // На экран выведено 15
r = a ^ b;
cout << r << endl; // На экран выведено 9
Иллюстрация:
Номер разряда:
Значение a:
Значение b:
Операция:
Значение r:
Операция:
Значение r:
Операция:
Значение r:
31 30 … 8 7 6 5
0 0 … 0 0 0 0
0 0 … 0 0 0 0
a&b
0 0 … 0 0 0 0
a|b
0 0 … 0 0 0 0
a^b
0 0 … 0 0 0 0
4 3 2 1 0
0 1 1 1 0
0 0 1 1 1
= 14
=7
0
0
1
1
0
=6
0
1
1
1
1
= 15
0
1
0
0
1
=9
32
Использование побитовых операций
Как уже говорилось, побитовые операции используются для обработки отдельных
двоичных разрядов памяти. Для манипулирования отдельным битом необходимо
научиться делать следующее:
 определять значение заданного бита;
 устанавливать значение заданного бита в значение 0 или 1;
 инвертировать значение заданного бита.
Это можно сделать так:
unsigned a = 1234; // Целое значение, битами которого мы будем управлять
unsigned short n = 4; // Номер необходимого бита (от 0 до 31)
bool r;
// Значение результата (0 или 1)
/* Узнаем, чему равен n-й бит (двоичный разряд) значения a. Результат поместим в
переменную r */
r = a & (1U << n);
cout << "Разряд с номером " << n << " равен " << r << endl; // значение 1
/* Установим n-й бит (двоичный разряд) значения a в 0. Результат поместим в
переменную а */
a = a & (~ (1U << n));
cout << "Значение а равно " << a << endl; // значение 1218
/* Проверяем */
r = a & (1U << n);
cout << "Разряд с номером " << n << " равен " << r << endl; // значение 0
/* Возвращаем n-й бит (двоичный разряд) значения a в 1. Результат поместим в
переменную а */
a = a | (1U << n);
cout << "Значение а равно " << a << endl; // значение 1234
/* Проверяем */
r = a & (1U << n);
cout << "Разряд с номером " << n << " равен " << r << endl; // значение 1
/* Инвертируем n-й бит (двоичный разряд) значения a. Результат поместим в
переменную а */
a = a ^ (1U << n);
cout << "Значение а равно " << a << endl; // значение 1218
/* Проверяем */
r = a & (1U << n);
cout << "Разряд с номером " << n << " равен " << r << endl; // значение 0
/* Еще раз инвертируем n-й бит (двоичный разряд) значения a. Результат поместим в
переменную а */
a = a ^ (1U << n);
cout << "Значение а равно " << a << endl; // значение 1234
/* Проверяем */
r = a & (1U << n);
cout << "Разряд с номером " << n << " равен " << r << endl; // значение 1
33
Изменяя значение переменной n в диапазоне от 0 до 31 можно выполнить все эти
действия над любым битом переменной a какое бы значение она не содержала.
Таким образом, для того, чтобы узнать, чему равен двоичный разряд с номером n в
значении переменной a, мы воспользовались выражением
a & (1U << n).
Иллюстрация вычисления этого выражения:
Номер разряда:
1U:
31 30 … 11 10 9 8 7 6 5 4 3 2 1 0
0 0 … 0 0 0 0 0 0 0 0 0 0 0 1
1U << n:
Значение a:
a & (1U << n):
0
0
0
0
0
0
… 0
… 0
… 0
0
1
0
0 0 0 0 0 1 0 0 0 0
0 0 1 1 0 1 0 0 1 0
0 0 0 0 0 1 0 0 0 0
=1
= 1234
= 16
Результатом вычисления этого выражения является целое значение не равное 0.
Операция присваивания этого значения логической переменной r автоматически
преобразует целое значение 16 в логическое значение true (т.е. 1).
Если бы значение a имело бы разряд с номером 4 равным 0, то результатом
вычисления этого выражения было бы значение 0. При выполнении операции
присваивания это значение было бы преобразовано в логическое значение false (т.е. 0).
Для установки значения разряда с номером n в переменной a в значение 0
используется выражение
a & (~ (1U << n)).
Иллюстрация вычисления этого выражения:
Номер разряда:
1U:
31 30 … 11 10 9 8 7 6 5 4 3 2 1 0
0 0 … 0 0 0 0 0 0 0 0 0 0 0 1
1U << n:
~ (1U << n):
0
1
0 … 0
1 … 1
0
1
0 0 0 0 0 1 0 0 0 0
1 1 1 1 1 0 1 1 1 1
Значение a:
a & (1U << n):
0
0
0
0
1
1
0 0 1 1 0 1 0 0 1 0
0 0 1 1 0 0 0 0 1 0
… 0
… 0
=1
= 1234
= 1218
Для установки значения разряда с номером n в переменной a в значение 1
используется выражение
a | (1U << n).
Иллюстрация вычисления этого выражения:
Номер разряда:
1U:
31 30 … 11 10 9 8 7 6 5 4 3 2 1 0
0 0 … 0 0 0 0 0 0 0 0 0 0 0 1
=1
1U << n:
Значение a:
0
0
= 1218
0
0
… 0
… 0
0
1
0 0 0 0 0 1 0 0 0 0
0 0 1 1 0 0 0 0 1 0
34
a | (1U << n):
0
0
…
0
1
0
0
1
1
0
1
0
0
1
0
= 1234
Для инвертирования значения разряда с номером n в переменной a используется
выражение
a ^ (1U << n).
Иллюстрация вычисления этого выражения при a = 1218:
Номер разряда:
1U:
31 30 … 11 10 9 8 7 6 5 4 3 2 1 0
0 0 … 0 0 0 0 0 0 0 0 0 0 0 1
=1
1U << n:
Значение a:
a ^ (1U << n):
0
0
0
= 1218
= 1234
0
0
0
… 0
… 0
… 0
0
1
1
0 0 0 0 0 1 0 0 0 0
0 0 1 1 0 0 0 0 1 0
0 0 1 1 0 1 0 0 1 0
Но, если a = 1234, то:
Номер разряда:
1U:
31 30 … 11 10 9 8 7 6 5 4 3 2 1 0
0 0 … 0 0 0 0 0 0 0 0 0 0 0 1
=1
1U << n:
Значение a:
a ^ (1U << n):
0
0
0
= 1234
= 1218
0
0
0
… 0
… 0
… 0
0
1
1
0 0 0 0 0 1 0 0 0 0
0 0 1 1 0 1 0 0 1 0
0 0 1 1 0 0 0 0 1 0
В C++ имеются и другие средства работы с отдельными битами, но они будут
рассмотрены позже.
Операции составного присваивания
Операции этой группы перечислены в следующей таблице:
Операция
Использование
Эквивалент
*=
a *= b
a=a*b
/=
a /= b
a=a/b
%=
a %= b
a=a%b
+=
a += b
a=a+b
-=
a -= b
a=a-b
<<=
a <<= b
a = a << b
>>=
a >>= b
a = a >> b
&=
a &= b
a=a&b
|=
a |= b
a=a|b
^=
a ^= b
a=a^b
Общий формат записи выражений с использованием этих операций:
35
< Переменная > < Операция >= < Выражение >
Эквивалентом этого формата в общем случае является:
< Переменная > = < Переменная > < Операция > < Выражение >
Таким образом, выражение с использованием составного присваивания (столбец
"Использование")
является
укороченной
формой
записи
соответствующего
эквивалентного выражения (столбец "Эквивалент").
Условная операция
Единственная в C++ тернарная операция. Формат записи:
< Условие > ? < Выражение 1 > : < Выражение 2 >
|
|
|
Операнд 1
Операнд 2
Операнд 3
"Условие" - любое выражение, результатом которого является число или логическое
значение. Значение "Условия" трактуется как логическое значение (0 - false; любое не
равное 0 значение - true). Если значение первого операнда true (не равное 0), то значение
этого условного выражения будет равно значению второго операнда (Выражение 1). Если
же значение первого операнда false (0), то значение этого условного выражения будет
равно значению третьего операнда (Выражение 2).
Например:
// Переменной max присваивается наибольшее из a и b значение
int max = a > b ? a : b;
Условные выражения можно вкладывать друг в друга. Например:
// Переменной max присваивается наибольшее из a, b и c значение
int max = a > b ? (a > c ? a : c) : (b > c ? b : c);
Часто условное выражение используется для вывода на экран одного из значений, в
зависимости от некоторого условия. Например:
cout << (a > b ? a : b) << endl; // Выводится наибольшее из a и b
Операция sizeof
Операция sizeof предназначена для определения объема памяти в байтах,
требующегося для размещения некоторого объекта или типа данных. Имеет две формы:
sizeof < Выражение >
или
sizeof (< Тип данных >)
Например:
long a = 0;
double d;
cout << sizeof (double) << endl;
cout << sizeof (5 / 3) << endl;
// Выведено 8
// Выведено 4
36
cout << sizeof a << endl;
cout << sizeof (a + 3.14f) << endl;
cout << sizeof (d = a + 3.14f) << endl;
// Выведено 4
// Выведено 4
// Выведено 8
Приоритеты рассмотренных операций
Результат вычисления выражений зависит от приоритета операций и от порядка
выполнения операций с одинаковым приоритетом.
Для правильной записи выражений очень важно знать, в каком порядке выполняются
операции внутри выражения. Например:
5+4*3/2+3
Если выполнить операции слева направо, то результат будет равен 16:
5+4 = 9
9 * 3 = 27
27 / 2 = 13
13 + 3 = 16
Но, поскольку, операции умножения и деления имеют одинаковый приоритет, и он
выше, чем у операции сложения, на самом деле результат будет равен 14:
4 * 3 = 12
12 / 6 = 6
5 + 6 = 11
11 + 3 = 14
В арифметических и логических выражениях операции с одинаковым приоритетом
выполняются слева направо. А вот выражения с операторами присваивания выполняются
справа налево.
В следующей таблице приведен перечень рассмотренных выше операций в порядке
уменьшения приоритетов. Операции, имеющий одинаковый приоритет, сгруппированы по
секциям.
Операция
Действие
++
Постфиксный оператор инкремента
--
Постфиксный оператор декремента
++
Префиксный оператор инкремента
--
Префиксный оператор декремента
!
Логическое «НЕ»
-
Унарный минус
+
Унарный плюс
тип
Явное преобразование типа
sizeof
Получение размерности операнда в байтах
*
Умножение
/
Деление
37
%
Остаток (деление по модулю)
+
Сложение
-
Вычитание
<<
Сдвиг влево
>>
Сдвиг вправо
<
Меньше
<=
Меньше или равно
>
Больше
>=
Больше или равно
==
Равно
!=
Не равно
&
Побитовое «И»
^
Побитовое исключающее «ИЛИ»
|
Побитовое «ИЛИ»
&&
Логическое «И»
||
Логическое «ИЛИ»
?:
=, +=, -=, *=, /=, %=, <<=, >>=,
&=, |=, ^=
Условная операция
Простое и составные присваивания
Изменить порядок выполнения операций внутри выражения можно с помощью
круглых скобок. В любом случае, когда возникают сомнения в определении приоритета
выполнения операций лучше использовать круглые скобки (на первых порах это особенно
рекомендуется).
3.7. Ввод и вывод простых типов данных
Ввод/вывод данных является неотъемлемой составляющей любой программы - без
ввода исходных данных для обработки и без вывода результатов не обходится ни одна
программа.
В этом разделе будут рассмотрены только некоторые аспекты организации
ввода/вывода в языке C++, относящиеся к консольному вводу/выводу простых типов
данных.
В языке C++ нет встроенных средств ввода/вывода — он осуществляется с помощью
функций и объектов, содержащихся в стандартных библиотеках.
В C++ можно использовать два различных способа реализации ввода-вывода.
Первый способ (унаследованный от языка C) основан на использовании ряда
библиотечных функций, наиболее употребимыми из которых являются функции printf и
scanf. Они выполняют форматированный ввод и вывод произвольного количества
величин в соответствии со строкой формата. Строка формата содержит символы, которые
при выводе копируются в поток (на экран) или запрашиваются из потока (с клавиатуры)
при вводе, и спецификации преобразования, начинающиеся со знака %, которые при вводе
и выводе заменяются конкретными величинами. Например:
#include <iostream>
38
using namespace std;
int main ()
{
setlocale (0, "");
int a;
printf ("Введите целое число:\t");
scanf ("%d", &a);
printf ("Вы ввели значение:\t%d\n\n", a);
system ("Pause");
return 0;
}
Второй способ, характерный для C++, основан на использовании стандартных
потоков ввода (cin) и вывода (cout). Та же самая программа в стиле C++ будет выглядеть
так:
#include <iostream>
using namespace std;
int main ()
{
setlocale (0, "");
int a;
cout << "Введите целое число:\t";
cin >> a;
cout << "Вы ввели значение:\t" << a << "\n\n";
// cout << "Вы ввели значение:\t" << a << endl << endl;
system ("Pause");
return 0;
}
В одной и той же программе совмещать эти два способа не рекомендуется.
В дальнейшем будем использовать именно второй способ организации ввода/вывода.
При использовании потоков для вывода данных на экран используется операция <<,
которая так и называется: операция вывода или операция вставки (данные
"вставляются" в поток вывода).
Ввод данных с клавиатуры осуществляется с помощью операции ввода >> (операция
извлечения данных из потока ввода).
Обе эти операции "знают" как осуществлять ввод и вывод стандартных простых типов
данных. Более того эти операции можно "научить", как осуществлять ввод/вывод
нестандартных пользовательских типов данных (перегрузка операций, которая будет
рассмотрена позднее).
А сейчас перейдем к изучению приемов ввода/вывода простых стандартных типов
данных.
Вывод текстовых строк
Текстовые (строковые) литералы в C++ представляются как последовательность
символов, заключенная в двойные кавычки. Например:
39
"Это пример текстовой строки".
Вывод текстовых строк на экран осуществляется через стандартный поток вывода с
помощью операции вывода <<:
cout << "Это пример текстовой строки";
Внутрь текстовых строк можно вставлять управляющие escape-последовательности.
Escape-последовательности служат для управления выводом, и представляют собой
специальные последовательности из двух или более символов, начинающиеся символом
обратной наклонной черты - \. При этом каждая такая последовательность воспринимается
компилятором как 1 символ. Примерами таких управляющих последовательностей в
предыдущих программах являются \t - символ табуляции и \n - символ перевода строки
(все эти последовательности приведены в разделе 3.5). С помощью Escapeпоследовательностей в текстовую строку можно включить любой символ с помощью его
восьмеричного или шестнадцатеричного кода (в том числе и символы, которых нет на
клавиатуре). Например:
cout << "Это символ с восьмеричным кодом 254:\t\254\n";
cout << "А это символ с шестнадцатеричным кодом xAA:\t\xAA\n";
На экран будут выведены две строки:
Это символ с восьмеричным кодом 254:
┐
А это символ с шестнадцатеричным кодом xAA:
Є
Если на экран необходимо вывести пустую строку, достаточно вставить в поток
дважды подряд управляющую последовательность \n:
cout << "Это первая строка\n";
cout << "\n";
cout << "Это третья строка\n";
// Вторая строка пустая
При выводе длинных текстовых строк их можно в тексте программы разбивать на
части следующим образом:
cout << "Это " \
"условный " \
"пример " \
"длинного " \
"текста\n";
или так
cout << "Это " "условный " "пример "
"длинного " "текста\n";
На экран будет выведена одна строка, после чего экранный курсор перейдет на новую
строку (управляющая последовательность \n):
Это условный пример длинного текста
40
Символ \ и символ пробела можно использовать для "сцепления" отдельных строк.
Если в программе встречаются два или более строковых литерала, разделенные только
пробелами, то они будут рассматриваться как одна символьная строка.
Ввод текстовых строк с клавиатуры будет рассмотрен позже.
Ввод/вывод арифметических типов данных
Пример простого ввода/вывода арифметических типов данных:
#include <iostream>
using namespace std;
int main ()
{
setlocale (0, "");
int i;
double d;
char c;
bool b;
cin >> i;
cout << i;
cin >> d;
cout << d;
cin >> c;
cout << c;
cin >> b;
cout << b;
system ("Pause");
return 0;
}
Особенности:
1. Ввод/вывод целочисленных значений осуществляется обычным образом в
десятичной системе счисления.
2. Ввод вещественных типов данных можно осуществлять либо в формате с
фиксированной точкой, либо в экспоненциальном формате.
3. Формат вывода вещественных значений выбирается автоматически в
зависимости от выводимого значения.
4. Ввод символьных значений можно осуществлять только в виде одиночного
символа. При вводе нескольких символов переменной c будет присвоен только
первый символ. Могут возникнуть сложности с вводом русских букв.
5. Ввод/вывод логических значений осуществляется в числовом формате (0 false, 1 - true).
Замечание:
При вводе числовых данных с клавиатуры могут возникать непредвиденные ошибки,
вызванные вводом символов, недопустимых для числовых форматов. Например:
41
int i;
cin >> i;
При попытке ввода с клавиатуры числа 1234 допущена ошибка - набрано 12y34 и
нажата клавиша ENTER (ошибочно была нажата клавиша y). Переменная i в этом случае
будет содержать значение 12, и эта ошибка может привести к непредсказуемому
дальнейшему поведению программы. В любом случае символы из потока ввода
извлекаются оператором >> до тех пор, пока они соответствуют числовому формату. Как
только в потоке ввода встречается символ, не соответствующий числовому формату, уже
извлеченные символы преобразуются в числовое значение и присваиваются переменной
ввода. Остальные символы игнорируются.
Форматирование ввода / вывода
В приведенных ранее примерах были использованы простейшие способы управления
вводом/выводом с помощью специальных управляющих символов (ESCAPE
последовательностей) - '\n' и '\t'. Однако, очень часто этого бывает недостаточно,
например, для аккуратного структурированного оформления данных на экране.
Более гибкое управление вводом/выводом (форматирование ввода/вывода) в C++
осуществляется либо с помощью установки флагов форматирования, либо с помощью
специальных манипуляторов ввода/вывода.
Использование флагов форматирования.
В этой таблице перечислены флаги форматирования.
Флаг
Числовое
значение
Назначение
Действие
ios:: skipws
1
Отменяет ввод из потока лидирующих
пробельных
символов
(пробелов,
символов табуляции, символов перевода
строки). Установлен по умолчанию.
ввод
ios:: unitbuf
2
Если буфер вывода не пуст, его
содержимое передаются на устройство
вывода сразу при завершении операции
вывода.
вывод
ios:: uppercase
4
Отображает шестнадцатеричные цифры
и символ экспоненты при выводе
вещественных значений в верхнем
регистре.
вывод
ios:: showbase
8
Отображает обозначение основания
системы
счисления,
в
которой
выводится
числовое
значение.
Например, если выводится значение
A1F, то оно будет выведено в виде
0xA1F.
вывод
42
ios:: showpoint
16
Отображает при выводе вещественных
значений десятичную точку и нули
дробной части, даже если дробная часть
отсутствует.
вывод
ios:: showpos
32
Приводит к отображению знака + при
выводе
положительных
числовых
значений.
вывод
ios:: left
64
Выравнивает вывод данных по левому
краю поля вывода, дополняя данные
справа пробелами (или установленными
символами) до ширины поля вывода.
вывод
ios:: right
128
Выравнивает вывод данных по правому
краю поля вывода, дополняя данные
слева пробелами (или установленными
символами) до ширины поля вывода
вывод
ios:: internal
256
Выводит
знак
числа
с
левым
выравниванием, а само число с правым
выравниванием. Между знаком и самим
числом выводятся либо пробелы, либо
установленные символы заполнения,
дополняя выводимое значение до
ширины поля вывода.
вывод
ios:: dec
512
Устанавливает
десятичную
форму ввод/вывод
представления целых чисел. Флаг
установлен по умолчанию.
ios:: oct
1024
Устанавливает восьмеричную
представления целых чисел.
ios:: hex
2048
Устанавливает
шестнадцатеричную ввод/вывод
форму представления целых чисел.
ios:: scientific
4096
Устанавливает
формат
вывода
вещественных значений в формате с
экспонентой. По умолчанию компилятор
сам выбирает формат вывода (либо
экспоненциальный,
либо
фиксированный).
вывод
ios:: fixed
8192
Устанавливает
формат
вывода
вещественных
значений
с
фиксированной точкой. По умолчанию
компилятор сам выбирает формат
вывода (либо экспоненциальный, либо
фиксированный).
вывод
форму ввод/вывод
43
ios:: boolalpha
16384
По умолчанию логические значения ввод/вывод
представляются в виде 0 и 1. При
установке этого флага эти значения
представляются словами false и true.
Замечания:
1. Приставка ios:: указывает на то, что определение флага принадлежит классу
ios, опускать ее не следует, иначе компилятор выдаст ошибку.
2. Некоторые компиляторы (старые) могут не воспринимать представления этих
флагов в виде идентификаторов. В этом случае необходимо использовать их
числовые эквиваленты.
3. По сути, каждый флаг можно рассматривать как целочисленную именованную
константу. Все эти константы определены в классе ios.
Флаги работают как переключатели (включен - выключен, установлен - сброшен). Все
флаги упакованы в одном целом значении типа long int. Это целое значение определяет
общее состояние всех флагов потока. Каждый флаг в этом целом значении представлен 1
битом (1 двоичным разрядом). Установленному флагу соответствует значение 1
некоторого двоичного разряда. Если флаг сброшен (отключен), соответствующий
двоичный разряд равен 0. Например, если значение состояния всех флагов равно 68, то
установлены флаги uppercase и left:
Номер разряда:
Состояния флагов:
31 30 … 8 7 6 5 4 3 2 1 0
0 0 … 0 0 1 0 0 0 1 0 0
= 68
Такое представление флагов позволяет, кроме компактной формы хранения,
обеспечить эффективное управление флагами на основе использования побитовых
логических операций.
Для управления флагами используются три функции: flags, setf и unsetf. Эти функции
являются членами потоковых классов cout и cin, поэтому обращаться к ним следует через
идентификаторы соответствующих потоковых классов: cout.flags, cout.setf, cout.unsetf
или cin.flags, cin.setf, cin.unsetf. Если необходимо управлять флагами потока вывода,
используется класс cout, если флагами потока ввода - класс cin. Все эти функции
возвращают предыдущее состояние флагов.
Функция flags может использоваться двумя способами. Если вызвать ее без
параметров, то она не изменяет состояние флагов, а только возвращает значение
состояния всех флагов потока. Например, строка
cout << cin.flags () << endl;
выведет на экран целое значение соответствующее состоянию флагов потока ввода. А
строка
cout << cout.flags () << endl;
выведет на экран целое значение соответствующее состоянию флагов потока вывода.
Эту функцию можно использовать и для изменения состояния флагов. Для этого при
ее вызове в качестве параметра ей необходимо передать необходимое значение флага
(флагов). Например, оператор cout.flags(ios::hex); установит флаг hex, все остальные
флаги будут сброшены.
Особенностью этой функции является то, что она сначала сбрасывает все флаги, а уже
потом устанавливает флаг (флаги), заданный параметром.
Замечание. Если вызвать функцию следующим образом: flags(0), то будут сброшены
все флаги соответствующего потока.
Функция setf также служит для установки флагов. Она добавляет новый флаг (флаги)
без изменения всех остальных.
44
/* 1 */
/* 2 */
/* 3 */
cout.flags(0);
// Отключены все флаги
cout.setf(ios :: showpos); // Установлен единственный флаг showpos
cout.setf(ios :: hex);
// Установлены два флага - showpos и hex
Если требуется сбросить флаг (флаги), используют функцию unsetf. В качестве
параметра используется значение флага (флагов), который необходимо отключить. При
этом остальные флаги изменены не будут. Например, если добавить к предыдущим
строкам программы строку:
/* 4 */
cout.unsetf(ios :: showpos);
флаг showpos будет отключен, и останется установленным только флаг hex.
При использовании этих процедур можно оперировать не одиночными флагами, а
объединением нескольких флагов. Например, строки 2 и 3 можно заменить одной
строкой:
cout.setf ( ios :: showpos | ios :: hex);
Объединение флагов осуществляется с помощью операции | - арифметическое ИЛИ.
Рассмотрим некоторые примеры использования флагов форматирования.
#include <iostream>
using namespace std;
int main ()
{
setlocale (0, "");
cout << 255 << endl;
// На экране видим 255 - по умолчанию установлен флаг dec
cout.setf (ios :: hex); // Включаем флаг hex - хотим видеть на экране ff
cout << 255 << endl; // На экране видим 255 - изменений не произошло
system ("Pause");
return 0;
}
В этом примере установка флага hex не привела ни к каким изменениям. Причиной
этого явилось то, что одновременно с установленным флагом hex остался установленным
и флаг dec. Для исправления ситуации необходимо сначала отключить флаг dec, а затем
уже установить флаг hex:
cout.unsetf (ios :: dec); // Отключаем флаг dec
cout.setf (ios :: hex);
// Включаем флаг hex - хотим видеть на экране ff
cout << 255 << endl;
// На экране видим ff - то, что хотели
Среди всех флагов можно выделить три группы, в каждой из которых флаги
управляют одной и той же характеристикой ввода / вывода, но являются
45
взаимоисключающими. Флагам каждой из этих групп в классе ios присвоены
обобщающие имена:
Группа
Флаги основания систем счисления
Флаги выравнивания
Флаги формата вещественных значений
Флаги
dec, oct, hex
left, right, internal
scientific, fixed
Обобщающее имя
basefield
adjustfield
floatfield
Обобщающие имена групп удобно использовать для выполнения операции со всеми
флагами группы, например, для сброса всех флагов группы:
cout.unsetf (ios :: basefield);
// Отключаем флаги dec, oct, hex за один прем
Более того, функцию setf можно использовать с двумя параметрами:
setf (новые флаги, маска)
В этом случае функция setf устанавливает только те флаги, которые одновременно
присутствуют и в первом и во втором параметре, а те флаги, которые присутствуют во
втором параметре, но отсутствуют в первом, будут сброшены. Это дает возможность
отключать и включать флаги за один вызов функции setf. Например, для установки
любого флага из группы basefield достаточно использовать всего один оператор:
cout.setf (ios :: hex, ios :: basefield);
Здесь второй параметр содержит три флага: dec, oct, hex. Первый параметр содержит
флаг hex. Этот флаг будет установлен, а флаги dec и oct будут сброшены.
В классе cout имеются еще несколько функций управляющих форматом вывода
данных. К ним относятся:
 precision - определяет точность представления вещественных значений;
 width - устанавливает ширину поля вывода;
 fill - определяет символ заполнения при выводе данных.
Примеры использования этих функций
Пример 1.
cout.width (10);
cout << 123 << endl;
cout.fill (‘.’);
cout.width (10);
cout << 123 << endl;
// Ширина поля вывода 10 позиций
// На экран выведено 7 пробелов и число 123
// Символ заполнения ‘.’
// Ширина поля вывода 10 позиций
// На экран выведено …….123
Пример 2
cout.width (10);
// Ширина поля вывода 10 позиций
cout.setf (ios :: fixed); // Вывод вещественных значений с фиксированной точкой
cout.precision (3);
// Ширина поля вывода 10 позиций
cout << 1.2345 << endl; // На экран выведено 7 пробелов и число 1.234
cout.fill (‘.’);
// Символ заполнения ‘.’
cout.width (10);
// Ширина поля вывода 10 позиций
cout << 123 << endl; // На экран выведено …….123
46
Форматирование ввода-вывода с помощью манипуляторов.
Управление флагами потоков ввода-вывода можно осуществлять с помощью, так
называемых, манипуляторов ввода-вывода. В следующей таблице перечислены
стандартные манипуляторы:
Манипулятор
Назначение
Действие
skipws
Устанавливает флаг skipws.
ввод
noskipws
Обнуляет флаг skipws.
ввод
unitbuf
Устанавливает флаг unitbuf.
вывод
nounitbuf
Обнуляет флаг unitbuf.
вывод
uppercase
Устанавливает флаг uppercase.
вывод
nouppercase
Обнуляет флаг uppercase.
вывод
showbase
Устанавливает флаг showbase.
вывод
noshowbase
Обнуляет флаг showbase.
вывод
showpoint
Устанавливает флаг showpoint.
вывод
noshowpoint
Обнуляет флаг showpoint.
вывод
showpos
Устанавливает флаг showpos.
вывод
noshowpos
Обнуляет флаг showpos.
вывод
left
Устанавливает флаг left.
вывод
right
Устанавливает флаг right
вывод
internal
Устанавливает флаг internal.
вывод
dec
Устанавливает флаг dec.
ввод/вывод
oct
Устанавливает флаг oct.
ввод/вывод
47
ввод/вывод
hex
Устанавливает флаг hex.
scientific
Устанавливает флаг scientific.
вывод
fixed
Устанавливает флаг fixed.
вывод
boolalpha
Устанавливает флаг boolalpha.
ввод/вывод
noboolalpha
Обнуляет флаг boolalpha.
ввод/вывод
endl
Выводит в поток символ перевода строки.
вывод
ends
Выводит в поток нулевой символ (‘\0’).
вывод
flush
«Сбрасывает» поток.
вывод
resetiosflags(флаги) Обнуляет флаги, указанные в параметре
ввод/вывод
setiosflags(флаги)
Устанавливает флаги, указанные в параметре
ввод/вывод
setbase(int base)
Устанавливает основание системы счисления в
значение base (допустимые значения параметра 8, 10,
16).
вывод
setfill(char ch)
Устанавливает символ для заполнения в значение ch.
вывод
setprecision(int p)
Устанавливает количество цифр после десятичной
точки в значение p.
вывод
setw(int w)
Устанавливает ширину поля вывода в значение w.
вывод
ws
Пропускает ведущие пробельные символы в потоке
ввода
ввод
Замечание. При использовании манипуляторов с параметрами необходимо
использовать заголовочный файл <iomanip>.
Манипуляторы ввода-вывода непосредственно включаются в потоки ввода-вывода.
Например:
cout << setw(20) << right << setfill(‘.’) << 123 << endl;
На экран будет выведено:
……………..123
Можно создавать свои собственные манипуляторы ввода-вывода.
48
4. Основные управляющие структуры программирования и
управляющие конструкции в языке С++
Структурное программирование. Основные управляющие структуры
программирования: последовательность, выбор (ветвление), итерации
(циклы). Примеры преобразования структур. Семантика управляющих
структур и инструкции языка С++. Операторы и блоки. Выбор вариантов:
оператор if, расширение оператора if, множественный выбор. Итерации:
цикл с предусловием, цикл с постусловием.
4.1. Идеи структурного программирования
При хаотическом программировании возникают очень серьезные отрицательные
последствия:
1) затруднено восприятие программы (алгоритма);
2) сложно проводить доказательство правильности программы, ее отладку,
тестирование;
3) практически невозможна модификация программы;
4) при отсутствии систематических приемов в программировании очень сложно
организовать коллективную работу над общей задачей.
Все это привело к созданию определенных методологий в программировании. И,
пожалуй, первым таким методом является структурное программирование. Его
основные положения:
1) программа должна составляться достаточно мелкими шагами, так чтобы
реализация каждого шага не вызывала никаких затруднений;
2) сложная задача должна разбиваться на достаточно простые, легко
воспринимаемые части, каждая из которых имеет только один вход и один
выход;
3) логика программы должна опираться на минимальное число достаточно
простых базовых управляющих структур.
Доказана теорема о структурировании (Бем и Джекопини):
Как бы ни была сложна задача, схема алгоритма соответствующей программы
всегда может быть представлена с использованием ограниченного набора базовых
структур.
Примером одного из таких наборов базовых структур являются следующие три
конструкции:
f THEN g - последовательность:
THEN
f
THEN
g
THEN
IF p THEN f ELSE g – выбор (ветвление):
49
THEN
ELSE
p
g
f
WHILE p DO f – итерации (цикл с предусловием):
p
ELSE
THEN
f
Эти базовые структуры могут соединяться между собой по тем же правилам, образуя
более сложные структуры. При этом f и g могут представлять собой очень сложные
схемы алгоритмов с одним входом и одним выходом.
Наборов базовых структур может быть несколько. Например, если заменить
последний элемент набора на
DO f WHILE p - итерации (цикл с постусловием):
f
p
THEN
ELSE
то получится еще один набор из трех базовых структур. Эти наборы эквивалентны, т.к. от
WHILE p DO f легко перейти к DO f WHILE p и наоборот:
50
DO f WHILE p
WHILE p DO f
IF p THEN (DO f WHILE p)
THEN
ELSE
p
DO f WHILE p
WHILE p DO f
f
p
THEN
ELSE
WHILE p DO f
DO f WHILE p
f THEN (WHILE p THEN f)
f
WHILE p DO f
DO f WHILE p
ELSE
p
THEN
f
Путем эквивалентных преобразований любую неструктурированную схему алгоритма
можно привести к структурированному виду. Например:
p
p
p1
f
p1
p2
g
h
f
p2
g
g
h
В некоторых случаях структуризация алгоритмов может привести к появлению в них
определенной избыточности (в последнем примере дважды осуществляется обращение к
51
g), но такие “накладные расходы” полностью оправдываются достоинствами
структурированных алгоритмов.
Для более эффективной разработки программ современные языки программирования
кроме минимального набора управляющих структур содержат и их модификации.
4.2. Управляющие структуры и инструкции языка C++
Управляющие структуры используются для управления ходом выполнения
программы. В языке C++ имеются три категории управляющих инструкций:
 инструкции выбора (ветвления):
o if - условная инструкция;
o switch – инструкция множественного выбора;
 итерационные (циклические) инструкции:
o while – цикл с предусловием;
o do while - цикл с постусловием;
o for – итерационный цикл;
 инструкции перехода:
o break – прекращение выполнения циклических инструкций и
инструкции switch;
o continue – переход к следующей итерации цикла;
o return – прекращение выполнения функции
o goto – переход по метке.
Условная инструкция (if)
Условная инструкция if позволяет выбрать одно из двух направлений выполнения
программы.
Имеются две формы записи этой инструкции:
if (<Выражение>)
<Инструкция 1>;
else
<Инструкция 2>;
true (не 0)
Инструкция 1
true (не 0)
if (<Выражение>)
<Инструкция>;
Выражение
false (0)
Инструкция 2
Выражение
false (0)
Инструкция
Если под термином <Инструкция> понимаются несколько последовательных
инструкций, то формат записи будет таким:
52
if (<Выражение>)
{
<Инструкция 1-1>;
……..
<Инструкция 1-N>;
}
else
{
<Инструкция 2-1>;
……..
<Инструкция 2-M>;
}
Блок инструкций
(составная инструкция)
Блок инструкций представляет собой последовательность инструкций, каждая из
которых заканчивается символом ;. Блок можно рассматривать как одну инструкцию
(составную инструкцию).
Термин <Выражение> представляет собой любое выражение C++, значение которого
может трактоваться как значение логического типа (bool).
Пример записи:
int K;
cin >> K;
if (K >= 0)
cout << “Вы ввели положительное число.” << endl;
else
cout << “Вы ввели отрицательное число.” << endl;
Здесь в качестве выражения использовано логическое выражение, значение которого
равно true или false в зависимости от введенного с клавиатуры значения переменной K.
Еще один пример:
int K;
cin >> K;
if (K)
// Здесь использовано арифметическое выражение
cout << “Вы ввели число не равное 0.” << endl;
else
cout << “Вы ввели 0.” << endl;
В этом примере выражение не является логическим, однако его значение может
трактоваться как логическое (помним, что любое числовое значение, отличное от 0,
соответствует значению true, а числовое значение 0 – логическому значению false). Этот
пример можно было бы переписать так (эквивалент предыдущего примера):
int K;
cin >> K;
if (K != 0) // Здесь использовано логическое выражение
cout << “Вы ввели число не равное 0.” << endl;
else
cout << “Вы ввели 0.” << endl;
53
Способ записи выражения во втором (из последних двух) примере следует считать
менее эффективным и с точки зрения написания текста, и с точки зрения использования
ресурсов (расхода памяти и быстродействия).
А вот пример с использованием блока инструкций:
int Max, Min, B;
cin >> Max >> Min;
if (Min > Max)
{
B = Max;
Max = Min;
Min = B;
}
В этом примере используется “укороченная” (без ветви else) форма инструкции if, и в
случае, когда переменная Min содержит значение большее, чем переменная Max,
выполняется последовательность инструкций (блок), осуществляющих перераспределение
значений этих переменных так, что переменная Max будет содержать большее значение, а
переменная Min - меньшее.
Выполняемые внутри оператора if инструкции могут быть любыми инструкциями
языка C++, в том числе и другими инструкциями if. То есть, другими словами, инструкции
if могут вкладываться друг в друга. Количество уровней вложения if – инструкций в языке
C++ ограничено 256 уровнями.
Рассмотрим несколько примеров вложений if - инструкций.
1
1
0
p
0
p1
e
1
f
p2
g
0
h
if (p)
if (p1)
e;
else
f;
else
if (p2)
g;
else
h;
При анализе текстов подобных программ используют следующее правило:
Слово else относится к ближайшему сверху слову if, находящемуся в том же
блоке инструкций, но еще не связанному ни с каким другим словом else.
1
1
e
p1
0
f
p
0
if (p)
if (p1)
e;
else
f;
54
1
Пустая инструкция
0
p
1
p1
if (p) ;
else
if (p1)
e;
else
f;
0
e
f
if ( !p )
if (p1)
e;
else
f;
или
Между словами if и else должна находиться хотя бы одна инструкция. Поэтому в
первой реализации последнего примера мы вынуждены были использовать так
называемую “пустую инструкцию”, которая не имеет никакого изображения и
располагается между записью выражения (p) и разделителем ;. Вторая реализация этой
схемы алгоритмы, основанная на инвертировании выражения p, является более
корректной и эффективной.
1
1
p1
p
0
if (p)
if (p1)
e;
else ;
else
g;
0
g
e
if (p)
{
if (p1)
e;
}
else
g;
или
Пустая инструкция
В первой реализации последнего примера мы также использовали “пустую
инструкцию”, так как после слова else (как и после слова if) также должна находиться хотя
бы одна инструкция или блок инструкций. Если в первой реализации не записать слово
else и пустую инструкцию вложенной инструкции if, а во второй реализации не оформить
эту вложенную инструкцию if в виде блока, то будет реализована схема совершенно
другого алгоритма:
1
if (p)
if (p1)
e;
else
g;
1
e
p1
p
0
0
g
В программах очень часто используется многоуровневое вложение if – инструкции
так называемой “лесенкой”, схема алгоритма которой выглядит так:
55
1
0
Р1
1
0
Р2
D1
1
0
РN
D2
DN
D
if (P1)
D1;
else
if (P2)
D2;
else
…
if (PN)
DN;
else
D;
Подобные схемы можно использовать для множественного выбора, однако для
реализации такой схемы более подходит инструкция, рассмотренная в следующем
параграфе.
Инструкция множественного выбора (switch)
Эта инструкция служит для ветвления программы во многих направлениях.
Ее синтаксис:
switch (<Выражение>)
{
case <Константа 1>:
<Последовательность инструкций 1>
break;
case <Константа 2>:
<Последовательность инструкций 2>
break;
……….
case <Константа N>:
<Последовательность инструкций N>
break;
default:
<Последовательность инструкций>
}
При совпадении значения выражения со значением одной из констант 1 – N будет
выполнена соответствующая этой ветви последовательность инструкций. Инструкция
break осуществляет прерывание выполнения инструкции switch и управление передается
следующему за switch-инструкцией оператору. Если значение выражения не совпадет ни с
одной из констант, то будут выполнены инструкции ветви default.
Ветвь default не обязательна. В случае отсутствия ветви default при несовпадении
значения выражения ни с одной из констант не будет выполнена ни одна из инструкций
оператора switch.
56
Значение выражения в инструкции switch обязательно должно быть либо целого, либо
символьного типа (в принципе тип выражения может быть и логическим, но в этом случае
выгоднее пользоваться if-инструкцией) – вещественные значения не допускаются.
Пример записи инструкции:
unsigned i;
cin >> i;
switch ( i )
{
case 0:
cout << "ноль\n";
break;
case 1:
cout << "один\n ";
break;
case 2:
cout << "два\n ";
break;
default:
cout << "много\n ";
}
Если в выбранной ветви будет отсутствовать инструкция break, то после выполнения
инструкций этой ветви начнут выполняться инструкции следующей ветви до тех пор, пока
не встретится инструкция break или не будет достигнут конец оператора switch.
Например:
unsigned i;
cin >> i;
switch ( i )
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
}
cout << 0;
cout << 1;
cout << 2;
cout << 3;
cout << 4;
cout << 5;
В этом примере на экран будет выведена последовательность цифр, начинающаяся с
цифры, введенной с клавиатуры.
Инструкция switch более эффективна, чем структура “лесенка”, реализованная с
помощью вложенных инструкций if.
Цикл с предусловием (while)
Формат записи этой инструкции:
57
while (<Выражение>)
{
<Инструкция1>;
<Инструкция2>;
……….
<ИнструкцияN>;
}
Блок инструкций тело цикла
Или, если тело цикла представляет собой одиночную инструкцию:
while (<Выражение>)
тело цикла
<Инструкция>;
И тому и другому варианту соответствует следующая схема алгоритма:
Выражение
false
true
Тело цикла
Выражение в этой инструкции может быть любого типа, значения которого можно
трактовать как значения логического типа данных (bool). Это выражение определяет
условие продолжения выполнения тела цикла, то есть, если значение этого выражения
истинно (true или не равно 0), то тело цикла выполняется вновь, если же ложно (false или
0) , то цикл заканчивается и управление передается следующей за циклом инструкции.
Очевидно, что тело цикла в этой инструкции может не выполниться ни разу, если при
входе в цикл значение выражения будет соответствовать значению false или 0.
Для того чтобы цикл начал выполняться, необходимо перед началом цикла выполнить
инициализацию его параметров так, чтобы значение выражения соответствовало
значению true или было не равно 0.
Неправильное использование этой инструкции может привести к образованию
бесконечного цикла (к зацикливанию программы). Такая ситуация возникает в том
случае, когда значение выражения не меняется в процессе выполнения цикла. Для того
чтобы избежать подобной ситуации, необходимо в теле цикла предусмотреть такие
изменения параметров цикла, при которых, в конце концов, условие продолжения цикла
перестанет выполняться, либо использовать принудительное завершение цикла с
помощью инструкции break.
Рассмотрим некоторые примеры.
Пример 1. Необходимо в виде строки вывести на экран цифры от 0 до 9.
int k = 0; // На экран выведено k цифр
while (k <= 9) // Здесь используется логическое выражение
{
cout << k;
++k;
}
// На экран выведено k = 10 цифр: 0123456789
58
Формулировка условия продолжения цикла в этом примере может быть и другой:
k < 10 или k != 9
Поскольку на каждом шаге цикла параметр цикла k увеличивает свое значение на 1
(начиная с 0), то после выполнения 10 шагов условие выполнения цикла (при любой
формулировке из перечисленных) обязательно перестанет выполняться и цикл закончится.
Но вот, если в теле цикла не предусмотреть наращивание параметра k, то получим
бесконечный цикл, в котором на экран будут выводиться одни нули:
int k = 0;
while (k <= 9)
{
cout << k;
}
Для остановки его нам придется принудительно прервать выполнение программы.
Причиной зацикливания может быть и неправильная формулировка условия
продолжения цикла.
Пример 2. Необходимо в виде строки вывести на экран только нечетные числа из
первого десятка.
int k = 1;
while (k != 10)
{
cout << k << “\t”;
k += 2;
}
В этом примере выражение k != 10 никогда не станет ложным, так как параметр цикла
k при его увеличении на каждом шаге цикла на 2 будет иметь только нечетные значения.
Правильной формулировкой условия является, например, такая: k < 10.
Пример 3. Для принудительного (досрочного) прекращения цикла можно
использовать инструкцию break. Например:
while (<Выражение>)
{
<Инструкция 1>;
if (<Ошибка>)
break;
<Инструкция 2>;
}
<Инструкция 3>;
Если при выполнении <Инструкции 1> возникает ошибка (о чем свидетельствует
значение true выражения <Ошибка>), после которой выполнение цикла должно быть
прекращено, выполняется инструкция break. При выполнении инструкции break цикл
прекращается (<Инструкция 2> выполнена не будет), и управление передается
<Инструкции 3>, следующей за оператором цикла.
Пример 4. Если в предыдущем примере при возникновении ошибки требуется только
пропустить выполнение <Инструкции 2), а затем продолжить выполнение цикла, следует
использовать инструкцию continue:
while (<Выражение>)
59
{
<Инструкция 1>;
if (<Ошибка>)
continue;
<Инструкция 2>;
}
При выполнении инструкции continue <Инструкция 2> выполнена не будет, но цикл
перейдет к выполнению следующей итерации (шага).
Инструкция continue на практике используется достаточно редко, так как обойтись
без нее очень просто:
while (<Выражение>)
{
<Инструкция 1>;
if (!<Ошибка>)
{
<Инструкция 2>;
}
}
Надо только не забыть инвертировать выражение <Ошибка>.
Цикл с постусловием (do while)
Формат записи этой инструкции:
do
{
<Инструкция1>;
<Инструкция2>;
……….
<ИнструкцияN>;
}
while (<Выражение>);
Блок инструкций тело цикла
Или, если тело цикла представляет собой одиночную инструкцию:
do
тело цикла
<Инструкция>;
while (<Выражение>);
;
И тому и другому варианту соответствует следующая схема алгоритма:
Тело цикла
true
Выражение
false
60
Так же, как и в предыдущем цикле, выражение в этой инструкции может быть любого
типа, значения которого можно трактовать как значения логического типа данных (bool).
Это выражение определяет условие продолжения выполнения тела цикла, то есть, если
значение этого выражения истинно (true или не равно 0), то тело цикла выполняется
вновь, если же ложно (false или 0), то цикл заканчивается и управление передается
следующей за циклом инструкции.
Принципиальным отличием этого цикла от предыдущего состоит в том, что тело
цикла в этой инструкции обязательно будет выполнено хотя бы один раз.
Использование этого цикла проиллюстрировано следующим примером:
Пример 1. Необходимо в виде строки вывести на экран цифры от 0 до 9.
int k = 0; // На экран выведено k цифр
do
{
cout << k;
++k;
}
while (k <= 9); // Здесь используется логическое выражение
// На экран выведено k = 10 цифр: 0123456789
Все остальное сказанное о предыдущем цикле, можно практически однозначно
применить и к циклу с постусловием.
Итерационный цикл (for)
Формат записи этой инструкции:
for (<Инициализация>; <Условие>; <Модификация>)
Заголовок
цикла
{
<Инструкция 1>;
<Инструкция 2>;
……….
<Инструкция N>;
Блок инструкций тело цикла
}
Или, если тело цикла представляет собой одиночную инструкцию:
for (<Инициализация>;
<Инструкция>;
<Условие>;
<Модификация>)
тело цикла
И тому и другому варианту соответствует следующая схема алгоритма:
61
Инициализация
Условие
Модификация
false
true
Тело цикла
При запуске цикла однократно выполняется Инициализация параметра (параметров)
цикла, после чего осуществляется проверка Условия, определяющего необходимость
выполнения тела цикла. После окончания выполнения инструкций тела цикла, на каждой
итерации выполняется Модификация параметра (параметров) цикла и снова проверяется
Условие. Так продолжается до тех пор, пока Условие не станет ложным (false).
Разделы Инициализации, Условия и Модификации в заголовке цикла разделяются
символом ‘;’.
Пример записи (пример из предыдущего параграфа):
int k;
for (k = 0; k <= 9; ++k)
cout << k;
Если параметр k цикла используется только внутри цикла (после выхода из цикла
переменная k больше не нужна), эту переменную можно (и лучше) определить
непосредственно в разделе Инициализации цикла:
for (int k = 0; k <= 9; ++k)
cout << k;
В разделах Инициализации и Модификации можно управлять сразу несколькими
параметрами цикла:
for (int k = 1, n = 10; k <= 10; ++k, --n)
cout << k << “ * ” << n << “ = ” << k * n << endl;
На экран будет выведено:
1 * 10 = 10
2 * 9 = 18
3 * 8 = 24
4 * 7 = 28
5 * 6 = 30
6 * 5 = 30
7 * 4 = 28
8 * 3 = 24
9 * 2 = 18
10 * 1 = 10
62
Отдельные элементы разделов Инициализации и Модификации отделяются друг от
друга символом ‘,’.
Любой раздел заголовка цикла может отсутствовать. Раздел Инициализации,
например, может отсутствовать, когда начальные значения параметров цикла
устанавливаются вне цикла, перед его началом. Модификация значений параметров
цикла может осуществляться внутри тела цикла, а не в его заголовке. При отсутствии
Условия продолжения выполнения цикла, цикл становится бесконечным и для выхода из
него придется использовать инструкцию break. Однако, какой бы из разделов ни
отсутствовал, соответствующие разделительные символы ‘;’ в заголовке цикла должны
обязательно присутствовать:
#include <conio.h>
……..
cout << “Для продолжения работы нажмите любую клавишу…” << endl;
for ( ; ! kbhit(); );
……..
В этом примере цикл, в заголовке которого отсутствуют разделы Инициализации и
Модификации, используется для приостановки выполнения программы до нажатия на
клавиатуре любой клавиши (функция kbhit() возвращает значение false, если на
клавиатуре не нажата никакая клавиша, и значение true, если клавиша была нажата – для
использования этой функции необходимо включить заголовочный файл conio.h).
Замечание. Приостановку работы программы значительно проще (без использования
циклов) можно выполнить с помощью функции getch(), которая ожидает нажатия
клавиши на клавиатуре и возвращает символ этой клавиши без отображения этого
символа на экране (необходим заголовочный файл conio.h):
#include <conio.h>
……..
cout << “Для продолжения работы нажмите любую клавишу…” << endl;
getch();
……..
Принудительный выход из цикла for осуществляется с помощью инструкции break, а
принудительный переход к следующей итерации (шагу цикла) – с помощью инструкции
continue.
Тела циклов могут содержать любые инструкции языка C++, в том числе и другие
циклы. Подобные конструкции называются вложенными циклами. Использование
вложенных циклов является весьма распространенным приемом программирования при
решении очень многих задач.
Инструкции перехода
Использование инструкций break и continue было рассмотрено при изучении
инструкции switch и циклических операторов. По поводу инструкции break следует
напомнить, что при вложенных циклах она обеспечивает прекращение того цикла, в теле
которого она непосредственно расположена.
Инструкция return, служащая для завершения выполнения функций и для
возвращения из функций некоторых значений, будет подробно рассмотрена позже, при
изучении функций.
В этом параграфе остается рассмотреть инструкцию безусловного перехода goto.
63
Характеризуя инструкции break (ее использование в циклах), continue и goto в целом,
следует сказать, что их применение противоречит принципам структурного
программирования и приводит к затруднению понимания алгоритмов программ, их
отладки и дальнейшей модификации. Однако, несмотря на это, их использование в ряде
случаев бывает оправдано. В принципе, как бы ни был сложен алгоритм программы, его
всегда можно реализовать без использования этих инструкций. В основном это
достигается за счет введения дополнительных логических переменных (флажков) и
некоторого усложнения условий продолжения циклов. Однако в некоторых случаях эти
“накладные расходы” оказываются чрезмерными и тогда выгоднее все-таки использовать
эти инструкции перехода. Как поступать в тех или иных ситуациях во многом зависит от
конкретного алгоритма и от внутренних предпочтений программиста. Но, все же,
злоупотреблять использованием этих инструкций не следует.
Инструкция goto обеспечивает переход на выполнение инструкции отмеченной с
помощью метки.
Формат записи: goto <Метка>;
Метка представляет собой некоторый идентификатор, за которым следует символ’:’.
Меткой может быть помечена любая инструкция, находящаяся в той же функции, в
которой находится оператор goto.
Пример использования:
…….
M1: <Инструкция>;
…….
if (<Условие>)
goto M1;
…….
Наиболее часто обоснованное использование инструкции goto связано с выходом из
глубоко вложенных циклов:
Вложенные
циклы
for ( …… )
for ( …… )
while ( …… )
do
{
…….
if ( …… )
goto Error;
}
while ( …… );
Error:
cout << “Все циклы прерваны” << endl;
Использование в этом случае инструкции break вместо оператора goto привело бы к
прерыванию только внутреннего цикла. Для прерывания выполнения всех циклов с
помощью инструкции break потребовались бы существенные усилия.
64
5. Приемы программирования циклов
Итерация как базисная вычислительная схема (рекуррентные
вычисления). Рекуррентные вычисления с целочисленными типами.
Рекуррентные вычисления с вещественными типами. Программирование
циклов в языке С++. Вложенные циклы. Циклы со сложным условием
продолжения (выхода). Пред- и постутверждения, инвариант цикла.
Примеры.
Циклические алгоритмы, соответствующие многократному повторению одного и того
же алгоритма и реализующиеся с помощью циклических инструкций, в том или ином виде
присутствуют в подавляющем большинстве практически значимых программ. С
алгоритмической точки зрения все циклы можно разделить на две группы: циклы с
заранее определенным числом повторений тела цикла и циклы, в которых количество
итераций (под итерацией понимается однократное выполнение тела цикла) заранее не
известно. Вторую разновидность циклов иногда называют итерационными циклами.
Существует много различных типовых схем вычислений, основанных на
использовании циклов. Одной из них является схема рекуррентных вычислений.
5.1. Рекуррентные вычисления
Рекуррентные схемы используются при вычислении значений некоторой
последовательности, в которой значение каждого очередного элемента определяется на
основе значений одного или нескольких предыдущих элементов.
В общем случае схему рекуррентных вычислений можно представить следующим
образом.
Пусть в некоторой последовательности известны первые n значений:
a0 , a1 , a2 ,...an1
Тогда элемент ai при i  n определяется так:
ai  f (ai 1 , ai  2 ,...ai  n )
Или, иными словами:
ai 
a0
при i = 0
a1
при i = 1
…
an 1
при i = n - 1
f (ai 1 , ai  2 ,...ai  n )
при i ≥ n
Простейшим примером рекуррентного соотношения является вычисление факториала
числа:
1 при i = 0
i * (i – 1)! при i > 1
Программная реализация вычисления факториала:
unsigned Factorial (unsigned n)
{
65
unsigned i = 0;
unsigned F = 1;
while (i < n)
{
++ i;
F *= i;
}
return F;
// Текущее значение i
// Текущее значение i!
// i = i + 1
// F = F * i
- текущее значение i!
// Возвращаем значение n!
}
Недостатком этой реализации является то, что с помощью этой функции можно
вычислить n! только для n от 0 до 12. Значение 13! уже выходит за верхнюю границу
диапазона значений типа unsigned и функция начинает возвращать неверные значения
факториала. Для предотвращения
получения неверных значений факториалов
модифицируем функцию следующим образом:
unsigned Factorial (unsigned n)
{
unsigned i = 0; // Текущее значение i
unsigned F = 1; // Текущее значение i!
while (i < n)
{
++ i;
// i = i + 1
if (0xffffffffu / i < F ) // 0xffffffffu – максимальное значение типа unsigned
{
F = 0;
break;
}
F *= i;
// F = F * i - текущее значение i!
}
return F;
// Возвращаем значение n!
}
Добавленная проверка обнаруживает ситуацию, когда умножение предыдущего
значения факториала на следующее значение i приведет к выходу полученного значения
произведения за верхнюю границу диапазона значений типа unsigned. В этом случае
значению факториала присваивается значение 0 (факториал любого числа всегда больше
0), и с помощью инструкции break выполнение цикла прерывается. В этом случае
функция вернет значение 0.
При такой реализации функцию Factorial в программе можно использовать,
например, так:
unsigned F, n;
…..
if (F = Factorial (n))
….. // Используем вычисленное значение факториала F
else
cout << “Ошибка. Факториал числа ” << n << “ не может быть вычислен! \n”;
Важно. При рекуррентном накоплении сумм, произведений (степеней, факториалов)
целых типов следует очень внимательно контролировать возможность выхода за
66
границы диапазона значений используемого целого типа данных. При возникновении
таких ситуаций поведение программы будет непредсказуемым.
Еще один пример. Требуется подсчитать сумму первых n элементов следующего
степенного ряда:
ex  1  x 
n
n
x2
x3
xi
xn
xi

 ... 


  ai
2!
3!
i!
n!
i 0 i!
i 0
Если подойти к решению этой задачи “в лоб”, то получится следующая весьма
неэффективная программа:
int n = 20;
// Количество суммируемых элементов ряда
double x = 2.5; // Значение аргумента x
double S = 0; // Начальное значение суммы ряда
int i = 0;
// Начальное значение индекса элемента ряда
while (i < n)
{
S = S + pow(x, i) / Factorial (i);
++ i;
}
cout << “Сумма первых ” << i << “ элементов ряда равна ” << S << endl;
Неэффективность этого варианта реализации объясняется двумя причинами. Вопервых, поскольку функция вычисления факториала представляет собой цикл, общее
количество операций будет быстро расти с увеличением n. Во-вторых, нам вообще не
удастся подсчитать сумму более чем 13 элементов этого ряда, так как при i = 13, функция
вычисления факториала возвратит значение 0, и наша программа аварийно завершится с
ошибкой “Деление на 0”. Избежать аварийного завершения программы можно, как это
было описано выше – путем проверки значения факториала на 0 и прерывания цикла,
однако более 13 элементов ряда все равно просуммировать не удастся.
Решить эту проблему поможет еще одно рекуррентное соотношение, связывающее
очередное значение элемента ряда с его предыдущим значением.
Несложно заметить, что
ai
x
x
 откуда следует, что a i  a i 1 при a 0  1 .
i
ai 1 i
Тогда можно предложить следующий вариант реализации:
int n = 20;
// Количество суммируемых элементов ряда
double x = 2.5; // Значение аргумента x
double a = 1; // Значение первого элемента ряда
double S = 1; // Начальное значение суммы ряда при i = 0
int i = 1;
// Начальное значение индекса элемента ряда
while (i < n)
{
a=a*x/i;
S = S + a;
++ i;
}
cout << “Сумма первых ” << i << “ элементов ряда равна ” << S << endl;
В этой реализации недостатки предыдущего варианта программы отсутствуют.
Удалось избавиться и от вычисления факториала, и от возведения в степень аргумента x.
Теперь количество операций на каждой итерации постоянно и равно 4. Программа
позволяет находить сумму практически любого количества элементов ряда.
67
Более сложный вариант рекуррентного соотношения. Требуется написать функцию
для получения значения многочлена Чебышева первого рода степени n >= 0 , задающегося
следующим рекуррентным соотношением:
Tn (x) 
1
x
при n = 0
при n = 1
2 xTn 1 ( x)  Tn  2 ( x) при n ≥ 2
Реализация:
double Cheb_1 (double x, int n)
{
double Tn, Tn_1 = x, Tn_2 = 1;
switch (n)
{
case 0: return Tn_2;
case 1: return Tn_1;
default:
int i = 2;
while (i < = n)
{
Tn = 2 * x * Tn_1 - Tn_2;
Tn_2 = Tn_1;
Tn_1 = Tn;
++ i;
}
return Tn;
}
}
При выполнении рекуррентных вычислений с вещественными значениями
беспокоиться о переполнении значений вещественного диапазона приходится очень редко
(особенно при использовании типа double). Но и здесь встречаются некоторые
“подводные камни”.
Предостережение № 1. При определении условия продолжения цикла никогда не
используйте проверку на точное равенство вещественных значений с помощью операции
== (впрочем, и в других условиях тоже). Например:
double a = 1.1, b = 0;
while ( ! (b == a) )
{
b = b + 0.1;
}
Такой цикл никогда не закончится, так как из-за погрешностей вычислений и
представления вещественных значений точного равенства a и b при выбранных значениях
никогда не будет (так, по крайней мере, происходит для этого примера в MS Visual C++
2010). Лучше сделать так:
double a = 1.11, b = 0;
while ( b <= a )
68
{
b = b + 0.1;
}
Здесь цикл закончится гарантированно, и последнее значение b будет очень близко к
1.1.
Предостережение № 2. Остерегайтесь складывать (вычитать) очень большие и
относительно малые вещественные значения. Например:
double a = 1e20, b = a, d = 1000;
int i = 1;
for ( int i = 1; i <= 1000000; ++ i)
{
b = b + d;
}
cout << b – a << endl;
Ожидается, что значение b после окончания цикла будет больше a на 1 000 000 000,
однако разность между b и a оказывается равной 0. Это происходит, потому что величина
d = 1 000 по отношению к значению a = 1e20 оказывается пренебрежимо малой из-за
дискретности представления вещественных значений.
Если увеличить значение d и сделать его равным 10 000, то разность b – a окажется
равной приблизительно 1.64e10, а не 1e10 как это следует из арифметики – достаточно
грубая ошибка.
Для того, чтобы избавиться от этой неприятности, можно поступить так:
double a = 1e20, b = a, d = 1000, s = 0;
int i = 1;
for ( int i = 1; i <= 1000000; ++ i)
{
s = s + d;
}
b = b + s;
cout << b – a << endl;
Вот теперь мы увидим то, что ожидалось (или очень близкое к тому) и в первом и во
втором случаях. Здесь мы сначала накопили отдельно сумму s относительно малых
величин d, а затем уже относительно большое значение s добавили к b.
5.2. Инвариант цикла
Инвариантом называется логическое выражение, истинное перед началом выполнения
цикла и после каждого прохода тела цикла, зависящее от переменных, изменяющихся в
теле цикла.
Инварианты используются для доказательства правильности выполнения цикла, а
также при проектировании и оптимизации циклических алгоритмов.
Порядок доказательства работоспособности цикла с помощью инварианта сводится к
следующему:
1. Доказывается, что выражение инварианта истинно перед началом цикла сразу
после инициализации параметров цикла.
69
2. Доказывается, что выражение инварианта сохраняет свою истинность до и после
выполнения тела цикла; таким образом, по индукции, доказывается, что по
завершении цикла инвариант будет выполняться.
3. Доказывается, что при истинности инварианта после завершения цикла
переменные примут именно те значения, которые требуется получить (это
элементарно определяется из выражения инварианта и известных конечных
значениях переменных, на которых основывается условие завершения цикла).
4. Доказывается, что цикл завершится, то есть условие завершения рано или поздно
будет выполнено.
5. Истинность утверждений, доказанных на предыдущих этапах, однозначно
свидетельствует о том, что цикл выполнится за конечное время и даст желаемый
результат.
Инварианты используют не только для доказательства корректности циклов, но и при
проектировании и оптимизации циклических алгоритмов.
Рассмотрим использование инварианта на примере реализации алгоритма Евклида для
нахождения наибольшего общего делителя двух чисел.
Постановка задачи. Требуется найти наибольший общий делитель d двух целых чисел
n и m: d = НОД(n, m).
Сформулируем инвариант цикла для нахождения НОД(n, m) следующим образом:
пусть имеется пара чисел a и b таких, что НОД(a, b) = НОД(n, m). На каждом шаге цикла
будем переходить к другой паре чисел a и b таких, что НОД(a, b) = НОД(n, m). И так
будем продолжать до тех пор, пока значение НОД не станет очевидным. Таким образом,
инвариант цикла сформулируем так: НОД(a, b) = НОД(n, m). Теперь стоит задача: как
найти очередную пару чисел a и b, при которых значение инварианта не изменится.
Из математики (теория чисел) известно, что если d = НОД(n, m), то это же значение
d является и НОД(m, r), где r – остаток от деления n на m, то есть НОД(n, m) = НОД(m,
r).
Например: НОД(126, 12) = НОД(12, 6) = НОД(6, 0) = 6
Алгоритм решения задачи можно представить так:
1. Начальная инициализация: пусть a = n, b = m. Очевидно, что НОД(a, b) = НОД(n,
m).
2. Находим r и делаем a = b и b = r. При этом выражение НОД(a, b) = НОД(n, m)
остается справедливым.
3. Как только b станет равно 0, тогда НОД(a, 0) = НОД(n, m) = a.
Программа, реализующая этот алгоритм:
int r, a = n, b = m;
// Инвариант: НОД(a, b) = НОД(n, m)
// Цикл заканчивается при b = 0, тогда НОД(a, 0) = a
while (b)
{
// Инвариант: НОД(a, b) = НОД(n, m) выполняется
r = a % b;
a = b;
b = r;
// Инвариант: НОД(a, b) = НОД(n, m) выполняется
}
// Инвариант: НОД(a, b) = НОД(n, m) выполняется
cout << “НОД (“ << n << “, ” << m << “) = ” << a << endl;
70
Можно предложить еще один алгоритм решения этой задачи, основанный на том же
инварианте, но использующий другой способ нахождения следующей пары a и b:
известно, что НОД(n, m) = НОД(n - m, m) при n > m и НОД(n, m) = НОД(n, m - n) при m
> n. Например: НОД(126, 12) = НОД(114, 12) = НОД(102, 12) = … = НОД(18, 12) =
НОД(6, 12) = НОД(6, 6) = 6. Но этот алгоритм является более затратным по сравнению с
предыдущим.
Еще одним наглядным примером использования инварианта для проектирования
цикла является реализация быстрого возведения чисел в целую положительную степень.
Пример его реализации приведен в Приложении. Предлагается разобрать его
самостоятельно.
В Приложении приведены программы, реализующие различные варианты
итерационных вычислений, основанных на использовании рекуррентных соотношений и
инвариантов.
Очень часто используются, так называемые, вложенные циклы. Примеры
использования таких конструкций будут рассмотрены при изучении массивов.
6. Массивы и указатели
Массивы. Индексирование. Объявление массивов. Двумерные и
многомерные массивы. Ввод-вывод массивов. Строки и тексты как
массивы символов.
Массивы и указатели. Арифметика указателей.
Правила сложных объявлений. Использование функций при работе с
массивами. Управление памятью.
6.1. Понятие массива
Массив представляет собой индексированную последовательность однотипных
элементов с заранее определенным количеством элементов. Все массивы можно разделить
на две группы: одномерные и многомерные.
Аналогом одномерного массива из математики может служить последовательность
некоторых элементов с одним индексом: a i при i = 0, 1, 2, … n – одномерный вектор.
Каждый элемент такой последовательности представляет собой некоторое значение
определенного типа данных. Наглядно одномерный массив можно представить как набор
пронумерованных ячеек, в каждой из которых содержится определенное значение:
0
3.02
1
1.5
2
7.0
3
-2.3
5
12.0
Это пример одномерного массива из 6 элементов, каждый из которых представляет
собой некоторое вещественное значение и каждое из этих значений имеет индекс от 0 до
5.
А вот пример одномерного массива из десяти элементов, представляющих собой
одиночные символы:
0
‘a’
1
‘b’
2
‘c’
3
‘+’
4
‘1’
5
‘2’
6
‘!’
7
‘#’
8
‘@’
9
‘&’
Каждый элемент в этих массивах определяется значением индекса элемента.
Например, в последнем массиве элемент с индексом 5 равен символу ‘2’.
71
Двумерный массив – это последовательность некоторых элементов с двумя
индексами: a ij при i = 0, 1, 2, … n и j = 0, 1, 2, … m – двумерная матрица. Например,
при n = 3 и m = 4:
j
0
1
2
3
0
1001
25
167
33
i
1
0
41
226
52
2
4
15
54
45
Эта матрица из 3-х строк и 4-х столбцов содержит 3 * 4 = 12 целых значений. Здесь
уже каждый элемент определяется значениями двух индексов. Например, элемент с
индексом i = 2 и индексом j = 1 равен целому значению 15.
Количество мерностей массивов может быть и больше двух, но при мерности
большей 3 наглядно представить такой массив достаточно сложно.
6.2. Объявление массивов
Объявление одномерных массивов
Объявление в программах одномерных массивов выполняется в соответствии со
следующим правилом:
<Базовый тип элементов> <Идентификатор массива> [<Количество элементов>]
Например:
int ArrInt [10], A1 [20];
double D [100];
char Chars [50];
bool B [200];
Значения индексов элементов массивов всегда начинается с 0. Поэтому
максимальное значение индекса элемента в массиве всегда на единицу меньше количества
элементов в массиве.
Обращение к определенному элементу массива осуществляется с помощью указания
значения индекса этого элемента:
A1 [8] = -2000;
cout << A1[8]; // На экран выведено -2000
В этом примере, обратившись к элементу массива A1 с индексом 8, мы, фактически,
обратились к его 9-му элементу.
При обращении к конкретному элементу массива этот элемент можно рассматривать
как обычную переменную, тип которой соответствует базовому типу элементов массива, и
осуществлять со значением этого элемента любые операции, которые характерны для
базового типа. Например, поскольку базовым типом массива A1 является тип данных int,
с любым элементом этого массива можно выполнять любые операции, которые можно
выполнять над значениями типа int.
При объявлении массива его можно инициализировать определенными значениями:
short S[5] = {1, 4, 9, 16, 25}
72
Эта инициализация будет эквивалентна следующим операциям присваивания:
S[0] = 1;
S[1] = 4;
S[2] = 9;
S[3] = 16;
S[4] = 25;
Количество значений, указанных в фигурных скобках (инициализирующих значений)
не должно превышать количества элементов в массиве (в нашем примере - 5).
Значения всех элементов массива в памяти располагаются в непрерывной области
одно за другим. Общий объем памяти, выделяемый компилятором для массива,
определяется как произведение объема одного элемента массива на количество элементов
в массиве и равно:
sizeof( <Базовый тип> ) * <Количество элементов>
Для предыдущего примера объем массива S будет равен sizeof( short) * 5 = 2 * 5 = 10
байтам.
Поскольку все элементы массивов располагаются в памяти один за другим без
разрывов, обращение к элементам массива по их индексам (какой бы длины не был этот
массив) осуществляется очень эффективно путем вычисления адреса нужного элемента.
Пусть, например, адрес памяти, где начинается массив S, равен 100, тогда адрес элемента
этого массива с индексом 3 будет равен 100 + sizeof( short) * 3 = 100 + 2 * 3 = 106.
Обращаемся по этому адресу и считываем 2 байта. Это и будет значением элемента с
индексом 3 массива S.
В языке C++ не осуществляется проверка выхода за границы массивов. То есть,
вполне корректно (с точки зрения компилятора) будет обращение к элементу массива S,
индекс которого равен 10. Это может привести к возникновению весьма серьезных
отрицательных последствий. Например, если выполнить присвоение S[10] = 1000 будут
изменены данные, находящиеся за пределами массива, а это может быть значение какойнибудь другой переменной программы. После этого предсказать поведение программы
будет невозможно. Единственный выход – быть предельно внимательным при работе с
индексами элементов массивов.
Объявление многомерных массивов
Многомерные массивы определяются аналогично одномерным массивам. Количество
элементов по каждому измерению указывается отдельно в квадратных скобках:
int A1 [5] [3];
//
//
double D [10] [15] [3]; //
//
Двумерный массив, элементами которого являются
значения типа int
Трехмерный массив, элементами которого являются
значения типа double
Здесь массив A1 представляет собой обычную двумерную матрицу из 5-ти строк и 3–х
столбцов.
Массив D – трехмерный массив, который можно представить как трехмерный
параллелограмм, навранный из 3-х двумерных матриц.
Общее число элементов в многомерном массиве определяется как произведение
количества элементов по каждому измерению. Так, например, массив D содержит 10 * 15
73
* 3 = 450 элементов типа double, а объем памяти, требующийся для этого массива, будет
равен 450 * 4 = 1800 байтам.
Массивы с большим, чем 3, количеством измерений используются достаточно редко.
Одной из причин этого является быстрый рост объема памяти, необходимой для
размещения таких массивов.
В следующей таблице показана схема размещения элементов массива A1 в памяти:
i
j
A1[i][j]
0
1
0
1
1
2
1
0
2
1
1
4
2
8
0
3
2
1
9
2
27
0
4
3
1
16
2
64
0
5
4
1
25
2
125
Так же как и в одномерном массиве, элементы многомерных массивов располагаются
друг за другом в непрерывном участке памяти.
При определении многомерные массивы могут инициализироваться определенными
значениями. Для получения массива A1 с теми значениями элементов, которые приведены
в таблице, можно инициализировать массив следующим образом:
int A1 [5] [3] =
{
1, 1, 1,
2, 4, 8,
3, 9, 27,
4, 16, 64,
5, 25, 125
};
Для доступа к определенному элементу многомерного массива необходимо указать в
квадратных скобках конкретные значения всех индексов этого элемента. Например:
cout << A1 [1] [2];
// На экран выведено значение 8
6.3. Ввод-вывод массивов
Ранее были рассмотрены приемы ввода-вывода простых предопределенных типов
данных (int, double, char и bool) с помощью потоков ввода и вывода. Стандартные потоки
ввода и вывода не “умеют” работать с массивами, поэтому ввод и вывод массивов
необходимо реализовывать самостоятельно, обрабатывая массивы поэлементно.
Большинство алгоритмов по обработке массивов реализуются с помощью циклов.
Ввод и вывод массивов не являются исключением.
Начнем с рассмотрения операций вывода значений элементов массивов на экран.
Простейший циклический алгоритм вывода значений элементов некоторого
одномерного массива выглядит так:
const int n = 10;
short A[n];
…
// Для использования setw() необходимо включить #include <iomanip>
for (int i = 0; i < n; ++i)
cout << setw(8) << left << A[i];
cout << endl;
На каждом шаге этого цикла в поток вывода отправляется очередной i-й элемент
массива, при этом
устанавливается ширина поля вывода, равная 8 позициям,
74
выравнивание по левому краю. После окончания цикла вывода всех n элементов массива
осуществляется переход на следующую строку экрана.
Обратим внимание на то, что в программах выгоднее задавать размеры массивов
через именованные константы (в данном примере – константа n), для того чтобы
использовать эти же константы для управления работой циклов. При необходимости
изменить размеры массива достаточно будет поменять значение этой константы. При этом
все циклы, использующие для управления своей работой эту константу, автоматически
приспособятся к изменившимся размерам обрабатываемого массива.
Вывод двумерных массивов, как правило, осуществляется в табличной форме.
Реализация такого алгоритма может быть, например, такой:
const int n = 10, m = 10;
short A [n] [m];
…
for (int i = 0; i < n; ++i)
// Выводим i-ю строку массива
{
for (int j = 0; j < m; ++j)
// Выводим j-й элемент i-й строки массива
cout << setw(7) << right << A [i] [j];
cout << endl;
}
Здесь используются вложенные циклы. Обратите внимание, что внутренний
(вложенный) цикл практически идентичен циклу, реализующему вывод элементов
одномерного массива.
6.4. Текстовые строки как массивы символов
6.5. Массивы и указатели
7. Разработка программ при работе с массивами
Картинки массивов при записи предусловий, постусловий и
инвариантов. Примеры: задачи разделения и слияния массивов,
перестановка сегментов массива (циклический сдвиг) и т.п. Линейный и
бинарный поиск в массиве. Оптимальность алгоритмов поиска.
Оптимизация программ. Простые алгоритмы сортировки (выбором,
вставками, обменами). Работа с двумерными и многомерными массивами.
8. Функции и структура программы
75
Создание и использование функций. Вызов функции (аргументы
функции) и возврат значения. Передача параметров по значению, по ссылке.
Глобальные и локальные переменные. Классы памяти и область действия.
Автоматические переменные. Внешние переменные. Статические
переменные. Внешние статические переменные. Регистровые переменные.
Указатели.
Функции
с
переменным
количеством
аргументов.
Представление программы в виде набора функций. Многофайловая
структура программы. Использование функции как параметра другой
функции; пример применения - итерационные методы решения нелинейных
уравнений.
9. Организация ввода/вывода и работа с файлами
Последовательность (как модель файла) и файл. Потоки и работа с
файлами. Базовые операции с файлами. Типовые действия с файлами:
генерация, чтение, копирование. Форматирование ввода и вывода. Схема
однопроходных алгоритмов обработки файлов (вычисление функций на
последовательностях). Примеры.
Заключение
Основные тенденции и направления развития методов и языков
программирования. Связь с учебной дисциплиной по программированию
(дополнительные главы) следующего семестра.
76
Приложение. Некоторые полезные примеры и
иллюстрации к разделам конспекта
Все программы, приведенные в этом разделе, реализованы в среде MS Visual C++
2010.
Примеры к разделу 5
Вычисление факториала числа
// Факториал.cpp: определяет точку входа для консольного приложения.
// Различные реализации функций для вычисления факториала числа
#include
#include
#include
#include
"stdafx.h"
<iostream>
<iomanip> // для манипулятора setw()
<limits.h> // для ULONG_MAX - максимальное значение типа unsigned long
using namespace std;
unsigned Factorial_Err(unsigned n)
// При n > 12 значение n! превышает максимальное значение ULONG_MAX типа unsigned
// и функция возвращает неправильные значения
{
unsigned i = 0; // Текущее значение i
unsigned F = 1; // Текущее значение i!
while (i < n)
{
++ i;
// i = i + 1
F *= i; // F = F * i
- Текущее значение i!
}
return F;
// Возвращаем значение n!
}
unsigned Factorial(unsigned n)
// При переполнении возвращает 0 с сообщением об ошибке
// Реализация с помощью цикла while
{
unsigned i = 0; // Текущее значение i
unsigned F = 1; // Текущее значение i!
while (i < n)
{
++ i;
// i = i + 1
if (ULONG_MAX / i < F)
{
F = 0;
cout << "Ошибка. При вычислении n! максимальное "
"значение n не может превышать " << --i << endl;
break;
}
F *= i; // F = F * i
- Текущее значение i!
}
return F;
// Возвращаем значение n!
}
unsigned Factorial_1(unsigned n)
// При переполнении возвращает 0 с сообщением об ошибке
// Реализация с помощью цикла for
{
77
unsigned F = 1; // Значение 0!
for (unsigned i = 1; i < n; ++i, F *= i)
if (ULONG_MAX / i < F)
{
F = 0;
cout << "Ошибка. При вычислении n! максимальное "
"значение n не может превышать " << --i << endl;
break;
}
return F;
// Возвращаем значение n!
}
unsigned Factorial_2(unsigned n)
// При переполнении возвращает 0 без сообщения об ошибке
// Реализация с помощью цикла for
{
unsigned F = 1; // Значение 0!
for (unsigned i = 1; (i < n) && F; ++i, F = (ULONG_MAX / i < F) ? 0 : F * i);
return F;
// Возвращаем значение n!
}
int main()
// Для проверки работы одного из вариантов необходимо
// снять комментарии с соответствующей строки цикла for
// и закомментировать остальные
{
for (int i = 0; i <= 13; ++ i)
{
cout << setw(2) << right << i << "! = " <<
//
cout << setw(2) << right << i << "! = " <<
//
cout << setw(2) << right << i << "! = " <<
//
cout << setw(2) << right << i << "! = " <<
}
system ("Pause");
return 0;
}
Factorial_Err(i) << endl;
Factorial(i) << endl;
Factorial_1(i) << endl;
Factorial_2(i) << endl;
Быстрое возведение чисел в целую степень
// ЦелаяСтепень.cpp: определяет точку входа для консольного приложения.
//
#include "stdafx.h"
#include <iostream>
#include <conio.h>
using namespace std;
double IntPow(double b, int k, int &Count)
{
// Инвариант: (b ^ k) * p = a ^ n
// Цикл заканчивается при k = 0, тогда p = a ^ n
double p = 1;
Count = 0;
while (k != 0)
{
if (k & 1) // k не четно
{
-- k;
// k = k - 1
p *= b; // p = p * b
}
else
{
78
k /= 2; // k = k / 2
b *= b; // b = b * b
}
++ Count;
}
return p;
}
double IntPow1(double a, int n, int &Count)
{
double p = 1;
double b = a;
for (int i = n, Count = 0; i; (i % 2) ? (p *= b, --i) : (b *= b, i /= 2),
++ Count);
return p;
}
int _tmain(int argc, _TCHAR* argv[])
{
setlocale(0, "");
cout << "
Алгоритм быстрого возведения числа в целую степень.\n";
cout << "
---------------------------------------------------\n";
for (char b = '1'; b != 27; cout << "\n\t\t\tПродолжим? (нет - Esc) ",
b = _getch(), cout << endl)
{
double a;
int n, N;
cout << "\nОснование степени: ";
cin >> a;
cout << "Целая степень: ";
cin >> n;
cout << '\n' << a << " в степени " << n << " равно " << fixed
<< IntPow(a, n, N) << ".\n";
cout << "Количество шагов: " << N << endl;
cout << '\n' << a << " в степени " << n << " равно " << fixed
<< IntPow1(a, n, N) << ".\n";
cout << "Количество шагов: " << N << endl;
}
return 0;
}
Нахождение наибольшего общего делителя (алгоритм Евклида)
#include "stdafx.h"
#include <iostream>
#include <conio.h>
using namespace std;
int NOD_1(int n, int m, int &Count)
// Известно, что: НОД(n, m) = НОД(n - m, m) при n > m и
// НОД(n, m) = НОД(n, m - n) при n < m.
{
int r, a = n, b = m;
Count = 0;
// Инвариант: НОД(a, b) = НОД(n, m)
// Цикл заканчивается при a = b, тогда НОД(n, m) = НОД(a, a) = a
while (a != b)
{
if (a > b)
a = a - b;
else
b = b - a;
79
++ Count;
}
return a;
}
int NOD_2(int n, int m, int &Count)
// Известно, что: НОД(n, m) = НОД(m, r), где r - остаток от деления n на m.
{
int r, a = n, b = m;
Count = 0;
// Инвариант: НОД(a, b) = НОД(n, m)
// Цикл заканчивается при b = 0, тогда НОД(n, m) = НОД(a, 0) = a
while (b)
{
r = a % b;
a = b;
b = r;
++ Count;
}
return a;
}
int _tmain(int argc, _TCHAR* argv[])
{
setlocale(0, "");
cout << "
Алгоритм Евклида для нахождения наибольшего общего делителя.\n";
cout << "
------------------------------------------------------------\n";
for (char b = '1'; b != 27; cout << "\n\t\t\tПродолжим? (нет - Esc) ",
b = _getch(), cout << endl)
{
int Count;
int n, m;
cout << "\n Введите два целых числа больших 0: ";
cin >> n >> m;
cout << "\n Значение НОД чисел " << n << " и " << m << " равно "
<< NOD_1 (n, m, Count) << endl;
cout << "Число итераций: " << Count << endl;
cout << "\n Значение НОД чисел " << n << " и " << m << " равно "
<< NOD_2 (n, m, Count) << endl;
cout << "Число итераций: " << Count << endl;
}
return 0;
}
80
Download