프로그래밍 언어/C언어

C언어 21. 전처리와 분할 컴파일-2

닉네임못짓는사람 2020. 7. 28. 12:52
반응형

저번 글에 이어서 전처리와 분할 컴파일에 대해 설명해보도록 하겠습니다.

 

저번 글에서 알아본 define에 대해서 간단하게 복습해봅시다.

define은 정수, 실수 또는 문자열 상수(확장 문자열)를 기호화(매크로명)해서

사용할 수 있게 해주는 명령어이며, 사용법은 다음과 같습니다.

#define 매크로명 확장문자열

이런 define과 매우 비슷한 것이 C언어에는 존재하고 있는데요,

바로 변수와 상수 글에서 알아보았던 const명령어입니다.

const는 데이터를 상수명으로 기호화해서 사용할 수 있게 해 준다고 할 수 있을 것입니다.

이러한 const를 사용해서 정수, 실수, 문자 등등의 상수를 저장할 수 있는데,

일반 자료형만이 아닌 포인터 변수 또한 상수화하여 사용할 수 있습니다.

포인터 변수의 상수화


그런데 이것은 일반적인 자료형과는 조금 다른 의미를 가지게 되는데,

바로 포인터 변수의 값을 고정하는 것이 아닌, 포인터가 가리키는 기억공간의 값을 고정시키는 것입니다.

어떤 의미인지 코드를 통해서 확인해보도록 합시다.

#include <stdio.h>

int main() {
	int a = 10, b = 20;
	const int* in_P = &a;
	printf("in_P의 값 : %d\n", *in_P);
	in_P = &b;
	printf("in_P의 값 : %d", *in_P);
    	return 0;
}

보시다시피 본래 상수라면 상수의 값을 변경하려 했을 때,

즉 in_P = &b를 실행하려 할 때 오류가 발생했어야 할 것입니다.

하지만 오류는커녕 아무런 문제 없이 값이 바뀐 것을 확인하실 수 있습니다.

따라서 포인터 변수를 상수화해도 포인터의 값을 고정하는 것이 아니라는 것입니다.

그럼 이 const의 의미는 무엇일까요?

바로 in_P가 가리키고 있는 포인터의 값을 in_P를 참조하여 변경할 수 없다는 것입니다.

코드로 확인해봅시다.

보시다시피 in_P를 참조해서 a의 값을 바꾸려고 하자 오류가 발생하는 것을 확인할 수 있습니다.

물론 변수 a자체가 상수화된 것은 아닙니다.

in_P를 참조하는 것이 아니라면 a의 값은 얼마든지 바꿀 수 있습니다.

#include <stdio.h>

int main() {
	int a = 10, b = 20;
	const int* in_P = &a;
	printf("in_P의 값 : %d\n", *in_P);
	a = 20;
	printf("in_P의 값 : %d", *in_P);
    	return 0;
}

이렇듯이 직접 변수 a에 접근하면 얼마든지 값을 변경할 수 있습니다.

그러면 이런 방법을 왜 사용하는 것일까요?

대표적인 예로는 문자열 상수를 사용할 때, 그 값이 바뀌면 안 되기 때문에 변수에 const를 사용합니다.

#include <stdio.h>

int main() {
	char* a = "hello";
	printf("%s\m", a);
	*a = "world";
	printf("%s", a);
    	return 0;
}

위와 같이 코드를 작성한다고 생각해봅시다.

작성할 땐 컴파일러가 아무런 오류도 찾아내지 못합니다.

하지만 정작 프로그램을 실행시키면 오류가 발생해서 프로그램이 정상적으로 실행되지 않습니다.

때문에 이러한 경우를 대비해서 변수 a에 const를 사용해서 값을 변경할 수 없게 하는 것입니다.

이렇게 하면 코드를 작성하는 단계에서 오류를 발견할 수 있습니다.

이를 사용해서 코드를 작성하면 좀 더 안전한 프로그램을 만들 수 있을 것입니다.

분할 컴파일과 모듈


다음은 분할 컴파일에 대해서 이야기해보도록 하겠습니다.

C언어 프로그램은 보통 기능에 따라 여러 개의 함수들로 구성되어 만들어집니다.

이 함수들은 다시 여러개의 파일 단위로 묶어서 작성할 수 있는데, 이 파일 단위를 모듈이라고 합니다.

하나의 프로그램을 여러개의 모듈로 나누어서 작성하는 것을 분할 컴파일이라고 합니다.

 

분할 컴파일을 하면 각 모듈들은 개별적으로 작성되어 목적 파일로 만들어집니다.

그 뒤 최종적으로 링크 단게에서 이 목적 파일들을 하나의 실행파일로 만듭니다.

 

이런 분할 컴파일을 사용하면 어떠한 이점이 있을까요?

여러분이 몇십 줄짜리 코드를 작성할 때는 혼자서도 충분히 여유롭게 작성할 수 있을 것입니다.

하지만 코드가 매우 길어지고, 프로그램의 크기가 커지면 커질수록

혼자서 코드를 작성하기는 버거울 것입니다.

때문에 분할 컴파일을 통해서 큰 프로그램을 여러 사람이 나누어서 작성하면 작업 능률이 오를 것입니다.

또한 오류 수정을 모듈별로 할 수 있기 때문에 프로그램의 생산성을 높일 수 있고,

검증된 모듈은 다른 프로그램에서도 충분히 재활용해서 사용할 수 있을 것입니다.

 

간단한 코드로 분할 컴파일을 사용해봅시다.

#include <stdio.h>

void inputNUM(int* n1, int* n2);
void showNUM(int n1, int n2);
void exchangeNUM(int *n1, int *n2);

int main() {
	int num1, num2;

	inputNUM(&num1, &num2);
	showNUM(num1, num2);
	exchangeNUM(&num1, &num2);
	showNUM(num1, num2);
	return 0;
}

void inputNUM(int *n1, int *n2) {
	printf("숫자 두개를 입력해주세요. : ");
	scanf("%d%d", n1, n2);
}

#include <stdio.h>

void showNUM(int n1, int n2) {
	printf("NUM1 : %d, NUM2 : %d\n", n1, n2);
}
void exchangeNUM(int* n1, int* n2) {
	int tmp;

	tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}

일단 두 개의 모듈, 즉 .c파일을 위와 같이 작성합니다.

첫 번째 모듈엔 main함수와 inputNUM함수를, 두번째 모듈엔 showNUM과 exchange함수를 작성합니다.

또한 첫번째 함수에선 실제로 inputNUM함수만 구현하는데,

showNUM함수와 exchange함수는 main함수에서 사용하고 있는 것을 알 수 있습니다.

때문에 이 두 함수를 정의해주지 않으면 컴파일 에러가 발생합니다.

따라서 첫 번째 모듈에서 두 함수를 정의해주고, 두 번째 모듈에서 실제 구현을 해주는 것입니다.

또한 두 번째 모듈에서 printf를 사용하기 때문에 stdio.h헤더 파일을 포함시켜주어야 합니다.

 

현재 위의 코드에선 포인터를 사용해서 두 변수의 값을 바꾸어주고 있습니다.

그럼 이번엔 이전에 배웠던 외부 변수를 사용해서 코드를 작성해볼까요?

외부 변수는 함수에 종속되지 않기 때문에 어느 함수에서든 사용할 수 있을 것입니다.

또한 매개변수가 필요 없기 때문에 더 간결한 코드를 작성할 수 있습니다.

#include <stdio.h>

void inputNUM();
void showNUM();
void exchangeNUM();

int num1, num2;

int main() {
	inputNUM();
	showNUM();
	exchangeNUM();
	showNUM();
	return 0;
}

void inputNUM() {
	printf("숫자 두개를 입력해주세요. : ");
	scanf("%d%d", &num1, &num2);
}

#include <stdio.h>

void showNUM() {
	printf("NUM1 : %d, NUM2 : %d\n", num1, num2);
}
void exchangeNUM() {
	int tmp;

	tmp = num1;
	num1 = num2;
	num2 = tmp;
}

그런데 실제론 두 번째 모듈에서 num1과 num2가 정의되어있지 않다는 오류가 발생합니다.

분할 컴파일은 링크 과정에서 두 파일이 합쳐 지기 때문에,

컴파일 과정에서는 두 모듈이 각각 개별적으로 떨어져 있기 때문에 이런 오류가 발생합니다.

따라서 이런 때는 다른 모듈에 외부 변수 num1과 num2가 있다고 컴파일러에게 알려주어야 합니다.

이때 사용하는 예약어가 바로 extern입니다.

extern으로 다른 모듈에서 사용하자


사용법은 아래와 같이 작성해주시면 됩니다

extern int num1, num2

그러면 컴파일러는 이를 보고 다른 모듈에 num1과 num2변수가 존재한다고 미리 알 수 있게 됩니다.

만약 첫 번째 모듈에서 외부 변수 num1과 num2을 선언하고 이를 두 번째 모듈에서 쓰려고 하면,

두 번째 모듈에서 extern int num1, num2와 같이 작성해주시면 됩니다.

또한 이때 첫 번째 모듈에서 외부 변수를 '정의'한다고 이야기하고

두 번째 모듈에서 외부 변수를 '선언'한다고 이야기합니다.

마치 함수의 선언과 실제 구현과 같은 것이며, 이제부턴 이 둘을 구분해서 말해주시기 바랍니다.

그럼 위의 코드를 바꿔서 작성해봅시다.

#include <stdio.h>

void inputNUM();
void showNUM();
void exchangeNUM();

int num1, num2;

int main() {
	inputNUM();
	showNUM();
	exchangeNUM();
	showNUM();
	return 0;
}

void inputNUM() {
	printf("숫자 두개를 입력해주세요. : ");
	scanf("%d%d", &num1, &num2);
}

#include <stdio.h>

extern int num1, num2;

void showNUM() {
	printf("NUM1 : %d, NUM2 : %d\n", num1, num2);
}
void exchangeNUM() {
	int tmp;

	tmp = num1;
	num1 = num2;
	num2 = tmp;
}

이처럼 외부변수를 사용해서 더 간결하게 코드를 작성할 수 있습니다.

그런데 이렇게 여러 모듈에서 외부 변수를 사용할 때 주의해야할 점이 있습니다.

바로 여러 모듈에서 동일한 이름의 외부변수를 사용할 때입니다.

예를 들어 모듈 1에서 num변수를 외부 변수로 선언했다고 가정해봅시다.

그리고 모듈 2에서 이를 extern으로 사용하도록 했다고 합시다.

 

이러면 위에서처럼 모듈2에서 num변수를 사용할 수 있을 것입니다.

그런데 이때 모듈3에서 다시 외부 변수 num을 선언하면 어떻게 될까요?

모듈2는 num변수가 중복되어있다고 인식하기 때문에 오류가 발생하게 됩니다.

정적 외부 변수로 중복을 방지하자


이런 상황을 방지하기 위해서 외부 변수를 자신이 포함된 모듈에서만 사용하도록 할 수 있습니다.

이를 외부 정적 변수라고 이야기하며, 이를 정의하는 법은 다음과 같습니다.

static int num;

위와 같이 외부 정적 변수를 모듈1에 정의하면, 이를 모듈2에선 모듈3에있는 num변수를 공유합니다.

이런 외부 정적변수를 사용하면 사용 범위가 하나의 모듈로 제한되기 때문에, 특정 데이터를

하나의 모듈에 선언해놓고 독립적으로 관리해서 다른 모듈에서 수정할 수 없도록 할 수 있습니다.

 

이것으로 전처리와 분할 컴파일에 대한 설명은 끝마치도록 하겠습니다.

또한 c언어의 기본적인 문법 설명은 이것으로 마지막입니다.

수고하셨습니다.

반응형

'프로그래밍 언어 > C언어' 카테고리의 다른 글

C언어 20. 전처리와 분할 컴파일-1  (0) 2020.07.27
C언어 19. 파일 입출력-2  (0) 2020.07.24
C언어 18. 파일 입출력-1  (0) 2020.07.23
C언어 17. 응용자료형  (0) 2020.07.23
C언어 16. 변수의 영역  (0) 2020.07.22