선언
선언 (Declarations)
C 프로그램에서 이름을 만드는 가장 기본적인 문법이 선언(declaration) 이에요. 어떤 식별자를 프로그램에 도입하고, 그 의미와 속성을 지정하는 역할을 하죠. 이 페이지에서 선언의 구조와, 선언이 진짜 정의(definitions)가 되는 경우를 살펴볼게요.
출처: cppreference
본문
선언(declaration) 은 하나 이상의 식별자를 프로그램에 도입하고 그 의미와 속성을 지정하는 C 언어 구문이에요.
선언은 어떤 스코프에서도 나타날 수 있어요. 각 선언은 세미콜론으로 끝나고(문처럼) 두 개(until C23) 세 개(since C23)의 별개 부분으로 구성돼요.
specifiers-and-qualifiers declarators-and-initializers (optional) ; (1)
attr-spec-seq specifiers-and-qualifiers declarators-and-initializers ; (2) (since C23)
attr-spec-seq ; (3) (since C23)
여기서
| 항목 | 설명 |
|---|---|
specifiers-and-qualifiers |
임의의 순서로, 공백으로 구분된 목록: 타입 지정자: void, 산술 타입의 이름, 원자 타입(atomic type)의 이름, typedef 선언으로 이전에 도입된 이름, struct·union·enum 지정자, typeof 지정자(since C23); 저장 클래스 지정자 0개 또는 1개: typedef, constexpr, auto, register, static, extern, _Thread_local; 타입 한정자 0개 이상: const, volatile, restrict, _Atomic(함수를 선언할 때만); 함수 지정자 0개 이상: inline, _Noreturn; 정렬 지정자 0개 이상: _Alignas(until C23) / alignas(since C23) |
declarators-and-initializers |
쉼표로 구분된 선언자 목록(각 선언자는 추가 타입 정보와/또는 선언할 식별자를 제공). 선언자에는 초기화자가 같이 올 수 있음. enum·struct·union 선언은 선언자를 생략할 수 있으며, 그 경우 열거형 상수와/또는 태그만 도입함 |
attr-spec-seq |
(C23) 선언된 개체에 적용되는 선택적인 속성 목록. 단독으로 나타나면 속성 선언(attribute declaration)을 이룸 |
예를 들어,
int a, *b=NULL; // "int"는 타입 지정자,
// "a"는 선언자
// "*b"는 선언자이고 NULL은 그 초기화자
const int *f(void); // "int"는 타입 지정자
// "const"는 타입 한정자
// "*f(void)"는 선언자
enum COLOR {RED, GREEN, BLUE} c; // "enum COLOR {RED, GREEN, BLUE}"는 타입 지정자
// "c"는 선언자
선언이 도입하는 각 식별자의 타입은 타입 지정자가 지정한 타입과 그 선언자가 적용한 타입 수정의 결합으로 결정돼요. 변수의 타입은 auto 지정자를 쓰면 추론될 수도 있어요. (since C23)
속성(since C23)은 specifiers-and-qualifiers에 나타날 수 있고, 그 경우 앞선 지정자들이 결정한 타입에 적용돼요.
선언자 (Declarators)
각 선언자는 다음 중 하나예요.
identifier attr-spec-seq (optional) (1)
( declarator ) (2)
* attr-spec-seq (optional) qualifiers (optional) declarator (3)
noptr-declarator [ static (optional) qualifiers (optional) expression ] (4)
noptr-declarator [ qualifiers (optional) * ]
noptr-declarator ( parameters-or-identifiers ) (5)
S * cvr D // S는 타입 지정자, cvr는 한정자, D는 선언자
S D[N] // N은 배열 크기
S D(params) // params는 매개변수
이 문법 뒤에는 이런 발상이 있어요. 선언자가 선언한 식별자가 선언자와 같은 형태의 표현식에 나타나면, 타입 지정자 시퀀스가 지정한 타입을 가지게 된다는 거죠.
struct C
{
int member; // "int"는 타입 지정자
// "member"는 선언자
} obj, *pObj = &obj;
// "struct C { int member; }"는 타입 지정자
// 선언자 "obj"는 struct C 타입의 객체를 정의
// 선언자 "*pObj"는 C에 대한 포인터를 선언,
// 초기화자 "= &obj"는 그 포인터의 초기값을 제공
int a = 1, *p = NULL, f(void), (*pf)(double);
// 타입 지정자는 "int"
// 선언자 "a"는 int 타입의 객체를 정의
// 초기화자 "=1"이 그 초기값을 제공
// 선언자 "*p"는 int에 대한 포인터 타입의 객체를 정의
// 초기화자 "=NULL"이 그 초기값을 제공
// 선언자 "f(void)"는 void를 받고 int를 반환하는 함수를 선언
// 선언자 "(*pf)(double)"은 double을 받고 int를 반환하는 함수에 대한
// 포인터 타입의 객체를 정의
int (*(*foo)(double))[3] = NULL;
// 타입 지정자는 int
// 1. 선언자 "(*(*foo)(double))[3]"은 배열 선언자:
// 선언된 타입은 "/중첩 선언자/ 3개 int의 배열"
// 2. 중첩 선언자는 "*(*foo)(double))", 이는 포인터 선언자
// 선언된 타입은 "/중첩 선언자/ 3개 int의 배열에 대한 포인터"
// 3. 중첩 선언자는 "(*foo)(double)", 이는 함수 선언자
// 선언된 타입은 "/중첩 선언자/ double을 받고 3개 int의 배열에 대한
// 포인터를 반환하는 함수"
// 4. 중첩 선언자는 "(*foo)", 이는 (함수 선언자 문법이 요구하는 대로
// 괄호로 묶인) 포인터 선언자.
// 선언된 타입은 "/중첩 선언자/ double을 받고 3개 int의 배열에 대한
// 포인터를 반환하는 함수에 대한 포인터"
// 5. 중첩 선언자는 "foo", 이는 식별자.
// 이 선언은 식별자 "foo"가
// "double을 받고 3개 int의 배열에 대한 포인터를 반환하는 함수에 대한 포인터"
// 타입의 객체를 가리키도록 도입.
// "foo"를 선언자 형태의 표현식에 쓰면, 그 타입은
// int가 됨.
int x = (*(*foo)(1.2))[0];
다른 선언자의 일부가 아닌 각 선언자의 끝은 시퀀스 포인트(sequence point)예요.
모든 경우 attr-spec-seq는 속성(since C23)의 선택적인 시퀀스예요. 식별자 바로 뒤에 나타나면 선언되는 객체나 함수에 적용돼요.
정의 (Definitions)
정의(definition) 는 선언하는 식별자에 대한 모든 정보를 제공하는 선언이에요.
함수에 대해서는 함수 본문을 포함하는 선언이 함수 정의예요.
int foo(double); // 선언
int foo(double x) { return x; } // 정의
객체에 대해서는 저장 공간을 할당하는(자동 또는 정적, 단 extern은 아님) 선언이 정의이고, 저장 공간을 할당하지 않는(외부 선언) 선언은 정의가 아니에요.
extern int n; // 선언
int n = 10; // 정의
struct와 union에 대해서는 멤버 목록을 지정하는 선언이 정의예요.
struct X; // 선언
struct X { int n; }; // 정의
재선언 (Redeclaration)
선언은 같은 스코프에서 같은 식별자에 대한 다른 선언이 더 일찍 나왔다면 그 식별자를 도입할 수 없어요. 단, 다음은 예외예요.
- 링크를 가진 객체(외부 또는 내부)의 선언은 반복될 수 있어요.
extern int x;
int x = 10; // OK
extern int x; // OK
static int n;
static int n = 10; // OK
static int n; // OK
- 비-VLA typedef는 같은 타입을 가리키는 한 반복될 수 있어요.
typedef int int_t;
typedef int int_t; // OK
struct X;
struct X { int n; };
struct X;
이 규칙들은 헤더 파일 사용을 단순화해요.
주의 (Notes)
| C89에서 복합문(블록 스코프) 내의 선언은 블록의 시작 부분, 어떤 문보다 앞에 나타나야 해요. 또한 C89에서 int를 반환하는 함수는 함수 호출 연산자로 암시적으로 선언될 수 있고, int 타입의 함수 매개변수는 옛 스타일 함수 정의를 쓸 때 선언할 필요가 없었어요. | (until C99) |
빈 선언자는 금지돼요. 간단한 선언은 선언자 하나 이상을 가지거나, struct/union/enum 태그를 하나 이상 선언하거나, 열거형 상수를 하나 이상 도입해야 해요.
| 선언자의 어떤 부분이 가변 길이 배열(VLA) 선언자라면, 전체 선언자의 타입을 "variably-modified type"이라고 불러요. variably-modified 타입으로부터 정의된 타입도 variably modified(VM)예요. variably-modified 타입의 선언은 블록 스코프나 함수 원형 스코프에서만 나타날 수 있고, struct·union의 멤버일 수 없어요. VLA는 자동 또는 할당된 저장 기간만 가질 수 있지만, VLA에 대한 포인터 같은 VM 타입은 static일 수 있어요. VM 타입의 사용에는 다른 제약도 있는데, goto·switch·longjmp를 참조하세요. | (since C99) |
static_assert는 C 문법 관점에서 선언으로 간주돼요(그래서 선언이 나타날 수 있는 곳이면 어디든 나타날 수 있어요). 그러나 어떤 식별자도 도입하지 않고 선언 문법을 따르지 않아요. |
(since C11) |
속성 선언도 선언으로 간주돼요(그래서 선언이 나타날 수 있는 곳이면 어디든 나타날 수 있어요). 그러나 어떤 식별자도 도입하지 않아요. attr-spec-seq 없는 단일 ;는 속성 선언이 아니라 문(statement)이에요. |
(since C23) |