Тип функции

Автор: manager Понедельник, Март 24th, 2008 Нет комментариев

Рубрика: C++. Бархатный путь

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

Например, пара функций
char MyF1 (int, int, int*, float);
char MyNew (int MyP1, int MyP2, int* MyP3, float MyP3);
имеют один и тот же тип:
char (int, int, int*, float)
Подобную конструкцию мы назовём описанием типа функции.
А вот как выглядит описание типа функции, которая возвращает указатель на объект типа char:
char * (int, int, int*, float)
Описанию этого типа соответствует, например, функция
char *MyFp (int MyP1, int MyP2, int* MyP3, float MyP3);
Комбинируя знак ptr-операции * с именем функции мы получаем новую языковую конструкцию:
char (*MyPt1) (int MyP1, int MyP2, int* MyP3, float MyP3);
Это уже не объявление функции. Это определение указателя на функцию! Это объект со следующими характеристиками:
его имя MyPt1,
это указатель на функцию,
эта функция должна возвращать значения типа char,
список её формальных параметров имеет вид (int,int,int*, float).
Так что это должны быть функции со строго определёнными характеристиками. В нашем случае — это функции типа
char (int, int, int*, float)
Описание типа указателя на функцию, возвращающую указатель на объект типа char с параметрами (int, int, int*, float)
char * (int, int, int*, float)
отличается от описания типа этой функции дополнительным элементом (*):
char * (*) (int, int, int*, float).
Пример определения подобного указателя:
char* (*MyPt2) (int MyP1, int MyP2, int* MyP3, float MyP3);
И опять новый объект:
его имя MyPt2,
это указатель на функцию,
эта функция должна возвращать указатель на объекты типа char,
список её формальных параметров имеет вид (int,int,int*, float).

Также можно определить функцию, которая будет возвращать указатель на объект типа void (то есть просто указатель). Это совсем просто:
void * (int)
Описанию этого типа соответствует, например, функция
void *malloc (int size);

Эта функция пытается выделить блок памяти размера size и в случае, если это удалось сделать, возвращает указатель на выделенную область памяти. В противном случае возвращается специальное значение NULL. Как распорядиться выделенной памятью — личное дело программиста. Единственное ограничение заключается в том, что при этом необходимо использовать явное преобразование типа:
#include
char *p = NULL;
void NewMemory ()
{
p = malloc(sizeof(char)*1024);// Этот оператор не пройдёт!
p = (char*) malloc(sizeof(char)*1024);
// Требуется явное преобразование типа.
}

Имя массива, если к нему не применяется операция индексации, оказывается указателем на первый элемент массива. Аналогично, имя функции, если к нему не применяется операция вызова, является указателем на функцию. В нашем случае ранее объявленная функция под именем MyFp приводится к безымянному указателю типа
char * (*) (int, int, int*, float)
К имени функции может быть применена операция взятия адреса. Её применение также порождает указатель на эту функцию. Таким образом, MyFp и &MyFp имеют один и тот же тип. А вот как инициируется указатель на функцию:
char* (*MyPt2) (int, int, int*, float) = MyFp;
Очевидно, что функция MyFp() должна быть к этому моменту не только объявлена, но и определена.
Новому указателю на функцию
char* (*MyPt3) (int, int, int*, float);
можно также присвоить новое значение.
Для этого достаточно использовать ранее определённый и проинициализированный указатель:
MyPt3 = MyPt2;
Или адрес ранее определённой функции:
MyPt3 = MyFp;

При этом инициализация и присваивание оказываются корректными лишь при условии, что имеет место точное сопоставление списков формальных параметров и списков формальных значений в объявлениях указателей и функций.
Для вызова функции с помощью указателя использование операции разыменования не обязательно. Полная форма вызова
char* MyPointChar = (*MyPT3)(7,7,NULL,7.7);
имеет краткую эквивалентную форму
char* MyPointChar = MyPT3(7,7,NULL,7.7);
Значением выражения MyPT3 является адрес функции.
А вот каким образом описывается массив указателей на функцию:
char* (*MyPtArray[3]) (int, int, int*, float);
Здесь описан массив указателей из 3 элементов. Инициализация массива указателей
возможна лишь после объявления трёх однотипных функций:
extern char* MyFF1 (int, int, int*, float);
extern char* MyFF2 (int, int, int*, float);
extern char* MyFF3 (int, int, int*, float);
char* (*MyPtArray[3]) (int, int, int*, float) =
{
MyFF1,
MyFF2,
MyFF3
}; // Инициализация массива указателей.
Вызов функции (например, MyFF3()) с помощью элемента массива указателей можно осуществить следующим образом:
char* MyPointChar = MyPtArray[2](7,7,NULL,7.7);
Указатель на функцию может быть описан как параметр функции:
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float));
// Торжество абстрактного описателя!
И этому параметру можно присвоить значение (значение по умолчанию):
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float)=MyFF1);
Функция, что используемая для инициализации последнего параметра функция должна быть к моменту инициализации, по крайней мере, объявлена.
А вот как может выглядеть определение функции MonitorF:
#include
/*
Заголовочный файл, содержащий макроопределение assert.
Это макроопределение преобразуется в условный оператор if.
Если в ходе проверки значение условного выражения оказывается
равным нулю, то происходит прерывание выполнения программы.
*/
void MonitorF (
int val1,
int val2,
int* pVal,
float fVal,
char*(*pParF)(int,int,int*,float)
)
{
char* pChar;
assert(pVal != NULL);
assert(pParF != NULL);
//Это всего лишь проверка того, не являются ли указатели пустыми…
pChar = pParF(val1, val2, pVal, fVal);
}
Возможные варианты вызова этой функции:
int MMM;
int* pIval = &MMM;
/*
Указатель pIval используется для инициализации третьего параметра.
*/
MMM = 100;
/*
А значение объекту, на который настроен указатель pIval, может быть
изменено в любой момент.
*/
MonitorF(9,9,pIval,9.9);
/*
При вызове используем значение указателя на функцию, присвоенное последнему
параметру по умолчанию.
*/
MonitorF(11,11,pIval,11.11,MyFF3);

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

ReturnerF(int, int)
Определим теперь тип указателя на функцию, который будет возвращаться функцией ReturnerF(int, int).
char* (*)(int,int,int*,float)
Теперь остаётся правильно соединить обе части объявления.
char* (*ReturnerF(int, int))(int,int,int*,float);
Получилась такая вот матрёшка. Функция о двух целочисленных параметрах, возвращающая указатель на функцию, которая возвращает указатель на объект типа char и имеет собственный список формальных параметров вида: (int,int,int*,float). Нет предела совершенству!

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

Есть такие функции! Здесь их целых три: MyFF1, MyFF2, MyFF3.
Приступаем к реализации и параллельно обыгрываем параметры.
char* (*ReturnerF(int param1, int param2))(int,int,int*,float)
{
char* (*PointF) (int,int,int*,float);
/*
Это всего лишь указатель на функцию. Мы можем себе позволить этот пустяк.
*/
if (!param1) return NULL;
switch param2
{
case 1: PointF = MyFF1; break;
case 2: PointF = MyFF2; break;
case 3: PointF = MyFF3; break;
default: PointF = NULL; break;
}
return PointF;
}
Теперь только вызов! Наша функция возвращает адрес функции. И поэтому самое простое — это вызов функции непосредственно из точки возврата функции ReturnerF:
int val1, val2;
:::::
MyPointChar = (ReturnerF(val1,val2))(7,7,NULL,7.7);
Всё было бы хорошо, если бы только не существовала вероятность возвращения пустого указателя.
Так что придётся воспользоваться ранее
объявленным указателем на функцию, проверять возвращаемое значение и только потом вызывать функцию по
означенному указателю. Это не намного сложнее:
MyPtArray[3] = ReturnerF(val1,val2);
if (MyPtArray[3])
{MyPointChar = (MyPtArray[3])(7,7,NULL,7.7);}
/* Вот и элемент массива указателей пригодился.*/
Настало время вспомнить о typedef-спецификаторе. С его помощью запись указателя на функцию можно сделать компактнее:
typedef char* (*PPFF) (int,int,int*,float);
Здесь надо представлять всё ту же матрёшку. Замещающий идентификатор PPFF располагается внутри определяемого выражения. И вот новое объявление старой функции.
PPFF ReturnerF(int, int);
В процессе трансляции будет восстановлен исходный вид объявления.
Совместно используемые функции

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

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

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

Механизм совместного использования заключается в том, в ходе трансляции исходного кода переименовываются все функции. Новые имена создаются транслятором на основе старых имен и списков типов параметров. Никакие другие характеристики функция при создании новых имён транслятором не учитываются.

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

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

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

и при этом в процессе трансляции, к имени каждой из объявленных функций будет прибавлена специальная цепочка символов, зависящая от типа и порядка параметров функции. Конкретный алгоритм декодирования зависит от транслятора. В соответствии с представленной в книге Б.Бабэ схемой декодирования имён в Borland C++, декодированные имена четвёрки функций будут выглядеть следующим образом:
@max$qii
@max$qpii
@max$qipi
@max$qpipi
Заметим, что при кодировании имён транслятор не использует информацию о типе возвращаемых значений и поэтому пара функций
int max(int*,int*);
int * max(int*,int*);
должна была бы получить одно и то же декодированное имя @max$qpipi, что неизбежно вызвало бы сообщение об ошибке.

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

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

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

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

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

1.Точное сопоставление.
Точное сопоставление предполагает однозначное соответствие количества, типа и порядка значений параметров выражения вызова и параметров в определении функции.
// Произвольная функция, которая возвращает целое значение.
int iFunction(float, char *);
//Объявление пары совместно используемых функций…
extern void FF(char *); //Вариант 1…
extern void FF(int); //Вариант 2…
//Вызов функции.
FF(0);
FF(iFunction(3.14, «QWERTY»));
Поскольку нуль имеет тип int, оба вызова сопоставляется со вторым вариантом совместно используемой функции.

2.Сопоставление с помощью расширения типа.
При таком сопоставлении производится приведение типа значения параметра в выражении вызова к типу параметра в определении функции. Для этого используется расширение типа.
Если ни для одного из вызовов точного сопоставления не произошло, то применяются следующие расширения типа:
Параметр типа char, unsigned char или short расширяются до типа int. Параметр типа unsigned short расширяется до типа int, если размер объекта типа int больше размера объекта типа short (это зависит от реализации). Иначе он расширяется до типа unsigned int.
Параметр типа float расширяется до типа double.
//Объявление пары совместно используемых функций…
extern void FF(char *); //Вариант 1…
extern void FF(int); //Вариант 2…
//Вызов функции.
FF(‘a’);
Литера ‘a’ имеет тип char и значение, допускающее целочисленное расширение. Вызов сопоставляется со вторым вариантом совместно используемой функции.

3.Сопоставление со стандартным преобразованием. Применяется в случае неудачи сопоставления по двум предыдущим критериям сопоставления. Фактический параметр преобразуется в соответствии с правилами стандартных преобразований. Стандартное преобразование типа реализует следующие варианты сопоставления значений параметров в выражениях вызова и параметров объявления:
любой целочисленный тип параметра выражения вызова сопоставляется с любым целочисленным типом параметра, включая unsigned,
значение параметра, равное нулю, сопоставляется с параметром любого числового типа, а также с параметром типа указатель, а значение параметра типа указатель на объект (любого типа) будет сопоставляться с формальным параметром типа void*.
//Объявление пары совместно используемых функций…
extern void FF(char *); //Вариант 1…
extern void FF(float); //Вариант 2…
//Вызов функции.
FF(0);

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

точное сопоставление формального и фактического параметров оценивается максимальным баллом по шкале соответствия параметров,
сопоставление с расширением типа оценивается средним баллом,
сопоставление со стандартным преобразованием оценивается низшим баллом по шкале соответствия,
несоответствие фактического и формального параметров является абсолютным нулём нашей замечательной шкалы.
В качестве примера рассмотрим следующие ситуации сопоставления:
Объявляются четыре варианта совместно используемых функций.
extern void FF(unsgned int); //Вариант 1…
extern void FF(char*); //Вариант 2…
extern void FF(char); //Вариант 3…
extern void FF(int); //Вариант 4…
И ещё несколько переменных различных типов…
unsigned int iVal;
int *p_iVal;
unsigned long ulVal;
Рассмотрим вызовы функций.
Успешные:
FF(‘a’);
//Успешное сопоставление с вариантом 3.
FF(«iVal»);
//Успешное сопоставление с вариантом 2.
FF(iVal);
//Успешное сопоставление с вариантом 1.
Неудачные:
FF(p_iVal); //Сопоставления нет. FF(ulVal); /* Поскольку по правилам стандартного преобразования тип unsigned long, пусть с потерей информации, но всё же может быть преобразован в любой целочисленный тип, сопоставление окажется неуспешным по причине своей неоднозначности. Сопоставление происходит со всеми вариантами функции за исключением функции, имеющей тип char*. */

Решение относительно вызова совместно используемой функции с несколькими параметрами принимается на основе алгоритма сопоставления параметров к каждому из параметров вызова функции. При этом применяется так называемое правило пересечения. Согласно этому правилу, из множества совместно используемых функций выбирается функция, для которой разрешение каждого параметра будет НЕ ХУЖЕ (баллы по шкале соответствия), чем для всего множества совместно используемых функций, и ЛУЧШЕ (баллы по шкале соответствия), чем для всех остальных функций, хотя бы для одного параметра. Например:
extern MyFFF(char*, int);
extern MyFFF(int, int);
MyFFF(0, ‘a’);

По правилу пересечения выбирается второй вариант функции. И происходит это по двум причинам:
Сопоставление первого фактического параметра вызова функции и первого параметра второй функции оценивается высшим баллом по шкале соответствия параметров, поскольку константа 0 точно сопоставляется с формальным параметром типа int.

Второй параметр вызова сопоставляется со вторым формальным параметром обеих функций. При этом литера ‘a’ имеет тип char и значение, допускающее целочисленное расширение. Таким образом, имеет место сопоставление с помощью расширения типа.

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

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

char* MyFF1 (int,int,int*,float);
char* MyFF1 (int,int*,float);
/* Прототипы перегруженных функций. */
:::::
char* MyFF1 (int key1, int key2, int* pVal, float fVal) {/* … */}
char* MyFF1 (int XX, int* pXX, float FF) {/* … */}
/* Определения перегруженных функций. */
:::::
char* (*fPointer1) (int,int,int*,float) = MyFF1;
/* Определение и инициализация указателя на первую функцию.
Транслятор делает правильный выбор. */
char* (*fPointer2) (int,int*,float);
/* Определение указателя на вторую функцию. */
fPointer2 = MyFF1;
/* И опять транслятор правильно выбирает соответствующую функцию. */
fPointer1(1,2,NULL,3.14);
fPointer2(1,NULL,3.14);
/* Вызовы функций по указателю. */

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

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

void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float)=MyFF1);
/*
Транслятор утверждает, что имя этих функций двусмысленно в
контексте инициализации.
*/
MonitorF(9,9,pIval,9.9);
/*
Использование значения параметра по умолчанию также невозможно.
*/
void MonitorF(int,int,int*,float,char*(*)(int,int,int*,float));
MonitorF(11,11,pIval,11.11,MyFF1);
/*
При явном указании имени функции в операторе вызова транслятор
однозначно идентифицирует функцию.
*/
Класс. Объявление класса

Класс — это тип. Этот производный тип вводится в программу с помощью специального оператора объявления класса. В объявлении класса используется ранее описанный инструментальный набор средств для построения и преобразования производных типов.
Очередное множество форм Бэкуса-Наура определяет синтаксис объявления класса.
Объявление ::= [СписокСпецификаторовОбъявления] [СписокОписателей];
СписокСпецификаторовОбъявления
::= [СписокСпецификаторовОбъявления] СпецификаторОбъявления
СпецификаторОбъявления ::= СпецификаторТипа
::= *****
СпецификаторТипа ::= СпецификаторКласса
::= УточнённыйСпецификаторТипа
::= *****
УточнённыйСпецификаторТипа ::= КлючевоеСловоКласса ИмяКласса
::= КлючевоеСловоКласса Идентификатор
::= enum ИмяПеречисления
КлючевоеСловоКласса ::= union
::= struct
::= class
ИмяКласса ::= Идентификатор
СпецификаторКласса ::= ЗаголовокКласса {[СписокЧленов]}
ЗаголовокКласса
::= КлючевоеСловоКласса [Идентификатор] [СпецификацияБазы]
::= КлючевоеСловоКласса ИмяКласса [СпецификацияБазы]
КлючевоеСловоКласса ::= union
::= struct
::= class
ИмяКласса ::= Идентификатор

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

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

Предварительное объявление обеспечивается уточнённым спецификатором типа и является своеобразным прототипом класса или перечисления. Его назначение — сообщение транслятору предварительной информации о том, что существует (должно существовать) объявление класса (или перечисления) с таким именем. Идентификатор, используемый в контексте уточнённого спецификатора имени становится именем класса (или именем перечисления).
Класс считается объявленным даже тогда, когда в нём полностью отсутствует информация о членах класса (пустой список членов класса). Неименованный класс с пустым множеством членов — уже класс!
Имя класса можно употреблять как имя (имя типа) уже в списке членов этого самого класса.
Класс может быть безымянным.
Следующая последовательность операторов объявления
class {}; /* Объявлен пустой неименованный класс.*/
class {};
class {};
class {};
/* Это всё объявления. Их количество ничем не ограничивается. */
struct {};
/* Структура — это класс, объявленный с ключевым словом struct.
Опять же пустой и неименованный.*/
не вызывает у транслятора никаких возражений.

На основе класса, пусть даже неименованного, может быть объявлен (вернее, определён) объект-представитель этого класса. В таком контексте объявление неименованного (пусть даже и пустого!) класса является спецификатором объявления. Имена определяемых объектов (возможно с инициализаторами) составляют список описателей.
class {} Obj1, Obj2, Obj3;/* Здесь объявление пустого класса.*/
class {} Obj4, Obj5, Obj6;/* Просто нечего инициализировать.*/
class {} Obj1;
/* ^ Ошибка. Одноименные объекты в области действия имени.*/

Неименованные классы также можно применять в сочетании со спецификатором typedef (здесь может быть объявление класса любой сложности — не обязательно только пустой). Спецификатор typedef вводит новое имя для обозначения безымянного класса. Описанное имя типа становится его единственным именем.
Сочетание спецификатора typedef с объявлением безымянного класса подобно объявлению класса с именем:
class MyClass {/*┘*/};
typedef class {/*┘*/} MyClass;
Правда в первом случае класс имеет собственное имя класса, а во втором — описанное имя типа. Использование описанного имени типа в пределах области действия имени делает эквивалентными следующие определения (и им подобные):
class {} Obj1;
MyClass Obj1;

Класс считается объявленным лишь после того, как в его объявлении будет закрыта последняя фигурная скобка. До этого торжественного момента информация о структуре класса остаётся неполной.
Если можно ОБЪЯВИТЬ пустой класс, то можно ОПРЕДЕЛИТЬ и объект-представитель пустого класса. Эти объекты размещаются в памяти. Их размещение предполагает выделение объекту участка памяти с уникальным адресом, а это означает, что объекты пустого класса имеют ненулевой размер.
Действительно, значения выражений sizeof(MyClass) и sizeof(MyObj1) (это можно очень просто проверить) отличны от нуля.
А вот пустое объединение (ещё одна разновидность класса — класс, объявленный с ключевым словом union) не объявляется:
union {}; /* Некорректное объявление объединения. */

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

СписокЧленов ::= ОбъявлениеЧленаКласса [СписокЧленов]
::= СпецификаторДоступа : [СписокЧленов]
ОбъявлениеЧленаКласса ::= [СписокСпецификаторовОбъявления]
[СписокОписателейЧленовКласса];
::= ОбъявлениеФункции
::= ОпределениеФункции [;]
::= КвалифицированноеИмя;
СписокОписателейЧленовКласса ::= ОписательЧленаКласса
::= СписокОписателейЧленовКласса,
ОписательЧленаКласса
ОписательЧленаКласса ::= Описатель [ЧистыйСпецификатор]
::= [Идентификатор] : КонстантноеВыражение
ЧистыйСпецификатор ::= = 0
КвалифицированноеИмяКласса ::= ИмяКласса
::= ИмяКласса :: КвалифицированноеИмяКласса
СпецификаторДоступа ::= private
::= protected
::= public

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

В соответствии с синтаксическими правилами, членами класса могут быть как определения функций, так и их прототипы. Действительно:
ОбъявлениеЧленаКласса ::=
[СписокСпецификаторовОбъявления] [СписокОписателейЧленовКласса]; ::=
СпецификаторОбъявления ОписательЧленаКласса; ::=
СпецификаторТипа Описатель; ::=
void Описатель (СписокОбъявленийПараметров); ::=
void ff (void);
С другой стороны,
ОбъявлениеЧленаКласса ::=
ОпределениеФункции [;] ::=
Описатель (СписокОбъявленийПараметров) ТелоФункции ::=
ff (void) {int iVal = 100;}
В соответствии с синтаксическими правилами, членами класса могут быть как определения функций, так и их прототипы. Действительно:
ОбъявлениеЧленаКласса ::=
[СписокСпецификаторовОбъявления] [СписокОписателейЧленовКласса]; ::=
СпецификаторОбъявления ОписательЧленаКласса; ::=
СпецификаторТипа Описатель; ::=
void Описатель (СписокОбъявленийПараметров); ::=
void ff (void);
С другой стороны,
ОбъявлениеЧленаКласса ::=
ОпределениеФункции [;] ::=
Описатель (СписокОбъявленийПараметров) ТелоФункции ::=
ff (void) {int iVal = 100;}

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

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

Наличие функций-членов делает объявление класса подобным определению (как и любые функции, функции-члены определяются). Как сказано в Справочном руководстве по C++, «Если бы не исторические причины, объявление класса следовало называть определением класса».

Данные-члены класса не могут объявляться со спецификаторами auto, extern, register.

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

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

Описатель члена класса в объявлении класса не может содержать инициализаторов (это всего лишь объявление).
Структура является классом, объявленным с ключевым словом класса struct. Члены такого класса и базовые классы по умолчанию обладают спецификацией доступа public.

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

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

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

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

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

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

Функция-член класса существует в единственном экземпляре для всех объектов-представителей данного класса. Переобъявление и уточнение структуры класса в С++ недопустимо.
Серия простых примеров демонстрирует, что можно, а что нельзя делать при объявлении данных-членов класса.
class C1
{
C1 MyC;
// Это ошибка. В классе не допускается объявления данных-членов
// объявляемого класса.
C1* pMyC;
// А указатель на класс объявить можно.
};
Для объявления таких указателей или ссылок на объекты объявляемого класса достаточно неполного предварительного объявления класса. Указатели и ссылки имеют фиксированные размеры, которые не зависят от типа представляемого объекта.
class C2;
class C1
{
C1* pMyC1;
C2* pMyC2;
};
C2* PointOnElemOfClassC2;
Назначение неполного объявления подобно прототипу функции и используется исключительно в целях предварительного информирования транслятора. Очевидно, что создание объектов на основе предварительного неполного объявления невозможно. Однако это не снижает ценности уточнённого спецификатора.
На втором проходе трансляции объявления класса осуществляется проверка списков параметров в объявлениях функций-членов класса, и определяется размер класса. К этому моменту транслятору становится известна общая структура класса. И потому, как ни странно это выглядит, в классе может быть объявлена функция-член класса, которая возвращает значение объявляемого класса и содержит в списке параметров параметры этого же класса:
class C2;
class C1
{
C1 F1(C1 par1) {return par1;};
//Объявить данные-члены класса C1 нельзя, а функцию — можно!
C1* pMyC1;
C2* pMyC2;
// C1 MyC;
};
C2* PointOnElemOfClassC2;
Где бы ни располагалась объявляемая в классе функция-член, транслятор приступает к её разбору лишь после того, как он определяет общую структуру класса.
В соответствии с формальным определением создадим наш первый класс:
СпецификаторКласса ::= ЗаголовокКласса { [СписокЧленов] }; ::=
КлючевоеСловоКласса Идентификатор { ОбъявлениеЧленаКласса
ОбъявлениеЧленаКласса }; ::=
class FirstClass { СпецификаторОбъявления ОписательЧленаКласса;
ОписаниеФункции; }; ::=
class FirstClass { СпецификаторОбъявления ОписательЧленаКласса;
int FirstClassFunction(void);}; ::=
class FirstClass {
long int* PointerToLongIntVal;
int FirstClassFunction(void);
};
За исключением квалифицируемого имени синтаксис определения функции-члена класса вне класса ничем не отличается от определения обычной функции:
int FirstClass::FirstClassFunction(void)
{
int IntVal = 100;
return IntVal;
};
Вот таким получилось построенное в соответствии с грамматикой C++ определение (или объявление) класса.
Заметим, что в C++ существует единственное ограничение, связанное с расположением определения функции-члена класса (конечно, если оно располагается вне тела класса): определение должно располагаться за объявлением класса, содержащего эту функцию. Именно «за объявлением»! Без каких-либо дополнительных ограничений типа «непосредственно за» или «сразу за».
Более того, в ряде случаев, например, когда требуется определить функцию-член, изменяющую состояние объекта другого класса, данная функция-член должна располагаться за объявлением класса, состояние объекта которого она изменяет. И это понятно. При разборе такой функции-члена транслятор должен иметь представление о структуре класса.
Допускается и такая схема расположения объявлений, при которой первыми располагаются неполные объявления классов, следом соответствующие объявления классов и лишь затем определения функций-членов. Подобные определения мы будем называть отложенными определениями. Позже мы рассмотрим пример программы, в которой отложенный вариант определения функции-члена является единственно возможным вариантом определения.
Класс — это то, что делает C++ объектно-ориентированным языком. На основе классов создаются новые производные типы и определяются функции, которые задают поведение типа.
Рассмотрим несколько строк программного кода, демонстрирующих свойства производных типов.
class Class1 {int iVal;};
class Class2 {int iVal;};
/*
Объявление производных типов Class1 и Class2. Эти объявления
вводят в программу два новых производных типа. Несмотря на
тождество их структуры, это разные типы.
*/
void ff(Class1);
/* Прототип функции с одним параметром типа Class1.*/
void ff(Class2);
/*
Прототип функции с одним параметром типа Class2. Это совместно
используемые (или перегруженные) функции. Об этих функциях мы
уже говорили.
*/
Class1 m1; /* Объявление объекта m1 типа Class1. */
Class2 m2; /* Объявление объекта m2 типа Class2. */
int m3;
m1 = m2;
m1 = m3;
m3 = m2;
/*
Последние три строчки в данном контексте недопустимы.
Неявное преобразование с участием производных типов в C++
невозможно. Транслятор не имеет никакого понятия о том, каким
образом проводить соответствующее преобразование. При объявлении
классов необходимо специально определять эти алгоритмы.
*/
void ff (Class1 pp)
// Определение первой совместно используемой функции…
{
:::::
}
void ff (Class2 pp)
// Определение второй совместно используемой функции…
{
:::::
}
ff(m1);//Вызов одной из двух совместно используемых функций…
ff(m2);//Вызов второй функции…
Ещё один пример объявления класса.
class ClassX
{
ClassX Mm; //Здесь ошибка. Объявление класса ещё не завершено.
ClassX* pMm; //Объект типа «Указатель на объект». Всё хорошо.
ClassX FF(char char,int i = sizeof(ClassX));
/*
Прототип функции. Второму параметру присваивается значение по
умолчанию. И напрасно! Здесь ошибка. В этот момент ещё неизвестен
размер класса ClassX.
*/
// А вот вполне корректное определение встроенной функции.
int RR (int iVal)
{
int i = sizeof(ClassX);
return i;
}
/*
Полный разбор операторов в теле функции производится лишь после
полного разбора объявления класса. К этому моменту размер класса
уже будет определён.
*/
}

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты