Страница:
class apple : public common (* /*...*/ *) class orange : public common (* /*...*/ *) class apple_vector : public cvector (* public:
cvector fruitbowl(100); //... apple aa; orange oo; //... fruitbowl[0] = amp;aa; fruitbowl[1] = amp;oo; *)
Однако, точный тип объекта, вошедшего в такой вмещающий класс, больше компилятору не известен. Например, в предыдущем примере вы знаете, что элемент вектора является common, но является он apple или orange? Обычно точный тип должен впоследствии быть восстановлен, чтобы обеспечить правильное использование объекта. Для этого нужно или в какой-то форме хранить информацию о типе в самом объекте, или обеспечить, чтобы во вмещающий класс помещались только объекты данного типа. Последнее легко достигается с помощью производного класса. Вы можете, например, создать вектор указателей на apple:
class apple_vector : public cvector (* public: apple* amp; elem(int i) (* return (apple* amp;) cvector::elem(i); *) //... *);
используя запись приведения к типу (тип)выражение, чтобы преобразовать common* amp; (ссылку на указатель на common), которую возвращает cvector::elem, в apple* amp;. Такое применение производных классов создает альтернативу обобщенным классам. Писать его немного труднее (если не использовать макросы таким образом, чтобы производные классы фактически реализовывали обобщенные классы, см. #7.3.5), но оно имеет то преимущество, что все производные классы совместно используют единственную копию функции базового класса. В случае обобщенных классов, таких, как vector(type), для каждого нового используемого типа должна создаваться (с помощью implement()) новая копия таких функций. Другой способ, хранение идентификации типа в каждом объекте, приводит нас к стилю программирования, который часто называют объекто-основанным или объектно-ориентированным.
1.18 Виртуальные Функции
Глава 2 Описания и Константы
В этой главе описаны основные типы (char, int, float и т.д.) и основные способы построения из них новых типов (функций, векторов, указателей и т.д.). Имя вводится в программе посредством описания, которое задает его тип и, возможно, начальное значение. Даны понятия описания, определения, области видимости имен, времени жизни объектов и типов. Описываются способы записи констант в С++, а также способы определения символических констант. Примеры просто демонстрируют характерные черты языка. Более развернутый и реалистичный пример приводится в следующей главе для знакомства с выражениями и операторами языка С++. Механизмы задания типов, определяемых пользователем, с присоединенными операциями представлены в Главах 4, 5 и 6 и здесь не упоминаются.
2.1 Описания
2.1.1 Область Видимости
2.1.2 Объекты и Адреса (Lvalue)
2.1.3 Время Жизни
2.2 Имена
2.3 Типы
2.3.1 Основные Типы
2.3.2 Неявное Преобразование Типа
2.3.3 Производные Типы
2.3.4 Тип void
2.3.5 Указатели
2.3.6 Вектора
2.3.7 Указатели и Вектора
cvector fruitbowl(100); //... apple aa; orange oo; //... fruitbowl[0] = amp;aa; fruitbowl[1] = amp;oo; *)
Однако, точный тип объекта, вошедшего в такой вмещающий класс, больше компилятору не известен. Например, в предыдущем примере вы знаете, что элемент вектора является common, но является он apple или orange? Обычно точный тип должен впоследствии быть восстановлен, чтобы обеспечить правильное использование объекта. Для этого нужно или в какой-то форме хранить информацию о типе в самом объекте, или обеспечить, чтобы во вмещающий класс помещались только объекты данного типа. Последнее легко достигается с помощью производного класса. Вы можете, например, создать вектор указателей на apple:
class apple_vector : public cvector (* public: apple* amp; elem(int i) (* return (apple* amp;) cvector::elem(i); *) //... *);
используя запись приведения к типу (тип)выражение, чтобы преобразовать common* amp; (ссылку на указатель на common), которую возвращает cvector::elem, в apple* amp;. Такое применение производных классов создает альтернативу обобщенным классам. Писать его немного труднее (если не использовать макросы таким образом, чтобы производные классы фактически реализовывали обобщенные классы, см. #7.3.5), но оно имеет то преимущество, что все производные классы совместно используют единственную копию функции базового класса. В случае обобщенных классов, таких, как vector(type), для каждого нового используемого типа должна создаваться (с помощью implement()) новая копия таких функций. Другой способ, хранение идентификации типа в каждом объекте, приводит нас к стилю программирования, который часто называют объекто-основанным или объектно-ориентированным.
1.18 Виртуальные Функции
Предположим, что мы пишем программу для изображения фигур на экране. Общие атрибуты фигуры представлены классом shape, а специальные атрибуты – специальными классами:
class shape (* point center; color col; //... public: void move(point to) (* center=to; draw(); *) point where() (* return center; *) virtual void draw(); virtual void rotate(int); //... *);
Функции, которые можно определить не зная точно определенной фигуры (например, move и where, то есть, «передвинуть» и «где»), можно описать как обычно. Остальные функции описываются как virtual, то есть такие, которые должны определяться в производном классе. Например:
class circle: public shape (* int radius; public: void draw(); void rotatte(int i) (**) //... *);
Теперь, если shape_vec – вектор фигур, то можно написать:
for (int i = 0; i«no_of_shapes; i++) shape_vec[i].rotate(45);
чтобы повернуть все фигуры на 45 градусов (и заново нарисовать)
Такой стиль особенно полезен в интерактивных программах, когда объекты разных типов одинаково обрабатываются основным
программным обеспечением. Ведь по сути дела, типичное действие пользователя – это ткнуть в какой-нибудь объект и сказать Кто ты? Что ты такое? и Делай, что надо! не давая никакой информации о типе. Программа может и должна уяснить это для себя сама.
class shape (* point center; color col; //... public: void move(point to) (* center=to; draw(); *) point where() (* return center; *) virtual void draw(); virtual void rotate(int); //... *);
Функции, которые можно определить не зная точно определенной фигуры (например, move и where, то есть, «передвинуть» и «где»), можно описать как обычно. Остальные функции описываются как virtual, то есть такие, которые должны определяться в производном классе. Например:
class circle: public shape (* int radius; public: void draw(); void rotatte(int i) (**) //... *);
Теперь, если shape_vec – вектор фигур, то можно написать:
for (int i = 0; i«no_of_shapes; i++) shape_vec[i].rotate(45);
чтобы повернуть все фигуры на 45 градусов (и заново нарисовать)
Такой стиль особенно полезен в интерактивных программах, когда объекты разных типов одинаково обрабатываются основным
программным обеспечением. Ведь по сути дела, типичное действие пользователя – это ткнуть в какой-нибудь объект и сказать Кто ты? Что ты такое? и Делай, что надо! не давая никакой информации о типе. Программа может и должна уяснить это для себя сама.
Глава 2 Описания и Константы
Совершенство достигается только к моменту краха.
С.Н. Паркинсон
В этой главе описаны основные типы (char, int, float и т.д.) и основные способы построения из них новых типов (функций, векторов, указателей и т.д.). Имя вводится в программе посредством описания, которое задает его тип и, возможно, начальное значение. Даны понятия описания, определения, области видимости имен, времени жизни объектов и типов. Описываются способы записи констант в С++, а также способы определения символических констант. Примеры просто демонстрируют характерные черты языка. Более развернутый и реалистичный пример приводится в следующей главе для знакомства с выражениями и операторами языка С++. Механизмы задания типов, определяемых пользователем, с присоединенными операциями представлены в Главах 4, 5 и 6 и здесь не упоминаются.
2.1 Описания
Прежде чем имя (идентификатор) может быть использовано в С++ программе, он должно быть описано. Это значит, что надо задать его тип, чтобы сообщить компилятору, к какого вида сущностям относится имя. Вот несколько примеров, иллюстрирующих разнообразие описаний:
char ch; int count = 1; char* name = «Bjarne»; struct complex (* float re, im; *); complex cvar; extern complex sqrt(complex); extern int error_number; typedef complex point; float real(complex* p) (* return p-»re; *); const double pi = 3.1415926535897932385; struct user;
Как можно видеть из этих примеров, описание может делать больше чем просто ассоциировать тип с именем. Большинство описаний являются также определениями то есть они также определяют для имени сущность, к которой оно относится. Для ch, count и cvar этой сущностью является соответствующий объем памяти, который должен использоваться как переменная – эта память будет выделена. Для real это заданная функция. Для constant pi это значение 3.1415926535897932385. Для complex этой сущностью является новый тип. Для point это тип complex, поэтому point становится синонимом complex. Только описания
extern complex sqrt(complex); extern int error_number; struct user;
не являются одновременно определениями. Это означает, что объект, к которому они относятся, должен быть определен где-то еще. Код (тело) функции sqrt должен задаваться неким другим описанием, память для переменной error_number типа int должна выделяться неким другим описанием, и какое-то другое описание типа user должно определять, что он из себя представляет. В С++ программе всегда должно быть только одно определение каждого имени, но описаний может быть много, и все описания должны согласовываться с типом объекта, к которому они относятся, поэтому в этом фрагменте есть две ошибки:
int count; int count; // ошибка: переопределение extern int error_number; extern int error_number; // ошибка: несоответствие типов
а в этом – ни одной (об использовании extern см. #4.2):
extern int error_number; extern int error_number;
Некоторые описания задают «значение» для сущностей, которые они определяют:
struct complex (* float re, im; *); typedef complex point; float real(complex* p) (* return p-»re *); const double pi = 3.1415926535897932385;
Для типов, функций и констант «значение» неизменно. Для неконстантных типов данных начальное значение может впоследствии изменяться:
int count = 1; char* name = «Bjarne»; //... count = 2; name = «Marian»;
Из всех определений только
char ch;
не задает значение. Всякое описание, задающее значение, является определением.
char ch; int count = 1; char* name = «Bjarne»; struct complex (* float re, im; *); complex cvar; extern complex sqrt(complex); extern int error_number; typedef complex point; float real(complex* p) (* return p-»re; *); const double pi = 3.1415926535897932385; struct user;
Как можно видеть из этих примеров, описание может делать больше чем просто ассоциировать тип с именем. Большинство описаний являются также определениями то есть они также определяют для имени сущность, к которой оно относится. Для ch, count и cvar этой сущностью является соответствующий объем памяти, который должен использоваться как переменная – эта память будет выделена. Для real это заданная функция. Для constant pi это значение 3.1415926535897932385. Для complex этой сущностью является новый тип. Для point это тип complex, поэтому point становится синонимом complex. Только описания
extern complex sqrt(complex); extern int error_number; struct user;
не являются одновременно определениями. Это означает, что объект, к которому они относятся, должен быть определен где-то еще. Код (тело) функции sqrt должен задаваться неким другим описанием, память для переменной error_number типа int должна выделяться неким другим описанием, и какое-то другое описание типа user должно определять, что он из себя представляет. В С++ программе всегда должно быть только одно определение каждого имени, но описаний может быть много, и все описания должны согласовываться с типом объекта, к которому они относятся, поэтому в этом фрагменте есть две ошибки:
int count; int count; // ошибка: переопределение extern int error_number; extern int error_number; // ошибка: несоответствие типов
а в этом – ни одной (об использовании extern см. #4.2):
extern int error_number; extern int error_number;
Некоторые описания задают «значение» для сущностей, которые они определяют:
struct complex (* float re, im; *); typedef complex point; float real(complex* p) (* return p-»re *); const double pi = 3.1415926535897932385;
Для типов, функций и констант «значение» неизменно. Для неконстантных типов данных начальное значение может впоследствии изменяться:
int count = 1; char* name = «Bjarne»; //... count = 2; name = «Marian»;
Из всех определений только
char ch;
не задает значение. Всякое описание, задающее значение, является определением.
2.1.1 Область Видимости
Описание вводит имя в области видимости. То есть, имя может использоваться только в определенной части программы. Для имени, описанного в функции (такое имя часто называют локальным), эта область видимости простирается от точки описания до конца блока, в котором появилось описание. Для имени не в функции и не в классе (называемого часто глобально видимым именем) область видимости простирается от точки описания до конца файла, в котором появилось описание. Описание имени в блоке может скрывать (прятать) описание во внутреннем блоке или глобальное имя. Это значит, что можно переопределять имя внутри блока для ссылки на другой объект. После выхода из блока имя вновь обретает свое прежнее значение. Например:
int x; // глобальное x
f() (* int x; // локальное x прячет глобальное x x = 1; // присвоить локальному x (* int x; // прячет первое локальное x x = 2; // присвоить второму локальному x *) x = 3; // присвоить первому локальному x *)
int* p = amp;x; // взять адрес глобального x
Сокрытие имен неизбежно при написании больших программ. Однако читающий человек легко может не заметить, что имя скрыто, и некоторые ошибки, возникающие вследствие этого,
очень трудно обнаружить, главным образом потому, что они редкие. Значит сокрытие имен следует минимизировать. Использование для глобальных переменных имен вроде i или x напрашиваемся на неприятности.
С помощью применения операции разрешения области видимости :: можно использовать скрытое глобальное имя. Например:
int x;
f() (* int x = 1; // скрывает глобальное x ::x = 2; // присваивает глобальному x *)
Но возможности использовать скрытое локальное имя нет.
Область видимости имени начинается в точке описания. Это означает, что имя можно использовать даже для задания его собственного значения. Например:
int x;
f() (* int x = x; // извращение *)
Это не является недопустимым, хотя и бессмысленно, и компилятор предупредит, что x «used before set» («использовано до того, как задано»), если вы попробуете так сделать. Можно, напротив, не применяя операцию ::, использовать одно имя для ссылки на два различных объекта в блоке. Например:
int x;
f() // извращение (* int y = x; // глобальное x int x = 22; y = x; // локальное x *)
Переменная y инициализируется значением глобального x, 11, а затем ему присваивается значение локальной переменной x, 22.
Имена параметров функции считаются описанными в самом внешнем блоке функции, поэтому
f(int x) (* int x; // ошибка *)
содержит ошибку, так как x определено дважды в одной и той же области видимости.
int x; // глобальное x
f() (* int x; // локальное x прячет глобальное x x = 1; // присвоить локальному x (* int x; // прячет первое локальное x x = 2; // присвоить второму локальному x *) x = 3; // присвоить первому локальному x *)
int* p = amp;x; // взять адрес глобального x
Сокрытие имен неизбежно при написании больших программ. Однако читающий человек легко может не заметить, что имя скрыто, и некоторые ошибки, возникающие вследствие этого,
очень трудно обнаружить, главным образом потому, что они редкие. Значит сокрытие имен следует минимизировать. Использование для глобальных переменных имен вроде i или x напрашиваемся на неприятности.
С помощью применения операции разрешения области видимости :: можно использовать скрытое глобальное имя. Например:
int x;
f() (* int x = 1; // скрывает глобальное x ::x = 2; // присваивает глобальному x *)
Но возможности использовать скрытое локальное имя нет.
Область видимости имени начинается в точке описания. Это означает, что имя можно использовать даже для задания его собственного значения. Например:
int x;
f() (* int x = x; // извращение *)
Это не является недопустимым, хотя и бессмысленно, и компилятор предупредит, что x «used before set» («использовано до того, как задано»), если вы попробуете так сделать. Можно, напротив, не применяя операцию ::, использовать одно имя для ссылки на два различных объекта в блоке. Например:
int x;
f() // извращение (* int y = x; // глобальное x int x = 22; y = x; // локальное x *)
Переменная y инициализируется значением глобального x, 11, а затем ему присваивается значение локальной переменной x, 22.
Имена параметров функции считаются описанными в самом внешнем блоке функции, поэтому
f(int x) (* int x; // ошибка *)
содержит ошибку, так как x определено дважды в одной и той же области видимости.
2.1.2 Объекты и Адреса (Lvalue)
Можно назначать и использовать переменные, не имеющие имен, и можно осуществлять присваивание выражениям странного вида (например, *p[a+10]=7). Следовательно, есть потребность в имени «нечто в памяти». Вот соответствующая цитата из справочного руководства по С++: "Объект есть область памяти.
lvalue есть выражение, ссылающееся на объект" (#с.5). Слово «lvalue» первоначально было придумано для значения «нечто, что может стоять в левой части присваивания». Однако не всякое lvalue можно использовать в левой части присваивания; бывают lvalue, ссылающиеся на константу (см. #2.4).
lvalue есть выражение, ссылающееся на объект" (#с.5). Слово «lvalue» первоначально было придумано для значения «нечто, что может стоять в левой части присваивания». Однако не всякое lvalue можно использовать в левой части присваивания; бывают lvalue, ссылающиеся на константу (см. #2.4).
2.1.3 Время Жизни
Если программист не указал иного, то объект создается, когда встречается его описание, и уничтожается, когда его имя выходит из области видимости, Объекты с глобальными именами создаются и инициализируются один раз (только) и «живут» до завершения программы. Объекты, определенные описанием с ключевым словом static, ведут себя так же. Например*:
– * Команда #include «stream.h» была выброшена из примеров в этой главе для экономии места. Она необходима в примерах, производящих вывод, чтобы они были полными. (прим. автора)
int a = 1;
void f() (* int b = 1; // инициализируется при каждом // вызове f() static int c = 1; // инициализируется только один раз cout «„ " a = " «« a++ «« " b = " «« b++ «« " c = " «« c++ «« «\n“; *)
main() (* while (a « 4) f(); *)
производит вывод
a = 1 b = 1 c = 1 a = 2 b = 1 c = 2 a = 3 b = 1 c = 3
Не инициализированная явно статическая (static) переменная неявно инициализируется нулем.
С помощью операций new и delete программист может также создавать объекты, время жизни которых управляется непосредственно, см. #3.2.4.
– * Команда #include «stream.h» была выброшена из примеров в этой главе для экономии места. Она необходима в примерах, производящих вывод, чтобы они были полными. (прим. автора)
int a = 1;
void f() (* int b = 1; // инициализируется при каждом // вызове f() static int c = 1; // инициализируется только один раз cout «„ " a = " «« a++ «« " b = " «« b++ «« " c = " «« c++ «« «\n“; *)
main() (* while (a « 4) f(); *)
производит вывод
a = 1 b = 1 c = 1 a = 2 b = 1 c = 2 a = 3 b = 1 c = 3
Не инициализированная явно статическая (static) переменная неявно инициализируется нулем.
С помощью операций new и delete программист может также создавать объекты, время жизни которых управляется непосредственно, см. #3.2.4.
2.2 Имена
Имя (идентификатор) состоит из последовательности букв и цифр. Первый символ должен быть буквой. Символ подчерка _ считается буквой. С++ не налагает ограничений на число символов в имени, но некоторые части реализации находятся вне ведения автора компилятора (в частности, загрузчик), и они, к сожалению, такие ограничения налагают. Некоторые среды выполнения также делают необходимым расширить или ограничить набор символов, допустимых в идентификаторе. Расширения (например, при допущении в именах символа $) порождают непереносимые программы. В качестве имени не могут использоваться ключевые слова С++ (см. #с.2.3). Примеры имен:
hello this_is_a_most_unusially_long_name DEFINED foO bAr u_name HorseSense var0 var1 CLASS _class ___
Примеры последовательностей символов, которые не могут использоваться как идентификаторы:
012 a fool $sys class 3var pay.due foo~bar .name if
Буквы в верхнем и нижнем регистрах считаются различными, поэтому Count и count – различные имена, но вводить имена, лишь незначительно отличающиеся друг от друга, нежелательно. Имена, начинающиеся с подчерка, по традиции используются для специальных средств среды выполнения, поэтому использовать такие имена в прикладных программах нежелательно.
Во время чтения программы компилятор всегда ищет наиболее длинную строку, составляющую имя, поэтому var10 – это оно имя, а не имя var, за которым следует число 10, и elseif – одно имя, а не ключевое слово else, после которого стоит ключевое слово if.
hello this_is_a_most_unusially_long_name DEFINED foO bAr u_name HorseSense var0 var1 CLASS _class ___
Примеры последовательностей символов, которые не могут использоваться как идентификаторы:
012 a fool $sys class 3var pay.due foo~bar .name if
Буквы в верхнем и нижнем регистрах считаются различными, поэтому Count и count – различные имена, но вводить имена, лишь незначительно отличающиеся друг от друга, нежелательно. Имена, начинающиеся с подчерка, по традиции используются для специальных средств среды выполнения, поэтому использовать такие имена в прикладных программах нежелательно.
Во время чтения программы компилятор всегда ищет наиболее длинную строку, составляющую имя, поэтому var10 – это оно имя, а не имя var, за которым следует число 10, и elseif – одно имя, а не ключевое слово else, после которого стоит ключевое слово if.
2.3 Типы
Каждое имя (идентификатор) в С++ программе имеет ассоциированный с ним тип. Этот тип определяет, какие операции моно применять к имени (то есть к объекту, на который оно ссылается), и как эти операции интерпретируются. Например:
int error number; float real(complex* p);
Поскольку error_number описано как int, его можно присваивать, использовать в арифметических выражениях и т.д. Тогда как функция real может вызываться с адресом complex в качестве параметра. Можно взять адрес любого из них. Некоторые имена, вроде int и complex, являются именами типов. Обычно имя типа используется в описании для спецификации другого имени. Единственные отличные от этого действия над именем типа – это sizeof (для определения количества памяти, которая требуется для хранения объекта типа) и new (для размещения объекта типа в свободной памяти). Например:
main() (* int* p = new int; cout «„ "sizeof(int) = " «« sizeof(int) «\n“; *)
Имя типа можно также использовать для задания явного преобразования одного типа в другой, например:
float f; char* p; //... long ll = long(p); // преобразует p в long int i = int(f); // преобразует f в int
int error number; float real(complex* p);
Поскольку error_number описано как int, его можно присваивать, использовать в арифметических выражениях и т.д. Тогда как функция real может вызываться с адресом complex в качестве параметра. Можно взять адрес любого из них. Некоторые имена, вроде int и complex, являются именами типов. Обычно имя типа используется в описании для спецификации другого имени. Единственные отличные от этого действия над именем типа – это sizeof (для определения количества памяти, которая требуется для хранения объекта типа) и new (для размещения объекта типа в свободной памяти). Например:
main() (* int* p = new int; cout «„ "sizeof(int) = " «« sizeof(int) «\n“; *)
Имя типа можно также использовать для задания явного преобразования одного типа в другой, например:
float f; char* p; //... long ll = long(p); // преобразует p в long int i = int(f); // преобразует f в int
2.3.1 Основные Типы
В С++ есть набор основных типов, которые соответствуют наиболее общим основным единицам памяти компьютера и наиболее общим основным способам их использования:
char short int int long int
для представления целых различных размеров,
float double
для представления чисел с плавающей точкой,
unsigned char unsigned short int unsigned int unsigned long int
для представления беззнаковых целых, логических значений, битовых массивов и т.п. Для большей компактности записи можно опускать int в комбинациях из нескольких слов, что не меняет смысла. Так, long означает long int, и unsigned тип означает тип unsigned int. В общем, когда в описании опущен тип, он предполагается int. Например:
const a = 1; static x;
все определяют объект типа int.
Целый тип char наиболее удобен для хранения и обработки символов на данном компьютере, обычно это 8-битовый байт. Размеры объектов С++ выражаются в единицах размера char, потому по определению sizeof(char)==1. В зависимости от аппаратного обеспечения char является знаковым или беззнаковым целым. Тип unsigned char, конечно, всегда беззнаковый, и при его использовании получаются более переносимые программы, но из-за применения его вместо просто char могут возникать значительные потери в эффективности.
Причина того, что предоставляется более чем один целый тип, более чем один беззнаковый тип и более чем один тип с плавающей точкой, в том, чтобы дать возможность программисту воспользоваться характерными особенностями аппаратного обеспечения. На многих машинах между различными разновидностями основных типов существуют значительные различия в потребностях памяти, временах доступа к памяти и временах вычислений. Зная машину обычно легко, например, выбрать подходящий тип для конкретной переменной. Написать действительно переносимую программу нижнего уровня сложнее. Вот все, что гарантируется относительно размеров основных типов:
1==sizeof(char)«=sizeof(short)«= sizeof(int)«=sizeof(long) sizeof(float)«=sizeof(double)
Однако обычно разумно предполагать, что в char могут храниться целые числа в диапазоне 0..127 (в нем всегда могут храниться символы машинного набора символов), что short и int имеют не менее 16 бит, что int имеет размер, соответствующий целой арифметике, и что long имеет по меньшей мере 24 бита. Предполагать что-либо помимо этого рискованно, и даже эти эмпирические правила применимы не везде. Таблицу характеристик аппаратного обеспечения для некоторых машин можно найти в #с. 2.6.
Беззнаковые (unsigned) целые типы идеально подходят для применений, в которых память рассматривается как массив битов. Использование unsigned вместо int с тем, чтобы получить еще один бит для представления положительных целых, почти никогда не оказывается хорошей идеей. Попытки гарантировать то, что некоторые значения положительны, посредством описания переменных как unsigned, обычно срываются из-за правил неявного преобразования. Например:
unsigned surprise = -1;
допустимо (но компилятор обязательно сделает предупреждение).
char short int int long int
для представления целых различных размеров,
float double
для представления чисел с плавающей точкой,
unsigned char unsigned short int unsigned int unsigned long int
для представления беззнаковых целых, логических значений, битовых массивов и т.п. Для большей компактности записи можно опускать int в комбинациях из нескольких слов, что не меняет смысла. Так, long означает long int, и unsigned тип означает тип unsigned int. В общем, когда в описании опущен тип, он предполагается int. Например:
const a = 1; static x;
все определяют объект типа int.
Целый тип char наиболее удобен для хранения и обработки символов на данном компьютере, обычно это 8-битовый байт. Размеры объектов С++ выражаются в единицах размера char, потому по определению sizeof(char)==1. В зависимости от аппаратного обеспечения char является знаковым или беззнаковым целым. Тип unsigned char, конечно, всегда беззнаковый, и при его использовании получаются более переносимые программы, но из-за применения его вместо просто char могут возникать значительные потери в эффективности.
Причина того, что предоставляется более чем один целый тип, более чем один беззнаковый тип и более чем один тип с плавающей точкой, в том, чтобы дать возможность программисту воспользоваться характерными особенностями аппаратного обеспечения. На многих машинах между различными разновидностями основных типов существуют значительные различия в потребностях памяти, временах доступа к памяти и временах вычислений. Зная машину обычно легко, например, выбрать подходящий тип для конкретной переменной. Написать действительно переносимую программу нижнего уровня сложнее. Вот все, что гарантируется относительно размеров основных типов:
1==sizeof(char)«=sizeof(short)«= sizeof(int)«=sizeof(long) sizeof(float)«=sizeof(double)
Однако обычно разумно предполагать, что в char могут храниться целые числа в диапазоне 0..127 (в нем всегда могут храниться символы машинного набора символов), что short и int имеют не менее 16 бит, что int имеет размер, соответствующий целой арифметике, и что long имеет по меньшей мере 24 бита. Предполагать что-либо помимо этого рискованно, и даже эти эмпирические правила применимы не везде. Таблицу характеристик аппаратного обеспечения для некоторых машин можно найти в #с. 2.6.
Беззнаковые (unsigned) целые типы идеально подходят для применений, в которых память рассматривается как массив битов. Использование unsigned вместо int с тем, чтобы получить еще один бит для представления положительных целых, почти никогда не оказывается хорошей идеей. Попытки гарантировать то, что некоторые значения положительны, посредством описания переменных как unsigned, обычно срываются из-за правил неявного преобразования. Например:
unsigned surprise = -1;
допустимо (но компилятор обязательно сделает предупреждение).
2.3.2 Неявное Преобразование Типа
Основные типы можно свободно сочетать в присваиваниях и выражениях. Везде, где это возможно, значения преобразуются так, чтобы информация не терялась. Точные правила можно найти в #с.6.6.
Существуют случаи, в которых информация может теряться или искажаться. Присваивание значения одного типа переменной другого типа, представление которого содержит меньшее число бит, неизбежно является источником неприятностей. Допустим, например, что следующая часть программы выполняется на машине с двоичным дополнительным представлением целых и 8-битовыми символами:
int i1 = 256+255; char ch = i1 // ch == 255 int i2 = ch; // i2 == ?
В присваивании ch=i1 теряется один бит (самый значимый!), и ch будет содержать двоичный код «все-единицы» (т.е. 8 единиц); при присваивании i2 это никак не может превратится в 511! Но каким же может быть значение i2? На DEC VAX, где char знаковое, ответ будет -1, на AT amp;T 3B-20, где char беззнаковые, ответ будет 255. В С++ нет динамического (т.е. действующего во время исполнения) механизма для разрешения такого рода проблем, а выяснение на стадии компиляции вообще очень сложно, поэтому программист должен быть внимателен.
Существуют случаи, в которых информация может теряться или искажаться. Присваивание значения одного типа переменной другого типа, представление которого содержит меньшее число бит, неизбежно является источником неприятностей. Допустим, например, что следующая часть программы выполняется на машине с двоичным дополнительным представлением целых и 8-битовыми символами:
int i1 = 256+255; char ch = i1 // ch == 255 int i2 = ch; // i2 == ?
В присваивании ch=i1 теряется один бит (самый значимый!), и ch будет содержать двоичный код «все-единицы» (т.е. 8 единиц); при присваивании i2 это никак не может превратится в 511! Но каким же может быть значение i2? На DEC VAX, где char знаковое, ответ будет -1, на AT amp;T 3B-20, где char беззнаковые, ответ будет 255. В С++ нет динамического (т.е. действующего во время исполнения) механизма для разрешения такого рода проблем, а выяснение на стадии компиляции вообще очень сложно, поэтому программист должен быть внимателен.
2.3.3 Производные Типы
Другие типы можно выводить из основных типов (и типов, определенных пользователем) посредством операций описания:
* указатель amp; ссылка [] вектор () функция
и механизма определения структур. Например:
int* a; float v[10]; char* p[20]; // вектор из 20 указателей на символ void f(int); struct str (* short length; char* p; *);
Правила построения типов с помощью этих операций подробно объясняются в #с.8.3-4. Основная идея состоит в том, что описание производного типа отражает его использование. Например:
int v[10]; // описывает вектор i = v[3]; // использует элемент вектора
int* p; // описывает указатель i = *p; // использует указываемый объект
Вся сложность понимания записи производных типов проистекает из того, что операции * и amp; префиксные, а операции [] () постфиксные, поэтому для формулировки типов в тех случаях, когда приоритеты операций создают затруднения, надо использовать скобки. Например, поскольку приоритет у [] выше, чем у *, то
int* v[10]; // вектор указателей int (*p)[10]; // указатель на вектор
Большинство людей просто помнят, как выглядят наиболее обычные типы.
Описание каждого имени, вводимого в программе, может оказаться утомительным, особенно если их типы одинаковы. Но можно описывать в одном описании несколько имен. В этом случае описание содержит вместо одного имени список имен, разделенных запятыми. Например, два имени можно описать так:
int x, y; // int x; int y;
При описании производных типов можно указать, что операции применяются только к отдельным именам (а не ко всем остальным именам в этом описании). Например:
int* p, y; // int* p; int y; НЕ int* y; int x, *p; // int x; int* p; int v[10], *p; // int v[10]; int* p;
Мнение автора таково, что подобные конструкции делают программу менее удобочитаемой, и их следует избегать.
* указатель amp; ссылка [] вектор () функция
и механизма определения структур. Например:
int* a; float v[10]; char* p[20]; // вектор из 20 указателей на символ void f(int); struct str (* short length; char* p; *);
Правила построения типов с помощью этих операций подробно объясняются в #с.8.3-4. Основная идея состоит в том, что описание производного типа отражает его использование. Например:
int v[10]; // описывает вектор i = v[3]; // использует элемент вектора
int* p; // описывает указатель i = *p; // использует указываемый объект
Вся сложность понимания записи производных типов проистекает из того, что операции * и amp; префиксные, а операции [] () постфиксные, поэтому для формулировки типов в тех случаях, когда приоритеты операций создают затруднения, надо использовать скобки. Например, поскольку приоритет у [] выше, чем у *, то
int* v[10]; // вектор указателей int (*p)[10]; // указатель на вектор
Большинство людей просто помнят, как выглядят наиболее обычные типы.
Описание каждого имени, вводимого в программе, может оказаться утомительным, особенно если их типы одинаковы. Но можно описывать в одном описании несколько имен. В этом случае описание содержит вместо одного имени список имен, разделенных запятыми. Например, два имени можно описать так:
int x, y; // int x; int y;
При описании производных типов можно указать, что операции применяются только к отдельным именам (а не ко всем остальным именам в этом описании). Например:
int* p, y; // int* p; int y; НЕ int* y; int x, *p; // int x; int* p; int v[10], *p; // int v[10]; int* p;
Мнение автора таково, что подобные конструкции делают программу менее удобочитаемой, и их следует избегать.
2.3.4 Тип void
Тип void (пустой) синтаксически ведет себя как основной тип. Однако использовать его можно только как часть производного типа, объектов типа void не существует. Он используется для того, чтобы указать, что функция не возвращает значения, или как базовый тип для указателей на объекты неизвестного типа.
void f() // f не возвращает значение void* pv; // указатель на объект неизвестного типа
Переменной типа указатель на void (void *), можно присваивать указатель любого типа. На первый взгляд это может показаться не особенно полезным, поскольку void* нельзя разименовать, но именно это ограничение и делает тип void* полезным. Главным образом, он применяется для передачи указателей функциям, которые не позволяют сделать предположение о типе объекта, и для возврата из функций нетипизированных объектов. Чтобы использовать такой объект, необходимо применить явное преобразование типа. Подобные функции обычно находятся на самом нижнем уровне системы, там, где осуществляется работа с основными аппаратными ресурсами. Например:
void* allocate(int size); // выделить void deallocate(void*); // освободить
f() (* int* pi = (int*)allocate(10*sizeof(int)); char* pc = (char*)allocate(10); //... deallocate(pi); deallocate(pc); *)
void f() // f не возвращает значение void* pv; // указатель на объект неизвестного типа
Переменной типа указатель на void (void *), можно присваивать указатель любого типа. На первый взгляд это может показаться не особенно полезным, поскольку void* нельзя разименовать, но именно это ограничение и делает тип void* полезным. Главным образом, он применяется для передачи указателей функциям, которые не позволяют сделать предположение о типе объекта, и для возврата из функций нетипизированных объектов. Чтобы использовать такой объект, необходимо применить явное преобразование типа. Подобные функции обычно находятся на самом нижнем уровне системы, там, где осуществляется работа с основными аппаратными ресурсами. Например:
void* allocate(int size); // выделить void deallocate(void*); // освободить
f() (* int* pi = (int*)allocate(10*sizeof(int)); char* pc = (char*)allocate(10); //... deallocate(pi); deallocate(pc); *)
2.3.5 Указатели
Для большинства типов T T* является типом арифметический указатель на T. То есть, в переменной типа T* может храниться адрес объекта типа T. Для указателей на вектора и указателей на функции вам, к сожалению, придется пользоваться более сложной записью:
int* pi; char** cpp; // указатель на указатель на char int (*vp)[10]; // указатель на вектор из 10 int'ов int (*fp)(char, char*); // указатель на функцию //получающую параметры(char, char*) // и возвращающую int
Основная операция над указателем – разыменование, то есть ссылка на объект, на который указывает указатель. Эта операция также называется косвенным обращением. Операция разыменования – это унарное * (префиксное). Например:
char c1 = 'a'; char* p = amp;c1; // в p хранится адрес c1 char c2 = *p; // c2 = 'a'
Переменная, на которую указывает p,– это c1, а значение, которое хранится в c1, это 'a', поэтому присваиваемое c2 значение *p есть 'a'.
Над указателями можно осуществлять некоторые арифметические действия. Вот, например, функция, подсчитывающая число символов в строке (не считая завершающего 0):
int strlen(char* p) (* int i = 0; while (*p++) i++; return i; *)
Другой способ найти длину состоит в том, чтобы сначала найти конец строки, а затем вычесть адрес начала строки из адреса ее конца:
int strlen(char* p) (* char* q = p; while (*q++) ; return q-p-1; *)
Очень полезными могут оказаться указатели на функции. Они обсуждаются в #4.6.7.
int* pi; char** cpp; // указатель на указатель на char int (*vp)[10]; // указатель на вектор из 10 int'ов int (*fp)(char, char*); // указатель на функцию //получающую параметры(char, char*) // и возвращающую int
Основная операция над указателем – разыменование, то есть ссылка на объект, на который указывает указатель. Эта операция также называется косвенным обращением. Операция разыменования – это унарное * (префиксное). Например:
char c1 = 'a'; char* p = amp;c1; // в p хранится адрес c1 char c2 = *p; // c2 = 'a'
Переменная, на которую указывает p,– это c1, а значение, которое хранится в c1, это 'a', поэтому присваиваемое c2 значение *p есть 'a'.
Над указателями можно осуществлять некоторые арифметические действия. Вот, например, функция, подсчитывающая число символов в строке (не считая завершающего 0):
int strlen(char* p) (* int i = 0; while (*p++) i++; return i; *)
Другой способ найти длину состоит в том, чтобы сначала найти конец строки, а затем вычесть адрес начала строки из адреса ее конца:
int strlen(char* p) (* char* q = p; while (*q++) ; return q-p-1; *)
Очень полезными могут оказаться указатели на функции. Они обсуждаются в #4.6.7.
2.3.6 Вектора
Для типа T T[size] является типом «вектор из size элементов типа T». Элементы индексируются (нумеруются) от 0 до size-1. Например:
float v[3]; // вектор из трех float: v[0], v[1], v[2] int a[2][5]; // два вектора из пяти int char* vpc; // вектор из 32 указателей на символ
Цикл для печати целых значений букв нижнего регистра можно было бы написать так:
extern int strlen(char*);
char alpha[] = «abcdefghijklmnoprstuvwxyz»;
main()
(* int sz = strlen(alpha);
for (int i=0; i«sz; i++) (* char ch = alpha[i]; cout „„ "'" „« chr(ch) «« "'" «« " = " «« ch «« « = 0“ «« oct(ch) «« « = 0x“ «« hex(ch) «« «\n“; *) *)
Функция chr() возвращает представление небольшого целого в виде строки; например, chr(80) это "P" на машине, на которой используется набор символов ASCII. Функция oct() строит восьмеричное представление своего целого аргумента, а hex() строит шестнадцатеричное представление своего целого аргумента; chr() oct() и hex() описаны в «stream.h». Функция strlen() использовалась для подсчета числа символов в alpha; вместо этого можно было использовать значение размера alpha (#2.4.4). Если применяется набор символов ASCII, то выдача выглядит так:
'a' = 97 = 0141 = 0x61 'b' = 98 = 0142 = 0x62 'c' = 99 = 0143 = 0x63 ...
Заметим, что задавать размер вектора alpha необязательно. Компилятор считает число символов в символьной строке, указанной в качестве инициализатора. Использование строки как инициализатора для вектора символов – удобное, но к сожалению и единственное применение строк. Аналогичное этому присваивание строки вектору отсутствует. Например:
char v[9]; v = «строка»; // ошибка
ошибочно, поскольку присваивание не определено для векторов.
Конечно, для инициализации символьных массивов подходят не только строки. Для остальных типов нужно применять более сложную запись. Эту запись можно использовать и для символьных векторов. Например:
int v1[] = (* 1, 2, 3, 4 *); int v2[] = (* 'a', 'b', 'c', 'd' *);
char v3[] = (* 1, 2, 3, 4 *); char v4[] = (* 'a', 'b', 'c', 'd' *);
Заметьте, что v4 – вектор из четырех (а не пяти) символов; он не оканчивается нулем, как того требуют соглашение и библиотечные подпрограммы. Обычно применение такой записи ограничивается статическими объектами.
Многомерные массивы представляются как вектора векторов, и применение записи через запятую, как это делается в некоторых других языках, дает ошибку при компиляции, так как запятая (,) является операцией следования (см. #3.2.2). Попробуйте, например, сделать так:
int bad[5,2]; // ошибка
и так:
int v[5][2];
int bad = v[4,1]; // ошибка int good = v[4][1]; // ошибка
Описание
char v[2][5];
описывает вектор из двух элементов, каждый из которых является вектором типа char[5]. В следующем примере первый из этих векторов инициализируется первыми пятью буквами, а второй – первыми пятью цифрами.
char v[2][5] = (* 'a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4' *)
main() (* for (int i = 0; i«2; i++) (* for (int j = 0; j„5; j++) cout „„ „v[“ «« i «« «][“ «« j «« «]=“ «« chr(v[i][j]) «« " "; cout «« «\n“; *) *)
это дает в результате
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4
float v[3]; // вектор из трех float: v[0], v[1], v[2] int a[2][5]; // два вектора из пяти int char* vpc; // вектор из 32 указателей на символ
Цикл для печати целых значений букв нижнего регистра можно было бы написать так:
extern int strlen(char*);
char alpha[] = «abcdefghijklmnoprstuvwxyz»;
main()
(* int sz = strlen(alpha);
for (int i=0; i«sz; i++) (* char ch = alpha[i]; cout „„ "'" „« chr(ch) «« "'" «« " = " «« ch «« « = 0“ «« oct(ch) «« « = 0x“ «« hex(ch) «« «\n“; *) *)
Функция chr() возвращает представление небольшого целого в виде строки; например, chr(80) это "P" на машине, на которой используется набор символов ASCII. Функция oct() строит восьмеричное представление своего целого аргумента, а hex() строит шестнадцатеричное представление своего целого аргумента; chr() oct() и hex() описаны в «stream.h». Функция strlen() использовалась для подсчета числа символов в alpha; вместо этого можно было использовать значение размера alpha (#2.4.4). Если применяется набор символов ASCII, то выдача выглядит так:
'a' = 97 = 0141 = 0x61 'b' = 98 = 0142 = 0x62 'c' = 99 = 0143 = 0x63 ...
Заметим, что задавать размер вектора alpha необязательно. Компилятор считает число символов в символьной строке, указанной в качестве инициализатора. Использование строки как инициализатора для вектора символов – удобное, но к сожалению и единственное применение строк. Аналогичное этому присваивание строки вектору отсутствует. Например:
char v[9]; v = «строка»; // ошибка
ошибочно, поскольку присваивание не определено для векторов.
Конечно, для инициализации символьных массивов подходят не только строки. Для остальных типов нужно применять более сложную запись. Эту запись можно использовать и для символьных векторов. Например:
int v1[] = (* 1, 2, 3, 4 *); int v2[] = (* 'a', 'b', 'c', 'd' *);
char v3[] = (* 1, 2, 3, 4 *); char v4[] = (* 'a', 'b', 'c', 'd' *);
Заметьте, что v4 – вектор из четырех (а не пяти) символов; он не оканчивается нулем, как того требуют соглашение и библиотечные подпрограммы. Обычно применение такой записи ограничивается статическими объектами.
Многомерные массивы представляются как вектора векторов, и применение записи через запятую, как это делается в некоторых других языках, дает ошибку при компиляции, так как запятая (,) является операцией следования (см. #3.2.2). Попробуйте, например, сделать так:
int bad[5,2]; // ошибка
и так:
int v[5][2];
int bad = v[4,1]; // ошибка int good = v[4][1]; // ошибка
Описание
char v[2][5];
описывает вектор из двух элементов, каждый из которых является вектором типа char[5]. В следующем примере первый из этих векторов инициализируется первыми пятью буквами, а второй – первыми пятью цифрами.
char v[2][5] = (* 'a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4' *)
main() (* for (int i = 0; i«2; i++) (* for (int j = 0; j„5; j++) cout „„ „v[“ «« i «« «][“ «« j «« «]=“ «« chr(v[i][j]) «« " "; cout «« «\n“; *) *)
это дает в результате
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4
2.3.7 Указатели и Вектора
Указатели и вектора в С++ связаны очень тесно. Имя вектора можно использовать как указатель на его первый элемент, поэтому пример с алфавитом можно было написать так:
char alpha[] = «abcdefghijklmnopqrstuvwxyz»; char* p = alpha; char ch;
while (ch = *p++) cout «„ chr(ch) „« " = " «« ch «« « = 0“ «« oct(ch) «« «\n“;
Описание p можно было также записать как
char* p = amp;alpha[0];
Эта эквивалентность широко используется в вызовах функций, в которых векторный параметр всегда передается как указатель на первый элемент вектора. Так, в примере
extern int strlen(char*); char v[] = «Annemarie»; char* p = v; strlen(p); strlen(v);
функции strlen в обоих вызовах передается одно и то же значение. Вся штука в том, что этого невозможно избежать; то есть не существует способа описать функцию так, чтобы вектор v в вызове функции копировался (#4.6.3). Результат применения к указателям арифметических операций +, -, ++ или – зависит от типа объекта, на который они указывают. Когда к указателю p типа T* применяется арифметическая операция, предполагается, что p указывает на элемент вектора объектов типа T; p+1
char alpha[] = «abcdefghijklmnopqrstuvwxyz»; char* p = alpha; char ch;
while (ch = *p++) cout «„ chr(ch) „« " = " «« ch «« « = 0“ «« oct(ch) «« «\n“;
Описание p можно было также записать как
char* p = amp;alpha[0];
Эта эквивалентность широко используется в вызовах функций, в которых векторный параметр всегда передается как указатель на первый элемент вектора. Так, в примере
extern int strlen(char*); char v[] = «Annemarie»; char* p = v; strlen(p); strlen(v);
функции strlen в обоих вызовах передается одно и то же значение. Вся штука в том, что этого невозможно избежать; то есть не существует способа описать функцию так, чтобы вектор v в вызове функции копировался (#4.6.3). Результат применения к указателям арифметических операций +, -, ++ или – зависит от типа объекта, на который они указывают. Когда к указателю p типа T* применяется арифметическая операция, предполагается, что p указывает на элемент вектора объектов типа T; p+1