[OS]/Embedded&Linux

C99 새 기술을 통해 보는 C 언어의 미래

하늘을닮은호수M 2007. 8. 6. 10:38
반응형

출처 : http://hawkshim.tistory.com/entry/펌-C99-새-기술을-통해-보는-C-언어의-미래

C 언어의 새 표준인 C99의 소개도 막바지에 이르렀다. 이번 마지막 시간에는 남은 기술 중 중요한 것 일부와 중요성이 떨어지거나 제한된 지면으로 깊이 다루기 어려웠던 기술을 언급하겠다. 그리고 C99 이후 C 언어가 어떠한 방향으로 변화해가고 있는지 살펴보면서 C++와의 관계도 짚어볼 계획이다.

이전 연재에서 소개했던 기준에 맞춰 C99에 새로 도입된 기술을 이어서 알아보자. 여기서 소개하는 것 외에도 더 많은 내용이 있지만, 중요한 항목만 자세히 언급하도록 하겠다.

와 를 통한 확장 문자의 지원[Lib][Open]
와 는 사실 C90의 확장인 AMD1에 추가된 표준 라이브러리로 C90 발표 이전부터 비영어권에서 제기됐던 대형 문자셋 지원을 위해 추가됐다. 확장 문자(wide character)와 항상 따라다니는 멀티바이트 문자라는 개념은 바이트/문자 충돌(byte and character conflict)을 해결하기 위해 도입된 개념이다.
컴퓨터에서 영문 알파벳과 일부 특수 문자만을 사용하는 경우, 표준이 최소 8비트 이상으로 규정하는 바이트를 통해 아무 문제없이 모든 문자를 표현할 수 있다. 하지만 비영어권의 경우에는 훨씬 많은 수의 글자(한자를 생각해 보자)를 표현할 수 있는 방법이 필요하고, 결국 그런 환경에서 문자를 표현하기 위해서는 바이트보다 더 큰 저장 단위를 사용해야 한다. 즉 하나의 바이트가 곧 하나의 문자라는 전통적인 공식이 깨졌고, 이를 바이트/문자 충돌이라고 부른다.
언급했듯이 C 언어가 필수적으로 필요로 하는 기본 문자 이외에 많은 수의 문자를 표현하는 가장 직관적인 방법은 바이트보다 큰 저장 단위를 사용하는 것이다. 하지만 문제는 프로그램 내부에서는 이 방법이 가능하지만 프로그램 외부(예를 들면, 저장 매체)에서는 여전히 바이트 중심이라는 것이다. 이를 위해 바이트보다 큰 저장 단위에 저장되어 있는 확장 문자를 바이트 기반의 표현으로 변환(인코딩)해놓은 멀티바이트 문자의 개념이 생긴다. 즉 많은 문자를 포함하는 문자셋을 다루기 위해 프로그램 내부에서는 확장 문자를, 외부에서는 멀티바이트 문자를 사용하며, 프로그램은 외부로부터 멀티바이트 문자를 입력받아 이를 확장 문자로 변환해 원하는 작업을 수행한 후에 다시 출력할 때 멀티바이트 문자로 변환해 출력할 수 있다.
다만 AMD1 이전에는 표준이 확장 문자(열)와 멀티바이트 문자(열) 사이의 변환 기능만을 를 통해 제공했기 때문에, 확장 문자(열)로 변환한 이후의 작업은 모두 프로그램의 일이었다. AMD1에서는 와 를 통해 프로그램 내에서 확장 문자(열)를 기존의 단일 바이트 문자(열)처럼 손쉽게 다룰 수 있는 편의를 제공한다. 예를 들어 다음에서 왼쪽 프로그램은 단일 바이트 문자를 입력받아 그 수를 세는 전통적인 프로그램이고, 오른쪽은 동일한 작업을 멀티바이트 문자가 변환된 결과인 확장 문자에 대해서 수행하는 것이다.

이 예에서 볼 수 있듯이 전통적인 단일 바이트 함수에 대응하는 확장 문자 함수를 도입할 때 위원회는 병렬성(parallelism)과 개선성(improvement)을 고려했다. 즉 가급적 기존의 형태와 유사하게 프로그램을 작성할 수 있되, 분명한 문제를 안고 있는 함수들에 대해서는 처음 확장 문자와 관련된 함수를 추가하는 것이기에 시작부터 과감히 변화를 준다는 원칙이었다. 다음은 이 두 가지 기준이 적용된 예를 보여준다(모든 예를 나열한 것은 아니다).

◆ 병렬성이 적용된 경우
char/wchar_t: 단일 문자를 표현하기 위한 데이터형
int/wint_t: EOF/WEOF를 추가로 표현할 수 있는 데이터형
isdigit()/isxdigit(): 문자 분류를 위한 함수
fgetc()/fgetwc(): 스트림에서 문자를 읽어오기 위한 함수
fprintf()/fwprintf(): 스트림으로 형식을 갖춰 출력하는 함수
◆ 개선성이 적용된 경우
swprintf(): sprintf()와는 달리 버퍼 오버런(buffer overrun)을 방지
wcstok(): strtok()와는 달리 내부 버퍼를 사용하지 않음
◆ 확장 문자 버전이 없는 경우(위험하거나 불필요한 경우)
gets()/puts()/atof()/perror()
◆ 단일 바이트 버전이 없는 경우(주로 문자 변환 함수)
btowc()/mbrtowc()

또 하나, C90과는 달리 AMD1에 추가된 흥미로운 개념은 일반화된 멀티바이트 문자라는 것이다. C90은 널 문자(null character)가 단일 바이트 문자열과 멀티 바이트 문자열 모두에서 문자열의 끝을 표시하는 역할을 하도록 하기 위해, 또 초기 변환 상태에서는 멀티바이트 문자열 내에서 C 언어의 기본 문자가 모두 본래의 의미를 갖도록 하기 위해 멀티바이트 문자에 엄격한 제한을 가했다. 하지만 이런 제한은 ISO/IEC 10646을 멀티바이트 문자로 사용하지 못하도록 만든다(3개의 널 바이트가 코드 값에 포함되어 있음에 유의하자). 예를 들어 a는 ISO/IEC 10646에서 a와 같은 표현을 갖는다. 따라서 파일에 한해서만 그러한 제한을 완화하고 멀티바이트 문자로 구성된 파일의 내용을 읽어 확장 문자로 변환해 사용할 수 있도록 일반화된 멀티바이트 문자라는 개념을 추가했다. 주의할 점은 일반화된 멀티바이트 문자는 프로그램 외부에서만 허용된다는 사실이다. 프로그램 내부에서 다루어지는 멀티바이트 문자에는 여전히 엄격한 제한이 그대로 적용된다.

가변 길이 배열[Lang][Open]
C에서 모든 배열은 정적인 크기를 갖는다. 즉 프로그램이 실행되기 이전에 모든 배열의 크기를 미리 결정할 수 있도록 정수와 상수만이 배열 크기를 명시하기 위해 쓰일 수 있다. 결국 동적인 크기의 배열을 위해서는 malloc()/free()를 사용해야 했고, 이런 불편을 줄이기 위해 다수의 컴파일러는 확장을 통해 보통의 배열 선언에서도 크기를 동적으로 명시할 수 있도록 허락했었다. 하지만 이제 C99를 통해 표준 C에서도 공식적으로 가변 길이를 갖는 배열(VLA, variable length array) 선언이 허락된다.

void foo(int size, int vla[*]);

int bar(void)
{
const int n = 10; // C에서는 상수가 아님
int m = 9;
int a[n], twodim[m][n];
size_t s = sizeof(a); // 실행 중에 평가됨
/* ... */
}

void foo(int size, int vla[size])
{ /* .... */ }

이 예는 VLA를 선언하고 사용하는 다양한 모습을 보여주고 있다. 특히 동적인 크기의 2차원 배열이 malloc()/free()를 통한 동적 메모리 할당에 비해 얼마나 쉽게 선언될 수 있는지 주목할 필요가 있다. 또한 주석에서도 밝히고 있지만 원래 번역시에 평가되는 sizeof 연산자가 VLA에 적용되는 경우에는 실행 중에 평가된다는 사실도 흥미롭다. 다음 예를 보자.

i = 0;
int a1[i++]; // 평가됨 (i 증가)
int s1 = sizeof(i++); // 평가되지 않음
int s2 = sizeof(int [++i]); // 평가됨 (i 증가)
int s3 = sizeof(int (*)[++i]); // 알 수 없음

여기서 마지막 예는 컴파일러에 따라 그 결과가 달라짐을 의미한다. VLA의 크기에 따라 포인터의 크기가 달라지는 환경에서는 실행 중에 평가되며, 항상 포인터 크기가 고정되어 있는 환경에서는 번역시에 평가될 수도 있다. 마지막으로 기억할 것은 gcc 등의 컴파일러가 C99 이전부터 확장으로 제공하던 가변 길이 배열은 상세하게 들여다보면 C99에서 제공하는 것과 차이가 있다는 사실이다. 지면의 제한으로 다루지는 못하지만 그 사실만은 꼭 기억해두길 바란다.

매개변수 배열 선언이 static과 형 지정자[Lang][Open]
기본적으로 아래 두 선언은 (프로그램을 보는 사람에게는 주석처럼 어떤 정보를 줄 수 있어도) 컴파일러의 입장에서는 동일한 선언이다.

void func(int a[10]);
void func(int *a);
C99에서는 매개변수에서의 이 무의미한 배열 선언을 변형하여 컴파일러가 최적화에 사용할 수 있는 정보를 제공하도록 했다.

void func(int a[static 10], int b[static 10]);

이 선언은 함수 안에서 두 포인터 a, b가 널 포인터(null pointer)가 아니며 자신이 가리키는 배열에서 최소 10개의 요소에는 접근함을 컴파일러에게 알린다. 컴파일러는 이런 정보를 바탕으로 루프 언롤링(loop unrolling) 같은 최적화를 보다 쉽게 적용할 수 있다. 물론 이는 어디까지나 최적화의 문제이기 때문에 컴파일러가 해당 선언의 “static 10”을 무시해도 무방하다.
또 다른 변화는 매개변수 선언시 포인터에 형 지정자를 적용하는 것과 관련된 문제이다. 지금까지 포인터의 값이 변하지 못하게 하기 위해 const 같은 형 지정자를 적용하고자 하는 경우, 매개변수 선언에 배열 형태의 선언을 사용할 수 없었다. 하지만 C99에서는 다음에서 보는 것처럼 매개변수 선언에 배열 선언을 사용해도 포인터에 형 지정자를 적용할 수 있는 방법을 마련해 주었다. 다음 두 선언은 C99에서 동일한 의미를 갖는다.

void func(int b[const]);
void func(int * const b);

를 통한 type-generic 수학 매크로[Lib][Open]
는 에서 제공하는 각종 수학 함수들을 매크로로 제공한다. 차이가 있다면 의 수학 함수들은 매개변수와 반환값의 데이터형에 따라 서로 다른 함수명으로 구분되어 있지만 의 수학 매크로는 주어지는 인자의 데이터형에 따라 해당하는 수학 함수로 확장된다는 것이다.

#include
float f = 3.14159;
result = sin(f);

예를 들어 이와 같은 경우, 매크로 sin()에 의해 호출되는 함수는 float 형의 인자와 반환값을 갖는 sinf() 함수가 된다. 가 제공하는 주요한 편의는 프로그램을 이식할 때 빛을 발한다. 환경이 바뀌어 호출하는 수학 함수의 종류를 바꾸고자 하는 경우(예를 들면 sinf()에서 sinl()로), 를 사용하면 함수명의 변화 없이 주어지는 인자와 반환값을 저장하는 대상체의 데이터형만 적절히 바꿔주면 된다. 사실 를 보고 C++의 “intrinsic”이나 “overloading”을 떠올리는 사람들이 있을지도 모르겠다. 하지만 위원회는 포트란의 “generic function”을 의 모델로 삼았기 때문에 이름도 type-generic 수학 매크로가 됐다.

UCN와 확장 명칭[Lang][Open]
C99부터는 C 언어가 본격적으로 ISO/IEC 10646을 지원하기 시작한다. 프로그램 소스 내에서 문자열 상수의 확장열(escape sequence)과 비슷한 형태(u 혹은 U로 시작)로 ISO/IEC 10646에 정의되어 있는 문자를 지정해 사용할 수 있도록 허락하고 있다. 물론 외관상 형태가 아름답지는 않기에 프로그램 소스 안에서 UCN(Universal Character Name)을 사용해 ISO/IEC 10646의 문자를 쓰는 사람은 없을 것이다. 하지만 UCN이 갖는 중요한 의미는 UCN이 서로 다른 인코딩을 사용하는 두 환경 사이에서 매개 역할을 할 수 있다는 것이다.
확장 문자셋을 위한 인코딩(A)와 인코딩(B)가 서로 다를지라도, 인코딩(A)를 사용하는 환경에서 C99 컴파일러가 프로그램을 출력할 때 비표준 문자들을 모두 UCN으로 출력해주고, 이를 인코딩(B)가 읽어 지원하는 문자들을 UCN에서 변환해주면 서로 다른 인코딩을 지원하는 환경임에도 큰 문제 없이 비표준 문자를 사용하는 프로그램들이 이식성을 갖는다는 사실을 알 수 있다. 물론 이를 위해서는 각 개발 환경이 UCN을 충분히 지원해주어야 한다.
또한 UCN 도입과 더불어 그동안은 표준 입장에서는 잘못된 프로그램을 컴파일러가 허락해주는 방식으로 지원하던 명칭에 쓰이는 비표준 문자도 이제는 표준에 의해 허락된다. 즉 컴파일러가 허락한다면 다음과 같은 프로그램도 가능해지는 것이다.

typedef int 레코드;
레코드 전체합, 현재;
for (현재 = 0; 현재 < 전체합; 현재++)
물론 이와 같은 프로그램이 완전한 이식성을 갖추지는 못하지만, 컴파일러가 UCN을 적절히 지원해준다면 비록 가독성은 떨어져도 C99 이전과는 달리 이식성을 갖출 방법이 없는 것은 아닌 셈이다.

16진 부동 상수[Lang][Lib][Open]
C 프로그램에서 부동 상수는 항상 10진수 형태였지만, 오래 전부터 많은 환경이 부동 소수 표현을 위해 기수(radix)를 2로 사용해 왔다. 결국 그 표현의 한계로 인해 10진 부동 상수를 사용해 부동 소수 표현의 가수부(significand)를 항상 정확히 명시할 수 있는 방법이 없던 셈이었다. C99에서는 기수가 2 (혹은 2n)인 부동 소수점 표현의 가수부를 더 명확하게 기술할 수 있도록 다음과 같은 형태의 16진 부동 상수를 지원한다.

0x10ff0100p-8f
== 00010000111111110000000100000000(2) * 2-8
이와 같은 16진 부동 상수는 예를 들어 이식성을 갖추며 같은 표준 라이브러리를 구현할 때 유용하게 쓰일 수 있다. 추가로 16진 부동 상수를 출력할 수 있도록 printf() 계열 함수에는 %a와 %A가 새로 추가됐다.

복합 상수[Lang][Open]
복합 상수(compound literal)란 집합체형(배열, 구조체) 혹은 공용체형의 상수를 의미한다. 지금까지는 문자열 상수(문자형의 배열로 취급됨) 같은 특수한 경우를 제외하면, 집합체형나 공용체형은 명칭이 붙지 않은 상수를 갖지 못했다. 하지만 이제는 복합 상수가 그 방법을 제공한다.

void func(struct foo param);
func((struct foo) { 123, 3.14159 });

void func2(struct foo *p);
func2(&(struct foo) { 123, 3.14159 });

int *p = (int [3]) { 1, 2, 3 });
const int *a1 = (const int []) { 1, 2, 3 };
const int *a2 = (const int []) { 1, 2, 3 };

복합 상수와 관련된 몇몇 규칙은 까다로워서 설명에 많은 지면을 필요로 한다. 여기서는 몇 가지 중요한 규칙만을 지적해 보겠다. 우선 ‘literal’을 ‘상수’로 번역하는 관례 때문에 묻힐 수 있는 사실이지만, 문자열 상수(string literal)가 그러하듯 복합 상수 역시 대상체에 해당한다. 따라서 정수 상수 같은 진짜 상수와는 달리 메모리에 존재하며 ‘&’ 연산자를 이용해 주소를 취하는 것도 가능하다. 또한 복합 상수의 데이터형이 const로 한정되어 그 내용이 변하지 않는 경우 같은 내용의 복합 상수는 같은 메모리 공간을 차지할 수도 있다. 즉 예에서 포인터 a1과 a2가 가리키는 메모리 공간이 동일할 수도 있다. 또 하나 유의해야 할 사항은 복합 상수가 메모리를 차지하고 있는 시간이다.


이 두 예의 행동은 완전히 다르다. for 문을 사용한 예에서는 우측에서 for 문에 의해 {...}로 구성된 블록을 드나들 때마다 복합 상수는 생성됐다가 사라지기를 반복한다. 결국 해당 루프가 종료되면 p[]의 각 포인터가 가리키는 메모리 공간은 유효하지 않은 공간이 된다. 참고로 복합 상수는 이전 호에서 설명했던 지정된 초기치를 통해 프로그램을 더 간단하고 직관적으로 만들 수 있다. 다음의 두 예를 비교해보자.


_Pragma 전처리기 연산자[Lang][Open]
전처리기 지시자인 #pragma는 기본적으로 컴파일러가 확장으로 제공하는 여러 기술에 접근할 때 사용한다. 하지만 한 가지 문제점은 #pragma 지시자를 다른 매크로와 섞어 사용하기가 어렵다는 것이다.

#define struct_pack(n) #pragma pack(n)
struct_pack(1)

매크로가 확장된 결과는 전처리기 지시자 역할을 할 수 없다는 규칙에 의해 이 예는 의도대로 작동하지 않는다.

#if defined(A_COMPILER) /* A_COMPILER */
# define pack strict_pack
#else /* B_COMPILER */
# define pack __pack(1)
#endif
#pragma pack

이 경우 #pragma 지시자 뒤의 매크로가 확장된다는 보장이 없어 역시나 의도대로 작동할지 미지수다. 이런 근본적인 문제 해결을 위해 _Pragma 전처리기 연산자가 등장했다. 작동 원리는 다음 예를 보면 쉽게 이해할 수 있을 것이다.

#define struct_pack(n) _Pragma("pack("#n")")
struct_pack(1) /* #pragma pack(1) */

#if defined(A_COMPILER) /* A_COMPILER */
# define pack "strict_pack"
#else /* B_COMPILER */
# define pack "__pack(1)"
#endif
_Pragma(pack) /* #pragma strict_pack 혹은 #pragma __pack(1) */

불 데이터형의 추가[Lang][Lib][Open]
C는 전통적으로 0 혹은 0이 아닌 값으로 참/거짓을 표현하며 보통의 정수형을 사용해 왔다. 이러한 전통은 굳게 자리잡아 오묘하게도 C 프로그래머들의 자부심으로까지 이어지기도 하지만, C99는 C 언어에 불 데이터형(boolean type)을 공식적으로 추가했다. 명칭 충돌을 피하기 위해 불 데이터형은 _Bool이라는 형 지정자를 갖는다(밑줄과 대문자로 시작하는 명칭은 표준에 의해 예약되어 있다). _Bool은 0 혹은 1만을 저장할 수 있는 무부호 정수형으로 산술형과 포인터형을 _Bool형으로 변환하는 경우 0과 비교한 결과(0 혹은 1)가 나온다.

_Bool b, *pb;
b = 0; /* b = 0; */
b = -1; /* b = 1; */
pb = &b; /* pb는 널 포인터가 아님 */
b = pb; /* 따라서 b = 1; */
b = (_Bool) 0.5; /* b = 1; */

예에서 포인터를 대입했을 때의 결과와 부동 상수를 대입했을 때의 결과에 유의하기 바란다. 0.5를 _Bool형이 아닌 정수형으로 변환하는 경우 소수점 이하를 자른 후에 0을 대입하게 된다. 프로그래머가 프로그램에 이라는 헤더를 첨가하는 경우, 다소 기이하게 생긴 명칭 _Bool과 0, 1이라는 값 대신 bool이라는 명칭과 true, false를 사용할 수 있다.

#include
bool b1, b2;
if (func1() && !func2())
b1 = true, b2 = false;

물론 bool, false, true라는 명칭을 다른 용도로 이미 사용 중인 프로그램은 를 첨가하지 않으면 새로 추가된 불 데이터형에 영향을 받지 않는다.

__func__ 기정의 명칭[Lang][Open]
프로그램을 디버깅하는 과정에서 빈번하게 필요한 기술 중 하나가 현재 실행 중인 함수의 명칭을 얻는 것이다. 디버깅 메시지에 함수의 명칭을 출력해 문제가 있는 부분을 좀 더 쉽게 찾기 위해서이다. 많은 컴파일러는 이를 위해 주로 현재 실행 중인 함수명으로 확장되는 매크로(gcc의 경우 __FUNCTION__)를 제공하고 있지만, C99는 같은 목적을 위해 매크로명이 아닌 기정의 명칭을 제공하고 있다. 즉, 함수 시작을 나타내는 { 이후에 다음과 같은 형태의 선언이 존재하는 것처럼 작동한다.
static const char __func__[] = "함수명";

주의할 점은 __func__은 매크로가 아니기에 다음의 확장 결과는 의도하지 않은 "__func__"가 된다.

#define str(x) #x
#define xstr(x) str(x)
xstr(__func__);

enum 형의 선언에서 여분의 쉼표 허락[Lang][Open]
C 언어를 표준화하는 과정에서 중요하게 생각했던 사실 중 하나는, C 프로그램 소스가 사람이 아닌 다른 프로그램에 의해 작성되는 경우도 고려해야 한다는 것이었다. 대표적으로 컴파일러가 C 프로그램을 인식하는 방법이나 C 언어가 지원하는(수직탭 같은) 특수한 문자들에 그러한 생각이 반영되어 있다. 또 다른 예는 배열이나 구조체, 공용체의 초기치를 제공할 때 항상 여분의 쉼표를 쓸 수 있다는 것이었다. 하지만 공용체는(비록 초기치는 아닐지라도) 이와 비슷한 선언 형태를 가지고 있지만 여분의 쉼표가 허락되지 않았다. C99에서는 이러한 불균형을 없애기 위해 열거형의 선언에도 다음과 같이 여분의 쉼표를 쓸 수 있도록 허락하고 있다.

enum { RED=1, GREEN, BLUE, PURPLE, } color;

주의할 점은 이와 같은 선언에서 여분의 쉼표가 특별한 의미를 갖지 않는다는 사실이다. 즉 마지막 쉼표가 없는 선언과 아무런 차이가 없다.

좌변값으로 제한되지 않는 배열의 포인터로의 변환[Lang][Open][C++]
몇 가지 예외적인 경우를 제외하면 C 언어에서 수식 안의 배열은 포인터로 변환된다. 단, C99 이전에는 이 규칙이 배열이 좌변값(lvalue)인 경우에만 적용되었다. 물론 일반적으로 좌변값이 아닌 배열을 만들기가 쉬운 것은 아니다. 다음과 같은 경우를 보자.

typedef struct { int x[10]; } foo;
foo func()
{
foo r;
r.x[2]=3;
return r;
}

함수의 반환값은 좌변값일 수 없기에 이렇게 구조체 멤버로 포함되어 반환되는 배열 역시 좌변값일 수 없다. C99 이전에는 이와 같은 경우에 대한 고려를 하지 못했고, 결국 아래의 첫 수식은 허락되고 두 번째 수식은 허락되지 않는 결과가 생겼다.

sizeof(foo().x); /* C90/C99 모두 가능 */
printf("%d ",foo().x[2]); /* C990 에서만 가능 */

하지만 C99에서는 이를 다루기 위해 규칙을 수정하였다. 사실, 이 변화는 언어에 새로운 기술을 추가한 것이라기보다는 기존 언어가 예상치 못한 경우를 기술하기 위해 언어의 결점을 보완한 것으로 볼 수 있다.

C99 이후의 C 언어
C 표준화위원회는 C99를 발표한 이후에도 C99와 관련된 여러 문제를 검토하는 작업을 꾸준히 진행하고 있으며, C90의 AMD1처럼 C 언어의 능력을 확장하기 위해 크게 세 가지 형태의 기술 보고서(TR)에 대한 작업을 진행 중이다.
첫 번째는 C 언어가 가장 큰 활약을 보이고 있는 임베디드 분야를 위한 C 언어의 확장이다. 임베디드 분야에서 가장 많이 사용되는 언어가 C 언어인만큼 임베디드 분야에서 C 언어가 보다 이식성을 갖고 효율적으로 사용될 수 있도록 C 언어를 확장하는 작업이 거의 마무리 중이다(TR18037).
두 번째는 C 언어가 ISO/IEC 10646을 보다 적극적으로 지원할 수 있도록 하기 위한 작업이다. 처음 소개했던 확장 문자나 멀티바이트 문자의 일반화된 개념을 뛰어넘어 더욱 명확한 방법으로 ISO/IEC 10646을 지원하기 위해 새로운 문자형을 도입하는 등의 확장을 진행하고 있다(TR19769).
마지막은 십진 부동소수점 연산에 대한 지원을 위한 작업이다. 이미 설명했듯이 일반 컴퓨터에서는 이진 부동소수점 연산이 대세를 이루고 있지만, 아직도 특수한 분야에서는 십진 부동소수점 연산을 사용하고 있다. C 언어는 겉으로는 부족함이 없어 보여도 십진 부동소수점 연산 지원에 몇몇 심각한 문제를 안고 있다. 이를 해결하기 위한 확장이 진행 중이다(TR24732).
이렇게 이루어지는 작업은 추후 표준의 확장이나 새로운 표준에서 다루는 형식으로 우리들에게 찾아올 것이다. 또 하나 C 언어의 미래에 대해 이야기할 때 강조하고 싶은 부분은 C++와의 관계이다. 현재 C와 C++는 서로 다른 위원회에 의해 표준화가 진행 중이다. 두 언어가 상당히 유사한 부분을 담고 있고 서로 영향을 주고받는다는 사실을 부인할 수는 없지만, 두 언어가 서로 다른 사용자층과 사용 환경을 바탕으로 발전해가고 있기 때문에 시간이 지날수록 두 언어의 공통점은 그야말로 “역사적 우연성”에 기인한 것으로 볼 수 있을 것이다. 따라서 두 언어 중 하나가 다른 하나의 다른 버전이라든가 나중 버전이라는 식의 해석은 그리 합리적이라고 볼 수 없다. 시간이 지나면 지날수록 두 언어를 독립된 언어로 보는 시각이 지배적이 될 것이다.

변해가는 C 언어를 위해
이제 C99의 새 기술을 소개하는 3개월간의 여정도 끝났다. 부족한 글 솜씨로 작은 지면에 많은 내용을 담고자 애쓰다보니 그리 친절한 기사가 되지 못한 것 같아 안타까움이 많이 남는다. 이곳에서 다룬 내용이 C99의 다양한 새 기술을 철저히 소개하고 있지는 못하지만 앞으로 변해가는 C 언어의 모습을 접할 때 조금이라도 도움이 되기를 바란다. 늘 그렇듯 이번 원고에 대한 어떠한 지적이나 질문, 기타 의견도 환영이며, 필자의 메일이나 홈페이지 게시판을 통해 알려주면 자세한 답변을 드릴 것을 약속한다. 참고로 이 연재는 두 차례에 걸쳐 KLDP 세미나를 통해 발표했던 내용을 보강 정리한 것이다. 자세한 내용은 http://doc.kldp.org/wiki.php/KLDPConf/20031011과 http://doc.kldp.org/wiki.php/KLDPConf/20040118에서 만날 수 있다.

정리 | 김세미 | semsem@korea.cnet.com

[ 소개하지 못한 C99의 새 기술 ]
연재의 첫회부터 지금까지 소개한 기술 이외에도 C99는 다수의 새로운 기술을 제공하지만 부족한 지면으로 나머지 기술들은 간단히 언급만하고 넘어가도록 하겠다. 세세한 내용을 언급하기보다는 의도와 흐름을 짚을 수 있도록 설명하겠다.

◆인라인 함수 추가와 반복문 및 선택문의 새로운 블럭 통용 범위[Lang][Open][C++]
C99는 C++에서 사용되던 개념인 인라인 함수(inline function)와 for 문의 첫 번째 수식이 수식뿐 아니라 선언이 될 수 있는 구조를 받아들였다. for 문과 관련된 변화는 다음 예로 간단히 설명될 수 있다.
for (int i = 0; i < LIMITS; i++) /* ... */
인라인 함수는 함수 형태의 매크로가 지니는 인자 수식의 부작용 문제 같은 한계를 극복하기 위해 도입된 것으로 유의할 점은 inline이라는 예약어가 register와 마찬가지로 강제성을 갖지 못한다는 것이다.
◆ signed/unsigned long long int형의 추가[Lang][Lib][Open]
C99에는 최소 64비트의 크기를 갖는 long long int형이 추가됐고, 이와 더불어 정수 상수의 데이터형을 결정하는 규칙, 정수 진급(integer promotion)과 관련된 규칙에도 변화가 생겼다. 물론 라이브러리 쪽에서도 long long int 형을 지원하기 위한 변화(예를 들면, printf()에 %lld 등을 추가)가 뒤따랐다. 이는 C99 이전에도 컴파일러 확장을 통해 일반적으로 지원되던 기술이다.
◆ 와 를 통해 제공되는 확장 정수형과 라이브러리[Lib]
일부 컴파일러는 확장을 통해 C 표준이 요구하는 일반적인 정수형 이외에 다양한 특성(정확한 비트수를 갖는 정수형, 가장 큰 크기를 갖는 정수형 등)을 갖는 비표준 정수형을 제공하고 또 적지 않은 프로그램이 이를 사용하고 있지만, C99 전까지는 이에 접근할 수 있는 표준적인 방법이 존재하지 않았다. C99에서는 두 표준 헤더인 , 를 통해 그와 같은 컴파일러가 제공하는 확장 정수형이 존재하는 경우 이를 보다 손쉽게 활용할 수 있도록 배려하고 있다.
◆ IEEE 754 연산 지원과 복소수 지원, 표준 pragma 제공[Lang][Lib]
C90/C95는 C 언어의 부동소수점 모델을 매우 너그럽게 풀어주는 수준에서 부동소수점 연산에 대한 지원을 멈췄으나 C99는 해당 환경이 지원하는 경우 IEEE 754 연산(IEC 559 연산)을 보다 잘 지원하고 있다. 또한 부동소수 환경에 대한 추가적인 정보와 접근을 허락하기 위해 라는 헤더가 새로 추가됐고, 기존의 가 확장됐다. 또한 함수는 다양한 수학 함수를 추가로 제공하며, 를 통해서 복소수에 대한 지원도 받을 수 있다. 부동소수 연산에 대한 지원의 일부분으로 #pragma STDC...와 같은 형태를 통해 사용할 수 있는 표준 pragma도 제공하고 있다.
◆ 전처리기 연산을 intmax_t/uintmax_t로 수행[Lang][Open]
전처리기 연산은 실행 중이 아닌 번역시에 수행된다는 특징이 있다. C90에서는 번역 환경에 몇 가지 제약을 가하면서 번역 환경을 기준으로 전처리기 연산이 수행되도록 규정했으나, C99에서는 실행 환경의 가장 큰 정수형인 intmax_t/uintmax_t를 사용해 실행 환경을 특성에 따라 수행되도록 요구하고 있다. 단, 어떠한 경우에도 전처리기 연산을 통해 실행 환경에 대한 검사를 시도하는 것은 위험하다.
◆ 기정의 매크로 추가 제공[Lang]
환경에 대한 구분이나 해당 환경이 지원하는 기술에 대한 검사를 수행할 수 있도록 __STDC_HOSTED__, __STDC_IEC_559__, __STDC_IEC_559_COMPLEX__, __STDC_ISO_10646__ 같은 추가적인 기정의 매크로를 제공하고 있다.
◆ 이식성 있는 헤더명에 대한 제한 완화[Lang][Open]
컴퓨팅 환경이 더 넉넉해졌기에 이식성을 갖는 헤더명에 대한 제한이 완화됐다. 물론 표준은 지극히 보수적인 태도를 취할 필요가 있기 때문에 이 제한은 실제 다수의 환경이 허락하는 것에 비하면 지극히 엄격하다.
◆ strftime() 함수의 추가 변환 지정자[Lib][Open]
날짜와 시간 표현에 대한 국제 표준인 ISO 8601을 지원하기 위해 을 통해 제공되는 strftime() 함수의 변환 지정자가 대폭 추가됐다.
◆ 바이너리 파일의 시작 위치에서 ungetc() 호출 금지[Lib][Open]
바이너리 파일의 시작 위치에서 처리된 입력을 되돌리기 위한 ungetc() 함수를 호출하는 것이 사실상 금지됐다. C99에서 사용을 억제한 후에 차기 표준에서 금지할 계획이다.
◆ 와 에 vscanf() 계열 함수 추가[Lib][Open]
printf() 계열과 scanf() 계열 함수의 차이를 줄이기 위해 vscanf() 계열 함수를 표준 라이브러리에 추가했다.

===============================================================================

전웅 | woong@icu.ac.kr http://www.woong.org

현재 한국정보통신대학교(ICU) 석사 과정에 재학 중이며, 컴파일러와 병렬처리 및 각종 표준에 깊은 관심을 가지고 있다.

반응형