Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Параметры-ссылки и параметры-указатели
Когда же лучше использовать параметры-ссылки, а когда – параметры-указатели? В конце концов, и те и другие позволяют функции модифицировать объекты, эффективно передавать в функцию большие объекты типа класса. Что выбрать: объявить параметр ссылкой или указателем? Как было сказано в разделе 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; Ссылки были введены в С++ именно для того, чтобы удовлетворить двум требованиям: эффективная реализация и интуитивно понятное применение. Параметры-массивы Массив в С++ никогда не передается по значению, а только как указатель на его первый, точнее нулевой, элемент. Например, объявление 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. Обычно для такого указания используют дополнительный параметр функции. Например:
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"; } Другой способ сообщить функции размер массива-параметра – объявить параметр как ссылку. В этом случае размер становится частью типа, и компилятор может проверить аргумент в полной мере.
Поскольку размер массива теперь является частью типа параметра, новая версия 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() шаблон, способный работать с массивами разных типов и размеров:
Параметры шаблона заключаются в угловые скобки. Ключевое слово class означает, что идентификатор Type служит именем параметра, при конкретизации шаблона функции putValues() он заменяется на реальный тип – int, double, string и т.д. (В главе 10 мы продолжим разговор о шаблонах функций.) Параметр может быть многомерным массивом. Для такого параметра должны быть заданы правые границы всех измерений, кроме первого. Например: putValues( int matrix[][10], int rowSize ); Здесь matrix объявляется как двумерный массив, который содержит десять столбцов и неизвестное число строк. Эквивалентным объявлением для matrix будет: int (*matrix)[10] Многомерный массив передается как указатель на его нулевой элемент. В нашем случае тип matrix – указатель на массив из десяти элементов типа int. Как и для одномерного массива, граница первого измерения не учитывается при проверке типов. Если параметры являются многомерными массивами, то контролируются все измерения, кроме первого. Заметим, что скобки вокруг *matrix необходимы из-за более высокого приоритета операции взятия индекса. Инструкция int *matrix[10]; объявляет matrix как массив из десяти указателей на int. |
Последнее изменение этой страницы: 2019-04-09; Просмотров: 283; Нарушение авторского права страницы