블로그 글 TIL day8 에서도 한 번 언급했었는데, 중요하다고 생각이 들어 다시 정리해보려고 한다.
effective c++의 항목 3을 많이 참고하여 정리했다.
- 리마인드
- const 가 함수 앞에 있다면, 반환값을 상수화 시키는 것
- const 가 함수 뒤에 있다면, 멤버 변수의 수정을 막는 것
1. 포인터변수의 const
- 순서대로
- 비상수 포인터, 비상수 데이터
- 비상수 포인터, 상수 데이터
- 상수 포인터, 비상수 데이터
- 상수 포인터, 상수 데이터
#include <iostream>
using namespace std;
int main()
{
char greeting[] = "Hello\n";
char* p = greeting;
const char* p = greeting;
char* const p = greeting;
const char* const p = greeting;
}
- 왜?
- const 키워드가 * 왼쪽에 있다면 포인터가 가리키는 대상은 상수 (char 가 const)
- const char* greeting; 과
- char const* greeting; 은 같은 의미이다.
- const 키워드가 * 오른쪽에 있다면 포인터 자체가 상수 (p 가 const)
- char* const greeting;
- const 키워드가 * 왼쪽에 있다면 포인터가 가리키는 대상은 상수 (char 가 const)
- 아래 코드가 const의 위치에 따라 불가능한 작업을 보여준다. (주석 부분이 불가능한 작업)
- 상수 포인터는 포인터가 가리키는 위치 바꾸지 못하며,
- 상수 데이터는 포인터가 가리키는 위치의 값을 바꾸지 못한다.
#include <iostream>
using namespace std;
int main()
{
int value1[] = { 1,2,3 };
int value2[] = { 10,20,30 };
int* p1 = value1;
p1 = value2;
*p1 = 100;
const int* p2 = value1;
p2 = value2;
//*p2 = 100;
int* const p3 = value1;
//p3 = value2;
*p3 = 100;
const int* const p4 = value1;
//p4 = value2;
//*p4 = 100;
}
2. STL iterator
- STL의 반복자는 포인터를 본떠 만든 것이기 때문에 포인터의 동작과 굉장히 유사하다.
- 아래의 예시를 보면 이해할 수 있다.
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vec = { 10, 20, 30 };
const vector<int>::iterator it1 = vec.begin(); //상수 포인터
*it1 = 100;
//++it1;
vector<int>::const_iterator it2 = vec.begin(); //상수 데이터
//*it2 = 100;
++it2;
}
3. 함수에서의 const
- const를 제대로 붙이지 않는다면, 아래와 같은 상황이 생길 수 있다.
- * 연산자의 반환값이 상수 객체가 아니라면
- a * b에 대입이 가능해진다.
class Rational { ... };
Rational operator*(const Rational& lhs, const Rational& rhs); //반환값을 상수로 만들지 않음
...
Rational a;
Rational b;
Rational c;
(a * b) = c; //에러가 발생하지 않음
4. 매개변수에서의 const
- const 타입의 지역객체와 똑같이 동작한다.
- 가능한 많이 붙이자 -> 수정하지 못하게 할 것이라면 꼭 const를 붙이자
5. 상수 멤버 함수
- 상수 멤버 함수: 멤버 함수 뒤에 const가 붙은 함수
- 멤버함수에 붙는 const 키워드의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다" 라는 것을 알려준다.
- 중요한 이유
- 클래스의 인터페이스를 이해하기 좋게 하려고
- 클래스로 만들어진 객체를 변경할 수 있는 함수와 변경할 수 없는 함수가 무엇인지 사용자가 알 수 있다.
- 상수 객체를 사용할 수 있게 하려고
- 클래스의 인터페이스를 이해하기 좋게 하려고
- const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다.
const char& operator[] (std::size_t position) const //상수 객체에 대한 []
{
return text[position];
}
char& operator[] (std::size_t position) //비 상수 객체에 대한 []
{
return text[position];
}
- 위의 함수를 통해 아래 코드를 실행하면, 다음과 같은 결과를 얻을 수 있다.
- 마지막 줄 코드가 에러가 발생하는 이유는 상수 데이터를 수정하려고 했기 때문이다.
TextBlock tb("Hello");
const TextBlock ctb("World");
std::cout << tb[0]; //상수버전
tb[0] = 'x'; //비상수 버전
std::cout << ctb[0]; //상수버전
ctb[0] = 'x'; //에러!
- 추가) 왜 위의 오버로딩 된 함수에서 char& 타입으로 반환을 했을까?
- char이 반환타입이라면, 복사된 값이 return 된다.
- 위의 코드에서본 예시처럼 tb[0] = 'x'; 이런식으로 수정할 일이 있을때, 원본을 수정해야지 사본을 수정하는 건 우리가 원하는 동작이 아니기 때문이다.
- 그래서 char& 로 반환하지 않으면, tb[0] = 'x'; 코드에서 컴파일 에러가 발생한다.
멤버함수가 상수 멤버라는 것의 의미
- 비트수준 상수성 (물리적 상수성)
- 어떤 멤버함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 const임을 인정하는 개념
- 그러나 이를 위반함(상수 객체가 상수 멤버함수를 호출했는데, 값이 변함) 에도 오류가 발생하지 않을 수 있는 경우가 존재한다. (아래코드 참고)
class CTextBlock
{
public:
...
char& operator[](std::size_t position) const
{
return pText[position];
}
...
private:
char* pText;
};
...
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J'; // cctb가 "Jello" 가 된다!
- 논리적 상수성
- 비트수준 상수성을 해결하기 위해 나온 개념 (결과적으로 이 방식을 통해 코드를 짜야한다.)
- 상수 멤버라고 해서 객체의 한 비트도 수정하지 못하게 하는 것이 아니라, 몇 비트정도는 바꿀 수 있지만, 사용자 측에서만 알아채지 못하게 하자는 개념
- 아래 코드의 개념처럼 유효성 체크를 통해 CTextBlock의 상수 객체에 대해서는 문제가 발생하지 않도록 하는 개념이다.
- 그러나, length 함수를 구현함에 있어서 문제가 발생한다. (상수 멤버함수에서 값 수정함)
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
std::size_t textlength; //바로 직전에 계산한 텍스트 길이
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid)
{
textlength = std::strlen(pText); //그러나 에러 발생! const 함수인데 값 수정함
lengthIsValid = true;
}
return textlength;
}
- 해결책
- mutable 키워드를 사용하는 것
- mutable은 상수성이 있는 곳에서 멤버변수의 수정을 허락해주는 키워드이다.
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
mutable std::size_t textlength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid)
{
textlength = std::strlen(pText); //제대로 동작한다
lengthIsValid = true;
}
return textlength;
}
6. 상수 멤버 및 비상수 멤버 함수에서 코드 중복을 피하기
- 위의 결과로 상수성을 지킬 수 있게 해줬지만, 코드의 중복이라는 문제가 남아있다.
- 우리가 [] 연산자를 오버로딩 했던 것을 보면, 같은 동작을 하면서 const 여부만 다른 함수가 존재하게 되고 코드의 중복이 발생하게 된다.
const char& operator[](std::size_t position) const
{
//경계검사
//접근 데이터 로깅
//자료 무결성 검증
return text[position];
}
const char& operator[](std::size_t position) const
{
//경계검사
//접근 데이터 로깅
//자료 무결성 검증
return text[position];
}
- 이 문제를 해결하기 위해서는 비상수 버전에 상수버전을 call하도록 캐스팅을 해서 구현하는 방법이다.
- 캐스팅은 최대한 안쓰는 것이 좋지만, 코드의 중복을 줄이기 위해서는 사용하는건 어쩔 수 없다.
- const_cast를 통해 상수 operator [] 의 반환 값에서 const를 떼어내기
- static_cast를 통해 *this타입에 const를 붙이기
class TextBlock
{
public:
const char& operator[] (std::size_t position) const
{
...
return text[position];
}
char& operator[] (std::size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
}
7. 결론
- const를 붙일 수 있는 곳에 최대한 붙여서 컴파일러가 에러를 잡아주도록 하자
- 컴파일러 딴에서는 비트수준 상수성을 지켜야 하지만, 코드를 짤때는 논리적인 상수성으로 코드를 짜자
- 상수 멤버함수와 비상수 멤버 함수가 기능적으로 똑같다면, 코드의 중복을 피하기 위해 비상수 멤버함수에 캐스팅을 통해 상수 멤버함수를 호출하자
'C++' 카테고리의 다른 글
C++ TIL day 12 (1) | 2025.01.02 |
---|---|
C++ TIL day 11 (포인터 연산 문제) (1) | 2024.12.31 |
C++ TIL day 10 (0) | 2024.12.30 |
C++ 템플릿 - 헤더파일에서 구현하자 (0) | 2024.12.30 |
C++ 코딩 스탠다드 (2) | 2024.12.30 |