Первая программа на c, типы данных и их размер


Содержание

Типы данных

Типы данных имеют особенное значение в C#, поскольку это строго типизированный язык. Это означает, что все операции подвергаются строгому контролю со стороны компилятора на соответствие типов, причем недопустимые операции не компилируются. Следовательно, строгий контроль типов позволяет исключить ошибки и повысить надежность программ. Для обеспечения контроля типов все переменные, выражения и значения должны принадлежать к определенному типу. Такого понятия, как «бестиповая» переменная, в данном языке программирования вообще не существует. Более того, тип значения определяет те операции, которые разрешается выполнять над ним. Операция, разрешенная для одного типа данных, может оказаться недопустимой для другого.

В C# имеются две общие категории встроенных типов данных: типы значений и ссылочные типы. Они отличаются по содержимому переменной. Концептуально разница между ними состоит в том, что тип значения (value type) хранит данные непосредственно, в то время как ссылочный тип (reference type) хранит ссылку на значение.

Эти типы сохраняются в разных местах памяти: типы значений сохраняются в области, известной как стек, а ссылочные типы — в области, называемой управляемой кучей.

Давайте разберем типы значений.

Целочисленные типы

В C# определены девять целочисленных типов: char, byte, sbyte, short, ushort, int, uint, long и ulong. Но тип char применяется, главным образом, для представления символов и поэтому рассматривается отдельно. Остальные восемь целочисленных типов предназначены для числовых расчетов. Ниже представлены их диапазон представления чисел и разрядность в битах:

Целочисленные типы C#

Тип Тип CTS Разрядность в битах Диапазон
byte System.Byte 8 0:255
sbyte System.SByte 8 -128:127
short System.Int16 16 -32768 : 32767
ushort System.UInt16 16 0 : 65535
int System.Int32 32 -2147483648 : 2147483647
uint System.UInt32 32 0 : 4294967295
long System.Int64 64 -9223372036854775808 : 9223372036854775807
ulong System.UInt64 64 0 : 18446744073709551615

Как следует из приведенной выше таблицы, в C# определены оба варианта различных целочисленных типов: со знаком и без знака. Целочисленные типы со знаком отличаются от аналогичных типов без знака способом интерпретации старшего разряда целого числа. Так, если в программе указано целочисленное значение со знаком, то компилятор C# сгенерирует код, в котором старший разряд целого числа используется в качестве флага знака. Число считается положительным, если флаг знака равен 0, и отрицательным, если он равен 1.

Отрицательные числа практически всегда представляются методом дополнения до двух, в соответствии с которым все двоичные разряды отрицательного числа сначала инвертируются, а затем к этому числу добавляется 1.

Вероятно, самым распространенным в программировании целочисленным типом является тип int. Переменные типа int нередко используются для управления циклами, индексирования массивов и математических расчетов общего назначения. Когда же требуется целочисленное значение с большим диапазоном представления чисел, чем у типа int, то для этой цели имеется целый ряд других целочисленных типов.

Так, если значение нужно сохранить без знака, то для него можно выбрать тип uint, для больших значений со знаком — тип long, а для больших значений без знака — тип ulong. В качестве примера ниже приведена программа, вычисляющая расстояние от Земли до Солнца в сантиметрах. Для хранения столь большого значения в ней используется переменная типа long:

Всем целочисленным переменным значения могут присваиваться в десятичной или шестнадцатеричной системе обозначений. В последнем случае требуется префикс 0x:

Если возникает какая-то неопределенность относительно того, имеет ли целое значение тип int, uint, long или ulong, то по умолчанию принимается int. Чтобы явно специфицировать, какой другой целочисленный тип должно иметь значение, к числу можно добавлять следующие символы:

U и L можно также указывать в нижнем регистре, хотя строчную L легко зрительно спутать с цифрой 1 (единица).

Типы с плавающей точкой

Типы с плавающей точкой позволяют представлять числа с дробной частью. В C# имеются две разновидности типов данных с плавающей точкой: float и double. Они представляют числовые значения с одинарной и двойной точностью соответственно. Так, разрядность типа float составляет 32 бита, что приближенно соответствует диапазону представления чисел от 5E-45 до 3,4E+38. А разрядность типа double составляет 64 бита, что приближенно соответствует диапазону представления чисел от 5E-324 до 1,7Е+308.

Тип данных float предназначен для меньших значений с плавающей точкой, для которых требуется меньшая точность. Тип данных double больше, чем float, и предлагает более высокую степень точности (15 разрядов).

Если нецелочисленное значение жестко кодируется в исходном тексте (например, 12.3), то обычно компилятор предполагает, что подразумевается значение типа double. Если значение необходимо специфицировать как float, потребуется добавить к нему символ F (или f):

Десятичный тип данных

Для представления чисел с плавающей точкой высокой точности предусмотрен также десятичный тип decimal, который предназначен для применения в финансовых расчетах. Этот тип имеет разрядность 128 бит для представления числовых значений в пределах от 1Е-28 до 7,9Е+28. Вам, вероятно, известно, что для обычных арифметических вычислений с плавающей точкой характерны ошибки округления десятичных значений. Эти ошибки исключаются при использовании типа decimal, который позволяет представить числа с точностью до 28 (а иногда и 29) десятичных разрядов. Благодаря тому что этот тип данных способен представлять десятичные значения без ошибок округления, он особенно удобен для расчетов, связанных с финансами:

Результатом работы данной программы будет:

Символы

В C# символы представлены не 8-разрядным кодом, как во многих других языках программирования, например С++, а 16-разрядным кодом, который называется юникодом (Unicode). В юникоде набор символов представлен настолько широко, что он охватывает символы практически из всех естественных языков на свете. Если для многих естественных языков, в том числе английского, французского и немецкого, характерны относительно небольшие алфавиты, то в ряде других языков, например китайском, употребляются довольно обширные наборы символов, которые нельзя представить 8-разрядным кодом. Для преодоления этого ограничения в C# определен тип char, представляющий 16-разрядные значения без знака в пределах от 0 до 65 535. При этом стандартный набор символов в 8-разрядном коде ASCII является подмножеством юникода в пределах от 0 до 127. Следовательно, символы в коде ASCII по-прежнему остаются действительными в C#.

Для того чтобы присвоить значение символьной переменной, достаточно заключить это значение (т.е. символ) в одинарные кавычки:

Несмотря на то что тип char определен в C# как целочисленный, его не следует путать со всеми остальными целочисленными типами. Дело в том, что в C# отсутствует автоматическое преобразование символьных значений в целочисленные и обратно. Например, следующий фрагмент кода содержит ошибку:

Наравне с представлением char как символьных литералов, их можно представлять как 4-разрядные шестнадцатеричные значения Unicode (например, ‘\u0041’), целочисленные значения с приведением (например, (char) 65) или же шестнадцатеричные значения (например, ‘\x0041’). Кроме того, они могут быть представлены в виде управляющих последовательностей.

Логический тип данных

Тип bool представляет два логических значения: «истина» и «ложь». Эти логические значения обозначаются в C# зарезервированными словами true и false соответственно. Следовательно, переменная или выражение типа bool будет принимать одно из этих логических значений. Кроме того, в C# не определено взаимное преобразование логических и целых значений. Например, 1 не преобразуется в значение true, а 0 — в значение false.

Типы данных в языке Си

Тип данных определяет множество значений, набор операций, которые можно применять к таким значениям и способ реализации хранения значений и выполнения операций.

Процесс проверки и накладывания ограничений на типы используемых данных называется контролем типов или типизацией программных данных . Различают следующие виды типизации:

  • Статическая типизация — контроль типов осуществляется при компиляции.
  • Динамическая типизация — контроль типов осуществляется во время выполнения.

Язык Си поддерживает статическую типизацию, и типы всех используемых в программе данных должны быть указаны перед ее компиляцией.

Различают простые, составные и прочие типы данных.

Простые данные

Простые данные можно разделить на

  • целочисленные,
  • вещественные,
  • символьные
  • логические.

Составные (сложные) данные

  • Массив — индексированный набор элементов одного типа.
  • Строковый тип — массив, хранящий строку символов.
  • Структура — набор различных элементов (полей записи), хранимый как единое целое и предусматривающий доступ к отдельным полям структуры.

Другие типы данных

  • Указатель — хранит адрес в памяти компьютера, указывающий на какую-либо информацию, как правило — указатель на переменную.

Программа, написанная на языке Си, оперирует с данными различных типов. Все данные имеют имя и тип. Обращение к данным в программе осуществляется по их именам (идентификаторам).

Идентификатор — это последовательность, содержащая не более 32 символов, среди которых могут быть любые буквы латинского алфавита a — z, A — Z, цифры 0 — 9 и знак подчеркивания (_). Первый символ идентификатора не должен быть цифрой.

Несмотря на то, что допускается имя, имеющее до 32 символов, определяющее значение имеют только первые 8 символов. Помимо имени, все данные имеют тип. Указание типа необходимо для того, чтобы было известно, сколько места в оперативной памяти будет занимать данный объект.

Компилятор языка Си придерживается строгого соответствия прописных и строчных букв в именах идентификаторов и лексем.

Верно Неверно
int a = 2, b;
b = a+3;
Int a=2; // правильно int
INT a=2; int a = 2, b;
b = A + 3; // идентификатор А не объявлен int a = 2;
b = a + 3; // идентификатор b не объявлен

Целочисленные данные

Целочисленные данные могут быть представлены в знаковой и беззнаковой форме.

Беззнаковые целые числа представляются в виде последовательности битов в диапазоне от 0 до 2 n -1, где n-количество занимаемых битов.

Знаковые целые числа представляются в диапазоне -2 n-1 …+2 n-1 -1. При этом старший бит данного отводится под знак числа (0 соответствует положительному числу, 1 – отрицательному).

Основные типы и размеры целочисленных данных:

Количество бит Беззнаковый тип Знаковый тип
8 unsigned char
0…255
char
-128…127
16 unsigned short
0…65535
short
-32768…32767
32 unsigned int int
64 unsigned long int long int

Вещественные данные

Вещественный тип предназначен для представления действительных чисел. Вещественные числа представляются в разрядной сетке машины в нормированной форме.

Нормированная форма числа предполагает наличие одной значащей цифры (не 0) до разделения целой и дробной части. Такое представление умножается на основание системы счисления в соответствующей степени. Например, число 12345,678 в нормированной форме можно представить как

12345,678 = 1,2345678·10 4

Число 0,009876 в нормированной форме можно представить как

0,009876 = 9,876·10 -3

В двоичной системе счисления значащий разряд, стоящий перед разделителем целой и дробной части, может быть равен только 1. В случае если число нельзя представить в нормированной форме (например, число 0), значащий разряд перед разделителем целой и дробной части равен 0.

Значащие разряды числа, стоящие в нормированной форме после разделителя целой и дробной части, называются мантиссой числа .

В общем случае вещественное число в разрядной сетке вычислительной машины можно представить в виде 4 полей.

  • знак — бит, определяющий знак вещественного числа (0 для положительных чисел, 1 — для отрицательных).
  • степень — определяет степень 2, на которую требуется умножить число в нормированной форме. Поскольку степень 2 для числа в нормированной форме может быть как положительной, так и отрицательной, нулевой степени 2 в представлении вещественного числа соответствует величина сдвига, которая определяется как

2 n -1,

где n — количество разрядов, отводимых для представления степени числа.

  • целое — бит, который для нормированных чисел всегда равен 1, поэтому в некоторых представлениях типов этот бит опущен и принимается равным 1.
  • мантисса — значащие разряды представления числа, стоящие после разделителя целой и дробной части в нормированной форме.
  • Различают три основных типа представления вещественных чисел в языке Си:

    Тип Обозна-
    чение в Си
    Кол-во бит Биты степени Мантисса Сдвиг
    простое float 32 30…23 22…0 127
    двойной точности double 64 62…52 51…0 1023
    двойной расширен- ной точности long double 80 78…64 62…0 16383

    Как видно из таблицы, бит целое у типов float и double отсутствует. При этом диапазон представления вещественного числа состоит из двух диапазонов, расположенных симметрично относительно нуля. Например, диапазон представления чисел типа float можно представить в виде:

    Пример : представить число -178,125 в 32-разрядной сетке (тип float ).

    Для представления числа в двоичной системе счисления преобразуем отдельно целую и дробную части:

    178,12510 = 10110010,0012=1,0110010001·2 111

    Для преобразования в нормированную форму осуществляется сдвиг на 7 разрядов влево).

    Для определения степени числа применяем сдвиг:

    0111111+00000111 = 10000110.

    Таким образом, число -178,125 представится в разрядной сетке как

    Символьный тип

    Символьный тип хранит код символа и используется для отображения символов в различных кодировках. Символьные данные задаются в кодах и по сути представляют собой целочисленные значения. Для хранения кодов символов в языке Си используется тип char .
    Подробнее о кодировке символов

    Логический тип

    Логический тип применяется в логических операциях, используется при алгоритмических проверках условий и в циклах и имеет два значения:

    В программе должно быть дано объявление всех используемых данных с указанием их имени и типа. Описание данных должно предшествовать их использованию в программе.

    Переменные в C++ — урок 3

    Всем привет! В данной статье я бы хотел рассказать о важнейшем компоненте в программировании — переменных. Без них очень трудно представить полноценную программу, ведь они позволяют взаимодействовать с пользователем, а также упрощают многие рутинные действие. Именно поэтому мы приступаем к изучению переменных!

    Видео урок

    Что такое переменные?

    Переменные — это выделенные ячейки в памяти под определенный тип данных. Сами же ячейки постоянно хранятся на компьютере пользователя. Их мы можем заполнять различными значениями, модифицировать и использовать в наших целях.

    Стандартные типы данных в C++

    Прежде чем мы продолжим знакомство с переменными в C++, давайте узнаем, значениями какого типа мы можем заполнять созданные нами переменные.

    Вот список стандартных типов данных:

    • int — это целый тип, который может хранить в себе только целые числа.
    • float — данный тип является неточным. Он позволяет хранить не только целую часть, но, в отличии от типа int, и дробную.
    • double — данный тип нечем не отличается от float , кроме более высокой точности (позволяет хранить больше чисел после запятой).
    • char — в данный тип данных можно записывать отдельные символы (абсолютно любые).
    • bool — хранит в себе значения логического типа: «правду» — true, либо «ложь» — false. О данном типе мы подробно поговорим в уроке о логических выражениях.

    Теперь когда мы вооружились знаниями о возможных типах данных, можем переходить непосредственно к созданию переменных на языке C++!

    Создание переменных

    Чтобы объявить переменную нам необходимо воспользоваться следующей конструкцией:

    BestProg

    Базовые типы данных Visual C++

    Вопросы

    1. Какие базовые (основные) типы данных используются в языке C++ ?

    Ответ:
    1. Целочисельные типы данных:

    short int , unsigned short int , int , unsigned int , long , unsigned long .

    1. Типы данных с плавающей запятой (соответствуют вещественным типам):

    float , double , long double .

    char ( signed char ), unsigned char, wchar_t .

    1. Перечислимый тип данных (введен в Visual C++ ):

    2. Какие особенности использования целочисленных типов данных?

    В C++ основные целочисленные типы данных: short int , unsigned short int , int , unsigned int , long ( long int ), unsigned long ( unsigned long int ).

    Эти типы данных представляют значения из множества целых чисел. Например:

    Типы данных, которые начинаются из приставки unsigned , могут содержать только положительные числа.

    Данные типа short int , unsigned short int занимают в два раза меньше места в памяти чем данные типа int , unsigned int .

    Данные типа long , unsigned long занимают в два раза больше места в памяти чем данные типа int , unsigned int .

    3. Как в программе описать переменную с именем x целого типа?

    Ответ:

    В результате под переменную x будет выделено место в памяти размером 4 байта. Размер памяти, которая выделяется под переменную зависит от характеристик компьютера, типа операционной системы и настроек компилятора.

    4. Как в переменную целого типа записать число 239?

    Для этого используется оператор присваивания, который обозначается символом ‘ = ‘.

    Ответ 1. Внесение числа в переменную после ее описания.

    Ответ 2. Внесение числа в переменную во время ее описания (начальная инициализация).

    5. Какие особенности типов данных с плавающей запятой?

    Типы данных с плавающей запятой разрешают представлять значения из множества вещественных чисел. Например:

    В C++ есть следующие базовые типы данных с плавающей запятой: float , double , long double .

    Переменная типа double занимает в 2 раза больше места в памяти компьютера чем переменная типа float .

    Так же переменная типа long double занимает в 2 раза больше места в памяти компьютера, чем переменная типа double .

    6. Как описать переменную, которая принимает значение с плавающей запятой?

    Пример описания переменных типа float , double , long double :

    7. Как в переменную с плавающей запятой записать числовые значения?

    Пример внесения числовых данных в переменные типы с плавающей запятой:

    8. Как перевести переменную типа float в тип int ?

    Для этого используется операция приведения типов. В скобках нужно указать название типа к которому происходит приведение.

    При использовании операций приведения типов, нужно учитывать ограничения, которые накладываются на типы, которые занимают меньше места в памяти компьютера.

    Например, переменная типа short int может представлять меньший диапазон чисел, чем переменные типов float , double . В следующему листинге происходит переполнение значения в переменной типа short int :

    9. Как перевести переменную из типа int в тип double ?

    Пример приведения с int в double :

    10. Какие особенности использования данных типа char (символьных данных) в программе?

    Данные типа char представляют символьное значение кода, введенного с клавиатуры. Код символа есть целое число.

    Например, код символа ‘f’ равен значению 102 .

    Фрагмент кода, в котором вычисляется код символа:

    Данные типа char есть теми же целыми числами. Данные типа char занимают в памяти компьютера 1 байт.

    Соотношение «символ-код» размещается в таблице символов Windows. Символы с кодами от 0 до 127 – это зарезервированные символы BIOS. Они включают наиболее употребляемые символы, символы цифр, символы латинской азбуки. Эти символы изменить нельзя.

    Символы с кодами от 128 до 255 – это региональные символы, которые привязанные к конкретной азбуке того компьютера на котором установленная операционная система Windows.

    11. Какие особенности использования данных типа bool (логический тип)?

    Переменные типа bool могут принимать только два значения:

    Эти переменные используются для проверки логических выражений. Числовое значение true равно 1 . Числовое значение false равно 0 .

    Фрагмент кода, который определяет числовые значения true и false :

    Фрагмент кода, который превращает типы int и float в bool :

    12. Как определить размер памяти, который занимает переменная данного типа?

    Для этого используется операция sizeof() .

    Фрагмент кода, который определяет размер некоторых типов данных:

    13. Каким образом осуществляется инициализация переменных разных типов?

    14. Каким образом определить максимально допустимое (минимально допустимое) значение переменной определенного типа?

    Чтобы определить максимально допустимое или минимально допустимое значение переменной некоторого типа в библиотеке .NET Framework используются свойства MaxValue и MinValue .

    Примеры определения предельных значений переменных разных типов.

    Для переменных типа int :

    Для переменных типа short int :

    Для переменных типа unsigned int :

    Для переменных типа float :

    Для переменных типа double :

    Для переменных типа char :

    15. Какие особенности использования типа enum ?

    Тип enum – это перечислительный тип данных. В нем задаются мнемонические значения для множеств целых значений. Каждое мнемоническое значение имеет определенное содержание и представляется целым числом.

    Пример использования типа enum для обозначения месяцев года:

    В приведенном примере описывается переменная с именем mn типа enum months . Мнемонические значения месяцев ( January , February , …) начинаются с 0 ( 0 , 1 , 2 , …). Мнемоническому значению January соответствует целое значение 0 , мнемоническому значению February соответствует целое значение 1 , и т.д.

    Итак, с помощью типа enum , в тексте программы можно использовать мнемонические обозначения для лучшей наглядности исходного кода.

    Можно написать и так:

    16. Какие особенности применения типа void в программах на C++ ?

    Тип данных void используется в следующих случаях:

    • если нужно описать функцию, которая не возвращает никакого значения (см. пример);
    • если нужно описать функцию, которая не получает параметров (см. пример).

    Пример. Функция MyFun() без параметров, которая не возвращает никакого значения (возвращает тип void ) и не получает параметров.

    17. Можно ли объявлять переменную типа void в программе?


    Нельзя, так как тип void не связан со значением.

    Объявление переменной типа void приводит к ошибке компиляции с выводом сообщения:

    18. Какие особенности применения типа wchar_t в Visual C++ ?

    Переменные типа char (смотрите предыдущие пункты) используются для сохранения 8-разрядных ASCII -символов.

    Тип wchar_t используется для сохранения символов, которые входят в состав больших символьных наборов. Например, в китайской азбуке есть огромное количество символов. 8 разрядов недостаточно, чтобы представить весь набор символов китайской азбуки. Поэтому, если нужно использовать программу на международном рынке, целесообразно заменить тип char на wchar_t .

    Пример использования типа wchar_t .

    19. Какой объем памяти резервируется для описания одной переменной типа wchar_t ?

    Одна переменная типа wchar_t занимает в памяти компьютера 2 байта (16 бит). Диапазон целочисленных значений переменных типа wchar_t составляет от 0 до 65535.

    Диапазоны типов данных Data Type Ranges

    Microsoft C++ 32-разрядных и 64-разрядные компиляторы распознает типы в таблице ниже в этой статье. The Microsoft C++ 32-bit and 64-bit compilers recognize the types in the table later in this article.

    int ( unsigned int ) int ( unsigned int )

    __int8 ( unsigned __int8 ) __int8 ( unsigned __int8 )

    __int16 ( unsigned __int16 ) __int16 ( unsigned __int16 )

    __int32 ( unsigned __int32 ) __int32 ( unsigned __int32 )

    __int64 ( unsigned __int64 ) __int64 ( unsigned __int64 )

    short ( unsigned short ) short ( unsigned short )

    long ( unsigned long ) long ( unsigned long )

    long long ( unsigned long long ) long long ( unsigned long long )

    Если имя начинается с двух символов подчеркивания ( __ ), тип данных является нестандартным. If its name begins with two underscores ( __ ), a data type is non-standard.

    Диапазоны, представленные в следующей таблице, включают указанные значения. The ranges that are specified in the following table are inclusive-inclusive.

    Имя типа Type Name Байты Bytes Другие имена Other Names Диапазон значений Range of Values
    int int 4 4 signed signed От -2 147 483 648 до 2 147 483 647 -2,147,483,648 to 2,147,483,647
    unsigned int unsigned int 4 4 unsigned unsigned От 0 до 4 294 967 295 0 to 4,294,967,295
    __int8 __int8 1 1 char char От -128 до 127 -128 to 127
    __int8 без знака unsigned __int8 1 1 unsigned char unsigned char От 0 до 255 0 to 255
    __int16 __int16 2 2 короткий, короткое целочисленное, короткое целочисленное число со знаком short, short int, signed short int От -32 768 до 32 767 -32,768 to 32,767
    unsigned __int16 unsigned __int16 2 2 unsigned short, короткое целое число unsigned short, unsigned short int От 0 до 65 535 0 to 65,535
    __int32 __int32 4 4 автоматический, целочисленное число со знаком, int signed, signed int, int От -2 147 483 648 до 2 147 483 647 -2,147,483,648 to 2,147,483,647
    unsigned __int32 unsigned __int32 4 4 без знака, типа int без знака unsigned, unsigned int От 0 до 4 294 967 295 0 to 4,294,967,295
    __int64 __int64 8 8 Long long, со знаком длинное длинное long long, signed long long От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
    unsigned __int64 unsigned __int64 8 8 long long без знака unsigned long long От 0 до 18 446 744 073 709 551 615 0 to 18,446,744,073,709,551,615
    bool bool 1 1 Нет none false или true false or true
    char char 1 1 Нет none -128 до 127 знаков по умолчанию -128 to 127 by default

    При компиляции при помощи /J— от 0 до 255 0 to 255 when compiled by using /J

    char со знаком signed char 1 1 Нет none От -128 до 127 -128 to 127
    unsigned char unsigned char 1 1 Нет none От 0 до 255 0 to 255
    short short 2 2 короткое целочисленное, короткое целочисленное число со знаком short int, signed short int От -32 768 до 32 767 -32,768 to 32,767
    unsigned short unsigned short 2 2 unsigned short int unsigned short int От 0 до 65 535 0 to 65,535
    long long 4 4 Long int, длинное целочисленное число со знаком long int, signed long int От -2 147 483 648 до 2 147 483 647 -2,147,483,648 to 2,147,483,647
    unsigned long unsigned long 4 4 unsigned long int unsigned long int От 0 до 4 294 967 295 0 to 4,294,967,295
    long long long long 8 8 Нет (но эквивалентно __int64) none (but equivalent to __int64) От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
    long long без знака unsigned long long 8 8 Нет (но эквивалентно unsigned __int64) none (but equivalent to unsigned __int64) От 0 до 18 446 744 073 709 551 615 0 to 18,446,744,073,709,551,615
    enum enum Возможны разные варианты varies Нет none
    float float 4 4 Нет none 3,4E +/- 38 (7 знаков) 3.4E +/- 38 (7 digits)
    double double 8 8 Нет none 1,7E +/- 308 (15 знаков) 1.7E +/- 308 (15 digits)
    long double long double Совпадение с кодом double same as double Нет none Совпадение с кодом double Same as double
    wchar_t wchar_t 2 2 __wchar_t __wchar_t От 0 до 65 535 0 to 65,535

    Зависимости от того, как она используется, переменная __wchar_t обозначает расширенный символьный или Многобайтовый символьный тип. Depending on how it’s used, a variable of __wchar_t designates either a wide-character type or multibyte-character type. Чтобы указать константу расширенного символьного типа, перед символьной или строковой константой следует использовать префикс L . Use the L prefix before a character or string constant to designate the wide-character-type constant.

    автоматический и без знака называются модификаторы, которые можно использовать с любым целочисленным типом за исключением bool. signed and unsigned are modifiers that you can use with any integral type except bool. Обратите внимание, что char, автоматический char, и unsigned char являются три различных типа для механизмов, подобных перегрузке и шаблонам. Note that char, signed char, and unsigned char are three distinct types for the purposes of mechanisms like overloading and templates.

    Int и unsigned int типы имеют размер 4 байта. The int and unsigned int types have a size of four bytes. Однако переносимый код не должно влиять на размер int так, как языковой стандарт позволяет это может быть от реализации. However, portable code should not depend on the size of int because the language standard allows this to be implementation-specific.

    C и C++ в Visual Studio также поддерживают целочисленные типы с указанием размера. C/C++ in Visual Studio also supports sized integer types. Дополнительные сведения см. в разделах __int8, __int16, __int32, __int64 и Пределы целых чисел. For more information, see __int8, __int16, __int32, __int64 and Integer Limits.

    Дополнительные сведения об ограничениях, связанных с размером, каждого типа см. в статье Фундаментальные типы. For more information about the restrictions of the sizes of each type, see Fundamental Types.

    Диапазон перечисляемых типов зависит от контекста языка и указанных флажков компилятора. The range of enumerated types varies depending on the language context and specified compiler flags. Дополнительные сведения см. в статьях Объявления перечислений C и Объявления перечислений C++. For more information, see C Enumeration Declarations and Enumerations.

    Типы данных и их вывод

    На этом уроке мы познакомимся с особенностями функции printf() и типами данных: целыми и вещественными числами, символами, массивами и строками. Это далеко не все допустимые в C типы. Есть еще указатели, структуры, объединения, перечисления, также в C есть возможность определять собственные типы данных.

    Функция printf() и форматированный вывод

    Вывод символов на экран, а точнее в стандартный поток вывода, осуществляется в языке C помощью функции printf(). Эта функция выводит на экран строку, переданную первым аргументом, предварительно заменив в ней специальные комбинации символов преобразованными в символы данными, переданными последующими аргументами. Следующие после первой строки данные могут быть строками, символами, целыми или вещественными числами, а также указателями. У каждого типа данных имеется свое обозначение — своя спецификация формата.

    На прошлом уроке мы выводили строку «Hello World» вот так:

    Однако то же самое можно было получить так:

    Здесь %s — это спецификация строкового формата, т. е. вместо %s будет подставлен следующий аргумент, данные которого должны быть строкой. Вывод целого числа может выглядеть так:

    Вместо числа 5 может стоять переменная целочисленного типа. Функция printf() может принимать произвольное число аргументов:

    При выводе данные подставляются по очередности следования: 3 на место первой спецификации, dogs на место второй и т.д. Т. е. следует строго соблюдать соответствие форматов и последующих данных.

    Под выводимые данные можно выделять больше знакомест, чем необходимо. Для этого между знаком % и буквой формата прописывается целое число, обозначающие ширину поля, например так: %10d. По умолчанию выравнивание происходит по правому краю. Для выравнивания по левому краю перед числом ставится знак минус.

    Задание

    Напишите программу, которая выводила бы на экране данные примерно так, как на картинке. При этом используйте возможность задать ширину поля, а также выравнивание по левому и правому краям.

    Целочисленные типы

    В языке C существует несколько типов целых чисел. Они различаются между собой объемом памяти, отводимым под переменную, а также возможностью присваивания положительных и отрицательных чисел. От объема памяти, т. е. от количества выделяемых байтов под переменную, зависит, каким может быть максимально возможное значение, записанное в данную переменную. Следует отметить, что в языке Си объем памяти, выделяемый под конкретный тип, может зависеть от операционной системы.

    Так, если под переменную какого-либо целочисленного типа выделяется 2 байта, что составляет 16 бит, и ей можно присваивать только положительные числа и ноль, то эти числа будут в диапазоне от 0 до 65535, т. к. 2 16 = 65536, но одна вариация забирается на нуль. Если же тип допускает отрицательные числа, то диапазон допустимых значений уже будет лежать в пределах от -32768 до +32767.

    Часто в программах используется тип int. Вот пример, где происходит объявление и определение (присваивание значений) целочисленных переменных, а также вывод их значений на экран:

    Обратите внимание, что в языке C присваивать значение можно при объявлении переменных.

    Обычно под переменную типа int, которая может принимать как положительные так и отрицательные значения, отводится 4 байта, что равно 32-м битам. Отсюда допустимый диапазон значений будет лежать в пределах от -2 147 483 648 до 2 147 483 647. Если в исходном коде на C мы объявим переменную int max, присвоим ей максимально допустимое значение, а потом будем его увеличивать, то сообщений об ошибке не будет ни на этапе компиляции, ни на этапе выполнения.

    Результат будет таким:

    Чтобы понять, почему такое происходит, представьте себе числовую ось не в виде прямой, а в виде окружности. Когда мы достигаем конца, двигаясь например по часовой стрелке, то это значит, что мы пришли в начало. Поэтому, продолжая движение по часовой стрелке, следующее число, которое мы увидим за максимально возможным, – это самое минимальное. Данную особенность языка Си следует иметь в виду при выполнении арифметических действий.

    То же самое с минимумом int. Если мы начнем из него вычитать, т. е. двигаться против часовой стрелки, то перескочим максимальную границу и будем идти в направлении уменьшения уже от нее:

    Помимо типа int в языке программирования C существуют другие (модифицированные) целочисленные типы:

    short — отводится меньше байтов, чем на int;

    long — отводится больше байтов, чем на int (не всегда, зависит от системы);

    unsigned — столько же байт как у int, но без отрицательных чисел; в результате чего знаковый разряд освобождается, и количество положительных значений увеличивается;

    При выводе длинных чисел следует дополнять спецификацию формата буквой l перед буквой формата. Например:

    Символы

    Под символьный тип данных отводится 1 байт памяти. У каждого символа есть соответствующее ему целое число по таблице символов ASCII.

    Тип char языка программирования C включает диапазон чисел от -128 до 127. Значения от 0 до 127 могут быть заданы или выведены на экран в виде соответствующих символов (на самом деле не все). Если значение переменной задается в виде символа, то символ заключается в одиночные кавычки, например, так: ‘w’. Также в языке существует тип unsigned char с диапазоном чисел от 0 до 255.

    С другой стороны, если переменная задана как int или short и ей присвоено значение в диапазоне, где оно может быть представлено символом, то значение можно вывести как символ. Соответственно целочисленной переменной можно присвоить символ.

    Если в программе вы будете использовать целые числа со значениями до 127 или 255 и хотите сэкономить память, то объявите переменную как char или unsigned char.

    Получается, что в программе символы — это числа, а числа — символы. Тогда как указать, что мы хотим видеть на экране: символ или число? Для вывода на экран символов используется спецификация формата вида %c.

    Так программа, представленная ниже,

    выдает такой результат:

    Число 63 по таблице символов ASCII соответствует знаку ‘?’. Сначала мы выводим значение переменной ch в формате символа, затем – числа. Тоже самое с переменной uch, однако ее значение было задано через символ, а не число.

    Вещественные типы данных

    В языке C существует три типа чисел с плавающей точкой: float и double (двойной точности) и long double. Также существует три формата вывода вещественных чисел, причем они не связаны с типами, а связаны с удобством представления числа. Вещественные числа могут иметь высокую точность, очень маленькое или очень большое значение. Если выполнить функции printf() с такими параметрами:

    , то на экране мы увидим следующее:

    В первом случае (%f) выводится число в обычном виде. По умолчанию точность представления числа равна шести знакам после точки.

    Во втором случае (%g) число выводится как обычно, если количество значащих нулей не больше четырех. Если количество значащих нулей четыре и больше, то число выводится в нормализованном виде (третий случай). Запись 5e-5 означает 5 * 10 -5 , что равно 0.00005. А, например, запись 4.325e+3 является экспоненциальной записью 4.325 * 10 3 , что равно 4325. Если с такой формой представления чисел вы сталкиваетесь первый раз, то почитайте дополнительные источники, например, статью в Википедии «Экспоненциальная запись».

    Четвертый формат (%e) выведет число исключительно в нормализованном виде, каким бы это вещественное число ни было.

    Если при выводе требуется округлить число до определенной точности, то перед буквой-форматом ставят точку и число-указатель точности. Например, printf(«%.2f», 0.23) выведет на экран 0.23, а не 0.230000. Когда требуется указать еще и поле, то его ширину прописывают перед точкой, например, %10.3f.

    Массивы

    Переменные, содержащие массивы, в языке программирования C объявляются, например, так:

    Если при указании количества элементов используется константа, она должна быть определена до своего использования следующим образом (чаще константы определяют вне функций):

    На самом деле #define является командой препроцессора, используемой не только для определения констант. Когда препроцессор обрабатывает исходный файл программы, он подставляет во все места, где была упомянута константа, ее значение.

    Индексация массивов в языке программирования C начинается с нуля.

    Присваивание значений элементам массивов можно произвести сразу или в процессе выполнения программы. Например:

    Когда переменная-массив объявляется и сразу определяется (как в случае vowels), то размер массива можно не указывать, т. к. он вычисляется по количеству элементов, переданных в фигурных скобках.

    Строки

    В языке программирования С нет отдельного строкового типа данных, хотя формат вывода строки есть (%s). Строки в C – это массивы символов, последний элемент которых является первым (с номером 0) символом в таблице ASCII. В этом месте таблицы стоит «ничто», имеющее символьное обозначение ‘\0’.

    С другой стороны, строки — это необычные массивы в том смысле, что работа с ними в языке программирования C несколько отличается от работы с числовыми массивами. В этом мы убедимся позже.

    Выше мы объявили и определили массив vowels. Если бы мы его определили вот так:

    то он был бы строкой. Во втором случае сами двойные кавычки «говорят» что это строка, и символ окончания строки ‘\0’ записывается в память автоматом.

    Массивы символов можно выводить на экран, просто указав имя переменной, а вот с массивами чисел такой номер не пройдет:

    Функция sizeof()

    Функция sizeof() языка C принимает в качестве аргумента константу, тип данных или переменную и возвращает количество байт, которые отведено под этот объект в памяти.

    При выводе на экран значения, возвращаемого sizeof() используется формат %lu (длинное целое без знака). Примеры:

    Задание

    Напишите программу, выводящую информацию о количестве байтов, отводимых в памяти под типы данных, которые были изучены на данном уроке. При работе с массивами символов, определяемыми как строковые литералы (в двойных кавычках), обратите внимание, что размер массива больше на единицу, чем количество видимых символов.

    C++ — Урок 002. Типы данных, Переменные и Арифметика

    Каждая переменная или выражение имеет свой тип данных, например, объявление

    указывает, что переменная some_variable имеет целочисленный тип int.

    Объявление позволяет ввести некую переменную в программу. Данная переменная будет обладать неким типом данных: целочисленный, с плавающей запятой, символьный в случае базовых типов данных, или кастомный тип (структура данных, класс). Тип переменной определяет набор операций, которые можно произвести над переменной. Объявление переменной определяет выделение области памяти, которая требуется для неё. А также значение этой переменной, которое с точки зрения компьютера будет являться последовательностью битов. Объявление переменной также несёт в себе то имя, по которому программист будет обращаться к данной переменной в программном коде.

    В выше приведенном примере имеем переменную, которая имеет базовый целочисленный тип, с которыми могут производиться арифметические действия, действия сравнения, присваивания и т.д. Обращаться в программном коде к этой переменной будем по имени some_variable.

    Фундаментальные типы данных

    C++ предоставляет следующие фундаментальные типы данных.

    void — является типом данных с пустым набором значений. Является незавершённым и не может быть установлен для объектов и переменных. Однако, позволяется использовать указатели на тип void, а также использовать void в качестве значения, возвращаемого функциями.

    nullptr

    nullptr — особый тип данных, который сам себе не является типом как таковым, поскольку его нельзя установить в качестве типа переменной, но он может использоваться в качестве нулевого указателя. Данный тип был введён в стандарте С++11 вместо определяемой реализацией нулевой макроконстанты NULL.

    boolean

    bool — логический тип данных, которые принимает значение true или false . Размер памяти, которую занимает данный тип данных может отличаться от 1 в зависимости от реализации в целевой системе. Определить размер можно с помощью оператора sizeof(bool).

    Символьные типы

    char — Символьные типы используются для представления текстовых символов. Размер символьного типа char 1 байт, что позволяет содержать 256 различных символов. Представление всех символов можно найти в таблице символов ASCII.

    Символьные типы данных делятся на три типа:

    • signed char — знаковый тип
    • unsigned char — беззнаковый тип
    • char — отдельный тип, который может быть как знаковым, так и беззнаковым, в зависимости от того, как отработает код компилятор.

    Различие в диапазоне значений, например:

    • char -128. 127
    • unsigned char 0. 255

    char может использоваться для хранения целочисленных значений, которые не превышают одного байта, но лучше использовать для целочисленных значений всё-таки тип Int. Но такое допустимо для встраиваемых систем, с жёстко ограниченным объёмом памяти.

    Также имеются особые типы символьных данных:

    • wchar_t — тип для представления символов, которым недостаточно одного байта. Это может быть 32 бита для ОС, поддерживающих UNICODE, или 16 бит в нотации Windows для UTF-16.
    • char16_t — тип для представления UTF-16, введён в стандарте C++11 .
    • char32_t — тип для представления UTF-32, введён в стандарте C++11 .

    int — целочисленный тип данных. Могут использоваться модификаторы, определяющие размер памяти, выделяемый под этот тип данных. Если нет модификаторов, то гарантируется, что размер типа данных не менее 16-ти бит. Однако, на большинстве 32/64 разрядных систем гарантируется, что занимаемый размер не менее 32-х бит.

    Модификаторы

    Знаковые модификаторы

    • signed — представление знакового типа данных (если опущено, то подразумевается по умолчанию)
    • unsigned — представление беззнакового типа данных.

    Модификаторы размера

    • short — целевой тип оптимизируется, чтобы размер был не менее 16 бит
    • long — целевой тип оптимизируется, чтобы размер был не менее 32 бит

    Модификатор long можно применять к типу данных дважды, что даёт оптимизацию занимаемого переменной пространства не менее 64 бит. Данная оптимизация введена в стандарте C++11.

    long long int

    Модификаторы размера и знаковости можно также комбинировать.

    signed long long int

    Типы данных с плавающей точкой

    • float — 32-х разрядный тип данных с плавающей точкой.
    • double — 64-х разрядный тип данных с плавающей точкой.
    • long double — расширенный тип данных с плавающей точкой, введён в стандарте C++11.

    Кстати, при разработке программного обеспечения можно заметить по использованию этих типов данных, какой разработчик начинал с чистого C, а какой начинал с C++. Поголовное использование float характерно для разработчиков, которые начинали с C, double же характерен для C++ разработчиков.

    Переменные

    Таким образом, переменные могут иметь типы данных, перечисленные выше, например:

    Инициализация переменных может производиться несколькими способами.

    Инициализация с фигурными скобками была введена в стандарте C++11. При инициализации фигурными скобками не позволяется неявное преобразование, поэтому компилятор выдаст ошибку в следующих случаях.

    Также для объявления переменных в стандарте C++11 был введён спецификатор auto , который позволяет объявлять переменную, без указания типа. В данном случае тип выводится из инициализатора, то есть значения, которое будет присвоено переменной. Таким образом auto невозможно использовать без инициализатора, то есть

    Спецификатор auto может использоваться для объявления лямбда функций, или переменных с очень сложным объявлением, что ведёт к упрощению программного кода.

    Арифметика

    Над переменными базовых типов можно выполнять различные арифметически операции:

    Также возможно использование операций сравнение:

    В дополнение к арифметическим и логическим операциям функционал C++ предлагает более специфические операции:

    Рекомендуем хостинг TIMEWEB

    Рекомендуемые статьи по этой тематике

    Часть II

    Код программы и данные, которыми программа манипулирует, записываются в память компьютера в виде последовательности битов. Бит – это мельчайший элемент компьютерной памяти, способная хранить либо 0, либо 1. На физическом уровне это соответствует электрическому напряжению, которое, как известно, либо есть , либо нет. Посмотрев на содержимое памяти компьютера, мы увидим что-нибудь вроде:

    Очень трудно придать такой последовательности смысл, но иногда нам приходится манипулировать и подобными неструктурированными данными (обычно нужда в этом возникает при программировании драйверов аппаратных устройств). С++ предоставляет набор операций для работы с битовыми данными. (Мы поговорим об этом в главе 4.)

    Как правило, на последовательность битов накладывают какую-либо структуру, группируя биты в байты и слова. Байт содержит 8 бит, а слово – 4 байта, или 32 бита. Однако определение слова может быть разным в разных операционных системах. Сейчас начинается переход к 64-битным системам, а еще недавно были распространены системы с 16-битными словами. Хотя в подавляющем большинстве систем размер байта одинаков, мы все равно будем называть эти величины машинно-зависимыми.

    Теперь мы можем говорить, например, о байте с адресом 1040 или о слове с адресом 1024 и утверждать, что байт с адресом 1032 не равен байту с адресом 1040.

    Однако мы не знаем, что же представляет собой какой-либо байт, какое-либо машинное слово. Как понять смысл тех или иных 8 бит? Для того чтобы однозначно интерпретировать значение этого байта (или слова, или другого набора битов), мы должны знать тип данных, представляемых данным байтом.

    С++ предоставляет набор встроенных типов данных: символьный, целый, вещественный – и набор составных и расширенных типов: строки, массивы, комплексные числа. Кроме того, для действий с этими данными имеется базовый набор операций: сравнение, арифметические и другие операции. Есть также операторы переходов, циклов, условные операторы. Эти элементы языка С++ составляют тот набор кирпичиков, из которых можно построить систему любой сложности. Первым шагом в освоении С++ станет изучение перечисленных базовых элементов, чему и посвящена часть II данной книги.

    Глава 3 содержит обзор встроенных и расширенных типов, а также механизмов, с помощью которых можно создавать новые типы. В основном это, конечно, механизм классов, представленный в разделе 2.3. В главе 4 рассматриваются выражения, встроенные операции и их приоритеты, преобразования типов. В главе 5 рассказывается об инструкциях языка. И наконец глава 6 представляет стандартную библиотеку С++ и контейнерные типы – вектор и ассоциативный массив.

    3. Типы данных С++

    В этой главе приводится обзор встроенных, или элементарных, типов данных языка С++. Она начинается с определения литералов, таких, как 3.14159 или pi, а затем вводится понятие переменной, или объекта, который должен принадлежать к одному из типов данных. Оставшаяся часть главы посвящена подробному описанию каждого встроенного типа. Кроме того, приводятся производные типы данных для строк и массивов, предоставляемые стандартной библиотекой С++. Хотя эти типы не являются элементарными, они очень важны для написания настоящих программ на С++, и нам хочется познакомить с ними читателя как можно раньше. Мы будем называть такие типы данных расширением базовых типов С++.

    3.1. Литералы

    В С++ имеется набор встроенных типов данных для представления целых и вещественных чисел, символов, а также тип данных “символьный массив”, который служит для хранения символьных строк. Тип char служит для хранения отдельных символов и небольших целых чисел. Он занимает один машинный байт. Типы short, int и long предназначены для представления целых чисел. Эти типы различаются только диапазоном значений, которые могут принимать числа, а конкретные размеры перечисленных типов зависят от реализации. Обычно short занимает половину машинного слова, int – одно слово, long – одно или два слова. В 32-битных системах int и long, как правило, одного размера.

    Типы float, double и long double предназначены для чисел с плавающей точкой и различаются точностью представления (количеством значащих разрядов) и диапазоном. Обычно float (одинарная точность) занимает одно машинное слово, double (двойная точность) – два, а long double (расширенная точность) – три.

    char, short, int и long вместе составляют целые типы, которые, в свою очередь, могут быть знаковыми (signed) и беззнаковыми (unsigned). В знаковых типах самый левый бит служит для хранения знака (0 – плюс, 1 – минус), а оставшиеся биты содержат значение. В беззнаковых типах все биты используются для значения. 8-битовый тип signed char может представлять значения от -128 до 127, а unsigned char – от 0 до 255.

    Когда в программе встречается некоторое число, например 1, то это число называется литералом, или литеральной константой. Константой, потому что мы не можем изменить его значение, и литералом, потому что его значение фигурирует в тексте программы. Литерал является неадресуемой величиной: хотя реально он, конечно, хранится в памяти машины, нет никакого способа узнать его адрес. Каждый литерал имеет определенный тип. Так, 0 имеет тип int, 3.14159 – тип double.

    Литералы целых типов можно записать в десятичном, восьмеричном и шестнадцатеричном виде. Вот как выглядит число 20, представленное десятичным, восьмеричным и шестнадцатеричным литералами:

    Если литерал начинается с 0, он трактуется как восьмеричный, если с 0х или 0Х, то как шестнадцатеричный. Привычная запись рассматривается как десятичное число.

    По умолчанию все целые литералы имеют тип signed int. Можно явно определить целый литерал как имеющий тип long, приписав в конце числа букву L (используется как прописная L, так и строчная l, однако для удобства чтения не следует употреблять строчную: ее легко перепутать с

    1). Буква U (или u) в конце определяет литерал как unsigned int, а две буквы – UL или LU – как тип unsigned long. Например:

    128u 1024UL 1L 8Lu

    Литералы, представляющие действительные числа, могут быть записаны как с десятичной точкой, так и в научной (экспоненциальной) нотации. По умолчанию они имеют тип double. Для явного указания типа float нужно использовать суффикс F или f, а для long double — L или l, но только в случае записи с десятичной точкой. Например:

    3.14159F 0/1f 12.345L 0.0 3el 1.0E-3E 2. 1.0L

    Слова true и false являются литералами типа bool.

    Представимые литеральные символьные константы записываются как символы в одинарных кавычках. Например:

    Специальные символы (табуляция, возврат каретки) записываются как escape-последовательности . Определены следующие такие последовательности (они начинаются с символа обратной косой черты):

    новая строка \n горизонтальная табуляция \t забой \b вертикальная табуляция \v возврат каретки \r прогон листа \f звонок \a обратная косая черта \\ вопрос \? одиночная кавычка \’ двойная кавычка \»

    escape-последовательность общего вида имеет форму \ooo, где ooo – от одной до трех восьмеричных цифр. Это число является кодом символа. Используя ASCII-код, мы можем написать следующие литералы:

    \7 (звонок) \14 (новая строка) \0 (null) \062 (‘2’)

    Символьный литерал может иметь префикс L (например, L’a’), что означает специальный тип wchar_t – двухбайтовый символьный тип, который применяется для хранения символов национальных алфавитов, если они не могут быть представлены обычным типом char, как, например, китайские или японские буквы.

    Строковый литерал – строка символов, заключенная в двойные кавычки. Такой литерал может занимать и несколько строк, в этом случае в конце строки ставится обратная косая черта. Специальные символы могут быть представлены своими escape-последовательностями. Вот примеры строковых литералов:

    «» (пустая строка) «a» «\nCC\toptions\tfile.[cC]\n» «a multi-line \ string literal signals its \ continuation with a backslash»

    Фактически строковый литерал представляет собой массив символьных констант, где по соглашению языков С и С++ последним элементом всегда является специальный символ с кодом 0 (\0).

    Литерал ‘A’ задает единственный символ А, а строковый литерал «А» – массив из двух элементов: ‘А’ и \0 (пустого символа).

    Раз существует тип wchar_t, существуют и литералы этого типа, обозначаемые, как и в случае с отдельными символами, префиксом L:

    L»a wide string literal»

    Строковый литерал типа wchar_t – это массив символов того же типа, завершенный нулем.

    Если в тесте программы идут подряд два или несколько строковых литералов (типа char или wchar_t), компилятор соединяет их в одну строку. Например, следующий текст

    породит массив из восьми символов – twosome и завершающий нулевой символ. Результат конкатенации строк разного типа не определен. Если написать:

    // this is not a good idea «two» L»some»


    то на каком-то компьютере результатом будет некоторая осмысленная строка, а на другом может оказаться нечто совсем иное. Программы, использующие особенности реализации того или иного компилятора или операционной системы, являются непереносимыми. Мы крайне не рекомендуем пользоваться такими конструкциями.

    Упражнение 3.1

    Объясните разницу в определениях следующих литералов:

    (a) ‘a’, L’a’, «a», L»a» (b) 10, 10u, 10L, 10uL, 012, 0*C (c) 3.14, 3.14f, 3.14L

    Упражнение 3.2

    Какие ошибки допущены в приведенных ниже примерах?

    (a) «Who goes with F\144rgus?\014» (b) 3.14e1L (c) «two» L»some» (d) 1024f (e) 3.14UL (f) «multiple line comment»

    3.2. Переменные

    Представим себе, что мы решаем задачу возведения 2 в степень 10. Пишем:

    Задача решена, хотя нам и пришлось неоднократно проверять, действительно ли 10 раз повторяется литерал 2. Мы не ошиблись в написании этой длинной последовательности двоек, и программа выдала правильный результат – 1024.

    Но теперь нас попросили возвести 2 в 17 степень, а потом в 23. Чрезвычайно неудобно каждый раз модифицировать текст программы! И, что еще хуже, очень просто ошибиться, написав лишнюю двойку или пропустив ее. А что делать, если нужно напечатать таблицу степеней двойки от 0 до 15? 16 раз повторить две строки, имеющие общий вид:

    где Х последовательно увеличивается на 1, а вместо отточия подставляется нужное число литералов?

    Да, мы справились с задачей. Заказчик вряд ли будет вникать в детали, удовлетворившись полученным результатом. В реальной жизни такой подход достаточно часто срабатывает, более того, бывает оправдан: задача решена далеко не самым изящным способом, зато в желаемый срок. Искать более красивый и грамотный вариант может оказаться непрактичной тратой времени.

    В данном случае “метод грубой силы” дает правильный ответ, но как же неприятно и скучно решать задачу подобным образом! Мы точно знаем, какие шаги нужно сделать, но сами эти шаги просты и однообразны.

    Привлечение более сложных механизмов для той же задачи, как правило, значительно увеличивает время подготовительного этапа. Кроме того, чем более сложные механизмы применяются, тем больше вероятность ошибок. Но даже несмотря на неизбежные ошибки и неверные ходы, применение “высоких технологий” может принести выигрыш в скорости разработки, не говоря уже о том, что эти технологии значительно расширяют наши возможности. И – что интересно! – сам процесс решения может стать привлекательным.

    Вернемся к нашему примеру и попробуем “технологически усовершенствовать” его реализацию. Мы можем воспользоваться именованным объектом для хранения значения степени, в которую нужно возвести наше число. Кроме того, вместо повторяющейся последовательности литералов применим оператор цикла. Вот как это будет выглядеть:

    value, pow, res и cnt – это переменные, которые позволяют хранить, модифицировать и извлекать значения. Оператор цикла for повторяет строку вычисления результата pow раз.

    Несомненно, мы создали гораздо более гибкую программу. Однако это все еще не функция. Чтобы получить настоящую функцию, которую можно использовать в любой программе для вычисления степени числа, нужно выделить общую часть вычислений, а конкретные значения задать параметрами.

    int pow( int val, int exp ) < for ( int res = 1; exp >0; —exp ) res = res * val; return res; >

    Теперь получить любую степень нужного числа не составит никакого труда. Вот как реализуется последняя наша задача – напечатать таблицу степеней двойки от 0 до 15:

    #include extern int pow(int,int); int main() < int val = 2; int exp = 15; cout

    Конечно, наша функция pow() все еще недостаточно обобщена и недостаточно надежна. Она не может оперировать вещественными числами, неправильно возводит числа в отрицательную степень – всегда возвращает 1. Результат возведения большого числа в большую степень может не поместиться в переменную типа int, и тогда будет возвращено некоторое случайное неправильное значение. Видите, как непросто, оказывается, писать функции, рассчитанные на широкое применение? Гораздо сложнее, чем реализовать конкретный алгоритм, направленный на решение конкретной задачи.

    3.2.1. Что такое переменная

    Переменная, или объект – это именованная область памяти, к которой мы имеем доступ из программы; туда можно помещать значения и затем извлекать их. Каждая переменная С++ имеет определенный тип, который характеризует размер и расположение этой области памяти, диапазон значений, которые она может хранить, и набор операций, применимых к этой переменной. Вот пример определения пяти объектов разных типов:

    int student_count; double salary; bool on_loan; strins street_address; char delimiter;

    Переменная, как и литерал, имеет определенный тип и хранит свое значение в некоторой области памяти. Адресуемость – вот чего не хватает литералу. С переменной ассоциируются две величины:

    • собственно значение, или r-значение (от read value – значение для чтения), которое хранится в этой области памяти и присуще как переменной, так и литералу;
    • значение адреса области памяти, ассоциированной с переменной, или l-значение (от location value – значение местоположения) – место, где хранится r-значение; присуще только объекту.

    переменная ch находится и слева и справа от символа операции присваивания. Справа расположено значение для чтения (ch и символьный литерал ‘0’): ассоциированные с переменной данные считываются из соответствующей области памяти. Слева – значение местоположения: в область памяти, соотнесенную с переменной ch, помещается результат вычитания. В общем случае левый операнд операции присваивания должен быть l-значением. Мы не можем написать следующие выражения:

    // ошибки компиляции: значения слева не являются l-значениями // ошибка: литерал — не l-значение 0 = 1; // ошибка: арифметическое выражение — не l-значение salary + salary * 0.10 = new_salary;

    Оператор определения переменной выделяет для нее память. Поскольку объект имеет только одну ассоциированную с ним область памяти, такой оператор может встретиться в программе только один раз. Если же переменная, определенная в одном исходном файле, должна быть использована в другом, появляются проблемы. Например:

    // файл module0.C // определяет объект fileName string fileName; // . присвоить fileName значение // файл module1.C // использует объект fileName // увы, не компилируется: // fileName не определен в module1.C ifstream input_file( fileName );

    С++ требует, чтобы объект был известен до первого обращения к нему. Это вызвано необходимостью гарантировать правильность использования объекта в соответствии с его типом. В нашем примере модуль module1.C вызовет ошибку компиляции, поскольку переменная fileName не определена в нем. Чтобы избежать этой ошибки, мы должны сообщить компилятору об уже определенной переменной fileName. Это делается с помощью инструкции объявления переменной:

    // файл module1.C // использует объект fileName // fileName объявляется, то есть программа получает // информацию об этом объекте без вторичного его определения extern string fileName; ifstream input_file( fileName )

    Объявление переменной сообщает компилятору, что объект с данным именем, имеющий данный тип, определен где-то в программе. Память под переменную при ее объявлении не отводится. (Ключевое слово extern рассматривается в разделе 8.2.)

    Программа может содержать сколько угодно объявлений одной и той же переменной, но определить ее можно только один раз. Такие объявления удобно помещать в заголовочные файлы, включая их в те модули, которые этого требуют. Так мы можем хранить информацию об объектах в одном месте и обеспечить удобство ее модификации в случае надобности. (Более подробно о заголовочных файлах мы поговорим в разделе 8.2.)

    3.2.2. Имя переменной

    Имя переменной, или идентификатор, может состоять из латинских букв, цифр и символа подчеркивания. Прописные и строчные буквы в именах различаются. Язык С++ не ограничивает длину идентификатора, однако пользоваться слишком длинными именами типа gosh_this_is_an_impossibly_name_to_type неудобно.

    Некоторые слова являются ключевыми в С++ и не могут быть использованы в качестве идентификаторов; в таблице 3.1 приведен их полный список.

    Таблица 3.1. Ключевые слова C++

    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 goto inline int long
    mutable namespace new operator private
    protected public register reinterpret_cast return
    short signed sizeof static static_cast
    short short signed sizeof static static_cast
    struct switch template this throw
    typedef true try typeid typename
    union volatile using virtual void

    Чтобы текст вашей программы был более понятным, мы рекомендуем придерживаться общепринятых соглашений об именах объектов:

    • имя переменной обычно пишется строчными буквами, например index (для сравнения: Index – это имя типа, а INDEX – константа, определенная с помощью директивы препроцессора #define);
    • идентификатор должен нести какой-либо смысл, поясняя назначение объекта в программе, например: birth_date или salary;

    если такое имя состоит из нескольких слов, как, например, birth_date, то принято либо разделять слова символом подчеркивания (birth_date), либо писать каждое следующее слово с большой буквы (birthDate). Замечено, что программисты, привыкшие к ОбъектноОриентированномуПодходу предпочитают выделять слова заглавными буквами, в то время как те_кто_много_писал_на_С используют символ подчеркивания. Какой из двух способов лучше – вопрос вкуса.

    3.2.3. Определение объекта

    В самом простом случае оператор определения объекта состоит из спецификатора типа и имени объекта и заканчивается точкой с запятой. Например:

    double salary; double wage; int month; int day; int year; unsigned long distance;

    В одном операторе можно определить несколько объектов одного типа. В этом случае их имена перечисляются через запятую:

    double salary, wage; int month, day, year; unsigned long distance;

    Простое определение переменной не задает ее начального значения. Если объект определен как глобальный, спецификация С++ гарантирует, что он будет инициализирован нулевым значением. Если же переменная локальная либо динамически размещаемая (с помощью оператора new), ее начальное значение не определено, то есть она может содержать некоторое случайное значение.

    Использование подобных переменных – очень распространенная ошибка, которую к тому же трудно обнаружить. Рекомендуется явно указывать начальное значение объекта, по крайней мере в тех случаях, когда неизвестно, может ли объект инициализировать сам себя. Механизм классов вводит понятие конструктора по умолчанию, который служит для присвоения значений по умолчанию. (Мы уже сказали об этом в разделе 2.3. Разговор о конструкторах по умолчанию будет продолжен немного позже, в разделах 3.11 и 3.15, где мы будем разбирать классы string и complex из стандартной библиотеки.)

    Начальное значение может быть задано прямо в операторе определения переменной. В С++ допустимы две формы инициализации переменной – явная, с использованием оператора присваивания:

    int ival = 1024; string project = «Fantasia 2000»;

    и неявная, с заданием начального значения в скобках:

    int ival( 1024 ); string project( «Fantasia 2000» );

    Оба варианта эквивалентны и задают начальные значения для целой переменной ival как 1024 и для строки project как «Fantasia 2000».

    Явную инициализацию можно применять и при определении переменных списком:

    double salary = 9999.99, wage = salary + 0.01; int month = 08; day = 07, year = 1955;

    Переменная становится видимой (и допустимой в программе) сразу после ее определения, поэтому мы могли проинициализировать переменную wage суммой только что определенной переменной salary с некоторой константой. Таким образом, определение:

    // корректно, но бессмысленно int bizarre = bizarre;

    является синтаксически допустимым, хотя и бессмысленным.

    Встроенные типы данных имеют специальный синтаксис для задания нулевого значения:

    // ival получает значение 0, а dval — 0.0 int ival = int(); double dval = double();

    В следующем определении:

    // int() применяется к каждому из 10 элементов vector ivec( 10 );

    к каждому из десяти элементов вектора применяется инициализация с помощью int(). (Мы уже говорили о классе vector в разделе 2.8. Более подробно об этом см. в разделе 3.10 и главе 6.)

    Переменная может быть инициализирована выражением любой сложности, включая вызовы функций. Например:

    #include #include double price = 109.99, discount = 0.16; double sale_price( price * discount ); string pet( «wrinkles» ); extern int get_value(); int val = get_value(); unsigned abs_val = abs( val );

    abs() – стандартная функция, возвращающая абсолютное значение параметра.

    get_value()– некоторая пользовательская функция, возвращающая целое значение.

    Упражнение 3.3

    Какие из приведенных ниже определений переменных содержат синтаксические ошибки?

    (a) int car = 1024, auto = 2048; (b) int ival = ival; (c) int ival( int() ); (d) double salary = wage = 9999.99; (e) cin >> int input_value;

    Упражнение 3.4

    Объясните разницу между l-значением и r-значением. Приведите примеры.

    Упражнение 3.5

    Найдите отличия в использовании переменных name и student в первой и второй строчках каждого примера:

    (a) extern string name; string name( «exercise 3.5a» );

    (b) extern vector students; vector students;

    Упражнение 3.6

    Какие имена объектов недопустимы в С++? Измените их так, чтобы они стали синтаксически правильными:

    (a) int double = 3.14159; (b) vector _; (c) string namespase; (d) string catch-22; (e) char 1_or_2 = ‘1’; (f) float Float = 3.14f;

    Упражнение 3.7

    В чем разница между следующими глобальными и локальными определениями переменных?

    string global_class; int global_int; int main() < int local_int; string local_class; // . >

    3.3. Указатели

    Указатели и динамическое выделение памяти были вкратце представлены в разделе 2.2. Указатель – это объект, содержащий адрес другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно указатели используются для работы с динамически созданными объектами, для построения связанных структур данных, таких, как связанные списки и иерархические деревья, и для передачи в функции больших объектов – массивов и объектов классов – в качестве параметров.

    Каждый указатель ассоциируется с некоторым типом данных, причем их внутреннее представление не зависит от внутреннего типа: и размер памяти, занимаемый объектом типа указатель, и диапазон значений у них одинаков . Разница состоит в том, как компилятор воспринимает адресуемый объект. Указатели на разные типы могут иметь одно и то же значение, но область памяти, где размещаются соответствующие типы, может быть различной:

    • указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе);
    • указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе).

    Вот несколько примеров:

    int *ip1, *ip2; complex *cp; string *pstring; vector *pvec; double *dp;

    Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long:

    В следующем случае fp интерпретируется как объект типа float, а fp2 – указатель на него:

    Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны:

    string *ps; string* ps;

    Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую:

    //внимание: ps2 не указатель на строку! string* ps, ps2;

    Можно предположить, что и ps, и ps2 являются указателями, хотя указатель – только первый из них.

    Если значение указателя равно 0, значит, он не содержит никакого адреса объекта.

    Пусть задана переменная типа int:

    Ниже приводятся примеры определения и использования указателей на int pi и pi2:

    //pi инициализирован нулевым адресом int *pi = 0; // pi2 инициализирован адресом ival int *pi2 = &ival; // правильно: pi и pi2 содержат адрес ival pi = pi2; // pi2 содержит нулевой адрес pi2 = 0;

    Указателю не может быть присвоена величина, не являющаяся адресом:

    // ошибка: pi не может принимать значение int pi = ival

    Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом объекта другого типа. Если определены следующие переменные:

    double dval; double *ps = &dval;

    то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции:

    // ошибки компиляции // недопустимое присваивание типов данных: int*

    Дело не в том, что переменная pi не может содержать адреса объекта dval – адреса объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов запрещены сознательно, потому что интерпретация объектов компилятором зависит от типа указателя на них.

    Конечно, бывают случаи, когда нас интересует само значение адреса, а не объект, на который он указывает (допустим, мы хотим сравнить этот адрес с каким-то другим). Для разрешения таких ситуаций введен специальный указатель void, который может указывать на любой тип данных, и следующие выражения будут правильны:

    Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.)

    Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных:

    int ival = 1024;, ival2 = 2048; int *pi = &ival;

    мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:

    // косвенное присваивание переменной ival значения ival2 *pi = ival2; // косвенное использование переменной ival как rvalue и lvalue *pi = abs(*pi); // ival = abs(ival); *pi = *pi + 1; // ival = ival + 1;

    Когда мы применяем операцию взятия адреса (&) к объекту типа int, то получаем результат типа int*

    Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** – это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к ppi необходимо применить дважды.

    int **ppi = π int *pi2 = *ppi; cout

    Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия:

    int i, j, k; int *pi = &i; // i = i + 2 *pi = *pi + 2; // увеличение адреса, содержащегося в pi, на 2 pi = pi + 2;

    К указателю можно прибавлять целое значение, можно также вычитать из него. Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать? Если объекты одного типа расположены в памяти друг за другом, то увеличение указателя на 1 приведет к тому, что он будет указывать на следующий объект. Поэтому арифметические действия с указателями чаще всего применяются при обработке массивов; в любых других случаях они вряд ли оправданы.

    Вот как выглядит типичный пример использования адресной арифметики при переборе элементов массива с помощью итератора:

    int ia[10]; int *iter = &ia[0]; int *iter_end = &ia[10]; while (iter != iter_end)

    Упражнение 3.8

    Даны определения переменных:

    int ival = 1024, ival2 = 2048; int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;

    Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?

    (a) ival = *pi3; (e) pi1 = *pi3; (b) *pi2 = *pi3; (f) ival = *pi1; (c) ival = pi2; (g) pi1 = ival; (d) pi2 = *pi1; (h) pi3 = &pi2;

    Упражнение 3.9

    Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код

    pi = &ival; pi = pi + 1024;

    почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?

    Упражнение 3.10

    Данная программа содержит ошибку, связанную с неправильным использованием указателей:

    В чем состоит ошибка? Как можно ее исправить?

    Упражнение 3.11

    Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована? Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной?

    3.4. Строковые типы

    В С++ поддерживаются два типа строк – встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.)

    3.4.1. Встроенный строковый тип

    Как уже было сказано, встроенный строковый тип перешел к С++ по наследству от С. Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций для манипулирования строками. Например:

    // возвращает длину строки int strlen( const char* ); // сравнивает две строки int strcmp( const char*, const char* ); // копирует одну строку в другую char* strcpy( char*, const char* );

    Стандартная библиотека С является частью библиотеки С++. Для ее использования мы должны включить заголовочный файл:

    Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал, например

    const char *st = «Цена бутылки вина\n»;

    компилятор помещает все символы строки в массив и затем присваивает st адрес первого элемента массива. Как можно работать со строкой, используя такой указатель?

    Обычно для перебора символов строки применяется адресная арифметика. Поскольку строка всегда заканчивается нулевым символом, можно увеличивать указатель на 1, пока очередным символом не станет нуль. Например:

    st разыменовывается, и получившееся значение проверяется на истинность. Любое отличное от нуля значение считается истинным, и, следовательно, цикл заканчивается, когда будет достигнут символ с кодом 0. Операция инкремента ++ прибавляет 1 к указателю st и таким образом сдвигает его к следующему символу.

    Вот как может выглядеть реализация функции, возвращающей длину строки. Отметим, что, поскольку указатель может содержать нулевое значение (ни на что не указывать), перед операцией разыменования его следует проверять:

    int string_length( const char *st )

    Строка встроенного типа может считаться пустой в двух случаях: если указатель на строку имеет нулевое значение (тогда у нас вообще нет никакой строки) или указывает на массив, состоящий из одного нулевого символа (то есть на строку, не содержащую ни одного значимого символа).

    // pc1 не адресует никакого массива символов char *pc1 = 0; // pc2 адресует нулевой символ const char *pc2 = «»;

    Для начинающего программиста использование строк встроенного типа чревато ошибками из-за слишком низкого уровня реализации и невозможности обойтись без адресной арифметики. Ниже мы покажем некоторые типичные погрешности, допускаемые новичками. Задача проста: вычислить длину строки. Первая версия неверна. Исправьте ее.

    #include const char *st = «Цена бутылки вина\n»; int main() < int len = 0; while ( st++ ) ++len; cout

    В этой версии указатель st не разыменовывается. Следовательно, на равенство 0 проверяется не символ, на который указывает st, а сам указатель. Поскольку изначально этот указатель имел ненулевое значение (адрес строки), то он никогда не станет равным нулю, и цикл будет выполняться бесконечно.

    Во второй версии программы эта погрешность устранена. Программа успешно заканчивается, однако полученный результат неправилен. Где мы не правы на этот раз?

    #include const char *st = «Цена бутылки вина\n»; int main() < int len = 0; while ( *st++ ) ++len; cout

    Ошибка состоит в том, что после завершения цикла указатель st адресует не исходный символьный литерал, а символ, расположенный в памяти после завершающего нуля этого литерала. В этом месте может находиться что угодно, и выводом программы будет случайная последовательность символов.

    Можно попробовать исправить эту ошибку:

    st = st – len; cout

    Теперь наша программа выдает что-то осмысленное, но не до конца. Ответ выглядит так:

    18: ена бутылки вина

    Мы забыли учесть, что заключительный нулевой символ не был включен в подсчитанную длину. st должен быть смещен на длину строки плюс 1. Вот, наконец, правильный оператор:

    а вот и и правильный результат:

    18: Цена бутылки вина

    Однако нельзя сказать, что наша программа выглядит элегантно. Оператор

    добавлен для того, чтобы исправить ошибку, допущенную на раннем этапе проектирования программы, – непосредственное увеличение указателя st. Этот оператор не вписывается в логику программы, и код теперь трудно понять. Исправления такого рода часто называют заплатками – нечто, призванное заткнуть дыру в существующей программе. Гораздо лучшим решением было бы пересмотреть логику. Одним из вариантов в нашем случае может быть определение второго указателя, инициализированного значением st:

    const char *p = st;

    Теперь p можно использовать в цикле вычисления длины, оставив значение st неизменным:

    3.4.2. Класс string

    Как мы только что видели, применение встроенного строкового типа чревато ошибками и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому достаточно распространена разработка собственного класса или классов для представления строкового типа – чуть ли не каждая компания, отдел или индивидуальный проект имели свою собственную реализацию строки. Да что говорить, в предыдущих двух изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости и переносимости программ. Реализация стандартного класса string стандартной библиотекой С++ призвана была положить конец этому изобретению велосипедов.

    Попробуем специфицировать минимальный набор операций, которыми должен обладать класс string:

    • инициализация массивом символов (строкой встроенного типа) или другим объектом типа string. Встроенный тип не обладает второй возможностью;
    • копирование одной строки в другую. Для встроенного типа приходится использовать функцию strcpy();
    • доступ к отдельным символам строки для чтения и записи. Во встроенном массиве для этого применяется операция взятия индекса или косвенная адресация;
    • сравнение двух строк на равенство. Для встроенного типа используется функция strcmp();
    • конкатенация двух строк, получая результат либо как третью строку, либо вместо одной из исходных. Для встроенного типа применяется функция strcat(), однако чтобы получить результат в новой строке, необходимо последовательно задействовать функции strcpy() и strcat();
    • вычисление длины строки. Узнать длину строки встроенного типа можно с помощью функции strlen();
    • возможность узнать, пуста ли строка. У встроенных строк для этой цели приходится проверять два условия:

    char str = 0; //. if ( ! str || ! *str ) return;

    Класс string стандартной библиотеки С++ реализует все перечисленные операции (и гораздо больше, как мы увидим в главе 6). В данном разделе мы научимся пользоваться основными операциями этого класса.

    Для того чтобы использовать объекты класса string, необходимо включить соответствующий заголовочный файл:

    Вот пример строки из предыдущего раздела, представленной объектом типа string и инициализированной строкой символов:

    #include string st( «Цена бутылки вина\n» );

    Длину строки возвращает функция-член size() (длина не включает завершающий нулевой символ).

    Вторая форма определения строки задает пустую строку:

    string st2; // пустая строка

    Как мы узнаем, пуста ли строка? Конечно, можно сравнить ее длину с 0:

    if ( ! st.size() ) // правильно: пустая

    Однако есть и специальный метод empty(), возвращающий true для пустой строки и false для непустой:

    if ( st.empty() ) // правильно: пустая


    Третья форма конструктора инициализирует объект типа string другим объектом того же типа:

    Строка st3 инициализируется строкой st. Как мы можем убедиться, что эти строки совпадают? Воспользуемся оператором сравнения (==):

    if ( st == st3 ) // инициализация сработала

    Как скопировать одну строку в другую? С помощью обычной операции присваивания:

    st2 = st3; // копируем st3 в st2

    Для конкатенации строк используется операция сложения (+) или операция сложения с присваиванием (+=). Пусть даны две строки:

    string s1( «hello, » ); string s2( «world\n» );

    Мы можем получить третью строку, состоящую из конкатенации первых двух, таким образом:

    string s3 = s1 + s2;

    Если же мы хотим добавить s2 в конец s1, мы должны написать:

    Операция сложения может конкатенировать объекты класса string не только между собой, но и со строками встроенного типа. Можно переписать пример, приведенный выше, так, чтобы специальные символы и знаки препинания представлялись встроенным типом, а значимые слова – объектами класса string:

    const char *pc = «, «; string s1( «hello» ); string s2( «world» ); string s3 = s1 + pc + s2 + «\n»;

    Подобные выражения работают потому, что компилятор знает, как автоматически преобразовывать объекты встроенного типа в объекты класса string. Возможно и простое присваивание встроенной строки объекту string:

    string s1; const char *pc = «a character array»; s1 = pc; // правильно

    Обратное преобразование, однако, не работает. Попытка выполнить следующую инициализацию строки встроенного типа вызовет ошибку компиляции:

    char *str = s1; // ошибка компиляции

    Чтобы осуществить такое преобразование, необходимо явно вызвать функцию-член с несколько странным названием c_str():

    char *str = s1.c_str(); // почти правильно

    Функция c_str() возвращает указатель на символьный массив, содержащий строку объекта string в том виде, в каком она находилась бы во встроенном строковом типе.

    Приведенный выше пример инициализации указателя char *str все еще не совсем корректен. c_str() возвращает указатель на константный массив, чтобы предотвратить возможность непосредственной модификации содержимого объекта через этот указатель, имеющий тип

    (В следующем разделе мы расскажем о ключевом слове const). Правильный вариант инициализации выглядит так:

    const char *str = s1.c_str(); // правильно

    К отдельным символам объекта типа string, как и встроенного типа, можно обращаться с помощью операции взятия индекса. Вот, например, фрагмент кода, заменяющего все точки символами подчеркивания:

    string str( «fa.disney.com» ); int size = str.size(); for ( int ix = 0; ix

    Вот и все, что мы хотели сказать о классе string прямо сейчас. На самом деле, этот класс обладает еще многими интересными свойствами и возможностями. Скажем, предыдущий пример реализуется также вызовом одной-единственной функции replace():

    replace( str.begin(), str.end(), ‘.’, ‘_’ );

    replace() – один из обобщенных алгоритмов, с которыми мы познакомились в разделе 2.8 и которые будут детально разобраны в главе 12. Эта функция пробегает диапазон от begin() до end(), которые возвращают указатели на начало и конец строки, и заменяет элементы, равные третьему своему параметру, на четвертый.

    Упражнение 3.12

    Найдите ошибки в приведенных ниже операторах:

    (a) char ch = «The long and winding road»; (b) int ival = &ch; (c) char *pc = &ival; (d) string st( &ch ); (e) pc = 0; (i) pc = ‘0’; (f) st = pc; (j) st = &ival; (g) ch = pc[0]; (k) ch = *pc; (h) pc = st; (l) *pc = ival;

    Упражнение 3.13

    Объясните разницу в поведении следующих операторов цикла:

    while ( st++ ) ++cnt; while ( *st++ ) ++cnt;

    Упражнение 3.14

    Даны две семантически эквивалентные программы. Первая использует встроенный строковый тип, вторая – класс string:

    Что эти программы делают? Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?

    Упражнение 3.15

    Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения

    3.5. Спецификатор const

    Возьмем следующий пример кода:

    for ( int index = 0; index

    С использованием литерала 512 связаны две проблемы. Первая состоит в легкости восприятия текста программы. Почему верхняя граница переменной цикла должна быть равна именно 512? Что скрывается за этой величиной? Она кажется случайной.

    Вторая проблема касается простоты модификации и сопровождения кода. Предположим, программа состоит из 10 000 строк, и литерал 512 встречается в 4% из них. Допустим, в 80% случаев число 512 должно быть изменено на 1024. Способны ли вы представить трудоемкость такой работы и количество ошибок, которые можно сделать, исправив не то значение?

    Обе эти проблемы решаются одновременно: нужно создать объект со значением 512. Присвоив ему осмысленное имя, например bufSize, мы сделаем программу гораздо более понятной: ясно, с чем именно сравнивается переменная цикла.

    В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления всего одного объекта! Теперь значение 512 локализовано.

    int bufSize = 512; // размер буфера ввода // . for ( int index = 0; index

    Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок – использование операции присваивания (=) вместо сравнения (==):

    // случайное изменение значения bufSize if ( bufSize = 1 ) // .

    В результате выполнения этого кода значение bufSize станет равным 1, что может привести к совершенно непредсказуемому поведению программы. Ошибки такого рода обычно очень тяжело обнаружить, поскольку они попросту не видны.

    Использование спецификатора const решает данную проблему. Объявив объект как

    const int bufSize = 512; // размер буфера ввода

    мы превращаем переменную в константу со значением 512, значение которой не может быть изменено: такие попытки пресекаются компилятором: неверное использование оператора присваивания вместо сравнения, как в приведенном примере, вызовет ошибку компиляции.

    // ошибка: попытка присваивания значения константе if ( bufSize = 0 ) .

    Раз константе нельзя присвоить значение, она должна быть инициализирована в месте своего определения. Определение константы без ее инициализации также вызывает ошибку компиляции:

    const double pi; // ошибка: неинициализированная константа

    Давайте рассуждать дальше. Явная трансформация значения константы пресекается компилятором. Но как быть с косвенной адресацией? Можно ли присвоить адрес константы некоторому указателю?

    const double minWage = 9.60; // правильно? ошибка? double *ptr = &minWage;

    Должен ли компилятор разрешить подобное присваивание? Поскольку minWage – константа, ей нельзя присвоить значение. С другой стороны, ничто не запрещает нам написать:

    *ptr += 1.40; // изменение объекта minWage!

    Как правило, компилятор не в состоянии уберечь от использования указателей и не сможет сигнализировать об ошибке в случае подобного их употребления. Для этого требуется слишком глубокий анализ логики программы. Поэтому компилятор просто запрещает присваивание адресов констант обычным указателям.

    Что же, мы лишены возможности использовать указатели на константы? Нет. Для этого существуют указатели, объявленные со спецификатором const:

    const double *cptr;

    где cptr – указатель на объект типа const double. Тонкость заключается в том, что сам указатель – не константа, а значит, мы можем изменять его значение. Например:

    const double *pc = 0; const double minWage = 9.60; // правильно: не можем изменять minWage с помощью pc pc = &minWage; double dval = 3.14; // правильно: не можем изменять minWage с помощью pc // хотя dval и не константа pc = &dval; // правильно dval = 3.14159; //правильно *pc = 3.14159; // ошибка

    Адрес константного объекта присваивается только указателю на константу. Вместе с тем, такому указателю может быть присвоен и адрес обычной переменной:

    Константный указатель не позволяет изменять адресуемый им объект с помощью косвенной адресации. Хотя dval в примере выше и не является константой, компилятор не допустит изменения переменной dval через pc. (Опять-таки потому, что он не в состоянии определить, адрес какого объекта может содержать указатель в произвольный момент выполнения программы.)

    В реальных программах указатели на константы чаще всего употребляются как формальные параметры функций. Их использование дает гарантию, что объект, переданный в функцию в качестве фактического аргумента, не будет изменен этой функцией. Например:

    // В реальных программах указатели на константы чаще всего // употребляются как формальные параметры функций int strcmp( const char *str1, const char *str2 );

    (Мы еще поговорим об указателях на константы в главе 7, когда речь пойдет о функциях.)

    Существуют и константные указатели. (Обратите внимание на разницу между константным указателем и указателем на константу!). Константный указатель может адресовать как константу, так и переменную. Например:

    int errNumb = 0; int *const currErr = &errNumb;

    Здесь curErr – константный указатель на неконстантный объект. Это значит, что мы не можем присвоить ему адрес другого объекта, хотя сам объект допускает модификацию. Вот как мог бы быть использован указатель curErr:

    Попытка присвоить значение константному указателю вызовет ошибку компиляции:

    Константный указатель на константу является объединением двух рассмотренных случаев.

    const double pi = 3.14159; const double *const pi_ptr = π

    Ни значение объекта, на который указывает pi_ptr, ни значение самого указателя не может быть изменено в программе.

    Упражнение 3.16

    Объясните значение следующих пяти определений. Есть ли среди них ошибочные?

    (a) int i; (d) int *const cpi; (b) const int ic; (e) const int *const cpic; (c) const int *pic;

    Упражнение 3.17

    Какие из приведенных определений правильны? Почему?

    (a) int i = -1; (b) const int ic = i; (c) const int *pic = ⁣ (d) int *const cpi = ⁣ (e) const int *const cpic = ⁣

    Упражнение 3.18

    Используя определения из предыдущего упражнения, укажите правильные операторы присваивания. Объясните.

    (a) i = ic; (d) pic = cpic; (b) pic = ⁣ (i) cpic = ⁣ (c) cpi = pic; (f) ic = *cpic;

    3.6. Ссылочный тип

    Ссылочный тип, иногда называемый псевдонимом, служит для задания объекту дополнительного имени. Ссылка позволяет косвенно манипулировать объектом, точно так же, как это делается с помощью указателя. Однако эта косвенная манипуляция не требует специального синтаксиса, необходимого для указателей. Обычно ссылки употребляются как формальные параметры функций. В этом разделе мы рассмотрим самостоятельное использование объектов ссылочного типа.

    Ссылочный тип обозначается указанием оператора взятия адреса (&) перед именем переменной. Ссылка должна быть инициализирована. Например:

    int ival = 1024; // правильно: refVal — ссылка на ival int &refVal = ival; // ошибка: ссылка должна быть инициализирована int &refVal2;

    Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель:

    int ival = 1024; // ошибка: refVal имеет тип int, а не int* int &refVal = &ival; int *pi = &ival; // правильно: ptrVal — ссылка на указатель int *&ptrVal2 = pi;

    Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту, которую адресует refVal.

    int min_val = 0; // ival получает значение min_val, // а не refVal меняет значение на min_val refVal = min_val;

    Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например:

    refVal += 2; прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично int ii = refVal; присваивает ii текущее значение ival, int *pi = &refVal; инициализирует pi адресом ival.

    Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например:

    // определено два объекта типа int int ival = 1024, ival2 = 2048; // определена одна ссылка и один объект int &rval = ival, rval2 = ival2; // определен один объект, один указатель и одна ссылка int inal3 = 1024, *pi = ival3, &ri = ival3; // определены две ссылки int &rval3 = ival3, &rval4 = ival2;

    Константная ссылка может быть инициализирована объектом другого типа (если, конечно, существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например:

    double dval = 3.14159; // верно только для константных ссылок const int &ir = 1024; const int &ir2 = dval; const double &dr = dval + 1.0;

    Если бы мы не указали спецификатор const, все три определения ссылок вызвали бы ошибку компиляции. Однако, причина, по которой компилятор не пропускает таких определений, неясна. Попробуем разобраться.

    Для литералов это более или менее понятно: у нас не должно быть возможности косвенно поменять значение литерала, используя указатели или ссылки. Что касается объектов другого типа, то компилятор преобразует исходный объект в некоторый вспомогательный. Например, если мы пишем:

    double dval = 1024; const int &ri = dval;

    то компилятор преобразует это примерно так:

    int temp = dval; const int &ri = temp;

    Если бы мы могли присвоить новое значение ссылке ri, мы бы реально изменили не dval, а temp. Значение dval осталось бы тем же, что совершенно неочевидно для программиста. Поэтому компилятор запрещает такие действия, и единственная возможность проинициализировать ссылку объектом другого типа – объявить ее как const.

    Вот еще один пример ссылки, который трудно понять с первого раза. Мы хотим определить ссылку на адрес константного объекта, но наш первый вариант вызывает ошибку компиляции:

    const int ival = 1024; // ошибка: нужна константная ссылка int *&pi_ref = &ival;

    Попытка исправить дело добавлением спецификатора const тоже не проходит:

    const int ival = 1024; // все равно ошибка const int *&pi_ref = &ival;

    В чем причина? Внимательно прочитав определение, мы увидим, что pi_ref является ссылкой на константный указатель на объект типа int. А нам нужен неконстантный указатель на константный объект, поэтому правильной будет следующая запись:

    const int ival = 1024; // правильно int *const &piref = &ival;

    Между ссылкой и указателем существуют два основных отличия. Во-первых, ссылка обязательно должна быть инициализирована в месте своего определения. Во-вторых, всякое изменение ссылки преобразует не ее, а тот объект, на который она ссылается. Рассмотрим на примерах. Если мы пишем:

    мы инициализируем указатель pi нулевым значением, а это значит, что pi не указывает ни на какой объект. В то же время запись

    const int &ri = 0; означает примерно следующее: int temp = 0;

    Что касается операции присваивания, то в следующем примере:

    int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; pi = pi2;

    переменная ival, на которую указывает pi, остается неизменной, а pi получает значение адреса переменной ival2. И pi, и pi2 и теперь указывают на один и тот же объект ival2.

    Если же мы работаем со ссылками:

    то само значение ival меняется, но ссылка ri по-прежнему адресует ival.

    В реальных С++ программах ссылки редко используются как самостоятельные объекты, обычно они употребляются в качестве формальных параметров функций. Например:

    // пример использования ссылок // Значение возвращается в параметре next_value bool get_next_value( int &next_value ); // перегруженный оператор Matrix operator+( const Matrix&, const Matrix& );

    Как соотносятся самостоятельные объекты-ссылки и ссылки-параметры? Если мы пишем:

    int ival; while (get_next_value( ival )) .

    это равносильно следующему определению ссылки внутри функции:

    (Подробнее использование ссылок в качестве формальных параметров функций рассматривается в главе 7.)

    Упражнение 3.19

    Есть ли ошибки в данных определениях? Поясните. Как бы вы их исправили?

    Упражнение 3.20

    Если ли среди нижеследующих операций присваивания ошибочные (используются определения из предыдущего упражнения)?

    (a) rval1 = 3.14159; (b) prval1 = prval2; (c) prval2 = rval1; (d) *prval2 = ival2;

    Упражнение 3.21

    Найдите ошибки в приведенных инструкциях:

    (a) int ival = 0; const int *pi = 0; const int &ri = 0; (b) pi = &ival; ri = &ival; pi = &rval;

    3.7. Тип bool

    Объект типа bool может принимать одно из двух значений: true и false. Например:

    // инициализация строки string search_word = get_word(); // инициализация переменной found bool found = false; string next_word; while ( cin >> next_word ) if ( next_word == search_word ) found = true; // . // сокращенная запись: if ( found == true ) if ( found ) cout

    Хотя bool относится к одному из целых типов, он не может быть объявлен как signed, unsigned, short или long, поэтому приведенное определение ошибочно:

    // ошибка short bool found = false;

    Объекты типа bool неявно преобразуются в тип int. Значение true превращается в 1, а false – в 0. Например:

    Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа bool. При этом 0 интерпретируется как false, а все остальное как true:

    // возвращает количество вхождений extern int find( const string& ); bool found = false; if ( found = find( «rosebud» )) // правильно: found == true // возвращает указатель на элемент extern int* find( int value ); if ( found = find( 1024 )) // правильно: found == true

    3.8. Перечисления

    Нередко приходится определять переменную, которая принимает значения из некоего набора. Скажем, файл открывают в любом из трех режимов: для чтения, для записи, для добавления.

    Конечно, можно определить три константы для обозначения этих режимов:

    const int input = 1; const int output = 2; const int append = 3;

    и пользоваться этими константами:

    bool open_file( string file_name, int open_mode); // . open_file( «Phoenix_and_the_Crane», append );

    Подобное решение допустимо, но не вполне приемлемо, поскольку мы не можем гарантировать, что аргумент, передаваемый в функцию open_file() равен только 1, 2 или 3.

    Использование перечислимого типа решает данную проблему. Когда мы пишем:

    мы определяем новый тип open_modes. Допустимые значения для объекта этого типа ограничены набором 1, 2 и 3, причем каждое из указанных значений имеет мнемоническое имя. Мы можем использовать имя этого нового типа для определения как объекта данного типа, так и типа формальных параметров функции:

    void open_file( string file_name, open_modes om );

    input, output и append являются элементами перечисления. Набор элементов перечисления задает допустимое множество значений для объекта данного типа. Переменная типа open_modes (в нашем примере) инициализируется одним из этих значений, ей также может быть присвоено любое из них. Например:

    open_file( «Phoenix and the Crane», append );

    Попытка присвоить переменной данного типа значение, отличное от одного из элементов перечисления (или передать его параметром в функцию), вызовет ошибку компиляции. Даже если попробовать передать целое значение, соответствующее одному из элементов перечисления, мы все равно получим ошибку:

    // ошибка: 1 не является элементом перечисления open_modes open_file( «Jonah», 1 );

    Есть способ определить переменную типа open_modes, присвоить ей значение одного из элементов перечисления и передать параметром в функцию:

    open_modes om = input; // . om = append; open_file( «TailTell», om );

    Однако получить имена таких элементов невозможно. Если мы напишем оператор вывода:

    то все равно получим:

    Эта проблема решается, если определить строковый массив, в котором элемент с индексом, равным значению элемента перечисления, будет содержать его имя. Имея такой массив, мы сможем написать:

    Кроме того, нельзя перебрать все значения перечисления:

    // не поддерживается for ( open_modes iter = input; iter != append; ++inter ) // .

    Для определения перечисления служит ключевое слово enum, а имена элементов задаются в фигурных скобках, через запятую. По умолчанию первый из них равен 0, следующий – 1 и так далее. С помощью оператора присваивания это правило можно изменить. При этом каждый следующий элемент без явно указанного значения будет на 1 больше, чем элемент, идущий перед ним в списке. В нашем примере мы явно указали значение 1 для input, при этом output и append будут равны 2 и 3. Вот еще один пример:

    // shape == 0, sphere == 1, cylinder == 2, polygon == 3 enum Forms< share, spere, cylinder, polygon >;

    Целые значения, соответствующие разным элементам одного перечисления, не обязаны отличаться. Например:

    // point2d == 2, point2w == 3, point3d == 3, point3w == 4 enum Points < point2d=2, point2w, point3d=3, point3w=4 >;

    Объект, тип которого – перечисление, можно определять, использовать в выражениях и передавать в функцию как аргумент. Подобный объект инициализируется только значением одного из элементов перечисления, и только такое значение ему присваивается – явно или как значение другого объекта того же типа. Даже соответствующие допустимым элементам перечисления целые значения не могут быть ему присвоены:

    Однако в арифметических выражениях перечисление может быть автоматически преобразовано в тип int. Например:

    const int array_size = 1024; // правильно: pt2w преобразуется int int chunk_size = array_size * pt2w;

    3.9. Тип «массив»

    Мы уже касались массивов в разделе 2.1. Массив – это набор элементов одного типа, доступ к которым производится по индексу – порядковому номеру элемента в массиве. Например:

    определяет ival как переменную типа int, а инструкция

    задает массив из десяти объектов типа int. К каждому из этих объектов, или элементов массива, можно обратиться с помощью операции взятия индекса:

    присваивает переменной ival значение элемента массива ia с индексом 2. Аналогично

    присваивает элементу с индексом 7 значение ival.

    Определение массива состоит из спецификатора типа, имени массива и размера. Размер задает количество элементов массива (не менее 1) и заключается в квадратные скобки. Размер массива нужно знать уже на этапе компиляции, а следовательно, он должен быть константным выражением, хотя не обязательно задается литералом. Вот примеры правильных и неправильных определений массивов:

    extern int get_size(); // buf_size и max_files константы const int buf_size = 512, max_files = 20; int staff_size = 27; // правильно: константа char input_buffer[ buf_size ]; // правильно: константное выражение: 20 — 3 char *fileTable[ max_files-3 ]; // ошибка: не константа double salaries[ staff_size ]; // ошибка: не константное выражение int test_scores[ get_size() ];

    Объекты buf_size и max_files являются константами, поэтому определения массивов input_buffer и fileTable правильны. А вот staff_size – переменная (хотя и инициализированная константой 27), значит, salaries[staff_size] недопустимо. (Компилятор не в состоянии найти значение переменной staff_size в момент определения массива salaries.)

    Выражение max_files-3 может быть вычислено на этапе компиляции, следовательно, определение массива fileTable[max_files-3] синтаксически правильно.

    Нумерация элементов начинается с 0, поэтому для массива из 10 элементов правильным диапазоном индексов является не 1 – 10, а 0 – 9. Вот пример перебора всех элементов массива:

    При определении массив можно явно инициализировать, перечислив значения его элементов в фигурных скобках, через запятую:

    const int array_size = 3; int ia[ array_size ] = < 0, 1, 2 >;

    Если мы явно указываем список значений, то можем не указывать размер массива: компилятор сам подсчитает количество элементов:

    // массив размера 3 int ia[] = < 0, 1, 2 >;

    Когда явно указаны и размер, и список значений, возможны три варианта. При совпадении размера и количества значений все очевидно. Если список значений короче, чем заданный размер, оставшиеся элементы массива инициализируются нулями. Если же в списке больше значений, компилятор выводит сообщение об ошибке:

    // ia ==> < 0, 1, 2, 0, 0 >const int array_size = 5; int ia[ array_size ] = < 0, 1, 2 >;


    Символьный массив может быть инициализирован не только списком символьных значений в фигурных скобках, но и строковым литералом. Однако между этими способами есть некоторая разница. Допустим,

    const char cal[] = <'C', '+', '+' >; const char cal2[] = «C++»;

    Размерность массива ca1 равна 3, массива ca2 – 4 (в строковых литералах учитывается завершающий нулевой символ). Следующее определение вызовет ошибку компиляции:

    // ошибка: строка «Daniel» состоит из 7 элементов const char ch3[ 6 ] = «Daniel»;

    Массиву не может быть присвоено значение другого массива, недопустима и инициализация одного массива другим. Кроме того, не разрешается использовать массив ссылок. Вот примеры правильного и неправильного употребления массивов:

    const int array_size = 3; int ix, jx, kx; // правильно: массив указателей типа int* int *iar [] = < &ix, &jx, &kx >; // error: массивы ссылок недопустимы int &iar[] = < ix, jx, kx >; int main() < int ia3< array_size ]; // правильно // ошибка: встроенные массивы нельзя копировать ia3 = ia; return 0; >

    Чтобы скопировать один массив в другой, придется проделать это для каждого элемента по отдельности:

    В качестве индекса массива может выступать любое выражение, дающее результат целого типа. Например:

    int someVal, get_index(); ia2[ get_index() ] = someVal;

    Подчеркнем, что язык С++ не обеспечивает контроля индексов массива – ни на этапе компиляции, ни на этапе выполнения. Программист сам должен следить за тем, чтобы индекс не вышел за границы массива. Ошибки при работе с индексом достаточно распространены. К сожалению, не так уж трудно встретить примеры программ, которые компилируются и даже работают, но тем не менее содержат фатальные ошибки, рано или поздно приводящие к краху.

    Упражнение 3.22

    Какие из приведенных определений массивов содержат ошибки? Поясните.

    (a) int ia[ buf_size ]; (d) int ia[ 2 * 7 — 14 ] (b) int ia[ get_size() ]; (e) char st[ 11 ] = «fundamental»; (c) int ia[ 4 * 7 — 14 ];

    Упражнение 3.23

    Следующий фрагмент кода должен инициализировать каждый элемент массива значением индекса. Найдите допущенные ошибки:

    3.9.1. Многомерные массивы

    В С++ есть возможность использовать многомерные массивы, при объявлении которых необходимо указать правую границу каждого измерения в отдельных квадратных скобках. Вот определение двумерного массива:

    Первая величина (4) задает количество строк, вторая (3) – количество столбцов. Объект ia определен как массив из четырех строк по три элемента в каждой. Многомерные массивы тоже могут быть инициализированы:

    Внутренние фигурные скобки, разбивающие список значений на строки, необязательны и используются, как правило, для удобства чтения кода. Приведенная ниже инициализация в точности соответствует предыдущему примеру, хотя менее понятна:

    Следующее определение инициализирует только первые элементы каждой строки. Оставшиеся элементы будут равны нулю:

    Если же опустить внутренние фигурные скобки, результат окажется совершенно иным. Все три элемента первой строки и первый элемент второй получат указанное значение, а остальные будут неявно инициализированы 0.

    При обращении к элементам многомерного массива необходимо использовать индексы для каждого измерения (они заключаются в квадратные скобки). Так выглядит инициализация двумерного массива с помощью вложенных циклов:

    является допустимой с точки зрения синтаксиса С++, однако означает совсем не то, чего ждет неопытный программист. Это отнюдь не объявление двумерного массива 1 на 2. Агрегат в квадратных скобках – это список выражений через запятую, результатом которого будет последнее значение 2 (см. оператор “запятая” в разделе 4.2). Поэтому объявление ia[1,2] эквивалентно ia[2]. Это еще одна возможность допустить ошибку.

    3.9.2. Взаимосвязь массивов и указателей

    Если мы имеем определение массива:

    то что означает простое указание его имени в программе?

    Использование идентификатора массива в программе эквивалентно указанию адреса его первого элемента:

    Аналогично обратиться к значению первого элемента массива можно двумя способами:

    // оба выражения возвращают первый элемент *ia; ia[0];

    Чтобы взять адрес второго элемента массива, мы должны написать:

    Как мы уже упоминали раньше, выражение

    также дает адрес второго элемента массива. Соответственно, его значение дают нам следующие два способа:

    Отметим разницу в выражениях:

    Операция разыменования имеет более высокий приоритет, чем операция сложения (о приоритетах операций говорится в разделе 4.13). Поэтому первое выражение сначала разыменовывает переменную ia и получает первый элемент массива, а затем прибавляет к нему 1. Второе же выражение доставляет значение второго элемента.

    Проход по массиву можно осуществлять с помощью индекса, как мы делали это в предыдущем разделе, или с помощью указателей. Например:

    #include int main() < int ia[9] = < 0, 1, 1, 2, 3, 5, 8, 13, 21 >; int *pbegin = ia; int *pend = ia + 9; while ( pbegin != pend ) < cout

    Указатель pbegin инициализируется адресом первого элемента массива. Каждый проход по циклу увеличивает этот указатель на 1, что означает смещение его на следующий элемент. Как понять, где остановиться? В нашем примере мы определили второй указатель pend и инициализировали его адресом, следующим за последним элементом массива ia. Как только значение pbegin станет равным pend, мы узнаем, что массив кончился. Перепишем эту программу так, чтобы начало и конец массива передавались параметрами в некую обобщенную функцию, которая умеет печатать массив любого размера:

    #inc1ude vo ; ++pbegin; > > int main() < int ia[9] = < 0, 1, 1, 2, 3, 5, 8, 13, 21 >; ia_print( ia, ia + 9 ); >

    Наша функция стала более универсальной, однако, она умеет работать только с массивами типа int. Есть способ снять и это ограничение: преобразовать данную функцию в шаблон (шаблоны были вкратце представлены в разделе 2.5):

    #inc1ude template vo ; ++pbegin; > >

    Теперь мы можем вызывать нашу функцию print() для печати массивов любого типа:

    Мы написали обобщенную функцию. Стандартная библиотека предоставляет набор обобщенных алгоритмов (мы уже упоминали об этом в разделе 3.4), реализованных подобным образом. Параметрами таких функций являются указатели на начало и конец массива, с которым они производят определенные действия. Вот, например, как выглядят вызовы обобщенного алгоритма сортировки:

    (Мы подробно остановимся на обобщенных алгоритмах в главе 12; в Приложении будут приведены примеры их использования.)

    В стандартной библиотеке С++ содержится набор классов, которые инкапсулируют использование контейнеров и указателей. (Об этом говорилось в разделе 2.8.) В следующем разделе мы займемся стандартным контейнерным типом vector, являющимся объектно-ориентированной реализацией массива.

    3.10. Класс vector

    Использование класса vector (см. раздел 2.8) является альтернативой применению встроенных массивов. Этот класс предоставляет гораздо больше возможностей, поэтому его использование предпочтительней. Однако встречаются ситуации, когда не обойтись без массивов встроенного типа. Одна из таких ситуаций – обработка передаваемых программе параметров командной строки, о чем мы будем говорить в разделе 7.8. Класс vector, как и класс string, является частью стандартной библиотеки С++.

    Для использования вектора необходимо включить заголовочный файл:

    Существуют два абсолютно разных подхода к использованию вектора, назовем их идиомой массива и идиомой STL. В первом случае объект класса vector используется точно так же, как массив встроенного типа. Определяется вектор заданной размерности:

    что аналогично определению массива встроенного типа:

    Для доступа к отдельным элементам вектора применяется операция взятия индекса:

    vo > ivec( e1em_size ); int ia[ e1em_size ]; for ( int ix = 0; ix

    Мы можем узнать размерность вектора, используя функцию size(), и проверить, пуст ли вектор, с помощью функции empty(). Например:

    Элементы вектора инициализируются значениями по умолчанию. Для числовых типов и указателей таким значением является 0. Если в качестве элементов выступают объекты класса, то инициатор для них задается конструктором по умолчанию (см. раздел 2.3). Однако инициатор можно задать и явно, используя форму:

    vector ivec( 10, -1 );

    Все десять элементов вектора будут равны -1.

    Массив встроенного типа можно явно инициализировать списком:

    Для объекта класса vector аналогичное действие невозможно. Однако такой объект может быть инициализирован с помощью массива встроенного типа:

    // 6 элементов ia копируются в ivec vector ivec( ia, ia+6 );

    Конструктору вектора ivec передаются два указателя – указатель на начало массива ia и на элемент, следующий за последним. В качестве списка начальных значений допустимо указать не весь массив, а некоторый его диапазон:

    // копируются 3 элемента: ia[2], ia[3], ia[4] vector ivec( &ia[ 2 ], &ia[ 5 ] );

    Еще одним отличием вектора от массива встроенного типа является возможность инициализации одного объекта типа vector другим и использования операции присваивания для копирования объектов. Например:

    Говоря об идиоме STL , мы подразумеваем совсем другой подход к использованию вектора. Вместо того чтобы сразу задать нужный размер, мы определяем пустой вектор:

    Затем добавляем к нему элементы при помощи различных функций. Например, функция push_back()вставляет элемент в конец вектора. Вот фрагмент кода, считывающего последовательность строк из стандартного ввода и добавляющего их в вектор:

    Хотя мы можем использовать операцию взятия индекса для перебора элементов вектора:

    более типичным в рамках данной идиомы будет использование итераторов:

    cout ::iterator it = text.begin(); it != text.end(); ++it ) cout

    Итератор – это класс стандартной библиотеки, фактически являющийся указателем на элемент массива.

    разыменовывает итератор и дает сам элемент вектора. Инструкция

    сдвигает указатель на следующий элемент. Не нужно смешивать эти два подхода. Если следовать идиоме STL при определении пустого вектора:

    будет ошибкой написать:

    У нас еще нет ни одного элемента вектора ivec; количество элементов выясняется с помощью функции size().

    Можно допустить и противоположную ошибку. Если мы определили вектор некоторого размера, например:

    то вставка элементов увеличивает его размер, добавляя новые элементы к существующим. Хотя это и кажется очевидным, тем не менее, начинающий программист вполне мог бы написать:

    const int size = 7; int ia[ size ] = < 0, 1, 1, 2, 3, 5, 8 >; vector ivec( size ); for ( int ix = 0; ix

    Имелась в виду инициализация вектора ivec значениями элементов ia, вместо чего получился вектор ivec размера 14.

    Следуя идиоме STL, можно не только добавлять, но и удалять элементы вектора. (Все это мы рассмотрим подробно и с примерами в главе 6.)

    Упражнение 3.24

    Имеются ли ошибки в следующих определениях?

    (c) vector ivec( ia, ia+7 );

    (d) vector svec = ivec;

    (e) vector svec( 10, string( «null» ));

    Упражнение 3.25

    Реализуйте следующую функцию:

    bool is_equa1( const int*ia, int ia_size,

    Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров “хвост” более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main(), обращающуюся к is_equal().

    3.11. Класс complex

    Класс комплексных чисел complex – еще один класс из стандартной библиотеки. Как обычно, для его использования нужно включить заголовочный файл:

    Комплексное число состоит из двух частей – вещественной и мнимой. Мнимая часть представляет собой квадратный корень из отрицательного числа. Комплексное число принято записывать в виде

    где 2 – действительная часть, а 3i – мнимая. Вот примеры определений объектов типа complex:

    // чисто мнимое число: 0 + 7-i comp1ex purei( 0, 7 ); // мнимая часть равна 0: 3 + Oi comp1ex rea1_num( 3 ); // и вещественная, и мнимая часть равны 0: 0 + 0-i comp1ex zero; // инициализация одного комплексного числа другим comp1ex purei2( purei );

    Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также определить массив элементов типа complex:

    Вот как определяются указатель и ссылка на комплексное число:

    Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.)

    3.12. Директива typedef

    Директива typedef позволяет задать синоним для встроенного либо пользовательского типа данных. Например:

    typedef double wages; typedef vector vec_int; typedef vec_int test_scores; typedef bool in_attendance; typedef int *Pint;

    Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов:

    // double hourly, weekly; wages hourly, weekly; // vector vecl( 10 ); vec_int vecl( 10 ); // vector test0( c1ass_size ); const int c1ass_size = 34; test_scores test0( c1ass_size ); // vector attendance; vector attendance( c1ass_size ); // int *table[ 10 ]; Pint table [ 10 ];

    Эта директива начинается с ключевого слова typedef, за которым идет спецификатор типа, и заканчивается идентификатором, который становится синонимом для указанного типа.

    Для чего используются имена, определенные с помощью директивы typedef? Применяя мнемонические имена для типов данных, можно сделать программу более легкой для восприятия. Кроме того, принято употреблять такие имена для сложных составных типов, в противном случае воспринимаемых с трудом (см. пример в разделе 3.14), для объявления указателей на функции и функции-члены класса (см. раздел 13.6).

    Ниже приводится пример вопроса, на который почти все дают неверный ответ. Ошибка вызвана непониманием директивы typedef как простой текстовой макроподстановки. Дано определение:

    typedef char *cstring;

    Каков тип переменной cstr в следующем объявлении:

    extern const cstring cstr;

    Ответ, который кажется очевидным:

    const char *cstr

    Однако это неверно. Спецификатор const относится к cstr, поэтому правильный ответ – константный указатель на char:

    char *const cstr;

    3.13. Спецификатор volatile

    Объект объявляется как volatile (неустойчивый, асинхронно изменяемый), если его значение может быть изменено незаметно для компилятора, например переменная, обновляемая значением системных часов. Этот спецификатор сообщает компилятору, что не нужно производить оптимизацию кода для работы с данным объектом.

    Спецификатор volatile используется подобно спецификатору const:

    volatile int disp1ay_register; volatile Task *curr_task; volatile int ixa[ max_size ]; volatile Screen bitmap_buf;

    display_register – неустойчивый объект типа int. curr_task – указатель на неустойчивый объект класса Task. ixa – неустойчивый массив целых, причем каждый элемент такого массива считается неустойчивым. bitmap_buf – неустойчивый объект класса Screen, каждый его член данных также считается неустойчивым.

    Единственная цель использования спецификатора volatile – сообщить компилятору, что тот не может определить, кто и как может изменить значение данного объекта. Поэтому компилятор не должен выполнять оптимизацию кода, использующего данный объект.

    3.14. Класс pair

    Класс pair (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл:

    pair author( «James», «Joyce» );

    создает объект author типа pair, состоящий из двух строковых значений.

    Отдельные части пары могут быть получены с помощью членов first и second:

    Если нужно определить несколько однотипных объектов этого класса, удобно использовать директиву typedef:

    typedef pair Authors; Authors proust( «marcel», «proust» ); Authors joyce( «James», «Joyce» ); Authors musil( «robert», «musi1» );

    Вот другой пример употребления пары. Первое значение содержит имя некоторого объекта, второе – указатель на соответствующий этому объекту элемент таблицы.

    class EntrySlot; extern EntrySlot* 1ook_up( string ); typedef pair SymbolEntry; SymbolEntry current_entry( «author», 1ook_up( «author»)); // . if ( EntrySlot *it = 1ook_up( «editor» ))

    (Мы вернемся к рассмотрению класса pair в разговоре о контейнерных типах в главе 6 и об обобщенных алгоритмах в главе 12.)

    3.15. Типы классов

    Механизм классов позволяет создавать новые типы данных; с его помощью введены типы string, vector, complex и pair, рассмотренные выше. В главе 2 мы рассказывали о концепциях и механизмах, поддерживающих объектный и объектно-ориентированный подход, на примере реализации класса Array. Здесь мы, основываясь на объектном подходе, создадим простой класс String, реализация которого поможет понять, в частности, перегрузку операций – мы говорили о ней в разделе 2.3. (Классы подробно рассматриваются в главах 13, 14 и 15). Мы дали краткое описание класса для того, чтобы приводить более интересные примеры. Читатель, только начинающий изучение С++, может пропустить этот раздел и подождать более систематического описания классов в следующих главах.)

    Наш класс String должен поддерживать инициализацию объектом класса String, строковым литералом и встроенным строковым типом, равно как и операцию присваивания ему значений этих типов. Мы используем для этого конструкторы класса и перегруженную операцию присваивания. Доступ к отдельным символам String будет реализован как перегруженная операция взятия индекса. Кроме того, нам понадобятся: функция size() для получения информации о длине строки; операция сравнения объектов типа String и объекта String со строкой встроенного типа; а также операции ввода/вывода нашего объекта. В заключение мы реализуем возможность доступа к внутреннему представлению нашей строки в виде строки встроенного типа.

    Определение класса начинается ключевым словом class, за которым следует идентификатор – имя класса, или типа. В общем случае класс состоит из секций, предваряемых словами public (открытая) и private (закрытая). Открытая секция, как правило, содержит набор операций, поддерживаемых классом и называемых методами или функциями-членами класса. Эти функции-члены определяют открытый интерфейс класса, другими словами, набор действий, которые можно совершать с объектами данного класса. В закрытую секцию обычно включают данные-члены, обеспечивающие внутреннюю реализацию. В нашем случае к внутренним членам относятся _string – указатель на char, а также _size типа int. _size будет хранить информацию о длине строки, а _string – динамически выделенный массив символов. Вот как выглядит определение класса:

    #inc1ude class String; istream& operator>>( istream&, String& ); ostream& operator

    Класс String имеет три конструктора. Как было сказано в разделе 2.3, механизм перегрузки позволяет определять несколько реализаций функций с одним именем, если все они различаются количеством и/или типами своих параметров. Первый конструктор

    является конструктором по умолчанию, потому что не требует явного указания начального значения. Когда мы пишем:

    для str1 вызывается такой конструктор.

    Два оставшихся конструктора имеют по одному параметру. Так, для

    String str2(«строка символов»);

    Тип вызываемого конструктора определяется типом фактического аргумента. Последний из конструкторов, String(const String&), называется копирующим, так как он инициализирует объект копией другого объекта.

    Если же написать:

    то это вызовет ошибку компиляции, потому что нет ни одного конструктора с параметром типа int.

    Объявление перегруженного оператора имеет следующий формат:

    return_type operator op (parameter_list);

    где operator – ключевое слово, а op – один из предопределенных операторов: +, =, ==, [] и так далее. (Точное определение синтаксиса см. в главе 15.) Вот объявление перегруженного оператора взятия индекса:

    Этот оператор имеет единственный параметр типа int и возвращает ссылку на char. Перегруженный оператор сам может быть перегружен, если списки параметров отдельных конкретизаций различаются. Для нашего класса String мы создадим по два различных оператора присваивания и проверки на равенство.

    Для вызова функции-члена применяются операторы доступа к членам – точка (.) или стрелка (->). Пусть мы имеем объявления объектов типа String:

    String object(«Danny»); String *ptr = new String («Anna»); String array[2]; //Вот как выглядит вызов функции size() для этих объектов: vector sizes( 3 ); // доступ к члену для objects (.); // objects имеет размер 5 sizes[ 0 ] = object.size(); // доступ к члену для pointers (->) // ptr имеет размер 4 sizes[ 1 ] = ptr->size(); // доступ к члену (.) // array[0] имеет размер 0 sizes[ 2 ] = array[0].size();

    Она возвращает соответственно 5, 4 и 0.

    Перегруженные операторы применяются к объекту так же, как обычные:

    String namel( «Yadie» ); String name2( «Yodie» ); // bool operator==(const String&) if ( namel == name2 ) return; else // String& operator=( const String& ) namel = name2;

    Объявление функции-члена должно находиться внутри определения класса, а определение функции может стоять как внутри определения класса, так и вне его. (Обе функции size() и c_str() определяются внутри класса.) Если функция определяется вне класса, то мы должны указать, кроме всего прочего, к какому классу она принадлежит. В этом случае определение функции помещается в исходный файл, допустим, String.C, а определение самого класса – в заголовочный файл (String.h в нашем примере), который должен включаться в исходный:

    // содержимое исходного файла: String.С // включение определения класса String #inc1ude «String.h» // включение определения функции strcmp() #inc1ude bool // тип возвращаемого значения String:: // класс, которому принадлежит функция operator== // имя функции: оператор равенства (const String &rhs) // список параметров

    Напомним, что strcmp() – функция стандартной библиотеки С. Она сравнивает две строки встроенного типа, возвращая 0 в случае равенства строк и ненулевое значение в случае неравенства. Условный оператор (?:) проверяет значение, стоящее перед знаком вопроса. Если оно истинно, возвращается значение выражения, стоящего слева от двоеточия, в противном случае – стоящего справа. В нашем примере значение выражения равно false, если strcmp() вернула ненулевое значение, и true – если нулевое. (Условный оператор рассматривается в разделе 4.7.)

    Операция сравнения довольно часто используется, реализующая ее функция получилась небольшой, поэтому полезно объявить эту функцию встроенной (inline). Компилятор подставляет текст функции вместо ее вызова, поэтому время на такой вызов не затрачивается. (Встроенные функции рассматриваются в разделе 7.6.) Функция-член, определенная внутри класса, является встроенной по умолчанию. Если же она определена вне класса, чтобы объявить ее встроенной, нужно употребить ключевое слово inline:

    inline bool String::operator==(const String &rhs) < // то же самое >

    Определение встроенной функции должно находиться в заголовочном файле, содержащем определение класса. Переопределив оператор == как встроенный, мы должны переместить сам текст функции из файла String.C в файл String.h.

    Ниже приводится реализация операции сравнения объекта String со строкой встроенного типа:

    inline bool String::operator==(const char *s)

    Имя конструктора совпадает с именем класса. Считается, что он не возвращает значение, поэтому не нужно задавать возвращаемое значение ни в его определении, ни в его теле. Конструкторов может быть несколько. Как и любая другая функция, они могут быть объявлены встроенными.

    Поскольку мы динамически выделяли память с помощью оператора new, необходимо освободить ее вызовом delete, когда объект String нам больше не нужен. Для этой цели служит еще одна специальная функция-член – деструктор, автоматически вызываемый для объекта в тот момент, когда этот объект перестает существовать. (См. главу 7 о времени жизни объекта.) Имя деструктора образовано из символа тильды (

    ) и имени класса. Вот определение деструктора класса String. Именно в нем мы вызываем операцию delete, чтобы освободить память, выделенную в конструкторе:

    В обоих перегруженных операторах присваивания используется специальное ключевое слово this.

    String namel( «orville» ), name2( «wilbur» );

    namel = «Orville Wright»;

    this является указателем, адресующим объект name1 внутри тела функции операции присваивания.

    this всегда указывает на объект класса, через который происходит вызов функции. Если

    то внутри size() значением this будет адрес, хранящийся в ptr. Внутри операции взятия индекса this содержит адрес obj. Разыменовывая this (использованием *this), мы получаем сам объект. (Указатель this детально описан в разделе 13.4.)

    При реализации операции присваивания довольно часто допускают одну ошибку: забывают проверить, не является ли копируемый объект тем же самым, в который происходит копирование. Мы выполним эту проверку, используя все тот же указатель this:

    Вот полный текст операции присваивания объекту String объекта того же типа:

    Операция взятия индекса практически совпадает с ее реализацией для массива Array, который мы создали в разделе 2.3:

    #include inline char& String::operator[] ( int elem ) < assert( elem >= 0 && elem

    Операторы ввода и вывода реализуются как отдельные функции, а не члены класса. (О причинах этого мы поговорим в разделе 15.2. В разделах 20.4 и 20.5 рассказывается о перегрузке операторов ввода и вывода библиотеки iostream.) Наш оператор ввода может прочесть не более 4095 символов. setw() – предопределенный манипулятор, он читает из входного потока заданное число символов минус 1, гарантируя тем самым, что мы не переполним наш внутренний буфер inBuf. (В главе 20 манипулятор setw() рассматривается детально.) Для использования манипуляторов нужно включить соответствующий заголовочный файл:

    Оператору вывода необходим доступ к внутреннему представлению строки String. Так как operator inline ostream& operator

    Ниже приводится пример программы, использующей класс String. Эта программа берет слова из входного потока и подсчитывает их общее число, а также количество слов «the» и «it» и регистрирует встретившиеся гласные.

    Протестируем программу: предложим ей абзац из детского рассказа, написанного одним из авторов этой книги (мы еще встретимся с этим рассказом в главе 6). Вот результат работы программы:

    Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, 1ike a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. «Daddy, shush, there is no such thing,» she tells him, at the same time wanting him to tell her more. Shyly, she asks, «I mean, Daddy, is there?» Слов: 65 the/The: 2 it/It: 1 согласных: 190 a: 22 e: 30 i: 24 о: 10 u: 7

    Упражнение 3.26

    В наших реализациях конструкторов и операций присваивания содержится много повторов. Попробуйте вынести повторяющийся код в отдельную закрытую функцию-член, как это было сделано в разделе 2.3. Убедитесь, что новый вариант работоспособен.

    Упражнение 3.27

    Модифицируйте тестовую программу так, чтобы она подсчитывала и согласные b, d, f, s, t.

    Упражнение 3.28

    Напишите функцию-член, подсчитывающую количество вхождений символа в строку String, используя следующее объявление:

    Упражнение 3.29

    Реализуйте оператор конкатенации строк (+) так, чтобы он конкатенировал две строки и возвращал результат в новом объекте String. Вот объявление функции:

    Типы данных и их модификаторы

    В общем случае программа C++ состоит из нескольких блоков или частей. С некоторой натяжкой можно утверждать, что таких частей четыре. При этом обязательными являются только первый и третий блоки: программа содержит блок подключения файлов и главный метод main ().

    Структура программы в C++

    1. Блок заголовков программы. Обычно в этом блоке с помощью инструкции #include подключаются внешние файлы;
    2. Блок с объявлением классов (базовых и производных), прототипами и объявлениями функций;
    3. Главный метод программы: каждая программа имеет такой метод. У метода стандартное название main()
    4. Блок с описанием функций (прототип которых указан во втором блоке).

    Ввод-вывод данных

    Для начала давайте рассмотрим простой пример. И нет, это не будет всеми любимый «Hello World!». В первом же примере мы будем считать.

    Задача: Даны переменные a и b. Необходимо найти их сумму и вывести в консоль.

    Для полного понимания, будем делать в лоб, без оптимизации кода, а уже после оптимизируем.

    #include
    using namespace std;

    int main() <
    int a; // Объявляем переменную a
    int b; // Объявляем переменную b
    cin >> a; // Вводим переменную а с клавиатуры
    cin >> b; // Вводим переменную b с клавиатуры
    int c; // Объявляем переменную с
    c = a + b; // Находим сумму
    cout

    Решений у этой задачи может быть очень много, кто-то напишет так, как мы только что это сделали, а кто-то уместит данный код в 3 строчки.

    Давайте попробуем это сделать.

    Первым делом давайте подумаем, а нужна ли нам переменная «C»? По сути нет. Мы с легкостью можем исключить ее и записать решение в cout.

    Мы сделали наш код проще и сократили его на целых 2 строчки. Разве от этого он стал непонятней? Думаю, нет.

    Теперь давайте подумаем, а можем ли мы еще больше сократить число строк? Можем. Давайте объявим переменные «a» и «b» в одну строчку. C++ нам этого делать не запрещает.

    Логически можно догадаться, что раз мы имеем право объявить переменные в одну строку, то и получить значение мы так же можем в одной, вместо двух.

    Таким образом мы уменьшили наше решение всего до трех строк.

    Пример показал нам наглядно, какими способами мы можем ввести значение в нашу программу, а какими вывести полученное значение. В ближайшие несколько десятков уроком мы будем пользоваться именно ими.

    1. cin – при помощи этой команды мы вводим значения. Мы уже разобрали на примере, что если мы пишем cin, а затем >> (больше, больше) значение, даем понять программе, что сейчас последует ввод каких-то данных;
    2. cout – при помощи этой команды мы выводим результат. Это мы так же увидели в примере, cout, а затем Tags

    Первая программа на C++, типы данных и их размер

    Что бы начать изучать C++ сначала создадим простое консольное приложение. Для этого запустите Visual C++. Выберите ‘New’ в меню ‘File’. Проверте, что бы в диалоговой панеле ‘New’ была выбрана закладка ‘Projects’. В списке типов проектов выберите ‘Win32 Console Application’. Выберите каталог для проекта( лучше оставить по умолчанию ) и имя проекта, например, ‘First’ и нажмите ‘OK’. У вас создатся ‘First classes’. После этого выберите опять ‘New’, но с закладкой ‘Files’ и выберите ‘C++ Source File’. Далее нажмите ‘OK’ и создастся файл ‘First.cpp’. Всё, теперь можно писать программу. Но перед тем, как писать программу, давайте разберёмся какие типы данных существуют в C++.

    В C++ существуют несколько часто используемых типов данных( не все ):

    1. Численные знаковые целые( int, short, char )
    2. Численные знаковые дробные( float, double, long( в С ), long double( в С ) )
    3. Численные без знаковые — все перечисленные выше типы с добавлением Unsigned
    4. Char так же может использоваться как символьный тип.

    Теперь напишем программыу, которая будет выводить размер типов данных в байтах.


      void main( void )
      <
      cout Вы можете запустить программу и увидеть результат, или посмотреть таблицу:

    Илон Маск рекомендует:  Что такое код domnode &#62;append_sibling
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL