[C] 선언

Chris Kim·2024년 10월 10일

프로그래밍언어

목록 보기
14/25

0. 들어가며

여태까지 자세한 설명없이 선언을 사용해왔다. 이번 장에서는 자세한 옵션 사항을 다룰 것이다.

1. 선언 문법

선언이란 컴파일러에게 식별자가 가지는 의미를 제공하는 것이다. 일반적으로 선언은 다음과 같은 형식으로 이뤄진다.

declaration-specifiers declarators;

선언지정자는 선언된 변수나 함수의 특징을 묘사한다. 선언자는 그들에게 이름을 제공하며 이에 대한 추가적인 정보를 제공하기도 한다.
스토리지 클래스에는 auto, static, extern 그리고 register가 있다. 대부분의 경우 하나의 클래스가 나타나며, 사용하는 경우 반드시 맨 앞에 있어야한다.
형식 한정자const, volatile, restrict가 있다. 이건 필수는 아니다.
형식 지정자 void, char, short, int, long, float, double, signed, unsigned 모두 형식 지정자다. 앞선 글에서 이들은 서로 결합될 수 있는 걸 살펴보았다. 형식 이름은 typedef로 만들어질 수 있다.

C99에는 함수 선언에만 사용되는 함수 지정자가 있다.(inline 밖에 없다) 형식 한정자와 형식 지정자는 반드시 스토리지 클래스 뒤에 와야한다. 나머지 둘의 순서는 상관 없다.
선언자는 식별자를 포함하며 식별자 뒤에는 [], *등이 따라온다. 선언자는 ,로 구분되기도 한다. 초기자가 붙을때도 있다.

2. 스토리지 클래스

스토리지 클래스는 변수와, 흔하진 않지만 함수와 매개변수에 사용된다. 잠깐 앞선 글에서 block(함수의 몸통)을 다뤘던 기억을 떠올리자. C99에서 선택문, 순환문은 블록으로 다뤄졌다.

2.1 변수의 성질

C 프로그램에서 모든 변수는 세 가지 성질을 가진다.

  • stroage duration: 스토리지 듀레이션은 한 변수에 대한 메모리 할당과 할당 해제 시점을 나타낸다. 자동 스토리지 듀레이션의 변수는 블록 내에서 메모리가 할당된다. 스태틱 스토리지 듀레이션은 프로그램이 실행되는한 메모리 할당이 유지된다.
  • Scope: 어떤 변수의 스코프란 프로그램 내에서 해당 변수가 참조될 수 있는 부분(범위)를 말한다. 변수는 블록스코프 혹은 파일스코프를 가질 수 있다.
  • Linkage: 변수의 링크는 어떤 변수가 프로그램의 다른 부분에서 어느 수준까지 공유될 수 있는지를 결정한다. 외부 링크는 프로그램 내 다른 파일에서의 공유를 가능케 한다. 내부 링크는 파일 내 공유만으로 한정할 것이다. no linkage 변수는 한 함수 내에만 속하며, 그 밖에서는 사용될 수 없다.

이러한 성질은 선언이 이뤄지는 위치에 따라 다르다. 블록 내에서 선언된 경우, 자동 스토리지 듀레이션, 블록 스코프, no linkage다. 하지만 블록 바깥에서 선언이 이뤄진 경우, 스태틱 스토리지 듀레이션, 파일 스코프, 외부 링크 성질을 가진다. 일반적으로 이 기본설정들로 충분할 수 있으나, 필요하면 명시적으로 변수를 정의 할 수 있다.

2.2 auto 스토리지 클래스

auto 스토리지 클래스는 블록에 속한 변수에게만 유효하다. auto 변수는 자동 스토리지 듀레이션, 블록 스코프, no linkage의 성질을 가진다. auto는 명시적으로 사용되지 않는다.(애초에 이게 기본 설정이니깐)

2.3 static 스토리지 클래스

static 스토리지 클래스는 모든 변수에 사용가능하나, 선언 위치에 따라 효과가 조금 다르다. 블록 바깥 변수에 사용된 경우, 내부 링크 성질을 가지게된다(블록 바깥 변수는 기본 설정이 static이다). 블록 내 변수에 사용된 경우 스태틱 스토리지 듀레이션으로 바뀐다.
블록 바깥 변수에 static을 사용하는 경우, 파일 내에 해당 변수를 숨기게 되며, 다른 파일의 함수는 해당 변수를 사용할 수 없다. information-hiding 기술로 알려져있다.
블록 내 변수에 static을 사용하는 경우 해당 변수의 값은 무기한으로 저장된다. static은 몇가지 흥미로운 성질을 가진다.

  • static 가 블록 내에서 사용되는 경우, 처음 한번만, 프로그램 실행 이전에 초기화가 이뤄진다.auto 변수는 등장할 때마다 초기화가 이뤄진다.(초기자가 있는 경우에)
  • 함수가 재귀적으로 호출될 때마다, 함수는 auto 변수를 가지게 된다. 만약 static 변수를 가지는 경우, 해당 변수는 함수의 모든 호출에서 공유된다.
  • auto 변수에는 포인터를 반환할 수 없으나, static은 가능하다.

static 으로 변수를 선언하는 것은 함수로 하여금, 프로그램이 접근 불가능한 "숨겨진" 공간, 호출 사이에서 정보를 유지하게 해준다. 더 많은 경우에 우리는 static으로 프로그램을 효율화 할 수 있다. 예를 들어,

char digit_to_hex_char(int digit)
{
	const char hex_chars[16] = "123456789ABCDEF";
    
    return hex_chars[digit];
}

char digit_to_hex_char(int digit)
{
	static const char hex_chars[16] = "123456789ABCDEF";
    
    return hex_chars[digit];
}

로 바꿀 수 있다.

2.4 extern 스토리지 클래스

extern을 통해 소스 파일들이 같은 변수를 공유할 수 있게 만들 수 있다. extern은 이미 앞서 다룬 바가 있다.

extern int i;

에서 iint 변수다 하지만 이 선언은 i에게 메모리를 할당하지 않는다. C 용어로 말하자면, 선언은 정의가 아니다. 이는 단지 컴파일러로 하여금 해당 변수에 접근할 필요를 전달하는 것 뿐이다. 한 변수는 많은 선언을 가질 수 있으나 정의는 하나 밖에 가지지 못한다.
하지만 예외가 있다. extern 선언을 통한 초기화는 변수 정의의 역할을 겸한다. 예를 들어,

extern int i = 0; // equal to int i = 0;

이 규칙은 서로 다른 방식으로 변수를 초기화하는 것으로부터 복수의 extern 선언을 방지해준다. extern 선언은 항상 스태틱 스토리지 듀레이션을 가진다. 블록 내에서 사용된 경우 블록 스코프를, 바깥에서 선언 된 경우에는 파일 스코프를 가진다.
extern 변수에 링크를 결정하는 것은 좀 힘들다. 만약 변수가 앞서 static으로 선언되었다면,(함수 바깥에서), 내부 링크를 가질 것이다. 그 외에는 외부 링크를 가질 것이다.

2.5 register 스토리지 클래스

register 스토리지 클래스를 변수 선언에 사용하는 것은 컴파일러에게 변수를 메인 메모리가 아닌 레지스터에 저장할 것을 요청한다. 이건 명령(지시)가 아니다. "요청"이다. 컴파일러는 레지스터 저장 여부를 선택할 자유가 있다.
register 스토리지 클래스는 오직 블록 내에서 선언된 변수에게만 유효하다. registerauto와 같은 듀레이션, 스코프, 링크 설정을 가진다. 하지만 한 가지 차이점이 있는데, register는 주소를 가지지 않는다. 즉 & 사용이 불가능하다. 이는 컴파일러가 레지스터가 아닌 메모리에 저장하기로 선택해도 적용된다.
register는 자주 접근, 변경이 필요할 때 유용하다. for 문에서 루프를 통제하는 변수가 좋은 예시다.
사실 요즘 컴파일러들은 알아서 최적화를 위해 레지스터 저장 여부를 판단한다. register을 쓰면 컴파일러에게 최적화를 위한 정보를 제공하는 것이다. 레지스터에 저장되면 주소를 가지지 못하고, 포인터를 통해 수정되지 못한다. 이런 측면에서 restrict와 유사하다고 볼 수 있다.

2.6 함수 스토리지 클래스

함수에는 externstatic 스토리지 클래스만 사용가능하다. 전자의 경우 외부 링크를, 후자의 경우, 내부 링크를 설정한다. 스토리지 클래스가 없는 경우, 기본적으로 외부링크로 가정된다. 기본적으로 외부 링크를 가정하므로 extern은 잘 사용하지 않는다.
static은 함수가 다른 파일에서 호출 되는 것을 방지하기 위해 사용된다.이렇게 하는데에는 몇가지 이점이 있다.

  • 유지보수의 용이성
  • 이름 공간 오염 방지: static을 사용하면 함수 이름을 다른 파일에서 재활용할 수 있다.
    함수의 매개변수는 auto와 같은 성질을 가진다. 매개변수에는 오직 register만 사용 가능하다.

2.7 요약

int a ;
extern int b; 
static int c;

void f(int d, register int e)
{
	auto int g; 
    int h;
	static int i; 
    extern int j; 
    register int k;
}

3. 형식 한정자

volatile은 저수준 프로그래밍으로 사용이 한정되므로 나중에 다룰것이다. const를 사용하면 읽기목적이 된다. 이렇게 하면 몇 가지 이점이 있다.

  • 일종의 문서화 형식이다. 읽기 목적 객체라는 정보를 전달 할 수 있다.
  • 컴파일러는 프로그램이 무심코 객체의 값을 변경하는지 검사할 수 있다.
  • 임베디듯 시스템 처럼, 특정 목적으로 작성된 프로그램에서 컴파일러는 ROM에 저장될 변수를 위해 const를 사용할 수 있다.

#define 지시자랑 const랑 별 차이가 없어보인다. 하지만 이 둘은 상당한 차이점을 가진다.

  • #define을 통해 수, 믄지, 문자열 상수를 만들 수 있으나, const는 모든 종류의 객체를 읽기 전용으로 만들 수 있다.(배열, 포인터, 구조체, 공용체 등)
  • const 객체는 변수와 같은 스코프를가진다. #define 상수는 그렇지 않다.(이 녀석은 블록스코프를 가질 수 없다.)
  • const 객체는 매크로 값과는 다르게, 디버거에서 보인다.
  • 매크로와는 다르게 const 객체는 상수 표현식에서 사용 불가능하다.
  • const 객체에는 &을 사용할 수 있다.

4. 선언자

선언자느 식별자와 여러 심볼로 구성될 수 있다.

    • 가 앞에있으면 포인터를 의미한다.
  • [] 로 끝나면 배열을 의미한다. []는 배열이 매개변수일때, 초기자를 가질 떄, extern 클래스일 때 비어있을 수 있다. C99는 [] 사이에 올 수 있는 선택지를 두 개 더 준다. static을 통해 배열의 최소 길이 정보를 전달 할 수 있으며, * 을 통해 함수 프로토타입에 VLA 입력변수를 지시 할 수있다.
  • ()로 끝나면 함수를 의미한다.

이걸로 끝나면 프로그래밍이 누워서 떡먹기였을거다.

4.1 복잡한 선언 해독

여태까지는 선언을 이해하는데 큰 힘을 들이지 않았지만, 이 녀석은 어떨까?

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

다행히 두 가지 간단한 규칙이 있다.

  • 선언자를 안쪽에서 바깥으로 읽어나가라. 다른말로 표현하자면, 식별자가 선언된 위치를 찾아라.
  • [],() 을 * 보다 먼저 선택해라

이 규칙을 적용하면 위의 예시가 뭔지 알 수 있다. 위의 선언문은 10개의 원소를 가지는 x 배열로서, 원소는 void 입력변수를 가지는 함수에 대한 포인터고, 이 함수들은 int를 반환한다.
선언을 숙달하는 것은 상당한 시간과 노력이 필요하다. 다행인건 C언어는 배열을 반환하지 못한다는 것이다. 함수는 함수를 반환할 수 없으며, 함수의 배열도 불가능하다. 하지만 이들을 가리키는 각 포인터들은 반환 할 수 있다.(포인터의 유용함)

4.2 선언 단순화를 위한 형정의

앞의 예시를 단순화 하기 위해 다음과 같이 형정의를 사용할 수 있다.

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

5. 초기자

사실 초기화를 위한 초기자 사용은 이전에서 많이 다뤘다. 추가적인것만 다루자면

  • 변수가 static으로 선언된 경우, 초기자는 상수여야만 한다.
  • 변수가 자동 스토리지 클래스를 가지는 경우 초기자는 상수가 아니어도 된다.
  • 배열, 구조체, 공용체 초기자는 반드시 상수 표현식을 포함해야하며, 변수나 함수 호출은 포함하지 못한다. C99에서는 static 스토리지 듀레이션인 경우에만 이 규칙이 적용된다.(VLA)
  • 자동 구조체 혹은 자동 공용체의 초기자는 다른 구조체 혹은 공용체가 될 수 있다.

5.1 미초기화 변수

여태까지 초기화되지 않은 변수는 정의되지 않은 값을 가졌다고 암묵적으로 인정했다. 하지만 그게 항상 사실인건 아니다. 변수의 초기값은 스토리지 듀레이션에 의존한다.

  • 자동 스토리지 듀레이션 변수는 기본 초기값이 없다.
  • 스태틱 스토리지 듀레이션 변수는 기본 초기값이 0이다.

6. 인라인(inline) 함수

C99 함수 선언은 추가 옵션을 가지는데 inline 이다. inline은 스토리지 클래스, 형식 한정자, 형식 지정자와는 다른 새로운 선언 옵션이다. inline을 이해하기 위해 먼저 우리는, 함수 호출 프로세스를 다루고 반환을 하기 위해 C 컴파일러가 생성하는 기계어를 시각화 해야한다.
기계 수준에서, 몇몇 지시는 반드시 호출을 준비하기위해 필요하며, 호출 그 자체는 함수내의 첫 지시(명령)으로 점프한다. 만약 함수가 입력변수를 가지는 경우, 입력변수는 복사되어야 한다. 함수 반환을 위해서는 호출된 함수 부분과 호출한 부분 모두 비슷한 노력이 필요하다. 함수 호출과 반환 작업의 누적을 이른바 오버헤드(overhead)라고 일컫는다. 왜냐하면 함수는 그 목적을 수행하기 위해 필요한 작업보다 더 많은 작업을 수행하기 때문이다. 함수 호출로 인한 오버헤드는 프로그램을 아주 조금 느리게 만들뿐이지만, 쌓이고 쌓여서 크게 될 수 있다. 수십, 수백만번 같은 함수를 호출하거나, 오래되고 느린 프로세서를 사용하는 경우에 말이다.
C89에서 이 문제를 회피하기 위한 방법은 parameterized macro 뿐이었다. 하지만 여기에도 단점이 있었다. C99는 이에 inline 함수를 제공한다. 함수의 기계어로 각 함수의 호출을 컴파일러가 대체하는것에 있어 구현-전략을 제시한다. 이 기술은 일반적인 함수 호출로 인한 오버헤드를 피할 수 있다. 하지만 컴파일된 프로그램의 크기가 약간 커질 수 있다.
inline으로 함수가 선언되었다고 컴파일러가 함수를 인라인 시키도록 강제하는 건 아니다. 이는 단순히 컴파일러에게 시도할 것을 제안하는 것에 불과하다. 그렇다 컴파일러는 선택할 자유가 있다.

6.1 인라인 정의

인라인 함수는 inline 키워드를 선언 지정자 중 하나로 가진다.

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

여기서 좀 더 복잡해진다. average 함수는 외부 링크를 가진다. 따라서 다른 소스파일이 average를 호출 할 수 있다. 그러나 average 의 정의는 컴파일러에 의해 external definition으로 다뤄지지 않는다.(이게 inline definition 이다.). 따라서 average 함수를 다른 파일에서 호출되는 것은 에러를 발생 시킨다.
에러를 피하는 방법에는 두 가지 방법이 있다. 하나는 static을 함수 정의에 넣는것이다.

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

average는 이제 내부 링크를 가진다. 따라서 이제 다른 파일에서 호출 될 수 없다. 다른 파일들은 average라는 그들만의 함수를 가질 수 있다.
다른 선택지는 average를 external definition을 통해 다른 파일에서의 호출을 허용하는 것이다. inline 없이 두 번째로 average를 정의하고 이를 다른 파일에 넣는 것이다. 이렇게 해도 되지만 같은 함수를 두 가지 버전으로 가지는 것은 좋지 않다.(일관성 측면에서 좋지 않다.)
여기 더 나은 접근법이 있다. 먼저 우리는 인라인 정의를 헤더 파일에 넣을 것이다.(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.h를 포함하면 된다. average.c파일은 extern average의 프로토타입을 가지며, 이 프로토타입은 average.h의 정의를 가지게 된다.
C99에서의 일반적인 규칙은 각 파일에서의 최상위 수준에서의 함수 선언은 inline을 포함한다는 것이다.(extern이 아니라)

6.2 인라인 함수의 제한

  • 인라인 함수는 변경 가능한 static 변수를 정의하지 못한다.
  • 인라인 함수는 내부 링크 변수 참조를 포함할 수 없다.
    따라서 static const만 정의할 수 있다.
profile
회계+IT=???

0개의 댓글