<< Пред.           стр. 32 (из 121)           След. >>

Список литературы по разделу

 Что будет, если попытаться вызвать функцию gcd() с аргументами типа char*?
 gcd( "hello", "world" );
 А если передать этой функции не два аргумента, а только один? Или больше двух? Что случится, если потеряется запятая между числами 24 и 312?
 gcd( 24312 );
 Единственное разумное поведение компилятора – сообщение об ошибке, поскольку попытка выполнить такую программу чревата весьма серьезными последствиями. С++ действительно не пропустит подобные вызовы. Текст сообщения будет выглядеть примерно так:
 
 // gcd( "hello", "world" )
 error: invalid argument types ( const char *, const char * ) --
  expecting ( int, int )
 ошибка: неверные типы аргументов ( const char *, const char * ) --
  ожидается ( int, int )
 
 // gcd( 24312 )
 error: missing value for second argument
 ошибка: пропущено значение второго аргумента
 
 А если вызвать эту функцию с аргументами типа double? Должен ли этот вызов расцениваться как ошибочный?
 gcd( 3.14, 6.29 );
 Как было сказано в разделе 4.14, значение типа double может быть преобразовано в int. Следовательно, считать такой вызов ошибочным было бы слишком сурово. Вместо этого аргументы неявно преобразуются в int (отбрасыванием дробной части) и таким образом требования, налагаемые на типы параметров, выполняются. Поскольку при подобном преобразовании возможна потеря точности, хороший компилятор выдаст предупреждение. Вызов превращается в
 gcd( 3, 6 );
 что дает в результате 3.
 С++ является строго типизированным языком. Компилятор проверяет аргументы на соответствие типов в каждом вызове функции. Если тип фактического аргумента не соответствует типу формального параметра, то производится попытка неявного преобразования. Если же это оказывается невозможным или число аргументов неверно, компилятор выдает сообщение об ошибке. Именно поэтому функция должна быть объявлена до того, как программа впервые обратится к ней: без объявления компилятор не обладает информацией для проверки типов.
 Пропуск аргумента при вызове или передача аргумента неуказанного типа часто служили источником ошибок в языке С. Теперь такие погрешности обнаруживаются на этапе компиляции.
 Упражнение 7.1
 Какие из следующих прототипов функций содержат ошибки? Объясните.
 (a) set( int *, int );
 (b) void func();
 (c) string error( int );
 (d) arr[10] sum( int *, int );
 Упражнение 7.2
 Напишите прототипы для следующих функций:
 Функция с именем compare, имеющая два параметра типа ссылки на класс matrix и возвращающая значение типа bool.
 Функция с именем extract без параметров, возвращающая контейнер set для хранения значений типа int. (Контейнерный тип set описывался в разделе 6.13.)
 Упражнение 7.3
 Имеются объявления функций:
 double calc( double );
 int count( const string &, char );
 void sum( vector &, int );
 vector vec( 10 );
 Какие из следующих вызовов содержат ошибки и почему?
 (a) calc( 23.4, 55.1 );
 (b) count( "abcda", 'a' );
 (c) sum( vec, 43.8 );
 (d) calc( 66 );
 7.3. Передача аргументов
 Функции используют память из стека программы. Некоторая область стека отводится функции и остается связанной с ней до окончания ее работы, по завершении которой отведенная ей память освобождается и может быть занята другой функцией. Иногда эту часть стека называют областью активации.
 Каждому параметру функции отводится место в данной области, причем его размер определяется типом параметра. При вызове функции память инициализируется значениями фактических аргументов.
 Стандартным способом передачи аргументов является копирование их значений, т.е. передача по значению. При этом способе функция не получает доступа к реальным объектам, являющихся ее аргументами. Вместо этого она получает в стеке локальные копии этих объектов. Изменение значений копий никак не отражается на значениях самих объектов. Локальные копии теряются при выходе из функции.
 Значения аргументов при передаче по значению не меняются. Следовательно, программист не должен заботиться о сохранении и восстановлении их значений при вызове функции. Без этого механизма любой вызов мог бы привести к нежелательному изменению аргументов, не объявленных константными явно. Передача по значению освобождает человека от лишних забот в наиболее типичной ситуации.
 Однако такой способ передачи аргументов может не устраивать нас в следующих случаях:
 передача большого объекта типа класса. Временные и пространственные расходы на размещение и копирование такого объекта могут оказаться неприемлемыми для реальной программы;
 иногда значения аргументов должны быть модифицированы внутри функции. Например, swap() должна обменять значения своих аргументов, что невозможно при передаче по значению:
 // swap() не меняет значений своих аргументов!
 void swap( int vl, int v2 ) {
  int tmp = v2;
  v2 = vl;
  vl = tmp;
 }
 swap() обменивает значения локальных копий своих аргументов. Те же переменные, что были использованы в качестве аргументов при вызове, остаются неизменными. Это можно проиллюстрировать, написав небольшую программу:
 #include
 void swap( int, int );
 
 int main() {
  int i = 10;
  int j = 20;
 
  cout << "Перед swap():\ti: "
  << i << "\tj: " << j << endl;
 
  swap( i, j );
 
  cout << "После swap():\ti: "
  << i << "\tj: " << j << endl;
 
  return 0;
 }
 Результат выполнения программы:
 
 Перед swap(): i: 10 j: 20
 После swap(): i: 10 j: 20
 
 Достичь желаемого можно двумя способами. Первый – объявление параметров указателями. Вот как будет выглядеть реализация swap() в этом случае:
 // pswap() обменивает значения объектов,
 // адресуемых указателями vl и v2
 void pswap( int *vl, int *v2 ) {
  int tmp = *v2;
  *v2 = *vl;
  *vl = tmp;
 }
 Функция main() тоже нуждается в модификации. Вместо передачи самих объектов необходимо передавать их адреса:
 pswap( &i, &j );
 Теперь программа работает правильно:
 
 Перед swap(): i: 10 j: 20
 После swap(): i: 20 j: 10
 
 Альтернативой может стать объявление параметров ссылками. В данном случае реализация swap() выглядит так:
 // rswap() обменивает значения объектов,
 // на которые ссылаются vl и v2
 void rswap( int &vl, int &v2 ) {
  int tmp = v2;
  v2 = vl;
  vl = tmp;
 }
 Вызов этой функции из main() аналогичен вызову первоначальной функции swap():
 rswap( i, j );
 Выполнив программу main(), мы снова получим верный результат.
 7.3.1. Параметры-ссылки
 Использование ссылок в качестве параметров модифицирует стандартный механизм передачи по значению. При такой передаче функция манипулирует локальными копиями аргументов. Используя параметры-ссылки, она получает l-значения своих аргументов и может изменять их.
 В каких случаях применение параметров-ссылок оправданно? Во-первых, тогда, когда без использования ссылок пришлось бы менять типы параметров на указатели (см. приведенную выше функцию swap()). Во-вторых, при необходимости вернуть из функции несколько значений. В-третьих, для передачи большого объекта типа класса. Рассмотрим два последних случая подробнее.
 Как пример функции, использующей параметр-ссылку для возврата дополнительного значения, возьмем look_up(), которая будет искать заданную величину в векторе целых чисел. В случае успеха look_up() вернет итератор, указывающий на найденный элемент, иначе – на элемент, расположенный за конечным. Если величина содержится в векторе несколько раз, итератор будет указывать на первое вхождение. Кроме того, дополнительный параметр-ссылка occurs возвращает количество найденных элементов.
 #include
 
 // параметр-ссылка 'occurs'
 // содержит второе возвращаемое значение
 
 vector::const_iterator look_up(
  const vector &vec,
 
  int value, // искомое значение
  int &occurs ) // количество вхождений
 {
  // res_iter инициализируется значением
  // следующего за конечным элемента
  vector::const_iterator res_iter = vec.end();
  occurs = 0;
 
  for ( vector::const_iterator iter = vec.begin();
  iter != vec.end();
  ++iter )
  if ( *iter == value )
  {
  if ( res_iter == vec.end() )
  res_iter = iter;
  ++occurs;
  }
 
  return res_iter;
 }
 Третий случай, когда использование параметра-ссылки может быть полезно, – это большой объект типа класса в качестве аргумента. При передаче по значению объект будет копироваться целиком при каждом вызове функции, что для больших объектов может привести к потере эффективности. Используя параметр-ссылку, функция получает доступ к той области памяти, где размещен сам объект, без создания дополнительной копии. Например:
 class Huge { public: double stuff[1000]; };
 extern int calc( const Huge & );
 
 int main() {
  Huge table[ 1000 ];
  // ... инициализация table
 
  int sum = 0;
  for ( int ix=0; ix < 1000; ++ix )
  // calc() ссылается на элемент массива
  // типа Huge
  sum += calc( tab1e[ix] );
  // ...
 }
 Может возникнуть желание использовать параметр-ссылку, чтобы избежать создания копии большого объекта, но в то же время не дать вызываемой функции возможности изменять значение аргумента. Если параметр-ссылка не должен модифицироваться внутри функции, то стоит объявить его как ссылку на константу. В такой ситуации компилятор способен распознать и пресечь попытку непреднамеренного изменения значения аргумента.
 В следующем примере нарушается константность параметра xx функции foo(). Поскольку параметр функции foo_bar() не является ссылкой на константу, то нет гарантии, что вызов foo_bar() не изменит значения аргумента. Компилятор сигнализирует об ошибке:
 class X;
 extern int foo_bar( X& );
 
 int foo( const X& xx ) {
  // ошибка: константа передается
  // функции с параметром неконстантного типа
  return foo_bar( xx );
 }
 Для того чтобы программа компилировалась, мы должны изменить тип параметра foo_bar(). Подойдет любой из следующих двух вариантов:
 extern int foo_bar( const X& );
 extern int foo_bar( X ); // передача по значению
 Вместо этого можно передать копию xx, которую позволено менять:
 int foo( const X &xx ) {
  // ...
  X x2 = xx; // создать копию значения
 
  // foo_bar() может поменять x2,
  // xx останется нетронутым
  return foo_bar( x2 ); // правильно
 }
 Параметр-ссылка может именовать любой встроенный тип данных. В частности, разрешается объявить параметр как ссылку на указатель, если программист хочет изменить значение самого указателя, а не объекта, который он адресует. Вот пример функции, обменивающей друг с другом значения двух указателей:
 void ptrswap( int *&vl, int *&v2 ) {
  int *trnp = v2;
  v2 = vl;
  vl = tmp;
 }
 Объявление
 int *&v1;
 должно читаться справа налево: v1 является ссылкой на указатель на объект типа int. Модифицируем функцию main(), которая вызывала rswap(), для проверки работы ptrswap():
 #include
 void ptrswap( int *&vl, int *&v2 );
 
 int main() {
  int i = 10;
  int j = 20;
 
  int *pi = &i;
  int *pj = &j;
 
  cout << "Перед ptrswap():\tpi: "
  << *pi << "\tpj: " << *pj << endl;
 
  ptrswap( pi, pj );
  cout << "После ptrswap():\tpi: "
  << *pi << "\tpj: " << pj << endl;
 
  return 0;
 }
 Вот результат работы программы:
 
 Перед ptrswap(): pi: 10 pj: 20
 После ptrswap(): pi: 20 pj: 10
 
 7.3.2. Параметры-ссылки и параметры-указатели
 Когда же лучше использовать параметры-ссылки, а когда – параметры-указатели? В конце концов, и те и другие позволяют функции модифицировать объекты, эффективно передавать в функцию большие объекты типа класса. Что выбрать: объявить параметр ссылкой или указателем?
 Как было сказано в разделе 3.6, ссылка может быть один раз инициализирована значением объекта, и впоследствии изменить ее нельзя. Указатель же в течение своей жизни способен адресовать разные объекты или не адресовать вообще.
 Поскольку указатель может содержать, а может и не содержать адрес какого-либо объекта, перед его использованием функция должна проверить, не равен ли он нулю:
 class X;
 void manip( X *px )
 {
  // проверим на 0 перед использованием
  if ( px != 0 )
  // обратимся к объекту по адресу...
 }
 Параметр-ссылка не нуждается в этой проверке, так как всегда существует именуемый ею объект. Например:
 class Type { };
 void operate( const Type& p1, const Type& p2 );
 
 int main() {
  Type obj1;
  // присвоим objl некоторое значение
 
  // ошибка: ссылка не может быть равной 0
  Type obj2 = operate( objl, 0 );
 }
 Если параметр должен ссылаться на разные объекты во время выполнения функции или принимать нулевое значение (ни на что не ссылаться), нам следует использовать указатель.
 Одна из важнейших сфер применения параметров-ссылок – эффективная реализация перегруженных операций. При этом использование операций остается простым и интуитивно понятным. (Подробнее данный вопрос рассматривается в главе 15.) Разберем маленький пример. Представим себе класс Matrix (матрица). Хорошо бы реализовать операции сложения и присваивания “привычным” способом:
 Matrix a, b, c;
 c = a + b;
 Эти операции реализуются с помощью перегруженных операторов – функций с немного необычным именем. Для оператора сложения такая функция будет называться operator+. Посмотрим, как ее определить:
 Matrix // тип возврата - Matrix
 operator+( // имя перегруженного оператора
 Matrix m1, // тип левого операнда
 Matrix m2 // тип правого операнда
 )
 {
  Matrix result;
  // необходимые действия
  return result;
 }
 При такой реализации сложение двух объектов типа Matrix выглядит вполне привычно:
 a + b;
 но, к сожалению, оказывается совершенно неэффективным. Заметим, что параметры у нас передаются по значению. Содержимое двух матриц будет копироваться в область активации функции operator+(), а поскольку объекты типа Matrix весьма велики, затраты времени и памяти на создание копий могут быть совершенно неприемлемыми.
 Представим себе, что мы решили использовать указатели в качестве параметров, чтобы избежать этих затрат. Вот модифицированный код operator+():
 // реализация с параметрами-указателями
 operator+( Matrix *ml, Matrix *m2 )
 {
  Matrix result;
  // необходимые действия
  return result;
 }
 Да, мы добились эффективной реализации, но зато теперь применение нашей операции вряд ли можно назвать интуитивно понятным. В качестве значений параметров-указателей требуется передавать адреса складываемых объектов. Поэтому для сложения двух матриц пришлось бы написать:
 &a + &b; // допустимо, хотя и плохо
 Хотя такая форма не может не вызвать критику, но все-таки два объекта сложить еще удается. А вот три уже крайне затруднительно:
 // а вот это не работает
 // &a + &b возвращает объект типа Matrix
 &a + &b + &c;
 Для того чтобы сложить три объекта, при подобной реализации нужно написать так:
 // правильно: работает, однако ...
 &( &a + &b ) + &c;
 Трудно ожидать, что кто-нибудь согласится писать такие выражения. К счастью, параметры-ссылки дают именно то решение, которое требуется. Если параметр объявлен как ссылка, функция получает его l-значение, а не копию. Лишнее копирование исключается. И тип фактического аргумента может быть Matrix – это упрощает операцию сложения, как и для встроенных типов. Вот схема перегруженного оператора сложения для класса Matrix:
 // реализация с параметрами-ссылками
 operator+( const Matrix &m1, const Matrix &m2 )
 {
  Matrix result;
  // необходимые действия
  return result;
 }
 При такой реализации сложение трех объектов Matrix выглядит вполне привычно:
 a + b + c;
 Ссылки были введены в С++ именно для того, чтобы удовлетворить двум требованиям: эффективная реализация и интуитивно понятное применение.
 7.3.3. Параметры-массивы
 Массив в С++ никогда не передается по значению, а только как указатель на его первый, точнее нулевой, элемент. Например, объявление
 void putValues( int[ 10 ] );
 рассматривается компилятором так, как будто оно имеет вид
 void putValues( int* );
 Размер массива неважен при объявлении параметра. Все три приведенные записи эквивалентны:
 // три эквивалентных объявления putValues()
 void putValues( int* );
 void putValues( int[] );
 void putValues( int[ 10 ] );
 Передача массивов как указателей имеет следующие особенности:
 изменение значения аргумента внутри функции затрагивает сам переданный объект, а не его локальную копию. Если такое поведение нежелательно, программист должен позаботиться о сохранении исходного значения. Можно также при объявлении функции указать, что она не должна изменять значение параметра, объявив этот параметр константой:
 void putValues( const int[ 10 ] );
 размер массива не является частью типа параметра. Поэтому функция не знает реального размера передаваемого массива. Компилятор тоже не может это проверить. Рассмотрим пример:
 void putValues( int[ 10 ] ); // рассматривается как int*
 int main() {
  int i, j [ 2 ];
  putValues( &i ); // правильно: &i is int*;
  // однако при выполнении возможна ошибка
  putValues( j ); // правильно: j - адрес 0-го элемента - int*;
  // однако при выполнении возможна ошибка
 При проверке типов параметров компилятор способен распознать, что в обоих случаях тип аргумента int* соответствует объявлению функции. Однако контроль за тем, не является ли аргумент массивом, не производится.
 По принятому соглашению C-строка является массивом символов, последний элемент которого равен нулю. Во всех остальных случаях при передаче массива в качестве параметра необходимо указывать его размер. Это относится и к массивам символов, внутри которых встречается 0. Обычно для такого указания используют дополнительный параметр функции. Например:

<< Пред.           стр. 32 (из 121)           След. >>

Список литературы по разделу