Зачем нужны ссылки в c
Перейти к содержимому

Зачем нужны ссылки в c

  • автор:

Ссылки в C++

Ссылка в С++ — это альтернативное имя объекта.

Ссылку можно понимать как безопасный вариант указателя. При этом ссылки имеют особенности, отличающие их от указателей:

  1. При объявлении ссылка обязательно на уже существующий объект данного типа. Ссылка не может ссылаться «ни на что».
  2. Ссылка от её объявления до её исчезновения указывает на один и тот же адрес.
  3. При обращении к ссылке разыменование происходит автоматически.
  4. Адрес ссылки — это адрес исходного объекта, на который она указывает.

Объявление ссылок очень похоже на объявление указателей, только вместо звёздочки * пишется амперсанд &.
При объявлении ссылка обязана быть инициализирована.

int &x; // недопустимо!
int &x = veryLongVariableName; // допустимо. Теперь x — это альтернативное имя переменной veryLongVariableName

int A[10];
int &x = A[5]; // Ссылка может указывать на элемент массива
x++; // то же, что A[5]++;
x = 1; // то же, что A[5] = 1;

Передача параметров в функцию по ссылке

Параметры можно передавать по ссылкам. При этом связывание ссылки с определённой переменной произойдёт в момент вызова функции (на этапе выполнения программы).

void foo(int &x)
x = 3;
>
int main()
int t = 1;
foo (t);
cout >

Нужно быть осторожным при передаче значений в такую функцию стороннего разработчика.
Если не планируется изменение передаваемой переменной, функцию лучше объявить так:

void foo(const int &x);

Это гарантирует программисту-пользователю функции неизменность передаваемого значения.

Для типа int передача по константной ссылке обычно не нужна, так как можно просто передать аргумент по значению, но для большого класса или структуры передача по ссылке гораздо быстрее и экономит память.

Чего нельзя делать со ссылкой

Возврат из функции ссылки на автоматически созданный объект (локальную переменную) приводит к появлению «битых ссылок», значение которых непредсказуемо.

Также синтаксис С++ не позволяет создавать указатели на ссылки и массивы ссылок.

Ссылки

Ссылка reference — механизм языка программирования (C++), позволяющий привязать имя к значению. В частности, ссылка позволяет дать дополнительное имя переменной и передавать в функции сами переменные, а не значения переменных.

Синтаксически ссылка оформляется добавлением знака & (амперсанд) после имени типа. Ссылка на ссылку невозможна.

Ссылка требует инициализации. В момент инициализации происходит привязка ссылки к тому, что указано справа от = . После инициализации ссылку нельзя “отвязать” или “перепривязать”.

Любые действия со ссылкой трактуются компилятором как действия, которые будут выполняться над объектом, к которому эта ссылка привязана. Следующий пример демонстрирует ссылку в качестве дополнительного имени переменной.

int n = 0; int &r = n; /* теперь r -- ссылка на n или второе имя переменной n */ n = 10; cout '\n'; // выведет 10 r = 20; cout '\n'; // выведет 20 cout '\n'; // выведет 1, т.е. истина

Казалось бы, зачем нам второе имя переменной? Ответа может быть, по крайней мере, два.

  1. Что-то имеет слишком длинное, неудобное название. Привязав к нему ссылку, мы получим более удобное, короткое локальное название. При этом мы можем не указывать тип этого “чего-то”, можно использовать вместо типа ключевое слово auto :
auto &short_name = some_namespace::some_long_long_name;
  1. Выбор объекта привязки ссылки может происходить во время исполнения программы и зависеть от некоего условия. Пример:
int a = 0, b = 0; cin >> a >> b; int &max = a < b? b: a; // привязать к b, если a < b, иначе -- к amax = 42; cout "a = " << a "; b = " << b '\n';

Впрочем, основным применением ссылок является передача параметров в функции “по ссылке” и возвращение функциями ссылок на некие внешние объекты.

Передача по ссылке by reference напоминает передачу “по имени”. Таким образом, можно сказать, что, используя ссылки, мы передаём не значения, а сами переменные, содержащие эти значения. В реальности “за ширмой” происходит передача адресов этих переменных. Передача ссылки на переменную, время жизни которой заканчивается, например, возврат из функции ссылки на локальную переменную, приводит к неопределённому поведению.

Ранний пример использования ссылок для возврата из функции более одного значения представлен в самостоятельной работе 3.

Приведём здесь ещё один пример: функцию, которая возвращает одну из двух переменных, содержащую максимальное значение. Для этого модифицируем предыдущий пример:

int& max_byref(int &a, int &b) < return a < b? b: a; >int main() < int x = 0, y = 0; // собственно имена переменных не обязаны совпадать cin >> x >> y; max_byref(x, y) = 42; cout "x = " << x "; y = " << y '\n'; return 0; >

Так как при передаче ссылки реально копируется лишь адрес значения, а не само значение, то передав ссылку можно избежать копирования значения. Поэтому ссылки широко используются для передачи в функцию аргументов, которые или запрещено копировать или вычислительно дорого копировать. Типичный пример — объекты string. При копировании строки происходит выделение динамической памяти, копирование всех символов, затем — при удалении этой копии — освобождение памяти. Часто нет никакой необходимости в копировании. Например, следующей функции, считающей количество повторений заданного символа в строке нет нужды копировать строку — можно обойтись ссылкой:

size_t char_freq(const string &s, char c) < size_t freq = 0; for (size_t i = 0, sz = s.size(); i != sz; ++i) freq += s[i] == c; return freq; >

Обратите внимание на ключевое слово const . Данное ключевое слово позволяет нам указать, что мы хотим ссылку на константу, т.е. функция char_freq использует s как константу и не пытается её изменять, а ссылка нужна для того, чтобы избежать копирования. Рекомендуется использовать const везде, где достаточно константы. Компилятор проверит, действительно ли мы соблюдаем константность.

Ставить слово const можно перед именем типа и после имени типа, это эквивалентные записи.

int x; const int &r1 = x; // ссылка на x "только для чтения" int const &r2 = x; // тоже ссылка на x "только для чтения" int & const r3 = x; // ошибка компиляции, нельзя ставить const после &

Указатели

Общие сведения

Что такое указатель pointer уже рассказывалось во введении.

В C и C++ указатель определяется с помощью символа * после типа данных, на которые этот указатель будет указывать.

Указатель — старший родственник ссылки. Указатели активно использовались ещё в машинных языках и оттуда были перенесены в C. Ссылки же доступны только в C++.

Указатели — простые переменные. Указатели не “делают вид”, что они — те значения в памяти, к которым они привязаны. Чтобы получить указатель на переменную, нужно явно взять её адрес с помощью оператора & . Чтобы обратиться к переменной, на которую указывает указатель, требуется явно разыменовать его с помощью оператора * .

int n = 0; int *r = &n; // теперь r -- указатель на n n = 10; cout '\n'; // выведет 10 *r = 20; cout '\n'; // выведет 20 cout '\n'; // выведет 1

Так же, как и в случае ссылок, можно использовать ключевое слово const , чтобы создать указатель на константу.

int x = 0, y = 1; const int *p1 = &x; // указатель на x "только для чтения" y = *p1; // можно *p1 = 10; // ошибка компиляции: нельзя изменить константу *p1 p1 = &y; // можно: сам указатель p1 не является константой int const *p2 = &x; // тоже указатель на x "только для чтения", всё аналогично p1 int * const p3 = &x; // теперь константа -- сам указатель y = *p3; // можно *p3 = 10; // тоже можно! p3 = &y; // ошибка компиляции: нельзя изменить константу p3 const int * const p4 = &x; /* комбо: теперь у нас константный указатель на x "только для чтения" */ y = *p4; // можно *p4 = 10; // ошибка компиляции: нельзя изменить константу *p4 p4 = &y; // ошибка компиляции: нельзя изменить константу p4

Указатели можно сравнивать друг с другом. Указатели равны, если указывают на один и тот же объект, и не равны в противном случае.

Указатели можно передавать в функции и возвращать из функций как и любые “элементарные” значения. Ещё пример с указателями:

int* max_byptr(int *a, int *b) < return *a < *b? b: a; >int main() < int x = 0, y = 0; // собственно имена переменных не обязаны совпадать cin >> x >> y; *max_byref(&x, &y) = 42; cout "x = " << x "; y = " << y '\n'; return 0; >

Для обращения к полю структуры по указателю на объект структуры предусмотрен специальный оператор -> (“стрелка”).

struct Point < float x, y; >; Point a = < 20, 30 >; cout ' ' '\n'; // > 20 30 Point *p = &a; p->x = 42; (*p).y = 23; // то же самое, что p->y = 23; cout ' ' '\n'; // > 42 23

В отличие от ссылок, указатели не обязательно инициализировать. Указатели можно инициализировать специальным значением нулевой указатель nullptr , которое сигнализирует об отсутствии привязки указателя к чему-либо. Присваивание указателю другого адреса меняет его привязку. Это позволяет использовать указатели там, где семантика ссылок слишком сильно ограничивает наши возможности.

Соответственно, ограничения, накладываемые на ссылки по сравнению с указателями, позволяют, с одной стороны, защитить программиста от ряда ошибок, и, с другой стороны, открывают ряд возможностей оптимизации кода для компилятора. Ссылки используются там, где нет нужды в “полноценных” указателях или есть желание не перегружать код взятиями адреса и разыменованиями.

Наличие нулевого указателя позволяет, например, возвращать указатель на искомый объект и в том случае, когда ничего не было найдено. Просто в этой ситуации возвращаем нулевой указатель, а принимающая сторона должна быть готова к такому развитию событий. Указатель автоматически преобразуется к булевскому значению: нулевой указатель даёт false , прочие указатели дают true , поэтому, если p — указатель, то

if (p) . 

есть то же самое, что

if (p != nullptr) . 
if (!p) . 

есть то же самое, что

if (p == nullptr) . 

Например, поиск самого левого нуля в массиве чисел с плавающей точкой может быть записан так:

// Ищет нулевой элемент в диапазоне [from, to). // Возвращает нулевой указатель, если нуль не был найден. float* find_next_zero(float *from, float *to) < for (; from != to; ++from) if (*from == 0.f) return from; // нашли return nullptr; // ничего не нашли > int main() < float num[] < 1, 2, 3, 0, 3, 4 >; if (auto zero_pos = find_next_zero(num, num + sizeof(num)/sizeof(num[0]))) cout '\n'; else cout "zero not found\n"; // невозможно! return 0; >

Данный пример использует арифметику указателей и массивы. Данная тема освещена в разделе массивы и ссылки.

Бестиповый указатель

Вместо типа данных при объявлении указателя можно поставить ключевое слово void . Данное ключевое слово означает, что мы описываем указатель “на что угодно”, т. е. просто адрес в памяти. Любой указатель автоматически приводится к типу void* — бестиповому указателю typeless pointer . Прочие указатели, соответственно, называются типизированными или типизованными typed . Приведение от void* к типизованному указателю возможно с помощью оператора явного приведения типа.

В C бестиповые указатели широко применяются для оперирования кусками памяти или реализации обобщённых функций, которые могут работать со значениями разных типов. В последнем случае конкретный тип маскируется с помощью void (“пустышка”). При использовании таких функций обычно приходится где-то явно приводить тип указателей. C++ позволяет отказаться от подобной практики благодаря поддержке полиморфизма и обобщённого программирования (материал 2-го семестра).

#include #include // setw -- ширина поля вывода, hex -- вывод в 16-ричной системе #include using namespace std; // Ещё один способ получить битовое представление числа с плавающей точкой. int main() < unsigned char buffer[sizeof(float)]; // Настройка потока вывода. cout.fill('0'); // Заполнять нулями. cout.setf(ios::right); // Выравнивать по правому краю. for (float x; cin >> x; ) < // Скопировать побайтово память x в память buffer. memcpy(buffer, &x, sizeof(float)); // Вывести каждый байт buffer в 16-ричной форме. for (int byte: buffer) cout 2) ' '; cout '\n'; > >

О цикле for (int byte: buffer) см. здесь.

Указатель на указатель

Так как указатель — обычная переменная, возможен указатель на указатель. И указатель на указатель на указатель. И указатель (на указатель) n раз для натурального n. Максимальный уровень вложенности задаётся компилятором, но на практике уровни больше 2 практически не используются.

int n = 4; int *p = &n; // уровень косвенности 1 *p = 5; cout // выведет 5 int **pp = &p; // уровень косвенности 2 **p = 6; cout // выведет 6 int ***ppp = &pp; // уровень косвенности 3 ***p = 7; cout // выведет 7

Система ранжирования C-программистов.

Чем выше уровень косвенности ваших указателей (т. е. чем больше “*” перед вашими переменными), тем выше ваша репутация. Беззвёздочных C-программистов практически не бывает, так как практически все нетривиальные программы требуют использования указателей. Большинство являются однозвёздочными программистами. В старые времена (ну хорошо, я молод, поэтому это старые времена на мой взгляд) тот, кто случайно сталкивался с кодом, созданный трёхзвёздочным программистом, приходил в благоговейный трепет.

Некоторые даже утверждали, что видели трёхзвёздочный код, в котором указатели на функции применялись более чем на одном уровне косвенности. Как по мне, так эти рассказы столь же правдивы, сколь рассказы об НЛО.

Просто чтобы было ясно: если вас назвали Трёхзвёздочным Программистом, то обычно это не комплимент.«

Условия для проверки себя на “трёхзвёздность” перечислены на другой странице того же сайта.

В случае C указатели на указатели (уровень косвенности 2) используются довольно часто, например, для возвращения указателя из функции, которая возвращает ещё что-то, или для организации двумерных массивов. Пример такой функции из Windows API:

DWORD WINAPI GetFullPathName( _In_ LPCTSTR lpFileName, _In_ DWORD nBufferLength, _Out_ LPTSTR lpBuffer, _Out_ LPTSTR *lpFilePart );

Функция принимает имя файла как указатель на си-строку lpFileName, а также размер буфера nBufferLength в символах и адрес буфера lpBuffer, куда записывается в виде си-строки полное имя файла. Функция возвращает длину строки, записанной в буфер, или 0, если произошла ошибка. Кроме того, последний параметр функции — указатель на указатель на си-строку lpFilePart, который используется, чтобы вернуть из функции указатель на последнюю часть имени файла, записанного в буфер.

В случае C++ с помощью ссылок и Стандартной библиотеки можно вообще избежать использования “классических” указателей. Так что “беззвёздочный” C++-программист возможен.

Неограниченный уровень косвенности

Несмотря на ограниченность применения уровня косвенности выше двух, довольно часто встречается то, что можно назвать неограниченным уровнем косвенности или рекурсивным типом данных. Типичный (и простейший) пример — структура данных, называемая “связанный список” linked list .

Следующий пример демонстрирует использование связанного списка для чтения последовательности строк и вывода этой последовательности в обратном порядке:

struct Line < Line *prev; string line; >; int main() < Line *last = nullptr; // Чтение строк. for (string line; getline(cin, line);) < Line *new_line = new Line; new_line->prev = last; new_line->line = line; last = new_line; > // Вывод строк в обратном порядке. while (last) < cout line '\n'; Line *old_line = last; last = last->prev; delete old_line; > return EXIT_SUCCESS; >

Упражнение. Попробуйте изменить этот пример так, чтобы введённые строки выводились в том же порядке, в котором были введены.

Указатели на функции

Язык C позволяет определять указатели на функции (в указателе хранится адрес точки входа в функцию) и вызывать функции по указателю. Таким образом, можно во время исполнения программы выбирать какая именно функция будет вызвана в конкретной точке, выбирая значение указателя. Язык C++ позволяет создавать также и ссылки на функции, но ввиду того, что ссылка после инициализации не может быть изменена, область применения ссылок на функции весьма узка.

Функцией высшего порядка higher order function называют функцию, принимающую в качестве параметров другие функции. Функции высшего порядка — одно из базовых понятий функционального программирования. Единственная форма функций высшего порядка в C — функции, принимающие указатели на функции. Язык C++ расширяет круг доступных форм функций высшего порядка, но в примерах ниже мы ограничимся возможностями C.

Простой пример использования указателя на функцию — функция, решающая уравнение вида f(x) = 0, где f(x) — произвольная функция. Конкретные функции f можно передавать по указателю. Приведение функций к указателю на функцию и наоборот производится неявно автоматически, поэтому при присваивании указателю адреса конкретной функции можно не использовать оператор взятия адреса & , а при вызове функции по указателю — не использовать оператор разыменования * (поведение, аналогичное поведению с массивами).

/// Тип "правая часть уравнения" -- функция одного действительного параметра. typedef double (*Unary_real_function)(double); /// Точность приближённого решения, используемая по умолчанию. const double Tolerance = 1e-8; /// Алгоритм численного решения уравнения f(x) = 0 на отрезке [a, b] делением отрезка пополам. /// Данный алгоритм является вариантом двоичного поиска. double nsolve(Unary_real_function f, double a, double b, double tol = Tolerance) < using namespace std; assert(f != nullptr); assert(a < b); assert(0. for (auto fa = f(a), fb = f(b);;) < // Проверим значения функции на концах отрезка. if (fa == 0.) return a; if (fb == 0.) return b; // Делим отрезок пополам. const auto mid = 0.5 * (a + b); // середина отрезка if (mid return abs(fa) < abs(fb)? a: b; if (b - a return mid; // Выберем одну из половин в качестве уточнённого отрезка. const auto fmid = f(mid); if (signbit(fa) != signbit(fmid)) < // Корень на левой половине. b = mid; fb = fmid; > else < assert(signbit(fb) != signbit(fmid)); // Корень на правой половине. a = mid; fa = fmid; > > >

Довольно типичной областью применения указателей на функции является связывание источников (регистраторов) некоторых событий, обычно определяемых в составе некоторой библиотеки, и обработчиков событий, предоставляемых пользователем этой библиотеки. Обработчики событий (функции) вызываются автоматически по переданным указателям. Такие функции также называются функциями обратного вызова callback functions или колбеками callbacks . Например, при щелчке мышью по элементу графического интерфейса вызывается функция-обработчик этого события, “зарегистрированная”, путём передачи её адреса библиотеке графического интерфейса.

В качестве простого примера применения функции обратного вызова рассмотрим функцию, занимающуюся поиском набора корней уравнения f(x) = 0 на заданном отрезке. Сама функция будет работать по достаточно простому алгоритму (который, естественно, не гарантирует, что будут найдены все или даже какие-то из существующих на отрезке корней): предполагаем, что есть некая функция, способная найти один корень на отрезке, если он там есть (например, функция nsolve из примера выше). Теперь берём исходный отрезок поиска [a, b] и некоторое значение “шага” step и проходим по этому отрезку с этим шагом, проверяя участки [a + i step, min(b, a + (i + 1)step], i = 0, … пока не пересечём правую границу отрезка. На каждом участке проверяем, являются ли его границы корнями, и есть ли на нём корень (принимает ли функция f разнознаковые значения на границах). В последнем случае используем “решатель” вроде nsolve (переданный по указателю), чтобы найти корень. Каждый найденный корень — это событие, вызываем для него “обработчик” — функцию обратного вызова по указателю report.

/// Тип "решатель уравнения на отрезке" -- функция вроде nsolve, определённой выше. typedef double (*Equation_solver)(Unary_real_function, double a, double b, double tol); /// Тип функции, вызываемой для каждого корня. /// Процесс поиска останавливается, если эта функция возвращает ложь. typedef bool (*Root_reporter)(double); /// Применяет заданный алгоритм поиска корня на отрезке, /// разбивая заданный отрезок [a, b] на отрезки одинаковой длины step (кроме, возможно, последнего). /// Для каждого найденного корня вызывает функцию report (callback-функция). /// Возвращает правую границу пройденного участка (идёт слева направо по заданному отрезку). double repeated_nsolve ( Unary_real_function f, double a, double b, double step, // шаг на отрезке Root_reporter report, double x_tol = TOLERANCE, // чувствительность по аргументу double f_tol = TOLERANCE, // чувствительность по значению функции Equation_solver solver = nsolve ) < assert(x_tol >= 0. && f_tol >= 0.); assert(a 0.); assert(f && report && solver); using namespace std; double left = a, f_left = f(left); bool f_left_zero = abs(f_left) // Корень на левой границе исходного отрезка? if (f_left_zero && !report(left)) return left; while (left != b) < // Правая граница очередного участка. const double right = fmin(b, left + step), f_right = f(right); const bool f_right_zero = abs(f_right) // Корень на правой границе участка? if (f_right_zero && !report(right)) return right; // Есть корень внутри участка? if (!(f_left_zero || f_right_zero) && signbit(f_left) != signbit(f_right)) < const double root = solver(f, left, right, x_tol); if (!report(root)) return root; > // Передвинуть левую границу. left = right; f_left = f_right; f_left_zero = f_right_zero; > return b; >

Следующий пример демонстрирует “двухзвёздное программирование” и использование указателя на функцию для определения порядка сортировки массива строк с помощью стандартной функции qsort .

#include // qsort #include // strcmp #include using namespace std; // Функция сравнения строк. int line_compare(const void *left, const void *right) < // Обращаем словарный порядок, поменяв местами left и right. return strcmp(*(const char**)right, *(const char**)left); > int main() < const char *lines[] < "may the force be with you", "this is it", "so be it", "it is a good day to die", "through the time and space", "the light shines in the darkness" >; // Сортировать: массив, количество элементов qsort(lines, sizeof(lines) / sizeof(lines[0]), // размер элемента, функция сравнения. sizeof(lines[0]), line_compare); // Распечатаем результат сортировки. for (auto line : lines) cout '\n'; return EXIT_SUCCESS; >

Функция qsort является частью Стандартной библиотеки C. Стандартная библиотека C++ предлагает более удобную и эффективную функцию sort (определённую в заголовочном файле ), однако её рассмотрение выходит за пределы темы данного раздела.

Следующий пример является развитием примера со списком из предыдущего подраздела и использует бестиповые указатели, указатели на указатели и указатели на функции для управления “обобщённым” связанным списком в стиле C. Звенья такого списка могут содержать произвольные данные. Основное требование к звеньям списка — наличие в начале звена указателя на следующее звено, фактически каждый предыдущий указатель указывает на следующий.

/// Возвращает ссылку на указатель на следующее звено звена link. void*& next(void *link) < return *(void**)link; > /// Вставляет link перед head и возвращает link (теперь это -- новая голова списка). void* insert_head(void *head, void *link) < next(link) = head; return link; > /// Вычисляет длину списка. size_t size(void *head) < size_t sz = 0; for (; head; head = next(head)) ++sz; return sz; > /// Указатель на функцию, выполняющую удаление звена. using Link_delete = void(*)(void*); /// Удаляет список, используя пользовательскую функцию удаления. void delete_list(void *head, Link_delete link_delete) < while (head) < auto next_head = next(head); link_delete(head); head = next_head; > >

Теперь сама программа, выводящая строки в обратном порядке, упрощается:

/// Звено списка -- одна строка. struct Line < void *prev; string line; >; /// Вывести строку и удалить объект Line. void print_and_delete(void *ptr) < auto line = (Line*)ptr; cout line '\n'; delete line; > int main() < Line *head = nullptr; // Чтение строк. for (string line; getline(cin, line);) < Line *new_line = new Line; new_line->line = line; head = (Line*)insert_head(head, new_line); > // Вывод количества строк -- элементов списка. cout "\nLines: " << size(head) "\n\n"; // Вывод строк в обратном порядке. delete_list(head, print_and_delete); cin.clear(); cin.ignore(); return EXIT_SUCCESS; >

Впрочем, необходимо отметить, что сочетая такие приёмы со средствами C++, выходящими за пределы “чистого” C, вы рискуете нарваться на неопределённое поведение. Низкоуровневые средства требуют особой внимательности, так как компилятор в таких случаях не страхует программиста. В частности, в общем случае нельзя интерпретировать произвольный указатель как void* и наоборот без выполнения приведения типа. А это может произойти неявно, например, в примере выше мы полагаем, что указатель prev, указывающий на объект структуры Line совпадает с указателем на поле prev этого объекта.

Синтаксическая справка

Правило чтения сложных описаний типов

Конструкции, определяющие переменные или вводящие новые типы в языках C и C++, могут порой иметь довольно запутанный вид. Ниже дано правило, помогающее разобраться в смысле сложных конструкций.

  1. Начиная с имени (в случае typedef , в случае using имя находится вне — см. ниже), читать вправо, пока это возможно (до закрывающей круглой скобки или точки с запятой).
  2. Пока невозможно читать вправо, читать влево (убирая скобки).

Некоторые примеры “расшифровки” типов переменных:

// c (влево) константа char (const и char можно поменять местами) const char c; // str (влево) указатель на (влево) константу char (или константный массив из char) const char* str; // str (влево) константный (влево) указатель на константу char const char* const str; // n (вправо) массив (вправо) из 10 (влево) int int n[10]; // n (вправо) массив (вправо) из 10 (влево) указателей на (влево) int int* n[10]; // n (влево) указатель на (вправо) массив из 10 (влево) указателей на int int* (*n)[10]; // n указатель на массив из 10 (влево) указателей на (вправо) функции, не принимающие аргументов, // (влево) возвращающие указатели (влево) на константы типа int const int* (*(*n)[10])();

Разница между typedef и using

Директива typedef объявляет синоним типа. Используется синтаксис определения переменной, к которой добавили ключевое слово typedef , только вместо собственно переменной вводится синоним типа этой как-бы переменной с её именем.

int * p; // переменная: указатель на int typedef int * pt; // имя pt -- синоним типа "указатель на int" pt px; // тоже переменная типа "указатель на int"

В С++11 появилась возможность объявлять синонимы типов с помощью using-директивы в стиле инициализации переменных:

using pointer = type*;

Объявление typedef можно превратить в using-директиву, заменив typedef на using , вставив после using имя типа и знак равно и убрав это имя типа из объявления справа.

// то же, что typedef double (*Binary_op)(double, double); using Binary_op = double (*)(double, double);

Типы, ассоциируемые с массивами

Пусть N — константа времени компиляции и дано определение

float a[N];
  • float — тип элемента;
  • float& — ссылка на элемент, тип результата операции обращения по индексу, например a[0] ;
  • float* — указатель на элемент, например &a[0] ; a и &a автоматически неявно приводятся к float* ;
  • float[N] — формальный тип переменной a ;
  • float(*)[N] — формальный тип указателя на массив a , результат операции взятия адреса &a ;
  • float(&)[N] — тип ссылки на массив a ; a автоматически неявно приводятся к этому типу; так же как сам массив, ссылка на него автоматически приводится к указателю на массив и на его первый элемент.

Типы, ассоциируемые с функциями

Пусть дано объявление

float foo(int, int);
  • float — тип результата, получаемый при вызове функции, например foo(1, 2) ;
  • float(int, int) — формальный тип символа foo — foo не является переменной, так как переменные функционального типа невозможны, и тем не менее, имеет тип;
  • float(*)(int, int) — указатель на функцию, результат &foo ; foo автоматически неявно приводится к этому указателю;
  • float(&)(int, int) — ссылка на функцию; foo автоматически неявно приводится к этому типу; так же как сама функция, ссылка на неё автоматически приводится к указателю на неё же.

Зачем нужны ссылки на указатели?

Отсюда вопрос, зачем в С++ вообще существует подобный синтаксис, ведь strange_ref, несмотря на то, что является типом int*&, все равно будет иметь такой же функционал, как и int*. Все также можно будет разыменовать эту ссылку и тд. Почему этот синтаксис вообще существует и где он применяется?

Отслеживать
123k 24 24 золотых знака 128 128 серебряных знаков 307 307 бронзовых знаков
задан 25 сен 2021 в 18:57
ComeInRage ComeInRage
1,601 6 6 серебряных знаков 14 14 бронзовых знаков
void change_ptr(int *&p) < p = . ; >
25 сен 2021 в 19:03
Неявно такой синтаксис возникает в шаблонах.
25 сен 2021 в 19:04
@StanislavVolodarskiy А можно немного подробнее? Не понятно из этого короткого примера
25 сен 2021 в 19:24
В ответе ниже сразу обе ситуации: меняем указатели местами в шаблонной функции.
25 сен 2021 в 20:55

4 ответа 4

Сортировка: Сброс на вариант по умолчанию

Ну вот конкретный практический пример: типичная функция навроде swap без проблем работает с указателями, принимая их по ссылке, как и другие объекты:

template void swap(T & left, T & right) < T tmp; left = right; right = tmp; > 

Или несколько операций с предварительно выбранным одним указателем:

int * p1<>; int * p2<>; int * & pcur; . // много операций, изменяющих pcur; 

Собственно указатели являются такими же объектами, как и все остальные, и для них можно использовать все те же сценарии работы со ссылками.

Отслеживать
ответ дан 25 сен 2021 в 20:02
user7860670 user7860670
29.8k 3 3 золотых знака 17 17 серебряных знаков 36 36 бронзовых знаков

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

25 сен 2021 в 20:39

@ComeInRage Если принимать указатель не по ссылке, а по значению, то тогда бы исходный указатель не изменился. А если бы по указателю, то получился бы нужен еще один слой из разыменований.

25 сен 2021 в 20:45

несмотря на то, что является типом int*&, все равно будет иметь такой же функционал, как и int*

int x = 1, y = 2; int *a = &x; int *b = a; int *&c = a; a = &y; std::cout  

Отслеживать
123k 24 24 золотых знака 128 128 серебряных знаков 307 307 бронзовых знаков
ответ дан 25 сен 2021 в 19:04
HolyBlackCat HolyBlackCat
27.3k 3 3 золотых знака 27 27 серебряных знаков 40 40 бронзовых знаков

Спасибо за ответ, но у вас указателю присваивается int. Из-за этого не совсем понятно, что вы хотели сказать)

25 сен 2021 в 19:18

Однако я понял в чем разница. Но все равно не понятно, зачем этот синтаксис вообще нужен. Но спасибо за ответ, хоть прояснилась ситуация

25 сен 2021 в 19:24

@ComeInRage ссылка это почти тоже самое что и указатель, только ссылка не может быть невалидной в отличии от указателя.

25 сен 2021 в 19:40
@ComeInRage, ну пропустил он один амперсанд.
25 сен 2021 в 20:17

Ссылка на указатель оправдана, например, если она параметр функции или операнд перегруженного оператора. Да, эта ссылка имеет синтаксис не целевой величины, а указателя. Но если функция должна не взять готовый указатель у вызывающего кода, а вернуть его в вызывающий код, но сделать это через параметр, то по указателю это сделать нельзя, так как он указывает на целевую величину, а не на указатель.

 size_t f(int *p) < size_t s; s=(512+((rand())% 1024)); p=new int [s]; return s; >int main ()

Здесь p – указатель не на x, а на память, выделенную оператором new. А где лежит x? Функции надо знать именно это, только тогда она сможет положить в этот x указатель на память, выделенную оператором new. Куда класть адрес выделенной памяти? В x? Или в y?

 size_t f(int *&p) < size_t s; s=(512+((rand())% 1024)); p=new int [s]; return s; >int main ()

А вот здесь всё в порядке, p – ссылка на x, теперь можно в этот x положить значение, возвращённое оператором new. А вот x уже будет указателем на память, выделенную по new. Или Вы не это имели ввиду?

Отслеживать
ответ дан 9 ноя 2022 в 6:26
Тарас Атавин Тарас Атавин
204 1 1 серебряный знак 9 9 бронзовых знаков

Могу дополнить уже данные ответы своей интерпретацией. Возможно кому-то так будет проще разобраться.

Смысл ссылки на указатель в том чтобы работать с исходным указателем. дело в том, что если передать просто указатель - это работает как передача по значению, то есть изменения переданного указателя внутри метода или функции не приводят к изменению значения исходного (переданного) указателя. изменится только его локальная копия. если вам нужно модифицировать не объект, расположенный в памяти на которую указывает аргумент, а сам адрес на который указывает аргумент - кажется нет другого выхода кроме как передавать указатель по ссылке (ссылку на указатель). да, синтаксис при этом останется как при использовании обычного указателя.

Указатели и ссылки в языке C++

Указатели представляют собой объекты, значением которых служат адреса других объектов:

  • переменных
  • констант
  • функций
  • других указателей
Объявление указателей

<тип> *<имя_переменной>[,*<имя_переменной>].

Синтаксис объявления указателей аналогичен объявлению переменных, за исключением того, что между типом данных и именем переменной должен быть указан символ "*" ("звездочка").

Инициализация указателей

Указателю можно присвоить адрес объекта, полученный с помощью оператора взятия адреса &. Стоит отметить, что оператор & не возвращает напрямую адрес своего операнда. Вместо этого он возвращает указатель, содержащий адрес.

Указателю нельзя присвоить адрес переменной другого типа. То есть нельзя указателю типа int* присвоить адрес переменной типа double.

Также указателю можно присвоить значение другого указателя.

Указатель также может быть проинициализирован пустым значением. Это можно сделать несколькими способами:

  • использовать значение 0 или макроопределение NULL
  • использовать значение nullptr
  • использовать значение std::nullptr_t (C++ 11)

В некоторых случаях, использование значения 0 в качестве аргумента функции может привести к проблемам, так как компилятор не сможет определить, используется ли нулевой указатель или целое число. Поэтому использование значения nullptr является предпочтительным способом присвоить указателю пустое значение.

Тип std::nullptr_t может иметь только одно значение - nullptr. Использование этого типа поможет в тех редких случаях, когда существуют перегруженные функции и требуется передать нулевой указатель. В этом случае непонятно какую именно функцию нужно будет вызвать. Поэтому в таком случае в функции можно задать аргумент с типом std::nullptr_t.

Напрямую записать адрес в указатель можно только с помощью операций преобразования типов, либо операции reinterpret_cast.

int a = 0; int *p = &a; double v = 0.1; double *pv = &v; char *pc = nullptr;
Разыменование указателей

Для получения значения переменной, на которую ссылается указатель, используется операция разыменования указателя. Эта операция записывается как символ * (звездочка), написанный перед указателем.

int a = 123; int *p = &a; int b = *p; // b присваивается значение 123
Арифметические действия с указателями

С указателем можно производить следующие арифметические действия:

  • сложение и вычитание с целым числом
  • операции инкремента/декремента

При использовании арифметических операций, указатель изменяется на величину кратную размеру типа указателя. Например, если указатель имеет тип 32-разрядного int, то увеличение указателя на 1 приведет к увеличению значения адреса в указателе на 4.

Указатель на указатель

В языке C++ можно объявить указатель, который будет указывать на другой указатель.

Синтаксис объявления такой же, как и у объявления указателя, за исключением того, что ставится два символа * (звездочка).

<тип> **<имя_переменной>[,**<имя_переменной>].

Указатель на указатель работает подобно обычному указателю: его можно разыменовать для получения значения, на которое он указывает. И, поскольку этим значением является другой указатель, для получения исходного значения потребуется выполнить разыменование еще раз. Разыменования можно выполнять последовательно:

int value = 1234; int *p = &value; int **pp = &p; int val = **pp; // 1234

Использовать указатели на указатели может потребоваться, например для создания массива из массивов, и в частности массива из строк.

Язык C++ также позволяет работать с указателями на указатели на указатели, или сделать еще большую вложенность. Их можно объявлять просто увеличивая количество символов * (звездочек). Однако на практике такие указатели используются крайне редко.

Неконстантный указатель на неконстантное значение

int val1 = 10; int val2 = 20; int* ptr = &val1; std::cout 

В этом случае можно изменять как сам указатель, так и значение, на которое он указывает.

Неконстантный указатель на константное значение

const int val1 = 10; const int val2 = 20; const int* ptr = &val1; std::cout 

В этом случае указатель можно изменять. Но само значение, на которое он указывает изменять нельзя.

То же самое поведение можно получить, даже если переменные указаны как неконстантные. Для этого достаточно сам указатель объявить таким образом, чтобы он якобы указывал на константное значение:

int val1 = 10; int val2 = 20; const int* ptr = &val1; std::cout 

Константный указатель на неконстантное значение

int val1 = 10; int val2 = 20; int* const ptr = &val1; std::cout 

В этом случае можно изменять значение, на которое указывает указатель. Но нельзя изменять сам указатель.

Кроме того указатель при объявлении нужно сразу инициализировать.

Константный указатель на константное значение

int val1 = 10; int val2 = 20; const int* const ptr = &val1; std::cout 

В этом случае нельзя менять ни указатель, ни значение, на которое он указывает.

Ссылки

Ссылка - это тип переменной в языке C++, который работает как псевдоним другого объекта или значения. При объявлении ссылки перед её именем ставится символ амперсанда &. Сама же ссылка не может быть пустой, и должна быть обязательно проинициализирована именем переменной, на которую она ссылается. Изменить значение ссылки после инициализации невозможно.

<тип> &<имя_ссылки> = <имя_переменной>[, &<имя_ссылки> = <имя_переменной>].

При создании ссылки на константную переменную, ссылка тоже должна быть создана как константная. Можно также создать константную ссылку на обычную переменную: в этом случае изменить переменную через ссылку не получится.

Любые действия со ссылкой трактуются компилятором как действия, которые будут выполняться над объектом, на который она ссылается.

Ссылки чаще всего используются в качестве параметров в функциях. В этом контексте ссылка-параметр работает как псевдоним аргумента, а сам аргумент не копируется при передаче в параметр. Это в свою очередь улучшает производительность, если аргумент слишком большой или затратный для копирования.

Также ссылка может использоваться и при возврате значений из функции, однако тут следует быть осторожным, так как, если переменная, на которую ссылается ссылка, выйдет из области видимости (например локальная переменная в функции), то это приведет к неопределенному поведению программы.

int value = 123; int &refval = value; refval = 12345; std::cout 

Ссылки r-value

В стандарте C++11 ввели новый тип ссылок - ссылки r-value. Ссылки r-value - это ссылки, которые инициализируются только значениями r-values. Объявляются такие ссылки, в отличие от обычных, с помощью двух символов амперсанда &&.

<тип> &&<имя_ссылки> = <выражение r-value>[, &&<имя_ссылки> = <выражение r-value>].

Ссылки r-value, в отличие от обычных ссылок, ссылаются не на постоянный, а на временный объект, созданный при инициализации ссылки r-value.

Такие ссылки обладают двумя важными свойствами:

  • продолжительность жизни объекта, на который ссылается ссылка увеличивается до продолжительности жизни самой ссылки
  • неконстантные ссылки r-value позволяют менять значение r-values, на который они ссылаются
int &&ref = 10; ref = ref + 20; std::cout 

Ссылки r-value - позволяют избегать логически ненужного копирования и обеспечивать возможность идеальной передачи (perfect forwarding). Прежде всего они предназначены для использования в высокопроизводительных проектах и библиотеках.

  • Уголок в Вконтакте
  • Уголок в Телеграм
  • Уголок в YouTube

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *