C++ 객체 메모리 모델
C++에서 클래스의 멤버 변수와 멤버 함수는 메모리 상에서 완전히 분리되어 저장됩니다. 객체의 크기(sizeof)를 계산할 때 포함되는 것은 오직 비정적(non-static) 멤버 변수뿐입니다.
멤버 변수가 하나도 없는 빈 클래스의 경우, 컴파일러는 각 객체가 고유한 메모리 주소를 가질 수 있도록 1바이트의 공간을 할당합니다. 반면, 정적 멤버 변수와 모든 멤버 함수(정적 및 비정적)는 클래스당 하나만 존재하며 모든 객체가 공유하므로 개별 객체의 메모리 크기에 영향을 주지 않습니다.
this 포인터의 메커니즘과 활용
클래스의 비정적 멤버 함수는 메모리에 단 하나의 인스턴스만 생성되어 모든 객체가 공유합니다. 그렇다면 공유된 함수는 어떻게 자신이 어떤 객체에 의해 호출되었는지 구분할 수 있을까요? C++는 이를 위해 숨겨진 포인터인 this 포인터를 제공합니다.
this 포인터는 현재 멤버 함수를 호출한 객체 자신의 주소를 가리키며, 모든 비정적 멤버 함수 내부에 암시적으로 전달됩니다. 개발자가 직접 선언할 필요 없이 바로 사용할 수 있습니다.
1. this 포인터의 주요 용도
- 이름 충돌 해결: 매개변수와 멤버 변수의 이름이 동일할 때
this를 통해 명확히 구분합니다. - 객체 자체 반환 (체인 호출):
return *this;를 사용하여 객체 자신을 반환하면 메서드 체이닝(Method Chaining)이 가능해집니다.
class Account {
private:
double balance;
public:
Account(double initialBalance) {
// 매개변수와 멤버 변수 이름이 같을 때 this 포인터로 구분
this->balance = initialBalance;
}
// 객체 자신을 참조로 반환하여 체인 호출 지원
Account& deposit(const Account& other) {
this->balance += other.balance;
return *this;
}
double getBalance() const {
return balance;
}
};
void testAccount() {
Account acc1(1000.0);
Account acc2(500.0);
// 체인 호출: acc1에 acc2를 연속으로 예치
acc1.deposit(acc2).deposit(acc2);
std::cout << "acc1 잔액: " << acc1.getBalance() << std::endl; // 2000.0
}
위 코드에서 deposit 함수가 참조(&)를 반환하지 않고 값을 반환했다면, 매 호출마다 객체의 복사본이 생성되어 원본 acc1에는 변경 사항이 반영되지 않았을 것입니다.
참고: this 포인터의 본질은 포인터 상수(Pointer Constant)입니다. 즉, 포인터가 가리키는 주소(방향)는 변경할 수 없지만, 해당 주소가 가리키는 값(객체의 멤버)은 수정할 수 있습니다.
널 포인터(Null Pointer)를 통한 멤버 함수 호출
C++에서는 널 포인터를 통해서도 멤버 함수를 호출할 수 있습니다. 단, 함수 내부에서 this 포인터를 역참조하여 멤버 변수에 접근하는 경우 프로그램이 충돌할 수 있으므로, 방어적 코딩을 위해 널 체크가 필요합니다.
class Sensor {
private:
int deviceId;
public:
void printType() {
// this 포인터를 사용하지 않으므로 널 포인터로 호출해도 안전함
std::cout << "This is a Sensor device." << std::endl;
}
void readDeviceId() {
// this 포인터 역참조 전 방어 코드
if (this == nullptr) {
std::cout << "Invalid sensor instance." << std::endl;
return;
}
// 컴파일러는 내부적으로 this->deviceId로 해석함
std::cout << "Device ID: " << deviceId << std::endl;
}
};
void testNullPointer() {
Sensor* ptr = nullptr;
ptr->printType(); // 정상 출력
ptr->readDeviceId(); // 방어 코드에 의해 충돌 방지
}
상수 함수와 상수 객체 (const 한정자)
1. 상수 함수 (Const Member Function)
멤버 함수의 선언 끝에 const를 붙이면 해당 함수는 '상수 함수'가 됩니다. 상수 함수 내부에서는 객체의 멤버 변수 값을 수정할 수 없습니다. 이는 const가 실제로는 숨겨진 this 포인터를 const ClassType* const this 형태로 제한하기 때문입니다.
단, 예외적으로 mutable 키워드로 선언된 멤버 변수는 상수 함수 내부에서도 수정이 가능합니다.
class DatabaseConfig {
private:
int maxConnections;
mutable int queryCount; // mutable로 선언
public:
void executeQuery() const {
// maxConnections = 200; // 오류: 상수 함수 내에서 일반 멤버 변수 수정 불가
queryCount++; // 정상: mutable 변수는 수정 가능
}
};
2. 상수 객체 (Const Object)
객체 선언 앞에 const를 붙이면 '상수 객체'가 됩니다. 상수 객체는 생성된 후 상태를 변경할 수 없으므로, 오직 상수 함수만 호출할 수 있습니다. 일반 멤버 함수는 내부에서 멤버 변수를 수정할 가능성이 있기 때문에 호출이 차단됩니다.
void testConstObject() {
const DatabaseConfig config;
// config.maxConnections = 100; // 오류: 상수 객체의 멤버 변수 수정 불가
config.executeQuery(); // 정상: 상수 함수 호출
}
friend 키워드를 통한 캡슐화 예외 처리
객체지향 프로그래밍에서는 캡슐화를 위해 중요한 데이터를 private으로 숨깁니다. 하지만 특정 상황에서는 외부의 함수나 클래스가 이 프라이빗 데이터에 접근해야 할 때가 있습니다. C++는 friend 키워드를 통해 이러한 예외적인 접근 권한을 부여합니다.
1. 전역 함수를 friend로 선언
특정 전역 함수가 클래스의 프라이빗 멤버에 접근할 수 있도록 권한을 줍니다.
class Vault {
// 전역 함수를 친구로 선언
friend void auditVault(Vault* v);
private:
std::string secretAssets;
public:
Vault() : secretAssets("Gold Bars") {}
std::string publicInfo = "Bank Branch A";
};
void auditVault(Vault* v) {
std::cout << "Public: " << v->publicInfo << std::endl;
std::cout << "Private: " << v->secretAssets << std::endl; // 접근 가능
}
2. 클래스 전체를 friend로 선언
특정 클래스의 모든 멤버 함수가 현재 클래스의 프라이빗 멤버에 접근할 수 있도록 합니다.
class SecureServer; // 전방 선언
class AdminPanel {
public:
void checkLogs(SecureServer* server);
};
class SecureServer {
// AdminPanel 클래스 전체를 친구로 선언
friend class AdminPanel;
private:
std::string systemLogs = "Critical system events...";
};
void AdminPanel::checkLogs(SecureServer* server) {
std::cout << "Accessing logs: " << server->systemLogs << std::endl;
}
3. 특정 멤버 함수만 friend로 선언
외부 클래스의 모든 함수가 아닌, 특정 멤버 함수에만 접근 권한을 부여하여 보안을 강화합니다.
class NetworkNode; // 전방 선언
class Monitor {
public:
void ping(NetworkNode* node);
void shutdown(NetworkNode* node); // 이 함수는 접근 권한 없음
};
class NetworkNode {
// Monitor 클래스의 ping 멤버 함수만 친구로 선언
friend void Monitor::ping(NetworkNode* node);
private:
std::string ipAddress = "192.168.1.100";
};
void Monitor::ping(NetworkNode* node) {
std::cout << "Pinging " << node->ipAddress << std::endl; // 접근 가능
}
void Monitor::shutdown(NetworkNode* node) {
// std::cout << node->ipAddress; // 오류: 접근 권한 없음
}