Отличие ссылок от указателей в языке C++
Очень часто, у тех кто успешно освоил указатели, при изучении ссылок возникает вопросы: а чем ссылки отличаются от указателей? Только синтаксисом использования и невозможностью сущестования неинициализованной ссылки? Каким образом сделаны ссылки и как они работают?
Пролить свет на этот вопрос поможет следующее, достаточно удачное объяснение:
Ссылка — это переменная, которая указывает на другую переменную, хранящуюся в той же памяти.
Следует обратить внимание, что в этом определении нигде не сказано, что значение ссылки — это адрес, указывающий на значение другой переменной. Здесь сказано лишь что переменная указывает на другую переменную. И это важно.
Переменная — это по-сути, именованная ячейка (либо именованный набор соседних ячеек) в памяти компьютера. Когда работает компилятор, он для текущего контекста создает таблицу используемых переменных. Таблица выглядит примерно так:
Имя переменной | Тип | Адрес расположения переменной | |
1 | first | int | 0x50A5B7F2 |
2 | second | long | 0x50A5B804 |
Значение переменной компилятор не хранит — все значения переменных существуют только в момент исполнения программы. Компилятор может только сгенерировать команды инициализации и изменения значений переменных. Поэтому в этой таблице нет столбца со значением переменной.
Теперь если добавить переменную-указатель на first, то в таблице переменных появится еще одна запись:
Имя переменной | Тип | Адрес расположения переменной | |
1 | first | int | 0x50A5B7F2 |
2 | second | long | 0x50A5B804 |
3 | pointer_first | int* | 0x50A5BA08 |
То есть, указатель pointer_first — это переменная, у которой есть свой адрес 0x50A5BA08, и по этому адресу (в момент выполнения программы) будет находиться значение, равное 0x50A5B7F2. То есть, указатель указывает не на переменную first, а на значение переменной, которое расположено в памяти по адресу 0x50A5B7F2. Переменная first может даже перестать существовать, и в месте, где лежали ее данные могут быть размещены другие данные. А указатель pointer_first так и будет указывать на ту же самую ячейку (ячейки) памяти с адресом 0x50A5B7F2. И в этом проблема «сырых» указателей, приводящая к утечкам памяти и обращению к неправильным адресам.
Чтобы в каком-то виде решить эти проблемы, и заодно упростить синтаксис, были придуманы ссылки. Для объяснения работы ссылок, можно создать ссылку на переменную second. В таблице переменных появится еще одна запись:
Имя переменной | Тип | Адрес расположения переменной | |
1 | first | int | 0x50A5B7F2 |
2 | second | long | 0x50A5B804 |
3 | pointer_first | int* | 0x50A5BA08 |
4 | link_second | long& | 0x50A5BA10 |
Чтобы было проще, можно сказать, что переменная-ссылка link_second — это переменная, у которой, как и у любой другой переменной, есть свой адрес 0x50A5BA10, в котором хранится некое значение. Но это значение — не адрес, по которому находятся данные переменной second. Нет! В этом значении хранится ссылка на переменную second. Можно считать, что там хранится номер строки таблицы, в данном случае 2.
Из предыдущего абзаца следует, что невозможно создать ссылку на несуществующую переменную, так как ее просто не будет в таблице переменных. Компилятору очень легко контролировать, на какую переменную ссылается переменная-ссылка. Невозможна ситуация, чтобы переменная-ссылка оказалась неинициализирована: ссылка всегда указывает на какую-то другую переменную (т. е. содержит номер уже существующей переменной).
Вот такого объяснения вполне достаточно для того, чтобы понять отличие ссылки от указателя. Остался только один вопрос: почему при описании внутреннего устройства ссылки использовались такие обтекаемые выражения как «можно сказать», «можно считать, что»? А все дело в том, что ссылки — это, как говорят некоторые программисты — синтаксический сахар, который в конечном итоге компилируется в косвенную адресацию. А как это будет конкретно реализовано — это вопрос к авторам компилятора. Реализация данного механизма может сильно отличаться от компилятора к компилятору. Но так как C++ в некотором смысле высокоуровневый язык, то устройство ссылок можно воспринимать так, как написано выше.
Указатели и ссылки в языке C++. А также Java и C♯
Указатели и ссылки в языке C++
А также Java и C♯
В языке C++ практически с самого начала его существования есть две реализации ссылочного типа: это указатели (pointers) и ссылки (references). Указатели достались от языка C. Ссылки не заменили указатели во всём, а умные/интеллектуальные указатели (smart pointers) не всегда стоит использовать вместо «сырых» (raw pointers). В C++11 появился новый тип — rvalue-ссылки — который преследует совсем иные цели.
Ссылки появились в C++ ещё в тот момент, когда он миновал этап C with Classes, для перегрузки операторов:
«Дуг Макилрой вспоминает, что однажды я объяснял ему некоторые проблемы, касавшиеся схемы перегрузки операторов. Он употребил слово „ссылка“, после чего я, пробормотав „спасибо“, выбежал из его кабинета, чтобы на следующий день появиться с практически готовым решением, которое и вошло в язык. Просто Дуг тогда напомнил мне об Algol68».
В первом приближении разница между указателями и ссылками такова (для сравнения — ссылки в языках Java и C♯).
Указатели в C
Указатели в C++
Ссылки в C++
Ссылки в языках Java и C♯
Обязательно инициализировать
–
–
+
–
Можно переназначить
+ (если не const)
+ (если не const)
–
+
Значение по умолчанию
Мусорное
Мусорное
–
null
Невалидное значение
NULL
NULL/nullptr (C++11)
–
null
Коллекции
+
+
–
+
Арифметика
+
+
–
–
Косвенность
+
+
–
–
Из таблицы видно, что ссылки в Java и C♯, несмотря на название, больше похожи на указатели в C и C++ (но без присущей им арифметики и косвенности — как указатели на указатели).
Указатели и ссылки различаются по константности. Ссылки больше всего напоминают в этом отношении указатели, которые нельзя изменять.
Может изменяться
Не может изменяться
Может изменять то, на что указывает
T* ptr
T* const ptr,
T& ref
Не может изменять то, на что указывает
const T * ptr ≡ T const * ptr
const T * const ptr,
const T& ref ≡ T const& ref
По указателю в C++ принято передавать объекты в низкоуровневые API или в такие функции, аргумент которых может равняться nullptr; их используют для внутренней организации структур данных. По ссылкам принято передавать объекты во всех остальных случаях, когда нужно избежать копирования или изменить объект внутри функции.
Бьерн Страуструп, создатель языка C++:
«В языке Java есть указатели. На самом деле там почти всё неявно оказывается указателем. Просто там их называют ссылками (references). В том, что указатели неявные, есть свои преимущества и недостатки.
Благодаря повсеместному и неявному использованию ссылок в Java, возможно, удалось упростить модель программирования и сборку мусора, но расход памяти при этом резко вырос — и соответственно выросли стоимость доступа к памяти (осуществляемого более косвенным образом) и расходы на выделение памяти.
Чего в языке Java, к счастью, нет, так это допускаемого языками C и C++ неправильного применения арифметики указателей. Но и эта проблема решается, если корректно писать код на языке C++, используя такие высокоуровневые абстракции, как потоки ввода/вывода, контейнеры и алгоритмы, а не занимаясь вознёй с указателями. […]
Есть, однако, важная область, где указатели и действия с ними оказываются большим подспорьем: прямое и эффективное описание структур данных.
Ссылок Java здесь оказывается недостаточно: например, с их помощью не опишешь операцию swap. Другой пример — простота непосредственного доступа к реальной памяти с помощью указателей: в любой системе должен быть язык, который может это сделать, и часто им оказывается C++.„Отрицательная“ сторона наличия указателей (и массивов в стиле C) — это, конечно, возможность злоупотреблений: переполнение буфера, указатели на удалённую память, неинициализированные указатели и т. п. Но если корректно писать код на языке C++, всё не столь страшно. […] Те, кто привык работать на C или придерживается старого стиля в языке C++, с трудом верят тому, что управление ресурсами на основе областей видимости представляет собой исключительно мощный инструмент».
14 июня
C++
© MMXI—MMXXII. RSS. Поддержать сайт
Светлая тема / тёмная тема
Свойство CSS-указатели-события
❮ Назад Полное руководство по CSS Далее ❯
Пример
Установить, должен ли элемент реагировать на события указателя:
div. ex1 {
события указателя: нет;
}
div.ex2 {
события указателя: авто;
}
Попробуйте сами »
Определение и использование
Свойство pointer-events
определяет, реагирует ли элемент на
события указателя.
Значение по умолчанию: | авто |
---|---|
Унаследовано: | да |
Анимация: | № Читать о анимируемом |
Версия: | CSS3 |
Синтаксис JavaScript: | объект .style.pointerEvents=»нет» Попытайся |
Поддержка браузера
Числа в таблице указывают первую версию браузера, которая полностью поддерживает это свойство.
Собственность | |||||
---|---|---|---|---|---|
указатели-события | 2,0 | 11,0 | 3,6 | 4,0 | 9,0 |
Синтаксис CSS
события указателя: auto|none;
Значения свойств
Значение свойства | Описание |
---|---|
авто | Элемент реагирует на события указателя, такие как :hover и click. Это по умолчанию |
нет | Элемент не реагирует на события указателя |
начальный | Устанавливает для этого свойства значение по умолчанию. Читать про начальный |
унаследовать | Наследует это свойство от родительского элемента. Читать о унаследовать |
❮ Предыдущий Полное руководство по CSS Следующий ❯
ВЫБОР ЦВЕТА
Лучшие учебники
Учебное пособие по HTMLУчебное пособие по CSS
Учебное пособие по JavaScript
Учебное пособие
Учебное пособие по SQL
Учебное пособие по Python
Учебное пособие по W3.CSS
Учебное пособие по Bootstrap
Учебное пособие по PHP
Учебное пособие по Java
Учебное пособие по C++
Учебное пособие по jQuery
9004 900 Справочник по HTML
Справочник по CSS
Справочник по JavaScript
Справочник по SQL
Справочник по Python
Справочник по W3. CSS
Справочник по Bootstrap
Справочник по PHP
Цвета HTML
Справочник по Java
Справочник по Angular
Справочник по jQuery
Основные примеры
Примеры HTMLПримеры CSS
Примеры JavaScript
Примеры инструкций
Примеры SQL
Примеры Python
Примеры W3.CSS
Примеры Bootstrap
Примеры PHP
Примеры Java
Примеры XML
Примеры jQuery
ФОРУМ | О
W3Schools оптимизирован для обучения и обучения. Примеры могут быть упрощены для улучшения чтения и обучения. Учебники, ссылки и примеры постоянно пересматриваются, чтобы избежать ошибок, но мы не можем гарантировать полную правильность всего содержания. Используя W3Schools, вы соглашаетесь прочитать и принять наши условия использования, куки-файлы и политика конфиденциальности.
Copyright 1999-2022 Refsnes Data. Все права защищены.
W3Schools работает на основе W3.CSS.
Техника двух указателей в связном списке
Два указателя перемещаются параллельно
Рассмотрим следующую задачу:
Создайте функцию, которая возвращает n-й последний элемент односвязного списка.
Обратите внимание, что нас просят искать не n-й элемент с начала, а n-й элемент с конца. Чтобы сделать это, нам нужен какой-то способ узнать, как далеко мы находимся от конца самого списка. Однако в односвязном списке нет простого способа вернуться назад по списку, когда вы найдете конец, поэтому нам нужно полагаться на нечто, известное как 9.0044 метод двух указателей . Метод двух указателей позволяет нам сохранить два указателя, ссылающихся на два разных места в связанном списке. Если мы смещаем указатели или увеличиваем их с разной скоростью, мы можем решить много интересных задач, которые мы не можем решить с помощью всего лишь одного указателя.
Наименее эффективное решение без указателя
Чтобы лучше понять технику двух указателей и ее эффективность, давайте кратко рассмотрим менее оптимальное решение, которое может прийти на ум первым.
Одним из решений, которое может прийти на ум, является использование массива для хранения представления связанного списка. Затем мы можем просто вернуть узел по индексу с конца минус n
.
func getNodeFromLast (из списка: LinkedList, at n: Int) -> Узел? {
var linkedListArray = [Node?]()
var currentNode = list.head
while currentNode != nil {
linkedListArray.append(currentNode!)
currentNode = currentNode!.next
}
return linkedListArray[linkedListArray.count - n];
}
Несмотря на то, что этот подход приводит к легко читаемой реализации, он также может использовать много памяти, поддерживая двойное представление одних и тех же данных. Если в связанном списке один миллион узлов, нам понадобится один миллион указателей в массиве ArrayList
, чтобы отслеживать его! Подобный подход приводит к дополнительному выделению O(n)
пространства.
Решение с двумя указателями
Вместо того, чтобы создавать полностью параллельный список, мы можем решить эту проблему, выполняя итерацию с двумя указателями, один из которых ищет хвост, а другой отстает на n-ю величину.
текущий = ноль tailSeeker = заголовок связанного списка количество = 0 в то время как tailSeeker не равен нулю если количество > n - 1 если текущий == ноль текущий = заголовок связанного списка переместить текущий указатель вперед переместить хвостовую искатель вперед счетчик приращений return current
Solution
В Swift мы могли бы реализовать функцию nth-last-node-finder как таковую:
функция getNodeFromLast (из списка: LinkedList, at n: Int) -> Узел? {
переменная текущая:узел? = nil
var tailSeeker = list.head
var count = 0
while tailSeeker != nil {
if count > n - 1 {
if current == nil {
current = list03. head }
current = current!.next
}
tailSeeker = tailSeeker!.next
count += 1
}
return current
}
Используя этот подход, мы можем решить эту задачу эффективно, за O(n)
времени (мы должны выполнить итерацию всего списка один раз) и O(1)
пространственной сложности (мы всегда использовать только три переменные независимо от размера списка: два указателя и счетчик).
Стрелки с разной скоростью
Использование двух указателей, движущихся со смещением, но параллельно, было хорошим решением предыдущей проблемы. Однако есть и другие проблемы, когда два указателя, перемещающиеся параллельно, не были бы так полезны. Давайте рассмотрим одну из этих проблем и рассмотрим новую стратегию, в которой используются два указателя, движущиеся с разной скоростью.
Попробуйте найти средний узел связанного списка.
Как и прежде, можно найти решение, перебрав весь список, создав представление массива и затем вернув средний индекс. Но, как и прежде, это потенциально может занять много дополнительного места:
create array в то время как связанный список не был полностью повторен через добавить текущий элемент в массив двигаться вперед на один узел return array[length / 2]
Подход: быстрые и медленные указатели
Вместо этого мы можем использовать два указателя для перемещения по списку. Первый указатель делает два шага по списку на каждый один шаг, который делает второй, поэтому он выполняет итерацию в два раза быстрее.
fastPointer = заголовок списка slowPointer = заголовок списка в то время как fastPointer не равен нулю переместить быстрый указатель вперед если конец списка не достигнут снова переместить fastPointer вперед двигаться медленноУказатель вперед return slowPointer
Когда первый указатель достигает конца списка, «более медленный» второй указатель будет указывать на средний элемент. Давайте визуализируем шаги алгоритма:
Начальное состояние
F С 1 2 3 4 5 6 7
Первый тик
F С 1 2 3 4 5 6 7
Второй тик
F С 1 2 3 4 5 6 7
Третий тик
F С 1 2 3 4 5 6 7
Финальный тик
F С 1 2 3 4 5 6 7 nil
Пока мы всегда сначала перемещаем быстрый указатель и проверяем, что он не равен нулю, прежде чем снова перемещать его и медленный указатель, мы завершим итерацию в нужное время и получим ссылка на средний узел с медленным указателем.
Решение и альтернативы
функция getMiddleNode(из списка:LinkedList) -> Узел? {
var fast = list.head
var slow = list.head
while fast != nil {
fast = fast?.next
if fast != nil {
fast = fast?.next
slow = slow?.next
}
}
return slow
}
Как и в предпоследнем решении, это решение имеет O(n)
временная сложность и O(1)
пространственная сложность, поскольку создаются только два узла независимо от размера входного списка.
Half-Speed
Другим не менее правильным решением является перемещение быстрого указателя один раз при каждой итерации цикла, но перемещение медленного указателя только при каждой второй итерации:
func getMiddleNode(from list:LinkedList) -> Node ? {
var count = 0
var fast = list.