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

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

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

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

322

#include <iostream> 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

Достичь желаемого можно двумя способами. Первый объявление параметров

//pswap() обменивает значения объектов,

//адресуемых указателями vl и v2

void pswap( int *vl, int *v2 ) { int tmp = *v2;

*v2 = *vl; *vl = tmp;

указателями. Вот как будет выглядеть реализация swap() в этом случае:

}

Функция main() тоже нуждается в модификации. Вместо передачи самих объектов необходимо передавать их адреса:

pswap( &i, &j );

Теперь программа работает правильно:

Перед

swap():

i:

10

j:

20

После

swap():

i:

20

j:

10

Альтернативой может стать объявление параметров ссылками. В данном случае реализация swap() выглядит так:

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

323

//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 возвращает количество найденных элементов.

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

324

#include <vector>

//параметр-ссылка 'occurs'

//содержит второе возвращаемое значение

vector<int>::const_iterator look_up( const vector<int> &vec,

int

value,

//

искомое значение

int

&occurs )

//

количество вхождений

{

//res_iter инициализируется значением

//следующего за конечным элемента

vector<int>::const_iterator res_iter = vec.end(); occurs = 0;

for ( vector<int>::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] );

// ...

копии. Например:

}

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

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

325

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

В следующем примере нарушается константность параметра xx функции foo(). Поскольку параметр функции foo_bar() не является ссылкой на константу, то нет гарантии, что вызов foo_bar() не изменит значения аргумента. Компилятор

class X;

extern int foo_bar( X& );

int foo( const X& xx ) {

//ошибка: константа передается

//функции с параметром неконстантного типа return foo_bar( xx );

сигнализирует об ошибке:

}

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

extern int foo_bar( const X& );

foo_bar(). Подойдет любой из следующих двух вариантов: extern int foo_bar( X ); // передача по значению

int foo( const X &xx ) { // ...

X x2 = xx; // создать копию значения

//foo_bar() может поменять x2,

//xx останется нетронутым

return foo_bar( x2 ); // правильно

Вместо этого можно передать копию xx, которую позволено менять:

}

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

void ptrswap( int *&vl, int *&v2 ) { int *trnp = v2;

v2 = vl; vl = tmp;

функции, обменивающей друг с другом значения двух указателей:

}

Объявление

int *&v1;

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

326

должно читаться справа налево: v1 является ссылкой на указатель на объект типа int. Модифицируем функцию main(), которая вызывала rswap(), для проверки работы

#include <iostream>

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():

}

Вот результат работы программы:

Перед

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 )

// обратимся к объекту по адресу...

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

}

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

327

Параметр-ссылка не нуждается в этой проверке, так как всегда существует именуемый ею

 

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;

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

Matrix

// тип возврата - Matrix

operator+(

// имя перегруженного оператора

Matrix m1,

// тип левого операнда

Matrix m2

// тип правого операнда

)

 

{

 

Matrix result;

// необходимые действия return result;

operator+. Посмотрим, как ее определить:

}

При такой реализации сложение двух объектов типа Matrix выглядит вполне привычно:

a + b;

но, к сожалению, оказывается совершенно неэффективным. Заметим, что параметры у нас передаются по значению. Содержимое двух матриц будет копироваться в область активации функции operator+(), а поскольку объекты типа Matrix весьма велики, затраты времени и памяти на создание копий могут быть совершенно неприемлемыми.

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

328

Представим себе, что мы решили использовать указатели в качестве параметров, чтобы

// реализация с параметрами-указателями operator+( Matrix *ml, Matrix *m2 )

{

Matrix result;

// необходимые действия return result;

избежать этих затрат. Вот модифицированный код operator+():

}

Да, мы добились эффективной реализации, но зато теперь применение нашей операции вряд ли можно назвать интуитивно понятным. В качестве значений параметров- указателей требуется передавать адреса складываемых объектов. Поэтому для сложения двух матриц пришлось бы написать:

&a + &b; // допустимо, хотя и плохо

Хотя такая форма не может не вызвать критику, но все-таки два объекта сложить еще

//а вот это не работает

//&a + &b возвращает объект типа Matrix

удается. А вот три уже крайне затруднительно:

&a + &b + &c;

// правильно: работает, однако ...

Для того чтобы сложить три объекта, при подобной реализации нужно написать так:

&( &a + &b ) + &c;

Трудно ожидать, что кто-нибудь согласится писать такие выражения. К счастью, параметры-ссылки дают именно то решение, которое требуется. Если параметр объявлен как ссылка, функция получает его l-значение, а не копию. Лишнее копирование исключается. И тип фактического аргумента может быть Matrix это упрощает операцию сложения, как и для встроенных типов. Вот схема перегруженного оператора

// реализация с параметрами-ссылками operator+( const Matrix &m1, const Matrix &m2 )

{

Matrix result;

// необходимые действия return result;

сложения для класса Matrix:

}

При такой реализации сложение трех объектов Matrix выглядит вполне привычно:

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

329

 

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* соответствует объявлению функции. Однако контроль за тем, не является ли аргумент массивом, не производится.

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

330

По принятому соглашению C-строка является массивом символов, последний элемент которого равен нулю. Во всех остальных случаях при передаче массива в качестве параметра необходимо указывать его размер. Это относится и к массивам символов, внутри которых встречается 0. Обычно для такого указания используют дополнительный

void putValues( int[], int size ); int main() {

int i, j[ 2 ]; putValues( &i, 1 ); putValues( j, 2 ); return 0;

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

}

putValues() печатает элементы массива в следующем формате:

( 10 )< 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >

где 10 – это размер массива. Вот как выглядит реализация putValues(), в которой

#include <iostream>

const lineLength =12; // количество элементов в строке void putValues( int *ia, int sz )

{

cout << "( " << sz << " )< "; for (int i=0;i<sz; ++i )

{

if ( i % lineLength == 0 && i )

cout << "\n\t"; // строка заполнена

cout << ia[ i ];

//разделитель, печатаемый после каждого элемента,

//кроме последнего

if ( i % lineLength != lineLength-1 && i != sz-1 )

cout << ", ";

}

cout << " >\n";

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

}

Другой способ сообщить функции размер массива-параметра объявить параметр как ссылку. В этом случае размер становится частью типа, и компилятор может проверить аргумент в полной мере.

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

331

// параметр - ссылка на массив из 10 целых void putValues( int (&arr)[10] );

int main() {

int i, j [ 2 ]; putValues(i); // ошибка:

// аргумент не является массивом из 10 целых putValues(j); // ошибка:

// аргумент не является массивом из 10 целых

return 0;

}

Поскольку размер массива теперь является частью типа параметра, новая версия putValues() способна работать только с массивами из 10 элементов. Конечно, это

#include <iostream>

void putValues( int (&ia)[10] )

{

cout << "( 10 )< ";

for ( int 1 =0; i < 10; ++i ) { cout << ia[ i ];

// разделитель, печатаемый после каждого элемента, // кроме последнего

if ( i != 9 ) cout << ", ";

}

cout << " >\n";

ограничивает ее область применения, зато реализация значительно проще:

}

Еще один способ получить размер переданного массива в функции использовать абстрактный контейнерный тип. (Такие типы были представлены в главе 6. В следующем подразделе мы поговорим об этом подробнее.)

Хотя две предыдущих реализации putValues() правильны, они обладают серьезными недостатками. Так, первый вариант работает только с массивами типа int. Для типа double* нужно писать другую функцию, для long* еще одну и т.д. Второй вариант производит операции только над массивом из 10 элементов типа int. Для обработки массивов разного размера нужны дополнительные функции. Лучшим решением было бы использовать шаблон функцию, или, скорее, обобщенную реализацию кода целого семейства функций, которые отличаются только типами обрабатываемых данных. Вот как можно сделать из первого варианта putValues() шаблон, способный работать с

template <class Type>

void putValues( Type *ia, int sz )

{

// так же, как и раньше

массивами разных типов и размеров:

}

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