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]를 참고하기 바랍니다.
그 후에 국제 표준 기구인 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] 용으로 새 판이 준비되고 있는 상황입니다.)
첫째, 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”라고 부르는 것이 올바른 표현입니다.
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 4650Herbert 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를 참고하기 바랍니다.
http://www.lysator.liu.se/c/index.html http://www.dkuug.dk/JTC1/SC22/WG14/ http://www.dmk.com/
http://www.dkuug.dk/JTC1/SC22/WG14/위 사이트의 새 주소는 다음과 같습니다:
http://www.open-std.org/jtc1/sc22/wg14/
extern int func(float); int func(x) float x; { ...
extern int func(float)
”을 오래된 (스타일) 정의인
“int func(x) float x;
”와 섞어 썼기 때문에 발생합니다.
보통 이 두 스타일을 섞어 쓰는 것이 가능하지만(질문
11.4 참고)
이 경우에는 안됩니다.
Traditional C (프로토 타입과 가변 인자 리스트를 제공하지 않는 ANSI C; 질문 15.2 참고) 언어에서는 함수에 전달되는 어떤 인자들을 “확장(widen)”시킵니다. 즉 float은 double로, char나 short int는 int로 확장시킵니다. (구 스타일로 정의한 함수에서는, 이렇게 확장되어 전달된 인자가, 함수의 몸체 부분에 들어갈 때, 다시 원래의 크기로 변환됩니다) 따라서 위의 오래된 스타일로 만든 정의는 사실상 func 함수를 double 타입 인자를 받도록 만든 것입니다. (물론 함수 내부에서 이 인자는 다시 float 타입으로 바뀝니다) 이 문제는 함수 정의에서 새 스타일의 문법을 써서 고칠 수 있습니다:
int func(float x) { ... }
또는 새 형식의 프로토타입 선언을 구 형식의 정의와 일치하도록 다음과 같이 만들어 주면 됩니다:
extern int func(double);(이 경우, 가능하다면 구 형식의 정의에서 double을 쓰도록 바꿔주는 것이 더 깨끗합니다.11.3)
“narrow” 효과를 피하기 위해, “narrow” 효과를 발생하는 타입들을 (예를 들어 char, short int, float 등) 함수 인자나 리턴 타입으로 쓰지 않는 편이 안전할 수 있습니다.
이 규칙은 생각보다 좀 더 까다롭습니다. 아래 Reference를 꼭 읽어 보기 바랍니다.
[C99] § 6.5.2.2 § 6.9.1
extern int f(struct x *p);“struct x introduced in prototype scope”라는 이상한 경고를 발생시킵니다.
함수 prototype 앞에 structure 선언을 두어 이 문제를 해결할 수 있습니다. (보통, prototype과 structure 선언은 같은 헤더 파일에 존재하며, 이렇기 때문에 한쪽이 다른 한쪽을 참조할 수 있습니다.) 만약 prototype에 아직 선언되지 않은 structure를 꼭 쓸 필요가 있다면, prototype 앞에 다음과 같이 써 줍니다:
struct x;아무것도 아닌 것 같은 이 선언은 struct x 이렇게 하면, 이 구조체의 (incomplete) 선언이 파일 스코프를 가지게 되어, 이후에 나올 선언에서 struct x를 사용할 때, 같은 struct x를 가리키도록 할 수 있습니다.
printf("%d", n);위에서 n은 long int 타입입니다. ANSI 함수 prototype이 이런식으로 인자와 파라메터 타입이 서로 일치하지 않을때, (conversion 등으로) 보호해 주지 않나요?
<stdio.h>
를 include해야 한다고
들었습니다. 왜 그런가요?
const int n = 5; int a[n];
#define
으로 (또는
enum으로) 상수를 정의하기 바랍니다.
const char *p
'와 `char * const p
'의 차이는
무엇인가요?
const char *p
'는 (`char const *p
'라고 쓸 수 있음)
상수 문자에 대한 포인터를 선언한 것입니다 (가리키는 문자를 바꿀 수
없는 포인터);
`char * const p
'는 문자에 대한 상수 포인터를 선언한 것입니다
(문자를 변경할 수는 있지만 포인터를 변경할 수는 없습니다).
해설을 잘 음미해보시기 바랍니다; 질문 1.21도 참고하시기 바랍니다.
const_pointer
는 포인터가 상수인 것을
나타냅니다. 그리고,
pointer_to_const
는 상수를 가리키는 포인터를 나타냅니다:
int * const const_pointer; const int *pointer_to_const;즉,
const_pointer
는 포인터가 가리키는 대상을
변경할 수 있으나, 다른 대상을 가리키도록 할 수는 없습니다. 그리고,
pointer_to_const
는 다른 대상을 가리키도록 할 수 있지만,
포인터가 가리키는 대상을 변경할 수 없습니다.
const char **
를 인자로 받는 함수에 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을 가리키고 있었던 것이 문제입니다. *p2가 p1을 가리키도록 만든 것은 바로 세번째 줄에서 했던 것이고, 이 것은 허용되지 않는 대입 연산이며, 이런 이유 때문에, 허용되지 않습니다.11.4
(앞 예제 세번째 줄에서처럼) const char **에 char **를 대입하는 것은 그 자체가 위험한 것은 아닙니다. 그러나 위에서 p2가 약속하고 있는 것, 즉 궁극적으로 가리키고 있는 값을 변경하지 않는다를 깨뜨릴 수 있는 실마리를 제공합니다. 그래도 이러한 연산이 필요하다면, 다시 말해, 가장 최상위 수준이 아닌 곳에서 qualifier가 서로 달라서 대입이 되지 않는 것을 대입시키려면, 직접 캐스트해서 (즉, 이 경우에는 (const char) **로) 쓸 수 있습니다.
typedef char *charp; const charp p;왜 p가 가리키는 char가 const가 되지 않고, p 자체가 const가 되는 것일까요?
const charp p;const int i가 i를 const로 만드는 것과 같은 원리에서, p는 const가 됩니다. p에 대한 선언은, 포인터가 관련이 되어있는지 typedef 안까지 쫓아가서 확인하지 않습니다.
int * const const_pointer;위에서
const_pointer
는 typedef를 써서 다음과
같이 쓸 수 있습니다:
typedef int *int_pointer; const int_pointer const_pointer;이 때,
const_pointer
는 상수 int를 가리키는
포인터처럼 보이지만, 실제로는 (상수가 아닌) int를 가리키는
const 포인터입니다. 또, 타입 specifier와 타입 qualifier의 순서는
중요하지 않기 때문에 (순서가 바뀔 수 있기 때문에), 다음과 같이
쓸 수도 있습니다:
int_pointer const const_pointer;
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까지 참고하기 바랍니다.
[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
[H&S2002] § 4.4.1
단순히 경고를 없애려고 함수를 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.)
#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을 쓸 것을 권장합니다.
Borland C++ 4.5에서 void main()을 썼을 때, 프로그램이 망가질 수 있다는 것이 이미 보고되었습니다. 그리고 어떤 컴파일러들은 (DEC C V4.1과 gcc) main을 void 타입으로 선언했을 때, 경고를 발생합니다.
여러분의 운영 체제가 종료 상태(exit status)를 무시할 수도 있고, void main()이 동작할 수도 있지만, 이는 이식성이 없을 뿐만 아니라, 올바른 것도 아닙니다.
여러 시스템에서 void main()으로 써도 동작한다는 것은 사실입니다. 만약 이식성을 전혀 고려할 생각이 없고, 이 방식이 더 편하다고 생각하면, 아무도 말릴 사람은 없습니다.
main에 local 데이터가 `cleanup' 과정에서 필요할 경우, main에서 리턴하는 방법은 제대로 동작하지 않을 수 있습니다; 질문 16.4를 참고하기 바랍니다. 그리고 아주 오래된 (표준을 지원하지 않는) 몇몇 시스템에서는 두 가지 형식 중 하나가 제대로 동작하지 않을 수 있습니다.
(마지막으로, 이 두가지 형태는 main()을 재귀적으로 호출할 경우, 다른 코드를 생성합니다.)
C99 표준에 따르면, (표준에 부합하는 형태, 즉 int를 리턴하는) main에서 어떤 값을 return 하는 것은, exit 함수를 그 값으로 부르는 것과 같다고 씌여 있습니다. 그리고 이 효과가 일어나는 것은, 프로그램 시작점으로 쓰인 main에서만 일어납니다. 즉, 재귀적으로 불려진 main에서 return한다고 exit가 자동으로 호출되지 않습니다.
또한 exit나 return 문장 없이 main이 끝나버리면, return 0을 쓴 것과 같은 효과를 얻을 수 있다고 씌여 있습니다. (이 것은 C99에 새로 추가된 내용입니다. 그 전 표준인 ANSI, C89 등에서는 해당되지 않습니다.)
return하는 것과 exit를 부르는 것에 미세한 차이가 있을 수 있는데, 만약 atexit로 exit handler를 등록시켜 놓았고, 그 handler가 main에서 만든 어떤 automatic (static이 아닌) 변수에 접근한다면, return하는 것은 이 변수에 접근할 수 없습니다. 즉, return을 써서 자동으로 exit를 부르게 하는 것은, 이미 main 함수의 블럭을 벗어났기 때문에, main에서 선언한 automatic 변수는 존재하지 않습니다.
#pragma
directive.
#
를 써서 심볼릭
상수의 값을 문자열에 집어 넣으려고 합니다, 그런데,
그 결과, 상수의 값이 들어가는 대신, 상수의 이름이 들어가는군요.
#
의 정의에 따르면, 이 것은 매크로 인자를
(인자가 또 다른 매크로 이름이더라도 더 이상 확장하지 않고)
바로 문자열로 만듭니다.
매크로가 원래 지닌 뜻으로 확장되길 원한다면 다음과 갈이
두 단계를 거쳐서 쓸 수 있습니다:
#define Str(x) #x #define Xstr(x) Str(x) #define OP plus char *opname = Xstr(OP);이 코드는 opname을 “OP”로 설정하지 않고, “plus”로 설정합니다. (즉, Xstr() 매크로가 인자를 확장하고 Str() 매크로가 문자열로 만듭니다.)
비슷한 상황이 “token-pasting” 연산자인 ##
를 쓸 때,
두 매크로의 값을 연결하려 할 때 발생할 수 있습니다.
#
나 ##
는 일반 소스 코드에서는 쓰일 수 없으며,
다만 매크로 정의 부분에서만 쓸 수 있는 연산인 것을 꼭 기억하기
바랍니다.
#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을 참고하기 바랍니다.
#ifdef
를 써서 컴파일하지 말라고 한 곳에서 매우 이상한
구문(syntax) 에러가 납니다.
#if
, #ifdef
, #ifndef
에 쓴 텍스트는
전처리기가 처리할 수 있는 유효한 것이어야 (valid preprocssing
token) 합니다.
즉, C 언어에서처럼 "
나 '
는 각각이 쌍을 이루어서
나와야 하며, 이처럼 둘러싼 문자열의 안에 newline 문자가 나와서는
안되며, 주석이 끝나지 않고 열려 있어서도 안됩니다.
서로 다른 따옴표가 엇갈려서 있어도 안됩니다.
(특히, 영어의 생략형(contracted word)에
쓰이는, 역 따옴표(apostophe, `
)는
문자 상수의 시작처럼 보일 수 있다는 것에 주의하기 바랍니다.)
따라서 긴 주석이나 pseudo code를 쓰는 것이 목적이라면
#ifdef
를 써서 빼라고 지정을 했더라도, 공식적(offical)인
주석(comment)인 /* ... */
을 써야 합니다.
#pragma
는 어디에 쓰나요?
#pragma
는 모든 종류의 (이식성이 떨어지는) 모든 구현 방법에
따른 기능을 제어하고, 확장 기능을 제공합니다; 여기에는
소스 리스팅 제어, 구조체 압축(packing), 그리고
경고 출력 수준(lint의
오래된 주석 형태인 /* NOTREACHED */
와 같이) 등이
포함됩니다.
#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 대신에 OFF나 DEFAULT를 쓸 수 있습니다.
#pragma once
”가 의미하는 것이 뭐죠?
#ifndef
트릭과 같은 역할을
합니다. 단 이식성이 떨어집니다.
char a[3] = "abc";
가 올바른 표현인가요?
\0
은 들어가지 않습니다.
따라서 이 배열은 C 언어의 문자열이라고 말하기가 곤란합니다.
그래서 strcpy나 printf와 같은 함수에 인자로
전달될 수 없습니다.
대개, 배열의 크기를 지정하지 않고, 컴파일러가 배열의 크기를 알아서 지정하도록 (즉 위의 경우에서, 크기를 지정하지 않으면, 배열의 크기는 4가 됨) 하는 것이 일반적입니다.
void *
타입의 포인터에는 산술(arithmetic) 계산을
할 수 없을까요?
char *
나 처리하고자 하는 포인터
타입으로 변환해야 합니다 (질문
4.5를 꼭 참고하기 바랍니다).
그러나, 현 ISO/IEC C 표준은 internal identifier나 매크로 이름으로 적어도 63글자, external idendifier로 31글자가 유효하다고 말하고 있습니다. 또한 이 제한 사항도 다음 C 표준에서는 사라질 예정입니다.
`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을 참고하기 바랍니다.)
int a[3] = { 1, 2, 3 }; int b[3] = { 0, 9, 8 }; int *c[2] = { a, b }; ...
§ 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를 지원하는 컴파일러로 바꾸기 바랍니다. 둘째, 초기값으로 쓰일 수식을 상수식으로 바꾸면 됩니다. 예를 들어, 주어진 코드에서 변수 a와 b를 static으로 선언하면 됩니다.
표준에서 `undefined behavior'라고 정의한 부분은 말 그대로 입니다. 컴파일러는 어떤! 일이라도 할 수 있습니다. 특히, 프로그램의 나머지 부분이 정상적으로 동작한다는 보장이 없습니다. 따라서 이런 부분이 여러분의 프로그램에 포함된다면 매우 위험합니다; 질문 3.2에서 간단한 예를 볼 수 있습니다.
이식성이 뛰어난 프로그램을 만들고자 한다면, 이러한 것을 다 무시하고, 위 세가지에 의존하는 어떠한 것도 만들어서는 안될 것입니다.
프로그래밍 언어 표준은 언어 사용자와 컴파일러 개발자 사이에 위치한 일종의 계약으로 생각할 수 있습니다. 절반은 컴파일러 개발자가 제공하려 하는 것과, 사용자가 `이런 것들은 제공될 것이다'하고 생각하는 것들로 이루어지고, 나머지 절반은 사용자가 따라야 하는 규칙과 개발자가 따를 것으로 생각되는 규칙으로 이루어 집니다. 따라서 어느 한 쪽이 이런 규약을 어긴다면, 어떤 일이 발생할 지 아무도 보장할 수 없습니다.
i = i++
과 같은 코드의 행동 방식은 정의되어 있지
않다고 들었습니다. 그런데, 이 코드를 ANSI를 준수하는
컴파일러에서 실행하면 제가 추측한 그 결과가 나옵니다.
일반적으로, 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, out를 volatile로 선언함으로써, 이 변수들이 컴파일러의 의도와 상관없이 변경될 수 있다는 것을 알려주면, 컴파일러는 이 변수가 관계된 코드는 최적화 고려 대상에서 제외시킵니다.
또한 non-local goto 역할을 수행하는 함수 setjmp와 longjmp를 쓸 때, volatile을 유용하게 쓸 수 있습니다. 자세한 것은 질문 20.A를 참고하기 바랍니다.
TODO: `restrict' qualifier에 대하여 소개하기.
C99 표준은 새로 “flexible array member”라는 개념을 도입해서 이런 문제를 쉽고 깔끔하게 해결할 수 있도록 도와줍니다.
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로 나와야 합니다.
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를 지켜야 하므로, ch와 i 사이에는 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.
<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 |
INTN_MAX |
uintN_t | 0 | UINTN_MAX |
int_leastN_t | INT_LEASTN_MIN |
INT_LEASTN_MAX |
uint_leastN_t | 0 | UINTN_MAX |
int_fastN_t | INT_FASTN_MIN |
INT_FASTN_MAX |
uint_fastN_t | 0 | UINT_FASTN_MAX |
intptr_t | INTPTR_MIN |
INTPTR_MAX |
uintptr_t | 0 | UINTPTR_MAX |
intmax_t | INTMAX_MAX |
INTMAX_MAX |
uintmax_t | 0 | UINTMAX_MAX |
표준에 따라 정확히 말하면, int_16_t
, int32_t
등이
정의된 헤더 파일은 <stdint.h>
에 정의되어 있고,
<inttype.h>
는 <stdint.h>
를
포함하게 되며, 부가적인 사항들을 제공합니다. 이 부가적인
사항들은 질문
12.A와
12.B를 참고하기 바랍니다.
Seong-Kook Shin