Почему разрешено приводить указатель на ссылку? - PullRequest
27 голосов
/ 08 мая 2011

Изначально являясь темой этого вопроса , выяснилось, что ФП просто упустил из виду разыменование. Между тем, этот ответ заставил меня и некоторых других задуматься - почему разрешено приводить указатель на ссылку с приведением в стиле C или reinterpret_cast?

int main() {
    char  c  = 'A';
    char* pc = &c;

    char& c1 = (char&)pc;
    char& c2 = reinterpret_cast<char&>(pc);
}

Приведенный выше код компилируется без каких-либо предупреждений или ошибок (относительно преобразования) в Visual Studio, в то время как GCC выдаст только предупреждение, как показано здесь .


Моей первой мыслью было, что указатель каким-то образом автоматически разыменовывается (я работаю с MSVC нормально, поэтому я не получаю предупреждения GCC), и попытался сделать следующее:

#include <iostream>

int main() {
    char  c  = 'A';
    char* pc = &c;

    char& c1 = (char&)pc;
    std::cout << *pc << "\n";

    c1 = 'B';
    std::cout << *pc << "\n";
}

С очень интересным выводом, показанным здесь . Таким образом, кажется, что вы обращаетесь к указанной переменной, но в то же время это не так.

Идеи? Объяснения? Стандартные цитаты?

Ответы [ 5 ]

37 голосов
/ 08 мая 2011

Ну, это цель reinterpret_cast!Как следует из названия, цель этого приведения - переосмыслить область памяти как значение другого типа.По этой причине, используя reinterpret_cast, вы всегда можете привести lvalue одного типа к ссылке другого типа.

Это описано в 5.2.10 / 10 спецификации языка.Там также говорится, что reinterpret_cast<T&>(x) - это то же самое, что и *reinterpret_cast<T*>(&x).

Тот факт, что вы используете указатель в этом случае, совершенно и совершенно не важен.Нет, указатель не разыменовывается автоматически (принимая во внимание интерпретацию *reinterpret_cast<T*>(&x), можно даже сказать, что верно обратное: адрес этого указателя берется автоматически).Указатель в этом случае служит просто «некоторой переменной, которая занимает некоторую область в памяти».Тип этой переменной не имеет значения вообще.Это может быть double, указатель, int или любое другое значение.Переменная просто обрабатывается как область памяти, которую вы переинтерпретируете как другой тип.

Что касается приведения в стиле C - в этом контексте она просто интерпретируется как reinterpret_cast, поэтомунемедленно применяется к нему.

Во втором примере вы прикрепили ссылку c к памяти, занятой переменной-указателем pc.Когда вы сделали c = 'B', вы принудительно записали значение 'B' в эту память, полностью уничтожив исходное значение указателя (перезаписав один байт этого значения).Теперь уничтоженный указатель указывает на непредсказуемое местоположение.Позже вы попытались разыменовать этот уничтоженный указатель.То, что происходит в таком случае, является вопросом чистой удачи.Программа может аварийно завершить работу, поскольку указатель, как правило, не подлежит исправлению.Или вам может повезти и указатель указывает на какое-то непредсказуемое, но действительное местоположение.В этом случае ваша программа что-то выведет.Никто не знает, что он будет выводить, и в этом нет никакого смысла.

Можно переписать вашу вторую программу в эквивалентную программу без ссылок

int main(){
    char* pc = new char('A');
    char* c = (char *) &pc;
    std::cout << *pc << "\n";
    *c = 'B';
    std::cout << *pc << "\n";
}

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

4 голосов
/ 08 мая 2011

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

Стандарт C ++ указывает, что приведение reinterpret_cast<U&>(t) эквивалентно *reinterpret_cast<U*>(&t).

ВВ нашем случае U равно char, а t равно char*.

Расширяя их, мы видим, что происходит следующее:

  • мы берем адресаргумент для приведения, приводящий к значению типа char**.
  • мы reinterpret_cast это значение к char*
  • мы разыменовываем результат, получая значение char l.1024 *

reinterpret_cast позволяет преобразовывать указатели любого типа в любой другой тип указателя.Итак, состав от char** до char* правильно сформирован.

3 голосов
/ 18 февраля 2017

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

  • C не имел ссылочных типов, он имел только значения иуказатели,
    , поскольку физически в памяти у нас есть только значения и указатели.
  • В C ++ мы добавили ссылки на синтаксис, но вы можете рассматривать их как своего рода синтаксический сахар - естьнет специальной структуры данных или схемы расположения памяти для хранения ссылок.

Что же это за ссылка с этой точки зрения?Или, скорее, как бы вы «внедрили» ссылку?С указателем, конечно.Поэтому, когда вы видите ссылку в каком-то коде, вы можете притвориться, что это действительно просто указатель, который использовался особым образом: если int x; и int& y{x};, тогда у нас действительно есть int* y_ptr = &x;и если мы говорим y = 123;, мы просто имеем в виду *(y_ptr) = 123;.Это не отличается от того, как, когда мы используем индексы массива C (a[1] = 2;), на самом деле происходит то, что a «затухает» для обозначения указателя на его первый элемент, а затем выполняется 1011 *.

(Примечание: компиляторы не на самом деле всегда держат указатели за каждой ссылкой; например, компилятор может использовать регистр для указанной ссылки, а затем указатель не может указыватьк этому. Но метафора все еще довольно безопасна.)

Приняв метафору «ссылка - это на самом деле просто замаскированный указатель», теперь не должно быть удивительным, что мы можем игнорировать эту маскировку с помощью reinterpret_cast<>().

PS - std::ref на самом деле просто указатель, когда вы углубляетесь в него.

0 голосов
/ 08 мая 2011

Это разрешено, потому что C ++ допускает почти все, когда вы читаете.

Но что касается поведения:

  • ПК - 4-байтовый указатель
  • (char) pc пытается интерпретировать указатель как байт, в частности последний из четырех байтов
  • (char &) pc тоже самое, но возвращает ссылку на этот байт
  • Когда вы впервые печатаете компьютер, ничего не происходит, и вы видите письмо, которое вы сохранили
  • c = 'B' изменяет последний байт 4-байтового указателя, поэтому теперь он указывает на что-то еще
  • Когда вы печатаете снова, вы теперь указываете на другое место, которое объясняет ваш результат.

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

0 голосов
/ 08 мая 2011

когда вы используете приведение в стиле C или reinterpret_cast, вы в основном говорите компилятору смотреть в другую сторону («не против, я знаю, что делаю»)

C ++ позволяет вам сказать компилятору сделать это.Это не значит, что это хорошая идея ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...