Этот пост является частью моего Руководства по C ++ для разработчиков EOS.
- Основы
- Вызов по значению / ссылке и указателям
- Классы и структуры
- Шаблоны
- Итераторы
- Лямбда-выражения
- Мультииндексный
- Заголовочные файлы
Звоните по стоимости / ссылке
Как аргументы передаются функциям - важная концепция C ++. Это часто бывает трудно понять новичкам, поскольку у вас нет этих разных способов передачи аргументов в таких языках, как Java или JavaScript. Вы можете передавать аргументы функции с помощью вызова по значению или вызова по ссылке. Разница между ними в том, что при вызове по значению создается копия фактического аргумента, а вызываемая функция работает с этой копией. При вызове по ссылке в функцию передается место в памяти (адрес) аргументов. Это означает, что функция будет работать с идентичным объектом, и поэтому любые изменения объекта будут сохраняться вне вызова функции, поскольку изменения, конечно, не отменяются при возврате функции. Давайте посмотрим на пример:
#include <iostream>
// this is call by value
// the integer x is copied and any modification is done on the copy
void test_by_value(int x) {
x = 1;
}
// call by reference is done using the `&` after the type
// no copy is created, behind the scenes the memory location is passed
// and the same number is used
void test_by_ref(int& x) {
x = 2;
}
int main() {
std::cout << "Hello World!\n";
int number = 0;
test_by_value(number);
// outputs 0, number unchaged
std::cout << "test_by_value " << number << "\n";
test_by_ref(number);
// outputs 2(!), number changed
std::cout << "test_by_ref " << number << "\n";
}
Обратите внимание, что код для вызывающего точно такой же, независимо от того, передаете ли вы по значению или по ссылке. Вызывающий всегда просто передает number
. Для вызова по ссылке в качестве индикатора в списке аргументов функции используется амперсанд &
. Неразличимость от вызывающей стороны делает невозможным рассуждать о том, были ли ваши данные изменены как часть побочных эффектов функции. Причина, по которой C ++ имеет эти два типа, - это ответ C ++ по умолчанию на вопросы: производительность. Создание копий больших объектов требует времени, и этого можно избежать, просто повторно используя существующий объект. Вы можете запретить изменение аргументов, объявив их как const
:
int test_by_ref(const int& x) {
// this raises a compile time error now
// x = 2;
// reading is OK
return x + 2;
}
Такое же поведение применяется не только к int
, но и ко всем другим типам данных (string
s, vector
s) и объекту класса.
Рекомендуется использовать вызов по ссылке, потому что он обычно более эффективен, и не изменять аргументы, объявляя их как
const
Например, Руководство по стилю Google C ++ гласит:
Все параметры, передаваемые по ссылке, должны быть помечены как const.
In fact, it is a very strong convention in Google code that input arguments are values or const references while output arguments are pointers.
Теперь мы понимаем values
и const references
, поэтому давайте поговорим о том, что Google подразумевает под выходными аргументами - указателями.
Указатели
Указатели - это переменные, которые хранят адрес памяти другой переменной. Они широко используются в C, поскольку в нем нет вызова по ссылке. Вместо этого в C вы определяете переменную-указатель, в которой хранится место в памяти вашего аргумента, а затем передаете переменную этот указатель по значению.
Давайте посмотрим, как мы могли бы написать нашу test
функцию на C:
int number = 0;
// in C++
void test_by_ref(int& x) {
x = 2;
}
test_by_ref(number);
// in C
void test_by_pointer(int* px) {
// remember the value of px is the address of x
// to get the actual value of x we need to dereference the pointer by using `*`
*x = 2;
}
// pointers are defined by <type>*
// you get the address of a variable by using &
int* pnumber = &number;
test_by_pointer(pnumber);
// or without intermediate pointer variable
test_by_pointer(&number);
Указатель на переменную int
определяется как int*
, указатель на string
будет определен как string*
. Однако фактический размер любого указателя одинаков: sizeof(int*) == sizeof(string*)
. Интуитивно, указатели можно рассматривать как 32-битные или 64-битные, в зависимости от платформы unsigned int
переменные, значение которых является адресом памяти другой переменной.
Почему тогда нам нужно указывать тип указателя (
int*
илиstring*
)?
Хороший вопрос. Тип становится важным при попытке получить доступ к значению переменной, на которую указывает указатель. В нашем случае, чтобы получить значение number
из нашего указателя int* pnumber
, нам нужно разыменовать указатель. Это выполняется оператором *
над переменной-указателем: int numberValue = *pnumber
Чтобы узнать, сколько байтов должен прочитать указатель, нам нужно определить тип указателя.
Например, вы можете побайтно прочитать целочисленную переменную, выполнив следующие действия:
uint32_t number = 0x01020304;
// we need to cast it to uint8_t* because &number is of type unit32_t*
// remember the pointers all have the same range as they all store memory addresses
uint8_t* p = (uint8_t*)(&number);
for(int i = 0; i < 4; i++) {
std::cout << "Byte " << i << ": " << std::to_string(*(p+i)) << "\n";
}
Как видите, мы можем производить расчеты по указателям. Это называется арифметикой указателя. Здесь *(p+i)
означает перемещение в i
раз больше размера типа указателя (sizeof(uint8_t)
) вперед в ячейке памяти p
и чтение uint8_t
.
Нужны ли нам еще указатели в C ++?
Хотя многое можно сделать с помощью более простого references
в C ++, вы все равно будете часто встречать указатели, например, при использовании итераторов или выходных параметров. Выходные параметры аналогичны значениям return
из функции, за исключением того, что они передаются как аргумент указателя, а затем объект, на который имеется ссылка, изменяется в функции. Давайте посмотрим на пример:
void split(const std::string &name, std::string *first, std::string *last)
{
std::size_t pos = name.find(" ");
*first = name.substr(0, pos);
*last = name.substr(pos + 1);
}
std::string name = "Dan Larimer";
std::string first, last;
split(name, &first, &last);
Здесь first
и last
- параметры вывода указателя, содержащие результат вычисления после завершения функции split
. Параметры вывода обычно используются вместо реальных возвращаемых аргументов (string split(...) { ... return <string> }
), когда вам нужно вернуть более одного значения, например два string
s в нашем случае.
Разве нельзя добиться того же, используя в качестве выходных параметров ссылки вместо указателей?
Да, мы могли бы переписать функцию, чтобы вместо этого использовались выходные параметры reference:
void split(const std::string &name, std::string& first, std::string& last)
{
std::size_t pos = name.find(" ");
first = name.substr(0, pos);
last = name.substr(pos + 1);
}
std::string name = "Dan Larimer";
std::string first, last;
split(name, first, last);
Что вы предпочитаете, зависит от вас и зависит от личного стиля. Одна из причин, по которой руководство Google C ++ Styleguide предпочитает указатели в качестве выходных аргументов, заключается в том, что оно дает понять на вызывающем сайте, что аргумент потенциально может быть изменен.
Тем не менее важно уметь читать и понимать как ссылки, так и указатели.
Вы, вероятно, запутались сейчас множеством различных способов передачи аргументов функциям. Как правило, это самый важный опыт обучения для разработчиков, плохо знакомых с C ++, так что не волнуйтесь. В какой-то момент вы увидите общие повторяющиеся закономерности.
Первоначально опубликовано на cmichel.io
Получайте лучшие предложения по программному обеспечению прямо в свой почтовый ящик