Здесь "очевидное" решение просто непригодно:
void print_mij(int m[][], int dim1, int dim2) // ошибка
{
for ( int i = 0; i<dim1; i++) {
for ( int j = 0; j<dim2; j++)
cout << ' ' << m[i][j];
cout << '\n';
}
}
Во-первых, описание параметра m[][] недопустимо, поскольку для
вычисления адреса элемента многомерного массива нужно знать
вторую размерность. Во-вторых, выражение m[i][j]
вычисляется как *(*(m+i)+j), а это, по всей видимости, не то, что
имел в виду программист. Приведем правильное решение:
void print_mij(int** m, int dim1, int dim2)
{
for (int i = 0; i< dim1; i++) {
for (int j = 0; j<dim2; j++)
cout << ' ' << ((int*)m)[i*dim2+j]; // запутано
cout << '\n';
}
}
Выражение, используемое для выбора элемента матрицы, эквивалентно
тому, которое создает для этой же цели транслятор, когда известна
последняя размерность. Можно ввести дополнительную переменную,
чтобы это выражение стало понятнее:
int* v = (int*)m;
// ...
v[i*dim2+j]
Лучше такие достаточно запутанные места в программе упрятывать.
Можно определить тип многомерного массива с соответствующей
операцией индексирования. Тогда пользователь может и не знать, как
размещаются данные в массиве (см. упражнение 18 в $$7.13).
4.6.6 Перегрузка имени функции
Обычно имеет смысл давать разным функциям разные имена. Если же
несколько функций выполняет одно и то же действие над объектами
разных типов, то удобнее дать одинаковые имена всем этим функциям.
Перегрузкой имени называется его использование для обозначения
разных операций над разными типами. Собственно уже для основных
операций С++ применяется перегрузка. Действительно: для операций
сложения есть только одно имя +, но оно используется для сложения
и целых чисел, и чисел с плавающей точкой, и указателей. Такой
подход легко можно распространить на операции, определенные
пользователем, т. е. на функции. Например:
void print(int); // печать целого
void print(const char*) // печать строки символов
Для транслятора в таких перегруженных функциях общее только
одно - имя. Очевидно, по смыслу такие функции сходны, но язык
не способствует и не препятствует выделению перегруженных функций.
Таким образом, определение перегруженных функций служит, прежде
всего, для удобства записи. Но для функций с такими традиционными
именами, как sqrt, print или open, нельзя этим удобством пренебрегать.
Если само имя играет важную семантическую роль, например,
в таких операциях, как + , * и << ($$7.2), или для конструктора
класса ($$5.2.4 и $$7.3.1), то такое удобство становится существенным
фактором. При вызове функции с именем f транслятор должен
разобраться, какую именно функцию f следует вызывать. Для этого
сравниваются типы фактических параметров, указанные в вызове, с типами
формальных параметров всех описаний функций с именем f. В результате
вызывается та функция, у которой формальные параметры наилучшим
образом сопоставились с параметрами вызова, или выдается ошибка
если такой функции не нашлось. Например:
void print(double);
void print(long);
void f()
{
print(1L); // print(long)
print(1.0); // print(double)
print(1); // ошибка, неоднозначность: что вызывать
// print(long(1)) или print(double(1)) ?
}
Подробно правила сопоставления параметров описаны в $$R.13.2. Здесь
достаточно привести их суть. Правила применяются в следующем
порядке по убыванию их приоритета:
[1] Точное сопоставление: сопоставление произошло без всяких
преобразований типа или только с неизбежными преобразованиями
(например, имени массива в указатель, имени функции в указатель
на функцию и типа T в const T).
[2] Сопоставление с использованием стандартных целочисленных
преобразований, определенных в $$R.4.1 (т. е. char в int,
short в int и их беззнаковых двойников в int), а также
преобразований float в double.
[3] Сопоставление с использованием стандартных преобразований,
определенных в $$R.4 (например, int в double, derived* в
base*, unsigned в int).
[4] Сопоставление с использованием пользовательских преобразований
($$R.12.3).
[5] Сопоставление с использованием эллипсиса... в описании
функции.
Если найдены два сопоставления по самому приоритетному правилу,
то вызов считается неоднозначным, а значит ошибочным. Эти правила
сопоставления параметров работают с учетом правил преобразований
числовых типов для С и С++. Пусть имеются такие описания функции
print:
void print(int);
void print(const char*);
void print(double);
void print(long);
void print(char);
Тогда результаты следующих вызовов print() будут такими:
void h(char c, int i, short s, float f)
{
print(c); // точное сопоставление: вызывается print(char)
print(i); // точное сопоставление: вызывается print(int)
print(s); // стандартное целочисленное преобразование:
// вызывается print(int)
print(f); // стандартное преобразование:
// вызывается print(double)
print('a'); // точное сопоставление: вызывается print(char)
print(49); // точное сопоставление: вызывается print(int)
print(0); // точное сопоставление: вызывается print(int)
print("a"); // точное сопоставление:
// вызывается print(const char*)
}
Обращение print(0) приводит к вызову print(int), ведь 0 имеет тип int.
Обращение print('a') приводит к вызову print(char), т. к. 'a' - типа
char ($$R.2.5.2).
Отметим, что на разрешение неопределенности при перегрузке не
влияет порядок описаний рассматриваемых функций, а типы возвращаемых
функциями значений вообще не учитываются.
Исходя из этих правил можно гарантировать, что если эффективность
или точность вычислений значительно различаются для
рассматриваемых типов, то вызывается функция, реализующая самый
простой алгоритм. Например:
int pow(int, int);
double pow(double, double); // из <math. h>
complex pow(double, complex); // из <complex. h>
complex pow(complex, int);
complex pow(complex, double);
complex pow(complex, complex);
void k(complex z)
{
int i = pow(2,2); // вызывается pow(int, int)
double d = pow(2.0,2); // вызывается pow(double, double)
complex z2 = pow(2,z); // вызывается pow(double, complex)
complex z3 = pow(z,2); // вызывается pow(complex, int)
complex z4 = pow(z, z); // вызывается pow(complex, complex)
}
4.6.7 Стандартные значения параметров
В общем случае у функции может быть больше параметров, чем в самых
простых и наиболее часто используемых случаях. В частности, это
свойственно функциям, строящим объекты (например, конструкторам,
см. $$5.2.4). Для более гибкого использования этих функций иногда
применяются необязательные параметры. Рассмотрим в качестве примера
функцию печати целого числа. Вполне разумно применить в качестве
необязательного параметра основание счисления печатаемого числа,
хотя в большинстве случаев числа будут печататься как десятичные
целые значения. Следующая функция
void print (int value, int base =10);
void F()
{
print(31);
print(31,10);
print(31,16);
print(31,2);
}
напечатает такие числа:
f 11111
Вместо стандартного значения параметра можно было бы использовать
перегрузку функции print:
void print(int value, int base);
inline void print(int value) { print(value,10); }
Однако в последнем варианте текст программы не столь явно демонстрирует
желание иметь одну функцию print, но при этом обеспечить удобную и
краткую форму записи.
Тип стандартного параметра сверяется с типом указанного значения
при трансляции описания функции, а значение этого параметра вычисляется
в момент вызова функции. Задавать стандартное значение можно только
для завершающих подряд идущих параметров:
int f(int, int =0, char* =0); // нормально
int g(int =0, int =0, char*); // ошибка
int h(int =0, int, char* =0); // ошибка
Отметим, что в данном контексте наличие пробела между символами * и =
весьма существенно, поскольку *= является операцией присваивания:
int nasty(char*=0); // синтаксическая ошибка
4.6.8 Неопределенное число параметров
Существуют функции, в описании которых невозможно указать число
и типы всех допустимых параметров. Тогда список формальных
параметров завершается эллипсисом (...), что означает:
"и, возможно, еще несколько аргументов". Например:
int printf(const char* ...);
При вызове printf обязательно должен быть указан параметр
типа char*, однако могут быть (а могут и не быть) еще другие
параметры. Например:
printf("Hello, world\n");
printf("My name is %s %s\n", first_name, second_name);
printf("%d + %d = %d\n", 2,3,5);
Такие функции пользуются для распознавания своих фактических
параметров недоступной транслятору информацией. В случае функции
printf первый параметр является строкой, специфицирующей формат вывода.
Она может содержать специальные символы, которые позволяют правильно
воспринять последующие параметры. Например, %s означает -"будет
фактический параметр типа char*", %d означает -"будет фактический
параметр типа int" (см. $$10.6). Но транслятор этого не знает, и
поэтому он не может убедиться, что объявленные параметры действительно
присутствуют в вызове и имеют соответствующие типы. Например,
следующий вызов
printf("My name is %s %s\n",2);
нормально транслируется, но приведет (в лучшем случае) к неожиданной
выдаче. Можете проверить сами.
Очевидно, что раз параметр неописан, то транслятор не имеет сведений
для контроля и стандартных преобразований типа этого параметра.
Поэтому char или short передаются как int, а float как double, хотя
пользователь, возможно, имел в виду другое.
В хорошо продуманной программе может потребоваться, в виде
исключения, лишь несколько функций, в которых указаны не все типы
параметров. Чтобы обойти контроль типов параметров, лучше использовать
перегрузку функций или стандартные значения параметров, чем
параметры, типы которых не были описаны. Эллипсис становится
необходимым только тогда, когда могут меняться не только типы, но
и число параметров. Чаще всего эллипсис используется
для определения интерфейса с библиотекой стандартных функций на С,
если этим функциям нет замены:
extern "C" int fprintf(FILE*, const char* ...);
extern "C" int execl(const char* ...);
Есть стандартный набор макроопределений, находящийся в <stdarg. h>,
для выбора незаданных параметров этих функций. Рассмотрим функцию
реакции на ошибку, первый параметр которой показывает степень тяжести
ошибки. За ним может следовать произвольное число строк. Нужно
составить сообщение об ошибке с учетом, что каждое слово из него
передается как отдельная строка:
extern void error(int...)
extern char* itoa(int);
main(int argc, char* argv[])
{
switch (argc) {
case 1:
error(0,argv[0],(char*)0);
break;
case 2:
error(0,argv[0],argv[1],(char*)0);
break;
default:
error(1,argv[0],
"With",itoa(argc-1),"arguments",(char*)0);
}
// ...
}
Функция itoa возвращает строку символов, представляющую ее целый
параметр. Функцию реакции на ошибку можно определить так:
#include <stdarg. h>
void error(int severity...)
/*
за "severity" (степень тяжести ошибки) следует
список строк, завершающийся нулем
*/
{
va_list ap;
va_start(ap, severity); // начало параметров
for (;;) {
char* p = va_arg(ap, char*);
if (p == 0) break;
cerr << p << ' ';
}
va_end(ap); // очистка параметров
cerr << '\n';
if (severity) exit(severity);
}
Вначале при вызове va_start() определяется и инициализируется
va_list. Параметрами макроопределения va_start являются имя типа
va_list и последний формальный параметр. Для выборки по порядку
неописанных параметров используется макроопределение va_arg().
В каждом обращении к va_arg нужно задавать тип ожидаемого фактического
параметра. В va_arg() предполагается, что параметр такого типа
присутствует в вызове, но обычно нет возможности проверить это.
Перед выходом из функции, в которой было обращение к va_start,
необходимо вызвать va_end. Причина в том, что в va_start()
могут быть такие операции со стеком, из-за которых корректный возврат
из функции становится невозможным. В va_end() устраняются все
нежелательные изменения стека.
Приведение 0 к (char*)0 необходимо потому, что sizeof(int)
не обязано совпадать с sizeof(char*). Этот пример демонстрирует
все те сложности, с которыми приходится сталкиваться
программисту, если он решил обойти контроль типов, используя
эллипсис.
4.6.9 Указатель на функцию
Возможны только две операции с функциями: вызов и взятие адреса.
Указатель, полученный с помощью последней операции, можно
впоследствии использовать для вызова функции. Например:
void error(char* p) { /* ... */ }
void (*efct)(char*); // указатель на функцию
void f()
{
efct = &error; // efct настроен на функцию error
(*efct)("error"); // вызов error через указатель efct
}
Для вызова функции с помощью указателя (efct в нашем примере)
надо вначале применить операцию косвенности к указателю - *efct.
Поскольку приоритет операции вызова () выше, чем приоритет
косвенности *, нельзя писать просто *efct("error"). Это будет
означать *(efct("error")), что является ошибкой. По той же
причине скобки нужны и при описании указателя на функцию. Однако,
писать просто efct("error") можно, т. к. транслятор понимает, что
efct является указателем на функцию, и создает команды, делающие
вызов нужной функции.
Отметим, что формальные параметры в указателях на функцию описываются
так же, как и в обычных функциях. При присваивании указателю на функцию
требуется точное соответствие типа функции и типа присваиваемого
значения. Например:
void (*pf)(char*); // указатель на void(char*)
void f1(char*); // void(char*);
int f2(char*); // int(char*);
void f3(int*); // void(int*);
void f()
{
pf = &f1; // нормально
pf = &f2; // ошибка: не тот тип возвращаемого
// значения
pf = &f3; // ошибка: не тот тип параметра
(*pf)("asdf"); // нормально
(*pf)(1); // ошибка: не тот тип параметра
int i = (*pf)("qwer"); // ошибка: void присваивается int
}
Правила передачи параметров одинаковы и для обычного вызова,
и для вызова с помощью указателя.
Часто бывает удобнее обозначить тип указателя на функцию именем,
чем все время использовать достаточно сложную запись. Например:
typedef int (*SIG_TYP)(int); // из <signal. h>
typedef void (SIG_ARG_TYP)(int);
SIG_TYP signal(int, SIG_ARG_TYP);
Также часто бывает полезен массив указателей на функции. Например,
можно реализовать систему меню для редактора с вводом, управляемым
мышью, используя массив указателей на функции, реализующие команды.
Здесь нет возможности подробно описать такой редактор, но дадим самый
общий его набросок:
typedef void (*PF)();
PF edit_ops[] = { // команды редактора
&cut, &paste, &snarf, &search
};
PF file_ops[] = { // управление файлом
&open, &reshape, &close, &write
};
Далее надо определить и инициализировать указатели, с помощью которых
будут запускаться функции, реализующие выбранные из меню команды.
Выбор происходит нажатием клавиши мыши:
PF* button2 = edit_ops;
PF* button3 = file_ops;
Для настоящей программы редактора надо определить большее число
объектов, чтобы описать каждую позицию в меню. Например, необходимо
где-то хранить строку, задающую текст, который будет выдаваться для
каждой позиции. При работе с системой меню назначение клавиш мыши
будет постоянно меняться. Частично эти изменения можно представить
как изменения значений указателя, связанного с данной клавишей. Если
пользователь выбрал позицию меню, которая определяется, например,
как позиция 3 для клавиши 2, то соответствующая команда реализуется
вызовом:
(*button2[3])();
Чтобы полностью оценить мощность конструкции указатель на функцию,
стоит попытаться написать программу без нее. Меню можно изменять
в динамике, если добавлять новые функции в таблицу команд.
Довольно просто создавать в динамике и новые меню.
Указатели на функции помогают реализовать полиморфические
подпрограммы, т. е. такие подпрограммы, которые можно применять
к объектам различных типов:
typedef int (*CFT)(void*,void*);
void sort(void* base, unsigned n, unsigned int sz, CFT cmp)
/*
Сортировка вектора "base" из n элементов
в возрастающем порядке;
используется функция сравнения, на которую указывает cmp.
Размер элементов равен "sz".
Алгоритм очень неэффективный: сортировка пузырьковым методом
*/
{
for (int i=0; i<n-1; i++)
for (int j=n-1; i<j; j--) {
char* pj = (char*)base+j*sz; // b[j]
char* pj1 = pj - sz; // b[j-1]
if ((*cmp)(pj, pj1) < 0) {
// поменять местами b[j] и b[j-1]
for (int k = 0; k<sz; k++) {
char temp = pj[k];
pj[k] = pj1[k];
pj1[k] = temp;
}
}
}
}
В подпрограмме sort неизвестен тип сортируемых объектов; известно
только их число (размер массива), размер каждого элемента и функция,
которая может сравнивать объекты. Мы выбрали для функции sort()
такой же заголовок, как у qsort() - стандартной функции сортировки
из библиотеки С. Эту функцию используют настоящие программы.
Покажем, как с помощью sort() можно отсортировать таблицу с такой
структурой:
struct user {
char* name; // имя
char* id; // пароль
int dept; // отдел
};
typedef user* Puser;
user heads[] = {
"Ritchie D. M.", "dmr", 11271,
"Sethi R.", "ravi", 11272,
"SZYmanski T. G.", "tgs", 11273,
"Schryer N. L.", "nls", 11274,
"Schryer N. L.", "nls", 11275
"Kernighan B. W.", "bwk", 11276
};
void print_id(Puser v, int n)
{
for (int i=0; i<n; i++)
cout << v[i].name << '\t'
<< v[i].id << '\t'
<< v[i].dept << '\n';
}
Чтобы иметь возможность сортировать, нужно вначале определить
подходящие функции сравнения. Функция сравнения должна возвращать
отрицательное число, если ее первый параметр меньше второго,
нуль, если они равны, и положительное число в противном случае:
int cmp1(const void* p, const void* q)
// сравнение строк, содержащих имена
{
return strcmp(Puser(p)->name, Puser(q)->name);
}
int cmp2(const void* p, const void* q)
// сравнение номеров разделов
{
return Puser(p)->dept - Puser(q)->dept;
}
Следующая программа сортирует и печатает результат:
int main()
{
sort(heads,6,sizeof(user), cmp1);
print_id(heads,6); // в алфавитном порядке
cout << "\n";
sort(heads,6,sizeof(user),cmp2);
print_id(heads,6); // по номерам отделов
}
Допустима операция взятия адреса и для функции-подстановки, и для
перегруженной функции ($$R.13.3).
Отметим, что неявное преобразование указателя на что-то в
указатель типа void* не выполняется для параметра функции, вызываемой
через указатель на нее. Поэтому функцию
int cmp3(const mytype*, const mytype*);
нельзя использовать в качестве параметра для sort().
Поступив иначе, мы нарушаем заданное в описании условие, что
cmp3() должна вызываться с параметрами типа mytype*. Если вы
специально хотите нарушить это условие, то должны использовать
явное преобразование типа.
4.7 Макросредства
Макросредства языка определяются в $$R.16. В С++ они играют гораздо
меньшую роль, чем в С. Можно даже дать такой совет: используйте
макроопределения только тогда, когда не можете без них обойтись.
Вообще говоря, считается, что практически каждое появление
макроимени является свидетельством некоторых недостатков
языка, программы или программиста. Макросредства создают определенные
трудности для работы служебных системных программ, поскольку
они перерабатывают программный текст еще до трансляции. Поэтому, если
ваша программа использует макросредства,
то сервис, предоставляемый такими программами, как отладчик,
профилировщик, программа перекрестных ссылок, будет для нее
неполным. Если все-таки вы решите использовать
макрокоманды, то вначале тщательно изучите описание препроцессора
С++ в вашем справочном руководстве и не старайтесь быть слишком умным.
Простое макроопределение имеет вид:
#define имя остаток-строки
В тексте программы лексема имя заменяется на остаток-строки. Например,
объект = имя
будет заменено на
объект = остаток-строки
Макроопределение может иметь параметры. Например:
#define mac(a, b) argument1: a argument2: b
В макровызове mac должны быть заданы две строки, представляющие
параметры. При подстановке они заменят a и b в макроопределении
mac(). Поэтому строка
expanded = mac(foo bar, yuk yuk)
при подстановке преобразуется в
expanded = argument1: foo bar argument2: yuk yuk
Макроимена нельзя перегружать. Рекурсивные макровызовы ставят
перед препроцессором слишком сложную задачу:
// ошибка:
#define print(a, b) cout<<(a)<<(b)
#define print(a, b,c) cout<<(a)<<(b)<<(c)
// слишком сложно:
#define fac(n) (n>1) ?n*fac(n-1) :1
Препроцессор работает со строками и практически ничего не знает о
синтаксисе C++, типах языка и областях видимости. Транслятор
имеет дело только с уже раскрытым макроопределением, поэтому
ошибка в нем может диагностироваться уже после подстановки, а не при
определении макроимени. В результате появляются довольно путанные
сообщения об ошибках.
Допустимы такие макроопределения:
#define Case break;case
#define forever for(;;)
А вот совершенно излишние макроопределения:
#define PI 3.141593
#define BEGIN {
#define END }
Следующие макроопределения могут привести к ошибкам:
#define SQUARE(a) a*a
#define INCR_xx (xx)++
#define DISP = 4
Чтобы убедиться в этом, достаточно попробовать сделать подстановку
в таком примере:
int xx = 0; // глобальный счетчик
void f() {
int xx = 0; // локальная переменная
xx = SQUARE(xx+2); // xx = xx +2*xx+2;
INCR_xx; // увеличивается локальная переменная xx
if (a-DISP==b) { // a-=4==b
// ...
}
}
При ссылке на глобальные имена в макроопределении используйте операцию
разрешения области видимости ($$2.1.1), и всюду, где это возможно,
заключайте имя параметра макроопределения в скобки. Например:
#define MIN(a, b) (((a)<(b))?(a):(b))
Если макроопределение достаточно сложное, и требуется комментарий
к нему, то разумнее написать комментарий вида /* */, поскольку
в реализации С++ может использоваться препроцессор С, который не
распознает комментарии вида //. Например:
#define m2(a) something(a) /* глубокомысленный комментарий */
С помощью макросредств можно создать свой собственный язык,
правда, скорее всего, он будет непонятен другим. Кроме того, препроцессор
С предоставляет довольно слабые макросредства. Если ваша задача
нетривиальна, вы, скорее всего, обнаружите, что решить ее с помощью этих
средств либо невозможно, либо чрезвычайно трудно. В качестве
альтернативы традиционному использованию макросредств в язык введены
конструкции const, inline и шаблоны типов. Например:
const int answer = 42;
template<class T>
inline T min(T a, T b) { return (a<b)?a:b; }
4.8 Упражнения
1. (*1) Составьте следующие описания: функция с параметрами типа
указатель на символ и ссылка на целое, невозвращающая значения;
указатель на такую функцию; функция с параметром, имеющим тип
такого указателя; функция, возвращающая такой указатель. Напишите
определение функции, у которой параметр и возвращаемое значение
имеют тип такого указателя. Подсказка: используйте typedef.
2. (*1) Как понимать следующее описание? Где оно может пригодиться?
typedef int (rifii&) (int, int);
3. (*1.5) Напишите программу, подобную той, что выдает "Hello, world".
Она получает имя (name) как параметр командной строки и выдает
"Hello, name". Измените программу так, чтобы она получала
произвольное число имен и всем им выдавала свое приветствие:
"Hello, ...".
4. (1.5) Напишите программу, которая, беря из командной строки
произвольное число имен файлов, все эти файлы переписывает
один за другим в cout. Поскольку в программе происходит
конкатенация файлов, вы можете назвать ее cat от слова
concatenation - конкатенация).
5. (*2) Переведите небольшую программу с языка С на С++. Измените
заголовочные файлы так, чтобы они содержали описание всех
вызываемых функций и описание типов всех параметров. По возможности
все команды #define замените конструкциями enum, const или
inline. Удалите из файлов. c все описания внешних, а определения
функций приведите к виду, соответствующему С++. Вызовы malloc() и
free() замените операциями new и delete. Удалите ненужные операции
приведения.
6. (*2) Напишите функцию sort() ($$4.6.9), использующую более
эффективный алгоритм сортировки.
7. (*2) Посмотрите на определение структуры tnode в $$R.9.3. Напишите
функцию, заносящую новые слова в дерево узлов tnode. Напишите
функцию для вывода узлов дерева tnode. Напишите функцию,
которая производит такой вывод в алфавитном порядке.
Измените структуру tnode так, чтобы в ней содержался
только указатель на слово произвольной длины, которое размещается
с помощью new в свободной памяти. Измените функцию так, чтобы
она работала с новой структурой tnode.
8. (*1) Напишите функцию itoa(), которая использовалась в примере
из $$4.6.8.
9. (*2) Узнайте, какие стандартные заголовочные файлы есть в вашей
системе. Поройтесь в каталогах /usr/include или /usr/include/CC
(или в тех каталогах, где хранятся стандартные заголовочные
файлы вашей системы). Прочитайте любой показавшийся интересным
файл.
10. (*2) Напишите функцию, которая будет переворачивать двумерный
массив. (Первый элемент массива станет последним).
11. (*2) Напишите шифрующую программу, которая читает символы из
cin и пишет их в cout в зашифрованном виде. Можно использовать
следующий простой метод шифрации: для символа s зашифрованное
представление получается в результате операции s^key[i], где
key - массив символов, передаваемый в командной строке. Символы
из массива key используются в циклическом порядке, пока не будет
прочитан весь входной поток. Первоначальный текст получается
повторным применением той же операции с теми же элементами key.
Если массив key не задан (или задана пустая строка), шифрация не
происходит.
12. (*3) Напишите программу, которая помогает дешифрировать текст,
зашифрованный описанным выше способом, когда ключ (т. е. массив
key) неизвестен. Подсказка: см. D Kahn "The Codebreakers",
Macmillan, 1967, New York, стр. 207-213.
13. (*3) Напишите функцию обработки ошибок, первый параметр который
подобен форматирующей строке-параметру printf() и содержит форматы
%s, %c и %d. За ним может следовать произвольное количество
числовых параметров. Функцию printf() не используйте. Если смысл
формата %s и других форматов вам неизвестен, обратитесь к $$10.6.
Используйте <stdarg. h>.
14. (*1) Какое имя вы выбрали бы для типов указателей на функции,
которые определяются с помощью typedef?
15. (*2) Исследуйте разные программы, чтобы получить представление
о разных используемых на практике стилях именования. Как
используются заглавные буквы? Как используется подчерк? В каких
случаях используются такие имена, как i или x?
16. (*1) Какие ошибки содержатся в следующих макроопределениях?
#define PI = 3.141593;
#define MAX(a, b) a>b? a:b
#define fac(a) (a)*fac((a)-1)
17. (*3) Напишите макропроцессор с простыми возможностями, как у
препроцессора С. Текст читайте из cin, а результат записывайте
в cout. Вначале реализуйте макроопределения без параметров.
Подсказка: в программе калькулятора есть таблица имен и
синтаксический анализатор, которыми можно воспользоваться.
18. (*2) Напишите программу, извлекающую квадратный корень из двух (2)
с помощью стандартной функции sqrt(), но не включайте в программу
<math. h>. Сделайте это упражнение с помощью функции sqrt()
на Фортране.
19. (*2) Реализуйте функцию print() из $$4.6.7.
* ГЛАВА 5. КЛАССЫ
"Эти типы не абстрактные, они столь же реальны,
как int и float"
- Даг Макилрой
В этой главе описываются возможности определения новых типов,
для которых доступ к данным ограничен заданным множеством
функций, осуществляющих его. Объясняется, как можно использовать
члены структуры данных, как ее защищать, инициализировать и,
наконец, уничтожать. В примерах приведены простые классы для
управления таблицей имен, работы со стеком, множеством и
реализации дискриминирующего (т. е. надежного) объединения.
Следующие три главы завершают описание возможностей С++ для
построения новых типов, и в них содержится больше интересных
примеров.
5.1 Введение и краткий обзор
Понятие класса, которому посвящена эта и три следующих главы, служит
в С++ для того, чтобы дать программисту инструмент построения новых
типов. Ими пользоваться не менее удобно, чем встроенными.
В идеале использование определенного пользователем типа не должно
отличаться от использования встроенных типов. Различия возможны только
в способе построения.
Тип есть вполне конкретное представление некоторого понятия.
Например, в С++ тип float с операциями +, -, * и т. д. является
хотя и ограниченным, но конкретным представлением математического
понятия вещественного числа. Новый тип создается для того, чтобы
стать специальным и конкретным представлением понятия, которое не находит
прямого и естественного отражения среди встроенных типов. Например,
в программе из области телефонной связи можно ввести тип
trunk_module (линия-связи), в видеоигре - тип explosion (взрыв),
а в программе, обрабатывающей текст, - тип list_of_paragraphs
(список-параграфов). Обычно проще понимать и изменять программу,
в которой типы хорошо представляют используемые в задаче понятия.
Удачно подобранное множество пользовательских типов делает программу
более ясной. Оно позволяет транслятору обнаруживать недопустимое
использование объектов, которое в противном случае останется
невыявленным до отладки программы.
Главное в определении нового типа - это отделить несущественные
детали реализации (например, расположение данных в объекте нового
типа) от тех его характеристик, которые существенны для правильного
его использования (например, полный список функций, имеющих доступ
к данным). Такое разделение обеспечивается тем, что вся работа со
структурой данных и внутрение, служебные операции над нею доступны
только через специальный интерфейс (через "одно горло").
Глава состоит из четырех частей:
$$5.2 Классы и члены. Здесь вводится основное понятие
пользовательского типа, называемого классом. Доступ к объектам
класса может ограничиваться множеством функций, описания
которых входят в описание класса. Эти функции называются
функциями-членами и друзьями. Для создания объектов класса
используются специальные функции-члены, называемые
конструкторами. Можно описать специальную функцию-член
для удаления объектов класса при его уничтожении. Такая
функция называется деструктором.
$$5.3 Интерфейсы и реализации. Здесь приводятся два примера
разработки, реализации и использования классов.
$$5.4 Дополнительные свойства классов. Здесь приводится много
дополнительных подробностей о классах. Показано, как
функции, не являющейся членом класса, предоставить доступ
к его частной части. Такую функцию называют другом класса.
Вводятся понятия статических членов класса и указателей
на члены класса. Здесь же показано, как определить
дискриминирующее объединение.
$$5.5 Конструкторы и деструкторы. Объект может создаваться как
автоматический, статический или как объект в свободной
памяти. Кроме того, объект может быть членом некоторого
агрегата (массива или другого класса), который тоже
можно размещать одним из этих трех способов. Подробно
объясняется использование конструкторов и деструкторов,
описывается применение определяемых пользователем функций
размещения в свободной памяти и функций освобождения памяти.
5.2 Классы и члены
Класс - это пользовательский тип. Этот раздел знакомит с основными
средствами определения класса, создания его объектов, работы с
такими объектами и, наконец, удаления этих объектов после
использования.
5.2.1 Функции-члены
Посмотрим, как можно представить в языке понятие даты, используя
для этого тип структуры и набор функций, работающих с переменными
этого типа:
struct date { int month, day, year; };
date today;
void set_date(date*, int, int, int);
void next_date(date*);
void print_date(const date*);
// ...
Никакой явной связи между функциями и структурой date нет. Ее можно
установить, если описать функции как члены структуры:
struct date {
int month, day, year;
void set(int, int, int);
void get(int*, int* int*);
void next();
void print();
};
Описанные таким образом функции называются функциями-членами. Их можно
вызывать только через переменные соответствующего типа, используя
стандартную запись обращения к члену структуры:
date today;
date my_birthday;
void f()
{
my_birthday. set(30,12,1950);
today. set(18,1,1991);
my_birthday. print();
today. next();
}
Поскольку разные структуры могут иметь функции-члены с одинаковыми
именами, при определении функции-члена нужно указывать имя структуры:
void date::next()
{
if (++day > 28 ) {
// здесь сложный вариант
}
}
В теле функции-члена имена членов можно использовать без указания
имени объекта. В таком случае имя относится к члену того объекта,
для которого была вызвана функция.
5.2.2 Классы
Мы определили несколько функций для работы со структурой date, но из ее
описания не следует, что это единственные функции, которые
предоставляют доступ к объектам типа date. Можно установить такое
ограничение, описав класс вместо структуры:
class date {
int month, day, year;
public:
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print()
};
Служебное слово public (общий) разбивает описание класса на две части.
Имена, описанные в первой частной (private) части класса, могут
использоваться только в функциях-членах. Вторая - общая часть -
представляет собой интерфейс с объектами класса. Поэтому структура - это
такой класс, в котором по определению все члены являются общими.
Функции-члены класса определяются и используются точно так же, как
было показано в предыдущем разделе:
void date::print() // печать даты в принятом в США виде
{
cout << month << '/' << day << '/' << year ;
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |


