C Language
Обычные подводные камни
Поиск…
Вступление
В этом разделе обсуждаются некоторые распространенные ошибки, о которых программист C должен знать и чего следует избегать. Подробнее о некоторых неожиданных проблемах и их причинах см. В разделе Неопределенное поведение
Смешение целых чисел без знака в арифметических операциях
Это, как правило , не очень хорошая идея , чтобы смешивать signed
и unsigned
целые числа в арифметических операциях. Например, что будет выводиться из следующего примера?
#include <stdio.h>
int main(void)
{
unsigned int a = 1000;
signed int b = -1;
if (a > b) puts("a is more than b");
else puts("a is less or equal than b");
return 0;
}
Поскольку 1000 больше, чем -1, вы ожидаете, что результат будет a is more than b
, однако этого не произойдет.
Арифметические операции между различными интегральными типами выполняются в общем типе, определяемом так называемыми обычными арифметическими преобразованиями (см. Спецификацию языка, 6.3.1.8).
В этом случае «общий тип» является unsigned int
, потому что, как указано в обычных арифметических преобразованиях ,
714 В противном случае, если операнд с целым типом без знака имеет ранг, больший или равный рану типа другого операнда, тогда операнд со знаком целочисленного типа преобразуется в тип операнда с целым типом без знака.
Это означает, что int
operand b
будет преобразован в unsigned int
перед сравнением.
Когда -1 преобразуется в unsigned int
результатом является максимально возможное значение unsigned int
, которое больше 1000, что означает, что a > b
является ложным.
Ошибочно писать = вместо == при сравнении
Оператор =
используется для назначения.
Для сравнения используется оператор ==
.
Нужно быть осторожным, чтобы не смешивать их. Иногда ошибочно пишет
/* assign y to x */
if (x = y) {
/* logic */
}
когда действительно нужно:
/* compare if x is equal to y */
if (x == y) {
/* logic */
}
Первый присваивает значение y x и проверяет, не является ли это значение ненулевым, вместо сравнения, что эквивалентно:
if ((x = y) != 0) {
/* logic */
}
Бывают случаи, когда тестирование результата присвоения предназначено и обычно используется, поскольку оно позволяет избежать дублирования кода и его обработки в первый раз. сравнить
while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
switch (c) {
...
}
}
против
c = getopt_long(argc, argv, short_options, long_options, &option_index);
while (c != -1) {
switch (c) {
...
}
c = getopt_long(argc, argv, short_options, long_options, &option_index);
}
Современные компиляторы распознают этот шаблон и не предупреждают, когда назначение находится в круглых скобках, как указано выше, но может предупреждать о других применениях. Например:
if (x = y) /* warning */
if ((x = y)) /* no warning */
if ((x = y) != 0) /* no warning; explicit */
Некоторые программисты используют стратегию размещения константы слева от оператора (обычно называемые условиями Йоды ). Поскольку константы являются значениями rvalues, этот стиль условия заставит компилятор выкинуть ошибку, если использовался неправильный оператор.
if (5 = y) /* Error */
if (5 == y) /* No error */
Однако это значительно снижает читаемость кода и не считается необходимым, если программист следует хорошей практике кодирования C и не помогает при сравнении двух переменных, поэтому он не является универсальным решением. Кроме того, многие современные компиляторы могут давать предупреждения, когда код написан с условиями Yoda.
Неверное использование точек с запятой
Будьте осторожны с точкой с запятой. Следующий пример
if (x > a);
a = x;
фактически означает:
if (x > a) {}
a = x;
что означает x
будут назначены в любом случае, который не может быть то , что вы хотели изначально. a
Иногда отсутствие точки с запятой также вызывает незаметную проблему:
if (i < 0)
return
day = date[0];
hour = date[1];
minute = date[2];
Точка с запятой позади пропущена, так что день = дата [0] будет возвращена.
Один из способов избежать этой и подобных проблем - всегда использовать фигурные скобки для многострочных условных и циклов. Например:
if (x > a) {
a = x;
}
Забыв выделить один дополнительный байт для \ 0
Когда вы копируете строку в буфер malloc
ed, всегда помните, чтобы добавить 1 в strlen
.
char *dest = malloc(strlen(src)); /* WRONG */
char *dest = malloc(strlen(src) + 1); /* RIGHT */
strcpy(dest, src);
Это связано с тем, что strlen
не включает длину \0
в длину. Если вы примете метод WRONG
(как показано выше), при вызове strcpy
ваша программа будет ссылаться на неопределенное поведение.
Это также относится к ситуациям, когда вы читаете строку известной максимальной длины из stdin
или другого источника. Например
#define MAX_INPUT_LEN 42
char buffer[MAX_INPUT_LEN]; /* WRONG */
char buffer[MAX_INPUT_LEN + 1]; /* RIGHT */
scanf("%42s", buffer); /* Ensure that the buffer is not overflowed */
Забывая освободить память (утечки памяти)
Лучшая практика программирования - освободить любую память, которая была выделена непосредственно вашим собственным кодом, или неявно, вызывая внутреннюю или внешнюю функцию, такую как библиотечный API, такой как strdup()
. Неспособность освободить память может привести к утечке памяти, которая может накапливаться в значительном количестве потерянной памяти, которая недоступна вашей программе (или системе), что может привести к сбоям или неопределенному поведению. Проблемы чаще возникают, если утечка возникает неоднократно в цикле или рекурсивной функции. Риск сбоя программы увеличивает время простоя программы. Иногда проблемы появляются мгновенно; в других случаях проблемы не будут наблюдаться в течение нескольких часов или даже лет постоянной работы. Ошибки исчерпания памяти могут быть катастрофическими, в зависимости от обстоятельств.
Следующий бесконечный цикл является примером утечки, которая в конечном итоге исчерпает доступную утечку памяти, вызвав функцию getline()
, которая неявно выделяет новую память, не освобождая эту память.
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *line = NULL;
size_t size = 0;
/* The loop below leaks memory as fast as it can */
for(;;) {
getline(&line, &size, stdin); /* New memory implicitly allocated */
/* <do whatever> */
line = NULL;
}
return 0;
}
Напротив, в приведенном ниже коде также используется функция getline()
, но на этот раз выделенная память правильно освобождается, избегая утечки.
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *line = NULL;
size_t size = 0;
for(;;) {
if (getline(&line, &size, stdin) < 0) {
free(line);
line = NULL;
/* Handle failure such as setting flag, breaking out of loop and/or exiting */
}
/* <do whatever> */
free(line);
line = NULL;
}
return 0;
}
Утечка памяти не всегда имеет ощутимые последствия и не обязательно является функциональной проблемой. В то время как «лучшая практика» диктует строгому освобождению памяти в стратегических точках и условиях, уменьшает объем памяти и снижает риск исчерпания памяти, могут быть исключения. Например, если программа ограничена по продолжительности и объему, риск сбоя распределения может считаться слишком маленьким, чтобы беспокоиться. В этом случае обход явного освобождения может считаться приемлемым. Например, большинство современных операционных систем автоматически освобождают всю память, потребляемую программой, когда она завершается, из-за сбоя программы, системного вызова exit()
, завершения процесса или достижения конца main()
. Явное освобождение памяти в момент неминуемой остановки программы может фактически быть избыточным или ввести штраф за производительность.
Выделение может завершиться неудачно, если недостаточно памяти, и обработка сбоев должна учитываться на соответствующих уровнях стека вызовов. getline()
, показанный выше, является интересным прецедентом, потому что он является библиотечной функцией, которая не только выделяет память, которую она оставляет для вызывающего абонента, но и может не работать по ряду причин, и все это необходимо учитывать. Поэтому при использовании C API важно прочитать документацию (справочную страницу) и обратить особое внимание на условия ошибки и использование памяти, а также знать, какой программный уровень несет бремя освобождения возвращенной памяти.
Еще одна распространенная практика обработки памяти состоит в том, чтобы последовательно устанавливать указатели на память в NULL сразу после освобождения памяти, на которую ссылаются эти указатели, поэтому эти указатели могут быть проверены на достоверность в любое время (например, для NULL / non-NULL), поскольку доступ к свободной памяти может привести к серьезным проблемам, таким как получение данных мусора (операция чтения) или повреждение данных (операция записи) и / или сбой программы. В большинстве современных операционных систем освобождение ячейки памяти 0 ( NULL
) является NOP (например, безвредным), как того требует стандарт C, поэтому, устанавливая указатель на NULL, нет риска двойной освобождающей памяти, если указатель передается в free()
. Имейте в виду, что память с двойным освобождением может привести к очень много времени, запутыванию и затруднению диагностики сбоев.
Копирование слишком много
char buf[8]; /* tiny buffer, easy to overflow */
printf("What is your name?\n");
scanf("%s", buf); /* WRONG */
scanf("%7s", buf); /* RIGHT */
Если пользователь вводит строку длиной более 7 символов (- 1 для нулевого терминатора), память за буфером buf
будет перезаписана. Это приводит к неопределенному поведению. Вредоносные хакеры часто используют это, чтобы перезаписать обратный адрес и изменить его на адрес вредоносного кода хакера.
Забыв скопировать возвращаемое значение realloc во временную
Если realloc
не работает, он возвращает NULL
. Если вы присвоите значение исходного буфера возвращаемому значению realloc
, и если он вернет NULL
, то исходный буфер (старый указатель) будет потерян, что приведет к утечке памяти . Решение состоит в том, чтобы скопировать во временный указатель, и если это временное значение не является NULL, то скопируйте его в реальный буфер.
char *buf, *tmp;
buf = malloc(...);
...
/* WRONG */
if ((buf = realloc(buf, 16)) == NULL)
perror("realloc");
/* RIGHT */
if ((tmp = realloc(buf, 16)) != NULL)
buf = tmp;
else
perror("realloc");
Сравнение чисел с плавающей запятой
Типы плавающей точки ( float
, double
и long double
) не могут точно представлять некоторые числа, потому что они имеют конечную точность и представляют значения в двоичном формате. Подобно тому, как мы повторяем десятичные числа в базе 10 для фракций, таких как 1/3, существуют фракции, которые также не могут быть представлены конечно в двоичном виде (например, 1/3, но также, что более важно, 1/10). Не сравнивать результаты с плавающей запятой напрямую; вместо этого используйте дельта.
#include <float.h> // for DBL_EPSILON and FLT_EPSILON
#include <math.h> // for fabs()
int main(void)
{
double a = 0.1; // imprecise: (binary) 0.000110...
// may be false or true
if (a + a + a + a + a + a + a + a + a + a == 1.0) {
printf("10 * 0.1 is indeed 1.0. This is not guaranteed in the general case.\n");
}
// Using a small delta value.
if (fabs(a + a + a + a + a + a + a + a + a + a - 1.0) < 0.000001) {
// C99 5.2.4.2.2p8 guarantees at least 10 decimal digits
// of precision for the double type.
printf("10 * 0.1 is almost 1.0.\n");
}
return 0;
}
Другой пример:
gcc -O3 -g -I./inc -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition rd11.c -o rd11 -L./lib -lsoq
#include <stdio.h>
#include <math.h>
static inline double rel_diff(double a, double b)
{
return fabs(a - b) / fmax(fabs(a), fabs(b));
}
int main(void)
{
double d1 = 3.14159265358979;
double d2 = 355.0 / 113.0;
double epsilon = 1.0;
for (int i = 0; i < 10; i++)
{
if (rel_diff(d1, d2) < epsilon)
printf("%d:%.10f <=> %.10f within tolerance %.10f (rel diff %.4E)\n",
i, d1, d2, epsilon, rel_diff(d1, d2));
else
printf("%d:%.10f <=> %.10f out of tolerance %.10f (rel diff %.4E)\n",
i, d1, d2, epsilon, rel_diff(d1, d2));
epsilon /= 10.0;
}
return 0;
}
Выход:
0:3.1415926536 <=> 3.1415929204 within tolerance 1.0000000000 (rel diff 8.4914E-08)
1:3.1415926536 <=> 3.1415929204 within tolerance 0.1000000000 (rel diff 8.4914E-08)
2:3.1415926536 <=> 3.1415929204 within tolerance 0.0100000000 (rel diff 8.4914E-08)
3:3.1415926536 <=> 3.1415929204 within tolerance 0.0010000000 (rel diff 8.4914E-08)
4:3.1415926536 <=> 3.1415929204 within tolerance 0.0001000000 (rel diff 8.4914E-08)
5:3.1415926536 <=> 3.1415929204 within tolerance 0.0000100000 (rel diff 8.4914E-08)
6:3.1415926536 <=> 3.1415929204 within tolerance 0.0000010000 (rel diff 8.4914E-08)
7:3.1415926536 <=> 3.1415929204 within tolerance 0.0000001000 (rel diff 8.4914E-08)
8:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000100 (rel diff 8.4914E-08)
9:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000010 (rel diff 8.4914E-08)
Выполнение дополнительного масштабирования в арифметике указателя
В арифметике указателя целое число, добавляемое или вычитаемое в указатель, интерпретируется не как изменение адреса, а как количество перемещаемых элементов .
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = ptr + sizeof(int) * 2; /* wrong */
printf("%d %d\n", *ptr, *ptr2);
return 0;
}
Этот код делает дополнительное масштабирование при вычислении указателя, назначенного ptr2
. Если sizeof(int)
равно 4, что характерно для современных 32-битных сред, выражение означает «8 элементов после array[0]
», которое выходит за пределы диапазона, и вызывает неопределенное поведение .
Чтобы ptr2
указывал на то, что является 2 элементами после array[0]
, вы должны просто добавить 2.
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = ptr + 2;
printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
return 0;
}
Явная арифметика указателя с использованием аддитивных операторов может вводить в заблуждение, поэтому использование подтипов массива может быть лучше.
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = &ptr[2];
printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
return 0;
}
E1[E2]
идентичен (*((E1)+(E2)))
( N1570 6.5.2.1, параграф 2) и &(E1[E2])
эквивалентен ((E1)+(E2))
( N 1570 6.5.3.2, сноска 102).
В качестве альтернативы, если предпочтительна арифметика указателей, то приведение указателя к другому типу данных позволяет разрешить байтовую адресацию. Будьте осторожны: endianness может стать проблемой, а отбрасывание на типы, отличные от «указателя на символ», приводит к строгим проблемам псевдонимов .
#include <stdio.h>
int main(void) {
int array[3] = {1,2,3}; // 4 bytes * 3 allocated
unsigned char *ptr = (unsigned char *) array; // unsigned chars only take 1 byte
/*
* Now any pointer arithmetic on ptr will match
* bytes in memory. ptr can be treated like it
* was declared as: unsigned char ptr[12];
*/
return 0;
}
Макросы - это простые замены строк
Макросы - это простые замены строк. (Строго говоря, они работают с токенами предварительной обработки, а не с произвольными строками).
#include <stdio.h>
#define SQUARE(x) x*x
int main(void) {
printf("%d\n", SQUARE(1+2));
return 0;
}
Вы можете ожидать, что этот код напечатает 9
( 3*3
), но на самом деле будет напечатано 5
, потому что макрос будет расширен до 1+2*1+2
.
Вы должны обернуть аргументы и все макрокоманды в круглые скобки, чтобы избежать этой проблемы.
#include <stdio.h>
#define SQUARE(x) ((x)*(x))
int main(void) {
printf("%d\n", SQUARE(1+2));
return 0;
}
Другая проблема заключается в том, что аргументы макроса не гарантируются для оценки один раз; они не могут быть оценены вообще или могут быть оценены несколько раз.
#include <stdio.h>
#define MIN(x, y) ((x) <= (y) ? (x) : (y))
int main(void) {
int a = 0;
printf("%d\n", MIN(a++, 10));
printf("a = %d\n", a);
return 0;
}
В этом коде макрос будет расширен до ((a++) <= (10) ? (a++) : (10))
. Так a++
( 0
) меньше 10
, a++
будет оцениваться дважды, и он заставит значение a
и то, что возвращается из MIN
отличается от того, что вы можете ожидать.
Этого можно избежать, используя функции, но обратите внимание, что типы будут фиксироваться определением функции, тогда как макросы могут быть (тоже) гибкими с типами.
#include <stdio.h>
int min(int x, int y) {
return x <= y ? x : y;
}
int main(void) {
int a = 0;
printf("%d\n", min(a++, 10));
printf("a = %d\n", a);
return 0;
}
Теперь проблема двойной оценки фиксирована, но эта функция min
не может обрабатывать double
данные без усечения, например.
Директивы макросов могут быть двух типов:
#define OBJECT_LIKE_MACRO followed by a "replacement list" of preprocessor tokens
#define FUNCTION_LIKE_MACRO(with, arguments) followed by a replacement list
То, что отличает эти два типа макросов, - это символ, который следует за идентификатором после #define
: если это lparen , это функционально-подобный макрос; в противном случае это объект-подобный макрос. Если намерение состоит в том, чтобы написать функцию, как макрос, там не должно быть белое пространство между концом имени макроса и (
. Проверьте это для подробного объяснения.
В C99 или более поздней версии вы можете использовать static inline int min(int x, int y) { … }
.
В C11 вы можете написать выражение типа «type-generic» для min
.
#include <stdio.h>
#define min(x, y) _Generic((x), \
long double: min_ld, \
unsigned long long: min_ull, \
default: min_i \
)(x, y)
#define gen_min(suffix, type) \
static inline type min_##suffix(type x, type y) { return (x < y) ? x : y; }
gen_min(ld, long double)
gen_min(ull, unsigned long long)
gen_min(i, int)
int main(void)
{
unsigned long long ull1 = 50ULL;
unsigned long long ull2 = 37ULL;
printf("min(%llu, %llu) = %llu\n", ull1, ull2, min(ull1, ull2));
long double ld1 = 3.141592653L;
long double ld2 = 3.141592652L;
printf("min(%.10Lf, %.10Lf) = %.10Lf\n", ld1, ld2, min(ld1, ld2));
int i1 = 3141653;
int i2 = 3141652;
printf("min(%d, %d) = %d\n", i1, i2, min(i1, i2));
return 0;
}
Общее выражение может быть расширено с помощью большего количества типов, таких как double
, float
, long long
, unsigned long
, long
, unsigned
- и соответствующие gen_min
макросов gen_min
.
Неопределенные опорные ошибки при связывании
Одна из наиболее распространенных ошибок в компиляции происходит на этапе связывания. Ошибка выглядит примерно так:
$ gcc undefined_reference.c
/tmp/ccoXhwF0.o: In function `main':
undefined_reference.c:(.text+0x15): undefined reference to `foo'
collect2: error: ld returned 1 exit status
$
Итак, давайте посмотрим на код, который породил эту ошибку:
int foo(void);
int main(int argc, char **argv)
{
int foo_val;
foo_val = foo();
return foo_val;
}
Мы видим здесь объявление foo ( int foo();
), но не определение его (фактическая функция). Таким образом , мы обеспечили компилятор с заголовком функции, но не было никакой такой функции , определенная в любом месте, так что проходит этап компиляции , но линкер выходит с Undefined reference
ошибкой.
Чтобы исправить эту ошибку в нашей маленькой программе, нам нужно было бы добавить определение для foo:
/* Declaration of foo */
int foo(void);
/* Definition of foo */
int foo(void)
{
return 5;
}
int main(int argc, char **argv)
{
int foo_val;
foo_val = foo();
return foo_val;
}
Теперь этот код будет скомпилирован. Возникает альтернативная ситуация, когда источник для foo()
находится в отдельном исходном файле foo.c
(и есть заголовок foo.h
чтобы объявить foo()
который включен как в foo.c
и undefined_reference.c
). Затем исправление должно связывать как объектный файл с файлами foo.c
и undefined_reference.c
или компилировать оба исходных файла:
$ gcc -c undefined_reference.c
$ gcc -c foo.c
$ gcc -o working_program undefined_reference.o foo.o
$
Или же:
$ gcc -o working_program undefined_reference.c foo.c
$
Более сложным является случай, когда библиотеки участвуют, как в коде:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(int argc, char **argv)
{
double first;
double second;
double power;
if (argc != 3)
{
fprintf(stderr, "Usage: %s <denom> <nom>\n", argv[0]);
return EXIT_FAILURE;
}
/* Translate user input to numbers, extra error checking
* should be done here. */
first = strtod(argv[1], NULL);
second = strtod(argv[2], NULL);
/* Use function pow() from libm - this will cause a linkage
* error unless this code is compiled against libm! */
power = pow(first, second);
printf("%f to the power of %f = %f\n", first, second, power);
return EXIT_SUCCESS;
}
Код синтаксически правильный, объявление для pow()
существует из #include <math.h>
, поэтому мы пытаемся скомпилировать и связать, но получим такую ошибку:
$ gcc no_library_in_link.c -o no_library_in_link
/tmp/ccduQQqA.o: In function `main':
no_library_in_link.c:(.text+0x8b): undefined reference to `pow'
collect2: error: ld returned 1 exit status
$
Это происходит потому, что определение для pow()
не было найдено на этапе связывания. Чтобы исправить это, мы должны указать, что мы хотим связать с математической библиотекой libm
, указав флаг -lm
. (Обратите внимание, что существуют такие платформы, как macOS, где -lm
не требуется, но когда вы получаете неопределенную ссылку, необходима библиотека.)
Итак, мы снова запускаем этап компиляции, на этот раз определяя библиотеку (после исходных или объектных файлов):
$ gcc no_library_in_link.c -lm -o library_in_link_cmd
$ ./library_in_link_cmd 2 4
2.000000 to the power of 4.000000 = 16.000000
$
И это работает!
Неверное разложение массива
Общей проблемой в коде, которая использует многомерные массивы, массивы указателей и т. Д., Является то, что Type**
и Type[M][N]
являются принципиально разными типами:
#include <stdio.h>
void print_strings(char **strings, size_t n)
{
size_t i;
for (i = 0; i < n; i++)
puts(strings[i]);
}
int main(void)
{
char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
print_strings(s, 4);
return 0;
}
Пример вывода компилятора:
file1.c: In function 'main':
file1.c:13:23: error: passing argument 1 of 'print_strings' from incompatible pointer type [-Wincompatible-pointer-types]
print_strings(strings, 4);
^
file1.c:3:10: note: expected 'char **' but argument is of type 'char (*)[20]'
void print_strings(char **strings, size_t n)
Ошибка указывает, что массив s
в main
функции передается функции print_strings
, которая ожидает другого типа указателя, чем полученная. Он также содержит примечание, выражающее тип, ожидаемый print_strings
и тип, который был передан ему из main
.
Проблема связана с чем-то, называемым распадом массива . Что происходит, когда s
с типом char[4][20]
(массив из 4 массивов из 20 символов) передается функции, он превращается в указатель на свой первый элемент, как если бы вы написали &s[0]
, который имеет тип char (*)[20]
(указатель на 1 массив из 20 символов). Это происходит для любого массива, включая массив указателей, массив массивов массивов (3-D массивов) и массив указателей на массив. Ниже приведена таблица, иллюстрирующая, что происходит, когда массив распадается. Изменения в описании типа выделены, чтобы проиллюстрировать, что происходит:
Перед распадом | После распада | ||
---|---|---|---|
char [20] | массив (20 символов) | char * | указатель на (1 символ) |
char [4][20] | массив (4 массива из 20 символов) | char (*)[20] | указатель на (1 массив из 20 символов) |
char *[4] | массив (4 указателя на 1 символ) | char ** | указатель на (1 указатель на 1 символ) |
char [3][4][20] | массив (3 массива из 4 массивов из 20 символов) | char (*)[4][20] | указатель на (1 массив из 4 массивов из 20 символов) |
char (*[4])[20] | массив (4 указателя на 1 массив из 20 символов) | char (**)[20] | указатель на (1 указатель на 1 массив из 20 символов) |
Если массив может распадаться на указатель, то можно сказать, что указатель может считаться массивом по меньшей мере из 1 элемента. Исключением является нулевой указатель, который ничего не указывает и, следовательно, не является массивом.
Распад массива происходит только один раз. Если массив разложился на указатель, он теперь является указателем, а не массивом. Даже если у вас есть указатель на массив, помните, что указатель может считаться массивом хотя бы одного элемента, поэтому разложение массива уже произошло.
Другими словами, указатель на массив ( char (*)[20]
) никогда не станет указателем на указатель ( char **
). Чтобы исправить функцию print_strings
, просто print_strings
ее получить правильный тип:
void print_strings(char (*strings)[20], size_t n)
/* OR */
void print_strings(char strings[][20], size_t n)
Проблема возникает, когда вы хотите, print_strings
функция print_strings
была общей для любого массива символов: что, если есть 30 символов вместо 20? Или 50? Ответ заключается в том, чтобы добавить еще один параметр перед параметром массива:
#include <stdio.h>
/*
* Note the rearranged parameters and the change in the parameter name
* from the previous definitions:
* n (number of strings)
* => scount (string count)
*
* Of course, you could also use one of the following highly recommended forms
* for the `strings` parameter instead:
*
* char strings[scount][ccount]
* char strings[][ccount]
*/
void print_strings(size_t scount, size_t ccount, char (*strings)[ccount])
{
size_t i;
for (i = 0; i < scount; i++)
puts(strings[i]);
}
int main(void)
{
char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
print_strings(4, 20, s);
return 0;
}
Компиляция не приводит к ошибкам и приводит к ожидаемому результату:
Example 1
Example 2
Example 3
Example 4
Передача несвязанных массивов в функции, ожидающие «реальных» многомерных массивов
При распределении многомерных массивов с помощью malloc
, calloc
и realloc
общий шаблон заключается в распределении внутренних массивов с несколькими вызовами (даже если вызов появляется только один раз, он может находиться в цикле):
/* Could also be `int **` with malloc used to allocate outer array. */
int *array[4];
int i;
/* Allocate 4 arrays of 16 ints. */
for (i = 0; i < 4; i++)
array[i] = malloc(16 * sizeof(*array[i]));
Разница в байтах между последним элементом одного из внутренних массивов и первым элементом следующего внутреннего массива может быть не 0, поскольку они были бы с «реальным» многомерным массивом (например, int array[4][16];
) :
/* 0x40003c, 0x402000 */
printf("%p, %p\n", (void *)(array[0] + 15), (void *)array[1]);
Принимая во внимание размер int
, вы получаете разницу в 8128 байт (8132-4), что составляет 2032 элемента массива int
-sized, и это проблема: «реальный» многомерный массив не имеет пробелов между элементами.
Если вам нужно использовать динамически выделенный массив с функцией, ожидающей «реального» многомерного массива, вы должны выделить объект типа int *
и использовать арифметику для выполнения вычислений:
void func(int M, int N, int *array);
...
/* Equivalent to declaring `int array[M][N] = {{0}};` and assigning to array4_16[i][j]. */
int *array;
int M = 4, N = 16;
array = calloc(M, N * sizeof(*array));
array[i * N + j] = 1;
func(M, N, array);
Если N
является макросом или целым литералом, а не переменной, код может просто использовать более естественную двухмерную запись массива после выделения указателя на массив:
void func(int M, int N, int *array);
#define N 16
void func_N(int M, int (*array)[N]);
...
int M = 4;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;
/* Cast to `int *` works here because `array` is a single block of M*N ints with no gaps,
just like `int array2[M * N];` and `int array3[M][N];` would be. */
func(M, N, (int *)array);
func_N(M, array);
Если N
не является макросом или целым литералом, то array
укажет на array
переменной длины (VLA). Это все еще можно использовать с func
путем литья в int *
а новая функция func_vla
заменит func_N
:
void func(int M, int N, int *array);
void func_vla(int M, int N, int array[M][N]);
...
int M = 4, N = 16;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;
func(M, N, (int *)array);
func_vla(M, N, array);
Примечание : VLA являются необязательными с C11. Если ваша реализация поддерживает C11 и определяет макрос __STDC_NO_VLA__
до 1, вы застряли в методах pre-C99.
Использование символьных констант вместо строковых литералов и наоборот
В C символьные константы и строковые литералы - это разные вещи.
Персонаж, окруженный одинарными кавычками типа 'a'
является символьной константой . Символьная константа представляет собой целое число, значение которого является символьным кодом, обозначающим символ. Как интерпретировать символьные константы с несколькими символами, такими как 'abc'
определяется реализацией.
Ноль или более символов, окруженных двойными кавычками типа "abc"
является строковым литералом . Строковый литерал - это немодифицируемый массив, чьи элементы являются char
типа. Строка в двойных кавычках плюс завершающий нуль-символ - это содержимое, поэтому "abc"
имеет 4 элемента ( {'a', 'b', 'c', '\0'}
)
В этом примере используется символьная константа, в которой должен использоваться строковый литерал. Эта константа символа будет преобразована в указатель в определенном реализацией образом, и мало шансов на то, что преобразованный указатель будет действительным, поэтому этот пример вызовет неопределенное поведение .
#include <stdio.h>
int main(void) {
const char *hello = 'hello, world'; /* bad */
puts(hello);
return 0;
}
В этом примере используется строковый литерал, в котором должна использоваться символьная константа. Указатель, преобразованный из строкового литерала, будет преобразован в целое число в соответствии с реализацией, и он будет преобразован в char
в соответствии с реализацией. (Как преобразовать целое число в подписанный тип, который не может представлять значение для преобразования, определено в соответствии с реализацией, а также подписан ли char
, также определяется реализацией.) Выход будет какой-то бессмысленной вещью.
#include <stdio.h>
int main(void) {
char c = "a"; /* bad */
printf("%c\n", c);
return 0;
}
Почти во всех случаях компилятор будет жаловаться на эти путаницы. Если это не так, вам нужно использовать дополнительные параметры предупреждения компилятора или рекомендуется использовать лучший компилятор.
Игнорирование возвращаемых значений функций библиотеки
Почти каждая функция в стандартной библиотеке C возвращает что-то об успехе, а что-то еще об ошибке. Например, malloc
вернет указатель на блок памяти, выделенный функцией при успешном завершении, и, если функции не удалось выделить запрошенный блок памяти, нулевой указатель. Поэтому вы всегда должны проверить возвращаемое значение для более легкой отладки.
Это плохо:
char* x = malloc(100000000000UL * sizeof *x);
/* more code */
scanf("%s", x); /* This might invoke undefined behaviour and if lucky causes a segmentation violation, unless your system has a lot of memory */
Это хорошо:
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char* x = malloc(100000000000UL * sizeof *x);
if (x == NULL) {
perror("malloc() failed");
exit(EXIT_FAILURE);
}
if (scanf("%s", x) != 1) {
fprintf(stderr, "could not read string\n");
free(x);
exit(EXIT_FAILURE);
}
/* Do stuff with x. */
/* Clean up. */
free(x);
return EXIT_SUCCESS;
}
Таким образом, вы сразу знаете причину ошибки, иначе вы можете потратить часы на поиск ошибки в совершенно неправильном месте.
Символ новой строки не потребляется при типичном вызове scanf ()
Когда эта программа
#include <stdio.h>
#include <string.h>
int main(void) {
int num = 0;
char str[128], *lf;
scanf("%d", &num);
fgets(str, sizeof(str), stdin);
if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
printf("%d \"%s\"\n", num, str);
return 0;
}
выполняется с этим вводом
42
life
выход будет 42 ""
вместо ожидаемого 42 "life"
.
Это связано с тем, что символ newline после 42
не потребляется при вызове scanf()
и он потребляется fgets()
прежде чем он считывает информацию о life
. Затем fgets()
перестает читать перед чтением life
.
Чтобы избежать этой проблемы, один из способов, который полезен, когда максимальная длина строки известна, - например, при решении проблем в онлайн-системе судьи - избегает напрямую использовать scanf()
и считывает все строки через fgets()
. Вы можете использовать sscanf()
для анализа прочитанных строк.
#include <stdio.h>
#include <string.h>
int main(void) {
int num = 0;
char line_buffer[128] = "", str[128], *lf;
fgets(line_buffer, sizeof(line_buffer), stdin);
sscanf(line_buffer, "%d", &num);
fgets(str, sizeof(str), stdin);
if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
printf("%d \"%s\"\n", num, str);
return 0;
}
Другой способ - прочитать, пока вы не нажмете символ новой строки после использования scanf()
и до использования fgets()
.
#include <stdio.h>
#include <string.h>
int main(void) {
int num = 0;
char str[128], *lf;
int c;
scanf("%d", &num);
while ((c = getchar()) != '\n' && c != EOF);
fgets(str, sizeof(str), stdin);
if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
printf("%d \"%s\"\n", num, str);
return 0;
}
Добавление точки с запятой в #define
Легко запутаться в препроцессоре С и рассматривать его как часть самого С, но это ошибка, потому что препроцессор - это просто механизм замены текста. Например, если вы пишете
/* WRONG */
#define MAX 100;
int arr[MAX];
код расширяется до
int arr[100;];
который является синтаксической ошибкой. Средством устранения является точка с запятой из строки #define
. Почти всегда ошибка заканчивается #define
точкой с запятой.
Многострочные комментарии не могут быть вложенными
В C многострочные комментарии / * и * / не вложены.
Если вы комментируете блок кода или функцию, используя этот стиль комментария:
/*
* max(): Finds the largest integer in an array and returns it.
* If the array length is less than 1, the result is undefined.
* arr: The array of integers to search.
* num: The number of integers in arr.
*/
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
Вы не сможете легко прокомментировать это:
//Trying to comment out the block...
/*
/*
* max(): Finds the largest integer in an array and returns it.
* If the array length is less than 1, the result is undefined.
* arr: The array of integers to search.
* num: The number of integers in arr.
*/
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
//Causes an error on the line below...
*/
Одним из решений является использование комментариев стиля C99:
// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
Теперь весь блок можно легко прокомментировать:
/*
// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
*/
Другое решение заключается в том, чтобы избежать отключения кода с использованием синтаксиса комментариев, вместо этого вместо него следует использовать директивы препроцессора #ifdef
или #ifndef
. Эти директивы делают гнездо, оставив вас свободно комментировать свой код в стиле вы предпочитаете.
#define DISABLE_MAX /* Remove or comment this line to enable max() code block */
#ifdef DISABLE_MAX
/*
* max(): Finds the largest integer in an array and returns it.
* If the array length is less than 1, the result is undefined.
* arr: The array of integers to search.
* num: The number of integers in arr.
*/
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
#endif
Некоторые руководства заходят так далеко, что рекомендуют, чтобы разделы кода никогда не комментировались и что, если код должен быть временно отключен, можно прибегнуть к использованию #if 0
.
См. #If 0 для блокировки разделов кода .
Пересечение границ массива
Массивы основаны на нуле, то есть индекс всегда начинается с 0 и заканчивается длиной массива индекса минус 1. Таким образом, следующий код не будет выводить первый элемент массива и выводит мусор для окончательного значения, которое он печатает.
#include <stdio.h>
int main(void)
{
int x = 0;
int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements
for(x = 1; x <= 5; x++) //Looping from 1 till 5.
printf("%d\t", myArray[x]);
printf("\n");
return 0;
}
Выход: 2 3 4 5 GarbageValue
Ниже демонстрируется правильный способ достижения желаемого результата:
#include <stdio.h>
int main(void)
{
int x = 0;
int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements
for(x = 0; x < 5; x++) //Looping from 0 till 4.
printf("%d\t", myArray[x]);
printf("\n");
return 0;
}
Выход: 1 2 3 4 5
Важно знать длину массива, прежде чем работать с ним, так как иначе вы можете повредить буфер или вызвать ошибку сегментации, обратившись к ячейкам памяти, которые находятся за пределами границ.
Рекурсивная функция - отсутствует базовое условие
Вычисление факториала числа является классическим примером рекурсивной функции.
Отсутствует базовое состояние:
#include <stdio.h>
int factorial(int n)
{
return n * factorial(n - 1);
}
int main()
{
printf("Factorial %d = %d\n", 3, factorial(3));
return 0;
}
Типичный выход: Segmentation fault: 11
Проблема с этой функцией заключается в том, что она будет чередоваться бесконечно, вызывая сбои сегментации - для этого требуется базовое условие остановки рекурсии.
Базовое условие:
#include <stdio.h>
int factorial(int n)
{
if (n == 1) // Base Condition, very crucial in designing the recursive functions.
{
return 1;
}
else
{
return n * factorial(n - 1);
}
}
int main()
{
printf("Factorial %d = %d\n", 3, factorial(3));
return 0;
}
Образец вывода
Factorial 3 = 6
Эта функция прекращается, как только она достигает условия n
равна 1 (при условии, что начальное значение n
достаточно мало - верхняя граница равна 12
когда int
является 32-разрядной величиной).
Правила, которым необходимо следовать:
- Инициализировать алгоритм. Рекурсивным программам часто требуется начальное значение. Это достигается либо с использованием параметра, переданного функции, либо путем предоставления нерекурсивной функции шлюза, но которая устанавливает начальные значения для рекурсивного вычисления.
- Проверьте, соответствует ли текущее значение (значения) совпадающим с базовым регистром. Если да, обработайте и верните значение.
- Переопределите ответ в терминах меньшей или более простой подзадачи или под-проблем.
- Запустите алгоритм в подзапросе.
- Объедините результаты в формулировке ответа.
- Верните результаты.
Источник: рекурсивная функция
Проверка логического выражения на 'true'
Первоначальный стандарт C не имел встроенного булева типа, поэтому bool
, true
и false
не имели неотъемлемого значения и часто определялись программистами. Обычно true
будет определяться как 1, а false
будет определяться как 0.
C99 добавляет встроенный тип _Bool
и заголовок <stdbool.h>
который определяет bool
(расширяется до _Bool
), false
и true
. Это также позволяет вам переопределить bool
, true
и false
, но отмечает, что это устаревшая функция.
Что еще более важно, логические выражения обрабатывают все, что оценивается как ноль как ложное, а любая ненулевая оценка - как истина. Например:
/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
if ((bitField & 0x80) == true) /* Comparison only succeeds if true is 0x80 and bitField has that bit set */
{
return true;
}
else
{
return false;
}
}
В приведенном выше примере функция пытается проверить, установлен ли верхний бит и вернуть значение true
если оно есть. Однако, явно проверяя true
, оператор if
будет успешным только в том случае, если (bitfield & 0x80)
оценивается как любое true
, которое обычно равно 1
и очень редко 0x80
. Либо явным образом проверяю случай, который вы ожидаете:
/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
if ((bitField & 0x80) == 0x80) /* Explicitly test for the case we expect */
{
return true;
}
else
{
return false;
}
}
Или оцените любое ненулевое значение как истинное.
/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
/* If upper bit is set, result is 0x80 which the if will evaluate as true */
if (bitField & 0x80)
{
return true;
}
else
{
return false;
}
}
Литералы с плавающей точкой имеют тип double по умолчанию
Следует проявлять осторожность при инициализации переменных типа float
до литеральных значений или их сравнении с литеральными значениями, поскольку обычные литералы с плавающей запятой, такие как 0.1
имеют тип double
. Это может привести к неожиданностям:
#include <stdio.h>
int main() {
float n;
n = 0.1;
if (n > 0.1) printf("Wierd\n");
return 0;
}
// Prints "Wierd" when n is float
Здесь n
получает инициализацию и округление до одной точности, что приводит к значению 0.10000000149011612. Затем n
преобразуется обратно в двойную точность для сравнения с 0.1
литеральным (что равно 0,10000000000000001), что приводит к несоответствию.
Помимо ошибок округления, смешивание переменных float
с double
литералами приведет к низкой производительности на платформах, которые не имеют аппаратной поддержки для двойной точности.