티스토리 뷰

/* 본 포스팅은 최호성 저자의 '독하게 시작하는 C프로그래밍' 교재를 참조로 작성되었음을 먼저 알려드립니다. */


 변수의 본질은 메모리이고, 상수의 본질은 메모리에 저장되는 정보 그 자체를 의미한다. 변수와 상수는 기본적으로 수(자료)이므로 모두 자료형 개념이 적용된다. 


# 형 한정어 const


 형한정어 const는 변수를 '상수화'하는 역할을 한다. 변수를 변하지 않는 숫자로 만드는 것이다. 변수의 본질이 메모리임을 생각할 때 쉽게 생각하면 어떤 메모리를 '읽기 전용'메모리로 만들어주는 기능을 제공하는 것이다.


 형한정어 const는 중요한 정보가 들어 있는 메모리를 보호하기 위한 것으로도 생각할 수 있다. 그러나 const 예약어는 변수를 선언할 때 사용한다. 즉, 처음에는 쓰기 가능한 변수였다가 몇몇 연산을 끝낸 후 나중에 상수화되는 것은 아니라는 말이다. 그렇다면 변수가 선언 및 정의될 때부터 상수화를 해야 하는 이유는 대체 무엇일지에 대해 고민해야 한다.


 상수화하는 이유는 '유지보수를 쉽게 하기 위함'이다. 그리고 컴파일러의 입장에서는 줄어든 변수의 수만큼 번역도 유리하며 성능향상의 효과도 가질 수 있다. const가 붙는지 여부에 따라 프로그램의 성능이 달라지기도 하고 실수를 줄여줄 안전장치가 되어주기도 한다. 아래의 코드를 예시로 살펴보자.

#include <stdio.h>

int main(void)
{
	int nInput = 0;

	printf("시험점수를 입력해주세요: ");
	scanf("%d", &nInput);

	if (nInput >= 80)	
		printf("합격입니다.\n");
	else
		printf("불합격입니다.\n");
	
	return 0;
}

 위 예제가 기술적으로나 문법적으로나 잘못된 것은 없다. 그러나 '유지보수' 측면에서 보면 문제가 있다. 만약 시험 합격 기준이 바뀌게 되어 합격 기준 점수가 70점으로 하향됐다고 생각해보자.


 위의 예제는 10줄 밖에 되지 않아 유지보수에 아무런 문제가 없을 수도 있겠지만, 기준 점수가 쓰이는 곳이 여러 군데인데다 코드는 엄청나게 길고 심지어 여러 명이서 작업하는 경우라면 충분히 문제가 된다. 일일이 그 부분을 찾아 점수를 70점으로 바꿔줄 수는 없는 노릇이다. 여기서의 문제는 80이라는 합격조건을 리터럴 상수로 기술한 것이다. 실력 있는 개발자라면 아래와 같이 코드를 작성했을 것이다.

#include <stdio.h>

int main(void)
{
	// 상수화된 변수 선언 및 정의
	// 문법적으로 nCut의 값은 변경할 수 없다.
	const int nCut = 80;
	int nInput = 0;

	printf("시험점수를 입력해주세요: ");
	scanf("%d", &nInput);

	// '80'이라는 확정된 숫자 대신, 
	// '합격 기준 점수'라는 상대적 의미를 부여해서 코드를 작성
	if (nInput >= nCut)	
		printf("합격입니다.\n");
	else
		printf("불합격입니다.\n");
	
	return 0;
}

 이처럼 상수형 변수를 사용해서 합격점수를 표기하면, 유지보수가 훨씬 수월해진다. 일일이 찾아서 수정할 필요가 없는 데다 결정적으로 무언가 빼먹거나 놓칠 가능성이 없다. 단지 변수의 선언 및 정의에서 초깃값을 70로 수정하기만 하면 모든 게 끝나기 때문이다. 게다가 문법적으로 변수의 값이 중간에 변경되는 일은 절대 있을 수 없기에 맘편히 사용할 수 있다. 만약 수정을 시도하면 아래와 같은 오류 메시지가 등장한다.

error C2166: l-value가 const 개체를 지정합니다.

그러므로 이 예제에서는 'nCut'라는 변수의 이름을 '합격점수'라는 뜻으로 해석할 수 있다. nCut이라 쓰고 커트라인으로 읽는다는 말이다. 프로그래머는 70이라는 숫자로 합격점수를 결정짓는 것이 아니라 '커트라인'과 같은 표현으로 프로그램을 작성하려고 노력해야 한다. 특정 숫자와 변수의 이름을 조합하여 좀 더 의미가 명확해 보이는 상수를 표현할 수 있는데, 이를 심볼릭 상수(symbolic constant)라고 한다.



# 상수형 포인터


 C언어로 장치를 제어할 목적이 아니라면 C언어는 오히려 비효율적인 개발방법이 될 수도 있는 세상이 왔다. 그 원인 중 하나가 바로 '포인터'인데, C언어의 포인터는 너무 강력해서, "허용하지 않는 편이 더 나았을 것들까지 허용하는 문제"가 있다. 그도 그럴 것이 메모리에 직접 접근하여 값을 쓸 수 있으니 충분히 위험할법 하다.


 때문에 다른 고급 언어들은 '포인터'를 아예 지원하지 않거나 안전한 사용형식으로만 사용할 수 있도록 제한하는 제3의 문법을 지원하고 이름도 다른 것으로 바꾼다. 대표적으로 C++의 '참조자(reference type)'가 그러하다.


 더 큰 문제는 포인터를 너무 남발할 경우 C언어를 기계어로 번역할 때 효율적인 기계어 코드가 될 수 있도록 번역하기가 어렵다는 점이다. 또한 프로그램의 동시성(병렬처리나 멀티스레딩)을 심각하게 떨어뜨리기도 한다. 이 문제는 포인터를 상수화시킴으로써 어느 정도 완화가 가능하다. 아래의 코드를 살펴보자.

#include <stdio.h>
int main(void)
{
	char szBuffer[32] = { "I'm a human." };

	// 가리키는 대상을 상수화한 포인터 변수 선언 및 정의
	const char *pszBuffer = szBuffer;

	// 문자 배열의 내용은 변경할 수 있다.
	szBuffer[0] = 'i';

	// 포인터가 가리키는 대상을 간접지정할 수는 있지만
	// l-value로는 사용할 수 없다.
	*pszBuffer = 'i';
	
	return 0;
}

이 예시 또한 l-value가 const 개체를 지정한다는 오류메시지가 등장한다. const가 수식하는 대상은 pszBuffer라는 포인터 자체가 아니라 포인터가 가리키는 대상이다. 포인터로 간접지정 연산을 수행하면 포인터가 가리키는 대상을 변수화할 수 있다. 만일 상수화된 포인터가 아니라 일반 포인터라면 그 대상 메모리에 쓰기를 수행(l-value로 활용)해도 문법적으로 아무 문제가 없다.


 그러나 상수형 포인터는 쓰기를 허용하지 않는다. return문 위에 있는 *pszBuffer = 'i' 문이 오류가 나는 이유도 상수화된 대상에 쓰기를 시도했기 때문이다. 물론 szBuffer 배열 전체가 읽기 전용 메모리로 둔갑한 것은 아니다. 다만 이 배열에 접근할 수 있는 또 다른 방법인 pszBuffer 포인터에 대해서, 읽기는 허용하지만 쓰기(혹은 변조)를 허용하지 않을 뿐이다.


 한 함수 내에서 상수형 포인터를 지역변수로 선언해서 사용할 일은 거의 없다. 그러나 코드가 두 개의 함수로 분할된 상황을 생각하면 얘기가 달라진다. 특히, 피호출자 함수가 주소를 매개변수로 받는 Call by reference 형식인 경우 피호출자 함수의 연산으로 인해 호출자의 메모리가 변경될 가능성이 없도록 문법적으로 강제화하는 노력이 필요하다.


 아래 예제의 PrintString( )함수는 puts( )함수를 이용해 매개변수로 전달받은 주소에 저장된 문자열을 '읽어서 출력'만 한다. 따라서 매개변수가 가리키는 대상에 쓰기를 시도할 일이 문법적으로 없다.

#include <stdio.h>

// 매개변수가 상수형 포인터다. 따라서 함수에서 포인터가 가리키는
// 대상 메모리에 쓰기를 시도할 수 없다.
void PrintString(const char *pszParam)
{
	puts(pszParam);
}

int main(void)
{
	char szBuffer[32] = { "I'm a human" };

	// 함수를 호출하더라도 szBuffer의 내용은 변하지 않을 것이다.
	// 매개변수로 넘길 때 const로 읽기만 가능하게 설정을 해뒀기 때문에.
	PrintString(szBuffer);
	PrintString("No. You are a dog");

	return 0;
}

 함수 내부에서 대상 메모리에 쓰기를 할 일이 없다는 확신이 있다면 위의 예제처럼 매개변수를 상수형 포인터로 선언하는 것이 중요하다. const 예약어는 낭떠러지의 '난간'과도 같다. 위험이 도사리고 있는 포인터에 난간이 되어줌으로써 프로그램의 안전성을 높이는 역할을 한다.


 아래 예제는 형한정어 const가 수식하는 대상이 달라지는 경우를 설명한다.

#include <stdio.h>

int main(void)
{
	int nData = 10;

	//포인터가 가리키는 대상을 상수화한다.
	const int *pnData = &nData;
	//포인터 변수 자체를 상수화한다.
	int* const pnNewData = &nData;

	// 아래 두 구문은 모두 에러
	*pnData = 20;
	pnNewData = NULL;
	return 0;
}

 const int *pnData = &nData; 구문은 우리가 위에서 봤던 것과 같이 포인터 변수가 '가리키는 대상'이 상수화 된다. 하지만 그 아래에 있는 int* const pnNewData = &nData; 구문은 '포인터 변수 그 자체'를 상수화한다. 즉, 포인터가 가리키는 메모리는 수정할 수 있으나 포인터가 변해서 다른 대상을 가리킬 수는 없는 상태가 된다.


 만약 const int* const pnNewData = &nData; 라고 수정하면, 포인터가 가리키는 대상도 그리고 그 자신도 모두 상수화된다. 따라서 간접지정한 대상을 변경할 수도 없고 포인터가 다른 대상을 가리키도록 변경할 수도 없다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday