Купить
 
 
Жанр: Учеба

Язык С++ с комментариями

страница №11

Вот пример, в котором преобразование необходимо:

overload print(double), print(long);

void f(int a);
{
print(a);
}

Здесь a может быть напечатано или как double, или как long.
Неоднозначность разрешается явным преобразованием типа (или
print(long(a)) или print(double(a))).
При этих правилах можно гарантировать, что когда эффективность
или точность вычислений для используемых типов существенно
различаются, будет использоваться простейший алгоритм (функция).
Например:

overload pow;
int pow(int, int);
double pow(double, double); // из
complex pow(double, complex); // из
complex pow(complex, int);
complex pow(complex, double);
complex pow(complex, complex);

Процесс поиска подходящей функции игнорирует unsigned и const.

4.6.8 Незаданное Число Параметров

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

int printf(char* ...);

- стр 130 -

Это задает, что в вызове printf должен быть по меньшей мере один
параметр, char*, а остальные могут быть, а могут и не быть.
Например:

printf("Hello, world\n");
printf("Мое имя %s %s\n", first_name, second_name);
printf("%d + %d = %d\n",2,3,5);

Такая функция полагается на информацию, которая недоступна
компилятору при интерпретации ее списка параметров. В случае
printf() первым параметром является строка формата, содержащая
специальные последовательности символов, позволяющие printf()
правильно обрабатывать остальные параметры. %s означает "жди
параметра char*", а %d означает "жди параметра int". Однако,
компилятор этого не знает, поэтому он не может убедиться в том, что
ожидаемые параметры имеют соответствующий тип. Например:

printf("Мое имя %s %s\n",2);

откомпилируется и в лучшем случае приведет к какой-нибудь странного
вида выдаче.
Очевидно, если параметр не был описан, то у компилятора нет
информации, необходимой для выполнения над ним проверки типа и
преобразования типа. В этом случае char или short передаются как
int, а float передается как double. Это не обязательно то, чего
ждет пользователь.
Чрезмерное использование многоточий, вроде wild(...), полностью
выключает проверку типов параметров, оставляя программиста открытым
перед множеством неприятностей, которые хорошо знакомы
программистам на C. В хорошо продуманной программе требуется самое
большее несколько функций, для которых типы параметров не
определены полностью. Для того, чтобы позаботиться о проверке
типов, можно использовать перегруженные функции и функции с
параметрами по умолчанию в большинстве тех случаев, когда иначе
пришлось бы оставить типы параметров незаданными. Многоточие
необходимо только если изменяются и число параметров, и тип
параметров. Наиболее обычное применеие многоточия в задании
интерфейса с функциями C библиотек, которые были определены в то
время, когда альтернативы не было:

extern int fprintf(FILE*, char* ...); // из
extern int execl(char* ...); // из
extern int abort(...); // из

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

- стр 131 -

void error(int ...);

main(int argc, char* argv[])
{
switch(argc) {
case 1:
error(0,argv[0],0);
break;
case 2:
error(0,argv[0],argv[1],0);
default:
error(1,argv[0],"с",dec(argc-1),"параметрами",0);
}
}

Функцию ошибок можно определить так:

#include

void error(int n ...)
/*
"n" с последующим списком char*, оканчивающихся нулем
*/
{
va_list ap;
va_start(ap,n); // раскрутка arg

for (;;) {
char* p = va_arg(ap,char*);
if(p == 0) break;
cerr "" p "" " ";
}

va_end(ap); // очистка arg

cerr "" "\n";
if (n) exit(n);
}

Первый из va_list определяется и инициализируется вызовом
va_start(). Макрос va_start получает имя va_list'а и имя последнего
формального параметра как параметры. Макрос va_arg используется для
выбора неименованных параметров по порядку. При каждом обращении
программист должен задать тип; va_arg() предполагает, что был
передан фактический параметр, но обычно способа убедиться в этом
нет. Перед возвратом из функции, в которой был использован
va_start(), должен быть вызван va_end(). Причина в том, что
va_start() может изменить стек так, что нельзя будет успешно
осуществить возврат; va_end() аннулирует все эти изменения.

4.6.9 Указатель на Функцию

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

- стр 132 -

void error(char* p) { /* ... */ }

void (*efct)(char*); // указатель на функцию

void f()
{
efct = &error; // efct указывает на error
(*efct)("error"); // вызов error через efct
}

Чтобы вызвать функцию через указатель, например, efct, надо сначала
этот указатель разыменовать, *efct. Поскольку операция вызова
функции () имеет более высокий приоритет, чем операция
разыменования *, то нельзя писать просто *efct("error"). Это
означает *efct("error"), а это ошибка в типе. То же относится и к
синтаксису описаний (см. также #7.3.4).
Заметьте, что у указателей на функции типы параметров описываются
точно также, как и в самих функциях. В присваиваниях указателя
должно соблюдаться точное соответствие полного типа функции.
Например:

void (*pf)(char*); // указатель на void(char*)
void f1(char*); // void(char*)
int f2(char*); // int(char*)
void f3(int*); // void(int*)

void f()
{
pf = &f1; // ok
pf = &f2; // ошибка: не подходит возвращаемый тип
pf = &f3; // ошибка: не подходит тип параметра

(*pf)("asdf"); // ok
(*pf)(1); // ошибка: не подходит тип параметра

int i = (*pf)("qwer"); // ошибка: void присваивается int'у
}

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

typedef int (*SIG_TYP)(); // из
typedef void (*SIG_ARG_TYP);
SIG_TYP signal(int,SIG_ARG_TYP);

Бывает часто полезен вектор указателей на фукнцию. Например,
система меню для моего редактора с мышью* реализована с помощью
векторов указателей на функции для представления действий. Подробно
эту систему здесь описать не получится, но вот общая идея:
____________________
* Мышь - это указывающее устройство по крайней мере с одной
кнопкой. Моя мышь красная, круглая и с тремя кнопками. (прим.
автора)

- стр 133 -

typedef void (*PF)();

PF edit_ops[] = { // операции редактирования
cut, paste, snarf, search
};

PF file_ops[] = { // управление файлом
open, reshape, close, write
};

Затем определяем и инициализируем указатели, определяющие действия,
выбранные в меню, которое связано с кнопками (button) мыши:

PF* button2 = edit_ops;
PF* button3 = file_ops;

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

(button2[3])();

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

- стр 134 -

typedef int (*CFT)(char*,char*);

int sort(char* base, unsigned n, int sz, CFT cmp)
/*
Сортирует "n" элементов вектора "base"
в возрастающем порядке
с помощью функции сравнения, указываемой "cmp".
Размер элементов "sz".

Очень неэффективный алгоритм: пузырьковая сортировка
*/
{
for (int i=0; iname, Puser(q)-"name);
}

int cmp2(char*p, char* q) // Сравнивает числа dept
{
return Puser(p)-"dept-Puser(q)-"dept;
}

Эта программа сортирует и печатает:

main ()
{
sort((char*)heads,6,sizeof(user),cmp1);
print_id(heads,6); // в алфавитном порядке
cout "" "\n";
sort((char*)heads,6,sizeof(user),cmp2);
print_id(heads,6); // по порядку подразделений
}

Можно взять адрес inline-функции, как, впрочем, и адрес
перегруженной функции(#с.8.9).

- стр 136 -

4.7 Макросы

Макросы* определяются в #с.11. В C они очень важны, но в C++
применяются гораздо меньше. Первое правило относительно них такое:
не используйте их, если вы не обязаны это делать. Как было
замечено, почти каждый макрос проявляет свой изъян или в языке, или
в программе. Если вы хотите использовать макросы, прочитайте,
пожалуйста, вначале очень внимательно руководство по вашей
реализации C препроцессора.
Простой макрос определяется так:

#define name rest of line

Когда name встречается как лексема, оно заменяется на rest of line.
Например:

named = name

после расширения даст:

named = rest of line

Можно также определить макрос с параметрами. Например:

#define mac(a,b) argument1: a argument2: b

При использовании mac должно даваться две строки параметра. После
расширения mac() они заменяют a и b. Например:

expanded = mac(foo bar, yuk yuk)

после расширения даст

expanded = argument1: foo bar argument2: yuk yuk

Макросы обрабатывают строки и о синтаксисе C++ знают очень мало,
а о типах C++ или областях видимости - ничего. Компилятор видит
только расширенную форму макроса, поэтому ошибка в макросе
диагностируется когда макрос расширен, а не когда он определен. В
результате этого возникают непонятные сообщения об ошибках.
Вот такими макросы могут быть вполне:

#define Case break;case
#define nl """\n"
#define forever for(;;)
#define MIN(a,b) (((a)"(b))?(a):(b))

Вот совершенно ненужные макросы:

#define PI 3.141593
#define BEGIN {
#define END }

А вот примеры опасных макросов:
____________________
* часто называемые также макроопределениями. (прим. перев.)

- стр 137 -

#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)
и заключайте вхождения имени параметра макроса в скобки везде, где
это возможно (см. MIN выше).
Обратите внимание на различие результатов расширения этих двух
макросов:

#define m1(a) something(a) // глубокомысленный комментарий
#define m2(a) something(a) /* глубокомысленный комментарий */

например,

int a = m1(1)+2;
int b = m2(1)+2;

расширяется в

int a = something(1) // глубокомысленный комментарий+2;
int b = something(1) /* глубокомысленный комментарий */+2;

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

#7.3.5).

4.8 Упражнения

1. (*1) Напишите следующие описания: функция, получающая параметр
типа указатель на символ и ссылку на целое и не возвращающая
значения; указатель на такую фунцию; функция, получающая такой
указатель в качестве параметра; и функция, возвращающая такой
указатель. Напишите определение функции, которая получает
такой указатель как параметр и возвращает свой параметр как
возвращаемое значение. Подсказка: используйте typedef.
2. (*1) Что это значит? Для чего это может использоваться?

- стр 138 -

typedef int (rifii&) (int, int);

3. (*1.5) Напишите программу вроде "Hello, world", которая
получает имя как параметр командной строки и печатает "Hello,
имя". Модифицируйте эту программу так, чтобы она получала
получала любое количество имен и говорила hello каждому из
них.
4. (*1.5) Напишите программу, которая читает произвольное число
файлов, имена которых задаются как аргументы командной стоки,
и пишет их один за другим в cout. Поскольку эта программа при
выдаче конкатенирует свои параметры, вы можете назвать ее cat
(кошка).
5. (*2) Преобразуйте небольшую C программу в C++. Измените
заголовочные файлы так, чтобы описывать все вызываемые функции
и описывать тип каждого параметра. Замените, где возможно,
директивы #define на enum и const или inline. Уберите из .c
файлов описания extern и преобразуйте определения функций к
синтаксису C++. Замените вызовы malloc() и free() на new и
delete. Уберите необязательные приведения типа.
6. (*2) Реализуйте sort() (#4.6.7) используя эффективный алгоритм
сортировки.
7. (*2) Посмотрите на определение struct tnode в с.#8.5. Напишите
функцию для введения новых слов в дерево узлов tnode.
Напишите функцию для вывода дерева узлов tnode. Напишите
функцию для вывода дерева узлов tnode со словами в алфавитном
порядке. Модифицируйте tnode так, чтобы в нем хранился
(только) указатель на слово произвольной длины, помещенное с
помощью new в свободную память. Модифицируйте функции для
использования нового определения tnode.
8. (*2) Напишите "модуль", реализующий стек. Файл .h должен
описывать функции push(), pop() и любые другие удобные функции
(только). Файл .c определяет функции и данные, необходимые для
хранения стека.
9. (*2) Узнайте, какие у вас есть стандартные заголовочные
файлы. Составьте список файлов, находящихся в /usr/include и
/usr/include/CC (или там, где хранятся стандртные заголовочные
файлы в вашей системе). Прочитайте все, что покажется
интересным.
10. (*2) Напишите функцию для обращения двумерного мосссива.
11. (*2) Напишите шифрующую программу, которая читает из cin и
пишет в cout закодированные символы. Вы можете воспользоваться
следующей простой схемой шифровки: Зашифрованная форма символа
c - это c^key[i], где key (ключ) - строка, которая передается
как параметр командной строки. Программа использует символы из
key циклически, пока не будет считан весь ввод.
Перекодирование зашифрованного текста с той же строкой key
дает исходный текст. Если не передается никакого ключа (или
передается пустая строка), то никакого кодирования не
делается.
12. (*3) Напишите программу, которая поможет расшифровывать
тексты, зашифрованные описанным выше способом, не зная ключа.
Подсказка: David Kahn: The Code-Breakers, Macmillan, 1967, New
York, pp 207-213.
13. (*3) Напишите функцию error, которая получает форматную
строку в стиле printf, которая содержит директивы %s, %c и %d,
и произвольное количество параметров. Не используйте printf().

- стр 139 -

Если вы не знаете значения %s и т.д., посмотрите #8.2.4.

Используйте .
14. (*1) Как вы будете выбирать имя для указателя на тип функции,
определенный с помощью typedef?
15. (*2) Посмотрите какие-нибудь программы, чтобы создать
представление о разнообразии стилей и имен, использующихся на
практике. Как используются буквы в верхнем регистре? Как
используется подчерк? Где используются короткие имена вроде x
и y?
16. (*1) Что неправильно в следующих макроопределениях?

#define PI = 3.141593
#define MAX(a,b) a"b?a:b
#define fac(a) (a)*fac((a)-1)

17. (*3) Напишите макропроцесор, который определяет и расширяет
простые макросы (как C препроцессор). Читайте из cin и пишите
в cout. Сначала не пытайтесть обрабатывать макросы с
параметрами. Подсказка: В настольном калькуляторе (#3.1) есть
таблица имен и лексический анализатор, которые вы можете
модифицировать.

Глава 5


Классы

Эти типы не "абстрактны",
они столь же реальны, как int и float.
- Дуг МакИлрой

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

5.1 Знакомство и краткий обзор

Предназначение понятия класса, которому посвящены эта и две
последующие главы, состоит в том, чтобы предоставить программисту
инструмент для создания новых типов, столь же удобных в обращении
сколь и встроенные типы. В идеале тип, определяемый пользователем,
способом использования не должен отличаться от встроенных типов,
только способом создания.
Тип есть конкретное представление некоторой концепции (понятия).
Например, имеющийся в C++ тип float с его операциями +, -, * и т.д.
обеспечивает ограниченную, но конкретную версию математического
понятия действительного числа. Новый тип создается для того, чтобы
дать специальное и конкретное определение понятия, которому ничто
прямо и очевидно среди встроенных типов не отвечает. Например, в
программе, которая работает с телефоном, можно было бы создать тип
trunk_module (элемент линии), а в программе обработки текстов - тип
list_of_paragraphs (список параграфов). Как правило, программу, в
которой создаются типы, хорошо отвечающие понятиям приложения,
понять легче, чем программу, в которой это не делается. Хорощо
выбранные типы, определяемые пользователем, делают программу более
четкой и короткой. Это также позволяет компилятору обнаруживать
недопустимые использования объектов, которые в противном случае
останутся необнаруженными до тестирования программы.
В определении нового типа основная идея - отделить несущественные
подробности реализации (например, формат данных, которые
используются для хранения объекта типа) от тех качеств, которые
существенны для его правильного использования (например, полный
список функций, которые имеют доступ к данным). Такое разделение
можно описать так, что работа со структурой данных и внутренними
административными подпрограммами осуществляется через специальный
интерфейс (каналируется).
Эта глава состоит из четырех практически отдельных частей:
#5.2 Классы и Члены. Этот раздел знакомит с основным понятием
типа, определяемого пользователем, который называется класс
(class). Доступ к объектам класса может ограничиваться набором
функций, которые описаны как часть этого класса. Такие функции
- стр 141 -

называются функциями членами. Объекты класса создаются и
инициализируются функциями членами, специально для этой цели
описанными. Эти функции называются конструкторами. Функция
член может быть специальным образом описана для "очистки"
каждого классового объекта при его уничтожении. Такая фнукция
называется деструктором.
#5.3 Интерфейсы и Реализации. В этом разделе приводится два
примера того, как класс проектируется, реализуется и
используется.
#5.4 Друзья и Объединения. В этом разделе приводится много
дополнительных подробностей, касающихся классов. В нем
показано, как предоставить доступ к закрытой части класса
функции, которая не является членом этого класса. Такая
функция называется друг (friend). В этом разделе показано
также, как определить дискриминирующее объединение.
#5.5 Конструкторы и Деструкторы. Объект может создаваться как
автоматический, статический или как объект в свободной памяти.
Объект может также быть членом некоторой совокупности (типа
вектора или класса), которая в свою очередь может размещаться
одним из этих трех способов. Довольно подробно объсняется
использование конструкторов и деструкторов.

5.2 Классы и Члены

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

5.2.1 Функции Члены

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

struct date { int month, day, year; };
// дата: месяц, день, год }
date today;
void set_date(date*, int, int, int);
void next_date(date*);
void print_date(date*);
// ...

Никакой явной связи между функциями и типом данных нет. Такую
связь можно установить, описав функции как члены:

- стр 142 -

struct date {
int month, day, year;

void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};

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

date today; // сегодня
date my_burthday; // мой день рождения

void f()
{
my_burthday.set(30,12,1950);
today.set(18,1,1985);

my_burthday.print();
today.next();
}

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

void date::next()
{
if ( ++day " 28 ) {
// делает сложную часть работы
}
}

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

5.2.2 Классы

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

- стр 143 -

class date {
int month, day, year;
public:
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};

Метка public делит тело класса на две части. Имена в первой,
закрытой части, могут использоваться только функциями членами.
Вторая, открытая часть, составляет интерфейс к объекту класса.
Struct - это просто class, у которого все члены общие, поэтому
функции члены определяются и используются точно так же, как в
предыдущем случае. Например:

void date::ptinr() // печатает в записи, принятой в США
{
cout "" month "" "/" "" day "" "/" year;
}

Однако функции не члены отгорожены от использования закрытых членов
класса date. Например:

void backdate()
{
today.day--; // ошибка
}

В том, что доступ к структуре данных ограничен явно описанным
списком функций, есть несколько преимуществ. Любая ошибка, которая
приводит к тому, что дата принимает недопустимое значение
(например, Декабрь 36, 1985) должна быть вызвана кодом функции
члена, поэтому первая стадия отладки, локализация, выполняется еще
до того, как программа будет запущена. Это частный случай общего
утверждения, что любое изменение в поведении типа date может и
должно вызываться изменениями в его членах. Другое преимущество -
это то, что потенциальному пользователю такого типа нужно будет
только узнать определение функций членов, чтобы научиться им
пользоваться.
Защита закрытых данных связана с ограничением использования имен
членов класса. Это можно обойти с помощью манипуляции адресами, но
это уже, конечно, жульничество.

5.2.3 Ссылки на Себя

В функции члене на члены объекта, для которого она была вызвана,

Список страниц

Закладка в соц.сетях

Купить

☏ Заказ рекламы: +380504468872

© Ассоциация электронных библиотек Украины

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