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

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

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

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

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

puts(a);

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

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

4b9b3361

Ответ 1

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

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

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

Итак, ваш код действителен и не вызывает undefined поведение, но следующее:

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 содержит адрес локальной переменной, которая затем выходит за пределы области видимости.

EDIT:

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

printf("%s", a);

Или проще:

puts(a);

Ответ 2

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

char* a = NULL;

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

{
    char* b = "stackoverflow";

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

    a = b;

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

}

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

printf(a);

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

Ответ 3

Строковые литералы статически распределены, поэтому указатель действителен неограниченно. Если вы сказали char b[] = "stackoverflow", тогда вы бы выделили массив char в стеке, который станет недействительным, когда область действия закончится. Эта разница также проявляется в модификации строк: char s[] = "foo" stack выделяет строку, которую вы можете изменить, тогда как char *s = "foo" дает вам указатель на строку, которая может быть помещена в постоянную память, поэтому ее изменение undefined поведение.

Ответ 4

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

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

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

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

Абсолютно важно понимать, что "поведение undefined" не означает, что программа будет непредсказуемо разбиваться ". Это означает, что" все может случиться", и что-то включает в себя работу как возможно программист (на этом компьютере, с этим компилятором, сегодня).


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

$ 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).)

Ответ 5

Код не генерирует никаких ошибок, потому что вы просто назначаете указатель символа b другому указателю на символ a, и это отлично.

В C вы можете назначить ссылку указателя на другой указатель. здесь фактически строка "stackoverflow" используется как литерал, а базовое адресное расположение этой строки будет присваиваться переменной a.

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

Ответ 6

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

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

printf(a);

Ответ 7

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

#include <string.h>

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

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

> cc -S main.c

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

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

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/