Главная страница
Навигация по странице:

  • Параметры-ссылки

  • 7.3.2. Параметры-ссылки и параметры-указатели

  • Параметры-массивы

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

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница30 из 93
    1   ...   26   27   28   29   30   31   32   33   ...   93
    322
    }
    Результат выполнения программы:
    Перед swap(): i: 10 j: 20
    После swap(): i: 10 j: 20
    Достичь желаемого можно двумя способами. Первый – объявление параметров указателями. Вот как будет выглядеть реализация swap() в этом случае:
    }
    Функция main() тоже нуждается в модификации. Вместо передачи самих объектов необходимо передавать их адреса: pswap( &i, &j );
    Теперь программа работает правильно:
    Перед swap(): i: 10 j: 20
    После swap(): i: 20 j: 10
    Альтернативой может стать объявление параметров ссылками. В данном случае реализация 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;
    // pswap() обменивает значения объектов,
    // адресуемых указателями vl и v2 void pswap( int *vl, int *v2 ) { int tmp = *v2;
    *v2 = *vl;
    *vl = tmp;

    С++ для начинающих
    323
    }
    Вызов этой функции из main() аналогичен вызову первоначальной функции swap(): rswap( i, j );
    Выполнив программу main(), мы снова получим верный результат.
    7.3.1.
    Параметры-ссылки
    Использование ссылок в качестве параметров модифицирует стандартный механизм передачи по значению. При такой передаче функция манипулирует локальными копиями аргументов. Используя параметры-ссылки, она получает l-значения своих аргументов и может изменять их.
    В каких случаях применение параметров-ссылок оправданно? Во-первых, тогда, когда без использования ссылок пришлось бы менять типы параметров на указатели (см. приведенную выше функцию swap()). Во-вторых, при необходимости вернуть из функции несколько значений. В-третьих, для передачи большого объекта типа класса.
    Рассмотрим два последних случая подробнее.
    Как пример функции, использующей параметр-ссылку для возврата дополнительного значения, возьмем look_up(), которая будет искать заданную величину в векторе целых чисел. В случае успеха look_up() вернет итератор, указывающий на найденный элемент, иначе – на элемент, расположенный за конечным. Если величина содержится в векторе несколько раз, итератор будет указывать на первое вхождение. Кроме того, дополнительный параметр-ссылка occurs возвращает количество найденных элементов.
    // rswap() обменивает значения объектов,
    // на которые ссылаются vl и v2 void rswap( int &vl, int &v2 ) { int tmp = v2; v2 = vl; vl = tmp;

    С++ для начинающих
    324
    }
    Третий случай, когда использование параметра-ссылки может быть полезно, – это большой объект типа класса в качестве аргумента. При передаче по значению объект будет копироваться целиком при каждом вызове функции, что для больших объектов может привести к потере эффективности. Используя параметр-ссылку, функция получает доступ к той области памяти, где размещен сам объект, без создания дополнительной копии. Например:
    }
    Может возникнуть желание использовать параметр-ссылку, чтобы избежать создания копии большого объекта, но в то же время не дать вызываемой функции возможности изменять значение аргумента. Если параметр-ссылка не должен модифицироваться внутри функции, то стоит объявить его как ссылку на константу. В такой ситуации
    #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] );
    // ...

    С++ для начинающих
    325
    компилятор способен распознать и пресечь попытку непреднамеренного изменения значения аргумента.
    В следующем примере нарушается константность параметра xx функции foo().
    Поскольку параметр функции foo_bar() не является ссылкой на константу, то нет гарантии, что вызов foo_bar() не изменит значения аргумента. Компилятор сигнализирует об ошибке:
    }
    Для того чтобы программа компилировалась, мы должны изменить тип параметра foo_bar()
    . Подойдет любой из следующих двух вариантов: extern int foo_bar( X ); // передача по значению
    Вместо этого можно передать копию xx, которую позволено менять:
    }
    Параметр-ссылка может именовать любой встроенный тип данных. В частности, разрешается объявить параметр как ссылку на указатель, если программист хочет изменить значение самого указателя, а не объекта, который он адресует. Вот пример функции, обменивающей друг с другом значения двух указателей:
    }
    Объявление int *&v1; class X; extern int foo_bar( X& ); int foo( const X& xx ) {
    // ошибка: константа передается
    // функции с параметром неконстантного типа return foo_bar( xx ); extern int foo_bar( const X& ); 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;

    С++ для начинающих
    326
    должно читаться справа налево: v1 является ссылкой на указатель на объект типа int.
    Модифицируем функцию main(), которая вызывала rswap(), для проверки работы ptrswap()
    :
    }
    Вот результат работы программы:
    Перед ptrswap(): pi: 10 pj: 20
    После ptrswap(): pi: 20 pj: 10
    7.3.2.
    Параметры-ссылки и параметры-указатели
    Когда же лучше использовать параметры-ссылки, а когда – параметры-указатели? В конце концов, и те и другие позволяют функции модифицировать объекты, эффективно передавать в функцию большие объекты типа класса. Что выбрать: объявить параметр ссылкой или указателем?
    Как было сказано в разделе 3.6, ссылка может быть один раз инициализирована значением объекта, и впоследствии изменить ее нельзя. Указатель же в течение своей жизни способен адресовать разные объекты или не адресовать вообще.
    Поскольку указатель может содержать, а может и не содержать адрес какого-либо объекта, перед его использованием функция должна проверить, не равен ли он нулю:
    }
    #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; class X; void manip( X *px )
    {
    // проверим на 0 перед использованием if ( px != 0 )
    // обратимся к объекту по адресу...

    С++ для начинающих
    327
    Параметр-ссылка не нуждается в этой проверке, так как всегда существует именуемый ею объект. Например:
    }
    Если параметр должен ссылаться на разные объекты во время выполнения функции или принимать нулевое значение (ни на что не ссылаться), нам следует использовать указатель.
    Одна из важнейших сфер применения параметров-ссылок – эффективная реализация перегруженных операций. При этом использование операций остается простым и интуитивно понятным. (Подробнее данный вопрос рассматривается в главе 15.) Разберем маленький пример. Представим себе класс Matrix (матрица). Хорошо бы реализовать операции сложения и присваивания “привычным” способом: c = a + b;
    Эти операции реализуются с помощью перегруженных операторов – функций с немного необычным именем. Для оператора сложения такая функция будет называться operator+
    . Посмотрим, как ее определить:
    }
    При такой реализации сложение двух объектов типа Matrix выглядит вполне привычно: a + b; но, к сожалению, оказывается совершенно неэффективным. Заметим, что параметры у нас передаются по значению. Содержимое двух матриц будет копироваться в область активации функции operator+(), а поскольку объекты типа Matrix весьма велики, затраты времени и памяти на создание копий могут быть совершенно неприемлемыми. class Type { }; void operate( const Type& p1, const Type& p2 ); int main() {
    Type obj1;
    // присвоим objl некоторое значение
    // ошибка: ссылка не может быть равной 0
    Type obj2 = operate( objl, 0 );
    Matrix a, b, c;
    Matrix // тип возврата - Matrix operator+( // имя перегруженного оператора
    Matrix m1, // тип левого операнда
    Matrix m2 // тип правого операнда
    )
    {
    Matrix result;
    // необходимые действия return result;

    С++ для начинающих
    328
    Представим себе, что мы решили использовать указатели в качестве параметров, чтобы избежать этих затрат. Вот модифицированный код operator+():
    }
    Да, мы добились эффективной реализации, но зато теперь применение нашей операции вряд ли можно назвать интуитивно понятным. В качестве значений параметров- указателей требуется передавать адреса складываемых объектов. Поэтому для сложения двух матриц пришлось бы написать:
    &a + &b; // допустимо, хотя и плохо
    Хотя такая форма не может не вызвать критику, но все-таки два объекта сложить еще удается. А вот три уже крайне затруднительно:
    &a + &b + &c;
    Для того чтобы сложить три объекта, при подобной реализации нужно написать так:
    &( &a + &b ) + &c;
    Трудно ожидать, что кто-нибудь согласится писать такие выражения. К счастью, параметры-ссылки дают именно то решение, которое требуется. Если параметр объявлен как ссылка, функция получает его l-значение, а не копию. Лишнее копирование исключается. И тип фактического аргумента может быть Matrix – это упрощает операцию сложения, как и для встроенных типов. Вот схема перегруженного оператора сложения для класса Matrix:
    }
    При такой реализации сложение трех объектов Matrix выглядит вполне привычно:
    // реализация с параметрами-указателями operator+( Matrix *ml, Matrix *m2 )
    {
    Matrix result;
    // необходимые действия return result;
    // а вот это не работает
    // &a + &b возвращает объект типа Matrix
    // правильно: работает, однако ...
    // реализация с параметрами-ссылками operator+( const Matrix &m1, const Matrix &m2 )
    {
    Matrix result;
    // необходимые действия return result;

    С++ для начинающих
    329
    a + b + c;
    Ссылки были введены в С++ именно для того, чтобы удовлетворить двум требованиям: эффективная реализация и интуитивно понятное применение.
    7.3.3.
    Параметры-массивы
    Массив в С++ никогда не передается по значению, а только как указатель на его первый, точнее нулевой, элемент. Например, объявление void putValues( int[ 10 ] ); рассматривается компилятором так, как будто оно имеет вид void putValues( int* );
    Размер массива неважен при объявлении параметра. Все три приведенные записи эквивалентны: void putValues( int[ 10 ] );
    Передача массивов как указателей имеет следующие особенности:

    изменение значения аргумента внутри функции затрагивает сам переданный объект, а не его локальную копию. Если такое поведение нежелательно, программист должен позаботиться о сохранении исходного значения. Можно также при объявлении функции указать, что она не должна изменять значение параметра, объявив этот параметр константой: void putValues( const int[ 10 ] );

    размер массива не является частью типа параметра. Поэтому функция не знает реального размера передаваемого массива. Компилятор тоже не может это проверить. Рассмотрим пример:
    // однако при выполнении возможна ошибка
    При проверке типов параметров компилятор способен распознать, что в обоих случаях тип аргумента int* соответствует объявлению функции. Однако контроль за тем, не является ли аргумент массивом, не производится.
    // три эквивалентных объявления putValues() void putValues( int* ); void putValues( int[] ); void putValues( int[ 10 ] ); // рассматривается как int* int main() { int i, j [ 2 ]; putValues( &i ); // правильно: &i is int*;
    // однако при выполнении возможна ошибка putValues( j ); // правильно: j - адрес 0-го элемента - int*;

    С++ для начинающих
    330
    По принятому соглашению C-строка является массивом символов, последний элемент которого равен нулю. Во всех остальных случаях при передаче массива в качестве параметра необходимо указывать его размер. Это относится и к массивам символов, внутри которых встречается 0. Обычно для такого указания используют дополнительный параметр функции. Например:
    } putValues()
    печатает элементы массива в следующем формате:
    ( 10 )< 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 > где 10 – это размер массива. Вот как выглядит реализация putValues(), в которой используется дополнительный параметр:
    }
    Другой способ сообщить функции размер массива-параметра – объявить параметр как ссылку. В этом случае размер становится частью типа, и компилятор может проверить аргумент в полной мере. void putValues( int[], int size ); int main() { int i, j[ 2 ]; putValues( &i, 1 ); putValues( j, 2 ); return 0;
    #include const lineLength =12; // количество элементов в строке void putValues( int *ia, int sz )
    { cout << "( " << sz << " )< "; for (int i=0;i { if ( i % lineLength == 0 && i ) cout << "\n\t"; // строка заполнена cout << ia[ i ];
    // разделитель, печатаемый после каждого элемента,
    // кроме последнего if ( i % lineLength != lineLength-1 && i != sz-1 ) cout << ", ";
    } cout << " >\n";

    С++ для начинающих
    331
    }
    Поскольку размер массива теперь является частью типа параметра, новая версия putValues()
    способна работать только с массивами из 10 элементов. Конечно, это ограничивает ее область применения, зато реализация значительно проще:
    }
    Еще один способ получить размер переданного массива в функции – использовать абстрактный контейнерный тип. (Такие типы были представлены в главе 6. В следующем подразделе мы поговорим об этом подробнее.)
    Хотя две предыдущих реализации putValues() правильны, они обладают серьезными недостатками. Так, первый вариант работает только с массивами типа int. Для типа double*
    нужно писать другую функцию, для long* – еще одну и т.д. Второй вариант производит операции только над массивом из 10 элементов типа int. Для обработки массивов разного размера нужны дополнительные функции. Лучшим решением было бы использовать шаблон – функцию, или, скорее, обобщенную реализацию кода целого семейства функций, которые отличаются только типами обрабатываемых данных. Вот как можно сделать из первого варианта putValues() шаблон, способный работать с массивами разных типов и размеров:
    }
    Параметры шаблона заключаются в угловые скобки. Ключевое слово class означает, что идентификатор Type служит именем параметра, при конкретизации шаблона функции
    // параметр - ссылка на массив из 10 целых void putValues( int (&arr)[10] ); int main() { int i, j [ 2 ]; putValues(i); // ошибка:
    // аргумент не является массивом из 10 целых putValues(j); // ошибка:
    // аргумент не является массивом из 10 целых return 0;
    #include void putValues( int (&ia)[10] )
    { cout << "( 10 )< "; for ( int 1 =0; i < 10; ++i ) { cout << ia[ i ];
    // разделитель, печатаемый после каждого элемента,
    // кроме последнего if ( i != 9 ) cout << ", ";
    } cout << " >\n"; template void putValues( Type *ia, int sz )
    {
    // так же, как и раньше

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

    С++ для начинающих
    333
    }
    Функция main(), вызывающая нашу новую функцию putValues(), выглядит так:
    }
    Заметим, что параметр putValues()передается по значению. В подобных случаях контейнер со всеми своими элементами всегда копируется в стек вызванной функции.
    Поскольку операция копирования весьма неэффективна, такие параметры лучше объявлять как ссылки.
    Как бы вы изменили объявление putValues()?
    Вспомним, что если функция не модифицирует значение своего параметра, то предпочтительнее, чтобы он был ссылкой на константный тип: void putValues( const vector & ) { ...
    #include
    #include const lineLength =12; // количество элементов в строке void putValues( vector 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 main() { int i, j[ 2 ];
    // присвоить i и j некоторые значения vector vec1(1); // создадим вектор из 1 элемента vecl[0] = i; putValues( vecl ); vector 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;

    С++ для начинающих
    1   ...   26   27   28   29   30   31   32   33   ...   93


    написать администратору сайта