if (p == NULL)
{ cout <<"He создан динамический массив!";
exit (l);
}
// Цикл создания одномерных массивов:
for (int i = 0; i < n; i++)
{ // Формирование строки элементов типа int:
p [i] = new int [n];
if (p [i] == NULL)
{ cout <<"He создан динамический массив!";
exit (l);
}
// Внутренний цикл заполнения строки:
for (int j = 0; j < n; j++)
if (j! = i) p [i][j] = 0; еlsе p [i][j] = 1;
}
return p;
}
void main ()
{ int n; // Порядок матрицы
cout <<"\n Введите порядок матрицы: ";
cin >> n;
int **matr; // Указатель для формируемой матрицы
// Обращение к функции для создания единичкой матрицы:
matr = single_matr (n);
for (int i = 0; i < n; i++) // Цикл перебора строк
{cout <<"\n строка " <<(i + 1) <<":";
// Цикл печати элементов строки:
for (int j = 0; j < n; j++)
cout <<"\t" << matr [i][j];
}
// Очистка памяти от динамических массивов:
for (i = 0; i < n; i++)
delete matr [i];
delete [] matr;
}
Результат выполнения программы:
Введите порядок матрицы: 4 < Enter >
строка 1: 1 0 0 0
строка 2: 0 1 0 0
строка 3: 0 0 1 0
строка 4: 0 0 0 1
Обратите внимание на тот факт, что динамические массивы создаются в функции, вызванной из основной программы, а доступ к ним выполняется в тексте основной программы. Здесь же освобождается память от динамических массивов перед окончанием программы.
Следующая программа иллюстрирует передачу матрицы через параметры, создание внутри функции динамических массивов для представления еще одной матрицы и возврат в точку вызова указателя на вновь созданную матрицу. Функция reduction () решает следующую задачу: по заданной прямоугольной матрице с размерами n на m и номерам строки 0 < k < n + 1 и столбца 0 < / < т + 1 сформировать новую матрицу с размерами (n - 1) на (m - 1), исключив из исходной матрицы k-ю строку и l-й столбец. Текст программы:
//Р6-16.СРР - создание матрицы по заданной матрице
#include < iostream.h >
// Функция, определяющая значения элементов матрицы:
int **reduction (int n, int m, int **matr, int k, int l)
{int **p; // Указатель на формируемую матрицу
int ii, jj;
p = new (int *[n-1]); // Массив указателей на строки
for (int i = 0; i < n - 1; i++)
p [i] = new (int [n-1]); // Формирование i-й строки
// Цикл перебора строк:
for (i - 0, ii = 0; i < n - 1; i++, ii++)
{if (i == k) ii++; // Пропускаем k-ю строку
// Цикл заполнения строки:
for (int j = 0, jj = 0; j < m - 1; j++, jj++)
{// Пропускаем 1-й элемент в строке:
if (j == 1) jj++;
p [i] [j] = matr [ii] [jj];
}
}
return p;
}
void main ()
{ int x[4] [6] = {11, 12, 13, 14, 15, 16,
21, 22, 23, 24, 25, 26,
31, 32, 33, 34, 35, 36,
41, 42, 43, 44, 45, 46);
int *px [4]; pac [0] = (int *)&x [0];
px [1] = (int *)&x [1];
px [2] = (int *)&x [2];
px [3] = (int *)&x [3];
int n = 3, m = 5;
int **ррm;
ррm = reduction(4, 6, рх, 2, 3);
for (int i = 0; i < n; i++) // Цикл перебора строк
{cout <<"\n строка " <<(i + 1) <<" : ";
// Цикл печати элементов строки:
for (int j = 0; j < m; j++)
cout <<"\t" << ppm [i] [j];
}
for (i = 0; i < n; i++)
delete ppm [i];
delete [] ppm;
}
Результат выполнения программы:
строка 1: 11 12 13 15 16
строка 2: 21 22 23 25 26
строка 3: 41 42 43 45 46
Прежде чем вводить указатель на функцию, напомним, что каждая функция характеризуется типом возвращаемого значения, именем и сигнатурой. Напомним, что сигнатура определяется количеством, порядком следования и типами параметров. Иногда говорят, что сигнатурой функции называется список типов ее параметров. При использовании имени функции без последующих скобок и параметров имя функции выступает в качестве указателя на эту функцию, и его значением служит адрес размещения функции в памяти. Это значение адреса может быть присвоено другому указателю, и затем уже этот новый указатель можно применять для вызова функции. Однако в определении нового указателя должен быть тот же тип, что и возвращаемое функцией значение, и та же сигнатура. Указатель на функцию определяется следующим образом:
тип_функции (*имя указателя) (спецификация_параметров);
Например: int (*funclPtr) (char); - определение указателя funclPtr на функцию с параметром типа char, возвращающую значение типа int.
Если приведенную синтаксическую конструкцию записать без первых круглых скобок, т.е. в виде int *fun(char); то компилятор воспримет ее как прототип некой функции с именем fun и параметром типа char, возвращающей значение указателя типа int *.
Второй пример: char * (*func2Ptr) (char *,int); - определение указателя func2ptr на функцию с параметрами типа указатель на char и типа int, возвращающую значение типа указатель на char.
В определении указателя на функцию тип возвращаемого значения и сигнатура (типы, количество и последовательность параметров) должны совпадать с соответствующими типами и сигнатурами тех функций, адреса которых предполагается присваивать вводимому указателю при инициализации или с помощью оператора присваивания. В качестве простейшей иллюстрации сказанного приведем программу с указателем на функцию:
//Р6-17.СРР - определение и использование указателей
// на функции
#include < iostream.h > // Для ввода-вывода
void fl (void) // Определение fl
{ cout <<"\nВыполняется fl() ";
}
void f2 (void) // Определение f2
{cout <<"\nВыполняется f2 ()>>;
}
void main ()
{void (*ptr)(void); // ptr - указатель на функцию
ptr = f2; // Присваивается адрес f2 ()
(*ptr)(); // Вызов f2 () no ее адресу
ptr = fl; // Присваивается адрес f1 ()
(*ptr)(); // Вызов fl () по ее адресу
ptr (); // Вызов эквивалентен (*ptr) ();
}
Результат выполнения программы:
Выполняется f2()
Выполняется fl()
Выполняется fl ()
В программе описан указатель ptr на функцию, и ему последовательно присваиваются адреса функций f2 и f1. Заслуживает внимания форма вызова функции с помощью указателя на функцию:
(*имя_ указателя) (список_фактических_параметров);
Здесь значением имеии_указателя служит адрес функции, а с помощью операции разыменования * обеспечивается обращение по адресу к этой функции. Однако будет ошибкой записать вызов функции без скобок в виде *ptr(). Дело в том, что операция () имеет более высокий приоритет, нежели операция обращения по адресу *. Следовательно, в соответствии с синтаксисом будет вначале сделана попытка обратиться к функция ptr(). И уже к результату будет отнесена операция разыменования, что будет воспринято как синтаксическая ошибка.
При определении указатель на функцию может быть инициализирован. В качестве инициализирующего значения должен использоваться адрес функции, тип и сигнатура которой соответствуют определяемому указателю.
При присваивании указателей на функции также необходимо соблюдать соответствие типов возвращаемых значений функций и сигнатур для указателей правой и левой частей оператора присваивания. То же справедливо и при последующем вызове функций с помощью указателей, т.е. типы и количество фактических параметров, используемых при обращении к функции по адресу, должны соответствовать формальным параметрам вызываемой функции. Например, только некоторые из следующих операторов будут допустимы:
char f1 (char) {...} // Определение функции
char f2 (int.) {...} // Определение функции
void f3 (float) {...} // Определение функции
int* f4 (char *) {...} // Определение функции
char (*pt1)(int); // Указатель на функцию
char (*pе2)(int); // Указатель на функцию
void (*ptr3) (float) = f3; // Инициализированный указатель
void main ()
{pt1 = f1; // Ошибка - несоответствие сигнатур
pt2 = f3; // Ошибка - несоответствие типов
// (значений и сигнатур)
pt1 = f4; // Ошибка - несоответствие типов
pt1 = f2; // Правильно
pt2 = pt1; // Правильно
char с = (*pt1)(44); // Правильно
с = (*pt2)('\t'); // Ошибка - несоответствие сигнатур
}
Следующая программа иллюстрирует гибкость механизма вызовов функций с помощью указателей.
//Р6-18.СРР - вызов функций по адресам через указатель
#include < iostream.h >
// Функции одного типа с одинаковыми сигнатурами:
int add(int n, int m) { return n + m; }
int div(int n, int m) { return n/m; }
int mult(int n, int m) { return n * m; }
int subt(int n, int m) { return т-m; }
void main()
{ int (*par)(int, int); // Указатель на функцию
int а = 6, b = 2;
char с = '+';
while (с != ' ')
{ cout <<"\n Аргументы: а = " <<а " ", b = " << b;
cout <<". Результат для с = \' " <<с <<" \ ' " <<"
равен ";
switch (с)
{ саsе '+': par = add; с = '/ '; break;
case ' - ': par = subt; с = ' '; break;
case ' * ': par = mult; с = ' - ' ; break;
case '/': par = div; с = '*'; break;
}
cout <<(a = (*par)(a,b)); // Вызов по адресу
}
}
Результаты выполнения программы:
Аргументы: а = 6, b = 2. Результат для с = '+' равен 8
Аргументы: а = 8, b = 2. Результат для с = '/' равен 4
Аргументы: а = 4, b = 2. Результат для с = '*' равен 8
Аргументы: а = 8, b = 2. Результат для с = '-' равен 6
Цикл продолжается. Пока значением переменной с не станет пробел. В каждой итерации указатель par получает адрес одной из функций, и изменяется значение с. По результатам программы легко проследить порядок выполнения ее операторов.
Указатели на функции могут быть объединены в массивы. Например, float (*ptrArray) (char) [4]; - описание массива с именем ptrArray из четырех указателей на функции, каждая из которых имеет параметр типа char и возвращает значение типа float. Чтобы обратиться, например, к третьей из этих функций, потребуется такой оператор:
float a = (*ptrArray [2]) ('f');
Как обычно, индексация массива начинается с 0, и поэтому третий элемент массива имеет индекс 2.
Приведенное описание массива ptrArray указателей на функции не особенно наглядное и ясное. Для функций с большим количеством параметров и сложным описанием возвращаемых значений оно может стать и вовсе непонятным. Ситуация особенно усложняется, когда такое описание типа должно входить в более сложные описания, например, когда указатели функций и их массивы используются в качестве параметров других функций. Для удобства последующих применений и сокращения производных описаний рекомендуется с помощью спецификатора typedef вводить имя типа указателя на функции:
typedef float (*PTF)(float);
typedef char *(*PTC)(char);
typedef void (*PTFUNC) (PTF, int, float);
Здесь PTF - имя типа "указатель на функцию с параметром типа float, возвращающую значение типа float". PTC - имя типа "указатель на функцию с параметром типа char, возвращающую указатель на тип char". PTFUMC - имя типа "указатель на функцию, возвращающую пустое значение (типа void)". Параметрами для этой функции служат: PTF - указатель на функцию float имя (float) , выражение типа int и выражение типа float. (В определение имени типа PTFUNC вошел только что определенный тип с именем PTF.)
Введя имена типов указателей на функции, проще описать соответствующие указатели, массивы и другие производные типы:
PTF ptfloat1, ptfloat2[5]; // Указатель и массив
// указателей на функции
// float имя(float)
РТС ptchar; // Указатель на функцию
char *(char) PTFUNC ptfunc[8]; // Массив указателей на функции
Массивы указателей на функции удобно использовать при разработке всевозможных меню, точнее программ, управление которыми выполняется с помощью меню. Для этого действия, предлагаемые на выбор будущему пользователю программы, оформляются в виде функций, адреса которых помещаются в массив указателей на функции. Пользователю предлагается выбрать из меню нужный ему пункт (в простейшем случае он вводит номер выбираемого пункта) и по номеру пункта, как по индексу, из массива выбирается соответствующий адрес функции. Обращение к функции по этому адресу обеспечивает выполнение требуемых действий. Самую общую схему реализации такого подхода иллюстрирует следующая программа для обработки файлов:
//Р6-19.СРР - массив указателей на функции
#include < stdlib.h > // Для exit()
#include < iostream.h > // Для cout, cin
// Определение функций для обработки меню:
void act1(char* nаme)
{ cout <<"Действия по созданию файла " << name;}
void act2(char* name)
{ cout <<"Действия по уничтожению файла " << name;}
void act3(char* name)
{ cout <<"Действия по чтению файла*" << name;}
void act4(char* nаmе)
{ cout <<"Действия по модификации файла " << name;}
void act5(char* name)
{ cout <<"Действия по закрытию файла.";
exit(0); // Завершить программу
}
// Тип MENU указателей на функции типа void (char *):
typedef void(*MENU)(char *);
// Инициализация таблицы адресов функций меню:
MENU MenuAct[5] = { act1, act2, act3, act4, act5 };
void main()
{ int number; // Номер выбранного пункта меню
char FileName[30]; // Строка для имени файла
cout <<"\n 1 - создание файла";
cout <<"\n 2 - уничтожение файла";
cout <<"\n 3 - чтение файла";
cout <<"\n 4 - модификация файла";
cout <<"\n 5 - выход из программы";
while (1) // Бесконечный цикл
{while (1)
{ // Цикл продолжается до ввода правильного номера
cout <<"\n\nВведите номер пункта меню:";
cin >> number;
if (number >= 1 && number <= 5) break;
cout <<"\n0шибка в номере пункта меню!";
}
if (number!=5)
{cout << "Введите имя файла: ";
cin >> FileName; // Читать имя файла
}
// Вызов функции по указателю на нее:
(*MenuAct(number-l])(FileName);
} // Конец бесконечного цикла
}
При выполнении программы возможен, например, такой диалог:
1 - создание файла
2 - уничтожение файла
3 - чтение файла
4 - модификация файла
5 - выход из программы
Введите номер пункта меню: 3 < Enter >
Введите имя файла: PROBA.TXT < Enter >
Действия по чтению файла PROBA.TXT
Введите номер пункта меню: 5 < Enter >
Действия по закрытию файла.
Пункты меню повторяются, пока не будет введен номер 5 - выход из программы.
Указатели на функции - незаменимое средство языков Си и Си++, когда объектами обработки должны служить функции. Например, создавая процедуры для вычисления определенного интеграла задаваемой пользователем функции или для построения таблицы значений произвольной функции, нужно иметь возможность передавать в программы функции. Удобнее всего организовать связь между функцией, реализующей метод обработки (например, численный метод интегрирования), и той функцией, для которой этот метод нужно применить, через аппарат параметров, в число которых входят указатели на функции.
Рассмотрим задачу вычисления корня функции f(х) на интервале локализации [А; В] для иллюстрации особенностей применения указателя функции в качестве параметра. Численный метод (деление пополам интервала локализации) оформляется в виде функции со следующим заголовком:
float root (указатель_на_функцию, float A, float В, float EPS)
Здесь А - нижняя граница интервала локализации корня; в - верхняя граница того же интервала; EPS - требуемая точность определения корня. Введем тип "указатель на функцию", для которой нужно определить корень:
typedef float(*pointFunc)(float);
Теперь можно определить функцию для вычисления корня заданной функции с указателем pointFunc. Прототип функции будет таким:
float root(pointFunc F, float A, float В, float EPS);
Приведем тестовую программу для определения с помощью root () корня функции у = х2 - 1 на интервале [0,2]:
//Р6-20.СРР - указатель функции как параметр функции
#include < iostream.h >
#include < stdlib.h > // Для функции exit()
// Определение типа указателя на функцию:
typedef float(*pointFunc)(float);
// Определение функции для вычисления корня:
float root(pointFunc F, float A, float В, float EPS)
{ float x, y, c, Fx, Fy, Fc;
x = А; у = В;
Fx = (*F) (x); // Значение функции на левой границе
Fy = (*F) (у); // Значение функции на правой границе
if (Fx * Fy > 0.0)
{ cout <<"\nНеверен интервал локализации";
exit(1); // Аварийное завершение программы
}
do
{ с = (у - х)/2; // Центр интервала локализации
Fc = (*F)(с); // Значение функции в с
if (Fc * Fx > 0) {х = с; Fx = Fc;}
else {у = с; Fy = Fc;}
} while (Fc!= О && у - х > EPS);
return с;
}
#include < math.h >
// Определение тестовой функции у = х * х - 1:
float testfunc(float x)
{ return х * х - 1;}
void main()
{ float root(pointFunc, float, float, float); // Прототип
float result;
result = root(testfunc, 0.0, 2.0, le-5);
cout <<"\nКорень тестовой функции: " << result;
}
Результат выполнения программы:
Корень тестовой функции: 1
Опыт работы на языках Си и Си++ показал, что даже не новичок в области программирования испытывает серьезные неудобства, разбирая синтаксис определения конструкций, включающих указатели на функции. Например, не каждому сразу становится понятным такое определение прототипа функции [6,11]:
void qsort(void *base, size_ t nelem, size_ t width,
int (*fcmp)(const void *pl, const void *p2));
Это прототип функции быстрой сортировки, входящей в стандартную для системы UNIX и для языка ANSI Си библиотеку функций. Прототип находится в заголовочном файле stdlib.h. Разберем элементы прототипа и напишем программу, использующую указанную функцию. Функция qsort() сортирует содержимое таблицы однотипных элементов, постоянно вызывая функцию сравнения, подготовленную пользователем. Для вызова функции сравнения ее адрес должен заместить указатель fcmp, специфицированный как формальный параметр. Итак, для использования qsort() программист должен подготовить таблицу сортируемых элементов в виде одномерного массива фиксированной длины и написать функцию, позволяющую сравнивать два любых элемента сортируемой таблицы. Остановимся на параметрах функции qsort ():
base указатель на начало таблицы сортируемых элементов (адрес 0-го элемента массива);
nelem количество элементов в таблице (целая величина, не большая размера массива);
width размер элемента таблицы (целая величина, определяющая в байтах размер одного элемента массива);
fcmp указатель на функцию сравнения, получающую в качестве параметров два указателя p1, р2 на элементы таблицы и возвращающую в зависимости от результата сравнения целое число: если *pl < *p2, tcmp возвращает целое < 0;
если *р1 == *р2, fcmp возвращает 0;
если *pl > *p2, fcmp возвращает целое > 0.
При сравнении символ "меньше чем" (<) означает, что после сортировки левый элемент отношения *р1 должен оказаться в таблице перед правым элементом *р2, т.е. значение *р1 должно иметь меньшее значение индекса в массиве, нежели *р2. Аналогично (но обратно) определяется расположение элементов при выполнении соотношения "больше чем" (>).
В следующей программе функция qsort() используется для упорядочения массива указателей на строки разной длины. Упорядочение должно быть выполнено таким образом, чтобы последовательный перебор массива указателей позволял получать строки в алфавитном порядке. Сами строки в процессе сортировки не меняют своих положений. Изменяются только значения указателей в массиве.
//Р6-21.СРР - упорядочение с помощью библиотечной функции
// qsort().
#include < iostream.h >
#include < stdlib.h > // Для функции qsort()
#include < string.h > // Для сравнения строк: strcap()
// Определение функции для сравнения:
int sravni(const void *a, const void *b)
{ unsigned long *pa = (unsigned long *)a,
*pb = (unsigned long *)b;
return strcmp((char *)*pa, (char *)*pb);
}
void main()
{ char *pc[] - {
"Sine Cura - Синeкура",
"Pro Forma - Ради формы",
"Differentia Specifica -"
"\n\t\t Отличительная особенность",
"Alea Jacta Est! - Жребий брошен!",
"Idem Per Idem -"
"\n\t\t Определение через определяемое",
"Fiat Lux! - Да будет свет!",
"Multa Pan Cis - Многое в немногих словах" } ;
// Размер таблицы:
int n = sizeof(pc)/sizeof(pc[0]);
cout <<"\n До сортировки:" << hex;
for (int i = 0; i < n; i++)
cout <<"\npc[" << i << "]=" <<
(unsigned long)pc[i] <<" -> " << pc[i];
// Вызов функции упорядочения:
qsort((void *)pc, // Адрес начала сортируемой таблицы
n, // Количество элементов сортируемой таблицы
sizeof(pc[0]), // Размер одного элементa
sravni // Имя функции сравнения (указатель);
}
cout <<"\n\n После сортировки:";
for (i = 0; i < n; i++)
cout <<"\nрc[" << i <<"]=" <<
(unsigned long)pc[i] <<" -> " << pc[il;
}
Результат выполнения программы:
До сортировки:
pc[0]=2de40Oac -> Sine Cura - Синекура
pc[l]=2de400cl -> Pro Forma - Ради формы
pc[2]=2de400d8 -> Differentia Specifica -
Отличительная особенность
pc[3]=2de4010e -> Alea Jacta Eat! - Жребий брошен!
pc[4]=2de4012f -> Idea Per Idem -
Определение через определяемое
pc[5]=2de40162 -> Fiat Lux! - Да будет свет!
pc[6]=2de4017d -> Multa Pan Cis - Многое в немногих словах
После сортировки:
pc[0]=2de4010e -> Alea Jacta Eat! - Жребий брошен!
pc[l]=2de400d8 -> Differentia Specifica -
Отличительная особенность
pc[2]=2de40162 -> Fiat Lux! - Да будет свет!
pc[3]=2de4012f -> Idem Per Idem -
Определение через определяемое
pc[4]=2de4017d -> Multa Pan Cis - Многое в немногих словах
pc[5]=2de400cl -> Pro Forma - Ради формы
pc[6]=2de400ac -> Sine Cura - Синекура
Обратите внимание на значения указателей pc[i]. До сортировки разность между рс[1] ирс[0] равна длине строки "Sine Cura - Синекура" и т.д. После упорядочения рс[0] получит исходное значение рс[3] и т.д.
Для выполнения сравнения строк (а не элементов массива рс[]) в функции sravni() использована библиотечная функция stromp(), прототип которой в заголовочном файле string. h имеет вид:
int strcmp(conat char *s1, const char *a2);
Функция strcmp() поддерживается системой UNIX и выполняет беззнаковое сравнение строк, связанных с указателями а1 и o2. Сравнение выполняется без учета регистров набора букв латинского алфавита. Функция выполняет сравнение посимвольно, начиная с начальных символов строк и до тех пор, пока не встретятся несовпадающие символы либо не закончится одна из строк.
Прототип функции strcmp() требует, чтобы параметры имели тип (const char *). Входные параметры функции sravni() имеют тип (conat void *), как предусматривает определение функции qsort (). Необходимые преобразования для наглядности выполнены в два этапа. В теле функции sravni () определены два вспомогательных указателя типа (unsigned long *), которым присваиваются значения адресов элементов сортируемой таблицы (элементов массива рс[3) указателей. В свою очередь, функция strcmp() получает разыменования этих указателей, т.е. адреса символьных строк. Таким образом, выполняется сравнение не элементов массива char* pc [], a тех строк, адреса которых являются значениями pc[i]. Однако функция qsort () работает с массивом рс[] и меняет местами только значения его элементов. Последовательный перебор массива рс[] позволяет в дальнейшем получить строки в алфавитном порядке, что иллюстрирует результат выполнения программы. Так как pc[i] -указатель на некоторую строку, то его разыменование в операции вывода в поток cout выполняется автоматически.
Если не использовать вспомогательных указателей ра, рb, то функцию сравнения строк можно вызвать из тела функции sravnio таким оператором:
return strcmp((char *)(*(unsigned long *)a), (char *)(*(unsigned long *) b) );
Здесь каждый родовой указатель (void *) вначале преобразуется к типу (unsigned long *). Последующее разыменование "достает" из четырех смежных байтов значение соответствующего указателя pc[i]. И уж затем преобразование (char *) формирует указатель на строку, который нужен функции strcmp ().
В языке Си++ ссылка определена как другое имя уже существующего объекта. Основные достоинства ссылок проявляются при работе с функциями, однако ссылки могут использоваться и безотносительно к функциям. Для определения ссылки используется символ &, если он употребляется в таком контексте:
type & имя_ссылки инициализатор
В соответствии с синтаксисом инициализатора, наличие которого обязательно, определение ссылки может быть таким:
type & имя_ссылки = выражение;
или
type& имя_ ссылки(выражение);
Раз ссылка есть "другое имя уже существующего объекта", то в качестве инициализирующего выражения должно выступать имеющее значение лево допустимое выражение, т.е. имя некоторого объекта, имеющего место в памяти. Значением ссылки после определения с инициализацией становится адрес этого объекта. Примеры определений ссылок:
int L = 777; // Определена и инициализирована переменная L
int& RL = L; // Значением ссылки RL является адрес
// переменной L
intf& RI(0); // Опасная инициализация - значением ссылки RI
// становится адрес объекта, в котором
// временно размерено нулевое целое значение
В определении ссылки символ ' & ' не является частью типа, т.е. RL или RX имеют тип int и именно так должны восприниматься в программе.
Итак, имя_ссылки определяет местоположение в памяти инициализирующего выражения, т.е. значением ссылки является адрес объекта, связанного с инициализирующим выражением.
Функционально ссылка ведет себя подобно обычной переменной того же, что и ссылка, типа. Для доступа к содержимому участка памяти, на который "смотрит" ссылка, нет необходимости явно выполнять разыменование, как это нужно для указателя. Если рассматривать переменную как пару "имя_пeрeменной - значение_переменной", то инициализированная этой переменной ссылка может быть представлена парой "имя_ссылки - значение_переменной". Из этого становится понятной необходимость инициализации ссылок при их определении. Тут ссылки схожи с константами языка Си++. Раз ссылка есть имя, связанное со значением (объектом), уже размещенным в памяти, то, определяя ссылку, необходимо с помощью начального значения определить тот объект (тот участок памяти), на который указывает ссылка.
После определения с инициализацией имя_ссылки становится еще одним именем (синонимом, псевдонимом, алиасом) уже существующего объекта. Таким образом, для нашего примера оператор
RL -= 77;
уменьшает на 77 значение переменной L. Связав ссылку (RL) с переменной (L), мы получаем две возможности изменять значение переменной:
RL = 88;
или
L = 88;
Здесь есть аналогия с указателями, однако отсутствует необходимость в явном разыменовании, что обязательно при обращении к значению переменной через указатель.
Проиллюстрируем сказанное следующей простой программой:
//Р6-22.СРР - обращение х значению переменной по ссылке
#include < iostream.h >
void main()
{long z = 12345678L; // Выделяется память для переменной
long& sz = z; // Определяется синоним для z
cout << "\nsz = " << sz << "\tz = " << z;
z = 87654321L;
cout << "\nsz = " << sz << "\tz = " << z;
z = 0;
cout <<"\nsz = " << sz <<"\t\tz = " << z;
}
Результат выполнения программы:
sz = 12345678 z = 12345678
sz = 87654321 z = 87654321
sz = 0 z = 0
Как видно из результатов, имена переменной и ссылки, "настроенной" на эту переменную, полностью равноправны и соотносятся с одним и тем же участком памяти. Присваивание значения переменной z приводит к изменению значения, связанного со ссылкой sx, и, наоборот, присваивание значения ссылке sz меняет значение переменной z.
Ссылки не есть полноправные объекты, подобные переменным, либо указателям. После инициализации значение ссылки изменить нельзя, она всегда "смотрит" на тот участок памяти (на тот объект), с которым она связана инициализацией. Ни одна из операций не действует на ссылку, а относится к тому объекту, с которым она связана. Можно считать, что это основное свойство ссылки. Таким образом, ссылка полностью аналогична исходному имени объекта. Конкретизируем и поясним сказанное. Пусть определены:
double а[] = { 10.0, 20.0, 30.0, 40.0 }; // а - массив
double *pa = a; // pa - указатель на массив
double& rа = а[О]; // rа - ссылка на первый элемент массива
double * & rpd = а; // Ссылка на указатель (на имя массива)
Для ссылок и указателей из нашего примера соблюдаются равенства
ра == &ra,*ра == rа, rpd == а,rа == а[0].
Тогда sizeof (ра) ==4 - размер указателя типа double *;
sizeof(ra) ==8 - размер элемента массива double а [О], связанного со ссылкой rа при инициализации; sizeof(rpd) ==4 - размер указателя на массив с элементами double.
Итак, результатом применения операции sizeof к ссылке является не ее размер, а размер именуемого ею объекта.
Так как квадратные скобки [] есть обозначение бинарной операции, то в соответствии с основным свойством ссылки оператор
xpd[ 0 ] += rpd[2] + rpd[3];
относится к элементам массива а []. После его выполнения а[0] == 80.0
Применив к ссылке операцию получения адреса с, определим не адрес ссылки, а адрес того объекта, которым инициализирована ссылка. Можно рассмотреть и другие операции, но вывод один - каждая операция над ссылкой является операцией над тем объектом, с которым она связана.
Так как ссылки не есть настоящие объекты, то существуют ограничения при определении и использовании ссылок. Во-первых, ссылка не может иметь тип void, т.е. определение void& имя-ссылки запрещено. Ссылку нельзя создать с помощью операции new, т.е. для ссылки нельзя выделить новый участок памяти. Не определены ссылки на другие ссылки. Нет указателей на ссылки и невозможно создать массив ссылок. Например, все следующие определения ссылок ошибочны:
long v1 = 6, v2 = 4, v3 = 8;
double& f; // Heт инициализации ссылки
void& refnew = v1; // void& недопустимо для ссылки
long& refnew = new(long &); // Ссылка не выделяют память
long& refarr[3] = { vl, v2, v3 }; // Массивов ссылок
// не бывает
long& & refref = v2; // Нет ссылок на ссылки
Ссылка, определенная с типом type, должна быть инициализирована либо объектом того же типа type, либо с помощью объекта, который может быть по умолчанию приведен к этому типу.
После определения ссылка не может быть никаким способом "оторвана" от объекта инициализации и связана с другим объектом, т.е. значение ссылки не может изменяться. Однако один объект может быть адресован любым числом ссылок и указателей:
long v = 7; // Определена переменная v
long& ref1 = v; // Ссылка ref1 связана с v
long* pfcr = &ref1; // Указатель ptr адресует v
long& ref2 = relf; // Ссылка ref2 указывает на v
Теперь к значению переменной v можно добраться четырьмя способами: с помощью имени v; ссылки ref1; разыменования указателя *ptr и ссылки ref2.
В определении типа для ссылки можно использовать модификатор const, т.е. допустимо определять ссылку на константу:
const type& имя_ссылки инициализатор;
В отличие от обычной ссылки (ссылки на переменную) ссылка на константу не позволяет изменить значение того объекта, с которым она связана. Например:
double Euler = 2.718282; // Определили переменную
const double& eRef = Euler; // eRef - ссылка на константу
При таких определениях:
Euler = 0.0; // Допустимый оператор
eRef =0.0; // Ошибка, т.к. ссылка объявлена на константу
При определении ссылок на константы инициализирующее выражение может быть не только лево допустимым и даже может не иметь типа type. Например:
const double& refpi = 3.141593;// Ссылка типа double
// на константу типа double
conat double& ref0 = О; // Ссылка типа double
// на константу 0 типа int
В последнем определении инициализирующее выражение имеет тип int, отличный от типа double ссылки ref (). Поэтому при инициализации выполняется автоматическое преобразование типов. Если при определении ссылки на константу в инициализаторе используется право допустимое выражение, например, константа, то создается временный объект, он инициализируется значением этого выражения, а ссылка становится его именем. В соответствии с общими принципами время жизни этого временного объекта определяется областью действия, в которой определяется ссылка.
Итак, если инициализирующее выражение для ссылки на константу право допустимое, то, прежде всего, вычисляется его значение. Следующий шаг - применяется необходимое преобразование типа полученного значения выражения к типу ссылки. Затем создается временная переменная нужного типа и туда заносится это значение. Адрес временной переменной используется в качестве значения инициализируемой константной ссылки. Иллюстрировать показанную цепочку действий можно таким образом. Пусть определение ссылки имеет вид:
const typel& имя_ ссылки = выражение_ type2;
Так как некоторым аналогом ссылки является указатель, то действия по инициализации ссылки с обозначением имя_ссылки эквивалентны в некотором приближении следующей последовательности:
type1 temp; // Временная переменная
temp = type1(выражение_tуре2); // Приведение типов
// и присваивание
typel* const имя_указателя = &temp; // Имитация
// инициализации указателя как аналога ссылки
Пример (определяется ссылка на константу):
const doublet cdr = 11; // Справа константа типа int
Интерпретация приведенного определения с помощью указателя:
double temp; // Временная переменная temp
temp a double(11); // Присваивание значения переменной temp
double *const cdrp = &temp; // Присваивание адреса
// постоянному указателю
Отметим, что такая интерпретация не совсем точна и отображает лишь последовательность действий. Полный аналог ссылки есть константный, т.е. неизменяемый указатель, который автоматически разыменовывается при каждом использовании. Таких константных указателей в Си++ нет -указатели автоматически разыменовываться "не умеют".
При определении ссылок обязательна их инициализация. Однако в описаниях ссылок инициализация не обязана присутствовать. К таким описаниям ссылок относятся:
С классами и их компонентами мы встретимся позже, описания внешних ссылок ничем не отличаются от описания внешних переменных или массивов, а вот соотношение ссылок и функций нужно рассмотреть подробно. Действительно, в качестве основных причин включения ссылок в язык Си++ указывают необходимость повысить эффективность обмена с функциями через аппарат параметров и целесообразность возможности использовать вызов функции в качестве лево допустимого значения. При использовании ссылки в качестве формального параметра обеспечивается доступ из тела функции к соответствующему фактическому параметру, т.е. к участку памяти, выделенному для фактического параметра. При этом параметр-ссылка обеспечивает те же самые возможности, что и параметр-указатель. Отличия состоят в том, что в теле функции для параметра-ссылки не нужно применять операцию разыменования *, а фактическим параметром должен быть не адрес (как для параметра-указателя), а обычная переменная. Следующая программа иллюстрирует в сравнении доступ к объектам (переменным) из тела функции через указатели и ссылки:
//Р6-23.СРР - сравнение ссылок с указателями в качестве
// параметров
#include < iostream.h > // Функции, меняющие значения фактических параметров:
void changePtr(double *a, double *b)
{ double с = *a;
*a = *b; *b = c;
}
void changeRef (double& x, double& y)
{ double z = x;
x = у; у = x;
}
void main()
{ double d = 1.23; // Выделяется память для переменной
double e = 4.56; // Выделяется память для переменной
changePtr(&d,&e);
cout <<"\nd = " << d <<"\t\te = " << e;
changeRef(d,e);
cout <<"\nd = " << d <<"\t\te = " << e;
}
Результат выполнения программы:
d = 4.56 e = 1.23
d = 1.23 e = 4.56
В функции changePtr () параметры специфицированы как указатели. Поэтому в ее теле выполняется их разыменование, а при обращении к changePtr () в качестве фактических переменных используются адреса (&d, &е) тех переменных, значения которых нужно поменять местами. В функции changeRef() параметры специфицированы как ссылки. Ссылки обеспечивают доступ из тела функции к фактическим параметрам, в качестве которых используются обычные переменные, определенные в вызывающей программе.
В спецификации ссылки как формального параметра инициализация необязательна, однако она не запрещена. Сложность состоит в том, что объект, имя которого используется для инициализации параметра-ссылки, должен быть известен при определении функции. Иначе параметр должен быть ссылкой на константу, и с его помощью можно будет передавать значения только внутрь функции, а не из нее.
//Р6-24.СРР - право допустимые выражения в качестве
// параметров
#include < iostream.h >
int n = -55;
void invert(int& k = n) // Инициализация параметра-ссылки
{
cout <<"\n3начение параметра в функции invert( ) : k = ";
cout << k;
k = -k;
}
void main()
{ int a = 22, b = 66: // Выделяется память для переменных
invert( ); // Изменяется знак умалчиваемого параметра
cout <<"\nn = " << n;
invert(а);
cout <<"\nа = " << а;
invert(а + b); // Переменные а и b не будут изменены
cout <<"\nа = " <<а <<"\tb = " << b;
double d = 3.33 ;
invert(int(d) ); // Переменная d сохранит свое значение
cout <<"\nd = " << d;
}
Результаты выполнения программы:
Значение параметра в функции invert k = -55
n = 55
Значение параметра в функции invert k = 22
а = -22
Значение параметра в функции invert k = 44
а = -22 b = 66
Значение параметра в функции invert k = 3
d = 3.33
В программе для параметра-ссылки проиллюстрированы:
В двух последних случаях при выполнении тела функции в него передаются значения вспомогательных временных переменных (локальные копии фактических параметров). В теле функции действия выполняются только с этими локальными копиями, и тем самым фактические параметры не могут быть изменены за счет выполнения функции.
Подобно указателю на функцию определяется и ссылка на функцию:
тип_функции (& имя_ссылки) (спецификация_параметров) инициализирующее_выражение;
Здесь тип_функции - это тип возвращаемого функцией значения, спецификация_параметров определяет сигнатуру функций, допустимых для ссылки, инициализирующее_выражение - включает имя уже известной функции, имеющей тот же тип и ту же сигнатуру, что и определяемая ссылка. Например,
int irunc (float, int); // Прототип функции
int (& iref)(float, int) = ifunc; // Определение ссылки
irеf - ссылка на функцию, возвращающую значение типа int и имеющую два параметра с типами float и int. Напомним, что использование имени функции без скобок (и без параметров) воспринимается как адрес функции.
Ссылка на функцию обладает всеми правами основного имени функции, т.е. является его синонимом (псевдонимом). Изменить значение ссылки на функцию невозможно, поэтому указатели на функции имеют гораздо большую сферу применения, чем ссылки.
Следующая программа иллюстрирует вызовы функции по основному имени, по указателю и по ссылке:
//Р6-25.СРР - ссылка и указатель на функцию
#include < iostream.h >
void func(char с) // Определение функции
{ cout <<"\n" <<с;}
void main()
{ void (*pf)(char); // pf - указатель на функцию
void (&rf)(char) = func; // rf - ссылка на функцию
func('A'); // Вызов no имени
pf = func; // Указателю присваивается адрес функции
(*pf) ('В'); // Вызов по адресу с помощью указателя
rf('C'); // Вызов по ссылке
}
Результат выполнения программы:
A
B
C
Определение ссылки может содержать ее инициализацию и в круглых скобках. В нашем примере была бы допустима и такая конструкция:
void (&rf)(char)(func); // rf - ссылка на функцию
Так же можно инициализировать и указатель на функцию:
void (*pf)(char)(func); // pf - указатель на функцию
Гораздо большими возможностями, чем ссылки на функции, обладают ссылки, формируемые как возвращаемый результат выполнения функции. Рассмотрим следующую программу:
//Р6-26.СРР - ссылка и указатель на функцию
#include < iostream.h >
// Функция определяет ссылку на элемент массива
// с максимальным значением:
int& rmax(int n, infc d[])
{ int im = 0;
for (int i = 1; i < n; i++)
im = d[im] > d[i] ? im : i;
return d[im];
}
void main()
{ int n = 4;
int x [] = { 10, 20, 30, 14 };
cout <<"\nrmax(n,x) = " << rmах(n,х);
rmax(n,x) = 0;
for (int i = 0; i < n; i++)
cout <<" x[" << i <<" ] = " << x[i];
}
Результат выполнения программы:
rmax(n,x) = 30 х[0] = 10 x[l] = 20 x[2] = 0 х[3] = 14
В программе дважды используется обращение к rmах(). Один из вызовов находится в левой части оператора присваивания, что было бы совершенно недопустимо для языка Си. С его помощью заносится нулевое значение в элемент х [2], вначале равный 30.
Возвращение функцией ссылки позволяет организовать многократное вложенное обращение к нескольким функциям. В результате таких вложенных обращений один и тот же объект можно многократно изменять по разным законам.
//Р6-27.СРР - вложенные вызовы функций, возвращающих ссылки
#include < iostream.h >
// Функция возводит в куб значение параметра и возвращает
// его адрес:
double & rcube(double& z)
{ x = z * x * z;
return х;
}
// Функция изменяет знак параметра и возвращает его адрес:
double& rinvert (double& d)
{ d = -d;
return d;
}
// Функция возвращает адрес параметра с максимальным
// значением:
double& rmax(double& х, double& у)
{ return х > у ? х : у;
}
// Функция печатает значение параметра и возвращает его
// адрес:
double& rprint(char* nаmе, double& e)
{ cout << nаmе <<е;
return е;
}
void main()
{ double a = 10.0, b = 8.0;
rprint("\nrcube (rinvert (rmax (а,b))) = ";
rcube(rinvert(rmax(a,b)))) = 0.0;
cout <<"\na = " << a <<",\tb >> " << b;
rcube(rinvert(rmax(a,b))) = 0.0;
cout <<"\na = " << a <<",\tb = " << b;
}
Результат выполнения программы:
rcube (rinvert (rmах (a,b))) = -1000
а = 0, b = 8
а = 0, b = 0
Присваивание rprint() = 0. 0 позволяет по цепочке передать нулевое значение аргументам функций и изменяет значение того из аргументов (а) функции rmах(), который вначале был максимальным. Последующее присваивание rcube() = 0.0 обнуляет значение параметра b.
В следующей программе введены две функции, возвращающие ссылки на элементы двухмерных массивов с изменяемыми размерами. Функция е1еm() позволяет по заданным значениям индексов "добираться" до конкретного элемента. Особого выигрыша применение такой функции не дает, она еще раз иллюстрирует возможности функций, возвращающих значения ссылок. Функция maxelem() возвращает адрес (ссылку) максимального элемента двумерного массива. Используя ее в левой части оператора присваивания, можно заменить значение максимального элемента. Именно это и выполняется в цикле основной программы. Результаты ее работы и комментарии в тексте поясняют сказанное. Текст программы:
//Р6-28.СРР - ссылки на элементы "двухмерных" массивов
#include < iostream.h >
// Функция возвращает ссылку на обозначенный элемент
// матрицы:
float& elem(float **matr, int k, int 1)
{ return matr[k] [1];
}
// Функция заполняет матрицу значениями от 1 до 9:
void matrix (int n, infc m, float **pmatr)
{ int k = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
elem(pmatr,i,j) = k++ % 9 + 1;
}
// Функция выбирает адрес максимального элемента матрицы:
float& maxelem(int n, int a, float **pmatr)
{ int im = 0, jm = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
{ if < pmatr[im] [jm] >= pmatr[i] [j])
continue;
im = i;
jm = j;
}
return pmatr[im] [jm];
}
// Функция печатает матрицу по строкам:
void matr_print(int n, int m, float **pmatr)
{ for (int i = 0; i < n; i++) // Цикл перебора строк
{ cout <<"\n строка " <<(i + 1) <<" : ";
// Цикл печати элементов строки:
for (int j = 0; j < m; j++)
cout <<"\t" << pmatr[i] [j];
}
}
void main()
{ float z[3] [4];
float *ptr[] = { (float *)&x[0], (float *)&z[1], (float *)&z[2] };
matrix(3,4,ptr); // Заполняем матрицу
matr_print(3,4,ptr); // Печать исходной матрицы
for (int i = 0; i < 4; i++) // Обнулим 4 максимальных
maxelem(3,4,ptr) =0.0; // элемента
matr_print(3,4,ptr); // Печать измененной матрицы
}
Результат выполнения программы:
строка 1: 1 2 3 4
строка 2: 5 6 7 8
строка 3: 9 1 2 3
строка 1: 1 2 3 4
строка 2: 5 0 0 0
строка З: 0 1 2 3
Цель перегрузки функций состоит в том, чтобы функция с одним именем по-разному выполнялась и возвращала разные значения при обращении к ней с разными по типам и количеству фактическими параметрами. Например, может потребоваться функция, возвращающая максимальное из значений элементов одномерного массива, передаваемого ей в качестве параметра. Массивы, использованные как фактические параметры, могут содержать элементы разных типов, но пользователь функции не должен беспокоиться о типе результата. Функция всегда должна возвращать значение того же типа, что и тип массива - фактического параметра.
Для обеспечения перегрузки функций необходимо для каждого имени определить, сколько разных функций связано с ним, т.е. сколько вариантов сигнатур допустимы при обращении к ним. Предположим, что функция выбора максимального значения элемента из массива должна работать для массивов типа int, long, float, double. В этом случае придется написать четыре разных варианта функции с одним и тем же именем. В следующей программе эта задача решена:
//Р6-29.СРР - перегрузка функций
#includе < iostream.h >
long max _element(int n, int array []) // Функция для
{ int value = array [0]; // массивов с элементами типа
for (int i = 1; i < n; i++) // int
value = value > array[i] ? value : array[i];
cout <<"\nДля (int) :";
return long(value);
}
long max_ element(int n, long array []) // Функция для
{ long value = array[0]; // массивов с элементами типа
for (int i = 1; i < n; i++) // long
value = value > array[i] ? value : array [i];
cout <<"\nДля (long) :";
return value;
}
double max_ element(int n, float array[]) // Функция для
{ float value = array[0]; // массивов с элементами типа
for (int i = 1; i < n; i++) // float
value = value > array [i] ? value : array[i];
cout <<"\nДля (float) :";
return double(value);
}
double max_ element(int n, double array[]) // Функция для
{ double value = array[0]; // массивов с моментами типа
for (int i = 1; i < n; i++) // double
value = value > array[i] ? value : array[i];
cout <<"\nДля (double) :";
return value;
}
void main()
{ int x [] = { 10, 20, 30, 40, 50, 60 };
long f [] = { 12L, 44L, 5L, 22L, 37L, 30L };
float y[] = { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 };
double z[] = { 0.01, 0.02, 0.03, 0.04, 0.05, 0 06 };
cout <<"mах_elеm(6,х) = " << max_element(6,x);
cout <<"max_elem(6,f) = " max_element(6,f);
cout <<"max_ еlem(6,у) = " << max_element(6,y);
cout <<"max_elem(6,z) = " << max_element(6,z);
}
Результат работы программы:
Для (int): mах_elеm(6,х) = 60
Для (long): max_ elem(6,f) = 44
Для (float): mах_еlеm(6,у) =0.6
Для (double): max_elem(6,z) =0.06
В программе для иллюстрации независимости перегруженных функций от типа возвращаемого значения две функции, обрабатывающие целью массивы (vat, long), возвращают значение одного типа long; функции, обрабатывающие вещественные массивы (double, float), обе возвращают значение типа double. Распознавание перегруженных функций при вызове выполняется по их сигнатурам. Перегруженные функции поэтому должны иметь одинаковые имена, но спецификации их параметров должны различаться по количеству и (или) по типам, и(или) по расположению.
При использовании перегруженных функций нужно с осторожностью задавать начальные значения их параметров. Предположим, мы следующим образом определили перегруженную функцию умножения разного количества параметров:
double multy(double x) { return x * х * х; }
double multy(double x, double у) { return x * у * у; }
double multy(double x, double у, double z)
{ return x * у * z; }
Каждое из следующих обращений к функции multy () будет однозначно идентифицировано и правильно обработано:
multy(0.4)
multy(4.0, 12.3)
multy(0.1e-6, 1.2e4, 6.4)
Однако добавление в программу такой функции прототипа с начальными значениями параметров:
double multy(double а = 1.0, double b = 1.0, double с = 1.0, double d = 1.0);
{ return a * b + с * d; }
навсегда запутает любой компилятор при попытках обработать, например, такой вызов:
multу(0.1е-6, 1.2e4)
Цель введения шаблонов функций - автоматизация создания функции, которые могут обрабатывать разнотипные данные. В отличие от механизма перегрузки, когда для каждой сигнатуры определяется своя функция, шаблон семейства функций определяется один раз, но это определение параметризуется.
Параметризовать в шаблоне функций можно тип возвращаемого функцией значения и типы любых параметров, количество и порядок размещения которых должны быть фиксированы. Для параметризации используется список параметров шаблона.
В определении шаблона семейства функций используется служебное слово template. Для параметризации используется список формальных параметров шаблона, который заключается в угловые скобки о. Каждый формальный параметр шаблона обозначается служебным словом class, за которым следует имя параметра (идентификатор). Пример определения шаблона функций, вычисляющих абсолютные значения числовых величин разных типов:
template < class type >
type abs(type x) { return x > 0 ? x : -x;}
Шаблон семейства функций состоит из двух частей - заголовка шаблона:
template <список_параметров_шаблона>
и из обыкновенного определения функции, в котором тип возвращаемого значения и типы любых параметров обозначаются именами: параметров шаблона, введенных в его заголовке. Те же имена параметров шаблона могут использоваться и в теле определения функции для обозначения типов локальных объектов.
В качестве еще одного примера рассмотрим шаблон семейства функций для обмена значений двух передаваемых им параметров.
template < class Т >
void swap (Т* x, T* у)
{Т z = *х;
*х = *у; *у = x;
}
Здесь параметр Т шаблона функций используется не только в заголовке для спецификации формальных параметров, но и в теле определения функций, где он задает тип вспомогательной переменной z.
Шаблон семейства функций служит для автоматического формирования конкретных определений функций по тем вызовам, которые транслятор обнаруживает в тексте программы. Например, если программист употребляет обращение abs(-l0.3), то на основе приведенного выше шаблона компилятор сформирует такое определение функции:
double abs(double x) { return x > 0 ? x : -x; }
Далее будет организовано выполнение именно этой функции и в точку вызова в качестве результата вернется числовое значение 10.3.
Если в программе присутствует приведенный выше шаблон семейства функций swap () и появится последовательность операторов:
long k = 4, d = 8; swap (&k, &d);
то компилятор сформирует определение функции:
void swap(long* x, long* y)
{ long z = *x;
*x = *y; *y = x;
}
Затем будет выполнено обращение именно к этой функции и значения, переменных k, d поменяются местами.
Если в той же программе присутствуют операторы:
double а = 2.44, b = 66.3; swap (&a, &b) ;
то сформируется и выполнится функция
void swap (double* x, double* у)
{ double z = *x;
*x = *y; *y = z;
}
Проиллюстрируем сказанное о шаблонах на более конкретном примере. Рассмотрим следующую программу, в которой вспомним некоторые возможности функций, возвращающих значение типа "ссылка". Но тип ссылки будет определяться параметром шаблона:
//Р6-ЗО.СРР - шаблон функций для поиска в массиве
#include < iostream.h >
// Функция определяет ссылку на элемент с максимальным
// значением:
template < class type >
type& rmax(int n, type d [])
{ int im = 0;
for (int i = 1; i < n; i++)
im = d[im] > d[i] ? im : i;
return d[im];
}
void main()
{ int n = 4;
int x[] = { 10, 20, 30, 14 };
// Аргумент - целочисленный массив:
cout <<"\nrmax(n,x) = " << rmax(n,x);
гmах(n) = 0; // Обращение с целочисленным массивом
for (int i = 0; i < n; i++)
cout <<"\tx[" << i <<" ] = " << x[i];
float arx [] = { 10.3, 20.4, 10.5 };
// Аргумент - массив float:
cout <<"\nrmax(3,arx)=" << rmax(3,arx);
rmax(3,arx) = 0; // Обращение с массивом типа float
for (i = 0; i < 3; i++)
cout <<"\tarx[" << i <<"] = " << arx[i];
}
Результат выполнения программы
rmax(n,x) = 30 х[0] = 10 х[1] = 20 х[2] = 0 х[3] = 14
rmax(3,arx)=20.4 arx[0] = 10.3 arx[l] = 0 arx[2] = 10.5
В программе используются два разных обращения к функции rmax (). В одном случае параметр - целочисленный массив и возвращаемое значение - ссылка типа int. Во втором случае фактический параметр - имя массива типа float и возвращаемое значение имеет тип ссылки на float.
По существу механизм шаблонов функций позволяет автоматизировать подготовку определений перегруженных функций. При использовании шаблонов уже нет необходимости готовить заранее все варианты функций с перегруженным именем. Компилятор автоматически, анализируя вызовы функций в тексте программы, формирует необходимые определения именно для таких типов параметров, которые использованы в обращениях. Дальнейшая обработка выполняется так же, как и для перегруженных функций.
Можно считать, что параметры шаблона функций являются его формальными параметрами, а типы тех параметров, которые используются в конкретных обращениях к функции, служат фактическими параметрами шаблона. Именно по ним выполняется параметрическая настройка и с учетом этих типов генерируется конкретный текст определения функции. Однако, говоря о шаблоне семейства функций, обычно употребляют термин "список параметров шаблона", не добавляя определения "формальных".
Перечислим основные свойства параметров шаблона.
template < class typel, class type2 >
Соответственно, неверен заголовок:
template < class typel, type2, type3 >
template < class t, с1аss t, class t >
Рис. 6.1. Схема параметризации шаблона функций
Следующая программа иллюстрирует указанную особенность имени параметра шаблона функций:
//Р6-31.СРР - параметр шаблона и внешняя переменная c
// тем же именем
#include < iostream.h >
int N; // Инициализирована по умолчаниию нулевым значением
// Функция определяет максимальное из двух значений
// параметров
template < class N >
N max(N х, N у)
{ N а = х;
cout <<"\nСчетчик обращений N = " <<++: : N;
if (а < у) а = у;
return a;
}
void main()
{ int а = 12, b = 42;
mах(а,b);
float х = 66.3, f = 222.4;
max(x,f);
}
Результат выполнения программы:
Счетчик обращений N = 1
Счетчик обращений N = 2
Итак, одно имя нельзя использовать для обозначения нескольких параметров одного шаблона, но в разных шаблонах функций могут быть одинаковые имена у параметров шаблонов. Ситуация здесь такая же, как и у формальных параметров при определении обычных функций, и на ней можно не останавливаться подробнее. Действительно, раз действие параметра шаблона заканчивается в конце определения шаблона, то соответствующий идентификатор свободен для последующего использования, в том числе и в качестве имени параметра другого шаблона.
Все параметры шаблона функций должны быть обязательно использованы в спецификациях параметров определения функции. Таким образом, будет ошибочным такой шаблон:
template < class A, class В, class C >
В func(A n, С m) {В valu; ... }
В данном неверном примере остался неиспользованным параметр шаблона с именем B. Его применений в качестве типа возвращаемого функцией значения и для определения объекта valu в теле функции недостаточно.
Определяемая с помощью шаблона функция может иметь любое количество не параметризованных формальных параметров. Может быть не параметризовано и возвращаемое функцией значение. Например, в следующей программе шаблон определяет семейство функций, каждая из которых подсчитывает количество нулевых элементов одномерного массива параметризованного типа:
//Р6-32.СРР - прототип шаблона для семейства функций
#include < iostream.h >
template < class D >
long count0(int, D *); // Прототип шаблона
void main ()
{ int A[] = {1, 0, 6, 0, 4, 10};
int n == sizeof(A)/sizeof A[0];
cout <<"\ncount0(n,A) = " << count0(n,A);
float X[] = { 10.0, 0.0, 3.3, 0.0, 2.1 };
n = sizeof(X)/sizeof Х[0] ;
cout <<"\ncount0(n,X) = " << count0(n,X);
}
// Шаблон функций для подсчета количества нулевых элементов
// в массиве
template <сlаss Т>
long count0(int size, T* array)
{ long k = 0;
for (int i = 0; i < size; i++)
if (int(array[i] ) == 0) k++;
return k;
}
Результат выполнения программы:
count0(n,A) = 2
count0(n,X) = 2
В шаблоне функций count0 () параметр Т используется только в спецификации одного формального параметра array. Параметр size и возвращаемое функцией значение имеют явно заданные не параметризованные типы.
Как и при работе с обычными функциями, для шаблонов функций существуют определения и описания. В качестве описания шаблона функций используется прототип шаблона:
template <список параметров шаблона>
< В списке параметров прототипа шаблона имена параметров не обязаны совпадать с именами тех же параметров в определении шаблона. Это и продемонстрировано в программе./p>
При конкретизации шаблонного определения функции необходимо, чтобы при вызове функции типы фактических параметров, соответствующие одинаково параметризованным формальным параметрам, были одинаковыми. Для определенного выше шаблона функций с прототипом
template < class E > void swap(E,E);
недопустимо использовать такое обращение к функции:
int n = 4; double d = 4.3;
swap(n,d); // Ошибка в типах параметров
Для правильного обращения к такой функции требуется явное приведение типа одного из параметров. Например, вызов
swap (double (n),d); // Правильные типы параметров
приведет к конкретизации шаблонного определения функций с параметром типа double.
При использовании шаблонов функций возможна перегрузка, как шаблонов, так и функций. Могут быть шаблоны с одинаковыми именами, но разными параметрами. Или с помощью шаблона может создаваться функция с таким же именем, что и явно определенная функция. В обоих случаях "распознавание" конкретного вызова выполняется по сигнатуре, т.е. по типам, порядку и количеству фактических параметров.
Из основных типов языка Си++ пользователь может конструировать производные типы, двум из которых посвящена настоящая глава. Это структуры и объединения. Вместе с массивами и классами структуры и объединения отнесены к структурированным типам. В этой главе рассмотрим те их особенности, которые относятся к процедурным возможностям языка, т.е. рассмотрим структуры и объединения в том виде, какой они унаследовали от языка Си.
В данной главе будем считать, что структура - это объединенное в единое целое множество поименованных элементов в общем случае разных типов. Сравнивая структуру с массивом, следует отметить, что массив - это совокупность однородных объектов, имеющая общее имя - идентификатор массива. Другими словами, все элементы массива являются объектами одного и того же типа. Это не всегда удобно. Пусть, например, библиотечная (библиографическая) карточка каталога должна включать сведения, которые приведены для книг в списке литературы, помещенном в конце нашей книги. Таким образом, для каждой книги будет указываться следующая информация:
Если к библиографической карточке каталога нужно обращаться как к единому целому, то воспользоваться массивом для представления всех ее данных весьма сложно. Все данные имеют разные длины и разные типы. Объединить такие разнородные данные удобно с помощью структуры. Каждая структура включает в себя один или несколько объектов (переменных, массивов, указателей, структур и т.д.), называемых элементами структуры. Сведения о данных, входящих в библиографическую карточку, с помощью структуры можно представить таким структурным типом:
struct card { char *author; // Ф.И.0. автора книги
char *title;
// Заголовок книги char *city;
// Место издания char *fim;
// Издательство int year;
// Год издания int pages;
// Количество страниц
};
Такое определение вводит новый производный тип, который будем называть структурным типом. В данном примере у этого структурного типа есть конкретное имя card.
В соответствии с синтаксисом языка определение структурного типа начинается со служебного слова struct, вслед за которым помещается выбранное пользователем имя типа. Описания элементов, входящих в структуру, помещаются в фигурные скобки, вслед за которыми ставится точка с запятой. Элементы структуры могут быть как базовых, так и производных типов. Например, в структурах типа card будут элементы базового типа int и производного типа char *.
Определив структурный тип, можно определять и описывать конкретные структуры, т.е. структурированные объекты, например, так:
card rec1, rec2, rec3;
Здесь определены три структуры (три объекта) с именами rec1, rec2, rec3. Каждая из этих структур содержит в качестве элементов свои собственные данные
char * title;
char *city;
...
состав которых определяет структурный тип с именем card.
Если структура определяется однократно, т.е. нет необходимости в разных частях программы определять или описывать одинаковые по внутреннему составу структурированные объекты, то можно не вводить именованный структурный тип, а непосредственно определять структуры одновременно с определением их компонентного состава. Следующий оператор определяет две структуры с именами хх, YY, массив структур с именем ЕЕ и указатель pst на структуру:
struct ( char N[12] ; int value; ) XX, YY, EE[8], *pst;
В хх,уy и в каждый элемент массива ЕЕ [0], ..., ЕЕ [7] входят в качестве элементов массив char N [12] и целая переменная value. Имени у соответствующего структурного типа нет.
Для обращения к объектам, входящим в качестве элементов в конкретную структуру, чаще всего используются уточненные имена. Общей формой уточненного имени элемента структуры является следующая конструкция:
имя_структуры. имя_элемента_структуры
Например, для определенной выше структуры YY оператор
YY.value = 86;
присвоит переменной value значение 86.
Для ввода значения переменной value структуры ЕЕ [4] можно использовать оператор
cin >> EE[4].value;
Точно так же можно вывести в выходной поток cout значение переменной из любой структуры. Другими словами, элемент структуры обладает правами объекта того типа, который указан в конструкции (в определении) структурного типа. Например, для переменных с именем value из структур ЕЕ [0],..., ЕЕ [7], XX, YY определен тип int.
При определении структур возможна их инициализация, т.е. задание начальных значений их элементов. Например, введя структурный тип card, можно следующим образом определить и инициализировать конкретную структуру:
card dictionary = { "Hornby A.S.", "Oxford students\
dictionary of Current English", "Oxford",
"Oxford University", 1984, 769 };
Такое определение эквивалентно следующей последовательности операторов:
card dictionary;
dictionary.author = "Hornby A.S.";
dictionary.title =
"Oxford students dictionary of Current English";
dictionary.city = "Oxford";
dictionary.firm = "Oxford University";
dictionary.year = 1984;
dictionary.pages = 769;
Нужно еще раз обратить внимание на отличие имени конкретной структуры (в наших примерах dictionary, recl, rec2, гесЗ, XX, YY, ЕЕ [0],..., ЕЕ [7]) от имени структурного типа (в нашем случае card).
С именем структурного типа не связан никакой конкретный объект, и поэтому с его помощью нельзя сформировать уточненные имена элементов. Определение структурного типа вводит только шаблон (формат, внутреннее строение) структур. Идентификатор card в нашем примере - это название структурного типа, т.е. "ярлык" или "этикетка" структур, которые будут определены в программе. Чтобы подчеркнуть отличие имени структурного типа от имени конкретных структур, имеющих этот тип, в англоязычной литературе по языку Си++ для обозначения структурного типа используется термин tag (ярлык, этикетка, бирка). В переводе на русский язык книги Б.Кернигана, Д.Ритчи [3] для обозначения структурного типа использована транслитерация тег. Переводчики с английского и авторы русскоязычных книг по языку Си++ не решились украсить этим термином описание синтаксиса языка Си++, поэтому мы будем говорить о структурном типе, не употребляя термина тег.
Итак, в нашем примере внутренний состав объектов, представляющих библиографические карточки, определен с помощью структурного типа с именем card. Тем самым вводится формат (набор) данных, входящих в будущие однотипные структуры, и этот формат, обозначенный именем card, имеет права типа, введенного пользователем. С его помощью в качестве примера выше определены конкретные структуры (объекты) recl, rec2, reсЗ.
Определение структурного типа может быть совмещено с определением конкретных структур этого типа:
struct PRIM {char *nаmе; long sum;) А, В, С;
Здесь определен структурный тип с именем PRIM и три структуры А, B, C, имеющие одинаковое внутреннее строение.
Повторяю, что будет ошибкой использовать имя структурного типа для именования элемента структуры:
PRIM. sum = 800L; // Ошибочная конструкция
С.sum = 800L; // Верная конструкция
Так как имя структурного типа обладает всеми правами имен типов, то разрешено определять указатели на структуры:
имя структурного_типа *имя_указателя_на_ структуру;
Как обычно, определяемый указатель может быть инициализирован. Значением каждого указателя на структуру может быть адрес структуры того же типа, т.е., грубо говоря, номер байта, начиная с которого структура размещается в памяти. Структурный тип задает ее размеры и тем самым определяет, на какую величину (на сколько байтов) изменится значение указателя на структуру, если к нему прибавить 1 (или из него вычесть 1).
Например, после наших определений структурного типа card и структуры rec2 можно так записать определение указателя на структуру типа card:
card *ptrcard = &rec2;
Здесь определен указатель ptrcard и ему с помощью инициализации присвоено значение адреса одной из конкретных структур типа card.
После определения такого указателя появляется еще одна возможность доступа к элементам структуры rec2. Ее обеспечивает операция ' ->' доступа к элементу структуры, с которой в этот момент связан указатель. Формат соответствующего выражения таков:
имя_указателя -> имя_элемента_структуры
Например, количество страниц книги, информация о которой хранится в структуре rec2, будет значением выражения
ptrcard -> pages
Вторая возможность обращения к элементу структуры с помощью адресующего ее указателя - это разыменование указателя и формирование уточненного имени такого вида:
(*имя_ указателя).имя_ элемента_ структуры
Обратите внимание на круглые скобки. Операция разыменования ' * ' должна относиться только к имени указателя, а не к уточненному имени элемента структуры. Таким образом, следующие три выражения эквивалентны:
(*ptrcard).pages
ptrcard->page
rec2.pages
Все они именуют один и тот же элемент int pages конкретной структуры rec2, имеющей тип card.
Как и для других объектов, для структур могут быть определены ссылки:
имя_структурного_типа&
имя_ссылки_на_структуру инициализатор;
Например, для введенного выше структурного типа PRIM можно таким образом ввести ссылки на структуры А, В:
PRIM& refA = А;
PRIM& refB(B);
Для разнообразия ссылки refA и refB инициализированы по-разному.
После таких определений refA есть синоним имени структуры А, refB есть другое имя для структуры в. Теперь возможны, например, такие обращения:
Вернемся к нашей задаче с библиотечными карточками и спроектируем простейшую базу данных, построив ее в виде двухсвязного списка структур (рис. 7.1), каждая из которых имеет такой формат:
struct record
{ card book; // Структура c датами о книге
record *prior; // На предыдущий элемент списка
record *next; // На следующий элемент списка
};
Структурный тип record предусматривает, что в каждую структуру входят три элемента: структура типа card и два указателя на структуры типа record. Предполагается, что до определения структурного типа record уже определен структурный тип card.
Указатель на структуру может входить в определение того же структурного типа. Именно так в определение формата структуры record введены два указателя:
Чтобы не усложнять нашу задачу создания библиотечной картотеки, примем следующие ограничения и упрощения. Чтобы не программировать процедур ввода исходных данных, подготовим сведения о книгах, включаемых в картотеку, в виде массива структур типа card. Массив структур типа card с исходными данными будем обрабатывать в цикле и включать каждую очередную запись о книге в двухсвязный список структур типа record. Включая записи о книгах в список, будем соблюдать алфавитный порядок по именам авторов. Чтобы не вводить средств для сравнения русских фамилий, а пользоваться стандартными библиотечными функциями сравнения строк, будем рассматривать только книги с фамилиями авторов на английском языке. Для каждой новой карточки, включаемой в двухсвязный список, будем запрашивать необходимый объем памяти, т.е. сформируем список с помощью средств динамического выделения памяти. Начало списка будем сохранять в отдельном указателе структурного типа record *. Указатель prior для первого элемента списка и указатель next для последнего элемента списка будут иметь значение NULL.
Рис. 7.1. Схема двухсвязного списка из трех элементов типа record:
Р - указатель prior; N - указатель next; 0 - значение NULL
Следующая программа соответствует приведенным соглашениям. До функции main () определены структурный тип card, функция printbook() для вывода на экран информации о выбранной книге, структурный тип record.
Здесь же, как внешние данные, доступные для всех функций программы, определены и инициализированы элементы массива book [] структур типа card.
//Р7-01.СРР - библиографическая картотека - двухсвязный
// список
// Для функции strсmp() сравнения строк:
#include < string.h >
#include < iostream.h >
struct card { // Определение структурного типа для книги
char *author; // Ф.И.0. автора
char *title; // Заголовок книги
char *city; // Место издания
char *firm; // Издательство
int year; // Год издания
int pages; // Количество страниц
};
// Функция печати сведений о книге:
void printbook(card& car)
{ static int count = 0;
cout <<"\n" <<++count <<". " << car.author;
cout <<' ' << car. title "".-"<< car.city;
cout <<": " << car.finm <<", ";
cout <<"\n" << car.year <<".- " << car.pages <<" c.";
}
struct record { // Структурный тип для элемента списка card book;
record *prior;
record *next;
};
// Исходные данные о книгах:
card books[] = { // Инициализация массива структур:
{ "Wiener R.S.", "Язык Турбо Си",
"М", "Мир", 1991, 384 },
{"Stroustrup В."," Язык Си++",
"Киев", "ДиаСофт", 1993, 560 },
{"Turbo C++.", "Руководство программиста",
"М", "ИHТКВ", 1991, 394 },
{"Lippman S.B.", "C++ для начинающих",
"М", "ГЭЛИОН", 1993, 496 }
};
void main()
{record * begin = NULL, // Указатель начала списка
*last = NULL, // Указатель на очередную запись
*list; // Указатель на элементы списка
// n - количество записей в списке:
int n = sizeof(books) / sizeof(books[0]);
// Цикл обработки исходных записей о книгах:
for (int i = 0; i < n; i++)
{ // Создать новую запись (элемент списка):
last = new(record);
// Занести сведения о книге в новую запись:
(*last).book.author = books[i].author;
(*last).book.title = books[i].title;
last->book.city = books[i].city;
last->book.firm = books[i].firm;
last->book.year = books[i].year;
last->book.pages = books[i].pages;
// Включить запись в список (установить связи):
if (begin = NULL) // Списка еще нет
{ last->prior = NULL;
begin = last; last->next = NULL;
}
else
{ // Список уже существует
list = begin;
// Цикл просмотра списка - поиск места для
// новой записи:
while (list)
{ if (stranp(last->book.author,
list->book.author) < 0)
{ // Вставить новую запись перед list:
if (begin == list)
{ // Начало списка:
last->prior = NULL;
begin = last;
}
else
{ // Вставка между записями:
list->prior->next = last;
last->prior = list->prior;
}
list->prior = last;
last->next = list;
// Выйти из цикла просмотра списка:
break;
}
if (list->next == NULL)
{ // Включить запись в конец списка:
last->nеxt = NULL;
last->prior = list;
list->next = last;
// Выйти из цикла просмотра списка:
break;
}
// Перейти к следующему элементу списка:
list = list->next;
} // Конец цикла просмотра списка
// (поиск места для новой записи)
} // Включение записи выполнено
} // Конец цикла обработки исходных данных
// Печать в алфавитном порядке библиографического списка:
list = begin;
cout <<'\n' ;
while (list)
{ printbook(list->book);
list = list->next;
}
}
Рис. 7.2. Последовательное формирование двухсвязного списка библиографической картотеки для программы Р7-01. СРР
Результаты выполнения программы Р7-01 .СРР:
Изучая структуры, имеет смысл обратить внимание на их представление в памяти ЭВМ. В следующей программе определена структура STR и выведены значения адресов ее элементов:
//Р7-02.СРР - размещение в памяти элементов структуры
#include < iostream.h >
void main ()
{struct {long L;
int i1, i2; char c[4];
} STR = (10L, 20, 30, 'а', 'b', 'с', 'd');
cout <<"\nsizeof(STR) = " << sizeof(STR) << hex;
cout <<"\n&STR.L = " <<&STR.L;
cout <<"\n&STR.i1 = " <<&STR.i1;
cout <<"\n&STR.i2 = " <<&STR.i2;
cout <<"\n&STR.c = " <<&STR.c;
}
Результат выполнения программы:
sizeof(STR) = 12
&STR.L = Ox8d800ff4
&STR.il = Ox8d800ff8
&STR.i2 = Ox8d800ffa
&STR.C = Ox8d800ffc
Результаты программы и соответствующая им схема на рис. 7.3 иллюстрируют основные соглашения о структурах: все элементы размещаются в памяти подряд и сообща занимают именно столько места, сколько отведено структуре в целом.
Рис. 7.3. Размещение в памяти конкретной структуры из программы Р7-02. СРР
Размещение элементов структуры в памяти может в некоторых пределах регулироваться с помощью опций компилятора. Например, в компиляторах ТС++ и ВС++ [4, 9] имеется возможность "выравнивания" структур по границам слов. При этом каждая структура размещается в памяти с начала слова, т.е. начинается в байте с четным адресом. Если при этом структура занимает нечетное количество байтов, то в ее конец добавляется дополнительный байт, чтобы структура занимала целое количество слов.
Приведем еще некоторые сведения о структурах, которые не сразу бросаются в глаза. Начнем с сопоставления структурного типа с типами, вводимыми с помощью служебного слова typedef. Напомним, что конструкция
typedef тип данных идентификатор;
вводит новое обозначение (идентификатор) для типа_данных, который может быть как базовым, так и производным. В качестве такого производного типа можно использовать структурный тип:
typedef struct PRIM { char *name;
long SUB; } NEWSTRUCT;
NEWSTRUCT st, *ps, as[8];
PRIM prim_st, *prim_ps, prim_as[8];
В этом примере введен структурный тип PRIM, и ему дополнительно с помощью typedef присвоено имя NEWSTRUCT. В дальнейшем это новое имя типа использовано для определения структуры st, указателя на структуру ps и массива структур as [8]. С помощью структурного типа PRIM определены такие же объекты: структура prim_st, указатель prim_ps, массив структур prim_as[8]. Таким образом, основное имя структурного типа (PRIM) и имя, дополнительно введенное для него с помощью typedef (NEWSTRUCT), совершенно равноправны. При определении с помощью typedef имени для структурного типа у последнего может отсутствовать основное имя. Например, структурный тип можно ввести и следующим образом:
typedef struct { char *name;
long sum; } STRUCT;
STRUCT struct_st, *struct_ps, struct_as[8];
В данном примере имя типа STRUCT вводится с помощью typedef для структуры, тип которой не поименован. Затем имя типа STRUCT используется для определения объектов.
Таким образом, имеются две возможности определения имени структурного типа, и программист вправе выбирать любую из них.
В отношении элементов структур существует практически только одно существенное ограничение - элемент структуры не может иметь тот же самый тип, что и определяемый структурный тип. Таким образом, следующее определение структурного типа ошибочно:
struct mistake { mistake s; int m; ); // Ошибка!
В то же время элементом определяемой структуры может быть указатель на структуру определяемого типа:
struct correct ( correct *pc; long f; ); // Правильно!
Элементом определяемой структуры может быть структура, тип которой уже определен:
struct begin { int k; char *h; } strbeg;
struct next {begin beg; float d;};
Если в определении структурного типа нужно в качестве элемента использовать указатель на структуру другого типа, то разрешена такая последовательность определений [9,21]:
struct A; // Неполное определение структурного типа
struct В { struct А *рa; };
struct A { struct В *рb; };
Неполное определение структурного типа А можно использовать в определении структурного типа в, так как определение указателя ра на структуру типа А не требует сведений о размере структуры типа А.
Последующее определение в той же программе структурного типа А обязательно. Использование в структурах типа А указателей на структуры уже введенного типа в не требует пояснений.
Рассматривая "взаимоотношение" структур и функций, видим две возможности: возвращаемое функцией значение и параметры функции.
Функция может возвращать структуру как результат:
struct help { char *name; int number;};
help fund(void); // Прототип функции
Функция может возвращать указатель на структуру:
help *func2(void); // Прототип функции Функция может возвращать ссылку на структуру:
help& func3(void); // Прототип функции
Через аппарат параметров информация о структуре может передаваться в функцию либо непосредственно, либо через указатель, либо с помощью ссылки:
void func4 (help str); // Прямое использование
void func5 (help *pst); // С помощью указателя
void func6 (help& rst); // С помощью ссылки
Напомним, что применение ссылки на объект в качестве параметра позволяет избежать дублирования объекта в памяти.
В программе Р7-01.СРР при формировании двухсвязного списка, объединяющего структуры с данными о книгах, для новых записей память выделялась динамически, т.е. во время выполнения программы:
struct record { card book;
record *prior; record *next };
record *last;
...
last = new (record);
...
Это позволяет обратить внимание на применение операции new к структурам. Итак, операндом для операции new может быть структурный тип. В этом случае выделяется память для структуры использованного типа, и операция new возвращает указатель на выделенную память. Память может быть выделена и для массива структур, например, так:
last = new record[9]; // Память для массива структур
В этом случае операция new возвращает указатель на начало массива.
Дальнейшие действия с элементами массива структур подчиняются правилам индексации и доступа к элементам структур. Например, разрешен такой оператор:
last[0].next = NULL;
Со структурами "в близком родстве" находятся объединения, которые вводятся с помощью служебного слова union. Чтобы пояснить I "степень родства" объединений со структурами, рассмотрим приведенное выше в программе Р7-02. СРР определение структуры STR:
struct { long L; int i1, i2; char с[4];} STR;
Размещение этой структуры в памяти схематично изображено нa рис.7.3. Важно то, что каждый элемент структуры имеет свое собственное место в памяти и размещаются эти элементы последовательно.
Определим очень похожее внешне на структуру STR объединение UNI:
union { long L; int it, i2; char с[4]; } UNI;
Количество элементов в объединении с именем UNI и их типы совпадают с количеством и типами элементов в структуре STR. Но существует одно очень важное отличие, которое иллюстрирует рис. 7.4, - все элементы объединения имеют один и тот же начальный адрес.
Рис. 7.4. Схема размещения в памяти объединения UNI
Следующая программа подтверждает сказанное:
//Р7-ОЗ.СРР - размещение в памяти объединения
#include < iostream.h >
void main ()
{union {long L;
int il, i2;
char с [4];
} UNI = {10L};
cout <<"\nsizeof(UNI) = " << sizeof(UNI) << hex;
cout <<"\n&UNI.L = " <<&UNI.L;
cout <<"\n&UNI.i1=" <<&UNI.i1;
cout <<"\n&UNI.i2=" <<&UNI.i2;
cout <<"\n&UNI.c=" <<&UNI.c;
cout <<"\nsizeof(UNI.i1) = " << sizeof (UNI. i1);
cout <<"\nsizeof(UNI.L) = " << sizeof(UNI.L) ;
}
Результат выполнения программы:
sizeof(UNI) = 4
&UNI.L = Ox8d7d0ffc
&UNI.il = Ox8ddOOffc
&UNI.i2 = Ox8d7d0ffc
&UNI.C = Ox8d7d0ffc
sizeof(UNI.i1) = 2
sizeof(UNI.L) = 4
Как подтверждают результаты выполнения программы, все элементы объединения OMI имеют один начальный адрес. Размеры элементов соответствуют их типам, а размер объединения определяется максимальным размером его элементов.
Итак, объединение можно рассматривать как структуру, все элементы которой при размещении в памяти имеют нулевое смещение от начала. Тем самым все элементы объединения размещаются в одном и том же участке памяти. Размер участка памяти, выделяемого для объединения, определяется максимальной из длин его элементов (рис. 7.5).
Рис. 7.5. Размещение в памяти объединения union {double D; int I;} UDI;
Как и для структур, для объединений может быть введен программистом производный тип, определяющий "внутреннее строение" всех объединений, относящихся к этому типу. Если для структур английский термин tag мы заменили на структурный тип, то для union type можно говорить об объединяющем типе. Поэтому будем говорить о типе объединения:
union имя_объединяющего_типа { элементы_объединения };
Пример объединяющего типа:
union mixture { double d;
long E[2];
int K[4]; };
Введя тип объединения, можно определять конкретные объединения, их массивы, а также указатели и ссылки на объединения:
mixture mA, mB[4]; // Объединение и массив объединений
mixture *pmix; // Указатель на объединение
mixtures rmix = mA; // Ссылка ма объединение
Для обращения к элементу объединения можно использовать либо уточненное имя: имя_объединения. имя_элемента либо конструкцию, включающую указатель:
укаэатель_на_объединение->имя_элемента (*указатель_на_объединение) .имя_элемента
либо конструкцию, включающую ссылку:
ссылка_на_объединение. имя_элемента
Примеры:
mA.d = 64.8;
мB[2].E[1] = 10L;
pmix = &mВ[0];
pmix->E[0] = 66;
cin >> (*pmix).K[1];
cin >> rmix.E[0];
Заносить значения в участок памяти, выделенный для объединения, можно с помощью любого из его элементов. То же самое справедливо и относительно доступа к содержимому участка памяти, выделенного для объединения. Если бы элементы объединения имели одинаковую длину и одинаковый тип, а отличались только именами, то использование объединения было бы подобно применению ссылок. Просто один участок памяти в этом случае имел бы несколько различных имен:
union { int ii, int jj } vnij:
unij.ii = 15; // Изменяем содержимое
cout << unij.jj; // Выводим содержимое
Основное достоинство объединения - возможность разных трактовок одного и того же содержимого (кода) участка памяти. Например, введя объединение
union { float F; unsigned long К; } FK;
можно занести в участок памяти, выделенный для объединения FK, вещественное число:
FK.F = 3.141593;
а затем рассматривать код его внутреннего представления как некоторое беззнаковое длинное целое:
cout << FК.К;
(В данном случае будет выведено 1078530012.)
Если включить в объединение символьный массив такой же длины, что и другие элементы объединения, то получим возможность доступа к отдельным байтам внутреннего представления объединения. Например, определим объединение:
union { float F; unsigned long L; char H[4]; } FLH;
Занеся в участок памяти, выделенный для объединения, вещественное число, например, так:
FLH.F = 2.718282;
можем получить значение кода его внутреннего представления с помощью уточненного имени FLH.L и(или) значения кодов, находящихся в отдельных байтах: FLH.H[0], FLH.H[1], FLН.Н [2], FLН.Н[З].
Итак, основное назначение объединений - обеспечить возможность доступа к одному и тому же участку памяти с помощью объектов разных типов. Необходимость в таком механизме возникает, например, для выделения из внутреннего представления (из кода) объекта определенной части. Например, следующее объединение с именем сc позволяет выделить из внутреннего представления целого числа его отдельные байты:
union { char hh[2];
int ii;
} cc;
Здесь символьный массив char hh[2] и целая переменная int ii - элементы объединения - соответствуют одному участку памяти (рис.7.6).
Рис. 7.6. Размещение в памяти объединения cс
Используем введенное объединение с именем cc для решения небольшой конкретной задачи, связанной с доступом к буферу клавиатуры ПЭВМ типа IBM PC. В MS-DOS принято, что нажатие на любую клавишу клавиатуры ПЭВМ приводит к занесению в буфер клавиатуры (буфер клавиатуры - это специально зарезервированный участок памяти) целого двухбайтового числа. Каждый байт этого целого числа имеет самостоятельное смысловое значение. Байт с младшим адресом содержит так называемый ASCII-код клавиши, а старший (с большим адресом) содержит дополнительный код, называемый скэн-кодом клавиши. В библиотеке компилятора Турбо Си (и ВС++) имеется специальная функция infc bioskay(int b); позволяющая получить доступ к буферу клавиатуры. Параметр int b функции bioskey () позволяет выбрать режим ее использования. Обращение bioskey(1) проверяет наличие в буфере хотя бы одного кода. Если буфер пуст, то bioskey (l) возвращает нулевое значение.
Как только в буфере появится код (от нажатия клавиши), функция bioskey (l) возвратит ненулевое значение. Прочитать тот код, который занесен в буфер, и очистить буфер от этого кода позволяет обращение к функции bioakey() с нулевым значением параметра. При обращении bioskey (9) функция выбирает из буфера клавиатуры очередной двухбайтовый код и возвращает его как целое число (двухбайтовое). Выделить из этого целого числа отдельные байты очень просто с помощью объединения. Следующая программа получает и выводит на экран значения кодов, поступающих в буфер клавиатуры ПЭВМ, работающей под управлением MS-DOS:
//Р7-04.СРР - объединение выделяет скэн и ASCII-коды
// клавиш
#include < bios.h > // Для функции bioskey()
#include < iostream.h >
void main()
{ union { char hh[2]; int ii; } сc;
unsigned char scn, // Скэн-коды
asc; // ASCII-коды
cout <<"\nВыход на программы no Ctrl+Z";
cout <<"\n\nSCAN | ASCII";
do { // Цикл до ctrl+Z
cout<<"\n";
while {bioskey(l) == 0); // До появления кода
cc.ii << bioskey(0);
аsс = cc.hh[0];
scn = cc.hh[1] ;
cout <<" " << int(scn) <<" | ";
cout << int(аsс) <<" " <<аsс;
} // Выход из цикла по Ctrl+Z, когда asc==26
// и sсn==44:
while (аsс !=26 || aсn != 44);
}
Результат выполнения программы:
Выход из программы по Ctrl+Z
SCAN | ASCII
34 | 103 g
35 | 104 h
36 | 106 j
24 | 111 о
44 | 26
Для ASCII-кода печатается не только числовое значение, но и соответствующий ему экранный символ.
Обратите внимание на внутренний цикл while с проверкой значения bioskey(l). Он прерывается только при появлении внешнего события - при нажатии на клавишу. Значение bioskey (l) становится при этом отличным от 0, и следует переход к оператору с обращением bioskey(0).
Возвращаемое целое значение заносится в элемент объединения сc. ii, а потом из объединения выбираются отдельные однобайтовые коды (асn, аsс). Внешний цикл будет прерван при появлении кодов asc==26 и scn==44 (сочетание клавиш ctrl+z). После этого программа прекращает выполнение.
Как массивы, так и структуры могут быть элементами объединений, причем здесь возможны весьма разнообразные сочетания. В качестве содержательного примера рассмотрим тип объединения REGS, введенный в заголовочном файле dos. h компиляторов ТС++ и ВС++. Объединение типа REGS позволяет обращаться к регистрам процессора 80х86 двумя способами. Можно рассматривать регистры как 16-разрядные, а можно обращаться к отдельным 8-разрядным половинам регистров. Прежде чем пояснять сказанное, рассмотрим определения нужных типов из файла dos. h:
struct WORDREGS { unsigned int ax, bx, cx, dx,
si, di, cflag, flags; };
struct BYTEREGS { unsigned char al, ah, bl, bh,
cl, ch, dl, dh; };
union BEGS { struct WORDREGS x;
Struct BYTEREGS h;
};
Как следует из определений, BEGS - это тип, обеспечивающий объединение двух структур. Структуры типа WORDREGS позволяют обращаться к регистрам как к двухбайтовым беззнаковым целым. Структуры типа BTTEREGS обеспечивают доступ к отдельным байтам первых четырех регистров. Заметьте, что структуры WORDREGS и BTTEREGS имеют разную длину в памяти. При их включении в одно объединение длина объединения равна длине, являющейся максимальной из длин его элементов, т.е.
sizeof(REGS) == sizeof{WORDREGS)
Не вдаваясь в подробности, отметим, что объединения типа REGS используются при обращении к библиотечным функциям обработки прерываний. Например, для вызова функции 8 прерывания 16 (0х10) можно использовать такой набор операторов;
union REGS in, out;
in.h.ah = 8; // Номер вызываемой функции
in.h.bh = page; // Номер страницы видеопамяти
int86(0x10, &in, &out); // Вызов прерывания 16
*ch = out.h.al; // Код ASCII символа из видеопамяти
*attr = out.h.ah; // Атрибуты символа из видеопамяти
О возможностях функции int86 () можно прочитать в описании стандартной библиотеки ТС++ или ВС++ (см. [6,11]).
При определении конкретных объединений разрешена их инициализация, причем инициализируется только первый элемент объединения.
Примеры:
union compound { long LONG;
int INT[2];
char CHAR[4];
};
compound mix1 = { 11111111 }; // Правильно
compound mix2 = { 'a', 'b', 'c', 'd' ); // Ошибка union
{ char CHAR[4];
long LONG;
int INT[2] } mix = ('a','b','c','d'); // Правильно
Для объединения mix инициализирован первый элемент - массив символов из четырех элементов, что вполне допустимо.
При определении объединений без явного указания имени объединяющего типа (как в последнем примере для объединения mix) разрешено не вводить даже имени объединения. В этом случае создается анонимное или безымянное объединение:
union { int IN[5]; char СН[10] } = { 1, 2, 3, 4, 5 };
К элементам анонимного объединения можно обращаться как к отдельным объектам, но при этом могут изменяться другие элементы объединения:
IN[0] = 10; // Изменятся значения СН[0], СН[1]
СН[9] = 'а'; // Изменится значение IN[4]
Разрешено формировать массивы объединений и инициализировать их:
compound mixture[] = { 1L, 2L, 3L, 4L };
Здесь для каждого элемента mixture [i] введенного массива из четырех объединений типа compound инициализация выполнена для первого компонента объединения, т.е. начальное значение явно получил каждый элемент mixture [i]. LONG. Доступ к внутренним кодам этих значений возможен также через элементы mixture [i] .INT[J] и mixture[i].CHAR [k].
Внутри структур и объединений могут в качестве их компонентов (элементов) использоваться битовые поля.
Каждое битовое поле представляет целое или беззнаковое целое значение, занимающее в памяти фиксированное число битов (в компиляторе ВС++ от 1 до 16 бит). Битовые поля могут быть только элементами структур, объединений (и, как увидим в дальнейшем, классов), т.е. битовые поля не могут появляться как самостоятельные объекты программ. Битовые поля не имеют адресов, т.е. для них не определена операция ' &', нет указателей и ссылок на битовые поля. Они не могут объединяться в массивы. Назначение битовых полей - обеспечить удобный доступ к отдельным битам данных. С помощью битовых полей можно формировать объекты с длиной внутреннего представления, не кратной байту. Это позволяет плотно "упаковывать" информацию и тем самым экономить память, например, при работе с однобитовыми флажками.
Определение структуры с битовыми полями имеет такой формат:
struct { тип_поля имя_поля: ширина_ поля;
тип_поля имя_ поля: ширина поля;
...
} имя_структуры;
Здесь тип_поля - один из базовых целых типов int, unsigned int (сокращенно unsigned), signed int (сокращенно signed), char, short, long и их знаковые и беззнаковые варианты. (В языке Си стандарт ANSI допускает только знаковый или беззнаковый вариант типа int.)
имя_поля
идентификатор, выбираемый пользователем;
ширина-поля
целое неотрицательное десятичное число, значение которого обычно не должно превышать длины слова конкретной ЭВМ.
Таким образом, диапазон возможных значений ширины_поля существенно зависит от реализации. В компиляторах ТС++ и ВС++ ширина-поля может выбираться в диапазоне от 0 до 16. Пример определения структуры с битовыми полями:
struct { int a:10;
int b:14;
} хх, *рх;
Для обращения к битовым полям используются те же конструкции, что и для обращения к обычным элементам структур:
имя _структуры.имя поля
указатель_ на_структуру->имя поля
ссылка_ на_ структуру.имя_ поля
(*указатель_на_структуру).имя_поля
Например, для введенной структуры хх и указателя рх допустимы такие операторы:
хх.а = 1;
рх = &хх;
рх->b = 48;
От реализации зависит порядок размещения полей структуры в памяти ЭВМ. Поля могут размещаться как справа налево, так и слева направо. Кроме того, реализация определяет, как размещаются в памяти битовые поля, длина которых не кратна длине слова и(или) длине байта (рис. 7.7). Для компиляторов, работающих на IBM PC, поля, размещенные в начале описания структуры, имеют младшие адреса. Именно такое размещение изображено на рис. 7.7.
Рис. 7.7. Варианты размещения в памяти битовых пoлей структуры
В компиляторах часто имеется возможность изменять размещение битовых полей, выравнивая их по границам слов или выполняя плотную упаковку. Некоторые возможности влиять на размещение битовых полей в памяти имеются и на уровне синтаксиса самого языка Си++. Во-первых, при определении битового поля разрешается не указывать его имя. В этом случае (когда указаны только двоеточие и ширина поля) в структуру вводятся неиспользуемые (недоступные) биты, формирующие промежуток между значимыми полями. Например:
struct { int a:10;
int :6;
int b:14;
} УУ;
Рис. 7.8. Структура с безымянным полем
В структуре уу между полем int a:10 и полем int b:14 размeщаются 6 бит, не доступных для использования. Их назначение - выравнивание полей по плану программиста (рис. 7.8).
Битовые поля в объединениях используются для доступа к нужным битам того или иного объекта, входящего в объединение. Например, следующее объединение позволяет замысловатым способом сформировать код символа 'D' (равный 68):
union { char simb;
struct { int x:5;
int y:3;
} hh;
} cod;
cod.hh.x = 4;
cod.hh.y =2;
cout << cod.simb; // Выведет на экран символ 'D'
Рис. 7.9 иллюстрирует формирование кода 68, соответствующего символу 'D'.
Рис. 7.9. Объединение со структурой из битовых полей
Для иллюстрации особенностей объединений и структур с битовыми полями рассмотрим следующую программу:
//Р7-05.СРР - битовые поля, структуры, объединения
#include < iostream.h >
// Функция упаковывает в один байт остатки от деления
// на 16 двух целых чисел - параметров:
unsigned char cod(int a,int b)
{ union { unsigned char z;
struct (unsigned int x:4; // Младшие биты
unsigned int y:4; // Старшие биты
} hh;
} un;
un.hh.x = a % 16;
un.hh.y = b % 16;
return un.z;
// Функция изображает на экране двоичное представление
// байта-параметра:
void binar(unsigned char ch)
{ union { unsigned char ss;
struct { unsigned a0:l
unsigned al:l
unsigned a2:l
unsigned a3:l
unsigned a4:l
unsigned a5:l
unsigned a6:l
unsigned a7:1
} byte;
} cod;
cod.ss = ch; // Занести значение параметра в объединение
// Выводим биты внутреннего кода значения параметра:
cout <<"\nНомера БИТОВ: 7 6 5 4 3 2 1 0";
cout <<"\n3начения битов:";
cout <<"\t" << cod.byte.a7 <<" " << cod.byte.a6;
cout <<" " << cod.byte.a5 <<" " << cod.byte.a4;
cout <<" " << cod.byte.a3 <<" " << cod.byte.a2;
cout <<" " << cod.byte.al <<" " << cod.byte.a0;
cout <<"\n";
}
void main()
{ int k;
int m, n;
cout <<"\nm = "; cin >> m;
cout <<"n = "; cin >> n;
k = cod(m.n);
cout <<"cod = " << k;
binar(k);
}
Возможный результат выполнения программы:
m = 1 < Enter >
n = 3 < Enter >
cod = 49
НОМЕРА БИТОВ: 7 6 5 4 3 2 1 0
Значения битов: 0 0 1 1 0 0 0 1
Результат еще одного выполнения программы:
m = о < Enter >
n = 1 < Enter >
cod = 16
НОМЕРА БИТОВ: 7 6 5 4 3 2 1 0
Значения битов: 0 0 0 1 0 0 0 0
Комментарии в тексте программы и приведенные результаты объясняют особенности программы. В функциях cod() и binar () использованы объединения, включающие структуры с битовыми полями. В функции cod () запись данных выполняется в битовые поля структуры hh, входящей в объединение un, а результат выбирается из того же объединения un, но как числовое значение байта. В функции binar () обратное преобразование - в нее как значение параметра передается байт, содержимое которого побитово "расшифровывается" за счет обращения к отдельным полям структуры byte, входящей в объединение cod.
Одно из принципиальных отличий языка Си++ от языка Си -возможность включения в структуры и объединения не только данных, но и функций. В этом случае структурный тип или объединяющий тип становится определением класса. Подробно о классах речь пойдет в гл. 9.
В интегрированную среду подготовки программ на Си++ или в компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора - обработка исходного текста программы до ее компиляции.
Препроцессорная обработка в соответствии с требованиями стандарта языка Си++ [2] включает несколько стадий, выполняемых последовательно. Конкретная реализация транслятора может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись последовательно:
Знакомство с перечисленными задачами препроцессорной обработки объясняет некоторые соглашения синтаксиса языка. Например, становится понятным смысл утверждений: каждая символьная строка может быть перенесена в файле на следующую строку, если использовать символ ' \ ' или "две символьные строки, записанные рядом, воспринимаются как одна строка".
Рассмотрим подробно стадию обработки директив препроцессора. При ее выполнении возможны следующие действия:
Для управления препроцессором, т.е. для задания нужных действий, используются команды (директивы) препроцессора, каждая из которых помещается на отдельной строке и начинается с символа #. Определены следующие препроцессорные директивы: "define, #include, #undef, #if, #ifdef, #ifndef, #else, #endif, #elif, #line, #error, #pragma, #.
Директива #define имеет несколько модификаций. Они предусматривают определение макросов или препроцессорных идентификаторов, каждому из которых ставится в соответствие некоторая символьная последовательность. В последующем тексте программы препроцессорные идентификаторы заменяются на заранее запланированные последовательности символов.
Директива #include позволяет включать в текст программы текст из выбранного файла.
Директива #undef отменяет действие команды #define, которая определила до этого имя препроцессорного идентификатора.
Директива #if и ее модификации #ifdef, #ifndef совместно с директивами #else, #endif, #elif позволяют организовать условную обработку текста программы. Условность состоит в том, что компилируется не весь текст, а только те его части, которые так или иначе выделены с помощью перечисленных директив.
Директива #line позволяет управлять нумерацией строк в файле с программой. Имя файла и начальный номер строки указываются непосредственно в директиве #line.
Директива #error позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок.
Директива #pragma вызывает действия, зависящие от реализации.
Директива # ничего не вызывает, так как является пустой директивой, т.е. не дает никакого эффекта и всегда игнорируется.
Рассмотрим возможности перечисленных команд при решении типичных задач, поручаемых препроцессору.
Для замены идентификатора заранее подготовленной последовательностью символов используется директива (обратите внимание на пробелы):
#define идентификатор строка_замeщeния
Директива может размещаться в любом месте обрабатываемого текста, а ее действие в обычном случае распространяется от точки размещения до конца текста. Директива, во-первых, определяет идентификатор как препроцессорный. В результате обработки все вхождения определенного командой #define идентификатора в текст программы заменяются строкой замещения, окончанием которой обычно служит признак конца той строки, где размещена команда #define. Символы пробелов, помещенные в начале и в конце строки замещения, в подстановке не используются. Например:
Исходный текст Результат препроцессорной обработки
#define begin {
#define end }
void main () void main( )
begin {
операторы операторы
end }
В данном случае программист решил использовать в качестве операторных скобок идентификаторы begin, end.
Компилятор языка Си++ не может обрабатывать таких скобок, и поэтому до компиляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствующие указания программист дал препроцессору с помощью директив #define.
Если строка_замещения оказывается слишком длинной, то, как любую символьную строку языка Си++, ее можно продолжить в следующей строке текста программы. Для этого в конце продолжаемой строки помещается символ ' \ ' (обратная наклонная черта). В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки будет удален из текста программы.
Пример:
#define STROKA "\n Multum, non multa - \
многое, но немного!"
...
cout << STROKA;
На экран будет выведено:
Mull tum, non multa - многое, но немного !
С помощью команды #define удобно выполнять настройку программы. Например, если в программе требуется работать с массивами, то их размеры можно явно определять на этапе препроцессорной обработки:
Исходный текст Результат препроцессорной обработки
#define K 40
void main( ) void main ()
{ int M[K] [K]; { int M[40] [40] ;
float A[K], B[K] [K]; float A[40], B[40] [40];
... ...
При таком описании очень легко изменять предельные размеры сразу всех массивов, изменив только одну константу в команде #define.
Те же возможности в языке Си++ обеспечивают константы, определенные в тексте программы. Например, того же результата можно достичь, записав:
void main()
{ const int k = 40;
int M[k] [k];
float A[k], B[k] [k];
...
Именно в связи с расширением возможностей констант в языке Си++ по сравнению с языком Си команда #define используется реже.
Предусмотренные директивой #define препроцессорные замены не выполняются внутри строк, символьных констант и комментариев, т.е. не распространяются на тексты, ограниченные кавычками ("), апострофами ( ' ) и разделителями (/*,*/). В то же время строка замещения может содержать перечисленные ограничители, например, так, как это было в замене препроцессорного идентификатора STROKA.
Если в программе нужно часто печатать или выводить на экран дисплея значение какой-либо переменной и, кроме того, снабжать эту печать одним и тем же пояснительным текстом, то удобно ввести сокращенное обозначение оператора печати
Например, так:
#define РК cout <<"\n Номер элемента N = " << N <<'.'
После этой директивы использование в программе оператора PK; будет эквивалентно (по результату) оператору из строки замещения. Например, последовательность операторов
int N = 4;
РК;
приведет к выводу такого текста:
Номер элемента N = 4.
Если в строку замещения входит идентификатор, определенный в другой команде #define, то в строке замещения выполняется следующая замена (цепочка подстановок). Например, программа, содержащая команды:
#define К 50
#define РЕ cout <<"\nКоличество элементов К = " <<К
...
РЕ;
...
выведет на экран такой текст:
Количество элементов К = 50
Обратите внимание, что идентификатор к внутри строки замещения, обрамленной кавычками ( " ), не заменен на 50.
Строку замещения, связанную с препроцессорным идентификатором, можно сменить, приписав уже определенному идентификатору новое значение другой командой #define:
#define M 16
... // Идентификатор М определен как 16
#define М 'С'
... // М определен как символьная константа 'С'
#define М "С" // М определен как символьная строка
... // с двумя элементами: 'С' и '\0'
Однако при такой смене значений препроцессорного идентификатора компилятор ВС++ выдает предупреждающее сообщение на каждую следующую директиву #define:
Warning ...: Redefinition of 'M' is not identical
Замены в тексте можно отменять с помощью команды:
#undef идентификатор
После выполнения такой директивы идентификатор для препроцессора становится неопределенным и его можно определять повторно. Например, не вызовут предупреждающих сообщений директивы:
#define M 16
#undef M
#define M 'С'
#undef M
#define M "С"
Директиву #undef удобно использовать при разработке больших программ, когда они собираются из отдельных "кусков текста", написанных в разное время или разными программистами. В этом случае могут встретиться одинаковые обозначения разных объектов. Чтобы не изменять исходных файлов, включаемый текст можно "обрамлять" подходящими директивами #define - #undef и тем самым устранять возможные ошибки. Приведем пример:
...
А = 10; // Основной текст
... #define A X
...
А = 5; // Включенный текст
... #undef A
...
В = А; // Основной текст
...
При выполнении программы в примет значение 10, несмотря на наличие оператора присваивания А = 5; во включенном тексте.
Для включения текста из файла используется команда #include, имеющая две формы записи:
#include <имя_файла> // Имя в угловых скобках
#include "имя_ файла" // Имя в кавычках
Если имя_файла - в угловых скобках, то препроцессор разыскивает файл в стандартных системных каталогах.
Если имя_файла заключено в кавычки, то вначале препроцессор просматривает текущий каталог пользователя и только затем обращается к просмотру стандартных системных каталогов.
Начиная работать с языком Си++, пользователь сразу же сталкивается с необходимостью использования в программах средств ввода-вывода. Для этого в начале текста программы помещают директиву:
#include < iostream.h >
Выполняя эту директиву, препроцессор включает в программу средства связи с библиотекой ввода-вывода. Поиск файла iostream.h ведется в стандартных системных каталогах.
По принятому соглашению суффикс .h приписывается тем файлам, которые нужно помещать в заголовке программы, т.е. до исполняемых операторов.
Кроме такого в некоторой степени стандартного файла, каким является iostream.h, в заголовок программы могут быть включены любые другие файлы (стандартные или подготовленные специально).
Заголовочные файлы оказываются весьма эффективным средством при модульной разработке крупных программ, когда связь между модулями, размещаемыми в разных файлах, реализуется не только с помощью параметров, но и через внешние объекты, глобальные для нескольких или всех модулей. Описания таких внешних объектов (переменных, массивов, структур и т.п.) помещаются в одном файле, который с помощью директив #include включается во все модули, где необходимы внешние объекты. В тот же файл можно включить и директиву подключения библиотеки функций ввода-вывода. Заголовочный файл может быть, например, таким:
#include < iostream.h > // Включение средств обмена
extern int ii, jj, 11; // Целые внешние переменные
extern float АА, ВВ; // Вещественные внешние переменные
В практике программирования на Си++ обычна и в некотором смысле обратная ситуация. Если в программе используется несколько функций, то иногда удобно текст каждой из них хранить в отдельном файле. При подготовке программы пользователь включает в нее тексты используемых функций с помощью команд #include.
В качестве примера рассмотрим задачу обработки строк, в которой используем функции обработки строк, текст каждой из которых находится в отдельном файле.
Задача об инвертировании слов в предложении. Ввести с клавиатуры заканчивающееся точкой предложение, слова в котором отделены друг от друга пробелами. Записать каждое слово предложения в обратном порядке (инвертировать слово) и напечатать полученное предложение. Для простоты реализации ограничим длину вводимого предложения 80 символами. Тогда программа решения сформулированной задачи может быть такой:
//Р8-01.СРР - препроцессорное формирование текста программы
#include < iostream.h >
#include "invert.cpp" // Функция инвертирования строк
#include "conc.cpp" // Функция соединения строк
void main()
{ char slovo[81], sр[81], с = ' ', *ptr = slovo;
sp[0] = ' \0'; // Очистка массива для нового предложения
cout <<"\nВведите предложение с точкой в конце: \n";
do
{ cin >> slovo; // Читается слово из входного потока
invert(slovo); // Инвертировать слово с = slovo[0];
// Убрать точку в начале последнего слова:
if (с == '.') ptr = &slovo[l];
if (sp[0] != '\0')
cone(sр," \0"); // Пробел перед словом
cone(sp,ptr); // Добавить слово в предложение
} while(с != '.' ); // Конец цикла чтения
conc(sp,".\0"); // Точка в конце предложения
cout <<"\n" << sp; // Вывод результата
}
В файле invert. cpp текст функции:
//invert.cpp - функция инвертирования строки
void invert(char *e)
{ char s;
for (int m = 0; e[m] != '\0'; m++);
for (int i = 0, j = m - 1; i < j; i++, j--)
{ s = e[i]; e[i] = e[j]; e[j] = s; }
}
В файле conc. cpp текст функции:
//CONC.CPP - функция соединения двух строк
void conc(char *c1, char *c2)
{ for (int m = 0; c1[m] != '\0'; m++);
// m - длина первой строки без символа '\0'
for (int i = 0; c2[i] != '\0'; i++) c1[m + i] = c2[i];
cl[m+i] = '\0';
}
Возможный результат выполнения:
Введите предложение с точкой в конце:
А ШОРОХ ХОРОШ. < Enter >
А ХОРОШ ШОРОХ.
В программе в символьный массив slovo считывается из входного потока (с клавиатуры) очередное слово; sр - формируемое предложение, в конец которого всегда добавляется точка. Переменная char с - первый символ каждого инвертированного слова. Для последнего слова предложения этот символ равен точке. При добавлении этого слова к новому предложению точка отбрасывается с помощью изменения значения указателя ptr.
Использованы директивы #include, включающие в программу средства ввода-вывода и тексты функций инвертирования строки invert () и конкатенации строк соnс(). Обратите внимание, что длина массива -первого из параметров функции соnс() должна быть достаточно велика, чтобы разместить результирующую строку.
Препроцессор добавляет тексты всех функций в программу из файла Р8-11. CPP и как единое целое передает на компиляцию.
Условная компиляция обеспечивается в языке Си++ набором команд, которые, по существу, управляют не компиляцией, а препроцессорной обработкой:
#if константное_выражение
#ifdef идентификатор
#ifndef идентификатор
#else
#endif
#elif
Первые три команды выполняют проверку условий, две следующие - позволяют определить диапазон действия проверяемого условия. Команду #elif рассмотрим несколько позже. Общая структура применения директив условной компиляции такова:
#if ... текст_ 1
#else текст_2
#endif
Конструкция #else текст_2 не обязательна. Текст_1 включается в компилируемый текст только при истинности проверяемого условия. Если условие ложно, то при наличии директивы #else на компиляцию передается текст_2. Если директива #else отсутствует, то весь текст от #if до #endif при ложном условии опускается. Различие между формами команд #if состоит в следующем.
В первой из перечисленных директив #if проверяется значение константного целочисленного выражения. Если оно отлично от нуля, то считается, что проверяемое условие истинно. Например, в результате выполнения директив:
#if 5+4
текст_1
#еndif
текст_1 всегда будет включен в компилируемую программу.
В директиве #ifdef проверяется, определен ли с помощью команды #define к текущему моменту идентификатор, помещенный после #ifdef. Если идентификатор определен как препроцессорный, то текст_1 используется компилятором.
В директиве #ifndef проверяется обратное условие - истинным считается неопределенность идентификатора, т.е. тот случай, когда идентификатор не был использован в команде #define или его определение было отменено командой #undef.
Условную компиляцию удобно применять при отладке программ для включения или исключения контрольных печатей. Например,
#define DE 1
...
#ifdef DE
cout <<" Отладочная печать ";
#endif
Таких печатей, появляющихся в программе в зависимости от определенности идентификатора DE, может быть несколько и, убрав директиву #define DE 1, сразу же отключаем все отладочные печати.
Файлы, предназначенные для препроцессорного включения в модули программы, обычно снабжают защитой от повторного включения. Такое повторное включение может произойти, если несколько модулей, в каждом из которых запланировано препроцессорное включение одного и того же файла, объединяются в общий текст программы. Например, такими средствами защиты снабжены все заголовочные файлы (подобные iosfcream.h) стандартной библиотеки. Схема защиты от повторного включения может быть такой:
// Файл с именем filename #ifndef _FILE_NAME ... // Включаемый текст файла filename #define_ FILE_ NAME 1 #endif
Здесь _FlLE_NAME - зарезервированный для файла filename препроцессорный идентификатор, который не должен встречаться в других текстах программы.
Для организации мультиветвлений во время обработки препроцессором исходного текста программы введена директива
#elif константное выражение
Структура исходного текста с применением этой директивы такова:
#if
текст_ для_ if
#elif выражение_ 1
текст_ 1
#elif выражение_2
текст_2
...
#else
текст_ для_случая_else
#endif
Препроцессор проверяет вначале условие в директиве #if, если оно ложно (равно 0) - вычисляет выражение_ 1, если выражекие_1 равно 0 - вычисляется выражение_2 и т.д. Если все выражения ложны, то в компилируемый текст включается текст_для_случая_е1sе. В противном случае, т.е. при появлении хотя бы одного истинного выражения (в #if или в #elif), начинает обрабатываться текст, расположенный непосредственно за этой директивой, а все остальные директивы не рассматриваются. Таким образом, препроцессор обрабатывает всегда только один из участков текста, выделенных командами условной компиляции.
Макрос, по определению, есть средство замены одной последовательности символов другой. Для выполнения замен должны быть заданы соответствующие макроопределения. Простейшее макроопределение мы уже ввели, рассматривая директиву
#define идентификатор строка_замещения
Такая директива удобна, однако она имеет существенный недостаток - строка замещения фиксирована. Большими возможностями обладает следующее макроопределение с параметрами
#define имя(список_параметров) строка замещения
Здесь имя - имя макроса (идентификатор), список_параметров -список разделенных запятыми идентификаторов. Между именем макроса и списком параметров не должно быть пробелов.
Классический пример макроопределения:
#define mах(а,b) (а < b ? b : a)
позволяет формировать в программе выражение, определяющее максимальное из двух значений аргументов. При таком определении вхождение в программу
mах(Х,Y)
заменяется выражением
(Х < Y ? Y : X)
а использование
mах(2,4)
приведет к формированию выражения
(Z < 4 ? 4 : Z)
В первом случае при истинном значении х < y возвращается значение Y, иначе - значение х. Во втором примере z сравнивается с константой 4 и выбирается большее из значений.
Не менее часто используется определение
#define ABS(X) (X < 0 ? -(X) : X)
С его помощью в программу можно вставлять выражение для определения абсолютных значений переменных.
Конструкция
АВS(Е - Z)
заменяется выражением
(Е - Z < 0 ? -(Е - Z) : Е-Z )
в результате вычисления которого определяется абсолютное значение выражения E - Z.
Сравнивая макросы с функциями, наиболее часто отмечают, что в отличие от функции, определение которой всегда присутствует в одном экземпляре, коды, формируемые макросом, вставляются в программу столько раз, сколько раз используется макрос. В этом отношении макросы подобны встраиваемым (inline) функциям, но в отличие от встраиваемых функций подстановка для макроса выполняется всегда. Обратим внимание на еще одно отличие: функция определена для данных того типа, который указан в спецификации ее параметров и возвращает значение только одного конкретного типа. Макрос пригоден для обработки параметров любого типа, допустимых в выражениях, формируемых при обработке строки замещения. Тип получаемого значения зависит только от типов параметров и от самих выражений. Таким образом, макрос может заменять несколько функций. Например, приведенные макросы mах() и ABS() верно работают для параметров с целыми или плавающими типами, а результат зависит только от типов параметров. Механизм перегрузки и шаблоны функций позволяют решать те же задачи, что и макросы. Именно поэтому в отличие от языка Си в программах на Си++ макросредства используются реже.
Покажем на примере некоторые ограничения и возможности макроопределений с параметрами:
//Р8-02.СРР - особенности макроопределений с параметрами
#define max(a,b) (a < b ? b : а)
#define t(e) e*3
#define PRNT(c) cout <<"\n" <<#с << "равно "; cout <<с;
#define Е х*х
#include < iostream.h >
void main()
{ int х;
х = 2;
РRNТ(mах(++х, ++х));
PRNT(t (x));
PRNT(t(x + х));
PRNT(t(x + х)/3);
PRNT(E);
В результате выполнения программы получен следующий, объясняемый ниже, результат:
mах (++х, ++х) равно 5
t(x) равно 15
t(x + х) равно 20
t(x + х)/3 равно 10
В равно 25
Обратите внимание на то, что в макроопределении PRNT строка замещения включает два оператора, разделенных точкой с запятой, причем в строке замещения имеются пробелы. При подстановке параметров макроса в строку замещения запрещены подстановки внутрь кавычек, апострофов или ограничителей комментариев. В случае необходимости параметр макроса можно заключить в строке замещения в кавычки (" "). Для этого используется специальная операция #, которая записывается непосредственно перед параметром. В макроопределении PRINT (E) будет сформирована последовательность операторов
cout <<"\n" <<"E" <<" равно " ; cout << E;
Последующая подстановка приведет к следующему результату:
cout <<"\n" <<"E" <<" равно "; cout <<х*х;
Именно поэтому в результатах слева от слова "равно" печатается изображение параметра, использованного в PRINT. Рассмотрим результаты подробнее. В операторе
PRINT (mах (++х, ++х));
печатается значение выражения
(++х < ++х ? ++х : ++х),
после вычисления которого х увеличивается на 3, т.е. становится равным 5. В операторе PRINT (t(x)); печатается значение выражения х*3. В операторе PRINT (t(x + х)); - значение выражения х + х*3. Чтобы подстановка t(e) утраивала значение любого аргумента, следует использовать скобки вокруг параметра в строке замещения, т.е. записать #define t(e) (e) *3. При таком определении t(e) в операторах
PRINT(t(x + х));
PRINT(t(x + х))/3;
сформировались бы выражения (х + х) *3, равное 30 и (х + х) *3/3, равное 10.
Приведенные примеры показывают, что для устранения неоднозначных или неверных использовании макроподстановок параметры в строке замещения и ее саму, по крайней мере, полезно заключать в скобки.
В заключение приведем еще некоторые сведения о работе препроцессора и его директивах.
При препроцессорной обработке исходного текста программы каждая строка обрабатывается отдельно. Напоминаем, что возможно "соединение" строк: если в конце строки стоит символ ' \ ', а за ним -символ перехода на новую строку ' \n ', то эта пара символов исключается и следующая строка непосредственно присоединяется к текущей строке. Анализируя полученные строки, препроцессор распознает лексемы. Лексемами для препроцессора являются:
Аргументы вызова макроса - лексемы, разделенные запятыми. Аргументы макрорасширениям не подвергаются.
В последовательности лексем, образующей строку замещения, предусматривается использование двух операций - ' # ' и ' ## ', первая из которых помещается перед параметром, а вторая - между любыми двумя лексемами
Операция '#' , уже использованная выше в программе Р8-02.СРР, требует, чтобы текст, замещающий данный параметр в формируемой строке, заключался в двойные кавычки. Например, для определения
#define sm(zip) cout <<#zip
обращение (макровызов) sm(сумма) ; приведет к формированию оператора cout <<"сумма";.
Операция ' # # ', допускаемая только между лексемами строки замещения, позволяет выполнять конкатенацию лексем, включаемых в строку замещения. Определение
#define abc(a,b,c,d) а # #(# # b # # c # # d)
позволит сформировать выражение sin (х+у), если использовать макровызов abc (sin, х,+, у).
Препроцессорная операция defined позволяет по-другому записать директивы условной компиляции:
#if defined
есть аналог #ifdef и
#if !defined
есть аналог #ifndef.
Для нумерации строк можно использовать директиву
#line константа
которая указывает компилятору, что следующая ниже строка текста имеет номер, определяемый целой десятичной константой. Команда может определять не только номер строки, но и имя файла:
#line константа "имя_файла"
Директива
#error последовательность_лексем
приводит к выдаче диагностического сообщения в виде последовательности лексем. Естественно применение директивы #error совместно с условными препроцессорными командами. Например, определив некоторую препроцессорную переменную NAME
#define NAME 5
в дальнейшем можно проверить ее значение и выдать сообщение, если у NAME другое значение:
#if (NAME != 5) #error NAME должно быть равно 5 !
Сообщение будет выглядеть так:
fatal: имя_файла номер_строки
#error directive: NAME должно бить равно 5 !
Допустима пустая директива:
#
не вызывающая никаких действий. Команда
#pragma последовательность лексем
определяет действия, зависящие от конкретной реализации компилятора. Например, в ТС++ и ВС++ входит вариант этой директивы для извещения компилятора о наличии в тексте программы команд ассемблера.
Существуют встроенные (заранее определенные) макроимена, доступные препроцессору во время обработки. Они позволяют получить следующую информацию:
_ _LINE_ _
десятичная константа - номер текущей обрабатываемой строки файла с программой Си++. Принято, что номер первой строки исходного файла равен 1.
_ _FILE_ _
строка символов - имя компилируемого файла. Имя изменяется всякий раз, когда препроцессор встречает директиву #include с указанием имени файла. После окончания включения файла по команде #include восстанавливается предыдущее значение макроимени _ _FILЕ_ _.
_ _DATE_ _
строка символов в формате: "месяц число год", определяющая дату начала обработки исходного файла.
_ _ТIМЕ_ _
строка символов вида "часы:минуты:секунды", определяющая время начала обработки препроцессором текущего исходного файла.
_ _STDC_ _
константа, равная 1, если компилятор работает в соответствии с ANSI-стандартом. В противном случае значение макроимени _ _STDC_ _ не определено. Проект стандарта Си++ предполагает, что наличие имени _ _STDC_ _ определяется реализацией.
_ _cplusplus_ _
имя, определенное равным 1 при компиляции программы на языке Си++. В остальных случаях этот макрос не определен.
В конкретных реализациях набор предопределенных имен шире. Например, в препроцессор ВС++ дополнительно включены:
Совокупность принципов проектирования, разработки и реализации программ, которая базируется на абстракции данных, предусматривает создание новых типов данных, с наибольшей полнотой отображающих особенности решаемой задачи. Одновременно с данными для каждого типа вводится набор операций, удобных для обработки этих данных. Таким образом, создаваемые пользователем абстрактные типы данных могут обеспечить представление понятий предметной области решаемой задачи. В языке Си++ программист имеет возможность вводить собственные типы данных и определять операции над ними с помощью классов.
Класс - это производный структурированный тип, введенный программистом на основе уже существующих типов. Механизм классов позволяет создавать типы в полном соответствии с принципами абстракции данных, т.е. класс задает некоторую структурированную совокупность типизированных данных и позволяет определить набор операций над этими данными.
Простейшим образом класс можно определить с помощью конструкции:
ключ_класса имя_класса { список_компонентов };
где ключ_класса - одно из служебных слов class, struct, union;
имя_класса - произвольно выбираемый идентификатор; список_компонентов - определения и описания типизированных данных и принадлежащих классу функций.
В проекте стандарта языка Си++ указано, что компонентами класса могут быть данные, функции, классы, перечисления, битовые поля, дружественные функции, дружественные классы и имена типов. Вначале для простоты будем считать, что компоненты класса - это типизированные данные (базовые и производные) и функции. Заключенныи в фигурные скобки список компонентов называют телом класса. Телу класса предшествует заголовок.
В простейшем случае заголовок класса включает ключ класса и его имя. Определение класса всегда заканчивается точкой с запятой.
Отметим терминологические трудности, связанные с классами. Все компоненты класса в английском языке обозначаются термином member (член, элемент, часть). Функции, принадлежащие классу, обозначают термином member functions, а данные класса имеют название data members. В русском языке терминология, относящаяся к классам, недостаточно устоялась, поэтому имеются многочисленные расхождения. Принадлежащие классу функции называют методами класса или компонентными функциями. Данные класса называют компонентными данными или элементами данных (объектов) класса.
В качестве ключа_класса можно использовать служебное слово struct, но класс отличается от обычного структурного типа, по крайней мере, возможностью включения компонентных функций. Например, следующая конструкция простейшим образом вводит класс "комплексное число":
struct complex1 // Вариант класса "комплексное число"
{ double real; // Вещественная часть
double imag; // Мнимая часть
// Определить значение комплексного числа:
void define (double re = 0.0, double im = 0.0)
{ real = re; imag = im; }
// Вывести на экран значение комплексного числа:
void display(void)
{ cout <<"real = " << real;
cout <<", imag = " << imag;
}
} ;
В отличие от структурного типа в класс (тип) complexl, кроме компонентных данных (real, imag), включены две компонентные функции define () и display().
Недостатков в нашем простейшем определении класса комплексных чисел несколько. Однако отложим их объяснение и устранение, а обратим еще раз внимание на тот факт, что класс (как и его частный случай - структура), введенный пользователем, обладает правами типа. Следовательно, можно определять и описывать объекты класса и создавать производные типы:
complex1 X1, Х2, D; // Три объекта класса complex1
complex1 *point = &D; // Указатель на объект класса
// complex1
complexl dim[8]; // Массив объектов класса complex1
complex1 &Name = Х2; // Ссылка на объект класса complex1
и.т.д.
(Класс "комплексное число" очень полезен в прикладных программах, и поэтому в компиляторах языка Си++ он включен в стандартные библиотеки. Библиотечный класс complex становится доступным при включении в программу заголовочного файла complex. h (СМ. Прил. 6.).)Итак, класс - это тип, введенный программистом. Каждый тип служит для определения объектов. Для описания объекта класса используется конструкция:
имя_класса имя_объекта;
В определяемые объекты (класса) входят данные (элементы), соответствующие компонентным данным класса. Компонентные функции класса позволяют обрабатывать данные конкретных объектов класса. Но в отличие от компонентных данных компонентные функции не тиражируются при создании конкретных объектов класса. Если перейти на уровень реализации, то место в памяти выделяется именно для элементов каждого объекта класса. Определение объекта класса предусматривает выделение участка памяти и деление этого участка на фрагменты, соответствующие отдельным элементам объекта, каждый из которых отображает отдельный компонент данных класса. Таким образом, и в объект x1, и в объект dim[3] класса complexl входит по два элемента типа double, представляющих вещественные и мнимые части комплексных чисел.
Как только объект класса определен, появляется возможность обращаться к его компонентам, во-первых, с помощью "квалифицированных" имен, каждое из которых имеет формат:
имя_о6ъекта.имя-класса : : имя_компонента
Имя класса с операцией уточнения области действия ':: ' обычно может быть опущено, и чаще всего для доступа к данным конкретного объекта заданного класса (как и в случае структур) используется уточненное имя:
имя_объекта.имя_элемента
При этом возможности те же, что и при работе с элементами структур. Например, можно явно присвоить значения элементам объектов класса complex1:
X1.real = dim[3].real = 1.24;
X1. image =2.3; dim [3] .image = 0.0;
Уточненное имя принадлежащей классу (т.е. компонентной) функции
имя_объекта.обращения_к_компонентной_функции
обеспечивает вызов компонентной функции класса для обработки данных именно того объекта, имя которого использовано в уточненном имени. Например, можно таким образом определить значения компонентных данных для определенных выше объектов класса complex1:
X1.define(); // Параметры выбираются по умолчанию:
// real==0.0, imag==0.0
Х2.define(4.3,20.0); // Комплексное число 4.3 + i*20.0
С помощью принадлежащей классу complexl функции display () можно вывести на экран значения компонентных данных любого из объектов класса. (Разумно выполнять вывод только для объектов, которым уже присвоены осмысленные значения.) Например, следующий вызов принадлежащей классу complexl функции:
X2. display ();
приведет к печати
real =4.3, imag = 20.0
Другой способ доступа к элементам объекта некоторого класса предусматривает явное использование указателя на объект класса и операции косвенного выбора компонента ('->'):
указатель на объект класса -> имя элемента
Определив, как сделано выше, указатель point, адресующий объект D класса complexl, можно следующим образом присвоить значения данным объекта D:
point->real =2.3; // Присваивание значения элементу
// объекта D
point->imag = 6.1; // Присваивание значения элементу
// объекта D
Указатель на объект класса позволяет вызывать принадлежащие классу функции для обработки данных того объекта, который адресуется указателем. Формат вызова функции:
указатель_на_объект_класса ->
обращение_к_компонентной функции
Например, вызвать компонентную функцию display ( ) для данных объекта () позволяет выражение
point->display ();
В качестве второго примера рассмотрим класс, описывающий товары на складе магазина. Компонентами класса будут:
Определение класса:
//GOODS.CPP - класс "товары на складе магазина"
#include < iostream.h >
struct goods // Определение класса "товары"
{ char nаme [40]; // Наименование товара
float price; // Оптовая (закупочная) цена
static int percent; // Торговая наценка, в %
// Компонентные функции:
void input() // Ввод сведений о товаре
{ cout <<"Наименование товара: "; cin >> nаmе;
cout <<"Закупочная цена: "; cin >> price;
}
void Display() // Вывод данных о продаваемом товаре
{ cout <<"\n" << name;
cout <<", розничная цена: ";
cout << long(price * (1.0 + goods : : percent * 0.01));
}
};
Торговая наценка определена как статический компонент класса. Статические компоненты классов не "дублируются" при создании объектов класса, т.е. каждый статический компонент существует в единственном экземпляре. Доступ к статическому компоненту возможен только после его инициализации. Для инициализации используется конструкция:
В нашем примере может быть такой вариант:
int goods : : percent = 12;
Это предложение должно быть размещено в глобальной области (global scope) после определения класса. Только при инициализации статический компонент класса получает память и становится доступным. Для обращения к статическому компоненту используется квалифицированное имя:
имя_класса : : имя-компонента
Кроме того, статический компонент доступен "через" имя конкретного объекта:
имя_объекта.имя_класса : : имя-компонента
либо
имя-объекта.имя_компонента
В следующей программе иллюстрируются перечисленные возможности и особенности классов со статическими компонентами, а также используется массив объектов:
//Р9-01.СРР - массив объектов класса goods
#include < iostream.h >
#include "goods.cpp" // Текст определения класс
// Инициализация статического компонента:
int goods::percent = 12;
void main(void)
{ goods wares[5] = { { "Мужской костюм", 190000 },
{ "Косметический набор", 27600 },
{ "Калькулятор", 11000 }
};
int k = sizeof(wares) / sizeof(wares[0]);
cout <<"\nВведите сведения о товарах:\n ";
for (int i = 3; i < k; i++)
wares[i].Input();
cout <<"\nСписок товаров при наценке " <<
wares[0].percent <<"%";
for (i = 0; i < k; i++) wares[i].Display ();
// Изменение статического компонента:
goods::percent = 10;
cout <<"\n\nСписок товаров при наценке " <<
wares[0].goods::percent <<"%";
goods *pGood>> = wares;
for (i = 0; i < k; i++) pGoods++->Display();
}
Результаты выполнения программы:
Введите сведения о товарах:
Наименование товара: Сигареты < Enter >
Закупочная цена: 780 < Еnter >
Наименование товара: Кроссовки < Enter >
Закупочная цена: 28400 < Enter >
Список товаров при наценке 12%
Мужской костюм, розничная цена: 212800
Косметический набор, розничная цена: 30912
Калькулятор, розничная цена: 12320
Сигареты, розничная цена: 873
Кроссовки, розничная цена: 31808
Список товаров при наценке 10%
Мужской костюм, розничная цена: 209000
Косметический набор, розничная цена: 30360
Калькулятор, розничная цена: 12100
Сигареты, розничная цена: 858
Кроссовки, розничная цена: 31240
Обратите внимание на инициализацию первых элементов массива wares [5] объектов класса goods. В списках значений не отражено существование статического компонента. Точно так же при вводе данных компонентной функцией input () не изменяется значение статического компонента. Он ведь один для всех объектов класса! Для иллюстрации разных способов доступа к компонентам классов определен указатель pGoods на объекты класса goods. Он инициализирован значением адреса первого элемента массива объектов &wares [0]. В цикле указатель с операцией '->' используется для вызова компонентной функции display (). После каждого вызова указатель изменяется - настраивается наследующий элемент массива, т.е. на очередной объект класса goods.
В определениях класса complexl и класса goods есть недостатки, которые легко устранить. Первый из них - отсутствие автоматической инициализации создаваемых объектов. Для каждого вновь создаваемого объекта класса complexl необходимо вызвать функцию define () либо явным образом с помощью уточненных имен присваивать значения данным объекта, т.е. переменным real и imag. Еще два способа использованы в предыдущей программе. Часть объектов класса goods получила начальные значения при инициализации, которая выполняется по правилам, относящимся к структурам и массивам. Объектам wares [3] и wares [4] значения присвоены с помощью явного вызова компонентной функции input ().
Для инициализации объектов класса в его определение можно явно включать специальную компонентную функцию, называемую конструктор. Формат определения конструктора в теле класса может быть таким:
имя_класса(список_формальных_параметров)
{ операторы_тела_конструктора };
Имя этой компонентной функции по правилам языка Си++ должно совпадать с именем класса. Такая функция автоматически вызывается при определении или размещении в памяти с помощью оператора new каждого объекта класса. Основное назначение конструктора - инициализация объектов. Для класса соmplех1 можно ввести конструктор, эквивалентный функции define (), но отличающийся от нее только названием:
complex1(double re = 0.0, double im = 0.0)
{ real = re; imag = im; }
В соответствии с синтаксисом языка для конструктора не определяется тип возвращаемого значения. Даже тип void недопустим. С помощью параметров конструктору могут быть переданы любые данные, необходимые для создания и инициализации объектов класса. В конструктор complex1 () передаются значения элементов объекта "комплексное число". По умолчанию за счет начальных значений параметров формируется комплексное число с нулевыми мнимой и вещественной частями. В общем случае конструктор может быть как угодно сложным.
Например, в классе "матрицы" конструктор будет выделять память для массивов, с помощью которых представляется каждая матрица - объект данного класса, а затем инициализировать эти массивы. Размеры матриц и начальные значения их элементов такой конструктор может получать через аппарат параметров, как и значения составных частей комплексного числа в конструкторе complexl ().
Для класса "товары на складе магазина" конструктор можно определить следующим образом:
goods(char *new_name, float new_price)
{ nаmе = new_name; // Наименование товара
price = new_price; // Закупочная цена
}
В конструкторе можно было бы изменять и значение заранее инициализированного статического компонента percent, однако в рассматриваемом примере это не делается.
Второй недостаток классов complexl и goods, введенных с помощью служебного слова struct, - это общедоступность компонент. В любом месте программы, где "видно" определение класса, можно с помощью уточненных имен (например, имя_объекта.геаl или имя_объекта.imаg) или с помощью указателя на объект и операции косвенного выбора '->' получить доступ к компонентным данным этого объекта. Тем самым не выполняется основной принцип абстракции данных - инкапсуляция (сокрытие) данных внутри объектов.
В соответствии с правилами языка Си++ все компоненты класса, введенного с помощью ключа класса struct, являются общедоступными (public). Для изменения видимости компонент в определении класса можно использовать спецификаторы доступа. Спецификатор доступа - это одно из трех служебных слов private (собственный), publk (общедоступный), protected (защищенный), за которым помещается двоеточие.
Появление любого из спецификаторов доступа в тексте определения класса означает, что до конца определения либо до другого спецификатора доступа все компоненты класса имеют указанный статус.
Защищенные (protected) компоненты классов нужны только в случае построения иерархии классов. При использовании классов без порождения на основе одних классов других (производных), применение спецификатора protected эквивалентно использованию спецификатора private.
Применение в качестве ключа класса служебного слова union приводит к созданию классов с несколько необычными свойствами, которые нужны для весьма специфических приложений. Пример такого приложения - экономия памяти за счет многократного использования одних и тех же участков памяти для разных целей. В каждый момент времени исполнения программы объект-объединение содержит только один компонент класса, определенного с помощью union. Все компоненты этого класса являются общедоступными, но доступ может быть изменен с помощью спецификаторов доступа protected (защищенный), рrivatе(собственный), publik (общедоступный).
Изменить статус доступа к компонентам класса можно и с помощью использования в определении класса ключевого слова class. Все компоненты класса, определение которого начинается со служебного слова class, являются собственными (private), т.е. недоступными для внешних обращений. Так как класс, все компоненты которого недоступны вне его определения, редко может оказаться полезным, то изменить статус доступа к компонентам позволяют спецификаторы доступа private (собственный), public (общедоступный), protected (защищенный).
Итак, для сокрытия данных внутри объектов класса, определенного с применением ключа struct, достаточно перед их появлением в определении типа (в определении класса) поместить спецификатор private. При этом необходимо, чтобы некоторые или все принадлежащие классу функции остались доступными извне, что позволило бы манипулировать с данными объектов класса. Этим требованиям будет удовлетворять следующее определение класса "комплексное число":
//COMPLEX.СРР - определение класса "комплексное число"
#include < iostream.h >
// Класс с конструктором и инкапсуляцией данных:
struct complex
{ // Методы класса (все общедоступные - public):
// Конструктор объектов класса:
complex (double re = 1.0, double im = 0.0)
{ real = re; imаg = im; }
// Вывести на дисплей значение комплексного числа:
void display(void)
{ cout <<"real = " << real;
cout <<", imag = " << imag;
}
// Получить доступ к вещественной части числа:
double &re(void) { return real; }
// Получить доступ к мнимой части числа:
double &im (void) { return imag; }
// Данные класса (скрыты от прямых внешних обращений):
private: // Изменить статус доступа на "собственный"
double real; // Вещественная часть
double imag; // Мнимая часть
};
По сравнению с классом complexl в новый класс complex, кроме конструктора, дополнительно введены компонентные функции re () и im (), с помощью которых можно получать доступ к данным объектов. Они возвращают ссылки соответственно на вещественную и мнимую части того объекта, для которого они будут вызваны.
Напомним, что для конструктора не задается тип возвращаемого значения. Существуют особенности и в вызове конструктора. Без явного указания программиста конструктор всегда автоматически вызывается при определении (создании) объекта класса. При этом используются умалчиваемые значения параметров конструктора. Например, определив объект ее с неявным вызовом конструктора
complex СС;
получим при вызове СС. re () значение 1.0. Функция cc.im () вернет ссылку на cc.imag, и этот элемент объекта cc будет иметь значение 0.0, заданное как умалчиваемое значение параметра конструктора.
Итак, "конструктор превращает фрагмент памяти в объект, для которого выполнены правила системы типов" [2], т.е. в объект того типа, который предусмотрен определением класса.
Конструктор существует для любого класса, причем он может быть создан без явных указаний программиста. Таким образом, для классов goods и complexl существуют автоматически созданные конструкторы.
По умолчанию формируются конструктор без параметров и конструктор копирования вида Т::T(const Т&) где T - имя класса. Например,
class F
{ ...
public: F(const F&);
...
}
Такой конструктор существует всегда. По умолчанию конструктор копирования создается общедоступным. В классе может быть несколько конструкторов (перегрузка), но только один с умалчиваемыми значениями параметров.
Нельзя получить адрес конструктора.
Параметром конструктора не может быть его собственный класс, но может быть ссылка на него, как у конструктора копирования.
Конструктор нельзя вызывать как обычную компонентную функцию. Для явного вызова конструктора можно использовать две разные синтаксические формы:
имя_ класса имя_объекта(фактические_параметры_конструктора);
имя_класса (фактические_параметры_конструктора);
Первая форма допускается только при непустом списке фактических параметров. Она предусматривает вызов конструктора при определении нового объекта данного класса:
complex SS(10.3,0.22); // SS.real == 10.3;
// SS.imag ==0.22
complex ЕЕ(2.345); // ЕЕ.real == 2.345;
// по умолчанию EE.imag ==0.0
complex DD (); // Ошибка! Компилятор решит, что это
// прототип функции без параметров,
// возвращающей значение типа complex
Вторая форма явного вызова конструктора приводит к созданию объекта, не имеющего имени. Созданный таким вызовом безымянный объект может использоваться в тех выражениях, где допустимо использование объекта данного класса. Например:
complex ZZ=complex(4.0,5.0);
Этим определением создается объект ZZ, которому присваивается значение безымянного объекта (с элементами real == 4.0, imag == 5.0), созданного за счет явного вызова конструктора.
Существуют два способа инициализации данных объекта с помощью конструкторов. Первый способ, а именно передача значений параметров в тело конструктора, уже продемонстрирован на примерах. Второй способ предусматривает применение списка инициализаторов данных объекта. Этот список помещается между списком параметров и телом конструктора:
имя_класса(список_ параметров):
список_инициализаторов_ компонентных данных { тело_конструктора }
Каждый инициализатор списка относится к конкретному компоненту и имеет вид:
имя_компонента_данных (выражение) Например:
class AZ { int ii; float ее; char сс;
public:
AZ(int in, float en, char cn) : ii(5),
ее(ii * en + in), cc(cn) { }
} ;
AZ A(2,3.0,'d'); // Создается именованный объект А
// с компонентами А.ii == 5,
// А.ее == 17, А.cc == 'd'
AZ X = AZ(0,2.0,'z'); // Создается безымянный объект, в
// котором ii == 5, ее == 10,
// cc == 'z', и копируется
// в объект Х
Перечисленные особенности конструкторов, соглашения о статусах доступа компонентов и новое понятие "деструктор" иллюстрирует следующее определение класса "символьная строка":
//STROKA.CPP - файл с определением класса "символьная
// строка"
#include < string.h > // Для библиотечных строковых функций
#include < iostream.h >
class stroka
{ // Скрытые от внешнего доступа данные:
char *ch; // Указатель на текстовую строку
int len; // Длина текстовой строки
public: // Общедоступные функции:
// Конструкторы объектов класса:
// Создает объект как новую пустую строку:
stroka(int N = 80) :
// Строка не содержит информации:
lеn(0)
{ ch = new char[N + 1]; // Память выделена для массива
ch[0] = '\0';
} // Создает объект по заданной строке:
stroka (const char *arch)
{ len = strlen(arch);
ch = new char[len+1];
strcpy(ch,arch);
}
int& len_str(void) // Возвращает ссылку на длину строки
{ return len; }
char * string(void) // Возвращает указатель на строку
{ return ch; }
void display(void) // Печатает информацию о строке
{ cout <<"\nДлина строки: " << len;
cout <<"\nСодержимое строки: " << ch;
} // Деструктор - освобождает память объекта:
~stroka()
{ delete [] ch; }
};
В следующей программе создаются объекты класса stroka и выводится информация на дисплей об их компонентах:
//Р9-02.СРР - программа с классом "символьные строки"
#include "stroka.cpp" // Текст определения класса
void main ()
{ stroka LAT("Non Multa, Sed Multum!");
stroka RUS("He много, но многое!");
Результат выполнения программы:
Длина строки: 22
Содержимое строки: Non Multa, Sed Multum!
В объекте RUS: He много, но многое!
Длина строки: О
Содержимое строки:
Так как класс stroka введен с помощью служебного слова class, то элементы char *ch и int len недоступны для непосредственного обращения. Чтобы получить значение длины строки из конкретного объекта, нужно использовать общедоступную компонентную функцию len_str(). Указатель на строку, принадлежащую конкретному объекту класса stroka, возвращает функция string (). У класса stroka два конструктора - перегруженные функции, при выполнении каждой из которых динамически выделяется память для символьного массива. При вызове конструктора с параметром int N массив из N + 1 элементов остается пустым, а длина строки устанавливается равной 0. При вызове с параметром char *arch длина массива и его содержание определяются уже существующей строкой, которую адресует фактический параметр, соответствующий указателю-параметру arch.
Заслуживает внимания компонентная функция ~stroka (). Это деструктор. Объясним его назначение и рассмотрим его свойства.
Динамическое выделение памяти для объектов какого-либо класса создает необходимость в освобождении этой памяти при уничтожении объекта. Например, если объект некоторого класса формируется как локальный внутри блока, то целесообразно, чтобы при выходе из блока, когда уже объект перестает существовать, выделенная для него память была возвращена системе. Желательно, чтобы освобождение памяти происходило автоматически и не требовало вмешательства программиста. Такую возможность обеспечивает специальный компонент класса - деструктор (разрушитель объектов) класса. Для него предусматривается стандартный формат;
~имя класса() { операторы_ тела_ деструктора };
Название деструктора в Си++ всегда начинается с символа тильда '~', за которым без пробелов или других разделительных знаков помещается имя класса. У деструктора не может быть параметров (даже типа void). Деструктор не имеет возвращаемого значения (даже типа void). Вызов деструктора выполняется неявно, автоматически, как только объект класса уничтожается. В нашем примере в теле деструктора только один оператор, освобождающий память, выделенную для символьного массива при создании объекта класса stroka.
При определении класса в его теле описываются и (или) определяются данные класса и принадлежащие ему функции.
Компонентные данные. Определение данных класса внешне аналогично обычному описанию объектов базовых и производных типов. Класс в этом отношении полностью сохраняет все особенности структурных типов. Именно поэтому данные класса могут быть названы его элементами. Элементы класса могут быть как базовых, так и производных типов, т.е. компонентными данными служат переменные, массивы, указатели и т.д. Как обычно, описания элементов одного типа могут быть объединены в одном операторе. Например:
class point ( float x, у, z; long а, b, с; );
В отличие от обычного определения данных при описании элементов класса не допускается их инициализация. Это естественное свойство класса, так как при его определении еще не существует участков памяти, соответствующих его компонентным данным. Напоминаем, что память выделяется не для класса, а только для объектов класса. Для инициализации компонентных данных объектов должен использоваться автоматический или явно вызываемый конструктор соответствующего класса. Существуют различия между обращениями к компонентным данным класса из принадлежащих ему функций и из других частей программы. Как уже показано на примерах, классы complex, goods, stroka, принадлежащие классу функции, имеют полный доступ к его данным, т.е. для обращения к элементу класса из тела компонентной функции достаточно использовать только имя компонента. Например, в одном конструкторе класса stroka использован оператор:
ch = new char[len + 1];
За простотой такого обращения к данным класса из его компонентных функций скрывается механизм неявного отождествления имен компонентных данных класса с элементами именно того объекта класса, для которого вызывается компонентная функция. Например, при таком определении объекта line класса stroka:
stroka line(20);
значения присваиваются именно переменным line.len и line.ch.
Для доступа к компонентным данным из операторов, выполняемых вне определения класса, непосредственное использование имен элементов недопустимо. Смысл такого запрета определяется упомянутым механизмом привязки данных класса к конкретным объектам. Напомним, что по существу в определение класса не вводятся его данные, а только обозначается возможность их формирования при определении конкретных объектов класса. Явно размещается в памяти не класс, а конкретный объект класса. В отведенной для объекта области памяти выделяются участки, соответствующие компонентным данным (элементам объекта). Для обращения к элементу объекта, как мы уже говорили, нужно использовать операции выбора компонентов класса ( '. ' или '->'). Первая из них позволяет сформировать уточненное имя по известному имени объекта:
имя_объекта. имя_ элемента
Вторая операция обеспечивает обращение к компонентным данным объекта по заданному указателю на объект:
указатель_на_объект->имя_элемента
Подытожим особенности компонентов класса. Хотя внешне компонентные данные класса могут быть подобны данным, определенным в блоке или в теле функции, но существуют некоторые существенные отличия. Данные класса не обязательно должны быть определены или описаны до их первого использования в принадлежащих классу функциях. То же самое справедливо и для принадлежащих классу функций, т.е. обратиться из одной функции класса к другой можно до ее определения внутри тела класса. Все компоненты класса "видны" во всех операторах его тела.
Именно поэтому, кроме областей видимости "файл", "блок", "функция", в Си++ введена особая область видимости "класс".
Статические компоненты класса. Статический элемент данных класса уже использовался в классе goods (см. Р9-01. СРР), где ко всем объектам класса относилась переменная static int percent - "торговая наценка". Рассмотрим эту возможность подробнее. Итак, каждый объект одного и того же класса имеет собственную копию данных класса. Можно сказать, что данные класса тиражируются при каждом определении объекта этого класса. Отличаются они друг от друга именно по "привязке" к тому или иному объекту. Это не всегда соответствует требованиям решаемой задачи. Например, при формировании объектов класса может потребоваться счетчик объектов. Если объекты создаются и при этом сцепляются в цепочку, образуя связный список, то для просмотра всего списка удобно иметь указатель на начало списка. Для добавления нового объекта в конец такого списка нужен указатель на последний элемент списка (на последний объект, включаемый в список). Такие указатели на первый и последний объекты списка, а также уже упомянутый счетчик объектов можно сделать компонентами класса, но иметь их нужно только в единственном числе (каждый).
Чтобы компонент класса был в единственном экземпляре и не тиражировался при создании каждого нового объекта класса, он должен быть определен в классе как статический, т.е. должен иметь атрибут static. Некоторые возможности статических компонентов уже были продемонстрированы на примере класса "товары на складе магазина".
Статические компоненты класса после инициализации можно использовать в программе еще до определения объектов данного класса. Такую возможность для общедоступных данных предоставляет квалифицированное имя компонента. Когда определен хотя бы один объект класса, к его статическим компонентам можно обращаться, как к обычным компонентам, т.е. с помощью операций выбора компонентов класса ('.' и '->'). Здесь возникает одно затруднение. На статические данные класса распространяются правила статуса доступа. Если статические данные имеют статус private или protected, то к ним извне можно обращаться через компонентные функции. При каждом вызове такой компонентной функции необходимо указать имя некоторого объекта. К моменту обращения к статическим данным класса объекты класса могут быть либо еще не определены, либо их может быть несколько и каждый пригоден для вызова компонентных функций. Без имени объекта обычную компонентную функцию вызвать нельзя в соответствии с требованиями синтаксиса. Но какой объект выбрать для вызова, ведь каждый статический элемент класса единственный в нем? Хотелось бы иметь возможность обойтись без имени конкретного объекта при обращении к статическим данным класса. Такую возможность обеспечивают статические компонентные функции.
Статическая компонентная функция сохраняет все основные особенности обычных (нестатических) компонентных функций. К ней можно обращаться, используя имя уже существующего объекта класса либо указатель на такой объект. Дополнительно статическую компонентную функцию можно вызвать, используя квалифицированное имя:
имя_класса::имя_статической_ функции
С помощью квалифицированного имени статические компонентные функции можно вызывать до определения конкретных объектов класса и не используя конкретных объектов. В следующей программе класс point3 определяет точку в трехмерном пространстве и одновременно содержит статический счетчик N таких точек. Обращение к счетчику обеспечивает статическая компонентная функция count ().
//Р9-ОЗ.СРР - статические компоненты класса
#include < iostream.h >
class point3 // Точка в трехмерном пространстве
{ double x, у, z; // Координаты точки
static int N; // Количество точек (счетчик)
public:
// Конструктор инициализирует значения координат:
point3(double xn = 0.0, double yn = 0.0,
double zn = 0.0)
{ N++; x = xn; у = yn; z = xn; }
// Обращение к счетчику:
static int& count() { return N; }
};
// Внешнее описание и инициализация статического элемента:
int point3::N = 0;
void main(void) { cout <<"\nsizeof(point3) = " << sizeof(point3);
point3 A(0.0,1.0,2.0);
cout <<"\nsizeof(A) = " <
cout <<"\n0пределены >> << point3::count() <<" точки.";
point3 С(6.0,7.0,8.0);
cout <<"\n0пределены " << В.count() <<" точки.";
}
Результаты выполнения программы:
sizeof(point3) = 24
sizeof(A) = 24
Определены 2 точки.
Определены 3 точки.
Обратите внимание, что размер типа point3 равен размеру одного объекта этого класса. Память выделена для трех элементов типа double, и никак не учтено наличие в классе статического компонента
int N.
Как уже говорилось, в отличие от обычных компонентных данных статические компоненты класса необходимо дополнительно описывать и инициализировать вне определения класса как глобальные переменные. Именно таким образом в программе Р9-О3.СРР получает начальное значение статический элемент класса point3::N. Так как N - собственный компонент класса, то последующие обращения к нему возможны только с помощью дополнительной общедоступной функции. В данном примере это статическая функция count (). Попытка обратиться к компоненту N с помощью квалифицированного имени point3: :N будет воспринята как ошибка, так как для N определен статус private.
Нестатический компонент класса может быть указателем или ссылкой на объект того же класса. Такая возможность позволяет формировать связные списки (цепочки) объектов одного класса. Статическим компонентом класса может быть указатель на объект класса. Это позволит однозначно определить начало связного списка объектов класса. Например, для моделирования списка можно ввести класс такой структуры:
class list
{ ... // Собственные компоненты
public:
static list *begin; // Начало связного списка
...
);
...
list *iist::begin = NULL; // Инициализация статического
// компонента
...
Полное определение класса, моделирующего список, здесь не приводится, так как его компонентные функции удобно строить с использованием указателя this, который будет введен в следующих параграфах.
Указатели на компоненты класса. Две специфичные операции языка Си++ '. * ' и ' ->* ' предназначены для работы с указателями на компоненты класса. Прежде чем объяснить их особенности, отметим, что указатель на компонент класса не является обычным указателем, унаследованным языком Си++ от языка Си. Обыкновенный указатель предназначен для адресации того или иного объекта (участка памяти) программы. Указатель на компонент класса не может адресовать никакого участка памяти, так как память выделяется не классу, а объектам этого класса при их создании. Таким образом, указатель на компонент класса при определении не адресует никакого конкретного объекта. Каким же образом определяются (описываются) указатели на компоненты классов? Как эти указатели получают значения? Чем являются эти значения указателей? Какими возможностями обладают указатели на компоненты класса? Для каких целей они используются? Почему и как с указателями на компоненты класса используются две операции разыменования ( '. * ' и ' ->*')?
Компоненты класса, как уже многократно повторялось, делятся на две группы - компоненты-данные и компоненты-функции. Указатели на компоненты класса по-разному определяются для данных и функций (методов) класса. Начнем с указателей на принадлежащие классу функции. Их определение имеет следующий формат:
тип_возвращаемого_функцией_значения
(имя_класса :: *имя_указателя_на_метод)
(спецификация_параметров_ функции);
Например, выше в классе complex ("комплексное число") определены методы (компонентные функции) double& re (), double& im (). Вне класса можно следующим образом описать указатель ptCom:
double& (complex::*ptCom) ();
Описав указатель ptCom на компонентные функции класса complex, можно почти обычным образом задать его значение:
ptCom = &complex :: re; // "Настройка" указателя
Теперь для любого объекта А класса complex
complex A(10.0,2.4); // Определение объекта А
можно таким образом вызвать принадлежащую классу функцию re ():
(A.*ptCom) () =11.1; // Изменится вещественная часть А
cout <<(A.*ptCom) (); // Вывод на печать A.real
Изменив значение указателя
ptCom = &complex :: im; // "Настройка" указателя
можно с его помощью вызывать другую функцию того же класса:
cout <<(A.*ptCom) (); // Вывод значения мнимой части А
complex В = А; // Определение нового объекта В
(B.*ptCom) () +=3.0; // Изменение значения мнимой части В
В данных примерах определен и использован указатель на компонентную функцию без параметров, возвращающую значение типа double&. Его не удастся настроить на принадлежащие классу complex функции с другой сигнатурой и другим типом возвращаемого значения. Для обращения к компонентной функции display (), указатель ptDisp нужно ввести следующим образом:
void (complex::*ptDisp)(void);
Настроив указатель ptDisp на вещественную функцию display () класса complex, можно вызвать эту функцию для любого объекта этого класса:
ptDisp = &complex::display; // "Настройка" указателя
(B.*ptDi3p) (); // Вызов функции display () для объекта В
Формат определения указателя на компонентные данные класса:
тип_ данных(имя_ класса::*имя_ указателя);
В определение указателя можно включить его явную инициализацию, используя адрес компонента:
&имя_класса :: имя_компонента
При этом компонент класса должен быть общедоступным (public). Например, попытка определить и использовать указатель на длину строки (компонент int len) класса stroka:
int(stroka :: *plen) = &stroka :: len;
окажется неверной, так как компонент len класса stroka по умолчанию имеет атрибут private.