Этот пост является частью моего Руководства по C ++ для разработчиков EOS.

  1. Основы
  2. Вызов по значению / ссылке и указателям
  3. Классы и структуры
  4. Шаблоны
  5. Итераторы
  6. Лямбда-выражения
  7. Мультииндексный
  8. Заголовочные файлы

Звоните по стоимости / ссылке

Как аргументы передаются функциям - важная концепция 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, но и ко всем другим типам данных (strings, vectors) и объекту класса.

Рекомендуется использовать вызов по ссылке, потому что он обычно более эффективен, и не изменять аргументы, объявляя их как 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> }), когда вам нужно вернуть более одного значения, например два strings в нашем случае.

Разве нельзя добиться того же, используя в качестве выходных параметров ссылки вместо указателей?

Да, мы могли бы переписать функцию, чтобы вместо этого использовались выходные параметры 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

Получайте лучшие предложения по программному обеспечению прямо в свой почтовый ящик