의미적인 제약(const 키워드가 붙은 객체는 외부 변경을 불가능하게 한다)을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 지켜준다는 점이 const의 주된 사용 이유이다.
클래스 바깥에서 전역 혹은 네임스페이스 유효범위의 상수 선언(정의)하는 데 사용도 가능하다(2장 참고). 또한 파일, 함수, 블록 유효범위에서 전역(static)으로 선언한 객체에도 const를 붙일 수 있다. 또한 포인터 자체를 상수로 혹은 포인터가 가르키는 데이터를 상수로 지정할 수 있다.
char greeting[] = "Hello";
char* p = greeting; //비상수 포인터, 비상수 데이터
const char* p = greeting; //비상수 포인터, 상수 데이터
char* const p = greeting; //상수 포인터, 비상수 데이터
const char* const p = greeting; //상수 포인터, 상수 데이터
개발자에 따라 포인터가 가리키는 대상을 상수로 만들 때 const를 사용하는 스타일이 다르다. 둘다 같은 의미이다.
const int a = 5;
int const b = 5;
STL 반복자(iterator)는 포인터 기반이기에, 기본적인 동작 원리가 T* 포인터와 비슷한데 반복자를 const로 선언하는 일은 포인터를 상수로 선언하는 것(T* const)이다. 반복자가 가르키는 대상 자체는 변경이 가능하지만(*it = 10;), 반복자가 가르키는 대상이 변경이 불가능한 반복자가 필요하다면 STL에는 const_iterator가 있다.
vector<int> a;
for (int i = 0; i < 2; i++)
a.emplace_back(i);
vector<int>::iterator it = a.begin();
vector<int>::const_iterator c_it = a.begin();
const vector<int>::iterator cit = a.begin();
*it = 10; //문제 없음
it++; //문제 없음
*c_it = 10; //에러(*c_it은 상수처리되어 변경 불가)
c_it++; //문제 없음
*cit = 10; //문제 없음
cit++; //에러(cit는 상수이기에 변경 불가)
const 사용을 생각해 볼 곳
함수 반환 값을 상수로 반환할때
함수 반환 값을 상수로 정해 주면, 안전성이나 효율을 포기하지 않고 사용자측의 에러를 줄일 수 있다.
const Rational operator*(const Rational& lhs, const Rational& rhs);
반환 값이 const일 필요가 있나 싶긴 한데 const 선언 시 개발자가 실수를 방지 할 수 있다.
Rational a, b, c;
if((a * b) = c)
위와 같은 경우 분명 개발자는 if((a * b) == c)로 생각하고 코딩했겠지만, 오타가 발생 할 때 const 가 없다면 a * b의 값은 c로 대입이 될 것이다. 하지만 반환 값을 const로 설정한다면, 에러로 오타를 미리 알 수 있다.
상수 멤버 함수
멤버 함수에 붙는 const의 역할은 해당 멤버 함수가 상수 객체에 대해 호출될 함수이다 라는 사실을 알려주는 것이다.
const 멤버 함수가 중요한 이유가 두가지있다.
1.클래스의 인터페이스를 이해하기 좋게 한다.
객체를 변경할 수 있는 함수는 무엇이고, 변경할 수 없는 함수는 무엇인가를 개발자가 바로 알 수 있다.
2.const를 통해 상수 객체를 사용할 수 있다.
객체 전달을 상수 객체에 대한 참조자(reference_to_const)로 가능한데(20장 참고) 이 기능을 사용하려면 const 멤버 함수, 즉 상수 멤버가 준비되어 있어야 한다.
const 키워드의 유무 차이만 있는 멤버 함수들은 오버로딩(*주 1)이 가능하다.
class TextBox
{
public:
const char& operator[](size_t pos) const
{ return text[pos]; }
char& operator[](size_t pos)
{ return text[pos]; }
private:
std::string text;
}
여기서 생각해 봐야 할것이 반환 값으로 char가 아닌 char&을 사용했다는 것이다.
char operator[](size_t pos)
{ return text[pos]; }
만약 위 처럼 반환값이 char인 경우
TextBlock tb("ABC");
tb[0] = 'D'; //에러
기본 데이터 타입을 반환하는 함수의 반환 값을 변경하는 일은 있을 수 없고, 통한다고 하더라도 "반환 시 값에 의한 반환"을 수행하는 C++의 성질(20장 참고)에서 막힌다. printf나 cout에서는 문제 없이 사용할 수 있으니 대학교 수준에선 문제 없었다.
const 멤버 함수안에서는 멤버 변수를 변경할 수 없는데, 변경할 수 있는 방법자체는 있다.
class TEST
{
public:
size_t length() const
{
testa = 10; //에러 (const 함수이기 때문에 멤버 변수 변경 불가)
testb = 10; //문제 없음 (mutable이 선언 되어 있기 때문에)
}
private:
size_t testa;
mutable size_t testb;
};
상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
class TextBox
{
public:
const char& operator[](size_t pos) const
{ return text[pos]; }
char& operator[](size_t pos)
{ return text[pos]; }
private:
std::string text;
}
위 코드에서 operator[]함수를 두 번 사용 하는데, 단순 값 반환 함수라 그렇지 이런 저런 코드가 붙여지면 코드의 길이는 상수/비상수 버전 으로 두배가 커진다. inline함수도 코드가 길어지면 비효율적이고(30장 참고), 컴파일 시간, 프로그램의 크기 증가로 인해 골치가 아파진다. 여기서 생각해야 하는 것은 두 함수는 반환값이 const이냐 아니냐의 차이일 뿐 완전히 같은 코드이라는 것이다. 캐스팅이 필요하지만, 안전성도 유지하면서 코드 중복을 피하는 방법은 비상수 함수가 상수 버전을 호출하도록 구현하는 것이다.
class TextBox
{
public:
const char& operator[](size_t pos) const
{ return text[pos]; }
char& operator[](size_t pos)
{ return const_cast<char&>(static_cast<const TextBox&>(*this)[pos]); }
private:
std::string text;
}
비상수 함수가 호출되면 상수 함수를 호출하고 반환값을 반환하는 코드이다.
두개의 캐스팅을 했는데 처음 static_cast는 *this에 const TextBox& 즉 const를 붙이는 작업이고, 두 번째 const_cast는 상수 operator[]의 반환된 const char&에서 const를 떼는 작업이다.
반대로 상수 함수에서 비상수 함수를 호출하는건 어떠한가 생각할 수 있는데 굉장히 비효율적이다.
우선 상수 멤버 함수란 컴파일러가 해당 객체의 상태를 바꾸지 않겠다는 의미인데 상수 멤버에서 비상수 멤버를 호출하게 된다면, 수정하지 않겠다고 약속한 객체가 변경될 위험이 있다.
이것만은 잊지 말자
const를 붙여 선언하면 컴파일러가 사용상 에러는 잡아내는데 도움을 준다.
컴파일러 입장에서 보면 비트수준 상수성을 지켜야 하지만, 개발자는 개념적인(논리적인) 상수성을 사용해야 한다.
상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우 코드 중복을 피하는게 좋은데, 비상수 버전이 상수 버전을 호출하도록 만들자(cast).
(*주 1) 오버로딩 vs 오버라이딩
오버로딩(overloading) : 같은 이름의 메소드(함수)를 여러개 가지면서 매개변수의 유형과 개수가 다르게 하는 기술
간단하게 템플릿 생각하면 됨
오버라이딩(overriding) : 상위 클래스가 갖고 있는 메소드(함수)를 하위 클래스가 재정의해서 사용하는 것
언리얼에서 tick, nativetick같은거 생각하면 됨
'서적 정리 > Effective C++' 카테고리의 다른 글
8.예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2021.12.10 |
---|---|
7.다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2021.12.10 |
6.컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2021.12.10 |
5.C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2021.12.10 |
4.객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2021.12.10 |
2.#define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2021.12.10 |
1.C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2021.12.10 |
차례 (0) | 2021.12.10 |
댓글