Subsections


11. ANSI/ISO Standard C

The release of the ANSI C Standard (X3.159-1989) in 1990 (now superseded by ISO 9899:1990) and its ongoing revisions) marked a major step in C's acceptance as a stable language. The standard clarified many existing ambiguities in the language, but it introduced a few new features and definitions that are occasionally troublesome. Misunderstandings also arise when an ambiguity was resolved contrary to someone's experience or when people with pre-ANSI compilers try to use code written since the standard became widely adopted.

Standard C can be referred to in several ways. It was originally written by a committee (X3J11) under the auspices of the American National Standards Institute, so it's often called “ANSI C.” The ANSI C Standard was adopted internationally by the International Organization for Standardization, so it's sometimes called “ISO C.” ANSI eventually adopted the ISO version (superseding the original), so it's now often called “ANSI/ISO C.” Unless you're making a distinction about the wording of the original ANSI standard before ISO's modifications, there's no important difference among these terms, and it's correct to simply refer to “the C Standard” or “standard C.” (When the subject of C is implicit in the discussion, it's also common to use the word “standard” by itself, often capitalized.)

C 언어 표준은 항상 진행중입니다. 현재 C99까지 나왔으며, 앞으로도 다음 표준이 나올 수 있습니다. 여기에서는 [C99 Rationale]에 나온 C89 위원회의 원칙에 대해 몇가지 간단히 소개하겠습니다:

좀 더 자세한 것은 [C99 Rationale]를 참고하기 바랍니다.

11.1 The Standard



Q 11.1
“ANSI C 표준”이란 무엇인가요?

Answer
1983년, 미국 규격 협회(American National Standards Institute, ANSI)는 C 언어의 표준을 제정하기 위해 X3J11이라는 위원회를 열었습니다. 매우 긴 기간동안 토론한 끝에 이 위원회의 보고서는 1989년 12월 14일 ANSX3.159-1989라는 이름으로 비준받아서, 1990년에 출판되었습니다. 대부분의 내용은 기존의 C 언어에서 가져온 것이며, 몇몇은 C++에서 (대부분 함수 prototype에 대한 것) 가져온 것입니다. 그리고 (논쟁의 여지가 있던 3중 음자(trigraph) 문자 시퀀스를 포함한) 다국적 문자 세트를 지원하는 기능도 포함시켰습니다. ANSI C 표준은 C run-time 라이브러리도 표준화시켰습니다.

그 후에 국제 표준 기구인 ISO11.1는 미국 표준인 X3.159를 국제 표준인 ISO/IEC 9899:1990으로 바꿔서 국제 표준으로 만들었습니다. 이 표준에서는 ANSI의 표준을 정정하고 보충한 것이 대부분이었기 때문에 흔히 `ANSI/ISO 9899-1990' [1992] 라고 부릅니다.

1994년 `Technical Corrigendum 1(TC1)'은 표준에서 약 40 가지를 수정하였습니다. 대부분 수정은 부분적으로 명확한 설명이 필요한 것에 보충 설명을 단 것입니다. 그리고 `Normative Addendum 1(NA1)'은 약 50 페이지 분량의 새로운 내용을 추가했으며, 대부분이 국제화(internationalization11.2)에 관한 함수 설명입니다. 1995년 TC2는 몇가지 정정 사항을 추가했습니다.

이 글을 쓸 때, 표준의 완전한 개정판은 이제 막바지 작업에 들어 갔습니다. 새로운 표준은 현재 “[C9X]”라고 이름이 붙었고, 1999년 말에 완성될 거라는 뜻을 나타냅니다. (이 글의 많은 부분도 새로운 [C9X]를 반영하려고 수정되었습니다.)

오리지널 ANSI 표준은 많은 부분에서 결정한 부분에 대한 설명과, 작은 문제들에 대한 논의를 포함한 “[ANSI Rationale] (이론적 해석)”을 포함하고 있습니다. 몇 가지는 이 글에 이미 포함되었습니다. ([ANSI Rationale] 자체는 ANSI 표준 X3.159-1989에 포함된 부분이 아니지만 정보 제공 목적으로만 제공되는 것이며, ISO 표준에 포함되는 내용도 아닙니다. [C9X] 용으로 새 판이 준비되고 있는 상황입니다.)

Note
[H&S2002] 소개 부분에 나온 내용을 인용 및 요약하면 크게, C 언어는 네번의 변화를 거쳤습니다.

첫째, Brian Kernighan씨와 Dennis Ritchie씨가 1978년에 쓴 The C Programming Language에서 소개한 C 언어입니다. 보통 “K&R”이라고 부르며, 1980년대에 쓰였던 모든 C 언어를 “traditional C”라고 합니다.

둘째, 표준을 정해 놓는 것이, C 언어가 널리 퍼지게 하는데 도움이 될 것이라는 믿음에서, 앞에서 소개한 ANSI 표준이 만들어졌습니다. (지금은 NCITS J11이 된) X3J11 위원회는 1989년에 C 언어 표준과 런타임 라이브러리를 “American National Standard X3.159-1989”로 공식화 했으며, 보통 “ANSI C”라고 부릅니다. 이는 곧 국제 표준으로 받아들여져, 국제 표준인 ISO/IEC 9899:1990으로 등록되었습니다. ANSI C와 이 국제 C 언어 표준과 차이점은 거의 없습니다. 그리고 이 국제 표준을 보통 “Standard C”라고 부릅니다. 그러나 이 표준은 또 변했기 때문에, 이 것을 “Standard C (1989)”로 부르거나, 줄여서 “C89”라고 합니다.

셋째, WG14 그룹은 C89에 대한 두 문서를 만들었는데 하나는 `Technical Corrigenda'이며 (버그 수정 문서), 또 하나는 `Amendment (확장)'입니다. 이들이 반영된 표준은, “C89 with Amendment 1” 또는 “C95”라고 부릅니다.

넷째, WG14는 계속 작업을 거듭했고, 그 추가 및 확장 사항들이 다시 반영되어, 1999년에 표준으로 제정되었습니다. 이는 “ISO/IEC 9899:1999” 또는, 줄여서 “C99”라고 부르며, 이 것이 가장 최신의 표준입니다.

참고로, Bjarne Stroustrup씨가 1980년 초반에 디자인한 C++은 현재 가장 인기있는 언어 중의 하나이며, 이 언어는 C 언어를 기초로 만들어 졌습니다. 이 C++ 언어도 역시 표준화가 (1998년) 이루어졌으며, 그 결과, ISO/IEC 14882:1998 또는 간단히 “Standard C++”이라고 부릅니다.

표준화가 이루어진 년도를 주의깊게 보았다면, C 언어 최신 표준이 C++ 언어의 표준이 제정된 다음에 개정된 것을 알 수 있습니다. 물론 단순히 이 사실 만으로 알 수 있는 것은 아니지만, 사실 표준 C++ 언어가, 표준 C 언어의 모든 점을 포함하고 있지 않습니다. 따라서 정확히 말해서, “C 언어는 C++ 언어의 부분집합이다” 또는 “C++ 언어는 C 언어를 포함한다”라는 말은 모두 틀린 말입니다. 물론 C++ 언어가 C 언어의 대부분을 포함하고 있지만, 전부는 아닙니다.

따라서, C 언어 표준을 완벽하게 지원하도록 만든 코드가 C++ 언어 표준에 맞지 않을 가능성이 있습니다. C 언어와 C++ 언어에서 완벽하게 동작하는 코드를 (어떤 사람들은) “Clean C”라고 부릅니다.

앞 답변에서 “C9X”라고 부르는 것은, 1990년대에 아직, 새 C 표준이 정해지지 않았을 때, (정확한 제정 연도를 몰랐기 때문에) 붙여진 이름입니다. 이제는 1999년에 제정된 것을 알기 때문에 “C99”라고 부르는 것이 올바른 표현입니다.

References
[H&S2002] § 1.1 pp. 4-5
[C99 Rationale] § 0



Q 11.2
표준 문서를 어디서 얻을 수 있죠?
Answer
미국이라면 다음 주소에서 사본을 신청할 수 있습니다:
  American National Standards Institute
  11 W.  42nd St., 13th floor
  New York, NY  10036  USA
  (+1) 212 642 4900
또는 다음 주소도 가능합니다:
  Global Engineering Documents
  15 Inverness Way E
  Englewood, CO  80112  USA
  (+1) 303 397 2715
  (800) 854 7179  (U.S.  & Canada)
다른 나라들에서는 제네바(Geneva)에 있는 ISO에 주문하거나 각 국의 표준 위원회에 연락하시기 바랍니다:
  ISO Sales
  Case Postale 56
  CH-1211 Geneve 20
  Switzerland
(또는 http://www.iso.ch를 방문하거나, comp.std.internat FAQ 리스트에서 Standards.Faq를 참고하기 바랍니다).

저자가 마지막으로 검사했을 때, ANSI에서 주문하려면 130.00$가 필요했으며, GLobal에서 주문할 때에는 400.50$가 필요했습니다. ([ANSI Rationale]을 포함한) 오리지널 X3.159는 ANSI에서 205.00$, Global에서는 162.50$가 필요했습니다. ANSI에서는 표준 문서를 판매한 수익금으로 운영하기 때문에 전자 출판 형식으로는 제공해주지 않습니다.

미국이라면 ([ANSI Rationale]를 포함한) 오리지널 ANSI X3.159의 사본을 “FIPS PUIB 160”으로 다음 주소에서 주문할 수 있을 것입니다:

  National Technical Information Service (NTIS)
  U.S.  Department of Commerce
  Springfield, VA  22161
  703 487 4650
Herbert Schildt씨가 해설한(annotated) “Annotated ANSI C Standard”는 ANSI가 아닌 ISO 9899를 설명하고 있습니다; Osborne/McGraw-Hill에서 출판되었으며, ISBN 0-07-881952-0이며, 대략 $40 선에서 판매되고 있습니다. 표준과 이 책의 가격 차이는 대부분 저자가 단 해설(annotation) 가격입니다: 그러나 많은 에러와 너무 많은 생략으로 평판이 좋지 않습니다. net의 대부분 사람들은 아예 이 책을 무시합니다. Clive Feather씨는 이 책에 대한 서평을 썼고 아래의 URL에서 볼 수 있습니다:
  http://www.lysator.liu.se/c/schildt.html

[ANSI Rationale] 문서는 (완전한 표준은 아님) anonymous FTP로 ftp.uu.net의 (질문 [*]18.16 참고) doc/standards/ansi/X3.159-1989 디렉토리에서 얻을 수 있습니다. 또 http://www.lysator.liu.se/c/rat/title.html에서 볼 수도 있습니다. Silicon Press에서 출판되기도 했습니다. ISBN 0-929306-07-4입니다.

ISO/IEC C9X의 진행판은 JTC1/SC22/WG14 사이트인 아래에서 얻을 수 있습니다:

  http://www.dkuung.dk/JTC1/SC22/WG14/
질문 [*]11.2b를 참고하기 바랍니다.
Note
최신 C 표준인 C99의 PDF 버전 (ISO web site에서) 공식 가격은 (2005년 1월 24일자로) 340 CHF입니다. 이 날짜로, 원화 가격은 약 30만원입니다. ANSI를 비롯, 다른 web site에서도 판매하고 있습니다. 구입할 때, 무료로 배포된는 Corrigenda (버그 수정) 문서도 꼭 함께 받으시기 바랍니다. 현재 다음 두 가지가 나와 있습니다: 검색 엔진을 쓸 때, “9899:1999”로 검색하시면 빨리 찾을 수 있습니다.



Q 11.2b
개정된 표준에 대한 정보는 어디에서 얻을 수 있나요?
Answer
([C9X] 진행판을 포함한) 관련된 정보는 다음 웹 사이트에서 찾을 수 있습니다:
  http://www.lysator.liu.se/c/index.html
  http://www.dkuug.dk/JTC1/SC22/WG14/
  http://www.dmk.com/

Note
  http://www.dkuug.dk/JTC1/SC22/WG14/
위 사이트의 새 주소는 다음과 같습니다:
  http://www.open-std.org/jtc1/sc22/wg14/

11.2 Function Prototypes

The most significant introduction in ANSI C is the function prototype (borrowed from C++), which allows a function's argument types to be declared. To preserve backward compatibility, nonprototype declarations are still acceptable, which makes the rules for prototypes somewhat more complicated.



Q 11.3
제 ANSI 컴파일러는 다음과 같은 코드를 봤을 때, 함수가 일치하지 않는다고 에러를 발생합니다:

  extern int func(float);

  int func(x)
  float x;
  { ...

Answer
그 이유는 새 (스타일) 프로토타입(prototype) 선언인 “extern int func(float)”을 오래된 (스타일) 정의인 “int func(x) float x;”와 섞어 썼기 때문에 발생합니다. 보통 이 두 스타일을 섞어 쓰는 것이 가능하지만(질문 [*]11.4 참고) 이 경우에는 안됩니다.

Traditional C (프로토 타입과 가변 인자 리스트를 제공하지 않는 ANSI C; 질문 [*]15.2 참고) 언어에서는 함수에 전달되는 어떤 인자들을 “확장(widen)”시킵니다. 즉 floatdouble로, charshort intint로 확장시킵니다. (구 스타일로 정의한 함수에서는, 이렇게 확장되어 전달된 인자가, 함수의 몸체 부분에 들어갈 때, 다시 원래의 크기로 변환됩니다) 따라서 위의 오래된 스타일로 만든 정의는 사실상 func 함수를 double 타입 인자를 받도록 만든 것입니다. (물론 함수 내부에서 이 인자는 다시 float 타입으로 바뀝니다) 이 문제는 함수 정의에서 새 스타일의 문법을 써서 고칠 수 있습니다:

  int func(float x) { ...  }

또는 새 형식의 프로토타입 선언을 구 형식의 정의와 일치하도록 다음과 같이 만들어 주면 됩니다:

  extern int func(double);
(이 경우, 가능하다면 구 형식의 정의에서 double을 쓰도록 바꿔주는 것이 더 깨끗합니다.11.3)

“narrow” 효과를 피하기 위해, “narrow” 효과를 발생하는 타입들을 (예를 들어 char, short int, float 등) 함수 인자나 리턴 타입으로 쓰지 않는 편이 안전할 수 있습니다.

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

References
[K&R1] § A7.1 p. 186
[K&R2] § A7.3.2 p. 202
[C89] § 6.3.2.2, § 6.5.4.3
[ANSI Rationale] § 3.3.2.2, § 3.5.4.3
[H&S] § 9.2 pp. 265-7, § 9.4 pp. 272-3

Note
함수 호출시, prototype이 확인되지 않다면, 컴파일러는 모든 인자에 대해 default argument promotion을 실시합니다. 즉, int보다 작은 정수 타입들은 모두 int로, floatdobule로 변환하며, 나머지 타입들은 그대로 전달됩니다. 만약, 인자(argument)가 파라메터와 갯수가 다를 경우, undefined behavior가 발생하며, 만약 나중에 함수 prototype이 발견되었는데, 이 함수 prototype이 가변 인자를 받는 것으로 선언되어 있거나, 파라메터들의 타입이, default argument promotion 후의 인자 타입과 서로 다를때에도 undefined behavior가 발생합니다. (마지막 문장은 함수 인자/파라메터 뿐만 아니라, 함수의 리턴 타입에도 적용됩니다.)

이 규칙은 생각보다 좀 더 까다롭습니다. 아래 Reference를 꼭 읽어 보기 바랍니다.

[C99] § 6.5.2.2, § 6.7.5.3
[H&S2002] § 9.2



Q 11.4
함수 구문에서 오래된 형식과 새 형식을 섞어 쓸 수 있나요?
Answer
섞어 쓸 수 있긴 하지만 매우 주의해야 합니다. (질문 [*]11.3을 꼭 보시기 바랍니다). 현재에는 prototype 형식이 선언과 정의에 모두 사용되고 있습니다. (오래된 형식은 쓸모 없이 되어가고 있기 때문에, 언젠가 공식적으로 제거될 것입니다.)

References
[ANSI] § 3.7.1, § 3.9.5
[C89] § 6.7.1, § 6.9.5
[H&S] § 9.2.2 pp. 265-7, § 9.2.5 pp. 269-70

[C99] § 6.5.2.2 § 6.9.1

Note
일반적으로, 오래된 형식과 섞어 쓰기 위해서는 다음 두 가지 규칙을 지켜야 합니다:

References
[H&S2002] § 9.2.2, pp. 291-292 § 9.2.5 pp. 294-295
GNU Coding Standard § 3.4



Q 11.5
컴파일러가 다음 선언을 만나면:
  extern int f(struct x *p);
“struct x introduced in prototype scope”라는 이상한 경고를 발생시킵니다.
Answer
C 언어의 블럭 스코프(scope) 규칙에 따르면 함수의 프로토타입에만 선언된 structure는 같은 소스의 다른 구조체와 호환성이 없습니다 이 structure와 tag는 함수의 프로토타입 선언이 끝날 때 스코프를 벗어납니다; 질문 [*]1.29를 참고하기 바랍니다.

함수 prototype 앞에 structure 선언을 두어 이 문제를 해결할 수 있습니다. (보통, prototype과 structure 선언은 같은 헤더 파일에 존재하며, 이렇기 때문에 한쪽이 다른 한쪽을 참조할 수 있습니다.) 만약 prototype에 아직 선언되지 않은 structure를 꼭 쓸 필요가 있다면, prototype 앞에 다음과 같이 써 줍니다:

  struct x;
아무것도 아닌 것 같은 이 선언은 struct x 이렇게 하면, 이 구조체의 (incomplete) 선언이 파일 스코프를 가지게 되어, 이후에 나올 선언에서 struct x를 사용할 때, 같은 struct x를 가리키도록 할 수 있습니다.
References
[ANSI] § 3.1.2.1, § 3.1.2.6, § 3.5.2.3
[C89] § 6.1.2.1, § 6.1.2.6, § 6.5.2.3.



Q 11.6
다음 코드가 이상하게 동작합니다:
  printf("%d", n);
위에서 nlong int 타입입니다. ANSI 함수 prototype이 이런식으로 인자와 파라메터 타입이 서로 일치하지 않을때, (conversion 등으로) 보호해 주지 않나요?
Answer
질문 [*]15.3을 보기 바랍니다.



Q 11.7
printf를 쓰기 전에 <stdio.h>를 include해야 한다고 들었습니다. 왜 그런가요?
Answer
질문 [*]15.1을 참고하기 바랍니다.

11.3 The const Qualifier

Another introduction from C++ is an additional dimension to the type system: type qualifiers. Type qualifiers can modify pointer types in several ways (affecting either the pointer or the object pointed to), so qualified pointer declarations can be tricky. (The questions in this section refer to const, but most of the issues apply to the other qualifiers, volatile, as well.)



Q 11.8
배열의 크기를 지정할 때, 다음과 같이 상수 값을 쓰면 안되는 이유가 뭔가요?
  const int n = 5;
  int a[n];
Answer
const qualifier는 “읽기 전용”인 것을 의미합니다; 즉 지정한 오브젝트는 실행할 때 (일반적으로) 변경할 수 없는 run-time 오브젝트입니다. 따라서 이러한 `const' 오브젝트는 상수 수식(constant expression)이 아니기 때문에, 배열의 크기 지정, case label등에 쓰일 수 없습니다. (이 부분에서 C 언어와 C++이 다릅니다.) 정말로 compile-time 상수가 필요하다면 #define으로 (또는 enum으로) 상수를 정의하기 바랍니다.
References
[ANSI] § 3.4
[C89] § 6.4
[H&S] § 7.11.2,7.11.3 pp. 226-7



Q 11.9
`const char *p'와 `char * const p'의 차이는 무엇인가요?

Answer
`const char *p'는 (`char const *p'라고 쓸 수 있음) 상수 문자에 대한 포인터를 선언한 것입니다 (가리키는 문자를 바꿀 수 없는 포인터); `char * const p'는 문자에 대한 상수 포인터를 선언한 것입니다 (문자를 변경할 수는 있지만 포인터를 변경할 수는 없습니다).

해설을 잘 음미해보시기 바랍니다; 질문 [*]1.21도 참고하시기 바랍니다.

References
[ANSI] § 3.5.4.1 examples
[C89] § 6.5.4.1
[ANSI Rationale] § 3.5.4.1
[H&S] § 4.4.4 p. 81
Note
아래 예에서, const_pointer는 포인터가 상수인 것을 나타냅니다. 그리고, pointer_to_const는 상수를 가리키는 포인터를 나타냅니다:
  int * const const_pointer;
  const int *pointer_to_const;
즉, const_pointer는 포인터가 가리키는 대상을 변경할 수 있으나, 다른 대상을 가리키도록 할 수는 없습니다. 그리고, pointer_to_const는 다른 대상을 가리키도록 할 수 있지만, 포인터가 가리키는 대상을 변경할 수 없습니다.
References
[H&S2002] § 4.4.4 pp. 89-91



Q 11.10
const char **를 인자로 받는 함수에 char **를 전달하면 안되나요?
Answer
(어떤 T 타입에 대해) const T가 와야 하는 곳에, T를 가리키는 포인터를 쓸 수 있습니다. 이 규칙은 포인터 타입이 qualified 부분이 서로 약간 다를 경우에도 쓸 수 있다는 것을 의미하며, 여기에는 한 가지 예외 사항이 있는데, 이 규칙은 계속 재귀적으로(recursively) 적용되지 않고, 단지 top-level에만 적용 된다입니다. (즉, const char **는 const char에 대한 포인터를 가리키는 포인터이므로, 이 예외 사항에 해당되지 않습니다.)

const char **가 필요한 곳에 char **를 쓸 수 없는 이유는 조금 불명확합니다. const가 붙어 있기 때문에, 컴파일러는 여러분이 const 값을 변경하지 않는다고 한 약속을 지킬 수 있도록 도와주려 합니다. 그렇기 때문에 const char *가 필요한 곳에 char * 타입을 쓸 수 있으며, 반대 경우에는 쓸 수 없습니다: 간단한 포인터 타입에 const를 붙이는 것은, 프로그램이 매우 안전하게 동작할 수 있도록 도와줍니다. 그러나, 반대로 const를 제거하는 것은 때때로 위험할 수 있습니다. 아래처럼 조금 복잡한 대입 연산들을 생각해보기 바랍니다:

  const char c = 'x';	/* 1 */
  char *p1;               /* 2 */
  const char **p2 = &p1;  /* 3 */
  *p2 = &c;               /* 4 */
  *p1 = 'X';              /* 5 */
세번째 줄에서 우리는 const char **가 필요한 곳에, char **를 대입했습니다. (컴파일러는 이 부분에서 경고를 발생시킵니다.) 네번째 줄에서, 우리는 const char *const char *가 필요한 곳에 대입했습니다; 이 것은 아무런 문제가 없습니다. 다섯번째 줄에서 우리는 char * 포인터가 가리키는 것을 변경시켰습니다. 이 것은 아무런 문제가 없어보이지만, p1은 사실 c를 가리키고 있고, 이 것은 const이기 때문에 문제가 됩니다. 다시 잘 분석하면, 이 것은 네번째 줄에서 *p2가 실제로 p1을 가리키고 있었던 것이 문제입니다. *p2p1을 가리키도록 만든 것은 바로 세번째 줄에서 했던 것이고, 이 것은 허용되지 않는 대입 연산이며, 이런 이유 때문에, 허용되지 않습니다.11.4

(앞 예제 세번째 줄에서처럼) const char **char **를 대입하는 것은 그 자체가 위험한 것은 아닙니다. 그러나 위에서 p2가 약속하고 있는 것, 즉 궁극적으로 가리키고 있는 값을 변경하지 않는다를 깨뜨릴 수 있는 실마리를 제공합니다. 그래도 이러한 연산이 필요하다면, 다시 말해, 가장 최상위 수준이 아닌 곳에서 qualifier가 서로 달라서 대입이 되지 않는 것을 대입시키려면, 직접 캐스트해서 (즉, 이 경우에는 (const char) **로) 쓸 수 있습니다.

References
[ANSI] § 3.1.2.6, § 3.3.16.1, § 3.5.3
[C89] § 6.1.2.6, § 6.3.16.1, § 6.5.3
[H&S] § 7.9.1 pp. 221-2



Q 11.11
다음과 같은 선언에서:
  typedef char *charp;
  const charp p;
p가 가리키는 charconst가 되지 않고, p 자체가 const가 되는 것일까요?
Answer
Typedef로 치환한 것은 순수하게 textual 치환이 아닙니다. (이 것은 typedef를 쓰는 한가지 장점이기도 합니다; 질문 [*]1.13 참고) 다음과 같은 선언에서:
  const charp p;
const int iiconst로 만드는 것과 같은 원리에서, pconst가 됩니다. p에 대한 선언은, 포인터가 관련이 되어있는지 typedef 안까지 쫓아가서 확인하지 않습니다.
References
[H&S] § 4.4.4 pp. 81-2
Note
아래와 같은 선언이 있다고 가정하고 (질문 [*]11.9 참고):
  int * const const_pointer;
위에서 const_pointertypedef를 써서 다음과 같이 쓸 수 있습니다:
  typedef int *int_pointer;
  const int_pointer const_pointer;
이 때, const_pointer는 상수 int를 가리키는 포인터처럼 보이지만, 실제로는 (상수가 아닌) int를 가리키는 const 포인터입니다. 또, 타입 specifier와 타입 qualifier의 순서는 중요하지 않기 때문에 (순서가 바뀔 수 있기 때문에), 다음과 같이 쓸 수도 있습니다:
  int_pointer const const_pointer;

References
[H&S2002] § 4.4.4 p. 90

11.4 Using main()

Although every C program must by definition supply a function named main, the declaration of main is unique because it has two acceptable argument lists, and the rest of the declaration (in particular, the return type) is dictated by a factor outside of the program's control, namely, the startup code that will actually call main.



Q 11.12a
main()의 정확한 선언 방법을 알고 싶습니다.
Answer
main()의 선언은 다음 중에서 골라 써야 합니다:
  int main(void);
  int main(int argc, char *argv[]);
argv를, char **argv로 선언할 수도 있습니다. (질문 [*]6.4를 참고하기 바랍니다.) (물론 이때 `argv'와 `argc'라는 이름은 얼마든지 바꿀 수 있습니다.) 또 오래된 스타일을 써서 다음과 같이 할 수도 있습니다:
  int main()

  int main(argc, argv)
  int argc;
  char **argv;

질문 [*]11.12b부터 [*]11.15까지 참고하기 바랍니다.

References
[C89] § 5.1.2.2.1, § G.5.1
[H&S] § 20.1 p. 416
[CT&P] § 3.10 pp. 50-51

[C99] § 5.1.2.2.1, § J.1.1, § J.2.1, § J.3.2.1, § J.5.1
[H&S2002] § 9.9, § 9.11.4, § 16.5

Note
원래 C 언어에서, 함수의 정의나 변수 선언에서 타입을 (type specifier) 생략했을 때, 디폴트로 int 타입이 됩니다. 그러나 이런 식으로 생략하는 것은 그 동안 나쁜 프로그래밍 스타일로 여겨졌으며, 현재 C99 표준에서는 에러입니다.

[H&S2002] § 4.4.1



Q 11.12b
“main returns no value”라는 경고를 피하기 위해, main()void 타입으로 선언해도 괜찮을까요?
Answer
안됩니다. main()은 반드시 int 타입을 리턴하도록 선언되어야 하며, 인자는 0개 또는 2개이어야 합니다. 종료할 때, exit()를 썼는데도 계속 이러한 경고가 발생한다면, 마지막에 쓸데없는 여분의 (redundant) 리턴 문장을 써 주어 경고가 발생하는 것을 막을 수 있습니다. (가능하다면 컴파일러가 제공하는 “not reached”를 의미하는 지시어(directive)를 써 주어도 됩니다).

단순히 경고를 없애려고 함수를 void 타입으로 선언하는 것은 매우 좋지 않습니다; 왜냐하면, 내부적인 함수 호출/리턴 시컨스가 함수를 호출하는 쪽과 (main()의 경우, C run-time startup code) 서로 다를 수 있기 때문입니다.

(Note that this discussion of main() pertains only to "hosted" implementations; none of it applies to "freestanding" implementations, which may not even have main(). However, freestanding implementations are comparatively rare, and if you're using one, you probably know it. If you've never heard of the distinction, you're probably using a hosted implementation, and the above rules apply.)

References
[C89] § 5.1.2.2.1, § G.5.1
[H&S] § 20.1 p. 416
[CT&P] § 3.10 pp. 50-51



Q 11.13
main의 세 번째 인자인 envp가 있다고 들었습니다.
Answer
세 번째 인자는 자주 쓰이기는 하지만 표준 인자는 아닙니다. 이는 환경(environment) 변수에 접근하기 위한 것으로, 이 목적으로 쓰이는 표준 함수 getenv()가 제공되고, 전역 변수로 environ이 제공되기 때문에, 이 비표준 세번째 인자를 쓸 이유가 없습니다 (하지만, 전역 변수 environ도 비표준이기는 마찬가지입니다).

References
[C89] § G.5.1
[H&S] § 20.1 pp. 416-7
Note
전역 변수 environ은 POSIX.1 표준입니다. (물론 POSIX.1은 C 표준과 다르기 때문에, C 언어 관점에서는 비표준이라 할 수 있습니다.)
  #include <unistd.h>

  extern char **environ;
원래, 전통적으로 UNIX 시스템에서는 main이 세번째 인자를 받을 수 있게 선언할 수 있었습니다. 즉 다음과 같습니다:
  int main(int argc, char *argv[], char *envp[]);
그러나, 이 것은 (C 표준이 아닌 것은 물론) POSIX.1 표준도 아닙니다. POSIX.1을 따르면, ISO C 표준을 존중하기 위해, main의 세번째 인자인 envp와 같은 방식으로 동작하는, 전역 변수 environ을 써야 한다고 씌여 있습니다.

한 환경 변수의 값을 얻기 위해서는, 표준 함수인 getenv를 쓰는 것을 권장합니다. 하지만, 모든 환경 변수의 이름과 값을 얻기 위한 표준 방법은 존재하지 않습니다. 모든 환경 변수의 이름과 값을 꼭 얻어야 겠다면, (C 표준은 아니지만) POSIX.1 표준인 전역 변수 environ을 쓸 것을 권장합니다.

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

References
[H&S2002] § 9.9 p. 304
[SUS] environ



Q 11.14
main()void 타입으로 선언한다 하더라도, exit() 함수를 써서 종료한다면 문제될 게 전혀 없지 않나요? 게다가 제가 쓰고 있는 운영 체제는 프로그램의 종료/리턴 코드를 아예 무시한답니다.
Answer
main()의 리턴 값이 쓰이냐, 쓰이지 않느냐는 중요한 문제가 아닙니다; 문제는 main()void 타입으로 선언함으로 인하여, main()을 호출하는 부분이 (런-타임 시작(startup) 코드) main()을 제대로 호출하지 못할 수 있다는 것입니다 (이는 calling convension 문제입니다; 질문 [*]11.12b를 참고하기 바랍니다).

Borland C++ 4.5에서 void main()을 썼을 때, 프로그램이 망가질 수 있다는 것이 이미 보고되었습니다. 그리고 어떤 컴파일러들은 (DEC C V4.1과 gcc) mainvoid 타입으로 선언했을 때, 경고를 발생합니다.

여러분의 운영 체제가 종료 상태(exit status)를 무시할 수도 있고, void main()이 동작할 수도 있지만, 이는 이식성이 없을 뿐만 아니라, 올바른 것도 아닙니다.

여러 시스템에서 void main()으로 써도 동작한다는 것은 사실입니다. 만약 이식성을 전혀 고려할 생각이 없고, 이 방식이 더 편하다고 생각하면, 아무도 말릴 사람은 없습니다.

Note
C 표준은, 질문 [*]11.12a에 나온 꼴의 main 함수를 지원할 것을, 컴파일러에게 요구합니다. 또한 컴파일러는 C 표준에 나와 있지 않는 형태의 main을 제공하는 것에는 관여하지 않습니다. 그러나, 다른 비표준 형태를 쓰는 것은 (표준 관점에서 봤을 때) `undefined behavior'를 낳습니다.



Q 11.15
제가 보고 있는 책 “C Programming for the Compleat Idiot”에서는 항상 void main()을 사용합니다.
Answer
아마도 그 책의 저자는 자신도 그 범주(complete idiot)에 계산한 모양입니다. 많은 책들이 void main()을 쓰고 있지만 이는 잘못된 것입니다.



Q 11.16
main()에서 어떤 값을 리턴하는 것과 exit()를 쓰는 것과 완전히 같나요?
Answer
그렇다고, 또는 그렇지 않다고 할 수 있습니다. C 표준에서는 완전히 같다고 언급하고 있지만, 어떤 오래된 시스템에서는 (특히 C 표준을 준수하지 않는), (리턴하는 것과 exit를 쓰는 것 중) 어느 한 쪽이 정상적으로 동작하지 않을 수 있습니다.

main에 local 데이터가 `cleanup' 과정에서 필요할 경우, main에서 리턴하는 방법은 제대로 동작하지 않을 수 있습니다; 질문 [*]16.4를 참고하기 바랍니다. 그리고 아주 오래된 (표준을 지원하지 않는) 몇몇 시스템에서는 두 가지 형식 중 하나가 제대로 동작하지 않을 수 있습니다.

(마지막으로, 이 두가지 형태는 main()을 재귀적으로 호출할 경우, 다른 코드를 생성합니다.)

References
[K&R2] § 7.6 pp. 163-4
[C89] § 5.1.2.2.3

Note
C++ 언어와는 달리, C 언어 표준에서는 main()이 재귀적으로 (즉, recursive하게, main이 다시 main을 부르는 경우) 호출되는 것을 막지 않았습니다.11.5 즉, 원한다면 main에서 다시 main을 부를 수 있습니다. 그러나, IOCCC에 출품할 것이 아니라면, 그런 코드를 만들 이유가 없습니다. (IOCCC에 대한 것은 질문 [*]20.36을 참고하기 바랍니다.)

C99 표준에 따르면, (표준에 부합하는 형태, 즉 int를 리턴하는) main에서 어떤 값을 return 하는 것은, exit 함수를 그 값으로 부르는 것과 같다고 씌여 있습니다. 그리고 이 효과가 일어나는 것은, 프로그램 시작점으로 쓰인 main에서만 일어납니다. 즉, 재귀적으로 불려진 main에서 return한다고 exit가 자동으로 호출되지 않습니다.

또한 exitreturn 문장 없이 main이 끝나버리면, return 0을 쓴 것과 같은 효과를 얻을 수 있다고 씌여 있습니다. (이 것은 C99에 새로 추가된 내용입니다. 그 전 표준인 ANSI, C89 등에서는 해당되지 않습니다.)

return하는 것과 exit를 부르는 것에 미세한 차이가 있을 수 있는데, 만약 atexit로 exit handler를 등록시켜 놓았고, 그 handler가 main에서 만든 어떤 automatic (static이 아닌) 변수에 접근한다면, return하는 것은 이 변수에 접근할 수 없습니다. 즉, return을 써서 자동으로 exit를 부르게 하는 것은, 이미 main 함수의 블럭을 벗어났기 때문에, main에서 선언한 automatic 변수는 존재하지 않습니다.

References
[C99] § 5.1.2.2.3, § 7.19.3.5

11.5 Preprocessor Features

ANSI C introduces a few new features into the C preprocessor, including the “stringizing” and “token pasting” operators and the #pragma directive.



Q 11.17
ANSI “stringizing” 전처리기 연산자인 #를 써서 심볼릭 상수의 값을 문자열에 집어 넣으려고 합니다, 그런데, 그 결과, 상수의 값이 들어가는 대신, 상수의 이름이 들어가는군요.
Answer
#의 정의에 따르면, 이 것은 매크로 인자를 (인자가 또 다른 매크로 이름이더라도 더 이상 확장하지 않고) 바로 문자열로 만듭니다. 매크로가 원래 지닌 뜻으로 확장되길 원한다면 다음과 갈이 두 단계를 거쳐서 쓸 수 있습니다:
  #define Str(x) #x
  #define Xstr(x) Str(x)
  #define OP plus
  char *opname = Xstr(OP);
이 코드는 opname을 “OP”로 설정하지 않고, “plus”로 설정합니다. (즉, Xstr() 매크로가 인자를 확장하고 Str() 매크로가 문자열로 만듭니다.)

비슷한 상황이 “token-pasting” 연산자인 ##를 쓸 때, 두 매크로의 값을 연결하려 할 때 발생할 수 있습니다.

###는 일반 소스 코드에서는 쓰일 수 없으며, 다만 매크로 정의 부분에서만 쓸 수 있는 연산인 것을 꼭 기억하기 바랍니다.

References
[ANSI] § 3.8.3.2, § 3.8.3.5 example
[C89] § 6.8.3.2, § 6.8.3.5.



Q 11.18
메시지 “warning: macro replacement within a string literal”은 무슨 뜻이죠?

Answer
: ANSI 이전의 어떤 컴파일러/전처리기는 매크로 정의를 다음과 같이 정의할 경우:

  #define TRACE(var, fmt) printf("TRACE: var = fmt\n", var)

다음과 같은 식으로 호출하게 되면:

  TRACE(i, %d);

다음과 같이 확장하게 됩니다:

  printf("TRACE: i = %d\n", i);

즉, 매크로 인자로 나온 이름이 문자열 안에 있는 경우라도 확장시켜 버립니다. (물론 이러한 버그가 위와 같이 유용하게 쓰일 수도 있지만, 이 것은 대개 초창기 컴파일러를 만들 때 잘 못 만든 것입니다.)

이러한 식의 매크로 확장은 K&R에 언급된 것도 아니며, 표준 C 언어에서 언급된 것도 아닙니다. (매우 위험한 방법이며, 코드가 어려워집니다. 질문 [*]10.22를 참고 바랍니다.) 매크로 인자 자체가 문자열이 되기를 원한다면 전처리기 연산자인 #를 쓰거나, 문자열 연결 (concatenation) 기능을 쓰면 됩니다 (이는 ANSI 표준의 새로운 기능입니다.):

  #define TRACE(var, fmt) \
    printf("TRACE: " #var " = " #fmt "\n", var)
질문 [*]11.17을 참고하기 바랍니다.

References
[H&S] § 3.3.8 p. 51



Q 11.19
#ifdef를 써서 컴파일하지 말라고 한 곳에서 매우 이상한 구문(syntax) 에러가 납니다.
Answer
ANSI C에서, #if, #ifdef, #ifndef에 쓴 텍스트는 전처리기가 처리할 수 있는 유효한 것이어야 (valid preprocssing token) 합니다. 즉, C 언어에서처럼 "'는 각각이 쌍을 이루어서 나와야 하며, 이처럼 둘러싼 문자열의 안에 newline 문자가 나와서는 안되며, 주석이 끝나지 않고 열려 있어서도 안됩니다. 서로 다른 따옴표가 엇갈려서 있어도 안됩니다. (특히, 영어의 생략형(contracted word)에 쓰이는, 역 따옴표(apostophe, `)는 문자 상수의 시작처럼 보일 수 있다는 것에 주의하기 바랍니다.) 따라서 긴 주석이나 pseudo code를 쓰는 것이 목적이라면 #ifdef를 써서 빼라고 지정을 했더라도, 공식적(offical)인 주석(comment)인 /* ... */을 써야 합니다.
References
[C89] § 5.1.1.2, § 6.1
[H&S] § 3.2 p. 40
Note
C99 표준에 따라서, C++ 언어에서 쓰는 것처럼 //로 시작하는 주석도 쓸 수 있습니다.



Q 11.20
#pragma는 어디에 쓰나요?
Answer
#pragma는 모든 종류의 (이식성이 떨어지는) 모든 구현 방법에 따른 기능을 제어하고, 확장 기능을 제공합니다; 여기에는 소스 리스팅 제어, 구조체 압축(packing), 그리고 경고 출력 수준(lint의 오래된 주석 형태인 /* NOTREACHED */와 같이) 등이 포함됩니다.
References
[ANSI] § 3.8.6
[C89] § 6.8.6
[H&S] § 3.7 p. 61

Note
예전에는 #pragma 뒤에 나오는 것들(간단히 pragma라고 부르기도 함)은 모두 시스템에 의존적인 사항이었으나, C99에서는 몇가지 표준 pragma를 만들었습니다. 표준 pragma는 #pragma 바로 다음에 STD가 나오며, 그 뒤에 나오는 정보는 매크로 확장이 되지 않습니다.
  #pragma STD FENV_ACCESS       ON
  #pragma STD FP_CONTRACT       ON
  #pragma STD CX_LIMITEED_RANGE ON
표준 C 언어에서 공식적으로 제공하는 pragma는 위 세 가지이며, 위에서 ON 대신에 OFFDEFAULT를 쓸 수 있습니다.
References
[H&S2002] § 3.7 pp. 67-69



Q 11.21
#pragma once”가 의미하는 것이 뭐죠?
Answer
이는 어떤 전처리기들이 제공하는 기능으로 헤더 파일이 단 한번씩만 포함되도록 하는, 질문 [*]10.7에 소개된 #ifndef 트릭과 같은 역할을 합니다. 단 이식성이 떨어집니다.

11.6 Other ANSI C Issues



Q 11.22
char a[3] = "abc";가 올바른 표현인가요?
Answer
ANSI C (그리고 ANSI 이전의 몇몇 시스템에서)는 이러한 것을 올바른 표현이라고 말하지만 쓰이는 곳은 거의 없습니다. 이 코드는 정확히 세개의 요소를 갖는 배열을 선언하고 각각을 `a', `b', `c'로 초기화합니다. 즉, 문자열의 끝을 나타내는 \0은 들어가지 않습니다. 따라서 이 배열은 C 언어의 문자열이라고 말하기가 곤란합니다. 그래서 strcpyprintf와 같은 함수에 인자로 전달될 수 없습니다.

대개, 배열의 크기를 지정하지 않고, 컴파일러가 배열의 크기를 알아서 지정하도록 (즉 위의 경우에서, 크기를 지정하지 않으면, 배열의 크기는 4가 됨) 하는 것이 일반적입니다.

References
[C89] § 6.5.7
[H&S] § 4.6.4 p. 98



Q 11.24
void * 타입의 포인터에는 산술(arithmetic) 계산을 할 수 없을까요?
Answer
포인터가 가리키는 오브젝트의 크기를 알 수 없기 때입니다. 따라서 연산을 하기 전에 포인터를 char *나 처리하고자 하는 포인터 타입으로 변환해야 합니다 (질문 [*]4.5를 꼭 참고하기 바랍니다).

References
[C89] § 6.1.2.5, § 6.3.6
[H&S] § 7.6.2 p. 204



Q 11.25
memcpy()memmove()은 하는 일이 같지 않나요?

Answer
memmove()는 원본과 대상이 겹칠 경우에도 안전하게 동작한다는 것을 보장합니다. memcpy()는 이러한 보증을 하지 않으므로 좀 더 빨리 동작할 수 있습니다.. 의심이 간다면 memmove()를 쓰는 것이 더 안전합니다.

References
[K&R2] § B3 p. 250
[C89] § 7.11.2.1, § 7.11.2.2
[ANSI Rationale] § 4.11.2
[H&S] § 14.3 pp. 341-2
[PCS] § 11 pp. 165-6



Q 11.26
malloc(0)은 무엇을 의미하죠? 이 때 널 포인터가 리턴되는 것인가요, 아니면 0 바이트를 가리키는 포인터가 리턴되는 것인가요?
Answer
ANSI/ISO 표준은 둘 중 하나일 수 있다고 말하고 있습니다; 그 결과는 구현 방법에 의존적11.6입니다. (질문 [*]11.33을 참고하기 바랍니다.

References
[C89] § 7.10.3
[PCS] § 16.1 p. 386



Q 11.27
외부 이름(external identifier)을 쓸 때, 왜 ANSI 표준은 여섯 글자 이상인 이름의 유일성을 보장할 수 없다고 할까요?
Answer
오래된 링커(linker)의 경우, ANSI/ISO C나, C 컴파일러 개발자와 상관없이 시스템에 의존적인 경우가 많다는 것이 문제입니다. 이 제한은 이름의 첫 여섯 글자만을 유일하다고 보장하기 때문에, 첫 여섯 글자가 같은 이름들은, 전체가 같은 이름으로 취급합니다. 이 제한은 이미 쓸모없어져 가고 있으므로 (obsolescent), ISO 표준에서는 없어질 예정입니다.

References
[C89] § 6.1.2, § 6.9.1
[ANSI Rationale] § 3.1.2
[C9X] § 6.1.2
[H&S] § 2.5 pp. 22-3
Note
즉, 여섯 글자의 제한은 “AAAAAAB”와 “AAAAAAC”가 같은 이름으로 취급될 수 있다는 말입니다.

그러나, 현 ISO/IEC C 표준은 internal identifier나 매크로 이름으로 적어도 63글자, external idendifier로 31글자가 유효하다고 말하고 있습니다. 또한 이 제한 사항도 다음 C 표준에서는 사라질 예정입니다.

References
[C99] § 5.2.4.1, § 6.11.3

11.7 Old or Nonstandard Compilers

Although ANSI C largely standardized existing practice, it introduced sufficient new functionality that ANSI code is not necessarily acceptable to older compilers. Furthermore, any compiler may provide nonstandard extensions or may accept (and therefore seem to condone) code that the standard says is suspect.



Q 11.29
제 컴파일러는 간단한 테스트 프로그램조차 컴파일하지 못합니다. 수 많은 구문 에러를 (syntax error) 출력합니다.

Answer
아마도 ANSI 이전의 컴파일러인 것 갈습니다. 그러한 컴파일러들은 함수 원형(prototype)과 같은 것을 처리하지 못합니다.

질문 [*]1.31, [*]10.9, [*]11.30, [*]16.1b를 참고하기 바랍니다.



Q 11.30
제가 쓰는 컴파일러는 ANSI 컴파일러인데도 어떤 ANSI/ISO 표준 함수들이 정의되어 있지 않다고 하는군요.
Answer
컴파일러가 ANSI의 구문을 썼다고 하더라도, ANSI 호환의 헤더 파일이나, 런-타임 라이브러리를 갖지 않을 수 있습니다. (사실 gcc와 같이, 시스템 벤더(vendor)가 제공하지 않는 컴파일러에서는 종종 있는 일입니다.) 질문 [*]11.29, [*]13.25, [*]13.26을 참고하기 바랍니다.



Q 11.31
구 스타일로 써진 C 프로그램을 ANSI C로 바꿔주거나, 또는 그와 반대 작업을 해주는 프로그램이 있을까요?

Answer
프로토타입을 쓰는 새 방식과, 구 방식을 바꿔주는 `protoize'와 `unprotoize'라는 프로그램이 있습니다. (이 프로그램이 구 스타일의 C와 ANSI C와의 100% 완전한 변환을 보장하지는 않습니다.) 이 프로그램들은 FSF의 GNU C compiler 배포판에 포함되어 있습니다; 질문 [*]18.3을 참고하기 바랍니다.

`unproto' 프로그램은 (ftp.win.tue.nl/pub/unix/unproto5.shar.Z) 전처리기와 컴파일러 사이에서 변환을 담당하는 일종의 `필터(filter)'입니다. 그리고 ANSI C 스타일과 구 스타일의 변환을 거의 완벽하게 해 줍니다.

GNU Ghostscript 패키지에는 간단한 ansi2knr이라는 프로그램이 포함되어 있습니다.

그러나, ANSI C 스타일을 구 스타일로 바꿀 때, 이러한 변환이 모두 자동으로 안전하게 변환되는 것은 아닙니다. ANSI C에서는 K&R C에는 없는 새로운 기능과 복잡성을 내포하고 있으므로, 프로토타입이 있는 함수를 호출할 때에 주의해야 합니다; 아마도 캐스팅이 필요할 지도 모릅니다. 질문 [*]11.3과 [*]11.29를 참고하기 바랍니다.

변형된 `lint' 같은 프로그램은 prototype을 만들어 내주기도 합니다. 1992년 3월에 comp.sources.misc에 게시된 CPROTO 프로그램도 이런 기능을 합니다. 또 “cextract”라는 프로그램도 있습니다. 또 이러한 프로그램들이 컴파일러와 함께 제공되기도 합니다. 질문 [*]18.16을 참고하기 바랍니다. (그러나 작은 (narrow) 인자를 갖는 구 스타일 함수를 프로토타입 스타일로 변경할 때, 주의해야 합니다; 질문 [*]11.3을 참고하기 바랍니다.)



Q 11.32
Froozz Magic C 컴파일러는 ANSI 호환이라고 되어 있는데도, 왜 제 코드를 컴파일하지 못할까요? 제 코드는 gcc에서 동작하는 것을 보니, ANSI 호환인데요.
Answer
많은 컴파일러들이 비표준 확장 기능을 제공합니다. gcc는 특히 더 많은 확장 기능을 제공하는 것으로 알려져 있습니다. 아마도 여러분이 만든 그 코드가 이러한 확장 기능을 사용하는 것 같습니다. 특히, 어떤 컴파일러를 가지고 한 언어의 성질을 검사한다는 것은 매우 위험합니다; 어떤 표준은 구현 방법에 따라 여러 가지 방법을 선택할 수 있도록 하고 있을 수 있으며, 컴파일러가 잘못된 경우도 많기 때문입니다. 질문 [*]11.35를 참고하기 바랍니다.



Q 11.J
제 컴파일러는 다음과 같은 코드를 실행하면 에러가 납니다. 무엇이 잘못된 것일까요?

  int a[3] = { 1, 2, 3 };
  int b[3] = { 0, 9, 8 };
  int *c[2] = { a, b };

  ...

Answer
아마도 C99 표준을 지원하지 못하는 (ANSI 표준만을 지원하는) 컴파일러를 쓰신 것 같습니다. ANSI 표준, 즉 C89 표준에 따르면 초기화에 쓸 수 있는 수식에 다음과 같은 제한이 있습니다:

§ 6.5.7 All the expressions in an initializer for an object that has static storage duration or in an initializer list for an object that has aggregate or union type shall be constant expressions All the expressions in an initializer for an object that has static storage duration or in an initializer list for an object that has aggregate or union type shall be constant expressions.

즉, 정적 변수나 배열/구조체/union등의 초기값은 반드시 상수식(constant expression)이어야 합니다. 주어진 코드에서 배열 c의 초기값으로 쓰인 주소값 a, b는 자동 변수에서 얻은 값이기 때문에 상수식이 아닙니다.

C99 표준에 따르면, 다음과 같은 제한 사항이 있습니다:

All the expressions in an initializer for an object that has static storage duration shall be constant expressions or string literals.

즉, 정적 변수일 경우에만 상수식 또는 상수 문자열을 써야 한다고 제한합니다. 배열/구조체/union에 대한 제한 사항은 사라진 것을 알 수 있습니다. 결국 C99 표준에 따르면, 주어진 코드는 완전히 합법적입니다.

두 가지 해결책이 있는데, 첫째, C99를 지원하는 컴파일러로 바꾸기 바랍니다. 둘째, 초기값으로 쓰일 수식을 상수식으로 바꾸면 됩니다. 예를 들어, 주어진 코드에서 변수 abstatic으로 선언하면 됩니다.

References
[C89] § 6.5.7
[C99] § 6.7.8

11.8 Compliance

Obviously, the whole point of having a standard is so that programs and compilers can be compatible with it (and therefore with each other). Compatibility is not a simple black-or-white issue, however: There are degrees of compliance, and the scope of the standard's definitions is not always as comprehensive as might be expected. In keeping with the “spirit of C,” several issues are not precisely specified; portable programs must simple avoid depending on these issues.



Q 11.33
“구현 방법에 따라 정의됨 (implementation defined)”, “나타나지 않은 (unspecified)”, “정의되어 있지 않은 행동 (undefined behavior)”이라는 용어가 있는데, 서로 차이가 있는 것인가요?
Answer
간단하게 말해서, “implementation defined”는 각각의 구현 방법에 따라, 어떤 행동 방식(behavior)이 결정될 수 있고, 그에 따라 문서화되어 있다는 뜻입니다. “unspecified”는 구현 방법에 따라 다른 행동 방식이 결정될 수는 있지만, 문서화될 필요가 없다는 뜻입니다. “undefined”는 어떤 일이라도 일어날 수 있다는 뜻입니다. 어떤 경우에도 표준은 필요 조건을 강요하지 않습니다11.7; 때때로 처음 두 가지 경우에 대해서는 여러 가지의 상태가 제안되기도 합니다.

표준에서 `undefined behavior'라고 정의한 부분은 말 그대로 입니다. 컴파일러는 어떤! 일이라도 할 수 있습니다. 특히, 프로그램의 나머지 부분이 정상적으로 동작한다는 보장이 없습니다. 따라서 이런 부분이 여러분의 프로그램에 포함된다면 매우 위험합니다; 질문 [*]3.2에서 간단한 예를 볼 수 있습니다.

이식성이 뛰어난 프로그램을 만들고자 한다면, 이러한 것을 다 무시하고, 위 세가지에 의존하는 어떠한 것도 만들어서는 안될 것입니다.

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

References
[C89] § 3.10, § 3.16, § 3.17
[ANSI Rationale] § 1.6



Q 11.34
ANSI 표준에는 많은 것들이 정의되지 않은 채 남아 있습니다. 이러한 것들에 대해서도 표준을 정해야 하지 않을까요?

Answer
C 언어의 한가지 특징은, 어떠한 것들은 컴퓨터와 컴파일러에 따라 각각 다른 행동 방식이 나올 수 있다는 것입니다. 이 것은, 컴파일러가 일반적인 작업을 좀더 효과적으로 수행할 수 있도록 하기 위해서입니다. 따라서 표준은 사소한 것까지 정의하지 않고 단순히 관습을 따르는 것입니다.

프로그래밍 언어 표준은 언어 사용자와 컴파일러 개발자 사이에 위치한 일종의 계약으로 생각할 수 있습니다. 절반은 컴파일러 개발자가 제공하려 하는 것과, 사용자가 `이런 것들은 제공될 것이다'하고 생각하는 것들로 이루어지고, 나머지 절반은 사용자가 따라야 하는 규칙과 개발자가 따를 것으로 생각되는 규칙으로 이루어 집니다. 따라서 어느 한 쪽이 이런 규약을 어긴다면, 어떤 일이 발생할 지 아무도 보장할 수 없습니다.

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

References
[ANSI Rationale] § 1.1



Q 11.35
i = i++과 같은 코드의 행동 방식은 정의되어 있지 않다고 들었습니다. 그런데, 이 코드를 ANSI를 준수하는 컴파일러에서 실행하면 제가 추측한 그 결과가 나옵니다.
Answer
컴파일러는 행동 방식이 정의되어 있지 않는(undefined behavior) 코드에 대해서는 여러분이 예상한 방식을 포함한, 어떠한 일도 할 수 있습니다. 이런 것에 의존하는 것은 매우 나쁩니다. 질문 [*]11.32와 [*]11.33, [*]11.34를 참고하기 바랍니다.

11.9 volatile qualifier

TODO: `volatile'에 대해 소개하기. volatileconst와 더불어 C89에서 소개된 것입니다.



Q 11.A
자주 쓰이지는 않다고 알고 있는데, volatile이 정확히 어떤 의미를 가지는 것인가요?
Answer
volatile 타입 qualifier는 주어진 오브젝트가 컴파일러가 의도하지 않은 방식으로 변경될 수 있다는 것을 나타냅니다. 따라서, 컴파일러는 이 오브젝트를 최적화(optimization) 과정에서 제외시킵니다. 좀더 정확히 말해서, 이 오브젝트에 대한 참조(reference)나 변경(modification)은 sequence point를 넘어다니며 최적화되지 않습니다. 단, sequence point 안에서 최적화될 수 있습니다. (sequence point에 관한 것은 질문 [*]3.8을 참고하기 바랍니다.)

일반적으로, volatile이 쓰이는 곳은 크게 두 가지로 나누어 생각할 수 있습니다.

예를 들어, 어떤 시스템은 세 개의 특정 메모리 주소를 제공하고, 이 중 두개는 하드웨어의 정보를 알려 주는 데에 쓰이며, 나머지 하나는 하드웨어에 직접 데이터를 쓰기 위한 목적으로 사용한다고 가정해 봅시다. 읽는 목적으로 쓰는 주소는 각각 in1, in2라는 포인터가 가리키고 있고, 쓰기 위한 주소는 out이라는 포인터에 저장되어 있다고 가정합시다. 이 경우 다음과 같은 코드를 예상할 수 있습니다:

  volatile unsigned char *out;
  volatile unsigned char *in1, *in2;
  int i;
  ...
  for (i = 0; i < N; ++i)
    *out = a[i] & (*in1 + *in2);
이 코드는 out이 가리키는 곳에, *in1*in2를 더해서, a[i]의 값과 AND한 결과를 쓰게 됩니다. (위 코드에서 volatile이 없다고 가정하면) 단순한 시스템일 경우, 루프를 매번 돌 때마다, *in1 + *in2를 수행해서, 그 결과를 a[i]와 더해, *out에 쓰게 되지만, 최적화를 수행한다면, 매번 *in1 + *in2 덧셈을 수행할 이유가 없습니다. 그래서 컴파일러는 보통 더한 결과를 특정 레지스터에 저장해 두고, 이 것을 루프를 반복할 때마다 a[i]와 더하는 코드를 만들어 냅니다. 그러나 *in1*in2는 하드웨어가 직접 건드리는 값이 들어 있으므로, 루프를 돌 때, 매번 같다는 보장을 할 수 없습니다. 따라서 최적화를 수행한 코드와 그렇지 않은 코드가 서로 실행 결과가 다르거나, 예상하지 못한 결과를 가져올 수 있습니다.

이 때, 관련된 변수인 in1, in2, outvolatile로 선언함으로써, 이 변수들이 컴파일러의 의도와 상관없이 변경될 수 있다는 것을 알려주면, 컴파일러는 이 변수가 관계된 코드는 최적화 고려 대상에서 제외시킵니다.

또한 non-local goto 역할을 수행하는 함수 setjmplongjmp를 쓸 때, volatile을 유용하게 쓸 수 있습니다. 자세한 것은 질문 20.A를 참고하기 바랍니다.

Signal handler에 대한 것은 질문 [*]19.A를 참고하기 바랍니다.

References
[H&S2002] § 4.4.5 [C99 Rationale] § 5.1.2.3, § 5.2.3, § 6.7.3 [C99] § 5.1.2.3, § 6.7.3 pp. 108-109,
§ 7.13.2.1, § 7.14

11.10 restrict qualifier

The type qualifier restrict is new in C99. It may only be used to qualify pointers to object or incomplete types, and in serves as a “no alias” hint to the C compiler...

TODO: `restrict' qualifier에 대하여 소개하기.

11.11 Flexible Array Members

질문 [*]2.6의 질문에 있는 코드는 매우 자주 쓰이지만, 엄밀하게 말해, C89 표준까지 “undefined behavior”로 간주되어 왔습니다. 즉, 동작은 하지만, C 표준에는 위배되는 코드였습니다.

C99 표준은 새로 “flexible array member”라는 개념을 도입해서 이런 문제를 쉽고 깔끔하게 해결할 수 있도록 도와줍니다.



Q 11.G
C99에 “flexible array member”가 소개된 것으로 아는데, 정확히 무엇을 뜻하는 것인가요?
Answer
먼저 질문 [*]2.6을 보기 바랍니다. 그 질문에 나온 structure와 답변 첫 번째로 주어진 makename 함수를 새로 만들 수 있습니다.

  struct name {
    int namelen;
    char namestr[];  /* flexible array member */
  };

즉, 배열의 크기를 지정하지 않으면 됩니다. 쓰는 방법은 다음과 같습니다:

  #include <stdlib.h>
  #include <string.h>

  struct name *makename(char *newname)
  {
    struct name *ret =
      malloc(sizeof(struct name) + 
              strlen(newname) + 1);
        /* No need to add -1; +1 for \0 */
    if (ret != NULL) {
      ret->namelen = strlen(newname);
      strcpy(ret->namestr, newname);
    }
    return ret;
  }

단, 다음 두 가지를 조심해야 합니다. 첫째, flexible array member를 쓰기 위해서는, structure에 다른 멤버가 하나 이상 나와야 합니다. 즉 아래와 같은 코드는 잘못된 것입니다:

  struct name {
    char namestr[];  /* flexible array member */
  };
둘째, flexible array member는 항상 structure의 마지막 member로 나와야 합니다.

References
[C99] § 6.7.2.1
[C99 Rationale] § 6.7.2.1



Q 11.H
질문 [*]11.G에 나온 것처럼, flexible array member를 가진 structure의 크기는 어떻게 계산되나요? 즉, sizeof 연산자를 쓰면, 어떤 값을 돌려주나요?
Answer
Flexible array member를 가진 structure에 sizeof 연산자를 쓰면, 그 flexible array member를 뺀 크기를 알려줍니다. 다음과 같은 structure를 생각해 봅시다:
  struct foo {
    char ch;
    int i[];
  };
그리고 쓰고 있는 시스템은 sizeof(int)가 4이고, int는 항상 4 byte의 배수인 주소에 align된다고 가정합시다. 위 structure에서 sizeof(ch)는 정의에 의해 1입니다. 그리고 i는 flexible array member이므로 무시됩니다. 단, i는 4 byte alignment를 지켜야 하므로, chi 사이에는 3 byte의 padding이 존재하게 됩니다. 이 padding은 structure의 크기에 포함되므로 결국 이 경우, sizeof(struct foo)는 4입니다.

단, 위 단락은 이해를 돕기 위해 쓴 글입니다. 특이한 방식의 alignment를 쓰는 시스템에서는 다르게 나올 수 있습니다. 중요한 것은 다음 문장입니다. 꼭 기억해 두기 바랍니다.

sizeof applied to the structure that includes a flexible array member ignores the array but counts any padding before it.
References
[C99] § 6.7.2.1
[C99 Rationale] § 6.7.2.1

11.12 Types



Q 11.I
원하는 크기에 정확히 맞는 정수 타입을 쓰고 싶습니다. 새로운 C 표준에서 이러한 타입을 제공한다고 들었는데, 맞습니까?

Answer
C99 표준에 따라, <stdint.h> 또는 <inttypes.h>를 포함시킬 경우, 다음과 같은 타입을 쓸 수 있습니다. 아래 표에서 N은 8, 24와 같은 십진수를 뜻합니다. 또한 두세번째 열에서 나온 최대/최소값을 위한 매크로는 <stdint.h>를 포함하기 전, __STDC_LIMIT_MACROS를 정의해야 쓸 수 있습니다.

  #define __STDC_LIMIT_MACROS
  #include <stdint.h>

(경고: 이 타입들은 시스템에 따라서 제공되지 않을 수도 있습니다.11.8):

intN_t INTN_MIN
$-(2^{N-1})$
INTN_MAX
$2^{N-1} - 1$
uintN_t 0 UINTN_MAX
$2^{N-1} - 1$
int_leastN_t INT_LEASTN_MIN
$-(2^{N-1} - 1)$
INT_LEASTN_MAX
$2^{N-1} - 1$
uint_leastN_t 0 UINTN_MAX
$2^N - 1$
int_fastN_t INT_FASTN_MIN
$-(2^{n-1}-1)$
INT_FASTN_MAX
$2^{N-1} - 1$
uint_fastN_t 0 UINT_FASTN_MAX
$2^{N} - 1$
intptr_t INTPTR_MIN
$-(2^{15}-1)$
INTPTR_MAX
$2^{15} - 1$
uintptr_t 0 UINTPTR_MAX
$2^{16} - 1$
intmax_t INTMAX_MAX
$-(2^{63} - 1)$
INTMAX_MAX
$2^{63} - 1$
uintmax_t 0 UINTMAX_MAX
$2^{64} - 1$

  1. intN_t는 크기 N인, 부호있는 정수를 위한 typedef 이름입니다. (시스템이 8, 16, 32, 64 비트를 가진 정수 타입을 지원한다면, 해당하는 intN_t도 정의되어야 합니다. 나머지는 optional입니다.)
  2. intN_t는 부호없는 정수를 위한 이름이며, 제한 사항은 위와 같습니다.

  3. int_leastN_t는 부호있는, 적어도 N보다 큰 크기를 가진 정수를 위한 typedef 이름입니다. (N이 8, 16, 32, 64인 경우는 필수로 제공되며, 나머지는 optional)
  4. int_leastN_t는 부호없는, 적어도 N보다 큰 크기를 가진 정수를 위한 typedef 이름입니다. 제한 사항은 위와 같습니다.

  5. int_fastN_t는 적어도 N 크기를 가지는, 빠르게 동작할 수 있는11.9, 부호있는 정수를 위한 typedef 이름입니다. (N이 8, 16, 32, 64인 경우는 필수로 제공되며, 나머지는 optional입니다.)
  6. uint_fastN_t는 부호없는 정수를 위한 것이며, 나머지는 위 타입과 같습니다.

  7. intptr_t는 void 포인터의 값을 저장하고, 다시 void 포인터로 변환했을 때 변화없음을 보장하는, 부호있는 정수를 위한 typedef 이름입니다. 즉, 포인터를 안전하게 저장할 수 있는 정수 타입이라고 생각하시면 됩니다. (이 타입은 optional입니다.)

  8. uintptr_tintptr_t와 같으며, 부호없는 정수를 위한 typedef 이름입니다. (이 타입은 optional입니다.)

  9. intmax_t는 부호있는 정수 중 가장 큰 값을 표현할 수 있는 타입입니다. (이 타입은 필수로 제공됩니다.)

  10. uintmax_t는 부호없는 정수 중 가장 큰 값을 표현할 수 있는 타입입니다. (이 타입은 필수입니다.)

표준에 따라 정확히 말하면, int_16_t, int32_t등이 정의된 헤더 파일은 <stdint.h>에 정의되어 있고, <inttype.h><stdint.h>를 포함하게 되며, 부가적인 사항들을 제공합니다. 이 부가적인 사항들은 질문 [*]12.A와 [*]12.B를 참고하기 바랍니다.

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

Seong-Kook Shin
2018-05-28