Есть еще один способ передачи переменных в функцию в языке C++ — по адресу.
Передача по адресу
Передача аргументов по адресу — это передача адреса переменной-аргумента (а не значения исходной переменной). Поскольку аргумент является адресом, то параметром функции должен быть указатель. Затем функция сможет разыменовать этот указатель для доступа или изменения исходного значения. Вот пример функции, которая принимает параметр, передаваемый по адресу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> void boo(int *ptr) { *ptr = 7; } int main() { int value = 4; std::cout << "value = " << value << '\n'; boo(&value); std::cout << "value = " << value << '\n'; return 0; } |
Результат выполнения программы:
value = 4
value = 7
Как вы можете видеть, функция boo() изменила значение аргумента (переменную value
) через параметр-указатель ptr
. Передачу по адресу обычно используют с указателями на обычные массивы. Например, следующая функция выведет все значения массива:
1 2 3 4 5 |
void printArray(int *array, int length) { for (int index=0; index < length; ++index) std::cout << array[index] << ' '; } |
Вот пример программы, которая вызывает эту функцию:
1 2 3 4 5 |
int main() { int array[7] = { 9, 8, 6, 4, 3, 2, 1 }; // помните, что массивы распадаются в указатели при передаче printArray(array, 7); // поэтому здесь array - это указатель на первый элемент массива (в использовании оператора & нет необходимости) } |
Результат:
9 8 6 4 3 2 1
Помните, что фиксированные массивы распадаются в указатели при передаче в функцию, поэтому их длину нужно передавать в виде отдельного параметра. Перед разыменованием параметров, передаваемых по адресу, не лишним будет проверить — не являются ли они нулевыми указателями. Разыменование нулевого указателя приведет к сбою в программе. Вот функция printArray() с проверкой (обнаружением) нулевых указателей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void printArray(int *array, int length) { // Если пользователь передал нулевой указатель в качестве array if (!array) return; for (int index=0; index < length; ++index) std::cout << array[index] << ' '; } int main() { int array[7] = { 9, 8, 6, 4, 3, 2, 1 }; printArray(array, 7); } |
Передача по константному адресу
Поскольку printArray() все равно не изменяет значения получаемых аргументов, то хорошей идеей будет сделать параметр array
константным:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void printArray(const int *array, int length) { // Если пользователь передал нулевой указатель в качестве array if (!array) return; for (int index=0; index < length; ++index) std::cout << array[index] << ' '; } int main() { int array[7] = { 9, 8, 6, 4, 3, 2, 1 }; printArray(array, 7); } |
Так мы видим сразу, что printArray() не изменит переданный аргумент array
. Когда вы передаете указатель в функцию по адресу, то значение этого указателя (адрес, на который он указывает) копируется из аргумента в параметр функции. Другими словами, он передается по значению! Если изменить значение параметра функции, то изменится только копия, исходный указатель-аргумент не будет изменен. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> void setToNull(int *tempPtr) { // Мы присваиваем tempPtr другое значение (мы не изменяем значение, на которое указывает tempPtr) tempPtr = nullptr; // используйте 0, если не поддерживается C++11 } int main() { // Сначала мы присваиваем ptr адрес six, т.е. *ptr = 6 int six = 6; int *ptr = &six; // Здесь выведется 6 std::cout << *ptr << "\n"; // tempPtr получит копию ptr setToNull(ptr); // ptr до сих пор указывает на переменную six! // Здесь выведется 6 if (ptr) std::cout << *ptr << "\n"; else std::cout << " ptr is null"; return 0; } |
В tempPtr
копируется адрес указателя ptr
. Несмотря на то, что мы изменили tempPtr
на нулевой указатель (присвоили ему nullptr
), это никак не повлияло на значение, на которое указывает ptr
. Следовательно, результат выполнения программы:
6
6
Обратите внимание, хотя сам адрес передается по значению, вы все равно можете разыменовать его для изменения значения исходного аргумента. Запутано? Давайте проясним:
При передаче аргумента по адресу в переменную-параметр функции копируется адрес из аргумента. В этот момент параметр функции и аргумент указывают на одно и то же значение.
Если параметр функции затем разыменовать для изменения исходного значения, то это приведет к изменению значения, на которое указывает аргумент, поскольку параметр функции и аргумент указывают на одно и то же значение!
Если параметру функции присвоить другой адрес, то это никак не повлияет на аргумент, поскольку параметр функции является копией, а изменение копии не приводит к изменению оригинала. После изменения адреса параметра функции, параметр функции и аргумент будут указывать на разные значения, поэтому разыменование параметра и дальнейшее его изменение никак не повлияют на значение, на которое указывает аргумент.
В следующей программе это всё хорошо проиллюстрировано:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> void setToSeven(int *tempPtr) { *tempPtr = 7; // мы изменяем значение, на которое указывает tempPtr (и ptr тоже) } int main() { // Сначала мы присваиваем ptr адрес six, т.е. *ptr = 6 int six = 6; int *ptr = &six; // Здесь выведется 6 std::cout << *ptr << "\n"; // tempPtr получит копию ptr setToSeven(ptr); // tempPtr изменил значение, на которое указывал, на 7 // Здесь выведется 7 if (ptr) std::cout << *ptr << "\n"; else std::cout << " ptr is null"; return 0; } |
Результат выполнения программы:
Передача адресов по ссылке
Следует вопрос: «А что, если мы хотим изменить адрес, на который указывает аргумент, внутри функции?». Оказывается, это можно сделать очень легко. Вы можете просто передать адрес по ссылке. Синтаксис ссылки на указатель может показаться немного странным, но все же:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> // tempPtr теперь является ссылкой на указатель, поэтому любые изменения tempPtr приведут и к изменениям исходного аргумента! void setToNull(int *&tempPtr) { tempPtr = nullptr; // используйте 0, если не поддерживается C++11 } int main() { // Сначала мы присваиваем ptr адрес six, т.е. *ptr = 6 int six = 6; int *ptr = &six; // Здесь выведется 6 std::cout << *ptr; // tempPtr является ссылкой на ptr setToNull(ptr); // ptr было присвоено значение nullptr! if (ptr) std::cout << *ptr; else std::cout << " ptr is null"; return 0; } |
Результат выполнения программы:
6 ptr is null
Наконец, наша функция setToNull() действительно изменила значение ptr
с &six
на nullptr
!
Существует только передача по значению
Теперь, когда вы понимаете основные различия между передачей по ссылке, по адресу и по значению, давайте немного поговорим о том, что находится «под капотом».
На уроке о ссылках мы упоминали, что ссылки на самом деле реализуются с помощью указателей. Это означает, что передача по ссылке является просто передачей по адресу. И чуть выше мы говорили, что передача по адресу на самом деле является передачей адреса по значению! Из этого следует, что C++ действительно передает всё по значению!
Плюсы и минусы передачи по адресу
Плюсы передачи по адресу:
Передача по адресу позволяет функции изменить значение аргумента, что иногда полезно. В противном случае, используем const для гарантии того, что функция не изменит аргумент.
Поскольку копирования аргументов не происходит, то скорость передачи по адресу достаточно высокая, даже если передавать большие структуры или классы.
Мы можем вернуть сразу несколько значений из функции, используя параметры вывода.
Минусы передачи по адресу:
Все указатели нужно проверять, не являются ли они нулевыми. Попытка разыменовать нулевой указатель приведет к сбою в программе.
Поскольку разыменование указателя выполняется медленнее, чем доступ к значению напрямую, то доступ к аргументам, переданным по адресу, выполняется также медленнее, чем доступ к аргументам, переданным по значению.
Когда использовать передачу по адресу:
при передаче обычных массивов (если нет никаких проблем с тем, что массивы распадаются в указатели при передаче).
Когда не использовать передачу по адресу:
при передаче структур или классов (используйте передачу по ссылке);
при передаче фундаментальных типов данных (используйте передачу по значению).
Как вы можете видеть сами, передача по адресу и по ссылке имеют почти одинаковые преимущества и недостатки. Поскольку передача по ссылке обычно безопаснее, чем передача по адресу, то в большинстве случаев предпочтительнее использовать передачу по ссылке.
Правило: Используйте передачу по ссылке, вместо передачи по адресу, когда это возможно.
Что то я не понял.
—
Когда не использовать передачу по адресу:
при передаче структур или классов (используйте передачу по ссылке);
—
На уроке о ссылках мы упоминали, что ссылки на самом деле реализуются с помощью указателей. Это означает, что передача по ссылке является просто передачей по адресу.
Насколько я понимаю, передавать фиксированный массив по ссылке не имеет смысла, т.к. придётся указывать его размер. Но если уж и передавать указатель на массив, то почему бы явно не передавать указатель на массив, вместо того, чтобы передавать указатель на первый элемент? Тогда не придётся как-то по особенному называть параметр функции, ведь итак всё понятно будет:
int (*arr)[] — PtrArray_int
int arr[] / int *arr — Ptr_int
Почему принято передавать указатель на первый элемент, если в С++ возможно передавать настоящий указатель на массив?
думаю, тут скорее не "как принято", а то как передача массивов в функцию умолчанию так работает в языке, поэтому это и используют, чтобы ничего не усложнять
Описание int (*arr)[] заинтересовало. Можно об єтом подробней или пример?
Цитата: Поскольку разыменование указателя выполняется медленнее, чем доступ к значению напрямую,
Это как?
"разыменование указателя" — это не более чем абстрация языка. Никакого машинного кода не генерируется. Как это может быть быстрее? Не думаю, что прямая адресация хоть как-то быстрее косвенно-регистровой. И та и другая команда выполняется за одинаковое количество тактов. Если смотреть трансляцию на RISС конвеер, то разницы найти не возможно, стадия MEM в обоих случаях выполняется за фиксированный такт.
Немного запутали, но на практике вопросы уйдут.
Я вот честно говоря тоже запутался как нужно передавать массив. В главе Урок №75 написано, что массив и без амперсанта и без указателя и так передается в функцию без копирования. Здесь написано, что нужно через адрес. При этом структуры и классы лучше через ссылку. Чем массив принципиально отличается от структуры? Не понятно, как правильно
Насколько я понял, фиксированный массив при передаче в функцию без ничего, приходит в неё в виде указателя на первый элемент, (ничего не копируется). Если же ты передаешь std::array или std::vector, то без ничего они полностью скопируются в аргумент функции (что затратно)
Поэтому лучше использовать адреса. А еще лучше ссылки.
Так и не понял, зачем передавать массивы по адресу, когда безопаснее по ссылке?
Потому что массив при передаче в функцию распадается в указатель.
так в чем собственно преимущество?
Даже если передавать по ссылке, все равно массив распадется в указатель. Так в чем преимущество передачи массива по ссылке?
Здесь ограничение по глубине комментариев, поэтому могу ответить только себе. Итак, вы отвечаете вопросом на вопрос? То есть у указателей все-таки нет преимуществ перед ссылками?
по ссылке на что Вы собираетесь передавать массив?
Я так понимаю, что немного дальше автор нас будет знакомить с итераторами. Так вот, ссылка для массива — это простой итератор. И передача массива по ссылке унифицирует интерфейсы работы функций с различными контейнерами. Если Вы умеете обрабатывать массив, жонглируя исключительно ссылками (при обращении к элементам пользуетесь разыменованием, а не оператором [] ), то в большинстве случаев можете тот же код применить и к другим контейнерам.
Кстати, желательно передавать в функцию не "указатель на начало массива + размер", а "указатель на начало и указатель на конец" (точнее, на элемент, который идет после последнего). Поначалу будет казаться, что это ненужное усложнение, но на самом деле так намного проще… да и многие стандартные функции обработки контейнеров принимают именно такой тип аргументов.
«и указатель на конец» (точнее, на элемент, который идет после последнего).» нихрена вы быкдоров в своём коде понаоставляли…
рай для хацкеров какойто
Здравствуйте! Извините за тупость, но вот тут:
мне не совсем понятно array[index].
Ведь array — это указатель на первый элемент массива, то-есть, фактически, это адрес первого элемента и как к этому адресу пристыкуется [index]?
При следующих итерациях цикла, по моей логике, должен вновь получится адрес первого элемента, но уже со следующим индексом в квадратных скобках.
Привет.
array — это тип указателя на массив (int *), а array[index] — это уже набор значений определенного типа, например, int-ов. Определив array[4] и используя возле array квадратные скобки со значением внутри вы автоматически обращаетесь к array, как к набору int-ов, а не как к указателю на первый элемент. Не указывая квадратных скобок и значения в них вы можете относиться к array как к указателю на массив (на первый элемент).
Т.е. если вы добавляете [index] к array, то array используется в этом случае как тип данных (int), а не как указатель (int *). И при последующих итерациях цикла у вас получается не адрес первого элемента + следующий индекс в квадратных скобках, а адрес 2-ого значения из набора int-ов (т.е. из массива).
Всё понял, спасибо!
Но возник ещё один вопрос: память для массива всегда одним целым куском выделяется? Не может быть так, к примеру, что сначала идут первые сто адресов для массива, потом адрес памяти выделен под переменную (массив инициализировали позже переменной), а потом следующие адреса продолжают массив и при инструкции в цикле
эта переменная попадёт в вывод?
Насколько я понял ваш вопрос, то память для массивов выделяется либо сразу (фиксированные массивы), либо по мере увеличения количества элементов (динамическое выделение памяти для динамических массивов). Может ли выделение памяти прерываться, как описали вы — не знаю.
при выделении памяти под массив (в том числе и под динамический) адреса элементов ГАРАНТИРОВАНО идут по порядку