Ссылки и указатели
Ссылка - это псевдоним к определенной ранее переменной. Указатель - это объект, значением которого является адрес ячейки памяти.
Разница?
Инициализация
Ссылку обязательно инициализировать. Она не может указывать вникуда или быть пустой, в отличие от указателя, которому задавать начальное значение необязательно.
Изменение значения
Ссылка не может быть переопределена, вместо этого присвоится значение переменной, на которую ссылка ссылается, если она не константная. Присваивать же новое значение указателю можно, если он не константный.
Взятие адреса
Ссылка - псевдоним к переменной, а не объект. Взятие адреса от ссылки будет возвращать адрес объекта, на который ссылка ссылается. Взятие же адреса указателя будет возвращать адрес указателя, а не объекта, на который указатель ссылается.
По стандарту ссылка - не объект, но при определенных условиях, ссылки могут компилироваться в указатели.
Рекомендация: используйте ссылки вместо указателей, если вам не нужно передавать пустое значение (читай, нулевой указатель).
Интересное про ссылки
Краткие факты
- Ссылки на ссылку не бывает. Они коллапсируют в одну.
- Ссылки типа
void
не бывает. - Ссылка может продлить время жизни объекта, если это
lvalue
-ссылка на const илиrvalue
-ссылка.
Dangling reference
Ссылки были созданы, чтобы сделать указатели безопаснее, но даже их можно сломать.
#include <iostream>
int& bar() {
int n = 10;
return n;
}
int main() {
int& i = bar();
std::cout << i << std::endl;
}
Функция bar()
вернет ссылку на локальную переменную, которая уничтожится при выходе из нее. Соответственно, такая ссылка будет невалидной и операции с ней - это undefined behavior.
Но вот так делать можно, конечно.
#include <iostream>
const int& bar(const int& a) {
return a;
}
int main() {
const int& i = bar(42);
std::cout << i << std::endl;
}
Передача массива по ссылке
Не секрет, что для передачи массивов в функции можно использовать указатели, но мало кто догадается, что можно использовать и ссылки. Но сначала: каким образом обычно передают указатели на массивы?
- Либо это указатель на массив фиксированного размера
- Либо это указатель на массив с передачей размера отдельным аргументом
В примере ниже мы сделаем первый вариант, но схитрим, написав шаблон.
#include <type_traits>
// Конвертирует строковый литерал в число
template <size_t N>
constexpr int convert(const char (&in)[N]) {
int res = 0;
for (const char * c = in; *c; ++c) {
(res *= 10) += *c - '0';
}
return res;
}
// Вычисляет длину массива
template <typename T, size_t N>
size_t len(T (&a)[N]) {
return N;
}
static_assert(convert("123") == 123);
Замечание: constexpr
здесь нужен только для демонстрации static_assert
.
Остальные фишки ссылок будут в другой заметке.
Интересное про указатели
Краткие факты
- Можно сделать указатель на указатель на указатель...
- Поддерживают арифметику (только указатели в рамках одного массива. остальное - undefined behavior)
Указатели на массивы фиксированной длины
int (*a)[2]; // create pointer to int[2]
int b[2];
int c[2];
a = new int[2]; // compile error (returns int*)
a = &b; // OK (returns int(*)[2])
a = (int(*)[2]) (new int[2]); // OK
a = &c; // compile error (returns int(*)[3])
Многомерные массивы на стеке
При объявлении многомерных массивов на стеке память выделяется непрерывно, что позволяет компилятору при индексации делать прыжок по указателю единожды.
int x[10][10]; // выделенный на стеке многомерный массив
int a = x[2][3]; // один прыжок
Но при индексации от указателя на него программа будет совершать честные прыжки в количестве измерений.
int **y = x;
int b = y[2][3]; // два прыжка
Как читать мешанину со словом const
Ключевое слово const
относится к тому, что слева, если не в начале строки, иначе к тому, что справа.
int a = 42;
const int b; // нет инициализации, compile error
const int c = a; // константный int
int const d = a; // то же самое
const int * e = &a; // изменяемый указатель на неизменяемый int
int const * f = &a; // то же самое
int * const g = &a; // неизменяемый указатель на изменяемый int
const int * const h = &a; // константный указатель на константу
Указатель на функцию
Функции приводятся к указателям и наоборот налету, неявно. Синтаксис для их объявления приведен ниже:
double sum(int a, long b) {
return a + b;
}
double (*ptr)(int, long) = sum;
double (*ptr)(int, long) = ∑ // эквивалентно
double c = (*ptr)(1, 2l);
double c = ptr(1, 2l); // эквивалентно
Массив указателей на массив
Интересно объявлен тип указателя C, разгадка будет ниже.
int A[5][5];
int B[5][5];
int (*C[])[5][5] = {&A, &B};
Пример от Артема К
double (* (*x[10]) (int &))[5];
x — это массив из 10 указателей на функции, которые принимают аргументом int&
, а возвращают массив из 5 double
.
Как парсить подобные вещи
- Парсим изнутри, начиная с имени переменной
- Идем вправо, потом влево
- Потом на следующий уровень наружу
Пример:
void* (*y[5])(char);
y — это:
- массив из пяти
- указателей
- на функцию, принимающую char
- и возвращающую void*
Пример от Артема К
int (* (** (* (* x)[5])(void))[10])();
x - это указатель на массив размера 5 из указателей на функции, принимающие void (то есть не принимающие аргументов - это альтернативный синтаксис) и отдающие указатель на указатель на массив размера 10 из указателей на функции без аргументов, возвращающие int