const 위치에 따른 구문의 의미
네이버 지식iN에 이런 질문이 올라와서 이에 답변을 달았던 적이 있다.
예제보니까 parameter에
- [네이버 지식iN] C++ int*const* 변수이름 은 도대체 뭐죠??
void foo(int* const* bar)
이렇게 있더라고요;; 이게 definition으로서 가능해요? 만일 가능하다면 도대체 이렇게 정의하는 목적이 뭔지;;
그리고 만일 이게 된다면 const* int* bar도 되나요?? 답변 부탁드리겠습니다 ㅠ
본디 상수 특성을 지정하는 const
키워드는 그 놓인 위치에 따라 약간의 미묘한 의미 차이를 갖는다. 본 포스팅에서는 const 키워드가 들어간 구문의 정확한 의미를 정리해 보고자 한다.
const의 선언 위치
const
는 선언 문장 내 여러 곳에 놓일 수 있다. 그 때문에 의미가 혼동되고 읽기 불편할 수도 있다.
먼저 변수를 선언할 때에는 다음과 같이 const
가 올 수 있다.
[const] 자료형 [const] [포인터] [const] 변수명;
함수를 선언할 때에는 다음과 같이 const
가 올 수 있다.
[const] 자료형 [const] [포인터] 함수명(매개변수…) [const] { 함수본문 }
const
키워드가 어느 곳에 붙든 결국 원칙은 이것이다.
const
가 걸리는 위치에서는 값 변경이 안 된다.
그렇다면 값 변경이 안 되는 경우(어디서 값 변경이 안 되는가?)를 찾는 것이 관건이다. 질문 원문에서 소개 된 3가지 경우를 예로 들어보면,
const int ** foo;
int * const * foo;
int ** const foo;
이 3가지의 자료형이 있다. const
가 붙는 위치가 조금씩 다름을 알 수 있는데 이 차이를 구분할 수 있다면 C의 고수이다.
위의 3가지 형식을 괄호로 쳐 본다면 다음과 같이 묶을 수 있다.
(const int) ** a;
(int * const) * a;
(int **) const a;
'const 자료형 **'의 해설
1번은 const int
형에 대한 이중 포인터이다. 즉 int
형 상수에 대한 이중 포인터라는 뜻이다. 보다 쉽게 나타낸다면 이런 과정으로 만들어지는 타입이다.
{
const int a = 10;
const int * b = &x;
const int ** c = &y;
}
위와 같이 &b
의 자료형이 const int **
형이라 보면 된다. 알맹이 값인 a = 10
이라는 값이 상수처리 되는 것이다.
반복하면,
{
c : const int ** 형 (값 변경 O)
*c : const int * 형 (값 변경 O)
**c : const int 형 (값 변경 X)
}
이므로,
int main(int argc, char * argv[]) {
const int a = 10;
const int * b = &a;
const int ** c = &b; // 이게 const int ** 형식임.
c = 0x00000000; // 대입시 에러 없음
*c = 0x00000000; // 대입시 에러 없음
**c = 5; // 여기서 에러.
return 0;
}
'자료형 * const *'의 해설
2번은 int *
형이 갖는 주소 값을 상수로 다룬다는 뜻이다. 즉 int *
형 상수에 대한 포인터라는 뜻이다. 보다 쉽게 나타낸다면 이런 과정을 통해 만들어지는 타입이다.
{
int a = 10;
int * const b = &a;
int * const * c = &b;
}
위와 같이 &b
의 자료형이 int * const *
형이라 보면 된다. 여기서 알맹이 값인 a = 10
은 수정이 자유로우나, 이것이 위치한 주소(a
의 주소)를 갖고 있는 b
가 상수처리 되는 것이다.
반복하면,
{
c : int * const * 형 (값 변경 O)
*c : int * const 형 (값 변경 X)
**c : int 형 (값 변경 O)
}
이므로,
int main(int argc, char * argv[]) {
int a = 10;
int * const b = &a; // 여기서 &b가 100% 완벽한 int * const * 형을 갖게 된다.
int * const * c = &b; // 이게 int * const * 형이다.
c = 0x00000000; // 대입시 에러 없음
*c = 0x00000000; // 여기서 에러.
**c = 5; // 대입시 에러 없음
return 0;
}
'자료형 ** const'의 해설
3번은 int **
타입 자체가 상수라는 뜻이다, 즉 이런 과정을 통해 만들어지는 타입이다.
{
int a = 10;
int * b = &a;
int ** const c = &b;
}
위와 같이 &b
의 자료형을 상수로 처리하는 것이 int ** const
형이라 보면 된다.
{
c : int ** const 형 (값 변경 X)
*a : int * 형 (값 변경 O)
**a : int 형 (값 변경 O)
}
이므로,
int main(int argc, char * argv[]) {
int a = 10;
int * b = &a;
int ** const c = &b;
c = 0x00000000;// 여기서 에러.
*c = 0x00000000; // 대입시 에러 없음
**c = 5; // 대입시 에러 없음
return 0;
}
마무리
지식 iN에도 올렸지만, 다시 한번 표로 정리하면 아래와 같이 요약 가능하다.
선언된 형식 | const int ** a | int * const * a | int ** const a |
---|---|---|---|
a 의 형식 | const int ** 형 (값 변경 O) | int * const * 형 (값 변경 O) | int ** const 형 (값 변경 X) |
*a 의 형식 | const int * 형 (값 변경 O) | int * const 형 (값 변경 X) | int * 형 (값 변경 O) |
**a 의 형식 | const int 형 (값 변경 X) | int 형 (값 변경 O) | int 형 (값 변경 O) |
덧 1. 문자열 상수와 const
strcpy
같은 함수를 보면 두 번째 인수가 const char *
형으로 되어 있을 것이다. 풀이해 보면, 그 문자열의 주소는 경우에 따라 가변적이지만 문자열이 갖는 자체의 내용만큼은 변함이 없다. 이런 뜻이다.
이를 테스트해보자.
void test(const char * str) {
str = "이건 뭥미?"; // 대입 가능
}
str
의 주소가 가리키는 문자열이 매개변수를 통해 넘어온 문자열이 아니라 전혀 새로운 문자열로 대치가 가능하다. 주소 자체에는 상수성이 없기 때문이다.
void test(const char * const str) {
str = "이건 뭥미?"; // 대입 불가
}
문자열의 내용 자체가 불변인 것은 위와 같다. 또한 문자열을 가리키는 주소 또한 상수성을 갖고 있으므로 전혀 다른 문자열로 대치하는 것 또한 불가하다.
void test(char * str) {
str = "ABCD"; // 대입 가능
str[0] = 'A'; // 대입 가능
}
문자열의 내용과 문자열이 가리키는 주소 모두 상수성이 없다. 자유롭게 대치 및 수정 가능하다.
void test(const char * str) {
str = "ABCD"; // 대입 가능
str[0] = 'A'; // 대입 불가
}
문자열이 가리키는 주소 자체는 상수성이 없으므로 전혀 다른 문자열로 대치가 가능하다. 그러나 문자열의 내용에 대해서는 상수성이 있으므로 const char *
로 선언된 문자열은 일부 내용의 수정이 불가하다.
덧 2. 멤버 함수와 const
C 언어에서 사용되는 const
는 이 정도에서 끝나지만 C++로 넘어올 경우 하나가 더 있다. 멤버 함수의 scope에 붙이는 const
키워드이다.
class Integer {
private:
int value;
public:
int getInteger() const;
void setInteger(int value);
}
int Integer::getInteger() const {
return this->value;
}
void Integer::setInteger(int value) {
this->value = value;
}
위와 같이 멤버 함수의 scope에도 상수성을 부여할 수 있다. 이것의 의미는...
이 함수는 멤버 변수의 값을 읽을 수는 있어도 수정할 수는 없다.
이다. 만일,
int Integer::getInteger() const {
this->value = -1; // 오류 발생
return this->value;
}
위와 같은 코드를 실행한다면 const
가 부여된 scope 내에서 멤버 변수의 수정을 시도하므로 오류가 발생할 것이다. 또한,
int Integer::getInteger() const {
this->setInteger(-1); // 오류 발생
return this->value;
}
또한 위와 같이 scope에 const
속성이 없는 다른 함수를 호출하는 것도 오류가 발생한다. 상수 함수에서 호출 가능한 멤버함수는 같은 상수 함수 뿐이다. 반대로 scope에 const
가 없는 함수는 상수 함수이든 비 상수 함수이든 호출이 가능하다. 이렇게 선언하는 이유는 객체의 상수성 때문이다.
{
Integer i1;
const Integer i2;
i1.setInteger(-1); // 가능
i1.getInteger(); // 가능
i2.setInteger(-1); // 불가
i2.getInteger(); // 가능
}
객체 i1
은 상수성이 없고, 객체 i2
는 상수성이 있다. 상수성이 있는 객체는 멤버 변수의 수정이 불가능하므로 scope에 const
가 명시된 멤버 함수만이 호출 가능하다. 따라서 i2.setInteger(-1);
와 같은 호출은 불가능하다.