Изучение C, ценил бы вход на том, почему это решение работает

Это - буквально первая вещь, которую я когда-либо писал в C, поэтому не стесняйтесь указывать на все, что это - дефекты.:) Моя проблема, однако это: если я пишу программу путем, я чувствую, является самым чистым, я получаю поврежденную программу:

#include <sys/queue.h> 

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

/* Removed prototypes and non related code for brevity */

int
main()
{
    char    *cmd = NULL; 
    unsigned int acct = 0; 
    int amount = 0; 
    int done = 0; 

    while (done==0) {
        scanf ("%s %u %i", cmd, &acct, &amount);

        if (strcmp (cmd, "exit") == 0)
            done = 1;
        else if ((strcmp (cmd, "dep") == 0) || (strcmp (cmd, "deb") == 0))
            debit (acct, amount);
        else if ((strcmp (cmd, "wd") == 0) || (strcmp (cmd, "cred") == 0))
            credit (acct, amount);
        else if (strcmp (cmd, "fee") == 0)
            service_fee(acct, amount);
        else
            printf("Invalid input!\n");
    }
    return(0);
}

void
credit(unsigned int acct, int amount)
{
}

void
debit(unsigned int acct, int amount)
{
}

void
service_fee(unsigned int acct, int amount)
{
}

В настоящий момент вышеупомянутое не генерирует ошибок в компиляции, но дает мне segfault, когда работал. Я могу зафиксировать это путем изменения программы для передачи cmd ссылкой при вызове scanf и strcmp. segfault уходит и заменяется предупреждениями для каждого использования strcmp во время компиляции. Несмотря на предупреждения, затронутые работы кода.

предупреждение: передающий аргумент 1 из 'strcmp' от несовместимого типа указателя

Как добавленная премия, изменяя scanf и вызовы strcmp позволяет программе прогрессировать достаточно далеко для выполнения возврата (0), в которой точке вещь отказывает с прерыванием Аварийного прекращения работы. Если я выгружаю возврат (0) для выхода (0) затем, все работает как ожидалось.

Это оставляет меня с двумя вопросами: почему была исходная программа неправильно? Как я могу зафиксировать его лучше, чем я имею?

Биту о необходимости использовать выход вместо возврата экранировали меня особенно.

7
задан Keifer 9 June 2010 в 17:18
поделиться

9 ответов

Это происходит из-за оператора scanf.

Посмотрите, как cmd указывает на NULL. Когда выполняется scanf, он записывает в адрес cmd, который является NULL, тем самым генерируя segfault.

Решением является создание буфера для cmd, например:

char cmd[20];

Теперь ваш буфер вмещает 20 символов. Однако теперь вам нужно беспокоиться о переполнении буфера, если пользователь введет более 20 символов.

Добро пожаловать в C.

EDIT: Также обратите внимание, что ваши функции кредита, дебета и платы за услуги не будут работать так, как вы их написали. Это связано с тем, что параметры передаются по значению, а не по ссылке. Это означает, что после возврата метода любые изменения будут отброшены. Если вы хотите, чтобы они изменяли передаваемые вами аргументы, попробуйте изменить методы на:

void credit(unsigned int *acct, int *amount)

А затем вызовите их так:

credit(&acct, &amt);

При этом параметры будут передаваться по ссылке, то есть любые изменения, которые вы сделаете внутри функции credit, повлияют на параметры, даже после возврата функции.

11
ответ дан 6 December 2019 в 05:42
поделиться

Как отметили другие, вы не выделили ничего для чтения scanf. Но вы также должны проверить возвращаемое значение scanf:

if ( scanf ("%s %u %i", cmd, &acct, &amount) != 3 ) {
   // do some error handling
}

Функция scanf возвращает количество успешных преобразований, так что если кто-то введет XXXX, когда вы ожидаете целое число, вы должны быть в состоянии обнаружить и справиться с этим. Но, откровенно говоря, код пользовательского интерфейса, использующий scanf(), никогда не будет защищен от такого рода вещей. scanf() вообще-то предназначалась для чтения форматированных файлов, а не случайного ввода от человека.

5
ответ дан 6 December 2019 в 05:42
поделиться

Это:

char    *cmd = NULL; 


Должно быть:

char cmd[100]; 



Обратите внимание: Убедитесь, что строка, которую пользователь вводит в cmd , имеет длину меньше 100 или n

4
ответ дан 6 December 2019 в 05:42
поделиться

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

1
ответ дан 6 December 2019 в 05:42
поделиться

Ваша основная проблема заключается в том, что вы не выделили память для вашей строки. В C вы сами отвечаете за управление памятью. Если вы объявляете переменные на стеке, это легко. С указателями это немного сложнее. Поскольку у вас есть строка char* str = NULL, при попытке scanf в нее, вы записываете байты в NULL, что незаконно. Спецификатор %s записывает в то, на что указывает str; он не может изменить str, поскольку параметры передаются по значению. Вот почему вы должны передать &acct, а не просто acct.

Так как же это исправить? Вам нужно предоставить память, в которой будет храниться прочитанная строка. Что-то вроде char str[5] = "". Это превращает str в пятиэлементный символьный массив, достаточно большой, чтобы вместить "exit" и его завершающий нулевой байт. (Массивы распадаются на указатели при малейшей провокации, так что в этом плане все в порядке). Однако это опасно. Если пользователь введет строку malicious, вы запишите "malic" в str, а байты для "icious\0" - во все, что будет после этого в памяти. Это переполнение буфера, и это классическая ошибка. Самый простой способ исправить ее - потребовать от пользователя ввести команду не более чем из N букв, где N - самая длинная команда, которая у вас есть; в данном случае N = 4. Затем вы можете сказать scanf считать не более четырех символов: scanf("%4s %u %i", cmd, &acct, &amt). В %4s сказано "считывать не более четырех символов", поэтому вы не можете испортить другую память. Однако обратите внимание, что если пользователь введет malformed 3 4, вы не сможете найти 3 и 4, поскольку будете смотреть на ormed.

Причина, по которой вы могли бы сделать scanf("%s %u %i", &cmd, &acct, &amount), заключается в том, что C не является безопасным для типов. Когда вы передали ему &cmd, вы передали ему char**; однако он с радостью воспринял это как char*. Таким образом, он записывал байты over cmd, поэтому если вы передали строку exit, cmd могла бы (если бы она была шириной в четыре байта и имела соответствующую эндианальность) быть равна 0x65786974 (0x65 = e, 0x78 = x, 0x69 = i, 0x74 = t). А затем нулевой байт или любой другой байт, который вы передали, начнет записываться в произвольную память. Однако если вы измените его в strcmp, то также будет рассматривать значение из str как строку, и все будет последовательно. Что касается того, почему return 0; не работает, а exit(0) работает, я не уверен, но у меня есть предположение: возможно, вы писали поверх адреса возврата main. Он тоже хранится в стеке, и если в схеме стека он идет после cmd, то, возможно, вы его обнулили или написали на нем. Теперь exit должен выполнять свою очистку вручную, переходя в нужные места и т.д. Однако, если (как мне кажется, хотя я не уверен) main ведет себя как любая другая функция, its return прыгает в место на стеке, сохраненное как адрес возврата (которое, вероятно, является какой-то процедурой очистки). Однако, поскольку вы проскриптовали это, вы получаете прерывание.

Теперь есть еще пара небольших улучшений, которые вы могли бы сделать. Во-первых, поскольку вы рассматриваете done как булево значение, вы должны выполнить цикл while (!done) { ... }. Во-вторых, текущая установка требует написать exit 1 1 для выхода из программы, хотя бит 1 1 не должен быть необходим. В-третьих, вы должны проверить, успешно ли вы прочитали все три аргумента, чтобы не получить ошибки/несоответствия; например, если вы не сделаете этого, то input

deb 1 2
deb 3 a

вызовет debit(1,2) и debit(3,2), оставив при этом a во входных данных, чтобы запутать вас. И, наконец, вы должны завершить работу при EOF, а не зацикливаться до бесконечности, выполняя последнее действие. Если мы соберем все это вместе, то получим следующий код:

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

void credit(unsigned int acct, int amount);
void debit(unsigned int acct, int amount);
void service_fee(unsigned int acct, int amount);

int main() {
  char         cmd[5] = ""; 
  unsigned int acct   = 0; 
  int          amount = 0; 
  int          done   = 0; 

  while (!done) {
    if (feof(stdin)) {
      done = 1;
    } else {
      if (scanf("%4s", cmd, &acct) != 1) {
        fprintf(stderr, "Could not read the command!\n");
        scanf(" %*s "); /* Get rid of the rest of the line */
        continue;
      }

      if (strcmp(cmd, "exit") == 0) {
        done = 1;
      } else {
        if (scanf(" %u %i", &acct, &amount) != 2) {
          fprintf(stderr, "Could not read the arguments!\n");
          scanf(" %*s "); /* Get rid of the rest of the line */
          continue;
        }

        if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
          debit(acct, amount);
        else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
          credit(acct, amount);
        else if (strcmp(cmd, "fee") == 0)
          service_fee(acct, amount);
        else
          fprintf(stderr, "Invalid input!\n");
      }
    }
    /* Cleanup code ... */
  }

  return 0;
}

/* Dummy function bodies */

void credit(unsigned int acct, int amount) {
  printf("credit(%u, %d)\n", acct, amount);
}

void debit(unsigned int acct, int amount) {
  printf("debit(%u, %d)\n", acct, amount);
}

void service_fee(unsigned int acct, int amount) {
  printf("service_fee(%u, %d)\n", acct, amount);
}

Обратите внимание, что если нет "кода очистки", вы можете заменить все ваши использования done на break и удалить объявление done, что даст более красивый цикл

while (1) {
  if (feof(stdin)) break;

  if (scanf("%4s", cmd, &acct) != 1) {
    fprintf(stderr, "Could not read the command!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if (strcmp(cmd, "exit") == 0) break;

  if (scanf(" %u %i", &acct, &amount) != 2) {
    fprintf(stderr, "Could not read the arguments!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
    debit(acct, amount);
  else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
    credit(acct, amount);
  else if (strcmp(cmd, "fee") == 0)
    service_fee(acct, amount);
  else
    fprintf(stderr, "Invalid input!\n");
}
1
ответ дан 6 December 2019 в 05:42
поделиться

Вы не выделяете память для cmd, поэтому он NULL.

Попробуйте объявить его с пробелом:

char cmd[1000];
7
ответ дан 6 December 2019 в 05:42
поделиться

cmd инициализируется нулевым указателем, который никогда не указывает на какую-либо память. scanf не проверяет допустимость cmd перед попыткой записи на то, на что указывает cmd.

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

char cmd[30]; /* DANGEROUS! */

, но это очень опасный ход, потому что вы все равно можете получить segfaults, если ввод длиннее, чем ожидалось, и scanf пытается записать в cmd [30] и далее.

По этой причине scanf считается небезопасным и не должен использоваться в производственном коде. Более безопасные альтернативы включают использование fgets для чтения строки ввода и sscanf для ее обработки.

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

2
ответ дан 6 December 2019 в 05:42
поделиться

В вашем примере scanf () передается нулевой указатель.

char    *cmd = NULL; 

scanf () не будет выделять место для строки - вам нужно будет где-то выделить место для строки.

char   cmd[80];
...
scanf ("%s",cmd);

Вы получаете ошибку сегментации, потому что scanf () пытается записать свой вывод в нераспределенное пространство.

2
ответ дан 6 December 2019 в 05:42
поделиться

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

http://www.cprogramming.com/tutorial.html#ctutorial

Наиболее частые причины ошибок сегментации подробно описаны здесь:

http://www.cprogramming.com/debugging/segfaults.html

0
ответ дан 6 December 2019 в 05:42
поделиться
Другие вопросы по тегам:

Похожие вопросы: