Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdfС++ для начинающих |
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 служит именем параметра, при конкретизации шаблона функции