Это - буквально первая вещь, которую я когда-либо писал в 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) затем, все работает как ожидалось.
Это оставляет меня с двумя вопросами: почему была исходная программа неправильно? Как я могу зафиксировать его лучше, чем я имею?
Биту о необходимости использовать выход вместо возврата экранировали меня особенно.
Это происходит из-за оператора 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, повлияют на параметры, даже после возврата функции.
Как отметили другие, вы не выделили ничего для чтения scanf. Но вы также должны проверить возвращаемое значение scanf:
if ( scanf ("%s %u %i", cmd, &acct, &amount) != 3 ) {
// do some error handling
}
Функция scanf возвращает количество успешных преобразований, так что если кто-то введет XXXX, когда вы ожидаете целое число, вы должны быть в состоянии обнаружить и справиться с этим. Но, откровенно говоря, код пользовательского интерфейса, использующий scanf(), никогда не будет защищен от такого рода вещей. scanf() вообще-то предназначалась для чтения форматированных файлов, а не случайного ввода от человека.
Это:
char *cmd = NULL;
Должно быть:
char cmd[100];
Обратите внимание:
Убедитесь, что строка, которую пользователь вводит в cmd
, имеет длину меньше 100
или n
Другие указали на ошибку в вашей программе, но для лучшего понимания указателей, поскольку вы только начинаете изучать C, посмотрите этот вопрос в SO.
Ваша основная проблема заключается в том, что вы не выделили память для вашей строки. В 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");
}
Вы не выделяете память для cmd, поэтому он NULL
.
Попробуйте объявить его с пробелом:
char cmd[1000];
cmd инициализируется нулевым указателем, который никогда не указывает на какую-либо память. scanf
не проверяет допустимость cmd перед попыткой записи на то, на что указывает cmd.
Предварительное решение вместо этого создает некоторое пространство для cmd, чтобы указать на:
char cmd[30]; /* DANGEROUS! */
, но это очень опасный ход, потому что вы все равно можете получить segfaults, если ввод длиннее, чем ожидалось, и scanf пытается записать в cmd [30] и далее.
По этой причине scanf
считается небезопасным и не должен использоваться в производственном коде. Более безопасные альтернативы включают использование fgets
для чтения строки ввода и sscanf
для ее обработки.
К сожалению, ввод-вывод C очень сложно реализовать без возможности переполнения буфера в вашей программе. Вам всегда нужно думать о том, сколько памяти у вас доступно и будет ли ее достаточно для хранения максимально длинного ввода, который вы можете получить. Вам также необходимо проверить возвращаемые значения большинства функций ввода-вывода на наличие ошибок.
В вашем примере scanf ()
передается нулевой указатель.
char *cmd = NULL;
scanf () не будет выделять место для строки - вам нужно будет где-то выделить место для строки.
char cmd[80];
...
scanf ("%s",cmd);
Вы получаете ошибку сегментации, потому что scanf ()
пытается записать свой вывод в нераспределенное пространство.
Чтобы полностью понять, что здесь происходит, вам необходимо понять некоторые основы указателей C. Я предлагаю вам взглянуть сюда, если вы действительно так плохо знакомы с C:
http://www.cprogramming.com/tutorial.html#ctutorial
Наиболее частые причины ошибок сегментации подробно описаны здесь: