c - Ссылка на символ*, который вышел из сферы действия




(5)

Внутри области, где b определен, ему присваивается адрес строкового литерала. Эти литералы обычно живут в разделе памяти только для чтения, а не в стеке.

Когда вы делаете a=b вы назначаете значение b для a , то a теперь содержит адрес строкового литерала. Этот адрес остается в силе после того, как b выходит за рамки.

Если вы взяли адрес b а затем попытались разыменовать этот адрес, вы будете ссылаться на неопределенное поведение .

Таким образом, ваш код действителен и не вызывает неопределенное поведение, но следующее:

int *a = NULL;
{
    int b = 6;
    a = &b;
}

printf("b=%d\n", *a);

Другой, более тонкий пример:

char *a = NULL;
{
    char b[] = "stackoverflow";
    a = b;
}

printf(a);

Разница между этим примером и вашим заключается в том, что b , который является массивом, распадается на указатель на первый элемент при назначении a . Таким образом, в этом случае a содержит адрес локальной переменной, которая затем выходит за пределы области видимости.

РЕДАКТИРОВАТЬ:

В качестве побочной заметки, это плохая практика передачи переменной в качестве первого аргумента printf , поскольку это может привести к уязвимости строки формата . Лучше использовать строчную константу следующим образом:

printf("%s", a);

Или проще:

puts(a);

Недавно я начал программировать на C после того, как программировал на C ++ некоторое время, и мое понимание указателей немного ржавое.

Я хотел бы спросить, почему этот код не вызывает никаких ошибок:

char* a = NULL;
{
    char* b = "stackoverflow";
    a = b;
}

puts(a);

Я думал, что, поскольку b выходит из области видимости, необходимо ссылаться на несуществующую ячейку памяти, и, таким образом, они будут ошибкой во время выполнения при вызове printf .

Я запускал этот код в MSVC примерно 20 раз, и никаких ошибок не было.


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

Давайте рассмотрим этот вариант кода, который недействителен:

#include <stdio.h>
int main(void)
{
    int *a;
    {
        int b = 42;
        a = &b;
    }
    printf("%d\n", *a); // undefined behavior
    return 0;
}

Эта программа имеет неопределенное поведение, но довольно вероятно, что на самом деле это будет печать 42 по нескольким причинам: многие компиляторы оставят слот стека для b выделенный для всего main объекта, потому что больше ничего не требуется пространство и минимизация количества корректировок стека упрощает создание кода; даже если компилятор официально освободил слот стека, число 42, вероятно, остается в памяти до тех пор, пока что-то его не перезапишет, и между ними нет ничего между a = &b и *a ; стандартные «оптимизации» («постоянное распространение и распространение копии») могут устранить обе переменные и записать последнее известное значение для *a непосредственно в оператор printf (как если бы вы написали printf("%d\n", 42) ).

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

Как последнее замечание, ни один из агрессивных инструментов отладки, к которым у меня есть удобный доступ (Valgrind, ASan, UBSan), достаточно долго отслеживает «автоматические» сроки жизни переменных, чтобы уловить эту ошибку, но GCC 6 действительно вызывает это забавное предупреждение:

$ gcc -std=c11 -O2 -W -Wall -pedantic test.c
test.c: In function ‘main’:
test.c:9:5: warning: ‘b’ is used uninitialized in this function
    printf("%d\n", *a); // undefined behavior
    ^~~~~~~~~~~~~~~~~~

Я верю, что произошло здесь, это была оптимизация, описанная выше - копирование последнего известного значения b в *a а затем в printf но его «последнее известное значение» для b было «эта переменная неинициализирована», скорее, чем 42. (Затем он генерирует код, эквивалентный printf("%d\n", 0) .)


Строка за строкой, это то, что делает ваш код:

char* a = NULL;

a - указатель, не ссылающийся на что-либо (установленное в NULL ).

{
    char* b = "";

b - указатель, ссылающийся на статический, константный строковый литерал "" .

    a = b;

a также ссылается на статический константный строковый литерал "" .

}

b выходит за рамки. Но так как a не ссылается на b , то это не имеет значения (он просто ссылается на один и тот же статический константный строковый литерал, как указывал b ).

printf(a);

Распечатывает статический константный строковый литерал "" ссылается a .


Строковые литералы всегда распределяются статически, и программа может получить доступ в любое время,

char* a = NULL;
{
    char* b = "";
    a = b;
}

printf(a);

Здесь память для строкового литерала выделяется компилятором так же, как он выделяет память для int / char переменных или указателей

Разница в том, что строковый литерал является местом в разделе / ​​сегменте READONLY. Переменная b выделяется в стеке, но она удерживает адрес памяти раздела только для чтения / segmemt.

В коде var 'b' имеется адрес строкового литерала. Даже когда b теряет свой объем, память для строкового литерала всегда будет выделена

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

Обратитесь к бинарной спецификации ELF, чтобы лучше понять ее


Я думаю, что, как доказательство предыдущих ответов, хорошо взглянуть на то, что действительно сидит внутри вашего кода. Люди уже упоминали, что строковые литералы лежат внутри раздела .text. Итак, они (литералы) просто, всегда, есть. Вы можете легко найти это для кода

#include <string.h>

int main() {
  char* a = 0;
  {
    char* b = "";
    a = c;
  }
  printf("%s\n", a);
}

используя следующую команду

> cc -S main.c

внутри main.s вы найдете, на самом дне

...
...
...
        .section        __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
        .asciz  ""

L_.str.1:                               ## @.str.1
        .asciz  "%s\n"

Подробнее о разделах ассемблера (например) можно прочитать здесь: https://docs.oracle.com/cd/E19455-01/806-3773/elf-3/index.html

И здесь вы можете найти очень хорошо подготовленное покрытие исполняемых файлов Mach-O: https://www.objc.io/issues/6-build-tools/mach-o-executables/




c  

c