Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
235
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

442

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

При установлении точного соответствия допустимо также преобразование функции в указатель. (Оно упоминалось в разделе 7.9.) Как и параметр-массив, параметр-функция становится указателем на функцию. Фактический аргумент типа функциятакже автоматически приводится к типу указателя на функцию. Такое преобразование типа фактического аргумента и называется преобразованием функции в указатель. Хотя трансформация производится, считается, что фактический аргумент точно соответствует

int lexicoCompare( const string &, const string & );

typedef int (*PFI)( const string &, const string & ); void sort( string *, string *, PFI );

string as[10];

int main()

{

// ...

sort( as,

as + sizeof(as)/sizeof(as[0] - 1 ), lexicoCompare // точное соответствие

// преобразование функции в указатель

);

return 0;

формальному параметру. Например:

}

Перед вызовом sort() применяется преобразование функции в указатель, которое приводит аргумент lexicoCompare от типа функцияк типу указатель на функцию”. Хотя формальным параметром функции является указатель, а фактическим имя функции и, следовательно, было произведено преобразование функции в указатель, считается, что фактический аргумент точно третьему формальному параметру функции sort().

Последнее из перечисленных выше это преобразование спецификаторов. Оно относится

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

int a[5] = { 4454, 7864, 92, 421, 938 }; int *pi = a;

bool is_equal( const int * , const int * );

void func( int *parm ) {

// точное соответствие между pi и parm: преобразование спецификаторов if ( is_equal( pi, parm ) )

// ...

return 0;

(или обоих) к типу, который адресует данный указатель:

}

С++ для начинающих

443

Перед вызовом функции is_equal() фактические аргументы pi и parm преобразуются из типа указатель на intв тип указатель на const int”. Эта трансформация заключается в добавлении спецификатора const к адресуемому типу, поэтому относится к категории преобразований спецификаторов. Несмотря на то, что функция ожидает получить два указателя на const int, а фактические аргументы являются указателями на int, считается, что точное соответствие между формальными и фактическими параметрами функции is_equal() установлено.

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

extern void takeCI( const int );

int main() {

 

int ii = ...;

// преобразование спецификаторов не применяется

takeCI(ii);

return 0;

 

или volatile, а фактический аргумент нет.

}

Хотя формальный параметр функции takeCI() имеет тип const int, а вызывается она с аргументом ii типа int, преобразование спецификаторов не производится: есть точное соответствие между фактическим аргументом и формальным параметром.

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

extern void init( int *const ); extern int *pi;

int main() { // ...

init(pi); // преобразование спецификаторов не применяется return 0;

const или volatile относятся к этому указателю:

}

Спецификатор const при формальном параметре функции init() относится к самому указателю, а не к типу, который он адресует. Поэтому компилятор при анализе преобразований, которые должны быть применены к фактическому аргументу, не учитывает этот спецификатор. К аргументу pi не применяется преобразование спецификатора: считается, что этот аргумент и формальный параметр точно соответствуют друг другу.

Первые три из рассмотренных преобразований (l-значения в r-значение, массива в указатель и функции в указатель) часто называют трансформациями l-значений. (В разделе 9.4 мы увидим, что хотя и трансформации l-значений, и преобразования спецификаторов относятся к категории преобразований, не нарушающих точного соответствия, его степень считается выше в случае, когда необходима лишь первая трансформация. В следующем разделе мы поговорим об этом несколько подробнее.)

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

С++ для начинающих

444

extern void ff(int); extern void ff(void *);

то вызов

ff( 0xffbc ); // вызывается ff(int)

будет точно соответствовать ff(int), хотя литерал 0xffbc записан в виде шестнадцатеричной константы. Программист может заставить компилятор вызвать функцию ff(void *), если явно выполнит операцию приведения типа:

ff( reinterpret_cast<void *>(0xffbc) );

// вызывается ff(void*)

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

9.3.2. Подробнее о расширении типов

Под расширением типа понимается одно из следующих преобразований:

фактический аргумент типа char, unsigned char или short расширяется до типа int. Фактический аргумент типа unsigned short расширяется до типа int, если машинный размер int больше, чем размер short, и до типа unsigned int в противном случае;

аргумент типа float расширяется до типа double;

аргумент перечислимого типа расширяется до первого из следующих типов, который способен представить все значения элементов перечисления: int,

unsigned int, long, unsigned long;

аргумент типа bool расширяется до типа int.

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

extern void manip( int );

int main() {

// тип char расширяется до int

manip( 'a' );

return 0;

 

соответствующему расширенному типу:

}

Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный тип соответствует типу формального параметра функции manip(), мы говорим, что ее вызов требует расширения типа аргумента.

С++ для начинающих

445

extern void print( unsigned int ); extern void print( int );

extern void print( char );

unsigned char uc;

Рассмотрим следующий пример:

print( uc ); // print( int ); для uc требуется только расширение типа

Для аппаратной платформы, на которой unsigned char занимает один байт памяти, а int четыре байта, расширение преобразует unsigned char в int, так как с его помощью можно представить все значения типа unsigned char. Для такой машинной

архитектуры из приведенного в примере множества перегруженных функций наилучшее соответствие аргументу типа unsigned char обеспечивает print(int). Для двух других функций установление соответствия требует стандартного приведения.

Следующий пример иллюстрирует расширение фактического аргумента перечислимого

enum Stat ( Fail, Pass );

extern void ff( int ); extern void ff( char );

int main() {

 

// правильно: элемент перечисления Pass расширяется до типа int

ff( Pass );

// ff( int )

ff( 0 );

// ff( int )

типа:

}

Иногда расширение перечислений преподносит сюрпризы. Компиляторы часто выбирают представление перечисления в зависимости от значений его элементов. Предположим, что в вышеупомянутой архитектуре (один байт для char и четыре байта для int) определено такое перечисление:

enum e1 { a1, b1, c1 };

Поскольку есть всего три элемента: a1, b1 и c1 со значениями 0, 1 и 2 соответственно и поскольку все эти значения можно представить типом char, то компилятор, как правило, и выбирает char для представления типа e1. Рассмотрим, однако, перечисление e2 со следующим множеством элементов:

enum e2 { a2, b2, c2=0x80000000 };

Так как одна из констант имеет значение 0x80000000, то компилятор обязан выбрать для представления e2 такой тип, который достаточен для хранения значения 0x80000000, то есть unsigned int.

Итак, хотя и e1, и e2 являются перечислениями, их представления различаются. Из-за этого e1 и e2 расширяются до разных типов:

С++ для начинающих

446

#include <string>

string format( int );

string format( unsigned int );

int main() {

// вызывается format( int )

format(a1);

format(a2);

// вызывается format( unsigned int )

return 0;

 

}

 

При первом обращении к format() фактический аргумент расширяется до типа int, так как для представления типа e1 используется char, и, следовательно, вызывается перегруженная функция format(int). При втором обращении тип фактического

аргумента e2 представлен типом unsigned int и аргумент расширяется до unsigned int, из-за чего вызывается перегруженная функция format(unsigned int). Поэтому следует помнить, что поведение двух перечислений по отношению к процессу разрешения перегрузки может быть различным и зависеть от значений элементов, определяющих, как происходит расширение типа.

9.3.3. Подробнее о стандартном преобразовании

Имеется пять видов стандартных преобразований, а именно:

1.преобразования целых типов: приведение от целого типа или перечисления к любому другому целому типу (исключая трансформации, которые выше были отнесены к категории расширения типов);

2.преобразования типов с плавающей точкой: приведение от любого типа с плавающей точкой к любому другому типу с плавающей точкой (исключая трансформации, которые выше были отнесены к категории расширения типов);

3.преобразования между целым типом и типом с плавающей точкой: приведение от любого типа с плавающей точкой к любому целому типу или наоборот;

4.преобразования указателей: приведение целого значения 0 к типу указателя или трансформация указателя любого типа в тип void*;

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

extern void print( void* ); extern void print( double );

int main() { int i;

print( i ); // соответствует print( double );

// i подвергается стандартному преобразованию из int в

double

print( &i ); // соответствует print( void* );

//&i подвергается стандартному преобразованию

//из int* в void*

return 0;

Вот несколько примеров:

С++ для начинающих

447

}

Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как целевой тип может и не обеспечивать представления всех значений исходного. Например, с помощью float нельзя адекватно представить все значения типа int. Именно по этой причине трансформации, входящие в эти группы, отнесены к категории стандартных

int i;

void calc( float ); int main() {

calc( i ); // стандартное преобразование между целым типом и типом с

//плавающей точкой потенциально опасно в зависимости от

//значения i

return 0;

преобразований, а не расширений типов.

}

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

Предполагается, что все стандартные изменения требуют одного объема работы. Например, преобразование из char в unsigned char не более приоритетно, чем из char в double. Близость типов не принимается во внимание. Если две устоявших функции

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

extern void manip( long );

Например, если даны две перегруженные функции: extern void manip( float );

int main() {

manip( 3.14 ); // ошибка: неоднозначность

// manip( float ) не лучше, чем manip( int )

return 0;

то следующий вызов неоднозначен:

}

Константа 3.14 имеет тип double. С помощью того или иного стандартного

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

manip ( static_cast<long>( 3.14 ) ); // manip( long )

либо используя суффикс, обозначающий, что константа принадлежит к типу float:

С++ для начинающих

448

 

manip ( 3.14F ) );

// manip( float )

 

 

 

 

Вот еще несколько примеров неоднозначных вызовов, которые помечаются как ошибки,

extern void farith( unsigned int ); extern void farith( float );

int main() {

// каждый из последующих вызовов

неоднозначен

farith( 'a' );

// аргумент

имеет тип char

farith( 0 );

// аргумент

имеет тип int

farith( 2uL );

// аргумент

имеет тип unsigned long

farith( 3.14159 );

// аргумент

имеет тип double

farith( true );

// аргумент

имеет тип bool

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

}

Стандартные преобразования указателей иногда противоречат интуиции. В частности, значение 0 приводится к указателю на любой тип; полученный таким образом указатель называется нулевым. Значение 0 может быть представлено как константное выражение

void set(int*);

int main() {

//преобразование указателя из 0 в int* применяется к аргументам

//в обоих вызовах

set( 0L ); set( 0x00 ); return 0;

целого типа:

}

Константное выражение 0L (значение 0 типа long int) и константное выражение 0x00 (шестнадцатеричное целое значение 0) имеют целый тип и потому могут быть преобразованы в нулевой указатель типа int*.

Но поскольку перечисления не относятся к целым типам, элемент, равный 0, не приводим

enum EN { zr = 0 };

к типу указателя:

set( zr ); // ошибка: zr нельзя преобразовать в тип int*

Вызов функции set() является ошибкой, так как не существует преобразования между значением zr элемента перечисления и формальным параметром типа int*, хотя zr равно 0.

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

С++ для начинающих

449

void print( int ); void print( void * );

void set( const char * ); void set( char * );

int main ()

{

// вызывается print( int );

print( 0

);

set( 0

);

// неоднозначность

return

0;

 

}

При вызове print(int) имеет место точное соответствие, тогда как для вызова print(void*) необходимо приведение значения 0 к типу указателя. Поскольку соответствие лучше преобразования, для разрешения этого вызова выбирается функция print(int). Обращение к set() неоднозначно, так как 0 соответствует формальным

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

Последнее из возможных преобразований указателя позволяет привести указатель любого типа к типу void*, поскольку void* это родовой указатель на любой тип данных. Вот

#include <string>

extern void reset( void * );

void func( int *pi, string *ps ) {

// ...

// преобразование указателя: int* в void*

reset( pi );

/// ...

// преобразование указателя: string* в void*

reset( ps );

несколько примеров:

}

Только указатели на типы данных могут быть приведены к типу void* с помощью

typedef int (*PFV)();

extern PFV testCases[10]; // массив указателей на функции

extern void reset( void * );

int main() { // ...

reset( textCases[0] ); // ошибка: нет стандартного преобразования

// между int(*)() и void*

return 0;

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

}

С++ для начинающих

450

9.3.4. Ссылки

Фактический аргумент или формальный параметр функции могут быть ссылками. Как это влияет на правила преобразования типов?

Рассмотрим, что происходит, когда ссылкой является фактический аргумент. Его тип никогда не бывает ссылочным. Аргумент-ссылка трактуется как l-значение, тип которого

int i;

int& ri = i;

void print( int );

int main() {

// аргумент - это lvalue типа int

print( i );

print( ri );

// то же самое

return 0;

 

совпадает с типом соответствующего объекта:

}

Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для его передачи во втором вызове не влияет на сам тип аргумента.

Стандартные преобразования и расширения типов, рассматриваемые компилятором, одинаковы для случаев, когда фактический аргумент является ссылкой на тип T и когда

int i;

int& ri = i;

void calc( double );

int main() {

// стандартное преобразование между целым типом

calc( i );

calc( ri );

// и типом с плавающей точкой

// то же самое

return 0;

 

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

}

А как влияет на преобразования, применяемые к фактическому аргументу, формальный параметр-ссылка? Сопоставление дает следующие результаты:

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

void swap( int &, int & );

void manip( int i1, int i2 ) {

// ...

swap( i1, i2 ); // правильно: вызывается swap( int &, int & )

// ...

return 0;

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

}

С++ для начинающих

451

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

int obj;

void frd( double & ); int main() {

frd( obj ); // ошибка: параметр должен иметь иметь тип const double & return 0;

функции. Например:

}

Вызов функции frd() является ошибкой. Фактический аргумент имеет тип int и должен быть преобразован в тип double, чтобы соответствовать формальному параметру-ссылке. Результатом такой трансформации является временная переменная. Поскольку ссылка не имеет спецификатора const, то для ее инициализации такие переменные использовать нельзя.

Вот еще один пример, в котором между формальным параметром-ссылкой и

class B;

void takeB( B& ); B giveB();

int main() {

takeB( giveB() ); // ошибка: параметр должен быть типа const B & return 0;

фактическим аргументом нет соответствия:

}

Вызов функции takeB() – ошибка. Фактический аргумент это возвращаемое значение, т.е. временная переменная, которая не может быть использована для инициализации ссылки без спецификатора const.

В обоих случаях мы видим, что если формальный параметр-ссылка имеет спецификатор const, то между ним и фактическим аргументом может быть установлено точное соответствие.

Следует отметить, что и преобразование l-значения в r-значение, и инициализация ссылки считаются точными соответствиями. В данном примере первый вызов функции

void print( int ); void print( int& );

int iobj;

int &ri = iobj;

int main() {

 

// ошибка: неоднозначность

print( iobj );

print( ri );

// ошибка: неоднозначность

print(

86

);

// правильно: вызывается print( int )

return

0;

 

 

приводит к ошибке: