#include «ctype.h»
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 TBLSZ = 23; name* table[TBLSZ];
name* look(char* p; int ins) (* /* ... */ *)
Заметьте, что table.c сам описывает стандартные функции для работы со строками, поэтому никакой проверки согласованости этих описаний нет. Почти всегда лучше включать заголвочный файл, чем описывать имя в .c файле как extern. При этом может включаться «слишком много», но это обычно не окзывает серьезного влияния на время, необходимое для компилции, и как правило экономит время программиста. В качестве примера этого, обратите внимание на то, как strlen() заново описывается в main() (ниже). Это лишние нажатия клавиш и воможный источник неприятностей, поскольку компилятор не может проверить согласованность этих двух определений. На самом дле, этой сложности можно было бы избежать, будь все описания extern помещены в dc.h, как и предлагалось сделать. Эта «нережность» сохранена в программе, поскольку это очень типично для C программ, очень соблазнительно для программиста, и чаще приводит, чем не приводит, к ошибкам, которые трудно обнаржить, и к программам, с которыми тяжело работать. Вас предуредили!
И main.c, наконец, выглядит так:
// main.c: инициализация, главный цикл и обработка ошибок
#include «dc.h»
int no_of_errors;
double error(char* s) (* /* ... */ *)
extern int strlen(const char*);
main(int argc, char* argv[]) (* /* ... */ *)
Важный случай, когда размер заголовочных файлов станвится серьезной помехой. Набор заголовочных файлов и библиотеку можно использовать для расширения языка множеством общи специальноприкладных типов (см. Главы 5-8). В таких случаях не принято осуществлять чтение тысяч строк заголовоных файлов в начале каждой компиляции. Содержание этих файлов обычно «заморожено» и изменяется очень нечасто. Наиболее плезным может оказаться метод затравки компилятора содержанием этих заголовочных фалов. По сути, создается язык специального назначения со своим собственным компилятором. Никакого стадартного метода создания такого компилятора с затравкой не принято.
4.3.2 Множественные Заголовочные Файлы
Стиль разбиения программы с одним заголовочным файлом наиболее пригоден в тех случаях, когда программа невелика и ее части не предполагается использовать отдельно. Поэтому то, что невозможно установить, какие описания зачем помещены в заголовочный файл, несущественно. Помочь могут комментарии. Другой способ – сделать так, чтобы каждая часть программы имела свой заголовочный файл, в котором определяются предотавляемые этой частью средства. Тогда каждый .c файл имеет соответствующий .h файл, и каждый .c файл включает свой собтвенный (специфицирующий то, что в нем задается) .h файл и, возможно, некоторые другие .h файлы (специфицирующие то, что ему нужно).
Рассматривая организацию калькулятора, мы замечаем, что error() используется почти каждой функцией программы, а сама использует только «stream.h». Это обычная для функции ошибок ситуация, поэтому error() следует отделить от main():
// error.h: обработка ошибок
extern int no_errors;
extern double error(char* s);
// error.c
#include «stream.h» #include «error.h»
int no_of_errors;
double error(char* s) (* /* ... */ *)
При таком стиле использования заголовочных файлов .h файл и связанный с ним .c файл можно рассматривать как мдуль, в котором .h файл задает интерфейс, а .c файл задает реализацию. Таблица символов не зависит от остальной части калькулятора за исключением использования функции ошибок. Это можно сделать явным:
// table.h: описания таблицы имен
struct name (* char* string; name* next; double value; *);
extern name* look(char* p, int ins = 0); inline name* insert(char* s) (* return look(s,1); *)
// table.c: определения таблицы имен
#include «error.h» #include «string.h» #include «table.h»
const TBLSZ = 23; name* table[TBLSZ];
name* look(char* p; int ins) (* /* ... */ *)
Заметьте, что описания функций работы со строками теперь включаются из «string.h». Это исключает еще один возможный источник ошибок.
// lex.h: описания для ввода и лексического анализа
enum token_value (* NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', 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 «stream.h» #include «ctype.h» #include «error.h» #include «lex.h»
token_value curr_tok; double number_value; char name_string[256];
token_value get_token() (* /* ... */ *)
Интерфейс синтаксического анализатора совершенно прозрчен: // syn.c: описания для синтаксического анализа и вычисления
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 «stream.h» #include «error.h» #include «lex.h» #include «syn.h» #include «table.h» #include «string.h»
main(int argc, char* argv[]) (* /* ... */ *)
Сколько заголовочных файлов использовать в программе, зависит от многих факторов. Многие из этих факторов сильнее связаны с тем, как ваша система работает с заголовочными фалами, нежели с С++. Например, если в вашем редакторе нет средств, позволяющих одновременно видеть несколько файлов, использование большого числа файлов становится менее привлкательным. Аналогично, если открывание и чтение 10 файлов по 50 строк в каждом требует заметно больше времени, чем чтение одного файла в 500 строк, вы можете дважды подумать, прежде чем использовать в небольшом проекте стиль множественных зголовочных файлов. Слово предостережения: набор из десяти зголовочных файлов плюс стандартные заголовочные файлы обычно легче поддаются управлению. С другой стороны, если вы разбили описания в большой программе на логически минимальные по рамеру заголовочные файлы (помещая каждое описание структуры в свой отдельный файл и т.д.), у вас легко может получиться нразбериха из сотен файлов.
4.3.3 Сокрытие Данных
Используя заголовочные файлы пользователь может опредлять явный интерфейс, чтобы обеспечить согласованное исползование типов в программе. С другой стороны, пользователь может обойти интерфейс, задаваемый заголовочным файлом, вводя в .c файлы описания extern.
Заметьте, что такой стиль компоновки не рекомендуется:
// file1.c: // «extern» не используется int a = 7; const c = 8; void f(long) (* /* ... */ *)
// file2.c: // «extern» в .c файле extern int a; extern const c; extern f(int); int g() (* return f(a+c); *)
Поскольку описания extern в file2.c не включаются вместе с определениями в файле file1.c, компилятор не может проверить согласованность этой программы. Следовательно, если только загрузчик не окажется гораздо сообразительнее среднго, две ошибки в этой программе останутся, и их придется икать программисту.
Пользователь может защитить файл от такой недисциплинрованной компоновки, описав имена, которые не предназначены для общего пользования, как static, чтобы их областью видмости был файл, и они были скрыты от остальных частей прораммы. Например:
// table.c: определения таблицы имен
#include «error.h» #include «string.h» #include «table.h»
const TBLSZ = 23; static name* table[TBLSZ];
name* look(char* p; int ins) (* /* ... */ *)
Это гарантирует, что любой доступ к table действительно будет осуществляться именно через look(). «Прятать» константу TBLSZ не обязательно.
4.4 Файлы как Модули
В предыдущем разделе .c и .h файлы вместе определяли часть программы. Файл .h является интерфейсом, который ипользуют другие части программы, .c файл задает реализацию. Такой объект часто называют модулем. Доступными делаются только те имена, которые необходимо знать пользователю, отальные скрыты. Это качество часто называют сокрытием данных, хотя данные – лишь часть того, что может быть скрыто. Модули такого вида обеспечивают большую гибкость. Например, реализция может состоять из одного или более .c файлов, и в виде .h файлов может быть предоставлено несколько интерфейсов. Инфомация, которую пользователю знать не обязательно, искусно скрыта в .c файлах. Если важно, что пользователь не должен точно знать, что содержится в .c файлах, не надо делать их доступными в исходом виде. Достаточно эквивалентных им выхоных файлов компилятора (.o файлов).
Иногда возникает сложность, состоящая в том, что подоная гибкость достигается без формальной структуры. Сам язык не распознает такой модуль как объект, и у компилятора нет возможности отличить .h файлы, определяющие имена, которые должны использовать другие модули (экспортируемые), от .h файлов, которые описывают имена из других модулей (импортиремые).
В других случаях может возникнуть та проблема, что мдуль определяет множество объектов, а не новый тип. Например, модуль table определяет одну таблицу, и если вам нужно две таблицы, то нет простого способа задать вторую таблицу с пмощью понятия модуля. Решение этой проблемы приводится в Глве 5.
Каждый статически размещенный объект по умолчанию иницализируется нулем, программист может задать другие (константные) значения. Это только самый примитивный вид инциализации. К счастью, с помощью классов можно задать код, который выполняется для инициализации перед тем, как модуль какимлибо образом используется, и/или код, который запускаеся для очистки после последнего использования модуля, см. #5.5.2.
4.5 Как Создать Библиотеку
Фразы типа «помещен в библиотеку» и «ищется в какой-то библиотеке» используются часто (и в этой книге, и в других), но что это означает для С++ программы? К сожалению, ответ звисит от того, какая операционная система используется; в этом разделе объясняется, как создать библиотеку в 8-ой весии системы UNIX. Другие системы предоставляют аналогичные возможности.
Библиотека в своей основе является множеством .o файлов, полученных в результате компиляции соответствующего множества .c файлов. Обычно имеется один или более .h файлов, в которых содержатся описания для использования этих .o файлов. В кчестве примера рассмотрим случай, когда нам надо задать (обычным способом) набор математических функций для некоторго неопределенного множества пользователей. Заголовочный файл мог бы выглядеть примерно так:
extern double sqrt(double); // подмножество «math.h» extern double sin(double); extern double cos(double); extern double exp(double); extern double log(double);
а определения этих функций хранились бы, соответственно, в файлах sqrt.c, sin.c, cos.c, exp.c и log.c.
Библиотеку с именем math.h можно создать, например, так:
$ 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.c вызывают только функции sqrt() и cos(), то кажется, что будет достаточно
$ CC myprog.c sqrt.o cos.o
Но это не так, поскольку cos.c использует sin.c.
Компоновщик, вызываемый командой CC для обработки .a файла (в данном случае, файла math.a) знает, как из того мнжества, которое использовалось для создания .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*, int*) // определение (* int t = *p; *p =*q; *q = t; *)
Чтобы избежать расходов на вызов функции, функцию можно описать как inline (#1.12), а чтобы обеспечить более быстрый доступ к параметрам, их можно описать как register (#2.3.11). Оба средства могут использоваться неправильно, и их следует избегать везде где есть какие-либо сомнения в их полезности.
4.6.3 Передача Параметров
Когда вызывается функция, дополнительно выделяется пмять под ее формальные параметры, и каждый формальный парметр инициализируется соответствующим ему фактическим парметром. Семантика передачи параметров идентична семантике инициализации. В частности, тип фактического параметра сопотавляется с типом формального параметра, и выполняются все
стандартные и определенные пользователем преобразования тпов. Есть особые правила для передачи векторов (#4.6.5), средство передавать параметр без проверки типа параметра (#4.6.8) и средство для задания параметров по умолчанию (#4.6.6). Рассмотрим
void f(int val, int amp; ref) (* val++; ref++; *)
Когда вызывается f(), val++ увеличивает локальную копию первого фактического параметра, тогда как ref++ увеличивает второй фактический параметр. Например:
int i = 1; int j = 1; f(i,j);
увеличивает j, но не i. Первый параметр – i, передается по значению, второй параметр – j, передается по ссылке. Как уже отмечалось в #2.3.10, использование функций, которые именяют переданные по ссылке параметры, могут сделать програму трудно читаемой, и их следует избегать (но см. #6.5 и #8.4). Однако передача большого объекта по ссылке может быть гораздо эффективнее, чем передача его по значению. В этом случае параметр можно описать как const, чтобы указать, что ссылка применяется по соображениям эффективности, а также чтобы не позволить вызываемой функции изменять значение обекта:
void f(const large amp; arg) (* // значение «arg» не может быть изменено *)
Аналогично, описание параметра указателя как const соощает читателю, что значение объекта, указываемого указателем, функцией не изменяется. Например:
extern int strlen(const char*); // из «string.h» extern char* strcpy(char* to, const char* from); extern int strcmp(const char*, const char*);
Важность такой практики возрастает с размером программы.
Заметьте, что семантика передачи параметров отлична от семантики присваивания. Это важно для const параметров, сслочных параметров и параметров некоторых типов, определяемых пользователем (#6.6).
4.6.4 Возврат Значения
Из функции, которая не описана как void, можно (и долно) возвращать значение. Возвращаемое значение задается опратором return. Например:
int fac(int n) (*return (n»1) ? n*fac(n-1) : 1; *)
В функции может быть больше одного оператора return: int fac(int n) (* if (n » 1) return n*fac(n-1); else return 1; *)
Как и семантика передачи параметров, семантика возврата функцией значения идентична семантике инициализации. Возврщаемое значение рассматривается как инициализатор переменной возвращаемого типа. Тип возвращаемого выражения проверяется на согласованность с возвращаемым типом и выполняются все стандартные и определенные пользователем преобразования тпов. Например:
double f() (* // ... return 1; // неявно преобразуется к double(1) *)
Каждый раз, когда вызывается функция, создается новая копия ее параметров и автоматических переменных. После возрата из функции память используется заново, поэтому возврщать указатель на локальную переменную неразумно. Содержание указываемого места изменится непредсказуемо:
int* f() (* int local = 1; // ... return amp;local; // так не делайте *)
Эта ошибка менее обычна, чем эквивалентная ошибка при использовании ссылок:
int amp; f() (* int local = 1; // ... return local; // так не делайте *)
К счастью, о таких возвращаемых значениях предупреждает компилятор. Вот другой пример:
int amp; f() (* return 1;*) // так не делайте
4.6.5 Векторные Параметры
Если в качестве параметра функции используется вектор, то передается указатель на его первый элемент. Например:
int strlen(const char*);
void f() (* char v[] = «a vector» strlen(v); strlen(«Nicholas»); *);
Иначе говоря, при передаче как параметр параметр типа T[] преобразуется к T*. Следовательно, присваивание элементу векторного параметра изменяет значение элемента вектора, кторый является параметром. Другими словами, вектор отличается от всех остальных типов тем, что вектор не передается (и не может передаваться) по значению.
Размер вектора недоступен вызываемой функции. Это может быть неудобно, но эту сложность можно обойти несколькими спсобами. Строки оканчиваются нулем, поэтому их размер можно легко вычислить. Для других векторов можно передавать второй
параметр, который задает размер, или определить тип, содержщий указатель и индикатор длины, и передавать его вместо просто вектора (см. также #1.11). Например:
void compute1(int* vec_ptr, int vec_size); // один способ
struct vec (* // другой способ int* ptr; int size; *);
void compute2(vec v);
С многомерными массивами все хитрее, но часто можно вместо них использовать векторы указателей, которые не требют специального рассмотрения. Например:
char* day[] = (* «mon», «tue», «wed», «thu», «fri», «sat», «sun» *);
С другой стороны, рассмотрим определение функции, котрая работает с двумерными матрицами. Если размерность извесна на стадии компиляции, то никаких проблем нет:
void print_m34(int m[3][4]) (* for (int i = 0; i«3; i++) (* for (int j = 0; j„4; j++) cout «« " " «« m[i][j]; cout «« «\n“; *) *)
Матрица, конечно, все равно передается как указатель, а размерности используются просто для удобства записи.
Первая размерность массива не имеет отношения к задаче поиска положения элемента (#2.3.6). Поэтому ее можно передвать как параметр:
void print_mi4(int m[][4], int dim1) (* for (int i = 0; i«dim1; i++) (* for (int j = 0; j„4; j++) cout «« " " «« m[i][j]; cout «« «\n“; *) *)
Сложный случай возникает, когда нужно передавать обе размерности. «Очевидное решение» просто не работает:
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]
4.6.6 Параметры по Умолчанию
Часто в самом общем случае функции требуется больше праметров, чем в самом простом и более употребительном случае. Например, в библиотеке потоков есть функция hex(), порождащая строку с шестнадцатиричным представлением целого. Второй параметр используется для задания числа символов для предтавления первого параметра. Если число символов слишком мало для представления целого, происходит усечение, если оно сликом велико, то строка дополняется пробелами. Часто програмист не заботится о числе символов, необходимых для предсталения целого, поскольку символов достаточно. Поэтому для нуля в качестве второго параметра определено значение «использвать столько символов, сколько нужно». Чтобы избежать засорния программы вызовами вроде hex(i,0), функция описывается так:
extern char* hex(long, int =0);
Инициализатор второго параметра является параметром по умолчанию. То есть, если в вызове дан только один параметр, в качестве второго используется параметр по умолчанию. Напрмер:
cout «„ „**“ «« hex(31) «« hex(32,3) «« «**“;
интерпретируется как
cout «„ „**“ «« hex(31,0) «« hex(32,3) «« «**“;
и напечатает:
** 1f 20**
Параметр по умолчанию проходит проверку типа во время описания функции и вычисляется во время ее вызова. Задавать параметр по умолчанию возможно только для последних парамеров, поэтому
int f(int, int =0, char* =0); // ok int g(int =0, int =0, char*); // ошибка int f(int =0, int, char* =0); // ошибка
Заметьте, что в этом контексте пробел между * и = явлется существенным (*= является операцией присваивания):
int nasty(char*=0); // синтаксическая ошибка
4.6.7 Перегрузка Имен Функций
Как правило, давать разным функциям разные имена – мысль хорошая, но когда некоторые функции выполняют одинаковую рботу над объектами разных типов, может быть более удобно дать им одно и то же имя. Использование одного имени для различных действий над различными типами называется перегрузкой (overloading). Метод уже используется для основных операций С ++: у сложения существует только одно имя, +, но его можно применять для сложения значений целых, плавающих и указателных типов. Эта идея легко расширяется на обработку операций, определенных пользователем, то есть, функций. Чтобы уберечь программиста от случайного повторного использования имени, имя может использоваться более чем для одной функции только если оно сперва описано как перегруженное имя функции. Напрмер:
overload print; void print(int); void print(char*);
Что касается компилятора, единственное общее, что имеют функции с одинаковым именем, это имя. Предположительно, они в каком-то смысле похожи, но в этом язык ни стесняет програмиста, ни помогает ему. Таким образом, перегруженные имена функций – это главным образом удобство записи. Это удобство значительно в случае функций с общепринятыми именами вроде sqrt, print и open. Когда имя семантически значимо, как это имеет место для операций вроде +, * и «« (#6.2) и в случае конструкторов (#5.2.4 и #6.3.1), это удобство становится сщественным. Когда вызывается перегруженная f(), компилятор должен понять, к какой из функций с именем f следует обртиться. Это делается путем сравнения типов фактических парметров с типами формальных параметров всех функций с именем f. Поиск функции, которую надо вызвать, осуществляется за три отдельных шага:
1. Искать функцию соответствующую точно, и использовать ее, если она найдена,
2. Искать соответствующую функцию используя встроенные преобразования и использовать любую найденную функцию и
3. Искать соответствующую функцию используя преобразвания, определенные пользователем (#6.3), и если множество преобразований единственно, использовать найденную функцию.
Например:
overload print(double), print(int);
void f(); (* print(1); print(1.0); *)
Правило точного соответствия гарантирует, что f напечтает 1 как целое и 1.0 как число с плавающей точкой. Ноль, char или short точно соответствуют параметру. Аналогично, float точно соответствует double.
К параметрам функций с перегруженными именами стандарные С++ правила неявного преобразования типа (#с.6.6) примняются не полностью. Преобразования, могущие уничтожить иформацию, не выполняются. Остаются int в long, int в double, ноль в long, ноль в double и преобразования указателей: преобразование ноль в указатель void*, и указатель на произвоный класс в указатель на базовый класс (#7.2.4).
Вот пример, в котором преобразование необходимо:
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); // из «math.h» complex pow(double, complex); // из «complex.h» complex pow(complex, int); complex pow(complex, double); complex pow(complex, complex);
Процесс поиска подходящей функции игнорирует unsigned и const.
4.6.8 Незаданное Число Параметров
Для некоторых функций невозможно задать число и тип всех параметров, которые можно ожидать в вызове. Такую функцию описывают завершая список описаний параметров многоточием (...), что означает «и может быть, еще какие-то неописанные параметры». Например:
int printf(char* ...);
Это задает, что в вызове 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);
откомпилируется и в лучшем случае приведет к какой-нбудь странного вида выдаче. Очевиднще хуже, каждый вызов функции с автоматическим объектом класса включает по меньшей мере один вызов программ выделения и освобождения свободной памяти. Это сделало бы также невозможным реализацию inline-функций членов, которые обращаются к данным закрытой части. Более того, такое изменение сделает невозможным совместную компоновку C и С++ программ (поскольку C компилятор обрабатывает struct не так, как это будет делать С++ компилятор). Для С++ это было сочтено неприемлемым.
5.3.2 Законченный Класс
Программирование без сокрытия данных (с применением структур) требует меньшей продуманности, чем программирование со сокрытием данных (с использованием классов). Структуру можно определить не слишком задумываясь о том, как ее предплагается использовать. А когда определяется класс, все внимние сосредотачивается на обеспечении нового типа полным мнжеством операций; это важное смещение акцента. Время, потраченное на разработку нового типа, обычно многократно окупается при разработке и тестировании программы.