Unknownpgr

C언어 선언과 정의, 매크로에 대한 정리

2021-05-19 17:11:29 | Korean

C언어에 대한 몇 가지 헷갈리는 개념들을 정리해둡니다.

선언(Declaration)과 정의(Definition)

C언어에서 변수나 함수를 만들 때, 이와 관련된 두 가지 개념이 있습니다. 하나는 선언이며, 하나는 정의입니다.

선언

C 언어에서 선언이란

  1. 어떤 이름(identifier)에 해당하는 대상, 즉 변수나 함수가 (어디에 있는지는 모르겠지만) 존재하며
  2. 그 이름을 갖는 대상의 형식이 무엇인지(e.g. int, char*, void function(int))

알려주는 것을 의미합니다. 그러므로 함수나 변수가 선언 되어 있는 경우 실제 그 값이나 함수의 동작 방식은 전혀 알 수 없으며, 컴파일 시에 오류를 발생시킵니다. 예를 들어 아래와 같이 함수를 선언하기만 하고, 정의하지는 않을 수 있습니다.

int sum(int a, int b);

int main(){
	int c = 2;
	int d = 3;
	int e = sum(c,d);
    return 0;
}

위 코드에서 1번째 줄과 같이 함수를 정의 없이 선언하는 것을 function prototype이라 합니다. 이 경우 이 코드를 컴파일하면 컴파일 에러가 발생합니다. 왜냐하면 첫 번째 줄에서 저런 형식을 갖는 sum이라는 함수가 어딘가에는 존재한다고 알려주었지만, 실제로는 그런 함수가 존재하지 않기 때문입니다.

그러므로 함수의 선언은 오직 컴파일 단계에서만 영향을 미치며, 실제로 생성되는 바이너리파일에는 아무 영향을 주지 않습니다. 실제로 C언어에서 아무리 많은 함수를 정의 없이 선언해도 생성되는 바이너리의 용량은 전혀 변하지 않습니다.

정의

C언어에서 정의란

  1. 어떤 이름에 해당하는 대상, 즉 변수나 함수를 선언함과 동시에
  2. 이에 대한 값까지 부여하는 것

을 의미합니다.

그러므로 C언어에서 어떤 대상을 선언 없이 정의하는 것은 불가능합니다. 다만 선언이 되어있지만 선언이 되어있다는 사실 자체를 모를 수는 있습니다. 예를 들어 아래와 같은 경우입니다.

void f1(){
	f2();
}

void f2(){
	// content of f2
}

이 경우 f1f2보다 먼저 정의돼있기 때문에 컴파일러가 f1을 컴파일하는 시점에는 f2라는 함수가 아직 선언되지 않았고, 따라서 경고를 발생시킵니다. 또는 아래와 같은 경우도 있습니다.

int main(){
	printf("Hello world!");
	return 0
}

이 경우 #include를 통해 printf함수를 포함하는 헤더 파일을 include하지 않았기 때문에 main함수를 컴파일하는 시점에는 printf함수가 선언되지 않았고, 따라서 컴파일러는 경고를 발생시킵니다.

그러나 위 두 케이스 모두에서 선언은 되어 있지 않지만, 어쨌든 그러한 함수가 존재하기는 하므로 컴파일러는 링킹 단계에서 정상적으로 함수를 찾아 넣을 수 있습니다. 따라서 컴파일 오류는 발생하지 않고, 정상적으로 바이너리가 생성됩니다.

extern

C언어에서 함수를 정의 없이 선언할 때에는 단순히 함수의 바디를 작성하지 않고 세미콜론으로 끝내버리면 됐었습니다. 그러나 C언어에서 변수를 정의 없이 선언하는 것은 단순히 값을 할당하지 않는 것으로는 이뤄지지 않습니다.

#include <stdio.h>

int a;

int main(){
	printf("%d", a);
    return 0;
}

예를 들어 위와 같이 하면, 언듯 보기에는 a에 어떠한 값도 할당하지 않았으므로 이는 선언만 한 것이고, 따라서 컴파일 에러가 발생할 것 같습니다. 그러나 이는 사실은 아래와 동일한 구문입니다.

#include <stdio.h>

int a = 0;

int main(){
	printf("%d", a);
    return 0;
}

C언어 컴파일러는 자동으로 모든 전역 변수를 0으로 초기화하므로, 단순히 값을 명시적으로 할당하지 않음으로써 정의 없는 선언을 할 수는 없습니다. C언어에서 정의 없이 선언하려면 아래와 같이 extern 키워드를 사용해야 합니다.

#include <stdio.h>

extern int a;

int main(){
	printf("%d", a);
    return 0;
}

그런데 이 경우에는 함수와 마찬가지로 에러가 발생해야 할 것만 같은데, 실제로는 에러가 발생하지 않습니다. 실제로 위 코드를 컴파일할 경우 컴파일이 잘 되는데, 이는 컴파일러가 정의 없이 선언된 변수를 발견할 경우 자동으로 정의를 추가해버리기 때문으로 생각됩니다. 함수는 그 내용을 명시적으로 정의하지 않으면 컴파일러가 이를 자동으로 처리할 방법이 없습니다. 그러나 변수의 경우 전역 변수에서 값을 할당하지 않은 경우와 마찬가지로 0으로 그 값을 초기화하는 것이 가능합니다. 그래서 이렇게 동작하는 것이라고 추측합니다. (이는 추측이며 실제로 그런 이유에서인지는 잘 모르겠습니다.)

#include

그리고 하나 더 정리해두고 싶은 것은 #include 전처리 구문이 하는 역할입니다. javapython 등 매우 추상화가 많이 이루어진 언어의 경우 import 구문이 여러가지 복잡한 역할을 수행합니다. 그러나 C언어에서 #include 구문은 단순히 다른 파일의 내용을 통째로 가져오는 것에 불과합니다. 예를 들어 아래와 같이 #include 구문을 사용할 수도 있습니다.

// test.txt
"This is some string!"
#include <stdio.h>

char* string =
#include "test.txt"
;

int main(){
    pritnf("test.txt = %s\n",string);
    return
}

이는 실제로 정상적으로 잘 동작하는 구문입니다. 이 사실을 통해 왜 전역 변수나 함수를 정의할 때 header파일에 선언해두면 되는지 알 수 있습니다. 그렇게 할 경우 선언한 내용들이 헤더 파일을 include하는 모든 소스파일에 그대로 옮겨지므로 컴파일 시점에 컴파일러가 선언이 존재함을 알 수 있고, 실제로 소스파일이 존재하므로 링킹 역시 잘 이뤄지기 때문입니다.


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -