Сайт о телевидении

Сайт о телевидении

» » Переменная int. Типы данных и операции в языке си. выражения

Переменная int. Типы данных и операции в языке си. выражения

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

  • прописные, строчные латинские буквы A, B, C…, x, y, z и знак подчеркивания;
  • арабские цифры от 0 до 9;
  • специальные знаки: { } , | , () + - / % * . \ ‘ : ? < > = ! & # ~ ; ^
  • символы пробела, табуляции и перехода на новую строку.

В тесте программы можно использовать комментарии . Если текст с двух символов «косая черта» // и заканчивается символом перехода на новую строку или заключен между символами /* и */, то компилятор его игнорирует.

Данные в языке C++

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

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

  • short - короткий;
  • long - длинный;
  • signed - знаковый;
  • unsigned - беззнаковый.

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

Переменная типа int в памяти компьютера может занимать либо 2, либо 4 байта. Это зависит разрядности процессора. По умолчанию все целые типы считаются знаковыми, то есть спецификатор signed можно не указывать. Спецификатор unsigned позволяет представлять только положительные числа. Ниже представлены некоторые диапазоны значений целого типа

Тип Диапазон Размер
int -2147483648…2147483647 4 байта
unsigned int 0…4294967295 4 байта
signed int -2147483648…2147483647 4 байта
short int -32768…32767 2 байта
long int -2147483648…2147483647 4 байта
unsigned short int 0…65535 2 байта

Вещественный тип

Число с плавающей точкой представлено в форме mE +- p, где m - мантисса (целое или дробное число с десятичной точкой), p - порядок (целое число). Обычно величины типа float занимают 4 байта, а double 8 байт. Таблица диапазонов значений вещественного типа:

float 3,4E-38…3,4E+38 4 байта
double 1,7E-308…1,7E+308 8 байт
long double 3,4E-4932…3,4E+4932 8 байт

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

Переменная типа bool может принимать только два значения true (истина) или fasle (ложь). Любоезначение, не равное нулю, интерпретируется как true. Значение false представлено в памяти как 0.

Тип void

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

Преобразование типов данных

В C++ различают два вида преобразования типов данных: явное и неявное.

  • Неявное преобразование происходит автоматически. Это выполняется во время сравнения, присваивания или вычисления выражения различных типов. Например, следующая программа выведет на консоль значение типа float.

#include "stdafx.h" #include using namespace std; int main() { int i=5; float f=10.12; cout<>void"); return 0; }

#include "stdafx.h"

#include

using namespace std ;

int main ()

int i = 5 ; float f = 10.12 ;

cout << i / f ;

system ("pause>>void" ) ;

return 0 ;

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

  • Явное преобразование в отличие от неявного осуществляется программистом. Существует несколько способов такого преобразования:
  1. Преобразование в стили C : (float ) a
  2. Преобразование в стили C++ : float ()

Также приведения типов может осуществляться при помощи следующих операций:

static_cast <> () const_cast <> () reinterpret_cast <> () dynamic_cast <> ()

static_cast <> ()

const_cast <> ()

reinterpret_cast <> ()

dynamic_cast <> ()

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

Тип static_cast <Тип> (объект);

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

Тип const_cast < Тип > (объект );

reinterpret_cast - применяется для преобразования разных типов, целых к указателю и наоборот. Если вы увидели новое слово «указатель» - не пугайтесь! это тоже тип данных, но работать с ним Мы будем не скоро. Синтаксис тут такой же как, у ранее рассмотренных операторах:

Тип reinterpret _cast < Тип > (объект );

dynamic_cast - используется для динамического преобразования типов, реализует приведение указателей или ссылок. Синтаксис:

Тип dynamic _cast < Тип > (объект );

Управляющие символы

С некоторыми из этих самых «управляющих символов» Вы уже знакомы (например, с \n ). Все они начинаются с обратного «слеша», а также обрамляются двойными кавычками.

Изображение

Шестнадцатеричный код

Наименование

Звуковой сигнал бипера

Возврат на шаг

Перевод страницы (формата)

Перевод строки

Возврат каретки

Горизонтальная табуляция

Вертикальная табуляция

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

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

[спецафикатор-класа-памяти] спецификатор-типа описатель [=инициатор] [,описатель [= инициатор] ]...

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

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

Инициатор - задает начальное значение или список начальных значений, которые (которое) присваивается переменной при объявлении.

Спецификатор класса памяти - определяется одним из четырех ключевых слов языка СИ: auto, extern, register, static, и указывает,каким образом будет распределяться память под объявляемую переменную, с одной стороны, а с другой, область видимости этой переменной, т.е., из каких частей программы можно к ней обратиться.

1.2.1 Категории типов данных

Ключевые слова для определения основных типов данных

Целые типы: Плавающие типы: char float int double short long double long signed unsigned

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

Const double A=2.128E-2; const B=286; (подразумевается const int B=286)

Примеры объявления составных данных будут рассмотрены ниже.

1.2.2. Целый тип данных

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

Таблица 6

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

Unsigned int n; unsigned int b; int c; (подразумевается signed int c); unsigned d; (подразумевается unsigned int d); signed f; (подразумевается signed int f).

Отметим, что модификатор-типа char используется для представления символа (из массива представление символов) или для объявления строковых литералов. Значением объекта типа char является код (размером 1 байт), соответствующий представляемому символу. Для представления символов русского алфавита, модификатор типа идентификатора данных имеет вид unsigned char, так как коды русских букв превышают величину 127.

Следует сделать следующее замечание: в языке СИ не определено представление в памяти и диапазон значений для идентификаторов с модификаторами-типа int и unsigned int. Размер памяти для переменной с модификатором типа signed int определяется длиной машинного слова, которое имеет различный размер на разных машинах. Так, на 16-ти разрядных машинах размер слова равен 2-м байтам, на 32-х разрядных машинах соответственно 4-м байтам, т.е. тип int эквивалентен типам short int, или long int в зависимости от архитектуры используемой ПЭВМ. Таким образом, одна и та же программа может правильно работать на одном компьютере и неправильно на другом. Для определения длины памяти занимаемой переменной можно использовать операцию sizeof языка СИ, возвращающую значение длины указанного модификатора-типа.

Например:

A = sizeof(int); b = sizeof(long int); c = sizeof(unsigned long); d = sizeof(short);

Отметим также, что восьмеричные и шестнадцатеричные константы также могут иметь модификатор unsigned. Это достигается указанием префикса u или U после константы, константа без этого префикса считается знаковой.

Например:

0xA8C (int signed); 01786l (long signed); 0xF7u (int unsigned);

1.2.3. Данные плавающего типа

Для переменных, представляющих число с плавающей точкой используются следующие модификаторы-типа: float, double, long double (в некоторых реализациях языка long double СИ отсутствует).

Величина с модификатором-типа float занимает 4 байта. Из них 1 байт отводится для знака, 8 бит для избыточной экспоненты и 23 бита для мантиссы. Отметим, что старший бит мантиссы всегда равен 1, поэтому он не заполняется, в связи с этим диапазон значений переменной с плавающей точкой приблизительно равен от 3.14E-38 до 3.14E+38.

Величина типа double занимает 8 бит в памяти. Ее формат аналогичен формату float. Биты памяти распределяются следующим образом: 1 бит для знака, 11 бит для экспоненты и 52 бита для мантиссы. С учетом опущенного старшего бита мантиссы диапазон значений равен от 1.7E-308 до 1.7E+308.

Float f, a, b; double x,y;

1.2.4. Указатели

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

спецификатор-типа [ модификатор ] * описатель.

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

В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, near, far, huge. Ключевое слово const указывает, что указатель не может быть изменен в программе. Размер переменной объявленной как указатель, зависит от архитектуры компьютера и от используемой модели памяти, для которой будет компилироваться программа. Указатели на различные типы данных не обязательно должны иметь одинаковую длину.

Для модификации размера указателя можно использовать ключевые слова near, far, huge.

Unsigned int * a; /* переменная а представляет собой указатель на тип unsigned int (целые числа без знака) */ double * x; /* переменная х указывает на тип данных с плавающей точкой удвоенной точности */ char * fuffer ; /* объявляется указатель с именем fuffer который указывает на переменную типа char */ double nomer; void *addres; addres = & nomer; (double *)addres ++; /* Переменная addres объявлена как указатель на объект любого типа. Поэтому ей можно присвоить адрес любого объекта (& - операция вычисления адреса). Однако, как было отмечено выше, ни одна арифмитическая операция не может быть выполнена над указателем, пока не будет явно определен тип данных, на которые он указывает. Это можно сделать, используя операцию приведения типа (double *) для преобразования addres к указателю на тип double, а затем увеличение адреса. */ const * dr; /* Переменная dr объявлена как указатель на константное выражение, т.е. значение указателя может изменяться в процессе выполнения программы, а величина, на которую он указывает, нет. */ unsigned char * const w = &obj. /* Переменная w объявлена как константный указатель на данные типа char unsigned. Это означает, что на протяжение всей программы w будет указывать на одну и ту же область памяти. Содержание же этой области может быть изменено. */

1.2.5. Переменные перечислимого типа

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

Объявление перечисления начинается с ключевого слова enum и имеет два формата представления.

Формат 1. enum [имя-тега-перечисления] {список-перечисления} описатель[,описатель...];

Формат 2. enum имя-тега-перечисления описатель [,описатель..];

Объявление перечисления задает тип переменной перечисления и определяет список именованных констант, называемый списком-перечисления. Значением каждого имени списка является некоторое целое число.

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

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

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

Список-перечисления содержит одну или несколько конструкций вида:

идентификатор [= константное выражение]

Каждый идентификатор именует элемент перечисления. Все идентификаторы в списке enum должны быть уникальными. В случае отсутствия константного выражения первому идентификатору соответствует значение 0, следующему идентификатору - значение 1 и т.д. Имя константы перечисления эквивалентно ее значению.

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

1. Переменная может содержать повторяющиеся значения.

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

3. Имена типов перечислений должны быть отличны от других имен типов перечислений, структур и смесей в этой же области видимости.

4. Значение может следовать за последним элементом списка перечисления.

Enum week { SUB = 0, /* 0 */ VOS = 0, /* 0 */ POND, /* 1 */ VTOR, /* 2 */ SRED, /* 3 */ HETV, /* 4 */ PJAT /* 5 */ } rab_ned ;

В данном примере объявлен перечислимый тег week, с соответствующим множеством значений, и объявлена переменная rab_ned имеющая тип week.

Во втором формате используется имя тега перечисления для ссылки на тип перечисления, определяемый где-то в другом месте. Имя тега перечисления должно относится к уже определенному тегу перечисления в пределах текущей области видимости. Так как тег перечисления объявлен где-то в другом месте, список перечисления не представлен в объявлении.

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

1.2.6. Массивы

Массивы - это группа элементов одинакового типа (double, float, int и т.п.). Из объявления массива компилятор должен получить информацию о типе элементов массива и их количестве. Объявление массива имеет два формата:

спецификатор-типа описатель [константное - выражение];

спецификатор-типа описатель ;

Описатель - это идентификатор массива.

Спецификатор-типа задает тип элементов объявляемого массива. Элементами массива не могут быть функции и элементы типа void.

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

При объявлении массив инициализируется,

Массив объявлен как формальный параметр функции,

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

Каждое константное-выражение в квадратных скобках определяет число элементов по данному измерению массива, так что объявление двухмерного массива содержит два константных-выражения, трехмерного - три и т.д. Отметим, что в языке СИ первый элемент массива имеет индекс равный 0.

Int a; /* представлено в виде матрицы a a a a a a */ double b; /* вектор из 10 элементов имеющих тип double */ int w = { { 2, 3, 4 }, { 3, 4, 8 }, { 1, 0, 9 } };

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

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

Если при обращении к некоторой функции написать s, то будет передаваться нулевая строка массива s.

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

Пример объявления символьного массива.

char str = "объявление символьного массива";

Следует учитывать, что в символьном литерале находится на один элемент больше, так как последний из элементов является управляющей последовательностью "\0".

1.2.7. Структуры

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

struct { список определений }

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

тип-данных описатель;

где тип-данных указывает тип структуры для объектов, определяемых в описателях. В простейшей форме описатели представляют собой идентификаторы или массивы.

Struct { double x,y; } s1, s2, sm; struct { int year; char moth, day; } date1, date2;

Переменные s1, s2 определяются как структуры, каждая из которых состоит из двух компонент х и у. Переменная sm определяется как массив из девяти структур. Каждая из двух переменных date1, date2 состоит из трех компонентов year, moth, day. >p>Существует и другой способ ассоциирования имени с типом структуры, он основан на использовании тега структуры. Тег структуры аналогичен тегу перечислимого типа. Тег структуры определяется следующим образом:

struct тег { список описаний; };

где тег является идентификатором.

В приведенном ниже примере идентификатор student описывается как тег структуры:

Struct student { char name; int id, age; char prp; };

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

struct тег список-идентификаторов;

struct studeut st1,st2;

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

Struct node { int data; struct node * next; } st1_node;

Тег структуры node действительно является рекурсивным, так как он используется в своем собственном описании, т.е. в формализации указателя next. Структуры не могут быть прямо рекурсивными, т.е. структура node не может содержать компоненту, являющуюся структурой node, но любая структура может иметь компоненту, являющуюся указателем на свой тип, как и сделано в приведенном примере.

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

St1.name="Иванов"; st2.id=st1.id; st1_node.data=st1.age;

1.2.8. Объединения (смеси)

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

Union { описание элемента 1; ... описание элемента n; };

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

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

Объединение применяется для следующих целей:

Инициализации используемого объекта памяти, если в каждый момент времени только один объект из многих является активным;

Интерпретации основного представления объекта одного типа, как если бы этому объекту был присвоен другой тип.

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

Union { char fio; char adres; int vozrast; int telefon; } inform; union { int ax; char al; } ua;

При использовании объекта infor типа union можно обрабатывать только тот элемент который получил значение, т.е. после присвоения значения элементу inform.fio, не имеет смысла обращаться к другим элементам. Объединение ua позволяет получить отдельный доступ к младшему ua.al и к старшему ua.al байтам двухбайтного числа ua.ax .

1.2.9. Поля битов

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

Struct { unsigned идентификатор 1: длина-поля 1; unsigned идентификатор 2: длина-поля 2; }

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

Struct { unsigned a1: 1; unsigned a2: 2; unsigned a3: 5; unsigned a4: 2; } prim;

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

1.2.10. Переменные с изменяемой структурой

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

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

Struct figure { double area,perimetr; /* общие компоненты */ int type; /* признак компонента */ union /* перечисление компонент */ { double radius; /* окружность */ double a; /* прямоугольник */ double b; /* треугольник */ } geom_fig; } fig1, fig2 ;

В общем случае каждый объект типа figure будет состоять из трех компонентов: area, perimetr, type. Компонент type называется меткой активного компонента, так как он используется для указания, какой из компонентов объединения geom_fig является активным в данный момент. Такая структура называется переменной структурой, потому что ее компоненты меняются в зависимости от значения метки активного компонента (значение type).

Отметим, что вместо компоненты type типа int, целесообразно было бы использовать перечисляемый тип. Например, такой

Enum figure_chess { CIRCLE, BOX, TRIANGLE } ;

Константы CIRCLE, BOX, TRIANGLE получат значения соответственно равные 0, 1, 2. Переменная type может быть объявлена как имеющая перечислимый тип:

enum figure_chess type;

В этом случае компилятор СИ предупредит программиста о потенциально ошибочных присвоениях, таких, например, как

figure.type = 40;

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

Struct { общие компоненты; метка активного компонента; union { описание компоненты 1 ; описание компоненты 2 ; ::: описание компоненты n ; } идентификатор-объединения; } идентификатор-структуры;

Пример определения переменной структуры с именем helth_record

Struct { /* общая информация */ char name ; /* имя */ int age; /* возраст */ char sex; /* пол */ /* метка активного компонента */ /* (семейное положение) */ enum merital_status ins; /* переменная часть */ union { /* холост */ /* нет компонент */ struct { /* состоит в браке */ char marripge_date; char spouse_name; int no_children; } marriage_info; /* разведен */ char date_divorced; } marital_info; } health_record; enum marital_status { SINGLE, /* холост */ MARRIGO, /* женат */ DIVOREED /* разведен */ } ;

Обращаться к компонентам структуры можно при помощи ссылок:

Helth_record.neme, helth_record.ins, helth_record.marriage_info.marriage_date .

1.2.11. Определение объектов и типов

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

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

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

Однако надо помнить, что некоторые комбинации модификаторов недопустимы:

Элементами массивов не могут быть функции,

Функции не могут возвращать массивы или функции.

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

Для интерпретации сложных описаний предлагается простое правило, которое звучит как "изнутри наружу", и состоит из четырех шагов.

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

2. Если они есть, то проинтерпретировать эту часть описателя и затем посмотреть налево в поиске звездочки.

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

4. Интерпретировать спецификатор типа.

Int * (* comp ) (); 6 5 3 1 2 4

В данном примере объявляется переменная comp (1), как массив из десяти (2) указателей (3) на функции (4), возвращающие указатели (5) на целые значения (6).

Char * (* (* var) ()) ; 7 6 4 2 1 3 5

Переменная var (1) объявлена как указатель (2) на функцию (3) возвращающую указатель (4) на массив (5) из 10 элементов, которые являются указателями (6) на значения типа char.

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

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

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

Typedef double (* MATH)(); /* MATH - новое имя типа, представляющее указатель на функцию, возвращающую значения типа double */ MATH cos; /* cos указатель на функцию, возвращающую значения типа double */ /* Можно провести эквивалентное объявление */ double (* cos)(); typedef char FIO /* FIO - массив из сорока символов */ FIO person; /* Переменная person - массив из сорока символов */ /* Это эквивалентно объявлению */ char person;

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

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

спецификатор-типа абстрактный-описатель;

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

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

1.2.12. Инициализация данных

При объявлении переменной ей можно присвоить начальное значение, присоединяя инициатор к описателю. Инициатор начинается со знака "=" и имеет следующие формы.

Формат 1: = инициатор;

Формат 2: = { список - инициаторов };

Формат 1 используется при инициализации переменных основных типов и указателей, а формат 2 - при инициализации составных объектов.

Переменная tol инициализируется символом "N".

const long megabute = (1024 * 1024);

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

static int b = {1,2,3,4};

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

static int b = { { 1,2 }, { 3,4 } };

При инициализации массива можно опустить одну или несколько размерностей

static int b == ".")
str[ 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; (k) ch = *pc;
(h) pc = st; (l) *pc = ival;

Упражнение 3.13

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

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

Упражнение 3.14

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

// ***** Реализация с использованием C-строк ***** #include #include
int main()
{
int errors = 0;
const char *pc = "a very long literal string"; for (int ix = 0; ix < 1000000; ++ix)
{
int len = strlen(pc);
char *pc2 = new char[ len + 1 ];
strcpy(pc2, pc);
if (strcmp(pc2, pc))
++errors; delete pc2;
}
cout << "C-строки: "
<< errors << " ошибок.\n";
} // ***** Реализация с использованием класса string ***** #include
#include
int main()
{
int errors = 0;
string str("a very long literal string"); for (int ix = 0; ix < 1000000; ++ix)
{
int len = str.size();
string str2 = str;
if (str != str2)
}
cout << "класс string: "
<< errors << " ошибок.\n;
}

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

Упражнение 3.15

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

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

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

For (int index = 0; index < 512; ++index) ... ;

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

Index < bufSize

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

Int bufSize = 512; // размер буфера ввода // ... for (int index = 0; index < bufSize; ++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; // ошибка

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

Pc = &dval;

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

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

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

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

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

Do_something(); if (*curErr) {
errorHandler();
*curErr = 0; // правильно: обнулим значение errNumb
}

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

CurErr = &myErNumb; // ошибка

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

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;

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

Int *pi = 0;

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

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

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

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

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

Int &ri = ival, &ri2 = ival2; ri = ri2;

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

Int ival; while (get_next_value(ival)) ...

Int &next_value = ival;

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

Упражнение 3.19

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

(a) int ival = 1.01; (b) int &rval1 = 1.01; (c) int &rval2 = ival; (d) int &rval3 = &ival; (e) int *pi = &ival; (f) int &rval4 = pi; (g) int &rval5 = pi*; (h) int &*prval1 = pi; (i) const int &ival2 = 1; (j) const int &*prval2 = &ival;

Упражнение 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 << "ok, мы нашли слово\n";
else cout << "нет, наше слово не встретилось.\n";

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

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

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

Bool found = false; int occurrence_count = 0; while (/* mumble */)
{
found = look_for(/* something */); // значение found преобразуется в 0 или 1
occurrence_count += found; }

Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа 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.
Использование перечислимого типа решает данную проблему. Когда мы пишем:

Enum open_modes{ input = 1, output, append };

мы определяем новый тип 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);

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

Cout << input << " " << om << endl;

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

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

Cout << open_modes_table[ input ] << " " << open_modes_table[ om ] << endl Будет выведено: input append

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

// не поддерживается 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 };

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

Void mumble() { Points pt3d = point3d; // правильно: pt2d == 3 // ошибка: pt3w инициализируется типом int Points pt3w = 3; // ошибка: polygon не входит в перечисление Points pt3w = polygon; // правильно: оба объекта типа Points pt3w = pt3d; }

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

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

3.9. Тип "массив"

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

Int ival;

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

Int ia[ 10 ];

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

Ival = ia[ 2 ];

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

Ia[ 7 ] = ival;

присваивает элементу с индексом 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 в момент определения массива salaries.)
Выражение max_files-3 может быть вычислено на этапе компиляции, следовательно, определение массива fileTable синтаксически правильно.
Нумерация элементов начинается с 0, поэтому для массива из 10 элементов правильным диапазоном индексов является не 1 – 10, а 0 – 9. Вот пример перебора всех элементов массива:

Int main() { const int array_size = 10; int ia[ array_size ]; for (int ix = 0; ix < array_size; ++ ix)
ia[ ix ] = ix;
}

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

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;
}

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

Const int array_size = 7; int ia1 = { 0, 1, 2, 3, 4, 5, 6 }; int main() {
int ia3[ array_size ]; for (int ix = 0; ix < array_size; ++ix)
ia2[ ix ] = ia1[ ix ]; 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

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

Int main() { const int array_size = 10; int ia[ array_size ]; for (int ix = 1; ix <= array_size; ++ix)
ia[ ia ] = ix; // ...
}

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

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

Int ia[ 4 ][ 3 ];

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

Int ia[ 4 ][ 3 ] = { { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }, { 9, 10, 11 } };

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

Int ia = { 0,1,2,3,4,5,6,7,8,9,10,11 };

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

Int ia[ 4 ][ 3 ] = { {0}, {3}, {6}, {9} };

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

Int ia[ 4 ][ 3 ] = { 0, 3, 6, 9 };

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

Int main() { const int rowSize = 4; const int colSize = 3; int ia[ rowSize ][ colSize ]; for (int = 0; i < rowSize; ++i)
for (int j = 0; j < colSize; ++j)
ia[ i ][ j ] = i + j j;
}

Конструкция

Ia[ 1, 2 ]

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

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

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

Int ia = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };

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

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

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

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

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

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

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

*(ia+1); ia;

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

*ia+1 и *(ia+1);

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

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

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

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

#include void ia_print(int *pbegin, int *pend) {
while (pbegin != pend) {
cout << *pbegin << " ";
++pbegin;
}
} int main()
{
int ia = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
ia_print(ia, ia + 9);
}

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

#include template void print(elemType *pbegin, elemType *pend) { while (pbegin != pend) { cout << *pbegin << " "; ++pbegin; } }

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

Int main() { int ia = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; double da = { 3.14, 6.28, 12.56, 25.12 }; string sa = { "piglet", "eeyore", "pooh" }; print(ia, ia+9);
print(da, da+4);
print(sa, sa+3);
}

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

#include int main() { int ia = { 107, 28, 3, 47, 104, 76 }; string sa = { "piglet", "eeyore", "pooh" }; sort(ia, ia+6);
sort(sa, sa+3);
};

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

3.10. Класс vector

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

#include

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

Vector< int > ivec(10);

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

Int ia[ 10 ];

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

Void simp1e_examp1e() { const int e1em_size = 10; vector< int > ivec(e1em_size); int ia[ e1em_size ]; for (int ix = 0; ix < e1em_size; ++ix)
ia[ ix ] = ivec[ ix ]; // ...
}

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

Void print_vector(vector ivec) { if (ivec.empty()) return; for (int ix=0; ix< ivec.size(); ++ix)
cout << ivec[ ix ] << " ";
}

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

Vector< int > ivec(10, -1);

Все десять элементов вектора будут равны -1.
Массив встроенного типа можно явно инициализировать списком:

Int ia[ 6 ] = { -2, -1, О, 1, 2, 1024 };

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

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

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

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

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

Vector< string > svec; void init_and_assign() { // один вектор инициализируется другим vector< string > user_names(svec); // ... // один вектор копируется в другой
svec = user_names;
}

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

Vector< string > text;

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

String word; while (cin >> word) { text.push_back(word); // ... }

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

Cout << "считаны слова: \n"; for (int ix =0; ix < text.size(); ++ix) cout << text[ ix ] << " "; cout << endl;

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

Cout << "считаны слова: \n"; for (vector::iterator it = text.begin(); it != text.end(); ++it) cout << *it << " "; cout << endl;

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

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

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

Vector ivec;

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

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

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

Vector ia(10);

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

Const int size = 7; int ia[ size ] = { 0, 1, 1, 2, 3, 5, 8 }; vector< int > ivec(size); for (int ix = 0; ix < size; ++ix) ivec.push_back(ia[ ix ]);

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

Упражнение 3.24

Имеются ли ошибки в следующих определениях?
int ia[ 7 ] = { 0, 1, 1, 2, 3, 5, 8 };

(a) vector< vector< int > > ivec;
(b) vector< int > ivec = { 0, 1, 1, 2, 3, 5, 8 };
(c) vector< int > ivec(ia, ia+7);
(d) vector< string > svec = ivec;
(e) vector< string > svec(10, string("null"));

Упражнение 3.25

Реализуйте следующую функцию:
bool is_equal(const int*ia, int ia_size,
const vector &ivec);
Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров “хвост” более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main(), обращающуюся к is_equal().

3.11. Класс complex

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

#include

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

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

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

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

Complex< double > conjugate[ 2 ] = { complex< double >(2, 3), complex< double >(2, -3) };

Complex< double > *ptr = &conjugate; complex< double > &ref = *ptr;

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< bool > attendance; vector< in_attendance > 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 (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл:

#include

Например, инструкция

Pair< string, string > author("James", "Joyce");

создает объект author типа pair, состоящий из двух строковых значений.
Отдельные части пары могут быть получены с помощью членов first и second:

String firstBook; if (Joyce.first == "James" &&
Joyce.second == "Joyce")
firstBook = "Stephen Hero";

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

Typedef pair< string, string > Authors; Authors proust("marcel", "proust"); Authors joyce("James", "Joyce"); Authors musil("robert", "musi1");

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

Class EntrySlot; extern EntrySlot* 1ook_up(string); typedef pair< string, EntrySlot* > SymbolEntry; SymbolEntry current_entry("author", 1ook_up("author"));
// ... if (EntrySlot *it = 1ook_up("editor")) {
current_entry.first = "editor";
current_entry.second = it;
}

(Мы вернемся к рассмотрению класса 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 – динамически выделенный массив символов. Вот как выглядит определение класса:

#include class String; istream& operator>>(istream&, String&);
ostream& operator<<(ostream&, const String&); class String {
public:
// набор конструкторов
// для автоматической инициализации
// String strl; // String()
// String str2("literal"); // String(const char*);
// String str3(str2); // String(const String&); String();
String(const char*);
String(const String&); // деструктор
~String(); // операторы присваивания
// strl = str2
// str3 = "a string literal" String& operator=(const String&);
String& operator=(const char*); // операторы проверки на равенство
// strl == str2;
// str3 == "a string literal"; bool operator==(const String&);
bool operator==(const char*); // перегрузка оператора доступа по индексу
// strl[ 0 ] = str2[ 0 ]; char& operator(int); // доступ к членам класса
int size() { return _size; }
char* c_str() { return _string; } private:
int _size;
char *_string;
}

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

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

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

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

Вызывается конструктор

String(const char*);

String str3(str2);

Конструктор

String(const String&);

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

String str4(1024);

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

Return_type operator op (parameter_list);

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

Char& operator (int);

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

String object("Danny");
String *ptr = new String ("Anna");
String array;
Вот как выглядит вызов функции size() для этих объектов:
vector sizes(3);

// доступ к члену для objects (.); // objects имеет размер 5 sizes[ 0 ] = object.size(); // доступ к члену для pointers (->)
// ptr имеет размер 4
sizes[ 1 ] = ptr->size(); // доступ к члену (.)
// array имеет размер 0
sizes[ 2 ] = array.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
#include "String.h" // включение определения функции strcmp()
#include
bool // тип возвращаемого значения
String:: // класс, которому принадлежит функция
operator== // имя функции: оператор равенства
(const String &rhs) // список параметров
{
if (_size != rhs._size)
return false;
return strcmp(_strinq, rhs._string) ?
false: true;
}

Напомним, что 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) { return strcmp(_string, s) ? false: true; }

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

#include // default constructor inline String::String()
{
_size = 0;
_string = 0;
} inline String::String(const char *str) { if (! str) { _size = 0; _string = 0; } else { _size = str1en(str); strcpy(_string, str); } // copy constructor
inline String::String(const String &rhs)
{
size = rhs._size;
if (! rhs._string)
_string = 0;
else {
_string = new char[ _size + 1 ];
} }

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

Inline String: :~String() { delete _string; }

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

String namel("orville"), name2("wilbur");
namel = "Orville Wright";
this является указателем, адресующим объект name1 внутри тела функции операции присваивания.
this всегда указывает на объект класса, через который происходит вызов функции. Если
ptr->size();
obj[ 1024 ];

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

Inline String& String::operator=(const char *s) { if (! s) { _size = 0; delete _string; _string = 0; } else { _size = str1en(s); delete _string; _string = new char[ _size + 1 ]; strcpy(_string, s); } return *this; }

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

Inline String& String::operator=(const String &rhs) { // в выражении // namel = *pointer_to_string // this представляет собой name1, // rhs - *pointer_to_string. if (this != &rhs) {

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

Inline String& String::operator=(const String &rhs) { if (this != &rhs) { delete _string; _size = rhs._size; if (! rhs._string)
_string = 0;
else {
_string = new char[ _size + 1 ];
strcpy(_string, rhs._string);
}
}
return *this;
}

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

#include inline char&
String::operator (int elem)
{
assert(elem >= 0 && elem < _size);
return _string[ elem ];
}

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

#include inline istream& operator>>(istream &io, String &s) { // искусственное ограничение: 4096 символов const int 1imit_string_size = 4096; char inBuf[ limit_string_size ]; // setw() входит в библиотеку iostream // он ограничивает размер читаемого блока до 1imit_string_size-l io >> setw(1imit_string_size) >> inBuf; s = mBuf; // String::operator=(const char*); return io; }

Оператору вывода необходим доступ к внутреннему представлению строки String. Так как operator<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода:

Inline ostream& operator<<(ostream& os, const String &s) { return os << s.c_str(); }

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

#include #include "String.h" int main() { int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0; // Слова "The" и "It"
// будем проверять с помощью operator==(const char*)
String but, the("the"), it("it"); // operator>>(ostream&, String&)
while (cin >> buf) {
++wdCnt; // operator<<(ostream&, const String&)
cout << buf << " "; if (wdCnt % 12 == 0)
cout << endl; // String::operator==(const String&) and
// String::operator==(const char*);
if (buf == the | | buf == "The")
++theCnt;
else
if (buf == it || buf == "It")
++itCnt; // invokes String::s-ize()
for (int ix =0; ix < buf.sizeO; ++ix)
{
// invokes String:: operator (int)
switch(buf[ ix ])
{
case "a": case "A": ++aCnt; break;
case "e": case "E": ++eCnt; break;
case "i": case "I": ++iCnt; break;
case "o": case "0": ++oCnt; break;
case "u": case "U": ++uCnt; break;
default: ++notVowe1; break;
}
}
} // operator<<(ostream&, const String&)
cout << "\n\n"
<< "Слов: " << wdCnt << "\n\n"
<< "the/The: " << theCnt << "\n"
<< "it/It: " << itCnt << "\n\n"
<< "согласных: " < << "a: " << aCnt << "\n"
<< "e: " << eCnt << "\n"
<< "i: " << ICnt << "\n"
<< "o: " << oCnt << "\n"
<< "u: " << uCnt << endl;
}

Протестируем программу: предложим ей абзац из детского рассказа, написанного одним из авторов этой книги (мы еще встретимся с этим рассказом в главе 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, используя следующее объявление:

Class String { public: // ... int count(char ch) const; // ... };

Упражнение 3.29

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

Class String { public: // ... String operator+(const String &rhs) const; // ... };