Subsections


1. Declarations and Initializations

C 언어의 선언 문법은 (declaration syntax) 그 자체가 하나의 프로그래밍 언어라고 할 수 있습니다. 선언은 다음과 같이 여러 부분으로 구성되어 (꼭 모든 부분을 다 가져야 할 필요는 없습니다.) 있습니다: storage class, base type, type qualifier, 그리고 declarator (declarator는 initializer를 포함할 수 있습니다.) 각 declarator는 새 identifier를 선언하는 것 이외에, identifier가 배열인지, 포인터인지, 함수인지, 또는 어떤 복잡한 타입인지를 알려줍니다. 따라서 선언이 실제 identifier가 어떻게 쓰일 것인지를 (declaration mimics use) 알려 줍니다 (질문 [*]1.21은 이 `declaration mimics use' 관계를 자세하게 다룹니다.)


1.1 Basic Type

프로그래머들은 종종 C 언어가 충분히 저수준 언어이면서도, C 언어의 type system이 상당한 수준으로 추상화되어 있다는 것에 놀라곤 합니다; 기본 타입들의 크기와 표현 방법이 언어 자체에 정확히 정의되어 있지 않습니다.



Q 1.1
어떤 타입의 정수를 쓸 것인지 어떻게 결정하죠?

Answer
큰 값 (32,767 이상이거나 -32,767 이하)이 필요한 경우, long을 쓰기 바랍니다. 차지하는 공간이 매우 중요하다면 (큰 배열이나 많은 구조체를 쓸 경우), short를 쓰기 바랍니다. 이런 경우가 아니라면 int를 쓰는 것이 가장 좋습니다. 오버플로우 문제가 중요시되고 음수가 필요하지 않는 경우라면, 또는 비트를 다룰 때 부호 확장1.1에서 문제가 발생하는 것을 원치 않는다면 적절한 크기를 가진 형의 unsigned 형을 쓰는 것도 좋습니다. (하지만 수식에서 signed 형과 unsigned 형을 섞어 쓰는 것은 좋지 않습니다.)

문자 타입도 (특히 unsigned char) “작은” 정수타입으로 쓰일 수 있지만, 예상치 못한 부호 확장이나 코드의 크기를 증가시킬 수 있기 때문에 바람직하지 않습니다. (unsigned char 타입을 쓰는 것이 도움이 될 경우도 있습니다; 관련된 문제는 질문 [*]12.1를 참고하기 바랍니다.)

비슷한 크기/빠르기 문제가 floatdouble에서 발생할 수 있습니다. 변수의 주소가 필요하고 어떤 특별한 타입이 필요한 경우라면 위의 규칙들은 모두 적용되지 않습니다.

C 언어에서 각각의 type들이 정확한 크기를 가지도록 정의되어 있다고 착각하기 쉬운데, 사실은 그렇지 않습니다. C 언어에서 정의하고 있는 것은 다음과 같습니다:

이 규칙은 char가 적어도 8 bit가 되어야 한다는 것과, short intint는 적어도 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 참고)

References

[K&R2] § 2.2 p. 36, § A4.2 pp. 195-6, § B11 p. 257
[C89] § 5.2.4.2.1, § 6.1.2.5
[H&S] § 5.1,5.2 pp. 110-114
Note
C99 표준은 새 정수 타입인 long long int를 추가했습니다. 이 타입은 최소 64 bit이며, 다음과 같은 특징을 지닙니다.
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를 참고하기 바랍니다.

References
[C99] § 5.2.4.2.1 pp. 22-23, § 6.2.5



Q 1.2
왜 표준에서는 각 type의 크기를 정확히 정의하지 않나요?
Answer
다른 high-level 언어에 비해 C 언어가 확실히 저 수준 언어이기는 하지만, object의 정확한 크기는 implementation이 결정할 문제입니다. (C 언어에서 여러분이 bit의 갯수를 지정할 수 있는 유일한 곳은 structure 안에서 bitfield를 쓸 때입니다.; 질문 [*]2.25와 [*]2.26을 참고하기 바랍니다.) 대부분의 프로그램에서는 크기를 정확히 지정할 필요가 없습니다; 크기를 정확히 지정하는 많은 프로그램들은 지정하지 않도록 프로그램을 작성했을 때, 더 낫습니다.

int type은 컴퓨터의 가장 자연스러운 word size를 나타내는 것으로 알려져 있으며, 대부분의 정수를 저장할 때 가장 적당한 type입니다. 질문 [*]1.1의 guideline을 보기 바랍니다; 덧붙여 질문 [*]12.42, [*]20.5도 참고하시기 바랍니다.

Note
C99에서 새로 제공하는 정수 타입들은 개발자가 원하는 크기를 만족시킬 수 있습니다. 덧붙여 질문 [*]11.I도 참고하시기 바랍니다.



Q 1.3
C 언어가 size를 정확히 정의하지 않기 때문에, 저는 int16, int32와 같은 typedef 이름을 씁니다. 그래서 컴퓨터에 따라서 int, short, long 등을 써서 만듭니다. 이 방법이 모든 문제를 해결해 줄 수 있을 것 같은데, 맞습니까?
Answer
정확히 size를 제어할 필요가 있다면, 좋은 방법입니다. 그러나 다음과 같은 사항을 주의해야 합니다: 덧붙여 질문 [*]10.16, [*]20.5도 참고하시기 바랍니다.

References
[C89] § 7.18.1.1
Note
C99에 따르면 <inttype.h>를 포함시켰을 때, 정확한 크기를 갖는 데이터 타입을 위한 int16_t, int32_t등을 쓸 수 있습니다. 자세한 것은 질문 [*]11.I를 참고하기 바랍니다.

표준에 따라 정확히 말하면, int_16_t, int32_t등이 정의된 헤더 파일은 <stdint.h>에 정의되어 있습니다. 다시, 표준에 따르면, <inttype.h><stdint.h>를 포함하게 되며, 부가적인 사항들을 제공합니다.

References
[C99] § 7.18



Q 1.4
64-bit를 지원하는 컴퓨터에서 64-bit 타입을 쓸 수 있는 방법이 있나요?

Answer
골치아픈 문제입니다. 다음과 같이 세 가지 방식으로 이 문제를 생각할 수 있습니다.

따라서, 64-bit 타입을 쓰고자 하는 개발자는 적절한 typedef를 써서 코드를 작성해야 합니다. (또 이식성이 높은 코드를 만들기 위해서, 16, 32-bit 시스템을 위해 64-bit를 일일히 수동으로 처리할 수 있는 코드도 만들어야 할 것입니다.) Vendor들은 또, “정확히 64 bit인 타입”보다 (현재 C 표준에 존재하지 않는) “적어도 64 bit 이상인 타입”을 소개해야 합니다.

Note
[ANSI] § F.5.6
[C89] § G.5.6

Answer
다가오는 C 표준(C9X)에서는 long long 타입이 적어도 64 비트 이상이 될 것을 명시하고 있습니다. 그리고 이 타입은 이미 여러 컴파일러에 의해 지원되고 있습니다 (어떠한 컴파일러는 __longlong 타입으로 지원합니다.) 또 대부분의 컴파일러들은 short int를 16 비트로, int를 32 비트로, long int를 64 비트로 지원하고 있으며, 이와 다르게 할 이유가 없습니다.

질문 [*]18.15d를 참고하시기 바랍니다.

References
[C9X] § 5.2.4.2.1, § 6.1.2.5.

Note
최신 C 표준, C99에서는 적어도 64 bit 이상인, long long 타입을 지원합니다. 질문 [*]1.1, [*]1.3을 참고하기 바랍니다.
References
[C99] § 5.2.4.2.1 pp. 22-23, § 6.2.5


1.2 Pointer Declarations

포인터에 관한 것은 Chapter 4부터 Chapter 7까지 설명되어 있지만, 여기에서는 선언에 관련된 것만 다룹니다.



Q 1.5
다음 선언에서 무엇이 잘못되었나요?
  char *p1, p2;
p2를 쓰려고 할 때 에러가 납니다.

Answer
위의 선언에서 잘못된 점은 없습니다 -- 여러분이 원하는 작업을 해 주지 못한다는 것을 제외하면 말입니다. Pointer 선언에서 *는 `base type'의 일부분이 아닙니다; *는 선언할 이름으로 구성된 declarator의 일부분입니다 (질문 [*]1.21 참고). 선언을 쓸 경우에 공백 문자의 구분은 의미가 없습니다. 따라서 첫번째 declarator는 “* p1”이며, *를 포함하고 있기 때문에, p1은 `a pointer to char', 즉 char를 가리키는 pointer가 됩니다. 그러나 p2의 declarator는 p2 이외에 다른 것을 가지고 있지 않으므로, (여러분이 원하는 바가 아니겠지만) 간단히 char type이 됩니다. 한 선언에서 두 개의 pointer를 선언하려면, 다음과 같이 해야 합니다:

  char *p1, *p2;

*가 declarator의 일부분이기 때문에, 위에 쓴 것처럼 공백 문자를 쓰는 것이 좋습니다; char*와 같이 쓰는 것은 실수를 내기 쉬우며, 혼동을 가져올 수 있습니다.

덧붙여 질문 [*]1.13도 참고하시기 바랍니다.



Q 1.6
Pointer를 선언하고 그 pointer가 어떤 공간을 할당하려고 했으나, 제대로 동작하지 않습니다. 아래 코드에 어떤 문제가 있습니까?
  char *p;
  *p = malloc(10);

Answer
여러분이 선언한 pointer는 p이지 *p가 아닙니다. 질문 [*]4.2를 참고하기 바랍니다.


1.3 Declaration Style

변수나 함수를 선언하는 것은 단순히 컴파일러를 기쁘게? 하기 위한 것이 아닙니다; it also injects useful order into a programming project. When declarations are arranged appropriately within a project, mismatches and other difficulties can be more easily avoided, and the compiler can more accurately catch any error that do occur.



Q 1.7
전역(global) 함수와 변수를 선언 또는 정의하는 가장 좋은 방법이 무엇일까요?

Answer
먼저 한 “global” 변수는 (여러 translation unit에서) 여러 개의 선언을 가질 수 있지만, 반드시 하나의 정의를 가져야 합니다. Global 변수에서 정의는 공간을 할당하고, 필요하다면 초기값을 지정하는 일종의 선언입니다. 함수에서 선언은 function body를 제공하는 일종의 선언입니다. 아래는 선언의 예입니다:

  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 파일에 넣지 않도록 하기 바랍니다. 이는 정의와 일치하는 지 검사해 주지도 않으며, 만약 정의와 일치하지 않는다면 오히려 쓰지 않는 것보다 못합니다.

질문 [*]10.6과 [*]18.8을 참고하기 바랍니다.

References
[K&R1] § 4.5 pp. 76-7
[K&R2] § 4.4 pp. 80-1
[C89] § 6.1.2.2, § 6.7, § 6.7.2, § G.5.11
[ANSI Rationale] § 3.1.2.2
[H&S] § 4.8 pp. 101-104, § 9.2.3 p. 267
[CT&P] § 4.2 pp. 54-56



Q 1.8
C 언어에서 추상화된 data type을 만드는 가장 좋은 방법이 무엇일까요?
Answer
질문 [*]2.4를 참고하기 바랍니다.



Q 1.9
“semiglobal” 변수, 즉, 몇 소스 파일의 함수들은 볼 수 있으나, 다른 소스 파일에서는 볼 수 없는 그러한 `절반 정도의' 전역 변수를 만들 수 있을까요?

Answer
C 언어에서 그런 일은 할 수 없습니다. 모든 함수를 같은 source 파일에 넣는 것이 불편하거나 불가능하다면, 아래 중 한가지 방법을 쓰시기 바랍니다:

어떤, 특별한 linker를 써서, 기존의 이름들의 scope를 제한해서 충돌을 미리 방지하는 방법도 있을 수 있지만, C 언어의 범위를 넘어가는 내용이므로 여기에서 다루지 않습니다.


1.4 Storage Class

우리는 선언의 두 가지 부분, base type과 declarator를 이미 다루었습니다. 다음 몇 질문에서는 storage class에 대한 것을 다룹니다. Storage class는 선언된 object나 함수의 visibility와 lifetime을 (각각 “scope”와 “duration”이라고 부르기도 합니다.) 다룹니다.



Q 1.10
static 함수나 변수를 선언할 때, 항상 static이라고 써 주어야 하나요?
Answer
언어 표준에서는 항상 써 줄 것을 요구하지는 않습니다. (가장 중요한 것은 첫 선언에 static을 써 주는 것입니다.) 하지만 규칙이 조금 복잡하게 얽혀있고, 함수냐 변수냐에 따라 조금씩 다릅니다. (게다가 이 문제에 있어서 현존하는 코드도 너무나도 다양합니다.) 따라서 가장 안전한 방법은, 모든 정의와 선언에서 항상 static을 써 주는 것입니다.
References
[ANSI] § 3.1.2.2
[C89] § 6.1.2.2
[ANSI Rationale] § 3.1.2.2
[H&S] § 4.3 p. 75



Q 1.11
함수 선언에서 extern이 의미하는 게 무엇인가요?
Answer
이 함수의 정의가 다른 소스 파일에 있을 수 있다는 것을 알려주는 단순한 스타일적인 문제입니다. 따라서 다음 두 줄의 차이는 없습니다:

  extern int f();
  int f();
덧붙여 질문 1.10도 참고하시기 바랍니다.
References
[C89] § 6.1.2.2, § 6.5.1
[ANSI Rationale] § 3.1.2.2
[H&S] § 4.3, 4.3.1 pp. 75-6



Q 1.12
auto 키워드는 어디에 쓰이나요?
Answer
전혀 쓰이지 않습니다. 이는 오래된(archaic) 문법에 쓰이는 것으로 현재에는 쓰이지 않습니다. 질문 [*]20.37을 참고하기 바랍니다.

References
[K&R1] § A8.1 p. 193
[C89] § 6.1.2.4, § 6.5.1
[H&S] § 4.3 p. 75, § 4.3.1 p. 76


1.5 Typedefs

typedef가 문법적으로는 storage class이지만, 이 키워드는 이름이 알려주듯, 새로운 type 이름을 정의하는 데에 쓰입니다.



Q 1.13
새 타입을 만들때, typedef를 쓰는 것하고, 매크로를 쓰는 것하고 무슨 차이가 있죠?
Answer
일반적으로, typedef를 쓰는 것이 더 좋습니다. 왜냐하면 포인터 타입을 쓸 때 그 차이가 드러납니다:
  typedef char *String_t;
  #define String_d char *
  String_t s1, s2;
  String_d s3, s4;
위의 예에서 보면, s1, s2, s3는 모두 char * 타입이지만, s4char 타입입니다. 물론 이 결과는 개발자가 원한 것이 아닙니다. (덧붙여 질문 [*]1.5도 참고하시기 바랍니다.)

매크로가 좋은 이유는, #ifdef를 쓸 수 있기 때문입니다. (덧붙여 질문 [*]10.15도 참고하시기 바랍니다.) 반면에 typedef는 또 스코프 규칙을 잘 따른다는 장점이 있습니다. (즉, 함수나 블럭의 안에서 선언되어, 그 안에서만 영향을 줄 수 있습니다.)

덧붙여 질문 [*]1.17, [*]2.22, [*]11.11, [*]15.11도 참고하시기 바랍니다.

References
[K&R1] § 6.9 p. 141
[K&R2] § 6.7 pp. 146-7
[CT&P] § 6.4 pp. 83-4



Q 1.14
Linked list 정의가 안됩니다. 다음과 같이 했는데 컴파일러는 계속 에러 메시지만 출력합니다. C 언어에서 구조체는 자신에 대한 포인터를 포함할 수 없는 것인가요?
  typedef struct {
    char *item;
    NODEPTR next;
  } *NODEPTR;

Answer
C 언어에서 구조체는 자신에 대한 포인터를 포함할 수 있습니다. 이 질문에서 문제는 NODEPTRtypedef 이름이고, 이 이름을 사용한 시점에서 이 이름의 정의가 끝나지 않았다는 것입니다. 이 코드를 고치기 위해서, 먼저 구조체에 태그(tag, struct node)를 만들고 “next” 필드를 “struct node *” 타입으로 선언합니다. 또는 typedef 선언과 구조체 정의를 분리시켜도 됩니다. 다음 코드가 해결 방법 중 하나입니다.

  struct node {
    char *item;
    struct node *next;
  };
  typedef struct node *NODEPTR;

이 문제를 해결하기 위해 적어도 세 가지의 다른 방법이 있습니다.

서로를 포함하는 한 쌍의 typedef된 구조체를 정의할 때도 비슷한 문제가 발생할 수 있으며, 위와 같은 방식으로 해결할 수 있습니다.

질문 [*]2.1을 참고하시기 바랍니다.

References
[K&R1] § 6.5 p. 101
[K&R2] § 6.5 p. 139
[C89] § 6.5.2, § 6.5.2.3
[H&S] § 5.6.1 pp. 132-3



Q 1.15
서로 참조하는 구조체를 만들 수 있을까요? 아래처럼 코드를 만들었는데 컴파일러가 BPTR이 없다고 에러를 출력합니다.
  typedef struct {
    int afield;
    BPTR bpointer;
  } * APTR;

  typedef struct {
    int bfield;
    APTR apointer;
  } * BPTR;
Answer
질문 [*]1.14처럼, 이 문제는 구조체나 포인터에 있는 것이 아니라, typedef에 관한 것입니다. 먼저, (typedef 없이) 두 구조체 tag 이름을 써서 다음과 같이 만듭니다:
  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도 참고하시기 바랍니다..
References
[K&R2] § 6.5 p. 140
[ANSI] § 3.5.2.3
[C89] § 6.5.2.3
[H&S] § 5.6.1 p. 132



Q 1.16
다음 두 선언은 무엇이 서로 다른가요?
  struct x1 { ... };
  typedef struct { ... };
Answer
질문 [*]2.1을 보기 바랍니다.



Q 1.17
typedef int (*funcptr)();”은 무슨 뜻인가요?
Answer
typedef 이름인 funcptr을 정의합니다. funcptr은 여기에서, 임의의 갯수의 인자를 받고 int를 리턴하는 함수에 대한 포인터입니다. 예를 들어 다음과 같이 하나 이상의 함수에 대한 포인터를 선언할 수 있습니다:
  funcptr pf1, pf2;
위 선언은 아래와 완전히 같은 뜻이지만, 좀 더 보기 쉽습니다:
  int (*pf1)(), (*pf2)();
덧붙여 질문 [*]1.21, [*]4.12, [*]15.11도 참고하시기 바랍니다.
References
[K&R1] § 6.9 p. 141
[K&R2] § 6.7 p. 147


1.6 The const qualifier

C 언어 선언에서는 type qualifier라는 게 있으며, 이는 ANSI C에서 새로 추가된 것입니다. Qualifier에 관한 질문은 Chapter 11에서 다룹니다.



Q 1.18
I've got the declarations
  typedef char *charp;
  const charp p;
Why is p turning out const instead of the characters pointed to?
Answer
질문 [*]11.11을 보기 바랍니다.



Q 1.19
아래와 같이 배열을 초기화하는데 왜 const 값을 쓸 수 없는 것인가요?
  const int n = 5;
  int a[n];
Answer
질문 [*]11.8을 보기 바랍니다.



Q 1.20
const char *p, char const *p, char * const p가 서로 어떻게 다른가요?
Answer
질문 [*]11.9, [*]1.21을 보기 바랍니다.


1.7 Complex Declarations

C 언어의 선언은 엄청나게 복잡해질 수 있습니다. 일단 이 암호?를 풀어낼 방법을 배우면, 물론 이런 복잡한 선언은 거의 쓰이지 않지만, 어떤 선언이 나오더라도 두려워하지 않게 됩니다.

여러분이 프로그램을 *(*(*a[N])())()와 같은 declarator로 암호화하려는 취미가 없다면, 질문 [*]1.21의 두번째와 같이 typedef를 써서 간단하게 만들 수 있습니다.



Q 1.21
문자를 가리키는 포인터를 리턴하는 함수에 대한 포인터를 리턴하는 함수에 대한 포인터 N 개로 이루어진 배열(array)을 어떻게 선언하죠?

Note
참고로 원문은 다음과 같습니다:
How do I declare an array of N pointers to functions returning pointers to functions returning pointers to characters?

Answer
다음과 같이 세 가지로 답할 수 있습니다:

C 언어를 설명하는 좋은 책이라면 이러한 복잡한 선언에 대한 내용이 있기 마련이니 이 부분을 꼭 읽어보시기 바랍니다.

위의 예에서 쓰인 함수 포인터(pointer-to-function) 선언은 파라메터 타입에 대한 정보를 포함하지 않았습니다. 만약 파라메터가 복잡한 함수라면 전체 선언은 매우 읽기 어렵습니다 (최신 버전의 cdecl은 이 경우에도 도움이 됩니다.)

References
[K&R2] § 5.12 p. 122
[C89] § 6.5ff (esp. § 6.5.4)
[H&S] § 4.5 pp. 85-92, § 5.10.1 pp. 149-50



Q 1.22
같은 타입의 함수에 대한 포인터를 리턴하는 함수를 선언할 수 있나요? 저는 상태 머신(state machine)을 만들고, 각 함수가 하나의 상태를 나타내게 한 다음, 이 함수가 다음 상태를 나타내는 함수 포인터를 리턴하게 하려고 합니다. 그러나 이런 함수를 선언할 방법을 찾지 못하고 있습니다.
Answer
직접 할 수는 없습니다. 한가지 방법은 함수가 일반적인(generic) 함수 포인터를 리턴하게 한 다음, 이를 원하는 타입으로 캐스팅하는 것과 또 다른 방법으론 함수가 어떤 구조체를 리턴하게 하고, 그 구조체에 이 타입의 함수에 대한 포인터를 저장하는 방식을 쓸 수 있습니다.


1.8 Array Sizes



Q 1.23
지역 변수나 파라메터로 배열을 선언할 때, 그 크기를 따로 인자로 받아서 선언할 수 있을까요?
Answer
간단히 말해, 불가능합니다. 질문 [*]6.15, [*]6.19를 참고하기 바랍니다.



Q 1.24
다음과 같이, 한 파일에 extern 배열을 만들고, 다른 파일이 그 배열을 쓰려고 합니다. file2.c에서 왜 sizeof 연산이 안되나요?
file1.c:
  int array[] = { 1, 2, 3 };

file2.c:
  extern int array[];

Answer
크기가 알려지지 않은, extern 배열 선언은 `incomplete type'입니다. 따러서 sizeof가 제대로 동작할 수 없습니다. 왜냐하면, 컴파일할 때, 다른 파일에 정의되어 있는 배열의 크기를 알 수 없기 때문입니다. 이 때, 사용자는 다음과 같이 세 가지 방법을 쓸 수 있습니다:
  1. 변수 하나를 따로 두어서, 그 배열의 크기로 초기화한 다음 씁니다:
    file1.c:
      int array[] = { 1, 2, 3};
      int arraysz = sizeof(array);
    

    file2.c:
      extern int array[];
      extern int arraysz;
    
    덧붙여 질문 [*]6.23도 참고하시기 바랍니다.

  2. 배열의 크기를 매크로 상수로 정의해 두고 이를 정의와 선언에 함께 사용합니다:
    file1.h:
      #define ARRAYSZ       3
    
    file1.c:
      #include "file1.h"
      int array[ARRAYSZ];
    
    file2.c:
      #include "file1.h"
      extern int array[ARRAYSZ];
    
  3. 배열의 마지막 요소 값으로, 어떤 특정한 값을 (sentinel value, 일반적으로 0, -1, 또는 NULL) 써서, (나중에 필요하면 그 값이 나올때까지 세어서 배열의 크기를 알 수 있게) 특별히 크기에 대한 것을 지정하지 않도록 합니다.
    file1.c:
      int array[] = { 1, 2, 3};
    

    file2.c:
      extern int array[];
    
물론, 배열을 정의할 때, 초기값이 이미 정해져 있느냐에 따라 선택이 달라질 수 있습니다. 만약에 초기값이 위와 같이 정해져 있는 경우에는, 두번째 방식은 별로 좋지 않습니다. 덧붙여 질문 [*]6.21도 참고하시기 바랍니다.
References
[H&S] § 7.5.2 p. 195


1.9 Declaraction Problems

Sometimes the compiler insists on complaining about your declarations no matter how carefully constructed you thought they were. These questions uncover some of the reasons why. (Chapter 16 is a similar collection of baffling run-time problems.) 때때로 컴파일러는 여러분이 얼마나 주의깊게 선언을 만들었는지에 상관없이 불평을 하기도 합니다. 이 section의 질문들은 왜 그런지 그 이유를 설명합니다 (Chapter 16에서는 실행 시간에 발생할 수 있는 이상한 문제들에 대해 설명합니다.)



Q 1.25
제 컴파일러는 함수가 중복되어 정의되어 있다고 에러 메시지를 출력하지만 저는 한번만 정의했고, 한 번만 호출했습니다. 무엇이 잘못되었는지 모르겠군요.

Answer
함수의 선언이, 이 함수를 호출할 때까지 나타나지 않으면, 컴파일러는 이 함수가 int를 리턴한다고 가정합니다. 그 다음 나중에 실제 선언이나 정의가 나오고, 가정과 일치하지 않을 경우 그러한 에러가 발생합니다. 즉, int가 아닌 다른 타입을 리턴하는 함수는 호출하기 전에 반드시 선언해야 합니다.

또는 어떤 헤더 파일에 선언되어 있는 함수 이름을 (정말로) 중복해서 다른 함수로 만들었을 경우에 이 문제가 발생할 수 있습니다.

질문 [*]11.3과 질문 [*]15.1을 참고하기 바랍니다.

References
[K&R1] § 4.2 p. 70
[K&R2] § 4.2 p. 72
[C89] § 6.3.2.2
[H&S] § 4.7 p. 101



Q 1.25b
main()을 선언하는 정확한 방법이 궁금합니다. void main()으로 해도 좋은가요?
Answer
질문 [*]11.12a와 [*]11.15를 참고하기 바랍니다. (어쨌든 void main()은 틀린 선언입니다.)



Q 1.26
제가 보기에는 제대로 되어 있는데, 컴파일러가 함수 prototype이 서로 다르다고 경고합니다. 왜 그럴까요?
Answer
질문 [*]11.3을 보기 바랍니다.



Q 1.27
파일 가장 첫 부분에서 이상한 문법 에러가 발생합니다. 왜 그럴까요?
Answer
질문 [*]10.9를 보기 바랍니다.



Q 1.28
다음과 같이, 큰 배열을 만들었는데, 컴파일러가 에러를 냅니다. 왜 그럴까요?
  double array[256][256];
Answer
질문 [*]19.23, [*]7.16을 보기 바랍니다.


1.10 Namespace

이름을 짓는 것이 어려워 보이지 않을 지 몰라도, 사실 매우 어려운 문제입니다. 물론 함수나 변수의 이름을 짓는 것이 책이나 건물, 아기들의 이름을 짓는 것보다는 쉽습니다 -- 여러분은 많은 사람들이 여러분 코드의 이름들을 싫어할 지 모른다고 고민할 필요는 없습니다 -- 적어도 같은 이름이 이미 쓰이고 있는지만 조사하면 됩니다.



Q 1.29
어떤 이름이 이미 예약되어 있고(reserved), 어떤 이름을 안전하게 쓸 수 있는지 알 수 있는 방법이 있을까요?
Answer
Namespace management는 항상 큰 이슈가 됩니다. 이 문제는 -- 항상 확실하게 문제를 정의하는 것은 불가능하지만 -- 여러분이 선택한 이름이 시스템이 쓰고 있는 이름과 충돌나지 않게 해서 “multiply defined”라는 에러가 발생하지 않게 하거나, (좀 더 나쁜 상황이라 할 수 있는) 충돌이 나서, 기존 시스템에서 정의한 내용을 바꾸어 엉망이 될 수 있는 상황을 방지하자는 취지입니다. 또한, 지금은 충돌이 나지 않지만, 나중에 개발될, 시스템 라이브러리가 제공하는 이름과 충돌나지 않는다는 어떤 보증이 필요할 경우도 있습니다.1.4 (실제로, 새 컴파일러에서 재컴파일했을 경우에 namespace와 다른 문제들 때문에 build 실패하는 경우가 많습니다.) 그래서, ANSI/ISO C 표준은 사용자와 시스템 사이의 독립된 namespace에 대한 정의에 신경을 많이 쓰고 있습니다.

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를 따르면 됩니다:

1, 2 밑줄로 시작하는 이름을 쓰지 말기 바랍니다.
3 (위 표에 나온 모든 이름을 포함해서) 표준 매크로와 같은 이름을 쓰지 말기 바랍니다.
4 표준 라이브러리에 있는 모든 변수 및 함수 이름과, 위 표에 나온 이름을 쓰지 말기 바랍니다. (엄밀하게 말해서, 이름 비교는 첫 여섯 글자만 가지고 이루어 집니다. 질문 [*]11.27 참고)
5 표준에서 제공하는 typedef나 tag 이름을 새로 정의하는 이름에 쓰지 말기 바랍니다.

사실 위 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 변수 이름으로 흔히 쓰이는 -- 그리고 합법적인 -- 이름입니다.

References
[ANSI] § 3.1.2.1, § 3.1.2.2, § 3.1.2.3, § 4.1.2.1, § 4.13
[C89] § 6.1.2.1, § 6.1.2.2, § 6.1.2.3, § 7.1.3, § 7.13
[ANSI Rationale] § 4.1.2.1
[H&S] § 2.5 pp. 21-3, § 4.2.1 p. 67, § 4.2.4 pp. 69-70, § 4.2.7 p. 78, § 10.1 p. 284
Note
현재 C 표준은 위에서 말한 것처럼 여섯 글자 제한을 쓰지 않습니다. 자세한 것은 질문 [*]11.27을 참고하기 바랍니다.


1.11 Initialization

변수의 선언은, 물론 그 변수에 대한 초기값을 포함할 수 있습니다. 만약 초기값을 주지 않으면, 기본적인 초기화(default initialization)가 수행될 수 있습니다.



Q 1.30
초기화되지 않은 변수의 값을 미리 예상할 수 있습니까? 전역 변수는 초기화되지 않은 경우 0을 가진다고 하는데, 이것을 널 포인터나 실수 0.0으로 해석해도 괜찮은가요?

Answer
“static” 속성을 가진 초기화되지 않은 (전역 변수이거나 static으로 선언된 변수) 변수는 0의 값을 가집니다. 즉 프로그래머가 “= 0”으로 써 준 것과 똑같다는 말입니다. 따라서 널 포인터일 경우에도 올바른 값을 가집니다 (Chapter 5 참고). 마찬가지로 실수일 경우 0.0으로 해석됩니다.

“automatic” 속성을 가진 (static으로 선언되지 않은 지역 변수) 변수는 초기화되지 않을 경우 쓰레기 값을 가집니다. (이 경우에 쓰레기 값이 무엇인지는 아무도 모르며, 쓸 이유가 없습니다.)

malloc()이나 realloc()으로, 동적으로 할당된 메모리도 처음에 쓰레기 값을 가집니다. 따라서 프로그램에서 적당히 초기화시켜 주어야 합니다. calloc()으로 할당받은 메모리는 비트 단위로 0을 가지게 되지만, 이 것이 포인터나 실수 타입에서 0을 의미한다고 말할 수는 없습니다 (질문 [*]7.31과 Chapter 5 참고).

References
[K&R1] § 4.9 pp. 82-4
[K&R2] § 4.9 pp. 85-86
[C89] § 6.5.7, § 7.10.3.1, § 7.10.5.3
[H&S] § 4.2.8 pp. 72-3, § 4.6 pp. 92-3, § 4.6.2 pp. 94-5, § 4.6.3 p. 96, § 16.1 p. 386



Q 1.31
이 코드는 어떤 책에서 나온 것인데 컴파일되지 않습니다.

  int f()
  {
    char a[] = "hello, world!";
  }

Answer
아마도 쓰고 있는 컴파일러가 ANSI 이전의 컴파일러일 것이라고 추측됩니다. ANSI 이전의 컴파일러는 “automatic aggregates” (예를 들어, static이 아닌 지역 배열, 구조체, union)의 초기화를 지원하지 않습니다.

(그리고 변수 a가 어떻게 쓰일 지에 따라 다르겠지만, 이 변수를 전역 (global) 또는 static으로, 또는 포인터로 바꾸어서 해결할 수 있으며, 또는 strcpy() 등을 써서 대입시켜주는 방법도 있습니다.)

질문 [*]11.29을 참고하기 바랍니다.



Q 1.31b
이 초기화에서 잘못된 것이 무엇인가요?

  char *p = malloc(10);

제 컴파일러는 “invalid initializer”라는 에러 메시지를 출력합니다.

Answer
아마 위의 선언이 정적(static)이거나 전역(non-local) 변수일 것입니다. 초기화에서 함수 호출을 쓰는 것은 자동(automatic) 변수1.5에서만 가능합니다.



Q 1.32
다음 두 선언에 차이점이 있나요?

  char a[] = "string literal";
  char *p  = "string literal";

Answer
문자열은 크게 두 가지 방법으로 쓰일 수 있습니다. 하나는 배열의 초기값 (위에서는 char a[]에 해당)으로 쓰이는 것입니다. 이는 배열의 각 요소들인 문자들에 대입되는 초기값을 나타냅니다. 이 경우가 아니라면 문자열이 이름이 없는 정적(static)인 배열에 저장되고 -- 대개 이 배열은 읽기 전용의 속성을 가집니다 -- 수식(expression)에서 쓰일 때에는 이 배열의 첫 요소를 가리키는 포인터로서 쓰이게 됩니다. 따라서 위의 선언 중 두번째 것은 실제 문자열이 읽기전용 배열에 저장되기 때문에 포인터 p를 가지고 문자열을 수정할 수 없습니다.

(오래된 C 언어 코드의 경우, p와 같은 포인터로 문자열의 내용을 변경하려고 시도하는 경우도 있습니다. 이러한 경우를 해결하기 위해 어떤 컴파일러는 문자열을 쓰기 가능한 메모리에 저장하도록 하는 옵션을 가지고 있습니다.)

질문 [*]1.31, [*]6.1, [*]6.2, [*]6.8을 참고하기 바랍니다.

References
[K&R2] § 5.5 p. 104
[C89] § 6.1.4, § 6.5.7
[ANSI Rationale] § 3.1.4
[H&S] § 2.7.4 pp. 31-2



Q 1.33
char a[3] = "abc";가 맞는 표현인가요?
Answer
맞습니다. 질문 [*]11.22를 보기 바랍니다.



Q 1.34
함수 포인터를 선언하는 문법은 알겠는데, 초기화시키는 방법을 모르겠습니다.
Answer
다음과 같이 하면 됩니다:

  extern int func();
  int (*fp)() = func;

위와 같이 함수 이름이 수식에서 쓰인 경우, 이 이름은 -- 이 함수의 시작 주소를 나타내는 -- 포인터로 변경(decay)됩니다. 배열 이름이 단독으로 쓰이는 경우와 비슷합니다.

일반적으로 명백히 함수의 선언을 미리 적어주게 됩니다. 왜냐하면 이 경우, 자동으로 외부 함수 선언1.6을 만들어주지 않기 때문입니다 (왜냐하면 초기화에서 쓰이는 함수 이름은 함수 호출이 아니기 때문입니다).

질문 [*]1.25와 [*]4.12를 참고하기 바랍니다.



Q 1.35
union을 초기화할 수 있나요?
Answer
질문 [*]2.20을 보기 바랍니다.



Q 1.A
구조체 멤버를 초기화하려는데 “Initializer element not computable at load time”라는 에러가 발생합니다.
Answer
질문 [*]11.J를 보기 바랍니다.

Seong-Kook Shin
2018-05-28