
Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdf
С++ для начинающих |
432 |
9.1.5. Директива extern "C" и перегруженные функции A
Вразделе 7.7 мы видели, что директиву связывания extern "C" можно использовать в программе на C++ для того, чтобы указать, что некоторый объект находится в части, написанной на языке C. Как эта директива влияет на объявления перегруженных функций? Могут ли в одном и том же множестве находиться функции, написанные как на C++, так и на C?
Вдирективе связывания разрешается задать только одну из множества перегруженных
// ошибка: для двух перегруженных функций указана директива extern "C" extern "C" void print( const char* );
функций. Например, следующая программа некорректна: extern "C" void print( int );
Приведенный ниже пример перегруженной функции calc() иллюстрирует типичное
class SmallInt ( /* ... */ ); class BigNum ( /* ... */ );
//написанная на C функция может быть вызвана как из программы,
//написанной на C, так и из программы, написанной на C++.
//функции C++ обрабатывают параметры, являющиеся классами extern "C" double calc( double );
extern SmallInt calc( const SmallInt& );
применение директивы extern "C":
extern BigNum calc( const BigNum& );
Написанная на C функция calc() может быть вызвана как из C, так и из программы на C++. Остальные две функции принимают в качестве параметра класс и, следовательно, их допустимо использовать только в программе на C++. Порядок следования объявлений несуществен.
Директива связывания не имеет значения при решении, какую функцию вызывать; важны только типы параметров. Выбирается та функция, которая лучше всего
Smallint si = 8; |
|
int main() { |
// вызывается C-функция calc( double ) |
calc( 34 ); |
|
calc( si ); |
// вызывается функция C++ calc( const SmallInt & ) |
// ... |
|
return 0; |
|
соответствует типам переданных аргументов:
}
9.1.6. Указатели на перегруженные функции A
Можно объявить указатель на одну из множества перегруженных функций. Например:

С++ для начинающих |
433 |
extern void ff( vector<double> ); extern void ff( unsigned int );
// на какую функцию указывает pf1?
void ( *pf1 )( unsigned int ) = &ff;
Поскольку функция ff() перегружена, одного инициализатора &ff недостаточно для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует указатель, компилятор ищет в множестве всех перегруженных функций ту, которая имеет тот же тип возвращаемого значения и список параметров, что и функция, на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned int).
А что если не найдется функции, в точности соответствующей типу указателя? Тогда
extern void ff( vector<double> ); extern void ff( unsigned int );
// ошибка: соответствие не найдено: неверный список параметров
компилятор выдаст сообщение об ошибке:
// ошибка: соответствие не найдено: неверный тип возвращаемого значения
void ( *pf2 )( int ) = &ff;
double ( *pf3 )( vector<double> ) = &ff;
Присваивание работает аналогично. Если значением указателя должен стать адрес перегруженной функции , то для выбора операнда в правой части оператора присваивания используется тип указателя на функцию. И если компилятор не находит функции, в точности соответствующей нужному типу, он выдает сообщение об ошибке. Таким образом, преобразование типов между указателями на функции никогда не
matrix calc( const matrix & ); int calc( int, int );
int ( *pc1 )( int, int ) = 0; int ( *pc2 )( int, double ) = 0;
//...
//правильно: выбирается функция calc( int, int ) pc1 = &calc;
//ошибка: нет соответствия: неверный тип второго параметра
производится. pc2 = &calc;
9.1.7. Безопасное связывание A
При использовании перегрузки складывается впечатление, что в программе можно иметь несколько одноименных функций с разными списками параметров. Однако это лексическое удобство существует только на уровне исходного текста. В большинстве систем компиляции программы, обрабатывающие этот текст для получения исполняемого

С++ для начинающих |
434 |
кода, требуют, чтобы все имена были различны. Редакторы связей, как правило, разрешают внешние ссылки лексически. Если такой редактор встречает имя print два или более раз, он не может различить их путем анализа типов (к этому моменту информация о типах обычно уже потеряна). Поэтому он просто печатает сообщение о повторно определенном символе print и завершает работу.
Чтобы разрешить эту проблему, имя функции вместе с ее списком параметров декорируется так, чтобы получилось уникальное внутреннее имя. Вызываемые после компилятора программы видят только это внутреннее имя. Как именно производится такое преобразование имен, зависит от реализации. Общая идея заключается в том, чтобы
представить число и типы параметров в виде строки символов и дописать ее к имени функции.
Как было сказано в разделе 8.2, такое кодирование гарантирует, в частности, что два объявления одноименных функций с разными списками параметров, находящиеся в разных файлах, не воспринимаются редактором связей как объявления одной и той же функции. Поскольку этот способ помогает различить перегруженные функции на фазе редактирования связей, мы говорим о безопасном связывании.
Декорирование имен не применяется к функциям, объявленным с помощью директивы extern "C", так как лишь одна из множества перегруженных функций может быть написана на чистом С. Две функции с различными списками параметров, объявленные как extern "C", редактор связей воспринимает как один и тот же символ.
Упражнение 9.1
Зачем может понадобиться объявлять перегруженные функции? Упражнение 9.2
Как нужно объявить перегруженные варианты функции error(), чтобы были корректны
int index;
int upperBound; char selectVal; // ...
error( "Array out of bounds: ", index, upperBound ); error( "Division by zero" );
следующие вызовы:
error( "Invalid selection", selectVal );
Упражнение 9.3
Объясните, к какому эффекту приводит второе объявление в каждом из приведенных примеров:

С++ для начинающих |
435 |
(a)int calc( int, int );
int calc( const int, const int );
(b)int get(); double get();
(c)int *reset( int * ); double *reset( double * ):
(d)extern "C" int compute( int *, int );
extern "C" double compute( double *, double );
Упражнение 9.4
(a)void reset( int * );
void (*pf)( void * ) = reset;
(b)int calc( int, int );
int (*pf1)( int, int ) = calc;
(c)extern "C" int compute( int *, int ); int (*pf3)( int*, int ) = compute;
Какая из следующих инициализаций приводит к ошибке? Почему?
(d)void (*pf4)( const matrix & ) = 0;
9.2.Три шага разрешения перегрузки
Разрешением перегрузки функции называется процесс выбора той функции из множества перегруженных, которую следует вызвать. Этот процесс основывается на указанных при
T t1, t2;
void f( int, int ); void f( float, float );
int main() { f( t1, t2 ); return 0;
вызове аргументах. Рассмотрим пример:
}
Здесь в ходе процесса разрешения перегрузки в зависимости от типа T определяется,
будет ли при обработке выражения f(t1,t2) вызвана функция f(int,int) или f(float,float) или зафиксируется ошибка.
Разрешение перегрузки функции – один и самых сложных аспектов языка C++. Пытаясь разобраться во всех деталях, начинающие программисты столкнутся с серьезными трудностями. Поэтому в данном разделе мы представим лишь краткий обзор того, как происходит разрешение перегрузки, чтобы у вас составилось хоть какое-то впечатление

С++ для начинающих |
436 |
об этом процессе. Для тех, кто хочет узнать больше, в следующих двух разделах приводится более подробное описание.
Процесс разрешения перегрузки функции состоит из трех шагов, которые мы покажем на
void f(); void f( int );
void f( double, double = 3.4 ); void f( char *, char * );
void main() { f( 5.6 ); return 0;
следующем примере:
}
При разрешении перегрузки функции выполняются следующие шаги:
1.Выделяется множество перегруженных функций для данного вызова, а также свойства списка аргументов, переданных функции.
2.Выбираются те из перегруженных функций, которые могут быть вызваны с данными аргументами, с учетом их количества и типов.
3.Находится функция, которая лучше всего соответствует вызову.
Рассмотрим последовательно каждый пункт.
На первом шаге необходимо идентифицировать множество перегруженных функций, которые будут рассматриваться при данном вызове. Вошедшие в это множество функции называются кандидатами. Функция-кандидат – это функция с тем же именем, что и вызванная, причем ее объявление видимо в точке вызова. В нашем примере есть четыре таких кандидата: f(), f(int), f(double, double) и f(char*, char*).
После этого идентифицируются свойства списка переданных аргументов, т.е. их количество и типы. В нашем примере список состоит из двух аргументов типа double.
На втором шаге среди множества кандидатов отбираются устоявшие (viable) – такие, которые могут быть вызваны с данными аргументами, Устоявшая функция либо имеет столько же формальных параметров, сколько фактических аргументов передано вызванной функции, либо больше, но тогда для каждого дополнительного параметра должно быть задано значение по умолчанию. Чтобы функция считалась устоявшей, для любого фактического аргумента, переданного при вызове, обязано существовать преобразование к типу формального параметра, указанного в объявлении.
В нашем примере есть две устоявших функции, которые могут быть вызваны с приведенными аргументами:
∙функция f(int) устояла, потому что у нее есть всего один параметр и
существует преобразование фактического аргумента типа double к формальному параметру типа int;
∙функция f(double,double) устояла, потому что для второго аргумента есть значение по умолчанию, а первый формальный параметр имеет тип double, что в точности соответствует типу фактического аргумента.
Если после второго шага не нашлось устоявших функций, то вызов считается ошибочным. В таких случаях мы говорим, что имеет место отсутствие соответствия.

С++ для начинающих |
437 |
Третий шаг заключается в выборе функции, лучше всего отвечающей контексту вызова.
Такая функция называется наилучшей из устоявших (или наиболее подходящей). На этом шаге производится ранжирование преобразований, использованных для приведения типов фактических аргументов к типам формальных параметров устоявшей функции. Наиболее подходящей считается функция, для которой выполняются следующие условия:
преобразования, примененные к фактическим аргументам, не хуже преобразований, необходимых для вызова любой другой устоявшей функции;
для некоторых аргументов примененные преобразования лучше, чем преобразования, необходимые для приведения тех же аргументов в вызове других устоявших функций.
Преобразования типов и их ранжирование более подробно обсуждаются в разделе 9.3. Здесь мы лишь кратко рассмотрим ранжирование преобразований для нашего примера.
Для устоявшей функции f(int) должно быть применено приведение фактического аргумента типа double к типу int, относящееся к числу стандартных. Для устоявшей
функции f(double,double) тип фактического аргумента double в точности соответствует типу формального параметра. Поскольку точное соответствие лучше стандартного преобразования (отсутствие преобразования всегда лучше, чем его наличие), то наиболее подходящей функцией для данного вызова считается f(double,double).
Если на третьем шаге не удается отыскать единственную лучшую из устоявших функцию, иными словами, нет такой устоявшей функции, которая подходила бы больше всех остальных, то вызов считается неоднозначным, т.е. ошибочным.
(Более подробно все шаги разрешения перегрузки функции обсуждаются в разделе 9.4. Процесс разрешения используется также при вызовах перегруженной функции-члена класса и перегруженного оператора. В разделе 15.10 рассматриваются правила разрешения перегрузки, применяемые к функциям-членам класса, а в разделе 15.11 – правила для перегруженных операторов. При разрешении перегрузки следует также принимать во внимание функции, конкретизированные из шаблонов. В разделе 10.8 обсуждается, как шаблоны влияют на такое разрешение.)
Упражнение 9.5 Что происходит на последнем (третьем) шаге процесса разрешения перегрузки функции?
9.3. Преобразования типов аргументов A
На втором шаге процесса разрешения перегрузки функции компилятор идентифицирует и ранжирует преобразования, которые следует применить к каждому фактическому
аргументу вызванной функции для приведения его к типу соответствующего формального параметра любой из устоявших функций. Ранжирование может дать один из трех возможных результатов:
∙ точное соответствие. Тип фактического аргумента точно соответствует типу формального параметра. Например, если в множестве перегруженных функций
void print( unsigned int ); void print( const char* );
print() есть такие: void print( char );

С++ для начинающих |
438 |
unsigned int a; |
|
print( 'a' ); |
// соответствует print( char ); |
print( "a" ); |
// соответствует print( const char* ); |
то каждый из следующих трех вызовов дает точное соответствие:
print( a ); |
// соответствует print( unsigned int ); |
∙соответствие с преобразованием типа. Тип фактического аргумента не
void ff( char );
соответствует типу формального параметра, но может быть преобразован в него:
ff( 0 ); |
// аргумент типа int приводится к типу char |
∙ отсутствие соответствия. Тип фактического аргумента не может быть приведен к типу формального параметра в объявлении функции, поскольку необходимого преобразования не существует. Для каждого из следующих двух
// функции print() объявлены так же, как и выше int *ip;
class SmallInt { /* ... */ }; SmallInt si;
print( ip ); // ошибка: нет соответствия
вызовов функции print() соответствия нет: print( si ); // ошибка: нет соответствия
Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:
∙преобразование l-значения в r-значение;
∙преобразование массива в указатель;
∙преобразование функции в указатель;
∙преобразования спецификаторов.
(Подробнее они рассмотрены ниже.)
Категория соответствия с преобразованием типа является наиболее сложной. Необходимо рассмотреть несколько видов такого приведения: расширение типов (promotions),
стандартные преобразования и определенные пользователем преобразования. (Расширения типов и стандартные преобразования изучаются в этой главе. Определенные пользователем преобразования будут представлены позднее, после детального рассмотрения классов; они выполняются конвертером, функцией-членом, которая позволяет определить в классе собственный набор “стандартных” трансформаций. В главе 15 мы познакомимся с такими конвертерами и с тем, как они влияют на разрешение перегрузки функций.)

С++ для начинающих |
439 |
При выборе лучшей из устоявших функций для данного вызова компилятор ищет функцию, для которой применяемые к фактическим аргументам преобразования являются “наилучшими”. Преобразования типов ранжируются следующим образом: точное соответствие лучше расширения типа, расширение типа лучше стандартного преобразования, а оно, в свою очередь, лучше определенного пользователем преобразования. Мы еще вернемся к ранжированию в разделе 9.4, а пока на простых примерах покажем, как оно помогает выбрать наиболее подходящую функцию.
9.3.1. Подробнее о точном соответствии
Самый простой случай возникает тогда, когда типы фактических аргументов совпадают с типами формальных параметров. Например, есть две показанные ниже перегруженные функции max(). Тогда каждый из вызовов max() точно соответствует одному из
int max( int, int );
double max( double, double );
|
|
int i1; |
|
|
|
void calc( double d1 |
) { |
|
|
max( 56, i1 ); |
// точно соответствует max( int, int ); |
|
|
max( d1, 66.9 ); |
// точно соответствует max( double, double ); |
объявлений: |
|
||
|
|
} |
|
|
|
||
|
|
|
|
Перечислимый тип точно соответствует только определенным в нем элементам
enum Tokens { INLINE = 128; VIRTUAL = 129; }; Tokens curTok = INLINE;
enum Stat { Fail, Pass };
extern void ff( Tokens ); extern void ff( Stat ); extern void ff( int );
int main() { |
// точно соответствует ff( Stat ) |
ff( Pass ); |
|
ff( 0 ); |
// точно соответствует ff( int ) |
ff( curTok ); |
// точно соответствует ff( Tokens ) |
// ... |
|
перечисления, а также объектам, которые объявлены как принадлежащие к этому типу:
}
Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых – преобразование l-значения в r- значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:
∙можно получить адрес объекта;
∙можно получить значение объекта;

С++ для начинающих |
440 |
∙ это значение легко модифицировать (если только в объявлении объекта нет спецификатора const).
Напротив, r-значение – это выражение, значение которого вычисляется, или выражение, обозначающее временный объект, для которого нельзя получить адрес и значение
int calc( int );
int main() {
int lval, res;
lval = 5; // lvalue: lval; rvalue: 5 res = calc( lval );
//lvalue: res
//rvalue: временный объект для хранения значения,
//возвращаемого функцией calc()
return 0;
которого нельзя модифицировать. Вот простой пример:
}
Впервом операторе присваивания переменная lval – это l-значение, а литерал 5 – r- значение. Во втором операторе присваивания res – это l-значение, а временный объект, в котором хранится результат, возвращаемый функцией calc(), – это r-значение.
Внекоторых ситуациях в контексте, где ожидается значение, можно использовать
int obj1; int obj2;
int main() {
// ...
int local = obj1 + obj2; return 0;
выражение, представляющее собой l-значение:
}
Здесь obj1 и obj2 – это l-значения. Однако для выполнения сложения в функции main() из переменных obj1 и obj2 извлекаются их значения. Действие, состоящее в извлечении значения объекта, представленного выражением вида l-значение, называется преобразованием l-значения в r-значение.
Когда функция ожидает аргумент, переданный по значению, то в случае, если аргумент
|
|
#include <string> |
|
|
|
string color( "purple" ); |
|
|
|
void print( string ); |
|
|
|
int main() { |
// точное соответствие: преобразование lvalue |
|
|
print( color ); |
|
|
|
return 0; |
// в rvalue |
|
|
|
|
является l-значением, выполняется его преобразование в r-значение: |
|||
|
|
} |
|
|
|
||
|
|
|
|

С++ для начинающих |
441 |
Так как аргумент в вызове print(color) передается по значению, то производится преобразование l-значения в r-значение для извлечения значения color и передачи его в функцию с прототипом print(string). Однако несмотря на то, что такое приведение имело место, считается, что фактический аргумент color точно соответствует объявлению print(string).
При вызове функций не всегда требуется применять к аргументам подобное преобразование. Ссылка представляет собой l-значение; если у функции есть параметр- ссылка, то при вызове функция получает l-значение. Поэтому к фактическому аргументу, которому соответствует формальный параметр-ссылка, описанное преобразование не
#include <list>
применяется. Например, пусть объявлена такая функция: void print( list<int> & );
В вызове ниже li – это l-значение, представляющее объект list<int>, передаваемый
list<int> li(20);
int main() { // ...
print( li ); // точное соответствие: нет преобразования lvalue в // rvalue
return 0;
функции print():
}
Сопоставление li с параметром-ссылкой считается точным соответствием.
Второе преобразование, при котором все же фиксируется точное соответствие, – это преобразование массива в указатель. Как уже отмечалось в разделе 7.3, параметр функции никогда не имеет тип массива, трансформируясь вместо этого в указатель на его первый элемент. Аналогично фактический аргумент типа массива из NT (где N – число элементов в массиве, а T – тип каждого элемента) всегда приводится к типу указателя на T. Такое преобразование типа фактического аргумента и называется преобразованием массива в указатель. Несмотря на это, считается, что фактический аргумент точно
int ai[3];
void putValues(int *);
int main() { // ...
putValues(ai); // точное соответствие: преобразование массива в // указатель
return 0;
соответствует формальному параметру типа “указатель на T”. Например:
}
Перед вызовом функции putValues() массив преобразуется в указатель, в результате чего фактический аргумент ai (массив из трех целых) приводится к указателю на int.