Рассмотрим следующий фрагмент кода:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

int main(){
    char again = 'Y';
    int code;

    do{
        printf("Please inform your option:\n1 - New record\n2 - Delete record\n3 - ecovery record\n4 - Search records\n");
        scanf("%d", &code);

        switch(code){

            case 1:
                printf("Option %d\n",code);
                break;
            case 2:
                printf("Option %d\n",code);
                break;
            case 3:
                printf("Option %d\n",code);
                break;
            case 4:
                printf("Opcao %d\n",code);
                break;
            default:
                printf("code invalido!");
        }

        do{
            printf("Do you wnat to again? [Y - Yes / N - No]: ");
            scanf("%s", &again);
            again = toupper(again);
        }while(again != 'Y' && again != 'N');
        printf("(DEBUG)Option after reading the string %d\n",code);
    }while(again == 'S');

    return 0;
}

Я знаю, что в коде есть ошибка, потому что я использую scanf с "%s" для чтения информации об одном символе. Лучше использовать "%c".

Однако, что мне интересно в этом коде, так это то, что после выполнения "scanf("%s", &again); значение переменной "code" меняется на ноль. И я не уверен, почему это происходит.

Основная основная гипотеза заключается в том, что, поскольку я читаю строку с «%s», в этом процессе scanf хранит в памяти информацию о двух символах: символе, предоставленном пользователем, и «\ 0». И я думаю, что информация о «\0» хранится в области памяти, назначенной переменной «код».

Имеет ли это смысл?

С уважением.

1
Zaratruta 4 Ноя 2019 в 23:10
Это не нулевые терминаторы. Это новые строки. Где вы отбрасываете пробелы? Вы должны проверить значение, возвращаемое scanf.
 – 
William Pursell
4 Ноя 2019 в 23:16
3
Вы правы, нулевой терминатор сохраняется в code.
 – 
Barmar
4 Ноя 2019 в 23:17
3
%s не сохраняет новую строку в переменной.
 – 
Barmar
4 Ноя 2019 в 23:17
Пробел автоматически отбрасывается как %d, так и %s.
 – 
Barmar
4 Ноя 2019 в 23:18
1
По сути, вы вызываете неопределенное поведение, сохраняя в переменной, которая недостаточно велика. Если вы вводите N символов в ответ на %s, вам необходимо сохранить в массиве не менее N+1 символов.
 – 
Barmar
4 Ноя 2019 в 23:23

1 ответ

Да, это имеет смысл. Но интуиция — это одно — давайте проверим!

Скомпилируйте с большим количеством отладочной информации:

tmp$ gcc -ggdb test.c

Запускаем gdb, запускаем программу:

tmp$ gdb ./a.out 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...done.
(gdb) run
Starting program: /tmp/a.out 
Please inform your option:
1 - New record
2 - Delete record
3 - ecovery record
4 - Search records
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
84  ../sysdeps/unix/syscall-template.S: No such file or directory.

Итак, я нажал Ctrl-C, чтобы прервать программу. Давайте добавим точку наблюдения на code.

(gdb) bt
#0  0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
#1  0x00007ffff7a875e8 in _IO_new_file_underflow (fp=0x7ffff7dd18e0 <_IO_2_1_stdin_>) at fileops.c:592
#2  0x00007ffff7a8860e in __GI__IO_default_uflow (fp=0x7ffff7dd18e0 <_IO_2_1_stdin_>) at genops.c:413
#3  0x00007ffff7a69260 in _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, 
    argptr=argptr@entry=0x7fffffffdbe8, errp=errp@entry=0x0) at vfscanf.c:634
#4  0x00007ffff7a785df in __isoc99_scanf (format=<optimized out>) at isoc99_scanf.c:37
#5  0x00000000004006c1 in main () at test.c:12
(gdb) frame 5
#5  0x00000000004006c1 in main () at test.c:12
12          scanf("%d", &code);
(gdb) watch code
Hardware watchpoint 1: code
(gdb) cont
Continuing.

Теперь, когда мы указываем «2» в качестве входных данных, мы можем видеть изменение значения:

2

Hardware watchpoint 1: code

Old value = 32767
New value = 2
0x00007ffff7a6cde7 in _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, 
    argptr=argptr@entry=0x7fffffffdbe8, errp=errp@entry=0x0) at vfscanf.c:1902
1902    vfscanf.c: No such file or directory.

Хорошо, это был первый scanf. Давайте перейдем ко второму и дадим «n» в качестве ответа.

(gdb) cont
Continuing.
Option 2
Do you wnat to again? [Y - Yes / N - No]: n

Hardware watchpoint 1: code

Old value = 2
New value = 0
_IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7fffffffdbe8, 
    errp=errp@entry=0x0) at vfscanf.c:1194
1194    in vfscanf.c
(gdb) bt
#0  _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7fffffffdbe8, 
    errp=errp@entry=0x0) at vfscanf.c:1194
#1  0x00007ffff7a785df in __isoc99_scanf (format=<optimized out>) at isoc99_scanf.c:37
#2  0x000000000040076d in main () at test.c:34

Да, он точно перезаписывает code!

Какое значение мы пишем?

(gdb) disass
Dump of assembler code for function _IO_vfscanf_internal:
[...]
   0x00007ffff7a6a74e <+7886>:  lea    0x1(%rax),%rbx
   0x00007ffff7a6a752 <+7890>:  movb   $0x0,(%rax)
=> 0x00007ffff7a6a755 <+7893>:  je     0x7ffff7a6a77f <_IO_vfscanf_internal+7935>
   0x00007ffff7a6a757 <+7895>:  mov    -0x620(%rbp),%r12
   0x00007ffff7a6a75e <+7902>:  mov    %rbx,%rsi
   0x00007ffff7a6a761 <+7905>:  mov    (%r12),%rdi
[...]

movb означает, что мы записываем байт. И это непосредственное значение (т.е. константа) равное нулю. Он выглядит, ходит и крякает, как струнный терминатор!

Если мы хотим быть действительно уверенными, мы можем попытаться найти точный исходный файл для этой библиотечной функции.

(gdb) disass /s $pc-3,+10
Dump of assembler code from 0x7ffff7a6a752 to 0x7ffff7a6a75c:
vfscanf.c:
1192    in vfscanf.c
   0x00007ffff7a6a752 <_IO_vfscanf_internal+7890>:  movb   $0x0,(%rax)

1193    in vfscanf.c
1194    in vfscanf.c
=> 0x00007ffff7a6a755 <_IO_vfscanf_internal+7893>:  je     0x7ffff7a6a77f <_IO_vfscanf_internal+7935>
   0x00007ffff7a6a757 <_IO_vfscanf_internal+7895>:  mov    -0x620(%rbp),%r12
End of assembler dump.

В моем случае это просто: я могу установить пакет «glibc-source» из репозитория apt Ubuntu. Вам может быть труднее, в зависимости от типа вашей системы.

Во всяком случае, проверьте строку 1192. Это определенно нулевой терминатор.

glibc-2.23$ find . -name vfscanf.c
./stdio-common/vfscanf.c
glibc-2.23$ less -N ./stdio-common/vfscanf.c
[...]
   1189 
   1190                   str = __mempcpy (str, buf, n);
   1191 #endif
   1192                   *str++ = '\0';
   1193 
   1194                   if ((flags & MALLOC) && str - *strptr != strsize)
   1195                     {
   1196                       char *cp = (char *) realloc (*strptr, str - *strptr);
2
Snild Dolkow 4 Ноя 2019 в 23:48