18. Declarations(2)

하모씨·2021년 12월 3일
0

KNKsummary

목록 보기
23/23

4. Declarators

declarator는 * 심볼이 앞에 붙거나 []()가 뒤에 붙을 수 있는 identifier(선언되는 변수나 함수의 이름)로 구성되어있다. *, [], ()를 결합하는 것으로, declarator가 복잡성을 가지도록 만들 수 있다.
더 복잡한 declarator를 보기 전에, 이전 chapter에서 본 declarator를 복습해보자. 가장 간단한 경우에, declarator는 아래의 예시의 i와 같은 단순한 identifier이다.

int i;

declarator는 *, [], ()또한 포함할 수 있다.

  • *으로 시작하는 declarator는 포인터를 나타낸다.
int *p;
  • []로 끝나는 declarator는 배열을 나타낸다.
int a[10];

만약 배열이 parameter거나, initializer를 가졌거나, storage class가 extern이라면 대괄호 내부를 비운채로 둘 수 있다.

extern int a[];

a가 프로그램의 어딘가에 정의되었기 때문에, 컴파일러는 여기에 대한 길이를 알 필요가 없다. (multidimensional array의 경우에, 첫번째 대괄호 내부만 비울 수 있다.) C99는 array parameter의 선언 내부의 대괄호 사이에 무엇이 들어갈지에 대해 2가지 선택지를 추가적으로 제공한다. 하나의 선택지는 키워드 static인데, 배열의 최소 길이를 명시하는 표현식이 뒤에 나온다. 두번째는 * 심볼인데, 가변 길이 배열 argument를 나타내기 위해 함수 prototype 내부에서 사용된다.

  • ()로 끝나는 declarator는 함수를 나타낸다.
int abs(int i);
void swap(int *a, int *b);
int find_largest(int a[], int n);

C는 함수 선언 내부에서 parameter 이름이 생략되는 것을 허용한다.

int abs(int);
void swap(int *, int *);
int find_largest(int [], int);

괄호 내부를 빈채로 둘 수도 있다.

int abs();
void swap();
int find_largest();

위의 3개 선언은 abs, swap, find_largest 함수의 return type은 명시했지만 argument에 대한 정보를 하나도 제공하지 않았다. 괄호 사이에 void를 넣어 parameter가 없다고 알리는 것과 괄호를 아예 비우는 것과는 동일하지 않다. 괄호를 비우는 함수 선언의 스타일은 대부분 사라졌다. C89에 도입된 prototype style에 비해서 더 안좋은데, 함수가 올바른 argument로 호출되는지 컴파일러가 체크하지 못하도록 하기 때문이다.

모든 declarator가 위의 것들처럼 간단하다면, C 프로그래밍은 간단했을 것이다.
불행하게도, 실제 프로그램에서 declarator는 *, [], ()의 결합이 많이 일어난다. 이런 결합들에 대한 예시는 많이 보았다.

int *ap[10];

위의 선언은 정수를 가리키는 10개의 포인터를 가지는 배열을 선언하는 것이다.

float *fp(float);

위의 선언은 float argument를 가지고, float를 가리키는 포인터를 반환하는 함수를 선언한다.
그리고 Section 17.7에서 우리가 배운 것이 있다.

void (*pf)(int);

위의 선언은 int argument를 가지고 void를 return type으로 하는 함수를 가리키는 포인터를 선언한다.

Deciphering Complex Declarations

지금까지 declarator를 이해하는 것에 많은 문제가 있지는 않았다. 그렇다면 아래와 같은 선언에 있는 declarator는 어떨까?

int *(*x[10])(void);

이 declarator는 *, [], ()가 모두 결합되었는데, 그래서 x가 포인터인지 배열인지 함수인지 명백하지가 않다.
다행히도, 어떠한 선언이라도 이해하게 해주는 간단한 2개의 규칙이 있는데 얼마나 뒤얽켜있든 문제가 되지 않는다.

  • 항상 declarators를 안팎으로 뒤집어 읽어라. : 다른말로 말하면, 선언된 identifier를 확인하고 거기서부터 선언을 해독하기 시작해야 한다.

  • 선택지가 있을 때, *보다는 []()를 먼저 보아라. : identifier의 앞에 *가 있고 뒤에 []가 있다면, identifier는 포인터가 아닌 배열을 나타내는 것이다. 마찬가지로, identifier의 앞에 *가 있고 ()가 뒤에 있다면, identifier는 포인터가 아닌 함수를 나타내는 것이다. (당연하게도, 우리는 항상 괄호를 사용하여 []()*보다 더 높은 우선순위를 가지도록 덮어쓸 수 있다.)

위의 규칙들을 간단한 예시에 적용시켜보자.

int *ap[10];

위의 선언에서 identifier는 ap이다. *ap의 앞에 있고 []가 뒤에 있기 때문에, []를 보는 것으로 ap가 포인터의 배열이라는 것을 알 수 있다.

float *fp(float);

위의 선언에서 identifier는 fp이다. *fp의 앞에 있고 ()가 뒤에 있기 때문에, ()를 보는 것으로 fp가 함수이고, 포인터를 반환한다는 것을 알 수 있다.

void (*pf)(int);

위의 선언은 조금 까다롭다. *pf가 괄호로 둘러싸여있기 때문에, pf는 반드시 포인터이다. 그러나 (*pf)뒤에 (int)가 오는데, 그렇기 때문에 pf는 반드시 int argument를 가지는 함수를 가리키는 포인터이다. void는 이 함수의 return type을 나타낸다.
마지막 예시가 보여주는 것처럼, 복잡한 declarator를 이해하는 것은 identifier의 한쪽에서 다른 한쪽으로 왔다갔다(zigzagging)를 반복할 수도 있다.

이 왔다갔다하는 기법을 사용하여 처음에 주어진 선언을 해독해보자.

int *(*x[10])(void);

첫번째로, (x)로 선언된 identifier를 찾았다. x의 앞에 *가 있고 뒤에 []가 있다. []*보다 높은 우선순위이기 때문에 오른쪽을 보는 것으로 x가 배열이라는 것을 알 수 있다. 다음으로, 배열 내부의 요소의 자료형을 찾기 위해 왼쪽을 보는 것으로 포인터라는 것을 알 수 있다. 다음으로, 오른쪽으로 가서 포인터가 가리키는 데이터의 종류가 무엇인지 확인할 수 있는데, 함수는 argument를 가지지 않는다. 마지막으로 왼쪽으로 가서 함수가 반환하는 것을 볼 수 있다. int에 대한 포인터를 반환한다. 그림으로 이 과정을 표현하면 아래와 비슷할 것이다.

C 선언을 마스터하는 것은 시간과 연습이 필요하다. 좋은 소식은 아래의 특정한 것들은 C에서 선언될 수 없다는 것이다.
1. 함수는 배열을 반환할 수 없다.

int f(int)[];    /*** WRONG ***/

함수는 함수를 반환할 수 없다.

int g(int)(int); /*** WRONG ***/

함수의 배열도 불가능하다.

int a[10](int);  /*** WRONG ***/

각각의 경우에, 우리는 포인터를 사용하여 원하는 효과를 얻을 수 있다. 함수는 배열을 반환하지 못하지만, 배열을 가리키는 포인터는 반환할 수 있다. 함수는 함수를 반환하지 못하지만, 함수를 가리키는 포인터는 반환할 수 있다. 함수의 배열은 허용되지 않지만, 함수를 가리키는 포인터를 가지는 배열은 가능하다.(Section 17.7의 예시에 이러한 배열이 있다.)

Using Type Definitions to Simplify Declarations

어떤 프로그래머들은 복잡한 선언들을 간단하게 하기 위해 type definition을 사용한다. 이 Section의 처음에 나왔던 x의 선언을 생각해보자.

int *(*x[10])(void);

x의 자료형을 더 이해하기 쉽게 만들기 위해, 일련의 자료형의 정의를 사용할 수 있다.

typedef int *Fcn(void);
typedef Fcn *Fcn_ptr;
typedef Fcn_ptr Fcn_ptr_array[10];
Fcn_ptr_array x;

만약 이 행들을 거꾸로 읽으면, xFcn_ptr_array 자료형을 가지고, Fcn_ptr_arrayFcn_ptr 값의 배열이고, Fcn_ptr이 자료형 Fcn을 가리키는 포인터이고, Fcn은 argument가 없고 int 값을 가리키는 포인터를 반환한다는 것을 볼 수 있다.

5. Initializers

편의를 위해, C언어는 변수를 선언할 때 변수에 대한 초기값을 명시하는 것을 허용한다. 변수를 초기화하기 위해, declarator이후에 = 심볼을 쓰고, 그 후 initializer를 작성한다. (= 심볼을 대입과 똑같이 보면 안된다. 초기화는 대입과 다르다.)
이전 chapter들에서 다양한 종류의 initializer를 보아왔다. 간단한 변수의 initializer는 변수와 같은 자료형의 표현식이다.

int i = 5 / 2;    /* i is initially 2 */

만약 자료형이 일치하지 않는다면, C언어는 대입과 동일한 규칙으로 initializer를 변환한다.

int j = 5.5;    /* converted to 5 */

포인터 변수에 대한 initializer는 반드시 변수와 동일한 자료형의 포인터 표현식이거나 void * 자료형이여야 한다.

int *p = &i;

배열, 구조체, 공용체에 대한 initializer는 일반적으로 중괄호로 둘러싸인 일련의 값이다.

int a[5] = {1, 2, 3, 4, 5};

C99에서는, 중괄호로 둘러싸인 initializer가 다른 형태를 가질 수도 있는데, designated initializer덕분이다.

initializer가 따르는 추가적인 규칙이 있다.

  • static storage duration을 가지는 변수에 대한 initializer는 반드시 상수여야 한다.
#define FIRST 1
#define LAST 100

static int i = LAST - FIRST + 1;

LASTFIRST가 macro이기 때문에, 컴파일러는 i(100 - 1 + 1 = 100)의 초기값을 계산할 수 있다. 만약 LASTFIRST가 변수라면, 이 initialzer는 규칙에 어긋난다.

  • 만약 변수가 automatic storage duration을 가진다면, initializer는 상수일 필요가 없다.
int f(int n)
{
    int last = n - 1;
    ...
}
  • 배열, 구조체, 공용체에 대한 중괄호로 둘러싸인 initializer는 상수 표현식만 포함해야 하고, 절대로 변수나 함수 호출을 포함하면 안된다.
#define N 2

int powers[5] = {1, N, N * N, N * N * N, N * N * N * N};

N이 상수이기 때문에, powers에 대한 initializer는 규칙에 어긋나지 않는다. N이 변수라면 프로그램은 컴파일되지 않을 것이다. C99에서는 이 제한이 오직 static stroage duration을 가진 배열에만 적용된다.

  • automatic 구조체나 공용체에 대한 initializer는 또다른 구조체나 공용체가 될 수 있다.
void g(struct part part1)
{
    struct part part2 = part1;
    ...
}

initializer는 변수나 parameter 이름일 필요는 없지만 적절한 자료형의 표현식일 필요는 있다. 예를 들어, part2의 initializer는 struct part * 자료형을 가지는 p*p가 될 수 있고, part 구조체를 반환하는 함수 ff(part1)이 될 수 있다.

Uninitialized Variables

이전 chapter들에서, 초기화되지 않은 변수들은 정의되지 않은 값을 가진다고 했었다. 이것이 항상 맞는것은 아니다. 변수의 초기값은 storage duration에 따라 다르기 때문이다.

  • automatic storage duration을 갖는 변수는 기본(default) 초기화 값이 없다. automatic 변수의 초기값은 예측될 수 없고, 변수가 존재하게 되었을 때마다 다를 수도 있다.

  • static storage duration을 가지는 변수는 값 0을 기본으로 가진다. calloc에 의해 메모리가 할당되고 0bit로 설정되는 것과 다르게, static 변수는 자료형에 따라 올바르게 초기화된다. 정수형 변수는 0으로 초기화되고, 부동소수점 변수는 0.0으로 초기화되고, 포인터 변수는 null 포인터로 초기화된다.

static 변수들이 0이 되는 것이 보장되었다고 하더라도 이 변수들에 대해 initializer를 제공하는 것이 좋다. 만약 명시적으로 초기화되지 않은 변수들에 프로그램이 접근했다면, 누군가 이 프로그램을 나중에 읽을 때 변수가 0인지, 아니면 프로그램의 나중 부분에 대입을 통해 초기화되는지 쉽게 알아내기가 힘들다.

6. Inline Functions (C99)

C99 함수 선언은 C89에 존재하지 않는 추가적인 선택지를 제공한다. C99 함수 선언에서는 키워드 inline을 가질 수 있다. 이 키워드는 storage class, type qualifer, type specifier와 구분되는 새로운 유형의 선언이다. inline의 효과를 이해하기 위해, 함수 호출과 반환의 과정을 조정하기 위해 C 컴파일러에 의해 생성되는 machine instruction을 시각화해볼 필요가 있다.
machine level에서는, 몇몇 instruction이 호출을 준비하기 위해 실행이 되어야할 필요가 있고, 호출은 자체적으로 함수 내부의 첫번째 instruction으로 건너뛸(jumping) 필요가 있고, 함수가 실행하기 시작할 때 함수 자체에 의해 추가적인 instruction들이 실행될 수 있다. 만약 함수가 argument를 가졌다면, argument들은 복사될 필요가 있을 것이다(C언어는 argument를 값으로 전달하기 때문에). 함수를 반환하는 것은 호출된 함수의 부분과 그것을 호출한 것 두 개의 노력의 총량과 비슷한 양을 필요로 한다. 누적되는 작업은 "오버헤드(overhead)"로 불리는 함수의 호출과 반환을 필요로 하는데, 왜냐하면 함수가 실제로 해야할 것보다 더 많은 작업을 하기 때문이다. 함수 호출의 오버헤드가 프로그램을 많이 느리게 만들지는 않지만 함수 호출이 100만번, 10억번씩 일어날 때, 오래되고 더 느린 프로세서에서 사용할때(임베디드 시스템같은 경우일수도 있다), 프로그램이 아주 엄격한 데드라인을 만족시켜야할 때(real-time system)와 같은 특정한 상황에서는 프로그램을 많이 느리게 할 것이다.
C89에서는 함수 호출의 오버헤드를 피하기 위한 방법은 parameterized macro를 사용하는 방법밖에 없었다. 그렇지만 parameterized macro는 특정한 약점을 가지고 있다. C99는 이 문제에 대해 더 좋은 해결책을 제시한다. 바로 inline function을 만드는 것이다. "inline"은 각각의 함수의 호출을 함수에 대한 machine instruction으로 대체하도록하는 구현 전략(implemetation strategy)을 제시한다. 이 기법은 컴파일된 프로그램의 크기가 조금 커지게 하지만, 함수 호출의 오버헤드를 줄일 수 있도록 해준다.
그러나 inline으로 선언된 함수는 실제로 컴파일러에게 함수를 "inline"하라도 강제하지는 않는다. inline은 함수가 호출되었을 때 inline 확장을 수행하는 것으로 단순히 컴파일러가 함수의 호출을 가능한 빠르게 할 수 있도록 시도했으면 좋겠다는 제안일 뿐이다. 컴파일러는 이 제안을 무시할 수 있다. 이러한 관점에서, inlineregisterrestrict 키워드와 비슷한데, 컴파일러가 프로그램의 성능을 증가시키기 위해 사용할 수 있지만 이것을 무시할 수도 있다.

Inline Definitions

inline 함수는 declaration specifier로써 키워드 inline을 가진다.

inline double average(double a, double b)
{
    return (a + b) / 2;
}

조금 복잡해질만한 것이 있다. average는 external linkage 갖기 때문에 다른 소스파일에서 average의 호출을 할 수 있다. 그러나 average의 정의는 컴파일러에 의해 외부에서 정의되는 것이 고려되지 않기 때문에 average를 다른 파일에서 호출하는 시도는 에러로써 여겨질 수도 있다.
이 에러를 피하기 위해 2가지 방법이 있다. 하나의 선택지는 함수 정의에 static을 추가하는 것이다.

static inline double average(double a, double b)
{
    return (a + b) / 2;
}

average는 이제 internal linkage를 가지기 때문에 다른 파일에서 호출될 수 없다. 다른 파일은 위의 정의와 동일하던지 다르던지간에 자기들만의 average 정의를 가질 수 있다.

또다른 선택지는 또다른 파일에서 호출을 허용하기 위해 average에 대한 external 정의를 제공하는 것이다. 이를 위한 한가지 방법은 두번째 average함수를 작성하는 것이다(inline 없이). 그리고 다른 소스파일에 두번째 정의를 넣는 것이다. 이렇게 하는 것은 규칙에 어긋나지는 않지만 동일한 함수의 두 version을 가지는 것은 좋지 않다. 왜냐하면 프로그램이 실행되었을 때 동일하게 남아있으리란 보장을 하지 못하기 때문이다.
아래에 더 좋은 접근이 있다. average의 inline 정의를 헤더파일에 넣는 것이다.(이름을 average.h라고 하자)

#ifndef AVERAGE_H
#define AVERAGE_H

inline double average(double a, double b)
{
    return (a + b) / 2;
}

#endif

다음으로, 일치하는 소스파일인 average.c를 작성하자.

#include "average.h"

extern double average(double a, double b);

이제, average 함수의 호출을 필요로 하는 어떤 파일이라도 간단하게 average inline 정의를 가진 average.h를 포함할 수 있다. average.c 파일은 extern 키워드를 사용한 average에 대한 prototype을 가지는데, 이는 average.h에 포함된 average의 정의가 average.c 내부의 external 정의로 처리되도록 한다.
C99의 일반적인 룰에 따르면 만약 특정한 파일 내부의 함수의 모든 top-level 선언이 extern이 아닌 inline을 포함한다면, 그 파일 내부의 함수의 정의는 inline이다. 만약 프로그램(inline 정의를 가지는 파일을 포함하여)의 어딘가에서 함수가 사용되었다면, 함수의 external 정의가 어떠한 다른 파일에 의해 제공되어야할 필요가 있을 것이다. 함수가 호출되었을 때, 컴파일러는 일반적인 호출을 수행하거나(함수의 external 정의를 사용하여) inline 확장을 수행(함수의 inline 정의를 사용하여)할 것이다. 컴파일러가 어떤 방법을 선택할지 알 방법이 없기 때문에 두 개의 정의가 동일하게 하는 것은 아주 중요하다. average.haverage.c 파일을 사용하여 논의했던 이 기법은 정의가 같다는 것을 보장한다

Restrictions on Inline Functions

위의 방식으로 구현된 inline 함수가 일반적인 함수와 많이 다르기 때문에, inline 함수는 다른 규칙과 제한에 따른다. static storage duration을 가지는 변수는 특히 external linkage를 갖는 inline 함수에서 문제가 된다. 결과적으로, C99에서는 external linkage를 갖는 inline함수에 아래와 같은 제한을 둔다(internal linkage에는 아래의 제한이 없다).

  • 함수는 수정가능한 static 변수를 정의해서는 안된다.
  • 함수는 internal linkage를 갖는 변수에 대한 참조를 가지면 안된다.

이러한 함수는 static이나 const를 갖는 변수를 정의하는 것이 허용되었지만, 함수의 각 inline 정의는 변수의 복사본만 만들 수 있다.

Using Inline Functions with GCC

GCC를 포함한 몇몇 컴파일러는 C99 표준보다 앞선 inline 함수들을 지원한다. 결과적으로, inline 함수를 사용하는 것에 대한 규칙은 표준과는 많이 다를 수 있다. 특히, 이전에 서술되었던 것은(average.haverage.c 파일을 사용한 것) 이 컴파일러에서는 작동하지 않을 수 있다. GCC의 4.3 Version은 C99표준에 서술된 방법으로 inline 함수를 지원한다(이 책을 쓸 당시에는 4.3 Version이 없었다).
GCC의 version과 상관없이 staticinline을 둘다 명시한 함수는 잘 작동한다. 이 전략은 C99에서 또한 규칙에 어긋나지 않는데, 그래서 가장 안전한 방법이다. static inline함수는 단일 파일이나 헤더파일에 위치하여 함수를 호출하기를 원하는 다른 소스파일에 포함되어 사용될 수 있다.
여러 개의 파일 중 inline 함수를 공유하는 또다른 방법은 C99와는 충돌하지만 오래된 version의 GCC를 사용하여 작업하는 것이다. 이 기법은 헤더파일에 함수의 정의를 넣고 함수가 externinline을 둘 다 명시하도록 하고, 그 후 함수의 호출을 가지는 어떠한 소스파일에 헤더파일을 포함시키도록 한다. 정의의 두번째 복사본(externinline이 없는)는 소스파일 중 하나에 위치한다.(만약 컴파일러가 "inline"함수를 어떠한 이유로 사용하지 않을 때 우리는 정의를 가지고 있어야 한다.)
GCC에서는 최적화가 -O 커맨드 라인 옵션을 통해 요청되었을 때만 함수가 "inlined"된다.



Others


왜 선택 구문과 반복 구문(그리고 이 구문들 "내부의" 구문)은 C99에서 block으로 처리되는가?

다소 놀라울 수 있는 이 규칙은, 복합 구문이 선택 구문과 반복 구문에서 사용되었을 때 발생하는 문제에서 비롯되었다. 이 문제는 복합 리터럴의 storage duration과 관련되었기 때문에, 여기에 대한 것을 먼저 논의해보자.
C99 표준은 만약 복합 리터럴이 함수 body의 바깥에서 발생하는 경우, 복합 리터럴을 통해 나타나는 object는 static storage duration을 갖는다고 서술한다. 그렇지 않다면, automatic storage duration을 가지게 된다. 결과적으로 object가 차지하는 메모리는 복합 리티럴이 나타나는 블록의 끝에서 할당해제된다. 아래의 함수는 복합 리터럴을 사용하여 만들어진 point 구조체를 반환한다.

struct point create_point(int x, int y)
{
    return (struct point) {x, y};
}

이 함수는 올바르게 작동하는데, 복합 리터럴에 의해 생성된 object가 함수가 반환될 때 복사되기 때문이다. 원래의 object는 더이상 존재하지 않지만, 복사본이 남을 것이다. 이제 함수를 조금 바꿨다고 생각해보자.

struct point *create_point(int x, int y)
{
    return &(struct point) {x, y};
}

이 version의 create_point는 undefined behavior인데, 왜냐하면 automatic storage duration을 갖는 object를 가리키는 포인터를 반환하고, 함수는 더이상 존재하지 않는 것을 반환하기 때문이다.
그러면 문제의 시작점으로 다시 돌아가보자. 왜 선택구문과 반복구문이 block으로 처리가 되는가? 아래의 예시를 생각해보자.

/* Example 1 - if statement without braces */

double *coefficients, value;

if (polynomial_selected == 1)
    coefficients = (double[3]) {1.5, -3.0, 6.0};
else
    coefficients = (double[3]) {4.5, 1.0, -3.5};
value = evaluate_polynomial(coefficients);

이 프로그램의 조각은 명백하게 의도된 대로 작동한다. coefficients는 복합 리터럴에 의해 생성된 2개의 object 중 하나를 가리킬 것이고, 이 object는 evaluate_polynomial이 호출되었을 때에도 존재할 것이다. "내부" 구문에 중괄호를 넣었을 때 어떤 일이 일어나는지 생각해보자.

/* Example 2 - if statement with braces */

double *coefficients, value;

if (polynomial_selected == 1)
{
    coefficients = (double[3]) {1.5, -3.0, 6.0};
}
else
{
    coefficients = (double[3]) {4.5, 1.0, -3.5};
}
value = evaluate_polynomial(coefficients);

이제 문제가 생긴다. 각각의 복합 리터럴은 object를 만들지만, 이 object는 리터럴이 나타난 중괄호로 둘러싸인 구문 내에서만 존재한다. evaluate_polynomial이 호출되었을 때, coefficients는 더이상 존재하지 않는 object를 가리킨다. 결과적으로 undefined behavior이다. C99의 제작자는 이 상황을 좋아하지 않았는데, 왜냐하면 프로그래머가 if 구문에 중괄호를 하나 추가한 것으로 undefined behavior가 일어날 것이라고 예측하는 것이 어려웠기 때문이다. 이 문제를 피하기 위해, C99의 제작자들은 내부 구문 또한 block으로써 처리되도록 결정하였다. 결과적으로, Example 1과 Example 2는 동일해졌고, 둘다 undefined behavior의 결과가 나올 것이다.
선택 구문과 반복 구문의 표현식을 제어하는 것의 일부가 복합 리터럴일 때 비슷한 문제가 발생할 수 있다. 이러한 이유로, 각 전체 선택 구문과 반복 구문 또한 block으로써 처리된다(마치 보이지 않는 중괄호가 전체 구문을 둘러싼 것처럼). 그래서, if 구문과 else 절이 3개의 block으로 구성된다. 전체 if구문처럼 내부 구문 2개가 block이다.


automatic storage duration을 가진 변수에 대한 공간은 둘러싼 block이 실행되었을 때 할당된다고 했었다. C99의 가변 길이 배열에서도 맞는 말인가?

아니다. 가변 길이 배열에 대한 공간은 둘러싼 block이 실행되었을 때 할당되지 않는데, 왜냐하면 배열의 길이가 아직 알려지지 않았기 때문이다. 대신에, block의 실행 도중 배열의 선언부에 도착했을 때 할당된다. 이러한 관점에서, 가변 길이 배열은 모든 다른 automatic 변수들과 구별된다.


"scope"와 "linkage"사이의 실질적인 차이는 무엇인가?

scope는 컴파일러의 이점이고, linkage는 linker의 이점이다. 컴파일러는 identifier의 scope를 사용하는데, 이는 파일 내부의identifier가 주어진 지점에서 참조 가능한지 확인하기 위함이다. 컴파일러가 소스파일을 object 코드로 변환할 때, external linkage를 가진 이름들을 기록하고, 최종적으로 object 파일 내부의 테이블 안에 이 이름들을 저장한다. 그래서, 링커가 external linkage의 이름에 접근할 수 있도록 한다. internal linkage나 no linkage는 linker에게 보이지 않는다.


이름이 block scope이지만 external linkage를 어떻게 가지는지 이해가 안된다.

변수 i를 정의한 하나의 소스파일이 있다고 생각해보자.

int i;

i의 정의가 함수의 바깥에 있다고 생각해보면, i는 기본적으로 external linkage를 갖는다. 또다른 파일에서 i에 접근할 필요가 있는 함수 f가 있고, 그래서 f의 body에서 extern으로써 i를 선언한다.

void f(void)
{
    extern int i;
    ...
}

첫번째 파일에서 i는 file scope이다. 그러나, f 내부에서는 i가 block scope를 가진다. 만약 f말고도 다른 함수들이 i를 필요로 한다면, i를 별도로 선언해야할 필요가 있다. (또는 i가 file scope를 가지게 하기 위해 f의 바깥에 i의 선언을 옮길 수 있다) 이 전체적인 일을 헷갈리게 만드는 것은 i의 각 선언 또는 정의가 다른 scope를 가질 때이다. 어떨 때는 file scope이고, 어떨 때는 block scope이다.


const object는 상수 표현식에서 사용될 수 없는가? const는 "상수(constant)"를 의미하는 것이 아닌가?

C에서, const는 "read-only"를 의미하는 것이지 "상수"임을 의미하는 것이 아니다. 왜 const object가 상수 표현식에서 사용할 수 없는지 설명하는 여러 예시를 보자.
const object는 오직 "lifetime"에만 상수이고, 프로그램 실행 전반에 걸쳐서 상수인 것은 아니다. 함수 내부에서 선언된 const object를 생각해보자.

void f(int n)
{
    const int m = n / 2;
    ...
}

f가 호출되었을 때, mn / 2로 초기화된다. m의 값은 f가 반환될 때까지 상수를 남긴다. f가 다음에 호출되었을 때, m은 다른 값을 가질 것이다. 여기서 문제가 발생한다. mswitch 구문에서 나타난다고 생각해보자.

void f(int n)
{
    const int m = n / 2;
    ...
    switch (...)
    {
        ...
        case m: ...    /*** WRONG ***/
        ...
    }
    ...
}

m의 값은 f가 호출될 때까지 알려지지 않았으므로 case label의 값은 반드시 상수 표현식이여야 한다는 C언어의 규칙을 어기게 된다.
다음으로, 바깥 block에서 선언된 const object를 생각해보자. 이 object들은 external linkage를 가지고, 파일들 간에 공유 가능하다. 만약 C언어가 상수 표현식 내부의 const object의 사용을 허용했다면, 우리는 아래와 같은 상황을 쉽게 발견할 수 있었을 것이다.

extern const int n;
int a[n];    /*** WRONG ***/

n은 아마도 또다른 파일에서 정의되었을 것이고, 이는 컴파일러가 a의 길이를 결정할 수 없게 만든다. (a가 external variable이라고 가정했고, 그래서 가변 길이 배열이 될 수 없다.)
만약 이것만으로 확신을 가지지 못했다면 이것도 생각해보자. const object가 volatile로 선언되었다면, 이 값은 실행 중에만 바뀔 수 있을 것이다.

extern const volatile int real_time_clock;

real_time_clock 변수는 프로그램에 의해 변경될 수 없다.(const로 선언되었기 때문) 그렇지만 어떠한 메커니즘으로는 이 값을 바꿀 수 있다.(왜냐하면 volatile로 선언되었기 때문)


왜 declarator의 문법은 이렇게 이상한가?

선언은 사용(use)을 모방(mimic)하기 위해 의도되었다. 포인터 declarator는 *p의 형태를 가지는데, 나중에 p에 적용할 indirection operator와 일치한다. 배열 declarator는 a[...]의 형태를 가지는데, 나중에 subscript될 배열과 일치한다. 함수 declarator는 f(...)의 형태를 가지는데, 함수 호출의 문법과 일치한다. 이러한 이유는 다른 복잡한 declarator로 확장된다. Section 17.7의 함수를 가리키는 포인터가 요소인 file_cmd 배열을 생각해보자. file_cmd에 대한 declarator는 아래의 형태를 가진다.

(*file_cmd[])(void)

함수들 중 하나의 호출은 아래의 형태를 가진다.

(*file_cmd[n])();

괄호, 대괄호, *는 동일한 위치를 가진다.

profile
저장용

0개의 댓글