블로그 이미지
Kanais
Researcher & Developer 퍼즐을 완성하려면 퍼즐 조각들을 하나 둘씩 맞춰나가야 한다. 인생의 퍼즐 조각들을 하나 둘씩 맞춰나가다 보면 인생이란 퍼즐도 완성되는 날이 오려나...?

calendar

1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

Notice

2015. 4. 16. 11:41 Programming/C/C++

참고 서적 : Effective C++ - Meyers / 곽용재 옮김 / 피어슨에듀케이션코리아

 

1. C++에 왔으면 C++의 법을 따릅시다.

가장 근본적인 것들을 다루고 있는 단원.


항목 3 : 낌새만 보이면 const를 들이대 보자!

Const의 면모에 대해 생각해 볼 때 정말 멋지다고 말할 수 있는 부분이 있다면 아마도 ‘의미적인 제약’(const 키워드가 붙은 객체는 외부 변경을 불가능하게 한다)을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 점일 것입니다.

  Const 키워드는 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는 데 쓸 수 있습니다. 그뿐 아니라 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있습니다. 클래스 내부의 경우에는, 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있습니다. 포인터는 기본적으로는 포인터 자체를 상수로, 혹은 포인터가 가리키는 데이터를 상수로 지정할 수 있는데, 둘 다 지정할 수도 있고 아무것도 지정하지 않을 수도 있습니다.

char greeting[] = "Hello";

char *p = greeting;              // 비상수포인터, 비상수데이터

const char *p = greeting;        // 비상수포인터, 상수데이터

charconst p = greeting;        // 상수포인터, 비상수데이터

const charconst 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 charoperator[] (std::size_t position) const

       {

             return text[position];

       }

       // 비상수객체에대한operator[]

       charoperator[] (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  상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데이때 비상수 버전이 상수 버전을 호출하도록 만드세요.

posted by Kanais