На сайте Stack Overflow много вопросов от людей, ещё только изучающих языки программирования. Лайфхак: ответы на многие эти вопросы можно получить сразу, запустив анализатор кода. Получится быстрее.
Эту заметку меня побудила написать дискуссия "Segmentation fault when converting char * to char **" на сайте Stack Overflow. Человек, изучающий программирование, интересуется, что не так с его кодом.
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
char **get_words(char *buffer, char delimiter)
{
printf("buffer = %s\n", buffer);
char **words = malloc(sizeof(char *) * 100);
if (words == NULL) {
printf("Malloc Error\n");
exit(84);
}
for (int i = 0; i < 100; i++) {
words[i] = malloc(sizeof(char) * 100);
if (words[i] == NULL) {
printf("Malloc Error\n");
exit(84);
}
}
int word_count = 0;
int l = 0;
for (int i = 0; buffer[i] != '\0' && buffer[i] != '\n'; i++, l++) {
if (buffer[i] == delimiter) {
words[word_count][l] = '\0';
word_count++;
l = -1;
}
else
words[word_count][l] = buffer[i];
}
words[word_count][l] = '\0';
return (words);
}
int main()
{
char *buffer = malloc(sizeof(char) * 100);
buffer = "hello world !\n";
char **words = get_words(buffer, ' ');
printf("words[0]= %s\n", words[0]);
free (buffer);
char **reply = get_words("Second call\n", ' ');
printf("reply[0] = %s\n", reply[0]);
}
Таких вопросов достаточно на Stack Overflow. И часто на них отвечают медленно и неохотно. Это обоснованно. Согласитесь, не хочется изучать достаточно большой текст программы только ради того, чтобы найти какую-то скучную ошибку. Эти ошибки, как правило, связаны с ещё недостаточным знанием языка, и ответ, скорее всего, сведётся к совету прочитать определённый раздел книги или документации.
Это не снобизм со стороны более опытных разработчиков. Им просто не очень интересно тратить время на то, чтобы разбираться, что не так с лабораторными работами.
Вернёмся к вопросу на Stack Overflow, про который я говорил выше. Вопрос висел уже пару дней, а ответа всё нет. Как человеку продвинуться дальше?
Одним из помощников в обучении программированию может стать статический анализатор. Это программа, которая выполняет code review и сообщает о подозрительных участках кода. Статические анализаторы не заменяют практику обзора кода, выполняемого коллегой, но хорошо дополняют его и позволяют находить многие ошибки на самом раннем этапе.
Запустим online-версию анализатора PVS-Studio для приведённого в вопросе кода. Первым интересным и важным предупреждением является сообщение: V1031 The 'malloc' function is not declared. Passing data to or from this function can be affected.
Без объявления функции malloc программа уже работает непонятным образом. В языке Си считается, что если функция не объявлена, то она возвращает int. А на самом деле это указатель. Чем это опасно, рассказано в заметке "Красивая 64-битная ошибка на языке Си". Исправим эту проблему, добавив #include <stdlib.h>.
Теперь вывод анализатора изменится, и мы видим следующую серьёзную проблему: 43:1: note: V773 The 'buffer' pointer was assigned values twice without releasing the memory. A memory leak is possible.
Ошибка здесь:
char *buffer = malloc(sizeof(char) * 100);
buffer = "hello world !\n";
....
free (buffer);
Значение указателя перетирается. Чтобы скопировать строку в буфер, нужно использовать специальные функции, например strcpy. Внесём исправления.
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>
char **get_words(char *buffer, char delimiter)
{
printf("buffer = %s\n", buffer);
char **words = malloc(sizeof(char *) * 100);
if (words == NULL) {
printf("Malloc Error\n");
exit(84);
}
for (int i = 0; i < 100; i++) {
words[i] = malloc(sizeof(char) * 100);
if (words[i] == NULL) {
printf("Malloc Error\n");
exit(84);
}
}
int word_count = 0;
int l = 0;
for (int i = 0; buffer[i] != '\0' && buffer[i] != '\n'; i++, l++) {
if (buffer[i] == delimiter) {
words[word_count][l] = '\0';
word_count++;
l = -1;
}
else
words[word_count][l] = buffer[i];
}
words[word_count][l] = '\0';
return (words);
}
int main()
{
char *buffer = malloc(sizeof(char) * 100);
if (buffer == NULL)
exit(84);
strcpy(buffer, "hello world !\n");
char **words = get_words(buffer, ' ');
printf("words[0]= %s\n", words[0]);
free (buffer);
char **reply = get_words("Second call\n", ' ');
printf("reply[0] = %s\n", reply[0]);
}
Этот код ещё нельзя назвать красивым и безопасным, но он работает. Так что такой способ поиска ошибок вполне может помочь в процессе обучения.
Дополнительные ссылки:
- Статический анализ кода.
- PVS-Studio: online версия.
- PVS-Studio: бесплатное использование для студентов.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. "Why doesn't my code work?" — to anyone learning the art of programming and writing to the Stack Overflow community.
Комментарии (7)
orfelin
29.06.2022 08:49+1Так что такой способ поиска ошибок вполне может помочь в процессе обучения.
Не поможет. Обучающийся студент/программист не поймет что ваш инструмент ему сообщает. Вы на разных с ним языках говорите. Для него ваши сообщения что послания с альфа центавры - такая же марсианская абракадабра.
Вы же не пишите ему на простом человеческом языке: "эй, парень, смотри на строчку %1 - ты там выделяешь память в переменную buffer. В строчке %2 ты в эту переменную присваиваешь указатель на другой участок памяти. Тут тебе надо по хорошему strcpy использовать. А падает твоя прога потому что в строке %3 ты вызываешь функцию free передавая в нее не тот адрес памяти который ты выделил в %1, а совершенно другой. Смекаешь?"
flashmozzg
29.06.2022 15:28О! То, что PVS studio есть в CE очень круто, но поводу диагностики этого конкретного случая - слабовато.
Запустите clang-tidy (бесплатный, и доступный в большинстве IDE) и вывод будет гораздо полезнее (особенно для начинающего - можно сразу копировать в ответ на SO) и сразу к делу.
yeputons
Мне кажется, такое сообщение от анализатора для начинающего вообще ничем не лучше шаблонной абракадабры или падения программы. Слишком далеко до решения, слишком много надо знать, чтобы понять сообщение анализатора и придумать решение.
Пример "наивных" рассуждений:
Я выделил память под строчку.
Я скопировал строчку. Но почему-то не работает, причём ломается где-то потом.
Чиво. Но у меня-то выделяется и почти работает => значит, не affected.
Кстати, чтобы исправить проблему, надо знать, что такое "function is not declared", как объявлять функции, и почему
malloc
на самом деле надо объявлять не так, а через#include
, причём ещё знать, какой именно.Ну possible и possible, меня это не волнует, у меня программа не работает. Да и память я в конце освобождаю, так что всё честно.
В сообщении идёт акцент либо на "утечку памяти" (что в этом примере вообще неважно и лишь сбивает с толку), либо на "двойное присваивание", что совершенно не помогает: автор вопроса хочет сначала выделить буфер, а потом скопировать туда строчку - это действительно по смыслу два каких-то "типа-присваивания". Просто так сложилось, что одно из них почему-то должно быть
=
, а другое должно бытьstrcpy
, а компилятор сам и не догадывается. Но про strcpy анализатор ничего не говорит.Да и причина падения-то на самом деле не в утечке памяти и не в переприсваивании, а в использовании free на статически выделенной строчке. Но как определить "что подразумевал программист" тут совершенно непонятно - то ли strcpy нужен, то ли malloc/free не нужен.
Andrey2008 Автор
Предупреждения статического анализатора (и компилятора) - это повод задуматься. И почитать хотя-бы те-же описания диагностик PVS-Studio. Там вполне можно узнать свою ситуацию.
Если предупреждения компилятора/анализатора воспринимаются как шум и "автору виднее"... Ну что же. Ему пока ему предстоит продолжить путь страданий :)
ReadOnlySadUser
Если бы студент хотел призадуматься, он бы не писал вопрос на SO :)
P.S.
Проверять лень, но я уверен, что -Wall -Wextra -Werror решили бы проблему не хуже PVS Studio)
Andrey2008 Автор
Ну или я что-то не так сделал, или не очень помогает.
Да, с -Wall -Wextra -Werror код не компилируется из-за malloc.
Но если добавить #include <stdlib.h>, то компилируется без проблем и ошибка с порчей указателя не находится.
ReadOnlySadUser
Так если плохой код не компилируется - это же вроде как уже хорошо. Уже повод задуматься: "а всё ли я делаю правильно?" :)