C++ 객체 모델, this 포인터, 그리고 friend 키워드 심층 분석

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; // 오류: 접근 권한 없음
}

태그: C++ 객체모델 this포인터 friend 상수함수

6월 12일 23:57에 게시됨