Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

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

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 служит именем параметра, при конкретизации шаблона функции

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

332

putValues() он заменяется на реальный тип int, double, string и т.д. (В главе 10 мы продолжим разговор о шаблонах функций.)

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

putValues( int matrix[][10], int rowSize );

Здесь matrix объявляется как двумерный массив, который содержит десять столбцов и неизвестное число строк. Эквивалентным объявлением для matrix будет:

int (*matrix)[10]

Многомерный массив передается как указатель на его нулевой элемент. В нашем случае тип matrix указатель на массив из десяти элементов типа int. Как и для одномерного массива, граница первого измерения не учитывается при проверке типов. Если параметры являются многомерными массивами, то контролируются все измерения, кроме первого.

Заметим, что скобки вокруг *matrix необходимы из-за более высокого приоритета операции взятия индекса. Инструкция

int *matrix[10];

объявляет matrix как массив из десяти указателей на int.

7.3.4. Абстрактные контейнерные типы в качестве параметров

Абстрактные контейнерные типы, представленные в главе 6, также используются для объявления параметров функции. Например, можно определить putValues() как имеющую параметр типа vector<int> вместо встроенного типа массива.

Контейнерный тип является классом и обеспечивает значительно большую функциональность, чем встроенные массивы. Так, vector<int> знаетсобственный размер. В предыдущем подразделе мы видели, что размер параметра-массива неизвестен функции и для его передачи приходится задавать дополнительный параметр. Использование vector<int> позволяет обойти это ограничение. Например, можно изменить определение нашей putValues() на такое:

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

333

#include <iostream> #include <vector>

const lineLength =12; // количество элементов в строке void putValues( vector<int> vec )

{

cout << "( " << vec.size() << " )< "; for ( int i = 0; i < vec.size(); ++1 ) {

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

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

cout << vec[ i ];

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

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

if ( 1 % lineLength != lineLength-1 && i != vec.size()-1 )

cout << ", ";

}

cout << " >\n";

}

void putValues( vector<int> ); int main() {

int i, j[ 2 ];

// присвоить i и j некоторые значения

vector<int> vec1(1); // создадим вектор из 1 элемента vecl[0] = i;

putValues( vecl );

vector<int> vec2; // создадим пустой вектор // добавим элементы к vec2

for ( int ix = 0;

ix < sizeof( j ) / sizeof( j[0] ); ++ix )

// vec2[ix] == j [ix] vec2.push_back( j[ix] );

putValues( vec2 ); return 0;

Функция main(), вызывающая нашу новую функцию putValues(), выглядит так:

}

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

Как бы вы изменили объявление putValues()?

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

void putValues( const vector<int> & ) { ...