C 언어의 선언 문법은 (declaration syntax) 그 자체가 하나의 프로그래밍 언어라고 할 수 있습니다. 선언은 다음과 같이 여러 부분으로 구성되어 (꼭 모든 부분을 다 가져야 할 필요는 없습니다.) 있습니다: storage class, base type, type qualifier, 그리고 declarator (declarator는 initializer를 포함할 수 있습니다.) 각 declarator는 새 identifier를 선언하는 것 이외에, identifier가 배열인지, 포인터인지, 함수인지, 또는 어떤 복잡한 타입인지를 알려줍니다. 따라서 선언이 실제 identifier가 어떻게 쓰일 것인지를 (declaration mimics use) 알려 줍니다 (질문 1.21은 이 `declaration mimics use' 관계를 자세하게 다룹니다.)
문자 타입도 (특히 unsigned char) “작은” 정수타입으로 쓰일 수 있지만, 예상치 못한 부호 확장이나 코드의 크기를 증가시킬 수 있기 때문에 바람직하지 않습니다. (unsigned char 타입을 쓰는 것이 도움이 될 경우도 있습니다; 관련된 문제는 질문 12.1를 참고하기 바랍니다.)
비슷한 크기/빠르기 문제가 float과 double에서 발생할 수 있습니다. 변수의 주소가 필요하고 어떤 특별한 타입이 필요한 경우라면 위의 규칙들은 모두 적용되지 않습니다.
C 언어에서 각각의 type들이 정확한 크기를 가지도록 정의되어 있다고 착각하기 쉬운데, 사실은 그렇지 않습니다. C 언어에서 정의하고 있는 것은 다음과 같습니다:
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
이 규칙은 char가 적어도 8 bit가 되어야 한다는 것과,
short int와 int는 적어도 16 bit여야 한다는 것과,
long int는 적어도 32 bit가 되어야 한다는 것을
뜻합니다. (각각 type의 signed와 unsigned version은 같은 크기를
가진다고 보장되어 있습니다.)
ANSI C에서 특정 machine에서
각각 type의 최소값과 최대값은 <limits.h>
에 정의되어 있으며,
요약하면 다음과 같습니다:
Base type | Min. size | Min. value | Max. value | Max. value |
(bits) | (signed) | (signed) | (unsigned) | |
char | 8 | -127 | 127 | 255 |
short | 16 | -32,767 | 32,767 | 65,535 |
int | 16 | -32,767 | 32,767 | 65,535 |
long | 32 | -2,147,483,647 | 2,147,483,647 | 4,294,967,295 |
이 표는 표준이 보장하는 최소값들을 보여줍니다. 많은 implementation이 이보다 더 큰 값을 제공하지만, portable한 프로그램을 만들고 싶다면 이러한 것에 의존해서는 안됩니다.
어떠한 이유에서 정확한 크기가 필요한 경우라면 -- 이런 경우로는 외부 저장 배치(externally-imposed stroage layout)가 필요한 경우를 들 수 있습니다. 질문 20.5를 참고하기 바랍니다 -- 적절한 typedef를 쓰시기 바랍니다. (아래 Note 참고)
Base type | long long |
Min. size (bits) | 64 |
Min. value (signed) | -9,223,372,036,854,775,807 |
Max. value (signed) | 9,223,372,036,854,775,807 |
Max. value (unsigned) | 18,446,744,073,709,551,615 |
또한, 정확한 크기를 요구하는 정수 타입을 선언하기 위해, C99
표준은 <stdint.h>
를 통해 여러 가지 정수 타입을 제공합니다.
여기에 관한 것은
11.I를 참고하기 바랍니다.
int type은 컴퓨터의 가장 자연스러운 word size를 나타내는 것으로 알려져 있으며, 대부분의 정수를 저장할 때 가장 적당한 type입니다. 질문 1.1의 guideline을 보기 바랍니다; 덧붙여 질문 12.42, 20.5도 참고하시기 바랍니다.
<inttype.h>
를 포함시켰을 때, 정확한 크기를 갖는
데이터 타입을 위한 int16_t
, int32_t
등을 쓸 수
있습니다. 자세한 것은 질문
11.I를 참고하기 바랍니다.
표준에 따라 정확히 말하면, int_16_t
, int32_t
등이
정의된 헤더 파일은 <stdint.h>
에 정의되어 있습니다.
다시, 표준에 따르면, <inttype.h>
는 <stdint.h>
를
포함하게 되며, 부가적인 사항들을 제공합니다.
__longlong
, 또는 __very long
등) 타입을
지원합니다.
따라서, 64-bit 타입을 쓰고자 하는 개발자는 적절한 typedef를 써서 코드를 작성해야 합니다. (또 이식성이 높은 코드를 만들기 위해서, 16, 32-bit 시스템을 위해 64-bit를 일일히 수동으로 처리할 수 있는 코드도 만들어야 할 것입니다.) Vendor들은 또, “정확히 64 bit인 타입”보다 (현재 C 표준에 존재하지 않는) “적어도 64 bit 이상인 타입”을 소개해야 합니다.
__longlong
타입으로 지원합니다.)
또 대부분의 컴파일러들은 short int를 16 비트로,
int를 32 비트로, long int를 64 비트로 지원하고 있으며,
이와 다르게 할 이유가 없습니다.
char *p1, p2;p2를 쓰려고 할 때 에러가 납니다.
char *p1, *p2;
*가 declarator의 일부분이기 때문에, 위에 쓴 것처럼 공백 문자를 쓰는 것이 좋습니다; char*와 같이 쓰는 것은 실수를 내기 쉬우며, 혼동을 가져올 수 있습니다.
char *p; *p = malloc(10);
extern int i; extern int f();
(extern keyword는 함수 선언에서 option입니다; 질문 1.11을 참고하기 바랍니다.)
아래는 정의의 예입니다:
int i = 0; int f() { return 1; }여러 소스 파일에서 변수나 함수를 공유할 필요가 있다면, 당연히 여러분은 모든 변수와 함수를 일관되게(consistent) 만들어야 합니다. 가장 좋은 방법은 각 정의를 관련된
.c
파일에 저장하고,
external 선언을 헤더 파일(“.h
”)에 두는 것입니다.
그리고 선언이
필요한 곳에서는 #include
를 써서 포함시키면 됩니다. 정의를
포함하는 .c
파일에도 같은 헤더 파일을 포함시켜야
컴파일러가 선언과 정의가 일치하는지 검사해 줍니다.
이 규칙은 매우 portability가 높은 방법입니다; 이는 ANSI C 표준의 요구에도 부합하며, ANSI 이전의 컴파일러와 링커에서도 잘 동작합니다 (UNIX 컴파일러와 링커는 최대 하나가 초기화된다는 조건 아래에 여러 개의 선언을 가능케 하는 “common model”을 지원합니다; 이는 ANSI 표준에 의해 “common extension”으로 언급되어 있지만, `pun1.3'은 아닙니다. 어떤 이상한 시스템에서는 외부 선언과 정의를 구별하기 위해서 반드시 초기값을 필요로 하기도 합니다.)
한 헤더 파일에 하나의 선언만 나오도록 하기 위해 다음과 같은 전처리기 트릭을 쓸 수도 있습니다:
DEFINE(int, i);
그리고 어떤 매크로의 설정에 따라 이 줄이 선언이나 정의가 되도록 할 수 있지만 이는 문제를 유발할 가능성이 많으므로 추천하지 않습니다.
컴파일러가 선언이 불일치하는지 검사하기 위해서는 반드시 전역 선언을
헤더 파일에 넣는 것이 중요합니다. 특히, external 함수의
prototype을 .c
파일에 넣지 않도록 하기 바랍니다.
이는 정의와 일치하는 지 검사해 주지도 않으며, 만약 정의와 일치하지
않는다면 오히려 쓰지 않는 것보다 못합니다.
_
')로 시작하는
이름을 사용합니다. (질문
1.29를 참고해서, 사용자와 시스템에서
제공하는 이름들과 충돌나지 않게 하기 바랍니다.)
어떤, 특별한 linker를 써서, 기존의 이름들의 scope를 제한해서 충돌을 미리 방지하는 방법도 있을 수 있지만, C 언어의 범위를 넘어가는 내용이므로 여기에서 다루지 않습니다.
extern int f(); int f();덧붙여 질문 1.10도 참고하시기 바랍니다.
typedef char *String_t; #define String_d char * String_t s1, s2; String_d s3, s4;위의 예에서 보면, s1, s2, s3는 모두
char *
타입이지만, s4는 char 타입입니다. 물론 이 결과는 개발자가
원한 것이 아닙니다.
(덧붙여 질문
1.5도 참고하시기 바랍니다.)
매크로가 좋은 이유는, #ifdef
를 쓸 수 있기 때문입니다.
(덧붙여 질문
10.15도 참고하시기 바랍니다.) 반면에 typedef는 또 스코프 규칙을 잘
따른다는 장점이 있습니다. (즉, 함수나 블럭의 안에서 선언되어,
그 안에서만 영향을 줄 수 있습니다.)
typedef struct { char *item; NODEPTR next; } *NODEPTR;
struct node { char *item; struct node *next; }; typedef struct node *NODEPTR;
이 문제를 해결하기 위해 적어도 세 가지의 다른 방법이 있습니다.
서로를 포함하는 한 쌍의 typedef된 구조체를 정의할 때도 비슷한 문제가 발생할 수 있으며, 위와 같은 방식으로 해결할 수 있습니다.
typedef struct { int afield; BPTR bpointer; } * APTR; typedef struct { int bfield; APTR apointer; } * BPTR;
struct a { int afield; struct b *bpointer; }; struct b { int bfield; struct a *apointer; };그러면, 컴파일러는, 아직 (자세히 말해, 아직 `incomplete'한 구조체인) struct b에 대해서 잘 모르지만 struct a 안에서 struct b 타입을 가리키는 struct b *bpointer를 쓰는 것을 허락합니다. 때때로 다음과 같이 먼저 선언해 주는 것이 필요합니다:
struct b;This empty declaration masks the pair of structure declarations (if in an inner scope) from a different struct b in an outer scope. 위와 같이 두 구조체를 tag 이름과 같이 선언한 다음, 다음과 같이 따로 typedef를 만들 수 있습니다:
typedef struct a *APTR; typedef struct b *BPTR;멤버 포인터에 typedef 이름을 쓰기 위해, 다음과 같이 typedef 이름을 먼저 선언할 수도 있습니다:
typedef struct a *APTR; typedef struct b *BPTR; struct a { int afield; BPTR bpointer; }; struct b { int bfield; APTR apointer; };덧붙여 질문 1.14도 참고하시기 바랍니다..
struct x1 { ... }; typedef struct { ... };
funcptr pf1, pf2;위 선언은 아래와 완전히 같은 뜻이지만, 좀 더 보기 쉽습니다:
int (*pf1)(), (*pf2)();덧붙여 질문 1.21, 4.12, 15.11도 참고하시기 바랍니다.
typedef char *charp; const charp p;Why is p turning out const instead of the characters pointed to?
const int n = 5; int a[n];
여러분이 프로그램을 *(*(*a[N])())()
와 같은 declarator로
암호화하려는 취미가 없다면, 질문
1.21의 두번째와 같이 typedef를 써서
간단하게 만들 수 있습니다.
How do I declare an array of N pointers to functions returning pointers to functions returning pointers to characters?
char *(*(*a[N])())();
typedef char *pc; /* pointer to char */ typedef pc fpc(); /* function returning pointer to char */ typedef fpc *pfpc; /* pointer to above */ typedef pfpc fpfpc(); /* function returning... */ typedef fpfpc *pfpfpc; /* pointer to... */ pfpfpc a[N]; /* array of... */
cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char char *(*(*a[])())()
C 언어를 설명하는 좋은 책이라면 이러한 복잡한 선언에 대한 내용이 있기 마련이니 이 부분을 꼭 읽어보시기 바랍니다.
위의 예에서 쓰인 함수 포인터(pointer-to-function) 선언은 파라메터 타입에 대한 정보를 포함하지 않았습니다. 만약 파라메터가 복잡한 함수라면 전체 선언은 매우 읽기 어렵습니다 (최신 버전의 cdecl은 이 경우에도 도움이 됩니다.)
file1.c: int array[] = { 1, 2, 3 };
file2.c: extern int array[];
file1.c: int array[] = { 1, 2, 3}; int arraysz = sizeof(array);
file2.c: extern int array[]; extern int arraysz;덧붙여 질문 6.23도 참고하시기 바랍니다.
file1.h: #define ARRAYSZ 3
file1.c: #include "file1.h" int array[ARRAYSZ];
file2.c: #include "file1.h" extern int array[ARRAYSZ];
file1.c: int array[] = { 1, 2, 3};
file2.c: extern int array[];
또는 어떤 헤더 파일에 선언되어 있는 함수 이름을 (정말로) 중복해서 다른 함수로 만들었을 경우에 이 문제가 발생할 수 있습니다.
double array[256][256];
ANSI 규칙에 따라, 우리는 어떤 identifier가 예약(reserved)되어 있는지 알 수 있습니다. 먼저, 우리는 identifier에 대한 특징에 대해 알아야 합니다: 이 특징에는 scope, namespace, linkage가 있습니다.
[ANSI] § 4.1.2.1 ([C89] § 7.1.3)에 따르면 다음과 같은 규칙이 있습니다:
Rule 1 | 밑줄 문자로 시작하고, 두 번째 문자가 밑줄이거나 대문자인 모든 이름은 (모든 scope와 모든 namespace에서) 항상 reserved 상태입니다. |
Rule 2 | 밑줄로 시작하는 모든 이름은 file scope에서 ordinary identifier(즉, 함수, 변수, typedef, enumeration constant 등)를 위해 reserve되어 있습니다. |
Rule 3 | 어떤 표준 헤더 파일을 포함했을 때, 그 헤더 파일에서 제공하는 모든 매크로 이름은 reserve되어 있습니다. |
Rule 4 | (함수 이름처럼) External linkage를 가지는 모든 표준 라이브러리 identifier들은 external linkage를 위한 identifier로 reserve되어 있습니다. |
Rule 5 | 표준 헤더 파일에 정의되어 있는, file scope를 가지는 typedef와 tag 이름은, 그 헤더 파일을 포함시켰을 경우, (같은 namespace를 지니는) file scope에서 모두 reserve되어 있습니다. (표준은 실제로 “each identifier with file scope,”라고 말하지만, 네번째 규칙에 적용되지 않는 이름은 typedef와 tag 이름밖에 없습니다.) |
게다가, 여러 매크로 이름과 표준 라이브러리 identifier가 미래 표준을 위해 예약되어 있기 때문에, 규칙 3, 4를 더 복잡하게 만듭니다. 즉, 어떤 패턴을 가지는 이름들이 미래에 나올 표준에 쓰일 수 있으며, 이 패턴은 아래 표에 나와 있습니다:
Header | Future directions patterns |
---|---|
<ctype.h> |
is[a-z]* ,
to[a-z]* (function) |
<errno.h> |
E[0-9]* ,
E[A-Z]* (macros) |
<locale.h> |
LC_[A-Z]* (macros) |
<math.h> |
cosf, sinf, sqrtf, etc. |
cosl, sinl, sqrtl, etc. (all functions) | |
<signal.h> |
SIG[A-Z]* , SIG_[A-Z]*
(macros) |
<stdlib.h> |
str[a-z]* (functions) |
<string.h> |
mem[a-z]* , str[a-z]* ,
wcs[a-z]* (functions) |
여기에서 [A-Z]는 “아무 대문자”를 뜻하며, [a-z]는
“아무 소문자”를 뜻하며, [0-9]는 “아무 숫자”를 뜻합니다.
*는 “아무것이나 다”를 뜻합니다. 예를 들어, 여러분이
<stdlib.h>
를 포함시켰다면, str로 시작하고, 그 다음
글자가 소문자로 이루어진 모든 external identifier는 예약되어 있습니다.
이 다섯개의 규칙이 어렵다면, 다음 advice를 따르면 됩니다:
사실 위 advice는 너무 고압적입니다. 원한다면 다음 예외 사항을 기억하기 바랍니다:
1, 2 | 밑줄로 시작하고, 두 번째 문자가 숫자나 소문자인 이름을, label 또는 structure/union 이름에 쓸 수 있습니다. 또 function, block, prototype scope에서 쓸 수 있습니다. |
3 | 어떤 매크로를 정의하는 표준 헤더 파일을 포함시키지 않는다면, 그 (매크로) 이름을 쓸 수 있습니다. |
4 | 표준 라이브러리 함수 이름을, static 또는 local 변수에 쓸 수 있습니다. (정확히 말해서, internal 또는 no linkage를 가지는 이름으로 쓸 수 있습니다.) |
5 | 어떤 typedef나 tag 이름을 정의하는 표준 헤더 파일을 포함시키지 않았다면, 그 이름을 쓸 수 있습니다. |
그러나, 위 예외 사항을 쓰기 전에, 몇 개는 위험할 수 있다는 것을 꼭 기억하기 바랍니다. (특히, 세번째와 다섯번째 예외 사항은 조심해야 합니다. 세번째와 다섯번째에 해당하는 이름을 쓴 후, 관련된 헤더 파일을 나중에 포함시키는 등의 실수가 우려됩니다.) 그리고 예외 1, 2는 이른바 시스템과 사용자 namespace 사이의 무인도(no man's land)에 해당하는 namespace를 다루고 있습니다.
이러한 예외 사항을 제공하는 이유는, 여러 add-in 라이브러리 제작자가, 자신만의 internal, hidden identifier를 선언할 수 있게 하기 위해서 입니다. 즉, 위 예외 사항에 해당하는 identifier를 썼을 경우, 시스템이 제공하는 identifier와 충돌할 가능성은 없습니다. 그러나 third-party가 제공하는 library의 identifier와 충돌할 가능성은 여전히 존재합니다. (또, 이러한 라이브러리 제작자라면, 필요할 경우에 시스템이 쓸 수 있는 identifier를 쓰는 것도 괜찮습니다. 단 매우 주의해야 합니다.)
일반적으로, 네번째 예외 사항을 써서, 함수 파라메터나 local 변수 이름에 표준 라이브러리 함수 이름이나, 앞 표에 나온 “future directions” 패턴에 해당하는 이름을 쓰는 것은 괜찮습니다. 예를 들어, “string”은 파라메터 이름이나 local 변수 이름으로 흔히 쓰이는 -- 그리고 합법적인 -- 이름입니다.
= 0
”으로 써 준 것과 똑같다는
말입니다. 따라서 널 포인터일 경우에도 올바른 값을
가집니다 (Chapter 5 참고).
마찬가지로 실수일 경우 0.0으로 해석됩니다.
“automatic” 속성을 가진 (static으로 선언되지 않은 지역 변수) 변수는 초기화되지 않을 경우 쓰레기 값을 가집니다. (이 경우에 쓰레기 값이 무엇인지는 아무도 모르며, 쓸 이유가 없습니다.)
malloc()이나 realloc()으로, 동적으로 할당된 메모리도 처음에 쓰레기 값을 가집니다. 따라서 프로그램에서 적당히 초기화시켜 주어야 합니다. calloc()으로 할당받은 메모리는 비트 단위로 0을 가지게 되지만, 이 것이 포인터나 실수 타입에서 0을 의미한다고 말할 수는 없습니다 (질문 7.31과 Chapter 5 참고).
int f() { char a[] = "hello, world!"; }
(그리고 변수 a가 어떻게 쓰일 지에 따라 다르겠지만, 이 변수를 전역 (global) 또는 static으로, 또는 포인터로 바꾸어서 해결할 수 있으며, 또는 strcpy() 등을 써서 대입시켜주는 방법도 있습니다.)
char *p = malloc(10);
제 컴파일러는 “invalid initializer”라는 에러 메시지를 출력합니다.
char a[] = "string literal"; char *p = "string literal";
(오래된 C 언어 코드의 경우, p와 같은 포인터로 문자열의 내용을 변경하려고 시도하는 경우도 있습니다. 이러한 경우를 해결하기 위해 어떤 컴파일러는 문자열을 쓰기 가능한 메모리에 저장하도록 하는 옵션을 가지고 있습니다.)
질문 1.31, 6.1, 6.2, 6.8을 참고하기 바랍니다.
char a[3] = "abc";
가 맞는 표현인가요?
extern int func(); int (*fp)() = func;
위와 같이 함수 이름이 수식에서 쓰인 경우, 이 이름은 -- 이 함수의 시작 주소를 나타내는 -- 포인터로 변경(decay)됩니다. 배열 이름이 단독으로 쓰이는 경우와 비슷합니다.
일반적으로 명백히 함수의 선언을 미리 적어주게 됩니다. 왜냐하면 이 경우, 자동으로 외부 함수 선언1.6을 만들어주지 않기 때문입니다 (왜냐하면 초기화에서 쓰이는 함수 이름은 함수 호출이 아니기 때문입니다).
Seong-Kook Shin