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
입니다. 일반적인 산술 변환 에서 설명한 것처럼,
그렇지 않으면 부호없는 정수 유형을 갖는 피연산자가 다른 피연산자 유형의 순위보다 크거나 같은 경우 부호가있는 정수 유형의 피연산자는 부호없는 정수 유형의 피연산자 유형으로 변환됩니다.
이것은 int
피연산자 b
가 비교 전에 unsigned int
로 변환된다는 것을 의미합니다.
-1을 unsigned int
로 변환하면 unsigned int
unsigned int
값이 최대가되며 결과는 1000보다 커 a > b
가 false입니다.
비교할 때 == 대신 실수로 글쓰기 =
=
연산자가 할당에 사용됩니다.
==
연산자가 비교에 사용됩니다.
하나는 두 가지를 혼합하지 않도록주의해야합니다. 때로는 실수로
/* assign y to x */
if (x = y) {
/* logic */
}
정말로 원했던 것이 :
/* compare if x is equal to y */
if (x == y) {
/* logic */
}
전자는 y의 값을 x에 대입하고 그 값이 비교가 아닌 0이 아닌지 검사합니다.
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 */
일부 프로그래머는 상수를 연산자 왼쪽에 놓는 전략을 사용합니다 (일반적으로 Yoda 조건 이라고 함). 상수는 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];
반품 뒤의 세미콜론이 누락되어 day = date [0]이 리턴됩니다.
이 문제와 유사한 문제를 피하는 한 가지 방법은 여러 줄 조건과 루프에서 항상 중괄호를 사용하는 것입니다. 예 :
if (x > a) {
a = x;
}
\ 0에 대해 하나의 여분의 바이트를 할당하는 것을 잊어 버림
문자열을 malloc
버퍼에 복사 할 때는 항상 strlen
에 1을 추가해야합니다.
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 */
메모리를 비우는 것을 잊어 버림 (메모리 누수)
프로그래밍 우수 사례는 자신의 코드로 직접 할당 된 메모리를 해제하거나 strdup()
와 같은 라이브러리 API와 같은 내부 또는 외부 함수를 호출하여 암시 적으로 해제하는 것입니다. 메모리를 해제하지 않으면 메모리 누수가 발생하여 프로그램 (또는 시스템)에서 사용할 수없는 많은 양의 낭비되는 메모리로 누적 될 수 있으며 이로 인해 충돌 또는 정의되지 않은 동작이 발생할 수 있습니다. 반복 또는 반복 함수에서 누수가 반복적으로 발생하는 경우 문제가 발생할 가능성이 큽니다. 누출 프로그램 실행 시간이 길어질수록 프로그램 오류의 위험이 높아집니다. 때때로 문제가 즉시 나타납니다. 다른 시간에는 문제가 몇 시간 또는 몇 년 동안 지속적으로 작동하지 않습니다. 메모리 고갈 실패는 상황에 따라 치명적일 수 있습니다.
다음의 무한 루프는 메모리를 해제하지 않고 암시 적으로 새 메모리를 할당하는 함수 인 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 / NULL이 아닌지 확인). 쓰레기 데이터 (읽기 작업) 또는 데이터 손상 (쓰기 작업) 및 / 또는 프로그램 충돌과 같은 심각한 문제가 발생할 수 있습니다. 대부분의 최신 운영 체제에서 메모리 위치 0 ( NULL
)을 확보하는 것은 C 표준에서 요구하는대로 NOP (예 : 무해)입니다. 따라서 포인터를 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 자 (null 종결 자에 대해 -1)보다 긴 문자열을 입력하면 버퍼 buf
뒤에있는 메모리를 덮어 씁니다. 이로 인해 정의되지 않은 동작이 발생합니다. 악의적 인 해커가 종종 이것을 사용하여 반송 주소를 덮어 쓰고 해커의 악성 코드 주소로 변경합니다.
realloc의 반환 값을 임시로 복사하는 것을 잊어 버림
realloc
이 실패하면 NULL
을 반환 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
)은 유한 정밀도를 갖고 이진 형식의 값을 나타 내기 때문에 일부 숫자를 정확하게 표현할 수 없습니다. 1/3과 같은 분수에 대해 10 진수를 반복 할 때와 마찬가지로 1/3뿐만 아니라 더 중요한 것은 1/10과 같이 2 진수로도 정 확하게 나타낼 수없는 분수가 있습니다. 부동 소수점 값을 직접 비교하지 마십시오. 대신 델타를 사용하십시오.
#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
할당 된 포인터를 계산할 때 추가 확장을 ptr2
합니다. 현대의 32 비트 환경에서 일반적으로 사용되는 sizeof(int)
가 4 인 경우 식은 범위를 벗어난 " array[0]
"다음의 8 개 요소를 나타내며 정의되지 않은 동작을 호출합니다.
ptr2
가 array[0]
뒤에 2 개 요소를 가리 키 ptr2
하려면, 단순히 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))
( N1570 6.5.3.2, 각주 102).
또는 포인터 연산이 선호되는 경우 포인터를 캐스팅하여 다른 데이터 형식을 주소 지정하면 바이트 주소 지정을 허용 할 수 있습니다. 그렇지만 엔디안 이 문제가 될 수 있으며 '포인터를 사용하는 문자'이외의 유형으로 캐스팅하면 엄격한 앨리어싱 문제가 발생 합니다.
#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
)를 인쇄 할 것으로 예상되지만 매크로는 1+2*1+2
로 확장되므로 실제로 5
가 인쇄됩니다.
이 문제를 피하려면 인수와 전체 매크로 식을 괄호로 묶어야합니다.
#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;
}
이제 double-evaluation의 문제는 수정되었지만,이 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에서는 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
매크로 호출로 확장 될 수 있습니다.
연결시 정의되지 않은 참조 오류
컴파일에서 가장 일반적인 오류 중 하나는 연결 단계에서 발생합니다. 오류는 다음과 유사합니다.
$ 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.c
및 undefined_reference.c
에 모두 포함 된 foo()
를 선언하는 헤더 foo.h
가있는 대체 상황이 발생합니다. 그런 다음 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;
}
이 코드는 구문 상 정확하다. #include <math.h>
에서 pow()
대한 선언이 존재하므로 컴파일과 링크를 시도하지만 다음과 같은 오류가 발생한다.
$ 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()
에 대한 정의 가 없기 때문에 발생합니다. 이 문제를 해결하기 위해 -lm
플래그를 지정하여 libm
이라는 수학 라이브러리를 연결하도록 지정해야합니다. ( -lm
이 필요없는 macOS와 같은 플랫폼이 있지만, 정의되지 않은 참조를 얻으면 라이브러리가 필요하다는 점에 유의하십시오.)
따라서 컴파일 단계를 다시 실행합니다. 이번에는 소스 또는 객체 파일 다음에 라이브러리를 지정합니다.
$ 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)
이 오류는 main
함수의 s
배열이 print_strings
함수에 전달되며,이 함수는받은 포인터 유형과 다른 포인터 유형을 요구합니다. 또한 print_strings
이 예상하는 유형을 print_strings
과 main
에서 전달 된 유형을 포함합니다.
문제는 어레이 붕괴 ( array decay )로 인한 것입니다. char[4][20]
(20 문자 중 4 배열의 배열)을 가진 s
가 함수에 전달되면 &s[0]
작성한 것처럼 첫 번째 요소에 대한 포인터가됩니다. char (*)[20]
(20 문자의 1 배열에 대한 포인터 char (*)[20]
유형 이것은 포인터의 배열, 배열의 배열 (3 차원 배열) 및 배열에 대한 포인터의 배열을 포함한 모든 배열에서 발생합니다. 다음은 배열이 붕괴 될 때 일어나는 일을 보여주는 표입니다. 유형 설명의 변경 사항은 어떻게되는지 설명하기 위해 강조 표시됩니다.
부패하기 전에 | 부패 후 | ||
---|---|---|---|
char [20] | 배열 (20 문자) | char * | 포인터 (1 문자) |
char [4][20] | 배열 ( 20 문자 중 4 배열 ) | char (*)[20] | 포인터 ( 20 문자 중 1 배열 ) |
char *[4] | 배열 ( 1 문자 4 포인터 ) | char ** | 포인터 ( 1 문자 1 포인터 ) |
char [3][4][20] | array of ( 20 개 문자로 구성된 4 개 배열의 3 개 배열 ) | char (*)[4][20] | 포인터 ( 20 문자 중 4 배열의 1 배열 ) |
char (*[4])[20] | 배열 ( 20 개 문자 중 1 개 배열에 대한 4 개의 포인터 ) | char (**)[20] | 포인터 ( 20 문자 중 1 배열에 1 포인터 ) |
배열이 포인터로 감쇄 될 수 있다면 포인터는 적어도 하나의 요소의 배열로 간주 될 수 있습니다. 이것에 대한 예외는 아무것도 가리 키지 않으며 결과적으로 배열이 아닌 널 포인터입니다.
어레이 감쇠는 한 번만 발생합니다. 배열이 포인터로 쇠퇴 한 경우, 배열이 아닌 포인터가됩니다. 배열에 대한 포인터가 있더라도 포인터가 적어도 하나의 요소의 배열로 간주 될 수 있으므로 배열 감쇠가 이미 발생했음을 기억하십시오.
즉, 배열 ( char (*)[20]
)에 대한 포인터는 포인터 ( char **
)에 대한 포인터가 될 수 없습니다. print_strings
함수를 수정하려면 올바른 유형을 받기 만하면됩니다.
void print_strings(char (*strings)[20], size_t n)
/* OR */
void print_strings(char strings[][20], size_t n)
print_strings
함수가 임의의 char 배열에 대해 일반화되기를 원할 때 문제가 발생합니다 : 20 대신 30 개의 문자가 있으면 어떨까요? 아니면 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]));
내부 배열 중 하나의 마지막 요소와 다음 내부 배열의 첫 번째 요소 사이의 바이트 차이는 "실제"다차원 배열 (예 : int array[4][16];
)과 같으므로 0이 아니어도됩니다 int array[4][16];
:
/* 0x40003c, 0x402000 */
printf("%p, %p\n", (void *)(array[0] + 15), (void *)array[1]);
int
의 크기를 고려하면, 8128 바이트 (8132-4)의 차이가 있는데, 이는 2032 개의 int
크기의 배열 요소입니다. 즉, "실제"다차원 배열에는 요소 사이에 간격이 없습니다.
"실제"다차원 배열을 기대하는 함수를 사용하여 동적으로 할당 된 배열을 사용해야하는 경우 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
이 변수가 아닌 매크로 또는 정수 리터럴 인 경우 코드는 배열에 대한 포인터를 할당 한 후보다 자연스러운 2 차원 배열 표기법을 사용할 수 있습니다.
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
는 가변 길이 배열 (VLA)을 가리 킵니다. 이것은 int *
로 캐스팅하여 func
와 함께 계속 사용할 수 있으며 새로운 함수 func_vla
는 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로 정의하면 C99 이전의 방법으로 문제가 발생합니다.
문자열 리터럴 대신 문자 상수 사용, 그 반대의 경우
C에서 문자 상수와 문자열 리터럴은 다른 것입니다.
'a'
와 같이 작은 따옴표로 묶인 문자 는 문자 상수 입니다. 문자 상수는 문자의 문자 코드를 값으로하는 정수입니다. 'abc'
와 같은 여러 문자가있는 문자 상수를 해석하는 방법은 구현에 따라 정의됩니다.
"abc"
와 같이 큰 따옴표로 묶인 0 개 이상의 문자는 문자열 리터럴 입니다. 캐릭터 라인 리터럴은, 요소가 char
형의 변경 불가능한 배열입니다. 큰 따옴표와 종료 null 문자의 문자열은 내용이므로 "abc"
에는 4 개의 요소 ( {'a', 'b', 'c', '\0'}
)가 있습니다.
이 예에서. 자 상수는. 자 리터 2이 g 용되어야하는 곳에서 g 용됩니다. 이 문자 상수는 구현 정의 방식으로 포인터로 변환되며 변환 된 포인터가 유효 할 확률이 거의 없으므로이 예제는 정의되지 않은 동작 을 호출합니다.
#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
은 성공했을 때 함수가 할당 한 메모리 블록에 대한 포인터를 반환하고, 함수가 요청 된 메모리 블록을 할당하지 못한 경우 null 포인터를 반환합니다. 따라서 항상 디버깅을 위해 반환 값을 확인해야합니다.
이것은 나쁘다 :
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 "life"
대신에 42 ""
됩니다.
이것은 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에 세미콜론 추가하기
C 전 처리기에서 혼동을 일으키기 쉽고 C 자체의 일부로 취급하지만 전 처리기가 텍스트 대체 메커니즘이기 때문에 실수입니다. 예를 들어,
/* 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부터 시작합니다. 인덱스는 항상 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
의 초기 값이 충분히 작 으면 int
가 32 비트 수량 12
때 상한은 12
).
지켜야 할 규칙 :
- 알고리즘을 초기화하십시오. 재귀 프로그램은 종종 시작하기 위해 시드 값이 필요합니다. 이것은 함수에 전달 된 매개 변수를 사용하거나 비 재귀 적이지만 재귀 계산을위한 시드 값을 설정하는 게이트웨이 함수를 제공하여 수행됩니다.
- 처리중인 현재 값이 기본 경우와 일치하는지 확인하십시오. 그렇다면 값을 처리하고 리턴하십시오.
- 작거나 간단한 하위 문제 또는 하위 문제로 대답을 재정의하십시오.
- 하위 문제에 대해 알고리즘을 실행합니다.
- 결과의 공식화에 결과를 결합하십시오.
- 결과를 반환하십시오.
출처 : 재귀 함수
'true'에 대한 논리적 표현 확인
원래의 C 표준에는 본질적인 부울 유형이 없었으므로 bool
, true
및 false
는 고유 한 의미가 없으며 종종 프로그래머가 정의했습니다. 일반적으로 true
는 1로 정의되고 false
는 0으로 정의됩니다.
C99에는 기본 _Bool
유형 인 _Bool
과 bool
( _Bool
확장), false
및 true
를 정의하는 헤더 <stdbool.h>
가 추가되었습니다. 또한 bool
, true
및 false
를 다시 정의 할 수 있지만이 기능은 시대에 뒤 떨어진 특성입니다.
더 중요한 것은 논리 표현식은 0으로 평가되는 항목을 모두 false로 처리하고 0이 아닌 평가는 모두로 처리합니다. 예 :
/* 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
. 그러나 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;
}
}
또는 0이 아닌 값을 true로 평가하십시오.
/* 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
리터럴로 혼합하면 배정 밀도에 대한 하드웨어 지원이없는 플랫폼에서 성능이 저하 될 수 있습니다.