콘텐츠로 건너뛰기

C++을 공부할 때 잘 이해안되던 코드들

1. Const와 포인터, 그리고 멤버 함수

const의 위치에 따라 의미가 달라지는 것은 C++ 입문자가 가장 헷갈려 하는 부분이다. 핵심은 const 바로 왼쪽(없으면 오른쪽)이 상수화된다는 것이다.

#include <iostream>

class SoSimple {
private:
    int num;
public:
    SoSimple(int n) : num(n) {}

    // 체이닝(Chaining)을 위한 참조 반환
    SoSimple& AddNum(int n) {
        num += n;
        return *this;
    }

    // 일반 멤버 함수
    void SimpleFunc() {
        std::cout << "SimpleFunc: " << num << std::endl;
    }

    // const 멤버 함수: 이 함수 내에서는 멤버 변수(num)를 변경할 수 없다.
    // const 객체는 const 함수만 호출할 수 있다.
    void SimpleFunc() const {
        std::cout << "const SimpleFunc: " << num << std::endl;
    }
};

void YourFunc(const SoSimple &obj) {
    // obj가 const 참조로 전달되었으므로, const 함수만 호출 가능하다.
    obj.SimpleFunc(); 
}

int main() {
    int val1 = 10, val2 = 20, val3 = 30;

    // 1. 포인터가 가리키는 대상이 상수 (값 변경 불가, 주소 변경 가능)
    const int * ptr = &val1; 
    
    // 2. 포인터 자체가 상수 (값 변경 가능, 주소 변경 불가)
    int * const ptr2 = &val2; 

    // 3. 둘 다 상수 (값, 주소 모두 변경 불가)
    const int * const ptr3 = &val3;

    SoSimple obj1(2);       
    const SoSimple obj2(7); // const 객체 생성

    obj1.SimpleFunc();      // 일반 함수 호출
    obj2.SimpleFunc();      // const 함수 호출 (오버로딩)

    YourFunc(obj1);
    YourFunc(obj2);

    return 0;
}

2. 참조자(Reference)와 배열

“배열의 레퍼런스”와 “레퍼런스의 배열”은 다르다. C++ 표준 문법상 레퍼런스의 배열은 만들 수 없지만, 배열을 가리키는 레퍼런스는 선언할 수 있다.

#include <iostream>

int main() {
    int arr[3] = {1, 2, 3};

    // 배열 포인터와 유사한 문법. 
    // 크기가 3인 int 배열에 대한 참조자 선언
    int (&ref)[3] = arr; 

    ref[0] = 10;
    ref[1] = 20;
    ref[2] = 30;

    // ref를 통해 원본 arr이 수정됨
    std::cout << arr[0] << ", " << arr[1] << ", " << arr[2] << std::endl; 

    return 0;
}

3. 동적 할당 (new & delete)

new는 메모리 공간 할당(malloc)과 동시에 생성자 호출(초기화)을 수행한다. 질문 답변: delete list가 아니라 delete[] list를 써야 하는 이유는 컴파일러에게 **”이 포인터가 가리키는 것이 단순 변수 하나가 아니라 배열 덩어리다”**라고 알려줘야 하기 때문이다. 그래야 배열의 모든 요소(N개)에 대해 소멸자를 호출하여 메모리 누수를 막을 수 있다.

#include <iostream>

int main() {
    // 1. 단일 변수 동적 할당
    int* p = new int;
    *p = 10;
    std::cout << "Single allocation: " << *p << std::endl;
    delete p; // 단일 해제

    // 2. 배열 동적 할당
    int arr_size;
    std::cout << "Array size: ";
    std::cin >> arr_size;

    // 런타임에 크기가 결정되는 배열 할당
    int *list = new int[arr_size]; 

    for (int i = 0; i < arr_size; i++) {
        list[i] = i * 10;
    }

    for (int i = 0; i < arr_size; i++) {
        std::cout << i << "th element: " << list[i] << std::endl;
    }

    // 배열 해제 시 반드시 []를 붙여야 함.
    // 그렇지 않으면 첫 번째 요소만 소멸되거나 정의되지 않은 동작 발생.
    delete[] list; 

    return 0;
}

4. 함수 객체 (Functor)와 operator()

operator()를 오버로딩하면 객체를 함수처럼 사용할 수 있다. 이를 **Functor(함수 객체)**라고 한다. 언제 쓰는가? 일반 함수와 달리 객체는 ‘상태(Member Variable)’를 가질 수 있다. 즉, 함수가 호출될 때마다 데이터를 누적하거나, 특정 설정값을 유지한 채로 동작해야 할 때 매우 유용하다. (STL의 정렬 기준이나 알고리즘 등에서 많이 쓰임).

#include <iostream>
using namespace std;

class WowCat {
public:
    // 객체를 함수처럼 호출할 때 실행됨 (인자 2개)
    int operator()(int arg1, int arg2) {
        return arg1 + arg2;
    }

    // 오버로딩 가능 (인자 3개)
    int operator()(int arg1, int arg2, int arg3);
};

int WowCat::operator()(int arg1, int arg2, int arg3) {
    return arg1 + arg2 + arg3;
}

int main() {
    WowCat obj;

    // 객체를 함수 이름처럼 사용
    cout << "Functor(2 args): " << obj(1, 2) << endl;       
    cout << "Functor(3 args): " << obj(1, 2, 3) << endl;    

    // 명시적 호출 (잘 안 씀)
    cout << "Explicit call: " << obj.operator()(2, 3) << endl; 

    // 멤버 함수 포인터 사용법 (복잡하지만 콜백 구현 시 사용됨)
    int (WowCat::*fp)(int, int) = &WowCat::operator();
    cout << "Function Pointer: " << (obj.*fp)(10, 100) << endl;

    return 0;
}

5. 클래스, 생성자, 그리고 깊은 복사(Deep Copy)

C++에서 클래스 내부에 포인터 변수가 있다면, 복사 생성자를 직접 정의해야 한다. 기본 복사 생성자는 포인터의 주소값만 복사하는 얕은 복사(Shallow Copy)를 수행하기 때문에, 소멸자에서 delete를 두 번 하려다 에러(Double Free)가 발생한다. 이를 해결하는 것이 깊은 복사(Deep Copy)다.

#include <iostream>
#include <cstring> // for strlen, strcpy

class PhotonCannon {
    int hp, shield;
    int coord_x, coord_y;
    int damage;
    char *name; // 동적 할당할 멤버 변수

public:
    PhotonCannon(int x, int y, const char *cannon_name);
    PhotonCannon(const PhotonCannon &pc); // 복사 생성자
    ~PhotonCannon(); // 소멸자
    void show_status();
};

// 일반 생성자
PhotonCannon::PhotonCannon(int x, int y, const char *cannon_name) {
    hp = shield = 100;
    coord_x = x;
    coord_y = y;
    damage = 20;
    
    // 깊은 복사를 위한 메모리 할당
    if (cannon_name) {
        name = new char[strlen(cannon_name) + 1];
        strcpy(name, cannon_name);
    } else {
        name = NULL;
    }
}

// 복사 생성자 (Deep Copy 구현)
PhotonCannon::PhotonCannon(const PhotonCannon &pc) {
    std::cout << "복사 생성자 호출! (Deep Copy)" << std::endl;
    hp = pc.hp;
    shield = pc.shield;
    coord_x = pc.coord_x;
    coord_y = pc.coord_y;
    damage = pc.damage;

    // 포인터 변수는 새로 메모리를 할당해서 내용물만 복사해야 한다.
    if (pc.name) {
        name = new char[strlen(pc.name) + 1];
        strcpy(name, pc.name);
    } else {
        name = NULL;
    }
}

// 소멸자
PhotonCannon::~PhotonCannon() {
    if (name)
        delete[] name; // 동적 할당 해제
    std::cout << "소멸자 호출" << std::endl;
}

void PhotonCannon::show_status() {
    std::cout << "Photon Cannon :: " << (name ? name : "No Name") << std::endl;
    std::cout << " HP : " << hp << std::endl;
}

int main() {
    PhotonCannon pc1(3, 3, "Cannon");
    
    // 복사 생성자 호출됨. pc1과 pc2는 서로 다른 name 메모리 공간을 가짐.
    PhotonCannon pc2 = pc1; 

    pc1.show_status();
    pc2.show_status();

    return 0;
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다