Подтвердить что ты не робот

Почему разрешено указывать указатель на ссылку?

Первоначально являясь темой этого вопроса, выяснилось, что ОП просто упустил разницу. Между тем, этот ответ заставил меня и некоторых других подумать - почему это разрешено отбрасывать указатель на ссылку с помощью 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";
}

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

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

4b9b3361

Ответ 1

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

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

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

Что касается каста 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";
}

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

Ответ 2

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

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

В нашем случае U есть char, а t - char*.

Развернув их, мы увидим следующее:

  • мы берем адрес аргумента в литье, давая значение типа char**.
  • we reinterpret_cast это значение char*
  • мы разыгрываем результат, получая char lvalue.

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

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

Ответ 5

Его разрешено, потому что С++ позволяет практически что угодно, когда вы делаете бросок.

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

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

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