참고 서적 : Effective C++ - Meyers / 곽용재 옮김 / 피어슨에듀케이션코리아
1. C++에 왔으면 C++의 법을 따릅시다.
가장 근본적인 것들을 다루고 있는 단원.
항목 3 : 낌새만 보이면 const를 들이대 보자!
Const의 면모에 대해 생각해 볼 때 정말 멋지다고 말할 수 있는 부분이 있다면 아마도 ‘의미적인 제약’(const 키워드가 붙은 객체는 외부 변경을 불가능하게 한다)을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 점일 것입니다.
Const 키워드는 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는 데 쓸 수 있습니다. 그뿐 아니라 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있습니다. 클래스 내부의 경우에는, 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있습니다. 포인터는 기본적으로는 포인터 자체를 상수로, 혹은 포인터가 가리키는 데이터를 상수로 지정할 수 있는데, 둘 다 지정할 수도 있고 아무것도 지정하지 않을 수도 있습니다.
char greeting[] = "Hello"; char *p = greeting; // 비상수포인터, 비상수데이터 const char *p = greeting; // 비상수포인터, 상수데이터 char* const p = greeting; // 상수포인터, 비상수데이터 const char* const p = greeting; // 상수포인터, 상수데이터 |
const키워드가 *표 왼쪽에 있으면 포인터가 가리키는 대상이 상수인 반면, const가 *표의 오른쪽에 있는 경우엔 포인터 자체가 상수입니다. Const가 *표의 양쪽에 다 있으면 포인터가 가리키는 대상 및 포인터가 다 상수라는 뜻이죠.
아래의 함수들이 받아들이는 매개변수 타입은 모두 똑같습니다.
void f1(const Widget *pw); // f1은상수Widget 객체에 대한 포인터를 매개변수로 취합니다. void f2(Widget const *pw); // f2도그렇고요. |
STL 반복자(iterator)는 포인터를 본뜬 것이기 때문에, 기본적인 동작 원리가 T* 포인터와 진짜 흡사합니다. 어떤 반복자를 const로 선언하는 일은 포인터를 상수로 선언하는 것(즉, T* const 포인터)과 같습니다. 반복자는 자신이 가리키는 대상이 아닌 것을 가리키는 경우가 허용되지 않지만, 반복자가 가리키는 대상 자체는 변경이 가능합니다. 만약 변경이 불가능한 객체를 가리키는 반복자(즉, const T* 포인터의 STL 대응물)가 필요하다면 const_iterator 를 쓰면 됩니다.
std::vector<int> vec; ... const std::vector<int>::iterator iter = vec.begin(); // iter는T* const처럼동작합니다. *iter = 10; // OK, iter가가리키는대상을변경합니다. ++iter; // 에러! iter는상수입니다. std::vector<int>::const_iterator cIter = vec.begin(); // cIter는const T*처럼동작합니다. *cIter = 10; // 에러! *cIter가상수이기때문에안됩니다. ++cIter; // 이건문제없습니다. cIter를변경하니까요. |
상수 멤버 함수
멤버 함수에 붙는 const 키워드의 역할은 “해당 멤버 함수가 상수 객체에 대해 호출될 함수이다”라는 사실을 알려 주는 것입니다. 이런 함수가 중요한 이유가 두 가지 있습니다. 첫째는 클래스의 인터페이스를 이해하기 좋게 하기 위해서인데, 그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자 쪽에서 알고 있어야 하는 것입니다. 둘째는 이 키워드를 통해 상수 객체를 사용할 수 있게 하자는 것인데, 코드의 효율을 위해 아주 중요한 부분이기도 합니다. C++ 프로그램의 실행 성능을 높이는 핵심 기법 중 하나가 객체 전달을 ‘상수 객체에 대한 참조자(reference-to-const)’로 진행하는 것이기 때문이죠. 그런데 이 기법이 제대로 살아 움직이려면 상수 상태로 전달된 객체를 조작할 수 있는 const 멤버 함수, 즉 상수 멤버 함수가 준비되어 있어야 한다는 것이 바로 포인트입니다.
const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능합니다.
class TextBlock { public: ... // 상수객체에대한operator[] const char& operator[] (std::size_t position) const { return text[position]; } // 비상수객체에대한operator[] char& operator[] (std::size_t position) { return text[position]; } private: std::string text; } |
위처럼 선언된 TextBlock의 operator[]는 다음과 같이 쓸 수 있습니다.
TextBlock tb("Hello"); // TextBlock::operator[]의비상수멤버를호출합니다. std::cout << tb[0]; const TextBlock ctb("World"); // TextBlock::operator[]의상수멤버를호출합니다. std::cout << ctb[0]; |
실제 프로그램에서 상수 객체가 생기는 경우는 ① 상수 객체에 대한 포인터 혹은 ② 상수 객체에 대한 참조자로 객체가 전달될 때입니다. 위의 ctb 예제는 이해를 돕기 위한 용도의 성격이 짙고, 아래의 예제가 더 실제의 경우와 가깝습니다.
void print(const TextBlock& ctb) { std::cout << ctb[0]; // TextBlock::operator[]의상수멤버를호출합니다. } |
Operator[]를 ‘오버로드(overload)’해서 각 버전마다 반환 타입을 다르게 가져갔기 때문에, TextBlock의 상수 객체와 비상수 객체의 쓰임새가 달라집니다.
// 좋습니다. 비상수버전의TextBlock 객체를읽습니다. std::cout << tb[0]; // 역시문제없죠. 비상수버전의TextBlock 객체를씁니다. tb[0] = 'x'; // 이것도됩니다. 상수버전의TextBlock 객체를씁니다. std::cout << ctb[0]; // 컴파일에러발생! 상수버전의TextBlock 객체에대해쓰기는안됩니다. ctb[0] = 'x'; |
주의할 것이 하나 있는데, 넷째 줄에서 발생한 에러는 순전히 operator[]의 반환타입(return type) 때문에 생긴 것이란 점입니다.
하나 더 눈여겨 볼 부분이 있습니다. 만약 operator[]가 그냥 char를 반환하게 만들어져 있으면, 다음과 같은 문장이 컴파일되지 않게 됩니다.
tb[0] = 'x'; |
왜 그럴까요? 기본제공 타입을 반환하는 함수의 반환 값을 수정하는 일은 절대로 있을 수 없기 때문입니다. 수정되는 값은 tb.text[0]의 사본이지, tb.text[0] 자체가 아니라는 거죠.
어떤 멤버 함수가 상수 멤버(const)라는 것이 대체 어떤 의미일까요? 여기에는 굵직한 양대 개념이 자리 잡고 있습니다. 하나는 비트수준 상수성[bitwise constness, 다른 말로 물리적 상수성(physical constness) 이라고도 함]이고, 또 하나는 논리적 상수성(logical constness)입니다.
비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야(정적 멤버는 제외) 그 멤버 함수가 ‘const’임을 인정하는 개념입니다.
논리적 상수성이란 개념을 부르짖는 사람들의 주장은 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것입니다.
상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
캐스팅이 필요하긴 하지만, 안전성도 유지하면서 코드 중복을 피하는 방법은 비상수 operator[]가 상수 버전을 호출하도록 구현하는 것입니다.
이것만은 잊지 말자!
l const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 개체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.
l 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인) 상수성을 사용해서 프로그래밍해야 합니다.
l 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.
'Programming > C/C++' 카테고리의 다른 글
[Effective C++] C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2015.04.16 |
---|---|
[Effective C++] 객체를 사용하기 전에 반드시 그 객체를 초기화 하자 (0) | 2015.04.16 |
[Effective C++] #define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2015.04.16 |
[Effective C++] C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2015.04.16 |
[C] 30자리 곱셈 연산 프로그램 (0) | 2015.04.16 |