* законченный учебник и руководство по языку

Вид материалаЗакон

Содержание


Name, number, end
Name, number, end
Подобный материал:
1   ...   4   5   6   7   8   9   10   11   ...   34



enum token_value {

^ NAME, NUMBER, END,

PLUS='+', MINUS='-', MUL='*', DIV='/',

PRINT=';', ASSIGN='=', LP='(', RP=')'

};


extern int no_of_errors;

extern double error(const char* s);

extern token_value get_token();

extern token_value curr_tok;

extern double number_value;

extern char name_string[256];

extern double expr();

extern double term();

extern double prim();


struct name {

char* string;

name* next;

double value;

};


extern name* look(const char* p, int ins = 0);

inline name* insert(const char* s) { return look(s,1); }


Если не приводить сами операторы, lex.c должен иметь такой вид:


// lex.c: ввод и лексический анализ


#include "dc.h"

#include


token_value curr_tok;

double number_value;

char name_string[256];


token_value get_token() { /* ... */ }


Используя составленный заголовочный файл, мы добьемся,

что описание каждого объекта, введенного пользователем, обязательно

окажется в том файле, где этот объект определяется. Действительно,

при обработке файла lex.c транслятор столкнется с описаниями


extern token_value get_token();

// ...

token_value get_token() { /* ... */ }


Это позволит транслятору обнаружить любое расхождение в типах,

указанных при описании данного имени. Например, если бы функция

get_token() была описана с типом token_value, но определена с

типом int, трансляция файла lex.c выявила бы ошибку: несоответствие

типа.

Файл syn.c может иметь такой вид:


// syn.c: синтаксический анализ и вычисления


#include "dc.h"


double prim() { /* ... */ }

double term() { /* ... */ }

double expr() { /* ... */ }


Файл table.c может иметь такой вид:


// table.c: таблица имен и функция поиска


#include "dc.h"


extern char* strcmp(const char*, const char*);

extern char* strcpy(char*, const char*);

extern int strlen(const char*);


const int TBLSZ = 23;

name* table[TBLSZ];


name* look(char* p, int ins) { /* ... */ }


Отметим, что раз строковые функции описаны в самом файле table.c,

транслятор не может проверить согласованность этих описаний по типам.

Всегда лучше включить соответствующий заголовочный файл,

чем описывать в файле .c некоторое имя как extern. Это может

привести к включению "слишком многого", но такое включение нестрашно,

поскольку не влияет на скорость выполнения программы и ее размер, а

программисту позволяет сэкономить время. Допустим, функция strlen() снова

описывается в приведенном ниже файле main.c. Это только лишний

ввод символов и потенциальный источник ошибок, т.к. транслятор

не сможет обнаружить расхождения в двух описаниях strlen() (впрочем,

это может сделать редактор связей). Такой проблемы не возникло бы,

если бы в файле dc.h содержались все описания extern, как первоначально

и предполагалось. Подобная небрежность присутствует в нашем примере,

поскольку она типична для программ на С. Она очень естественна

для программиста, но часто приводит к ошибкам и таким программам,

которые трудно сопровождать. Итак, предупреждение сделано!

Наконец, приведем файл main.c:


// main.c: инициализация, основной цикл, обработка ошибок


#include "dc.h"


double error(char* s) { /* ... */ }


extern int strlen(const char*);


int main(int argc, char* argv[]) { /* ... */ }


В одном важном случае заголовочные файлы вызывают большое неудобство.

С помощью серии заголовочных файлов и стандартной

библиотеки расширяют возможности языка, вводя множество типов (как

общих, так и рассчитанных на конкретные приложения; см. главы 5-9).

В таком случае текст каждой единицы трансляции может начинаться

тысячами строк заголовочных файлов. Содержимое заголовочных

файлов библиотеки, как правило, стабильно и меняется редко. Здесь

очень пригодился бы претранслятор, который обрабатывает его. По сути,

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

методов построения такого претранслятора пока нет.


4.3.2 Множественные заголовочные файлы


Разбиение программы в расчете на один заголовочный файл больше

подходит для небольших программ, отдельные части которых не

имеют самостоятельного назначения. Для таких программ допустимо,

что по заголовочному файлу нельзя определить, чьи описания там

находятся и по какой причине. Здесь могут помочь только комментарии.

Возможно альтернативное решение: пусть каждая часть программы

имеет свой заголовочный файл, в котором определяются средства,

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

свой файл .h, определяющий, что может предоставить первый. Каждый файл

.c будет включать как свой файл .h, так и некоторые другие файлы .h,

исходя из своих потребностей.

Попробуем использовать такую организацию программы для

калькулятора. Заметим, что функция error() нужна практически во всех

функциях программы, а сама использует только . Такая

ситуация типична для функций, обрабатывающих ошибки.

Следует отделить ее от файла main.c:


// error.h: обработка ошибок


extern int no_of_errors;


extern double error(const char* s);


// error.c


#include

#include "error.h"


int no_of_errors;


double error(const char* s) { /* ... */ }


При таком подходе к разбиению программы каждую пару файлов .c

и .h можно рассматривать как модуль, в котором файл .h задает

его интерфейс, а файл .c определяет его реализацию.

Таблица имен не зависит ни от каких частей калькулятора, кроме

части обработки ошибок. Теперь этот факт можно выразить

явно:


// table.h: описание таблицы имен


struct name {

char* string;

name* next;

double value;

};


extern name* look(const char* p, int ins = 0);

inline name* insert(const char* s) { return look(s,1); }


// table.h: определение таблицы имен


#include "error.h"

#include

#include "table.h"


const int TBLSZ = 23;

name* table[TBLSZ];


name* look(const char* p, int ins) { /* ... */ }


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

файла . Тем самым удален еще один источник ошибок.


// lex.h: описания для ввода и лексического анализа


enum token_value {

^ NAME, NUMBER, END,

PLUS='+', MINUS='-', MUL='*',

PRINT=';', ASSIGN='=', LP='(', RP= ')'

};


extern token_value curr_tok;

extern double number_value;

extern char name_string[256];


extern token_value get_token();


Интерфейс с лексическим анализатором достаточно запутанный. Поскольку

недостаточно соответствующих типов для лексем, пользователю

функции get_token() предоставляются те же буферы number_value

и name_string, с которыми работает сам лексический анализатор.


// lex.c: определения для ввода и лексического анализа


#include

#include

#include "error.h"

#include "lex.h"


token_value curr_tok;

double number_value;

char name_string[256];


token_value get_token() { /* ... */ }


Интерфейс с синтаксическим анализатором определен четко:


// syn.h: описания для синтаксического анализа и вычислений


extern double expr();

extern double term();

extern double prim();


// syn.c: определения для синтаксического анализа и вычислений


#include "error.h"

#include "lex.h"

#include "syn.h"


double prim() { /* ... */ }

double term() { /* ... */ }

double expr() { /* ... */ }


Как обычно, определение основной программы тривиально:


// main.c: основная программа


#include

#include "error.h"

#include "lex.h"

#include "syn.h"

#include "table.h"


int main(int argc, char* argv[]) { /* ... */ }


Какое число заголовочных файлов следует использовать для данной

программы зависит от многих факторов. Большинство их определяется

способом обработки файлов именно в вашей системе, а не

собственно в С++. Например, если ваш редактор не может работать

одновременно с несколькими файлами, диалоговая обработка нескольких

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

что открытие и чтение 10 файлов по 50 строк каждый занимает

существенно больше времени, чем открытие и чтение одного файла из 500

строк. В результате придется хорошенько подумать, прежде чем

разбивать небольшую программу, используя множественные заголовочные

файлы. Предостережение: обычно можно управиться с множеством, состоящим

примерно из 10 заголовочных файлов (плюс стандартные заголовочные

файлы). Если же вы будете разбивать программу на минимальные логические

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

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

множество из сотен заголовочных файлов.


4.4 Связывание с программами на других языках


Программы на С++ часто содержат части, написанные на других языках, и

наоборот, часто фрагмент на С++ используется в программах,

написанных на других языках. Собрать в одну программу

фрагменты, написанные на разных языках, или, написанные на одном

языке, но в системах программирования с разными соглашениями о

связывании, достаточно трудно. Например, разные языки или разные

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

при передаче параметров, порядком размещения параметров в стеке,

упаковкой таких встроенных типов, как целые или строки, форматом

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

контроля типов, который требуется от редактора связей. Чтобы

упростить задачу, можно в описании внешних указать условие

связывания. Например, следующее описание объявляет strcpy внешней

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

связывания в С:


extern "C" char* strcpy(char*, const char*);


Результат этого описания отличается от результата обычного описания


extern char* strcpy(char*, const char*);


только порядком связывания для вызывающих strcpy() функций. Сама

семантика вызова и, в частности, контроль фактических параметров

будут одинаковы в обоих случаях. Описание extern "C" имеет смысл

использовать еще и потому, что языки С и С++, как и их

реализации, близки друг другу. Отметим, что в описании extern "C"

упоминание С относится к порядку связывания, а не к языку, и часто

такое описание используют для связи с Фортраном или ассемблером.

Эти языки в определенной степени подчиняются порядку связывания

для С.

Утомительно добавлять "C" ко многим описаниям внешних, и

есть возможность указать такую спецификацию сразу для группы

описаний. Например:


extern "C" {

char* strcpy(char*, const char);

int strcmp(const char*, const char*)

int strlen(const char*)

// ...

}


В такую конструкцию можно включить весь заголовочный файл С, чтобы

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


extern "C" {

#include

}


Обычно с помощью такого приема из стандартного заголовочного файла

для С получают такой файл для С++. Возможно иное решение с

помощью условной трансляции:


#ifdef __cplusplus

extern "C" {

#endif


char* strcpy(char*, const char*);

int strcmp(const char*, const char*);

int strlen(const char*);

// ...


#ifdef __cplusplus

}

#endif


Предопределенное макроопределение __cplusplus нужно, чтобы обойти

конструкцию extern "C" { ...}, если заголовочный файл используется

для С.

Поскольку конструкция extern "C" { ... } влияет только на

порядок связывания, в ней может содержаться любое описание,

например:


extern "C" {

// произвольные описания


// например:


static int st;

int glob;

}


Никак не меняется класс памяти и область видимости

описываемых объектов, поэтому по-прежнему st подчиняется внутреннему

связыванию, а glob остается глобальной переменной.

Укажем еще раз, что описание extern "C" влияет только на

порядок связывания и не влияет на порядок вызова функции. В частности,

функция, описанная как extern "C", все равно подчиняется правилам

контроля типов и преобразования фактических параметров, которые в C++

строже, чем в С. Например:


extern "C" int f();


int g()

{

return f(1); // ошибка: параметров быть не должно

}


4.5 Как создать библиотеку


Распространены такие обороты (и в этой книге тоже): "поместить

в библиотеку", "поискать в такой-то библиотеке". Что они

означают для программ на С++? К сожалению, ответ зависит от

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

создать и использовать библиотеку для десятой версии системы ЮНИКС.

Другие системы должны предоставлять похожие возможности. Библиотека

состоит из файлов .o, которые получаются в результате трансляции

файлов .c. Обычно существует один или несколько файлов .h, в которых

содержатся необходимые для вызова файлов .o описания.

Рассмотрим в качестве примера, как для четко не оговоренного множества

пользователей можно достаточно удобно определить некоторое

множество стандартных математических функций. Заголовочный файл

может иметь такой вид:


extern "C" { // стандартные математические функции

// как правило написаны на С


double sqrt(double); // подмножество

double sin(double);

double cos(double);

double exp(double);

double log(double);

// ...


}


Определения этих функций будут находиться в файлах sqrt.c, sin.c,

cos.c, exp.c и log.c, соответственно.

Библиотеку с именем math.a можно создать с помощью таких

команд:


$ CC -c sqrt.c sin.c cos.c exp.c log.c

$ ar cr math.a sqrt.o sin.o cos.o exp.o log.o

$ ranlib math.a


Здесь символ $ является приглашением системы.

Вначале транслируются исходные тексты, и получаются модули

с теми же именами. Команда ar (архиватор) создает архив под именем

math.a. Наконец, для быстрого доступа к функциям архив индексируется.

Если в вашей системе нет команды ranlib (возможно она и не нужна),

то, по крайней мере, можно найти в справочном руководстве

ссылку на имя ar. Чтобы использовать библиотеку в своей

программе, надо задать режим трансляции следующим образом:


$ CC myprog.c math.a


Встает вопрос: что дает нам библиотека math.a? Ведь можно было бы

непосредственно использовать файлы .o, например так:


$ CC myprog.c sqrt.o sin.o cos.o exp.o log.o


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

файлы .o действительно нужны. В приведенной выше команде

использовались все из них. Если же в myprog вызываются только

sqrt() и cos(), тогда, видимо, достаточно задать такую команду:


$ CC myprog.c sqrt.o cos.o


Но это будет неверно, т.к. функция cos() вызывает sin().

Редактор связей, который вызывается командой CC для обработки

файлов .a (в нашем случае для файла math.a), умеет из множества

файлов, образующих библиотеку, извлекать только нужные файлы

.o. Иными словами, связывание с библиотекой позволяет включать

в программы много определений одного имени (в том числе определения

функций и переменных, используемых только внутренними функциями,

о которых пользователь никогда не узнает). В то же время в

результирующую программу войдет только минимально необходимое

число определений.


4.6 Функции


Самый распространенный способ задания в С++ каких-то действий - это

вызов функции, которая выполняет такие действия. Определение функции

есть описание того, как их выполнить. Неописанные функции

вызывать нельзя.


4.6.1 Описания функций


Описание функции содержит ее имя, тип возвращаемого значения

(если оно есть) и число и типы параметров, которые должны

задаваться при вызове функции. Например:


extern double sqrt(double);

extern elem* next_elem();

extern char* strcpy(char* to, const char* from);

extern void exit(int);


Семантика передачи параметров тождественна семантике

инициализации: проверяются типы фактических параметров и, если

нужно, происходят неявные преобразования типов. Так, если

учесть приведенные описания, то в следующем определении:


double sr2 = sqrt(2);


содержится правильный вызов функции sqrt() со значением с плавающей

точкой 2.0. Контроль и преобразование типа фактического параметра

имеет в С++ огромное значение.

В описании функции можно указывать имена параметров. Это

облегчает чтение программы, но транслятор эти имена просто

игнорирует.


4.6.2 Определения функций


Каждая вызываемая в программе функция должна быть где-то в ней

определена, причем только один раз. Определение функции - это ее

описание, в котором содержится тело функции. Например:


extern void swap(int*, int*); // описание


void swap(int* p, int* q) // определение

{

int t = *p;

*p = *q;

*q = *t;

}


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

некоторые параметры:


void search(table* t, const char* key, const char*)

{

// третий параметр не используется


// ...

}


Как видно из этого примера, параметр не используется, если

не задано его имя. Подобные функции появляются при упрощении

программы или если рассчитывают на ее дальнейшее расширение. В

обоих случаях резервирование места в определении функции для

неиспользуемого параметра гарантирует, что другие функции,

содержащие вызов данной, не придется менять.

Уже говорилось, что функцию можно определить как подстановку

(inline). Например:


inline fac(int i) { return i<2 ? 1 : n*fac(n-1); }


Спецификация inline служит подсказкой транслятору, что вызов

функции fac можно реализовать подстановкой ее тела, а не с помощью

обычного механизма вызова функций ($$R.7.1.2). Хороший оптимизирующий

транслятор вместо генерации вызова fac(6) может просто использовать

константу 720. Из-за наличия взаиморекурсивных вызовов функций-подстановок,

а также функций-подстановок, рекурсивность которых зависит от входных

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

действительно реализуется подстановкой ее тела. Степень оптимизации,

проводимой транслятором, нельзя формализовать, поэтому одни

трансляторы создадут команды 6*5*4*3*2*1, другие - 6*fac(5), а