Часть II. Объектно-ориентированное программирование и язык C++

В этой части мы будем изучать объектно-ориентированное программирование на C++. Конечно, C++ не единственный язык, работающий с классами и объектами. Есть Object Pascal, есть Java, есть и более старые языки вроде Smalltalk. Поэтому, наверное, имеет смысл поговорить и об общих принципах организации подобных языков.

Помимо общих принципов объектной архитектуры и классов языка C++ мы поговорим о таких вещах, как стандартные потоки, исключения, шаблоны и RTTI. К концу этой части книги вы узнаете практически все о стандартном ANSI C++, и в части III мы перейдем к изучению средств визуального программирования C++Builder.

Глава 6. Модифицированный С

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

Отличия C++ от ANSI С

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

Указатели типа void*

В языке С значение типа void* можно непосредственно присваивать переменной-указателю любого типа, без каких-либо приведений. В C++ это не допускается. Другими словами, код вроде

SomeStruct *sPtr;

void AllocBuffer(int n)

{

sPtr = malloc(n * sizeof(SomeStruct));

}

вызовет ошибку в C++. Необходимо явное приведение типа указателя:

sPtr = (SomeStruct *)malloc(n * sizeof(SomeStruct));

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

В языке С тип char эквивалентен либо signed char, либо unsigned char; какому именно, определяется реализацией или установками компилятора. Обычно считается, что char — это signed char. В C++ char.

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

  

void OutC(char с)

{

printf("Unspec: %c\n", c);

}

void OutC(signed char c)

{

printf("Signed: %c\n",'c);

}

void OutC(unsigned char c)

{

printf("Unsigned: %c\n", c);

}

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

void OutI(int i)

{

printf("Unspec: %d\n", i);

}

void OutI(signed int i)

{

printf("Signed: %d\n", i);

} void OutI(unsigned int i)

{

printf("Unsigned: %d\n", i);

}

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

Символьные константы

Типом символьной константы (символ, заключенный в апострофы) в С является int, В языке C++ символьная константа имеет тип char. Конечно, это различие практически несущественно, но можно придумать случай, когда из-за него код С окажется несовместимым с C++.

Глобальные константы

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

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

extern const double eConst = 2.718281828;

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

extern const double eConst;

Такое поведение глобальных констант C++ упрощает их объявление. Можно поместить определение константы (без extern) в заголовочный файл и включать его во все исходные модули, где она используется. Тем самым будет генерированы отдельные константы для каждого модуля, с одним именем и одинаковыми значениями. В С такие объявления вызывали бы ошибку дублирования определения.

“Улучшенный С”

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

Дополнительные обозначения операций

Для ряда операций, в основном логических, в ANSI C++ введены альтернативные обозначения (ключевые слова) в стиле языка Pascal. Это было сделано потому, что на некоторых национальных клавиатурах трудно вводить знаки вроде “^” или “~”. Ниже перечислены все такие обозначения.

Ключевое слово

Эквивалентный знак

Операция

and

&&

Логическое AND

and eq

&=

Присвоение поразрядного AND

bitand

&

Поразрядное AND

bitor

|

Поразрядное OR

coiripl

~

Поразрядное NOT (дополнение до 1)

not

!

Логическое NOT

not eq

! =

Отношение “не равно”

or

||

Логическое OR

or eq

|=

Присвоение поразрядного OR

xor

^

Поразрядное исключающее OR

xor eq

^=

Присвоение исключающего OR

 

К сожалению, в C++Builder, даже в 5-й версии, эти ключевые слова пока не реализованы, но мы все равно их здесь перечислили. О них следует знать, особенно если вам придется переносить в C++Builder код, написанный для других компиляторов, например, Borland C++ 5.

Тип bool

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

#define FALSE0

#define TRUE 1

int done = FALSE;

while (!done) { // И т.д...

}

Теперь в ANSI C++ есть тип bool, позволяющий объявлять переменные специально булева типа. Кроме того, для представления булевых значений имеются предопределенные константы true и false. Внутренним представлением true является 1, представлением fal5e .— 0. С их помощью можно присваивать значения булевым переменным:

bool done;

const bool forever = true;

done = false;

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

bool Continue(bool showPrompt);

В условиях циклов и выражениях условных операторов булевы переменные ведут себя точно так же, как прежние “логические” типа int.

Тип wchar_t

Это еще один новый встроенный тип C++. Он предназначен для хранения “широких” символов (в противоположность “узким” символам типа char). Тип wchar_t имеет достаточный диапазон, чтобы отображать очень большие наборы символов — вплоть до китайских иероглифов. В С++Вuilder он эквивалентен типу short int.

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

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

const int aSize = 20 * sizeof(int);

char byteArray[aSize];

Объявления переменных

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

#include <stdio.h>

int main(void) {

int n = 10;

printf("Hello! i =") ;

int i;

for (i=0; i<n; i++)

{

printf("%4d", i);

}

printf("\nAfter loop i = %d\n", i);

return 0;

Счетчик i объявляется непосредственно перед заголовком цикла for, а не в начале блока.

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

for (int i=0; i<n; i++) {...}

Ранее считалось, что при таком объявлении i остается доступной и после завершения цикла. Но стандарт ANSI постулирует, что область действия объявленной в заголовке цикла переменной ограничивается телом цикла. То же самое относится к циклам while.

Имена-этикетки

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

struct Person {

struct Person *link;

char firstName[32];

char lastName[32];

};

struct Person aPerson;

или введения нового имени с помощью typedef достаточно будет написать

struct Person {

Person *link;

char firstName[32];

char lastName[32] ;

};

Person aPerson;

Person, таким образом, будет настоящим именем типа.

Анонимные объединения

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

Глобальные анонимные объединения должны объявляться как static.

Аргументы по умолчанию

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

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

Вот небольшая иллюстрация:

//////////////////////////////////////////////////

// Defaults.срр: Аргументы по умолчанию.

//

#include <stdio.h>

int Sum(int a, int b, int с = 0, int d = 0);

//Корректный прототип.

//

// Следующий прототип был бы некорректным:

// int Sum(int а, int b = 0, int с, int d = 0) ;

// int Sum(int a, int b, int с, int d)

// Определение функции.

{

return а + b + с + d;

}

int main(void) (

printf("l + 2 + 3 + 4 = %d\n", Sum(l, 2, 3, 4));

printf("1 + 2 + 3 = %d\n", Sum(l, 2, 3) ) ;

printf("1 + 2 = %d\n", Sum(1, 2));

//

// Недопустимый вызов:

// printf(" 1 + 2 + 4 = %d\n", Sum(l, 2,, 4)) ;

//

return 0;

}

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

Главная функция определяется в примерах как int main (void), но это не существенно, и если вы захотите посмотреть, как они работают, можно оставить заголовок, который генерирует C++Builder.

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

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

Ссылки

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

Ссылки как псевдонимы переменных

Переменная-ссылка объявляется со знаком “&”, предшествующим ее имени; инициализирующим выражением должно быть имя переменной. Рассмотрите такой пример:

//////////////////////////////////////////////////

// Alias.cpp: Ссылка как псевдоним.

//

#include <stdio.h>

int main(void)

{

int iVar = 7777;

// Инициализация целой переменной.

int *iPtr = siVar;

// Инициализация указателя адресом

// iVar.

int &iRef = iVar;

// Инициализация ссылки как

// псевдонима iVar.

int *irPtr = &iRef;

// Инициализация указателя "адресом"

// ссылки.

printf("iVar = %d\n", iVar) ;

printf("*iPtr = %d\n", *iPtr);

printff'iRef = %d\n", iRef);

printf("*irPtr = %d\n", *irPtr) ;

printff'iPtr = %p, irPtr = %p\n", iPtr, irPtr);

return 0;

}

Первые четыре оператора printf выводят одно и то же число 7777. последний оператор печатает значения двух указателей, первый из которых инициализирован адресом переменной, а второй адресом ссылки. Эти адреса также оказываются одинаковыми. Как видите, после инициализации ссылки ее имя используется в точности как имя ассоциированной с ней переменной, т. е. как псевдоним.

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

Ссылки в качестве параметров функции

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

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

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

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

//////////////////////////////////////////////////

// RefPar.cpp: Передача параметров по ссылке.

//

#include <stdio.h>

void SwapVal(int x, int y)

// Передача значений.

{

int temp;

temp = x;

x = y;

у = temp;

}

void SwapPtr(int *x, int *y)

// Передача указателей.

{

int temp;

temp = *x;

*x = *y;

*y = temp;

}

void SwapRef(int &x, int &y)

// Передача ссылок.

{

int temp;

temp = x;

x = y;

y = temp;

}

int main(void)

(

int i = 11, i = 22;

printf("Initial values: i=%d,j=%d.\n", i,j ) ;

SwapVal(i, j);

printf ("After SwapVal():i = %d,j =%d.\n",i,j);

SwapPtr (&i, &j) ;

printf("After SwapPtr():i =%d,j=%d.\n",i,j);

SwapRef(i, j) ;

printf("After SwapRef():i =%d,j=%d.\n",i,j);

return 0;

Программа напечатает:

Initial values: i = 11, j = 22.

After SwapValO i = 11, j = 22.

After SwapPtr():i = 22, j = 11.

After SwapRef () i = 11, j =22.

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

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

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

Ссылка в качестве возвращаемого значения

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

////////////////////////////////////////////////////////

// массива.

//

#include <stdio.h>

#include <assert.h>

const int arrSize = 8;

int &refItem(int indx)

// Возвращает ссылку на элемент

//

iArray[indx] {

static int iArray[arrSize];

//

// Проверка диапазона:

//

assert(indx >= 0 && indx< arrSize);

return iArray[indx];

}

int main(void) {

int i;

for (i=0; .KarrSise; i++)

refltem(i) = 1 << i;

// Присваивает значение элементу

// iArray[i].

for (i=O; i<arrSize; i++)

printf("iArray[%02d] = %4d\n'\ i, refltem(i));

refltem(i) = 0; // Попытка выхода за пределы

// массива.

return 0;

}

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

Расширяемые функции

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

По своем завершении функция возвращает управление инструкцией RET. Выполнение вызывающей процедуры возобновляется с инструкции, следующей за call.

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

Вот простейший пример расширяемой функции:

#include <stdio.h>

inline int. Max(int a, int b)

{

return a > b? a : b;

}

int main(void)

int x = 11, у = 22;

printf("Max(%d, %d) = %d.\n", x, у, Мах(х, у)) ;

return 0;

}

Если зы хотите, чтобы компилятор генерировал расширения iniine-функций, нужно сбросить флажок Disable inline expansions на странице Compiler диалога Project Options.

Тут же следует сделать целый ряд оговорок относительно того, будет ли компилятор действительно генерировать встроенный код для функций, которые вы определили как inline.

Прежде всего, в самом стандарте ANSI сказано, что модификатор inline является лишь рекомендацией компилятору генерировать встроенное расширение функции. Наподобие модификатора register, который рекомендует разместить переменную в регистре процессора. Это не всегда возможно, поэтому компилятор может безо всяких предупреждений игнорировать модификатор inline.

Привлекая самые общие соображения, можно сформулировать следующее правило: для того, чтобы вызов iniine-функций замещался ее расширением, компилятор должен видеть определение функции в точке ее вызова. Если он ее не видит, то рассматривает функцию как внешнюю, т. е. определенную ниже или вообще в другом файле. Современные компиляторы по большей части однопроходные. Термин не имеет отношения к животному миру Австралии, а означает, что компилятор сканирует текст транслируемого модуля единственный раз и не может “заглянуть вперед”.

Компилятор Borland C++ не может расширять встроенные функции также в следующих случаях:

 

В качестве иллюстрации рассмотрите такой пример:

/////////////////////////////////////////////////

// Inline.срр: Расширение inline-функций.

//

#pragma hdrstop

#include <condefs.h>

#include <stdio.h>

inline int Max(int, int);

int МахЗ(int, int, int);

int main(void) {

int x = 11, у = 22, z = 33, v = 44;

x = МахЗ(х, z, v) ;

z = Мах(х, у);

// Не расширяется - генерируется вызов!

printf("Max(%d, %d) = %d.\n", х, у, z);

return 0;

}

inline int Max(int a, int b) {

return a > b? a : b;

}

int МахЗ (int a, int b, int c)

{

b = Max(a, b);

// Эти вызовы расширяются как встроенные.

return Max(с, b) ;

}

Здесь функция Мах () определяется после main (), поэтому ее вызов из main () не расширяется как встроенный. Однако после определения Мах () дальнейшие ее вызовы (в Мах3 ()) уже расширяются. Воспользуйтесь отладчиком, чтобы это проверить — внутрь встроенной функции нельзя войти командой Trace Into; ее можно отлаживать только в окне CPU.

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

Операция разрешения области действия

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

В C++ имеется операция разрешения области действия, позволяющая в такой ситуации обращаться к глобальной переменной, предваряя ее имя символами “ : : ”. Вот пример, поясняющий разрешение области действия:

#include <stdio.h> int aVar = 111; // Глобальная переменная.

int main(void)

{

int aVar == 222; // Локальная переменная.

printf("Local aVar is %d.\n", aVar);

printf("Global aVar is, %d.\n", ::aVar);

return 0;

}

Пространства имен

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

Язык C++ решает эту проблему, позволяя разбить проект на сегменты с различными пространствами имен (namespaces). Заключив свои символы в отдельное пространство имен, вы, по существу, снабжаете имя всякого типа, переменной и т. д. некоторым скрытым префиксом. Например, если определить пространство имен

namespace MYSPACE { int х;

}

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

MYSPACE::х = 11;

Пространства имен могут быть вложенными:

namespace OUTER { int x;

namespace INNER { int x;

}

Соответственно, чтобы ссылаться на эти переменные, нужно будет написать:

OUTER::х = 11;

OUTER::INNER::x = 22;

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

Создание пространства имен

К созданию именных пространств можно отнести три момента:

Первоначальное объявление пространства имен — это определение пространства с именем, еще не встречавшимся в программе. Синтаксис его следующий:

namespace новое_имя {тело пространства имен}

Тем самым в программе объявляется пространство имен с именем новое_имя.

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

namespace существующее_имя [тело_пространства__имен]

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

Синтаксис C++ позволяет определять анонимные пространства имен:

namespase{

int x;

double у;

}

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

static int x;

static double у;

Доступ к пространству имен

Доступ к элементам конкретного пространства имен может осуществляться тремя способами:

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

Квалифицирующее объявление выглядит следующим образом:

using иденгификатор_пространства_имен::имя;

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

namespace ALPHA {

int RetJ(int j) {return j;} } namespace BETA {

int RetJ(int j) {return (j * 2);}

}

int main(void) {

using BETA::RetJ; // Квалифицирующее объявление RetJ().

int x = 11;

printf("Calling RetJ(): %d\n"/ RetJ(x));

// Вызывает BETA::RetJ(). return 0;

}

Последняя форма доступа к пространству имен — директива using. Она имеет вид:

using namespace идентификатор пространства_имен;

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

#include <stdio.h>

namespace ALPHA {

int x = 11;

int у = 111;

int v = 1111;

} namespace BETA {

int x = 22;

int у - 222;

int z = 2222;

}

int main(void) {

using namespace ALPHA; // Конфликта не возникает до

using namespace BETA; // момента действительного

// обращения к элементам х и у.

using ВЕТА::х;

// Локальная квалификация х.

int z = 3333; // Локальная переменная.

printf ("Global х = %d, у = %d, z = %d, v = %d.\n", х, ALPHA::y, ::z, v);

// х квалифицирована

// локально, у - явно.

printf("Local z = %d.\n", z) ;

return 0;

}

Программа печатает:

Global х = 22, у = 111, z = 2222, v = 1111. Local z = 3333.

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

Псевдонимы именных пространств

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

namespace BORLAND_INTERNATIONAL

{

// Тело именного пространства...

namespace NESTED_BORLAND_INTERNATIONAL

{

// Тело вложенного именного пространства... }

// Псевдонимы:

namespace BI = BORLAND_INTERNATIONAL;

namespace NBI = BORLAND_INTERNATIONAL::

NESTED_BORLAND_INTERNATIONAL;

Перегруженные функции

В программировании то и дело случается писать функции для схожих действий, выполняемых над различными типами и наборами данных. Возьмите, например, функцию, которая должна возвращать квадрат своего аргумента. В C/C++ возведение в квадрат целого и числа с плавающей точкой — существенно разные операции. Вообще говоря, придется написать две функции — одну, принимающую целый аргумент и возвращающую целое, и вторую, принимающую тип double и возвращающую также double. В С функции должны иметь уникальные имена. Таким образом, перед программистом встает задача придумывать массу имен для различных функций, выполняющих аналогичные действия. Например, Square-Int() и SquareDbl() .

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

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

#include <stdio.h>

int Square(int arg)

{

return arg*arg;

}

 

double Square(double arg)

{

return arg*arg;

char *Square(const char *arg, int n)

{

static char res[256];

int j = 0;

while (*arg && j < n) { if (*arg != ' ') res[j++] = *arg;

res[j++] = *arg++;

}

res[j] = 0;

return res;

}

int main(void)

{

int x = 11;

double у = 3.1416;

char msg[] = "Output from overloaded Function!";

printf("Output: %d, %f, %s\n", Square (x) , Square (y) , Square (msg, 32) ) ;

return 0 ;

}

}

Результат работы программы показан на рис. 6.1.

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

Рис. 6.1 Пример с тремя перегруженными функциями

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

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

Как это делается

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

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

void Func(void); // @Func$qv

void Func(int); // @Func$qi

void Func(int, int); // @Func$qii

void Func(*char); // 8Func$qpc

void Func(unsigned); // @Func$qui

void Func(const char*); // @Func$qpxc

Тип возвращаемого значения никак не отражается на декорировании имени.

Спецификация связи

Если функция, написанная на C++, должна вызываться из программы на С, Pascal или языке ассемблера (и наоборот, что часто бывает при использовании существующих динамических библиотек), то механизм декорирования имен C++ создает некоторые трудности. Они могут быть причиной сообщений об ошибках при компоновке вроде “Неопределенный символ ххх в модуле уyу”.

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

Вот примеры:

// Функция, которую можно вызывать из С (прототип):

extern "С" void FuncForC(void);

// Прототип функции из некоторой библиотеки не на C++:

extern "С" void SomeExtFunc (int);

// Определение - extern "С" не требуется, если включен

// прототип:

void FuncForC(void)

{

printf("Hello!\n");

}

// Вызов внешней функции:

SomeExtFunc(count);

Следует отличать спецификацию связи от соглашений o вызове. Функция, например, может вызываться в соответствии с соглашением Pascal (что означает определенный порядок передачи параметров и имя в верхнем регистре), но иметь спецификацию связи C++, т. е. декорированное имя. Алгоритм здесь такой: сначала формируется (если не указано extern "С") декорированное имя, а затем оно подвергается модификации в соответствии с соглаЕсли вы вызываете функции библиотек, написанных на С или языке ассемблера и у вас есть необходимые заголовочные файлы на С, то соответствующие директивы включения можно поместить в блок extern "С", как показано ниже. В противном случае нужно было бы модифицировать эти файлы, добавив спецификации связи к прототипам функций.

extern "С" { #include "asmlib.h" #include "someclib.h" }

Операции распределения памяти

В языке C++ для управления динамической памятью введены операции new и delete (для массивов new [ ] и delete [ ]). В С для этого применялись в C++, однако новые операции имеют некоторые преимущества.

Переопределение операций new и delete

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

#include <stdlib.h>

#include <stdio.h>

////////////////////////////////////////////////////

/ / Переопределение операций new и delete.

//

void* operator new(size_t size)

{

printf("%u bytes requested.\n", size);

return malloc(size);

void operator delete(void *ptr)

{

printf("Delete performed.\n") ;

free(ptr) ;

}

/////////////////////////////////////////////////

// Перегрузка new для выдачи дополнительной информации.

//

void* operator new (size t size, char *file, int line)

printf("%u bytes requested in file %s line %d.\n", size, file, line) ;

return malloc(size);

}

int main(void) {

double *dptr = new double; // Вызывает новую new.

*dptr = 3.14159265;

delete dptr; // Вызывает новую delete.

// Вызов перегруженной new.

dptr = new(_FILE_, _LINE_) double;

*dptr = 1.0;

delete dptr;

return 0;

}

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

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

Перегруженная версия new может быть, кстати, определена таким образом, что она не будет выделять никакой новой памяти, а будет использовать уже существующий объект. Адрес, где нужно разместить “новый” объект, должен быть одним из дополнительных параметров функции-операции. Эта форма new известна как синтаксис размещения (placement syntax).

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

Операция delete имеет тип void, а ее параметр — void*.

Обработка ошибок

В C++ имеется библиотечная функция 5et_new_handler().Он будет вызываться при любой ошибке выделения памяти.

#include <stdlib.h>

#include <stdio.h>

#include <new.h>

void MyHandler(void)

{

prir-tf("No memory!\n");

exit(l) ;

}

int main(void) {

set_new_handler (MyHandler) ; //Установка обработчика.

return 0;

}

Обработчик ошибок new должен:

Заключение

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