Язык программирования C++ |
Распределение памяти
Распределение памяти при передаче аргументов функции
Рассказывая о функциях, мы отметили, что у функций (как и у методов классов) есть аргументы, фактические значения которых передаются при вызове функции.
Рассмотрим более подробно метод Add класса Complex. Изменим его немного, так, чтобы он вместо изменения состояния объекта возвращал результат операции сложения:
Complex Complex::Add(Complex x) { Complex result; result.real = real + x.real; result.imaginary = imaginary + x.imaginary; return result; }
При вызове этого метода
Complex n1; Complex n2; . . . Complex n3 = n1.Add(n2);
значение переменной n2 передается в качестве аргумента. Компилятор создает временную переменную типа Complex, копирует в нее значение n2 и передает эту переменную в метод Add. Такая передача аргумента называется передачей по значению. У передачи аргументов по значению имеется два свойства. Во-первых, эта операция не очень эффективна, особенно если объект сложный и требует большого объема памяти или же если создание объекта сопряжено с выполнением сложных действий (о конструкторах объектов будет рассказано в лекции 12). Во-вторых, изменения аргумента функции не сохраняются. Если бы метод Add был бы определен как
Complex Complex::Add(Complex x) { Complex result; x.imaginary = 0; // изменение аргумента метода result.real = real + x.real; result.imaginary = imaginary + x.imaginary; return result; }
то при вызове n3 = n1.Add(n2) результат был бы, конечно, другой, но значение переменной n2 не изменилось бы. Хотя в данном примере изменяется значение аргумента метода Add, этот аргумент – лишь копия объекта n2, а не сам объект. По завершении выполнения метода Add его аргументы просто уничтожаются, и первоначальные значения фактических параметров сохраняются.
При возврате результата функции выполняются те же действия, т.е. создается временная переменная, в которую копируется результат, и уже затем значение временной переменной копируется в переменную n3. Временные переменные потому и называют временными, что компилятор сам создает их на время выполнения метода и сам их уничтожает.
Другим способом передачи аргументов является передача по ссылке. Если изменить описание метода Add на
Complex Complex::Add(Complex& x) { Complex result; result.real = real + x.real; result.imaginary = imaginary + x.imaginary; return result; }
то при вызове n3 = n1.Add(n2) компилятор будет создавать ссылку на переменную n2 и передавать ее методу Add. В большинстве случаев это намного эффективнее, так как для ссылки требуется немного памяти и создать ее проще. Однако мы получим нежелательный в данном случае эффект. Метод
Complex Complex::Add(Complex& x) { Complex result; x.imaginary = 0; // изменение значения // по переданной ссылке result.real = real + x.real; result.imaginary = imaginary + x.imaginary; return result; }
изменит значение переменной n2. Операция Add не предусматривает изменения собственных операндов. Чтобы избежать ошибок, лучше записать аргумент с описателем const, который определяет соответствующую переменную как неизменяемую.
Complex::Add(const Complex& x)
В таком случае попытка изменить значение аргумента будет обнаружена на этапе компиляции, и компилятор выдаст ошибку. Передачей аргумента по неконстантной ссылке можно воспользоваться в том случае, когда функция действительно должна изменить свой аргумент. Например, метод Coord класса Figure записывает координаты некой фигуры в свои аргументы:
void Figure::Coord(int& x, int& y) { x = coordx; y = coordy; }
При вызове
int cx, cy; Figure fig; . . . fig.Coord(cx, cy);
переменным cx и cy будет присвоено значение координат фигуры fig.
Вернемся к методу Add и попытаемся оптимизировать передачу вычисленного значения. Простое на первый взгляд решение возвращать ссылку на результат не работает:
Complex& Complex::Add(const Complex& x) { Complex result; result.real = real + x.real; result.imaginary = imaginary + x.imaginary; return result; }
При выходе из метода автоматическая переменная result уничтожается, и память, выделенная для нее, освобождается. Поэтому результат Add – ссылка на несуществующую память. Результат подобных действий непредсказуем. Иногда программа будет работать как ни в чем не бывало, иногда может произойти сбой, иногда результат будет испорчен. Однако возвращение результата по ссылке возможно, если объект, на который эта ссылка ссылается, не уничтожается после выхода из функции или метода. Если метод Add прибавляет значение аргумента к текущему значению объекта и возвращает новое значение в качестве результата, то его можно записать:
Complex& Complex::Add(const Complex& x) { real += x.real; imaginary += x.imaginary; return *this; // передать ссылку на текущий объект }
Как и в случае с аргументом, передача ссылки на текущий объект позволяет использовать метод Add слева от операции присваивания, например в следующем выражении:
x.Add(y) = z;
К значению объекта x прибавляется значение y, а затем результату присваивается значение z (фактически это эквивалентно x = z ). Чтобы запретить подобные конструкции, достаточно добавить описатель const перед типом возвращаемого значения:
const Complex& Complex::Add(const Complex& x) . . .
Передача аргументов и результата по ссылке аналогична передаче указателя в качестве аргумента:
Complex* Complex::Add(Complex* x) { real += x->real; imaginary += x->imaginary; return this; }
Если нет особых оснований использовать в качестве аргумента или результата именно указатель, передача по ссылке предпочтительней. Во-первых, проще запись операций, а во-вторых, обращения по ссылке легче контролировать.
Рекомендации по передаче аргументов
- Встроенные типы лучше передавать по значению. С точки зрения эффективности разницы практически нет, поскольку встроенные типы занимают минимальную память, и создание временных переменных и копирование их значений выполняется быстро.
- Если в функции или методе значение аргумента используется, но не изменяется, передавайте аргумент по неизменяемой ссылке.
- Передачу изменяемой ссылки необходимо применять только тогда, когда функция должна изменить переменную, ссылка на которую передается.
- Передача по указателю используется, только если функции нужен именно указатель, а не значение объекта.