III Flashcards
Синтаксис имен переменных, венгерская нотация
- Имена переменных в C++ могут состоять из букв и цифр, но не могут начинаться с цифр, а также содержать пробелы и арифметические операторы (такие, как +, - и т.п.).
- Именами переменных не могут быть зарезервированные ключевые слова. Например, переменная по имени return приведет к ошибке при компиляции.
- В именах переменных можно использовать символ подчеркивания, который позволяет создавать более понятные, самодокументируемые имена переменных.
- Вы можете встретить код C++, в котором имя переменной предваряется символами, описывающими тип переменной. Это соглашение называется венгерской нотацией и часто используется в программировании в Windows. Так, переменная firstNumber в венгерской нотации имела бы имя iFirstNumber, где префикс i означает тип int. Глобальная переменная имела бы имя giFirstNumber. Венгерская нотация в последние годы теряет популярность, частично из-за улучшения интегрированных сред разработки, при необходимости отображающих тип переменной, например при наведении на них указателя мыши.
Проблемы с использованием глобальных переменных
Безосновательное использование глобальных переменных обычно считается плохой практикой программирования.
Это связано с тем, что значение глобальной переменной может быть присвоено в любой функции, и это значение может оказаться непредсказуемым, в особенности если разные функциональные модули разрабатываются разными программистами группы или выполняются в разных потоках.
Концепция знаковых и беззнаковых целых чисел, самый старший бит
- Область памяти размером 1 байт может хранить одно из 2 в степени 8, т.е. 256 разных значений. Аналогично область памяти размером 16 битов может хранить одно из 2 в степени 16 разных значений, т.е. одно из 65536 уникальных значений.
- Но как же представить в этой же области отрицательные числа? Один из способов — “пожертвовать” одним из разрядов для хранения знака, который указывал бы, положительное или отрицательное значение содержится в других битах. Такой знаковый разряд имеет смысл делать самым старшим битом (Most-Significant-Bit — MSB). Если старший бит содержит информацию о знаке, предполагается, что значение 0 означает положительное число, а значение 1 — отрицательное. Другие биты содержат абсолютное значение числа.
- Таким образом, занимающее 8 битов знаковое число может содержать значения в пределах от -128 до 127, а занимающее 16 битов — значения в пределах от -32 768 до 32 767.
Беззнаковые целочисленные типы unsigned short, unsigned int, unsigned long и unsigned long long
- В отличие от знаковых аналогов, беззнаковые целочисленные типы не могут содержать информацию о знаке, зато могут содержать вдвое большие положительные значения.
- Переменные беззнакового типа используются тогда, когда ожидаются только неотрицательные значения. Так, если вы подсчитываете количество яблок, не используйте тип int; воспользуйтесь типом unsigned int. Последний может содержать вдвое больше положительных значений, чем первый.
- Размер переменной знакового и беззнакового типов одинаков; единственным различием этих двух типов является знаковый старший бит.
Переполнение типов
- Типы данных, такие как short, int, long, unsigned short, unsigned int, unsigned long и другие, имеют ограниченную емкость и могут содержать числа, не превышающие некоторые пределы. Превысив предел для выбранного типа, вы получаете переполнение.
- Возьмем в качестве примера unsigned short. Этот тип данных обычно содержит 16 бит, а потому может содержать значения от 0 до 65 535. Если вы прибавите 1 к 65535 в переменной типа unsigned short, циклический переход приведет к значению 0.
Использование разделителя разрядов
Определение размера переменной с использованием оператора sizeof
Размер представляет собой объем памяти, резервируемый компилятором при объявлении программистом переменной для хранения присваиваемых ей данных.
Размер переменной зависит от ее типа, и в языке C++ есть очень удобный оператор sizeof, который возвращает размер переменной или типа в байтах.
Применение оператора sizeof очень простое. Чтобы определить размер целого числа, вызовите оператор sizeof с параметром в виде типа int:
cout << “Размер int: “ << sizeof (int);
Запрет сужающего преобразования при использовании инициализации списком
При инициализации переменной меньшего целочисленного типа (скажем, short) значением переменной большего типа (скажем, int) вы рискуете получить ошибку сужающего преобразования, при которой компилятор должен преобразовать значение, хранящееся в типе, который потенциально может содержать гораздо большие числа, в тип, который имеет меньшие размеры, например:
- int largeNum = 5000000;*
- short smallNum = largeNum; // Компилируется, но возможна ошибка*
Чтобы избежать этой проблемы, C++11 рекомендует инициализацию списком, которая предотвращает сужение. Для использования этой возможности поместите значения или переменные инициализации в фигурные скобки { }. Синтаксис инициализации списком выглядит следующим образом:
- int largeNum = 5000000;*
- short anotherNum{ largeNum }; // Ошибка сужения!*
- int anotherNum{ largeNum }; // OK!*
- float someFloat{ largeNum }; // Ошибка! Возможно сужение*
- float someFloat{ 5000000 }; // OK! 5000000 помещается в float*
Автоматический вывод типа с использованием auto
- В ряде случаев тип переменной очевиден — по присваиваемому при инициализации значению. Например, если переменная инициализируется значением true, следует ожидать, что, скорее всего, типом переменной будет bool. Компиляторы с поддержкой C++11 и выше дают возможность определять тип неявно, с использованием вместо типа переменной его ключевого слова auto:
- auto coinFlippedHeads = true;*
- Здесь задача определения конкретного типа переменной coinFlippedHeads оставлена компилятору. Компилятор просто проверяет природу значения, которым инициализируется переменная, а затем выбирает тип, наилучшим образом подходящий для этой переменной.
- Использование ключевого слова auto требует инициализации переменной, поскольку компилятор нуждается в инициализирующем значении, чтобы принять решение о наилучшем типе для переменной. Если вы не инициализируете переменную, то применение ключевого слова auto приведет к ошибке при компиляции.
- Хотя, на первый взгляд, ключевое слово auto кажется не особенно полезным, оно существенно упрощает программирование в тех случаях, когда тип переменной сложен.
Использование ключевого слова typedef для замены типа
Язык C++ позволяет переименовывать типы переменных так, как вам кажется более удобным. Для этого используется ключевое слово typedef. Например, программист хочет назначить типу unsigned int более описательное имя STRICTLY_POSITIVE_INTEGER.
typedef unsigned int STRICTLY_POSITIVE_INTEGER;
STRICTLY_POSITIVE_INTEGER numEggsInBasket = 4532;
При компиляции первая строка указывает компилятору, что STRICLY_POSITIVE_INTEGER — это не что иное, как тип unsigned int. Впоследствии, когда компилятор встречает уже определенный тип STRICLY_POSITIVE_INTEGER, он заменяет его типом unsigned int и продолжает компиляцию.
Что такое константа
После того как значение константы определено, оно не может быть изменено. Попытки присваивания значения константе в языке C++ приводят к ошибке при компиляции.
Таким образом, в C++ константы похожи на переменные, за исключением того, что они не могут быть изменены. Подобно переменной, константа также занимает пространство в памяти и имеет имя для идентификации адреса выделенной для нее области. Однако содержимое этой области не может быть перезаписано. В языке C++ возможны следующие константы.
- Литеральные константы.
- Константы, объявленные с использованием ключевого слова const.
- Константные выражения, использующие ключевое слово constexpr (нововведение С++11).
- Константы перечислений, использующие ключевое слово enum.
- Константы, определенные с помощью макроопределений, использование которых не рекомендуется и осуждается.
Литеральные константы
Литеральные константы могут быть многих типов — целочисленные, строки и т.д.
В нашей первой программе строка “Hello World” выводится с помощью следующей инструкции:
std : : cout << “Hello World” << std : : endl;
Здесь “Hello World” — это константа строкового литерала (string literal). Когда вы объявляете целое число наподобие
int someNumber = 10;
целочисленной переменной someNumber присваивается начальное значение, равное 10. Здесь 10 — это часть кода, компилируемая в приложение, которая является неизменной и тоже является литеральной константой (literal constant).
Объявление переменных как констант с использованием ключевого слова const
Самый важный тип констант C++ с практической и программной точек зрения объявляется с помощью ключевого слова const, расположенного перед типом переменной. В общем виде объявление выглядит следующим образом:
const имя_типа имя_константы = значение;
Хорошей практикой программирования является определение переменных, значения которых предполагаются неизменными, как констант. Применение ключевого слова const указывает, что программист позаботился об обеспечении неизменности данных и защищает свое приложение от непреднамеренных изменений этой константы. Это особенно полезно, когда над проектом работает несколько программистов.
Константы полезны при объявлении массивов постоянной длины, которые неизменны во время компиляции.
Объявление констант с использованием ключевого слова constexpr
Ключевое слово constexpr позволяет объявлять константы подобно функциям:
constexpr double GetPi( ) {return 3.1415926;}
Одно constexpr - выражение может использовать другое:
constexpr double TwicePi( ) {return 2 * GetPi( );}
GetPi( ) и TwicePi( ) могут казаться функциями, но это не совсем функции. Дело в том, что функции вызываются во время выполнения программы. Эти же константные выражения компилятор заменяет числом 3.141593 при каждом использовании GetPi( ) и числом 6.283186 при использовании TwicePi( ). Такое разрешение TwicePi( ) в константу увеличивает скорость выполнения программы по сравнению выполнением вычисления, содержащегося в функции.
Перечисления
Иногда некая переменная должна принимать значения только из определенного набора. Например, вы не хотите, чтобы среди цветов радуги случайно оказался бирюзовой или среди направлений компаса оказалось направление влево. В обоих этих случаях необходим тип переменной, значения которой ограничиваются определенным вами набором.
Перечисления (enumerations) — это именно то, что необходимо в данной ситуации. Перечисления объявляются с помощью ключевого слова enum.
Вот пример перечисления, которое определяет цвета радуги:
enum RainbowColors
{
Violet = 0,
Indigo,
Blue,
Green,
Yellow,
Orange,
Red
};
А вот другой пример — направления компаса:
enum CardinalDirections
{
North,
South,
East,
West
};
Перечисления используются как пользовательские. Переменные этого типа могут принимать значения, ограниченные объявленными ранее значениями перечисления. Так, при определении переменной, которая содержит цвет радуги, вы объявляете ее следующим образом:
RainbowColors MyWorldsColor = Blue; // Начальное значение
При объявлении перечисления компилятор преобразует его константы, такие как Violet и другие, в целые числа. Каждое последующее значение перечисления на единицу больше предыдущего. Начальное значение вы можете задать сами, но если вы этого не сделаете, компилятор начнет счет с 0. Так, значению North соответствует числовое значение 0.
По желанию можно также явно определить числовое значение напротив каждой из перечисляемых констант при их инициализации.
Определение констант с использованием директивы #define
Первое и главное: не используйте этот способ при написании новых программ.
Единственная причина упоминания определения констант с использованием директивы #define в этой книге — помочь вам понять некоторые устаревшие программы, в которых для определения числа к мог бы использоваться такой синтаксис:
#define pi 3.141593
Это макрокоманда препроцессора, предписывающая компилятору заменять все упоминания pi значением 3.141593 . Обратите внимание: это текстовая (читай: неинтеллектуальная) замена, осуществляемая препроцессором. Компилятор не знает фактический тип рассматриваемой константы и не заботится о нем.
Определение констант с использованием директивы препроцессора #define считается устаревшим и не рекомендуется.
Массивы (+ синтаксис), статические и динамические массивы, длина массива
- В языке C++ массивы позволяют сохранить в памяти элементы данных некоторого типа в последовательном упорядоченном виде.
- Массивы называются статическими (static array) если количество содержащихся в них элементов, а также размер выделенной для них области памяти остаются неизменными во время компиляции.
- Для объявления массива в языке C++ используется следующий синтаксис:
- Тип_элемента Имя_массива [Количество_элементов] = {Необязательные исходные значения};*
- Все элементы массива можно инициализировать нулем (значение по умолчанию, предоставляемое компилятором):
- int myNumbers[5] = {0}; // Инициализировать все элементы нулем*
- Вы можете также инициализировать только часть элементов массива:
- int myNumbers[5] = {34, 56}; // инициализировать первые два*
- // элемента значениями 34 и 56, прочие элементы равны нулю*
- Вы можете определить длину массива (т.е. указать количество элементов в нем) как константу и использовать ее при определении массива:
- const int ARRAY_LENGTH = 5;*
- int myNumbers[ARRAYJLENGTH] = {34, 56, -21, 5002, 365};*
- Это особенно полезно, когда необходимо иметь доступ и использовать длину массива в нескольких местах, например при переборе всех элементов массива. В таком случае при изменении длины массива достаточно будет исправить лишь одно значение, объявленное как const int.
- Если исходное количество элементов в массиве неизвестно, его можно не указывать:
int myNumbers[] = {2017, 2052, -525};
- Массивы, размер которых определяется во время выполнения, называются динамическими.
- Длина массива должна быть константным целочисленным значением.
- Динамические массивы позволяют программисту не беспокоиться об установке максимальной длины массива во время компиляции, а также обеспечивают лучшее управление памятью в случае, если размер массива меньше ожидаемого максимума.
Объем памяти в байтах, резервируемой компилятором для массива?
В общем виде объем памяти в байтах, резервируемой компилятором для массива, составляет:
Байты массива = sizeof (Тип элемента) * Количество элементов
Доступ к данным, хранимым в массиве (ключевая проблема), переполнение буфера
- Когда запрашивается доступ к элементу с индексом N, компилятор использует адрес первого элемента (позиция элемента с нулевым индексом) в качестве отправной точки, а затем пропускает N элементов, добавляя к этому адресу смещение, вычисляемое как N*sizeof(тип_элемента), чтобы получить адрес N+1-го элемента. Компилятор C++ не проверяет, находится ли индекс в пределах фактически определенных границ массива.
- Результат доступа к массиву за его пределами непредсказуем. Как правило, такое обращение ведет к аварийному завершению программы.
- Важно не выходить за границы массива. Это явление называется переполнением буфера (buffer overflow), и проверка ввода перед его использованием для индексации элементов позволяет гарантировать отсутствие пересечения границ массива.
Многомерные массивы
- Что если нам нужно использовать массив для моделирования солнечных панелей, показанных на рис. 4.3? Солнечные панели, в отличие от книжных полок, распространяются в двух размерностях: по длине и по ширине.
- Как можно заметить на рис. 4.3, шесть солнечных панелей располагаются в двумерном порядке: два ряда (строки) по три столбца. Но можно рассматривать такое расположение и как массив из двух элементов, каждый из которых сам является массивом из трех панелей; другими словами, как массив массивов.
- В языке C++ вы може те создавать двумерные массивы, но вы не ограничены только двумя размерностями. В зависимости от необходимости и характера приложения вы можете создавать в памяти многомерные массивы.
- Язык C++ позволяет объявлять многомерные массивы, указывая количество элементов, которое необходимо выделить в каждой размерности. Таким образом, двумерный массив целых чисел, представляющий солнечные панели на рис. 4.3, можно объявить так:
- int solarPanelIDs [2] [3];*
- Обратите внимание, что на рис. 4.3 каждой из шести панелей присвоен также идентификатор в диапазоне от 0 до 5. Если мы инициализируем целочисленный массив в том же порядке, то эта инициализация будет иметь следующий вид:
- int solarPanellDs [2] [3] = {{0, 1, 2}, {3, 4, 5}};*
- Несмотря на то что язык C++ позволяет использовать модель многомерных массивов, в памяти такие массивы все равно хранятся как одномерные. Компилятор отображает многомерный массив на область памяти, которая расширяется только в одном направлении.
Доступ к элементам многомерного массива
Рассмотрим следующий массив:
int threeRowsThreeColumns [3] [3] = {{-501, 206, 2017}, {989, 101, 206}, {303, 456, 596}};
Он инициализирован так, что его можно рассматривать как три массива, каждый из которых содержит три целых числа. Здесь целочисленный элемент со значением 206 находится в позиции [0] [1], а элемент со значением 456 — в позиции [2] [1].
Строки символов в стиле С
- Строки в стиле С (C-style string) — это частный случай массива символов. Вы уже видели несколько примеров таких строк в виде строковых литералов, когда писали код:
std : : cout << “Hello World”;
- Это эквивалентно такому объявлению массива:
char sayHello[] = {‘Н’,’е’,’l’,’l’,’о’,’ ‘,’W’,’о’,’г’,’l’,’d’,’\0’}
std : : cout << sayHello << std : : endl;
- Обратите внимание: последний символ в массиве — нулевой символ ‘\0’. Он также называется завершающим нулевым символом (string-terminating character), поскольку указывает компилятору, что строка на этом заканчивается. Такие строки в стиле С — это частный случай символьных массивов, последним символом которых всегда является нулевой символ ‘\0’. Когда вы используете в коде строковый литерал, компилятор сам добавляет после него символ ‘\0’.
- Если вставить символ ‘\0’ в середину массива, то это не изменит его размер; однако обработка строки, хранящейся в данном массиве, остановится на этой точке. При этом размер массива не изменится, несмотря на изменение отображаемых данных.
- Если при объявлении и инициализации символьного массива вы забудете добавить символ ‘\0’ то после вывода
“Hello World” на консоль будет выведен случайный набор символов. Дело в том, что оператор std : : cout не остановится по окончании массива и будет продолжать вывод, пока не встретит нулевой символ, даже если для этого придется перейти границы массива. Эта ошибка может привести вашу программу к аварийному останову, а в некоторых случаях поставить под угрозу стабильность системы. - Приложения, написанные на языке С (или на языке C++ программистами с большим опытом в языке С), зачастую используют в своем коде функции копирования строк, такие как strcpy, функции конкатенации, такие как strcat, и определения длины строк, такие как strlen. Эти функции используют строки в стиле С и потому опасны, так как ищут завершающий нулевой символ и могут легко выйти за границы символьного массива, если программист не гарантировал наличие завершающего
нулевого символа.
Строки C++: использование std : : string
Язык C++ предоставляет мощное и в то же время безопасное средство работы со
строками — класс s t d : : s tr in g . Класс s t d : : s tr in g не является статическим мас
сивом элементов типа char неизменного размера, как строки в стиле С, и допускает
увеличение размера, когда в нем необходимо сохранить больше данных.
Для использования строк C++ в код необходимо включить заголовочный
файл s trin g :
tin c lu d e < strin g >
Зачем заботиться об инициализации элементов статического массива?
Если не инициализировать массив, он будет содержать случайные и непредсказуемые значения, поскольку область занимаемой им памяти останется неизменной после последних операций. Инициализация массивов гарантирует, что находящая ся в нем информация будет иметь определенное и предсказуемое начальное состояние.
Следует ли инициализировать элементы динамического массива по причинам,
упомянутым в первом вопросе?
Вообще-то, нет. Динамический массив весьма интеллектуален. Нет необходимости инициализировать элементы динамического массива значениями по умолчанию, если для этого нет причин, связанных с приложением, которому нужно иметь в массиве определенные исходные значения.