상수 객체 (const object)
C++에서 상수(const) 객체는 그 값이 변경될 수 없는 객체입니다.
상수 객체는 객체를 생성할 때 값이 할당되며, 이후에는 변경되지 않습니다.
const int num = 10; // 'num'은 상수 객체이며, 값은 10입니다.
위 드에서 const 키워드는 num 변수가 상수임을 나타냅니다. 이후에는 num의 값을 변경할 수 없습니다.
상수 객체는 객체 지향 프로그래밍에서 매우 중요한 역할을 합니다.
상수 객체를 사용하면, 값이 변경되면 안되는 상황에서 값이 변경되는 것을 방지할 수 있습니다.
또한, 상수 객체를 이용하면 컴파일러가 값을 임베딩(embedding)할 수 있어서, 코드의 실행 속도를 높일 수 있습니다.
코드의 실행 속도를 높일 수 있는 이유
상수 객체는 일반적으로 값을 변경할 수 없기 때문에, 컴파일러는 이러한 객체를 메모리에 상수로 저장하도록 최적화할 수 있습니다.
이를 통해 실행 속도를 향상시키고, 프로그램의 안정성을 높일 수 있습니다
1. 메모리 접근 시간 단축
상수 객체의 값을 직접 코드에 삽입함으로써, 상수 객체의 값을 참조하기 위한 메모리 접근을 생략할 수 있습니다.
이는 프로그램 실행 시간을 단축시키는데 큰 도움이 됩니다.
const int num = 10;
int x = num + 5;
컴파일러는 num + 5를 15로 대체할 수 있습니다.
이렇게 상수 값을 바로 사용하면, 변수나 메모리를 참조하지 않고 값을 얻을 수 있으므로 실행 속도를 높일 수 있습니다.
2. 코드 크기 감소
상수 객체를 사용하면, 상수 값이 중복해서 사용될 때 메모리에 상수 값의 복사본이 저장되는 것을 방지할 수 있습니다.
대신, 컴파일러는 상수 값을 임베딩해서 사용하므로 코드 크기가 감소하게 됩니다.
상수 값을 메모리에 저장할 경우, 상수 값이 여러 번 사용될 때마다 해당 메모리 주소를 참조하므로 코드가 길어질 수 있습니다.
하지만 상수 값을 임베딩하면 코드가 더 간결해지고, 프로그램의 크기가 작아질 수 있습니다.
결론적으로, 상수 객체를 이용하면 컴파일러가 값을 임베딩할 수 있어서, 코드의 실행 속도를 높일 수 있습니다.
이를 통해 프로그램의 성능을 향상시킬 수 있습니다.
C++에서 상수 객체는 전역 상수(constant)와 지역 상수(constant)로 나뉘어서 메모리의 서로 다른 영역에 저장됩니다.
전역 상수는 프로그램이 실행될 때 프로그램의 데이터 섹션(data section)에 저장됩니다.
데이터 섹션은 전역 변수와 정적(static) 변수도 저장하는 영역으로, 프로그램이 종료될 때까지 메모리에 남아있습니다.
지역 상수는 해당 변수가 정의된 블록이 실행될 때 스택(stack) 메모리 영역에 저장됩니다.
블록이 실행될 때마다 해당 상수의 값을 읽어와서 사용하며, 블록이 끝나면 해당 상수는 스택에서 제거됩니다.
상수 멤버함수 (const member function)
상수 멤버함수란 클래스 멤버함수 선언 뒤에 'const' 키워드로 선언 된 함수로,
상수 멤버 함수 안에서는 모든 멤버를 상수 처리한다는 특징이 있습니다.
여기서 상수 처리한다는 것은 함수 내부에서 사용되는 변수 값을 바꿀 수 없다는 의미입니다.
#include <iostream>
class Point
{
private:
int x, y;
public:
Point(int a = 0, int b= 0) : x(a), y(b) {}
void set(int a, int b)
{
x= a;
y= b;
}
void print() const // 상수 멤버 함수 : 안에서 모든 멤버를 상수처리
{
std::cout<< x << " , " << y <<std::endl;
}
};
int main()
{
const Point p(1,1);
p.x = 10; //error 값 바꿀 수 없음
p.set(10,20); //error 값 바꿀 수 없음
p.print(); // error
return 0;
}
위 코드에서 객체 p 는 const object 로 선언되었습니다. 따라서 내부 멤버 변수인 x 값을 직접 변경하거나, 변수 값을 변경하는 기능이 구현된 set함수를 호출할 경우 error 가 발생합니다.
그런데 print 함수는 값을 변경하지 않음에도 호출하지 못합니다.
그 이유는 p이 상수 객체이므로 호출하는 함수 또한 상수함수임이 보장되어야 하기 때문입니다.
이것을 보장하기 위해, 멤버함수를 const 선언 해줌으로써 컴파일러가 해당 함수가 변수 값을 변경하지 않는 const 함수라는 것을 알수 있도록 해주면 됩니다.
사실, 객체의 상태를 변경하지 않는 '모든' 멤버함수는 (getXXX 류), 반드시 const 멤버함수가 되어야 합니다.
#include <iostream>
struct Rect
{
int ox, oy, width, height;
Rect(int x= 0, int y= 0, int w= 0, int h = 0): ox(x), oy(y), width(w), height(h){}
int getArea() const { return width*height;}
};
void foo(const Rect& r) // call by value overhaed --> const & 가 좋음
{
int n = r.getArea();
}
int main()
{
Rect r(0,0, 10, 10);
int n= r.getArea();
foo(r);
return 0;
}
위 예제는 getArea 함수가 const 선언 되어야 하는 이유에 대해 설명하는 코드 입니다.
foo() 라는 함수에서 Rect 의 인스턴스를 통해 getArea 함수를 호출하는 경우를 생각해봅시다.
이때 오버헤드 측면에서 call by value 형태로 인자를 받아오는 것보다는, const 객체 참조로 받는 것이 타당합니다.
이 과정에서 r이 '상수 객체'로서 복사(?) 되므로 getArea() 가 반드시 const 선언 되어있어야 r.getArea()를 호출 할 수 있게 됩니다.
논리적 상수성
Case1. mutable 이용
#include <iostream>
#include <stdio.h>
class Point
{
private:
int x, y;
// mutable 멤버 변수 : 상수 멤버함수 안에서도 값 변경 가능
mutable char cache[16];
mutable bool cache_valid = false;
public:
Point(int a = 0, int b= 0) : x(a), y(b) {}
const char* toString() const// 상수 멤버함수여야 하는 이유
{
if(cache_valid == false){
sprintf(cache,"%d, %d", x,y ); // 멤버함수(cache, cache_valid를 변경할 수 없음)
cache_valid = true;
}
return cache;
}
};
위 코드 예제는 sprintf 의 오버헤드가 크다고 가정하고, cache 값이 변경됐을 경우에만 버퍼를 새롭게 써서 리턴하는 구조를 가져가는 형태로 구현을 한 것입니다.
그런데 toString 함수는 const 함수로 내부에서 멤버 변수들인 cache 및 cache_valid 값을 변경할 수 없습니다.
이 때, 이 두 변수들을 mutable 로 선언해주면, const 함수 내에서도 값을 변경할 수 있게 됩니다.
이렇게 함으로써 const 함수를 유지하면서도 내부적으로 변경이 필요한 변수들의 값을 바꿀 수 있습니다.
Case2. 포인터 이용
struct Cache
{
char cache[16];
bool cache_valid = false;
};
class Point
{
private:
int x, y;
Cache* pCache;
public:
Point(int a = 0, int b= 0) : x(a), y(b) {
pCache = new Cache;
}
~Point(){
delete pCache;
}
const char* toString() const// 상수 멤버함수여야 하는 이유
{
if(pCache->cache_valid == false){
sprintf(pCache->cache,"%d, %d", x,y ); // 멤버함수(cache, cache_valid를 변경할 수 없음)
pCache->cache_valid = true;
}
return pCache->cache;
}
};
const 함수 내에 일부 변수를 변경하고자 할때 사용할 수 있는 두번째 방법은, 변경이 필요한 변수들을 별도 자료구조(Cache)에 담아두고, 이를 가리키는 포인터를 클래스 멤버 변수로 선언하는 것입니다.
이 객체가 생성자에서 Cache 인스턴스를 할당하고, const 함수 내에서는 이 인스턴스를 통해서 변수에 접근 및 변경을 할 수 있습니다.
const return
const를 반환하면, 반환된 값이 수정되지 않도록 보장합니다. 이는 다른 함수나 코드에서 반환된 값을 수정하는 것을 방지하므로 코드 안정성을 높이는 데 도움이 됩니다. 또한, 반환된 값을 const로 선언함으로써, 해당 값을 수정하는 코드가 컴파일러에서 에러로 처리되므로 유지 보수성을 높이는 데 도움이 됩니다.
또한, const를 반환하면 임시 객체에 대한 복사를 피할 수 있습니다. 예를 들어, 다음과 같은 함수가 있다고 가정해 봅시다.
const string getGreeting() {
return "Hello";
}
이 함수가 호출되면, 문자열 "Hello"를 반환하는 임시 객체가 생성됩니다. 이 객체는 const로 반환되기 때문에, 함수 외부에서 수정될 수 없습니다. 만약 반환 값이 const가 아니라면, 임시 객체가 생성되는 것 외에도, 복사 생성자가 호출되어 복사본이 생성되어야 하므로 성능 저하를 초래할 수 있습니다.따라서, C++에서 const를 반환하는 것은 코드의 안정성, 유지 보수성, 성능 향상 등 여러 가지 이점을 제공합니다.
다른 예제를 살펴보겠습니다.
class Car {
public:
const Engine& getEngine() const {
return engine;
}
private:
Engine engine;
};
Car 클래스는 Engine 클래스의 객체를 포함하고 있습니다.
getEngine() 함수는 const 로 선언되어 있으며, Engine 객체를 반환합니다.
이 함수는 const 로 선언되었기 때문에 Car 객체가 const 로 선언되었을 때 호출될 수 있습니다.
const Car myCar;
const Engine& myEngine = myCar.getEngine();
위 코드에서 myCar는 const로 선언되었으므로, getEngine() 함수는 const 버전이 호출됩니다. getEngine() 함수가 반환하는 Engine 객체는 const로 선언되어 있으므로, myEngine 변수도 const로 선언되어야 합니다.
이렇게 함으로써, myEngine 객체가 수정되지 않도록 보장됩니다.
만약 getEngine() 함수가 const를 반환하지 않는다면, myCar 객체가 const로 선언되었을 때 getEngine() 함수를 호출할 수 없으며, myEngine 변수를 const로 선언하지 않으면 myEngine 객체가 수정될 수 있습니다.
따라서, const를 반환하는 함수를 사용함으로써 코드의 안정성과 유지 보수성을 높일 수 있습니다.
const pointer
C++에서 const 포인터는 포인터 자체가 변경될 수 없다는 것을 나타냅니다. 즉, 포인터가 가리키는 메모리의 값을 변경할 수는 있지만, 포인터가 가리키는 주소 자체는 변경할 수 없습니다.
const 포인터를 사용하면, 코드 안정성과 유지 보수성을 높일 수 있습니다. 예를 들어, 다음과 같은 코드를 생각해봅시다
void printValues(const int* values, int size) {
for (int i = 0; i < size; ++i) {
cout << values[i] << " ";
}
}
위 코드에서 values 매개변수는 const int*로 선언되어 있습니다. 이는 values 포인터가 가리키는 메모리의 값을 변경할 수 있지만, values 포인터가 가리키는 주소 자체는 변경할 수 없다는 것을 나타냅니다.
이렇게 함으로써, printValues() 함수는 values 포인터를 통해 전달된 값들을 변경하지 않으므로, 호출한 쪽에서 전달한 값을 보존할 수 있습니다. 또한, const int* 타입을 사용하면, 잘못된 포인터 연산이나 잘못된 메모리 참조 등과 같은 문제를 방지할 수 있습니다.
또한, const 포인터는 포인터를 상수로 취급할 때도 유용합니다. 다음과 같은 예를 생각해보세요.
int value = 10;
const int* const pValue = &value;
위 코드에서 pValue는 const int* const로 선언되어 있습니다.
이는 pValue 포인터가 가리키는 주소 자체와, pValue 포인터가 가리키는 메모리의 값을 변경할 수 없다는 것을 나타냅니다.
이렇게 함으로써, pValue는 상수 포인터가 되어, 가리키는 값을 변경할 수 없으므로, 값이 변경되지 않도록 보장할 수 있습니다
'C++ > syntax' 카테고리의 다른 글
const char* 와 string class (0) | 2023.03.30 |
---|---|
[C++] shared_ptr 사용시 주의할 점 (0) | 2023.03.27 |
virtual table (가상함수 테이블)이 생성 및 참조 되는 시점 (0) | 2023.03.17 |
[C++] enum 과 enum class (0) | 2023.01.02 |
[C++] Lambda expression(람다 표현식) (0) | 2022.12.29 |