공부/C++

객체 복사, 이동 생성자, 이동 대입 연산자, 임시 객체, noexcept 키워드, 상속, 오버라이드, 가상 함수, 동적 바인딩, 가상 소멸자

월러비 2025. 6. 23. 20:10

짧은 설명

  • nullptr로 초기화를 한다면 반드시 문자열 생성때 null 체크를 해줘야한다.
  • strcpy_s : 버퍼 오버플로우를 방지하는 문자열 복사 함수
  • c_str() : 문자열 객체의 현재 값을 나타내는 null을 포함하는 배열의 포인터를 반환하는 함수
  • [] 연산자는 경계검사를 하지 않는다.

객체 복사

  • 이해가 안된다면 생성자를 만들때 기본생성자, 소멸자, 복사 생성자, 대입 연산자, 이동생성자, 이동 대입 연산자 모두 구현해라
  • l-value , r-value는 오버로딩 조건이 될 수 있다.

이동 생성자와 이동 대입 연산자

int num = 10;
  • l-value : 읽고 쓸 수 있는 변수
  • r-value : 임시객체, 리터럴 상수, 반환값
    • 메모리는 잡히지만 한번 사용하면 메모리가 버려진다.
    • 값을 할당할 수 없기에 r-value이다.
    • 반환값도 반환되는 값만 사용할 수 있기에 r-value 다.
class String {
public:
    String() { /*...*/ }
    String(const char* str) { /*...*/ }
    String(const String& rhs) { /*...*/ } //복사 생성자
    String(String&& rhs) noexcept // 이동 생성자 / r-value를 참조한것이다.
    {  
        cout << "String(String&&) : " << this << endl; //이동대입 연산자 호출 확인용 출력
        len = rhs.len;
        strData = rhs.strData;  // 포인터만 복사
        rhs.strData = NULL;     // 원본 무효화
    }

    ~String() { /*...*/ }

    String& operator=(const String& rhs) { /*...*/ }
    String& operator=(String&& rhs) {  // 이동 대입 연산자
        cout << "String &operator=(String&&) : " << this << endl;
        len = rhs.len;
        strData = rhs.strData;  // 포인터만 복사
        rhs.strData = NULL;     // 원본 무효화
        rhs.len = 0 //모든 멤버를 초기화 시켜주는것이 좋다.
        return *this;
    }

    char* GetStrData() const { /*...*/ }
    int GetLen() const { /*...*/ }

private:
    void alloc(int len) { /*...*/ }
    void release() { /*...*/ }

    char* strData;
    int len;
};

String getName() {
    cout << "===== 2 =====" << endl;
    String res("Doodle");
    cout << "===== 3 =====" << endl;
    return res;
}

int main() {
    String a;
    cout << "===== 1 =====" << endl;
    a = getName();  // 이동 대입 연산자 호출
    cout << "===== 4 =====" << endl;
}

  • r-value 참조 (&&)를 사용
    • l-value는 & 하나를 참조로 사용한다.
    • &&를 받는 복사 생성자도 오버로딩 조건이 된다.
    • l-value가 정의 안되어있다면 r-value 참조 오버로딩 된 매개변수로 들어가게 된다.
    • r-value 레퍼런스 변수에 l-value를 넣을 수 는 없다.
    • r-value 즉, 이동 생성자의 매개변수에는 const를 붙이면 안된다.
    • 초기화 후 임시 객체의 값은 0 또는 그에 준하는 수치로 초기화를 해줘야한다.
  • 깊은 복사 대신 포인터만 이동
    • 즉, 주소의 소유권을 이전하는 것이다.
    • 메모리를 새로 할당 할 필요 없다.
  • 원본 객체는 NULL로 설정하여 무효화
  • 불필요한 메모리 할당/해제를 많이 줄일 수 있다.

메모리 관리 규칙

  1. 할당과 해제의 쌍: new와 delete, new[]와 delete[] 반드시 쌍으로 사용
  2. 소유권 명확화: 각 객체가 자신의 메모리에 대한 책임 명확히
  3. RAII 원칙: 생성자에서 자원 획득(즉, 생성), 소멸자에서 해제
  4. 예외 안전성: 생성자에서 예외 발생 시 메모리 누수 방지

동적 할당 Rule of Three/Five

동적 메모리를 관리하는 클래스는 다음을 구현해야 한다. : 4, 5는 r-value 참조 생성 및 대입이다.

  1. 소멸자: 동적 할당된 자원 해제
  2. 복사 생성자: 깊은 복사 구현
  3. 복사 대입 연산자: 자기 대입 검사와 깊은 복사
  4. 이동 생성자: 효율적인 자원 이동
  5. 이동 대입 연산자: 효율적인 자원 이동 대입

임시 객체

MyClass obj; //l-value
obj = MyClass(매개변수);
  • MyClass(매개변수) : 잠시 메모리가 생겨났다가 대입이 끝나고 삭제되는 객체다.
    • 대입이 끝나고 소멸자가 호출되는 것이다.
    • 값을 할당할 수 없기에 r-value이다.

noexcept 키워드

  • 최적화할때 도움을 주는 키워드다.
  • 후에 예외처리에 사용된다.
  • 예외처리를 하지 않아도 된다고 사용자에게 알려주는 키워드다.
    • 컴파일러가 실행될때 예외처리에 관한 작동을 안해도 되도록 해주는 기능이다.
    • 해당 키워드가 붙은 함수가 오류가 생기면 프로그램이 강제로 정지된다.

사용자 정의 변환

  • 대체로 사용하지 않는 문법이기에 이런것이 있다고만 알아두면 좋다.
class Item {
public:
    Item() {
        cout << "Item()" << endl;
    }
    Item(int num) : num(num) {  // int를 Item으로 변환
        cout << "Item(int)" << endl;
    }
    Item(string name) : name(name) {  // string을 Item으로 변환
        cout << "Item(string)" << endl;
    }
    Item(int num, string name) : num(num), name(name) {
        cout << "Item(int, string)" << endl;
    }

private:
    int num;
    string name;
};

int main() {
    cout << "===== A =====" << endl;
    Item a1 = Item(1);      // 명시적 변환
    Item a2(2);             // 직접 초기화
    Item a3 = (Item)3;      // C 스타일 캐스팅
    Item a4 = 4;            // 암시적 변환 / ㅁ4(4) 로 생각하면 이해될것이다.
    Item a5, a6, a7;
    a5 = Item(5);
    a6 = 6;                 // 암시적 변환
    a7 = (Item)7;

    cout << "===== B =====" << endl;
    Item b4 = string("Stone");  // string에서 Item으로 변환

    cout << "===== C =====" << endl;
    Item c1 = Item(1, "Stone");
    Item c2(2, "Dirt");
    Item c4 = { 3, "Wood" };    // 중괄호 초기화
    Item c5, c6;
    c5 = Item(4, "Grass");
    c6 = { 5, "Water" };        // 중괄호 대입
}
  • 이렇게도 동작이 되지만 클래스의 암시적 형변환같은 애매모호한 코드는 작성하지 않는게 좋다.

변환 연산자

class Item {
public:
    Item() { }
    Item(int num) : num(num) { }
    Item(string name) : name(name) { }
    Item(int num, string name) : num(num), name(name) { }
	
		//operator int() : int형변환 연산자다.
    operator int() const {  // Item을 int로 변환
        cout << "Item::operator int()" << endl;
        return num;
    }
    //operator string() : string형변환 연산자다.
    operator string() const {  // Item을 string으로 변환
        cout << "Item::operator string()" << endl;
        return name;
    }

private:
    int num;
    string name;
};

int main() {
    Item i1(1, "Stone");
    int inum = i1;      // int inum = (int)i1;과 같음 / 역 형변환이다.
    string iname = i1;  // string iname = (string)i1;과 같음

    cout << inum << endl;   // 1
    cout << iname << endl;  // Stone
}

  • operator 타입() 형식으로 정의
  • 반환 타입을 명시하지 않음
  • const 멤버 함수로 구현하는 것이 일반적

explicit 키워드

  • 암시적 형변환을 막는 키워드다.
  • 의도하지 않은 암시적 변환 방지
  • 타입 안정성 향상
  • 명시적 변환만 허용
class SafeNumber {
public:
    explicit SafeNumber(int value) : value(value) {}
    explicit operator int() const { return value; }
private:
    int value;
};

// 사용 예
SafeNumber num1(10);           // OK
SafeNumber num2 = 20;          // 에러! explicit로 인해 암시적 변환 불가
SafeNumber num3 = SafeNumber(30); // OK, 명시적 변환

int x = static_cast<int>(num1);  // OK, 명시적 변환
int y = num1;                     // 에러! explicit로 인해 암시적 변환 불가

사용자 정의 변환 주의사항

  • 암시적 변환은 예상치 못한 동작 야기 가능
  • 가능하면 explicit 사용으로 명시적 변환만 허용
  • 변환의 의미가 명확한 경우에만 구현

상속

  • 부모클래스의 멤버를 물려받는 것이다.
  • 자식 클래스는 부모 클래스의 private에 접근할 수 없다.
  • 간접 참조 : 포인터, 레퍼런스
    • 변수들이다.
  • 업캐스팅 / 다운캐스팅 : 변수들의 형변환이다.
  • 절차 지향 : 순서대로 작업 처리
    • ~한다. → 함수의 목적
    • 함수들의 집합 처리 프로그래밍을 의미한다.
  • 객체 지향 : 주체 우선 처리
    • ~한다가 아니라 ‘누가’ 뭘 한다 처럼 주체가 중요한 프로그래밍이다.
    • 클래스들의 상호작용이다.

상속 기본

  • 상속은 기존 클래스의 특성을 물려받아 새로운 클래스를 만드는 객체지향 프로그래밍의 핵심 개념이다.
  • 코드 재사용성 향상: 공통 기능을 기본 클래스에 정의
  • 계층적 관계 표현: 현실 세계의 계층 구조를 프로그램으로 모델링
    • 계층 관계 : 파일시스템처럼 부모안에 자식안의 자식같이 계층이 나눠진것이다.
  • 유지보수 용이: 공통 기능 수정 시 한 곳만 수정

파생 클래스

  • 파생 클래스(Derived Class): 상속을 받는 클래스, 자식 클래스라고도 함
    • 기본 클래스(Base Class): 상속을 제공하는 클래스, 부모 클래스라고도 함
class 파생클래스명 : 접근지정자 상속받을 기본클래스명 {
    // 파생 클래스의 멤버들
};

class Animal {
public:
    void Breathe() { cout << "숨을 쉰다." << endl; }
    int age;
};

class Dog : public Animal {
public:
    void Walk() { cout << "걷는다." << endl; }
};

class Sparrow : public Animal {
public:
    void Fly() { cout << "난다." << endl; }
};

int main() {
    Dog d;
    d.age = 7;
    d.Breathe();  // 상속받은 멤버 함수
    d.Walk();     // 자신의 멤버 함수
    cout << d.age << endl;

    Sparrow s;
    s.age = 2;
    s.Breathe();  // 상속받은 멤버 함수
    s.Fly();      // 자신의 멤버 함수
    cout << s.age << endl;
}

  • Animal 클래스는 모든 동물의 공통 특성인 Breathe() 메서드와 age 속성을 가짐
  • Dog 클래스는 Animal을 상속받아 Walk() 메서드를 추가
  • Sparrow 클래스는 Animal을 상속받아 Fly() 메서드를 추가
  • 파생 클래스 객체는 기본 클래스의 모든 public 멤버에 접근 가능

생성자 호출 순서

  • 상속 관계에서 객체 생성 시 생성자 호출 순서:
  1. 기본 클래스 생성자가 먼저 호출
  2. 파생 클래스 생성자가 나중에 호출
class Base {
public:
    Base() {
        cout << "Base()" << endl; // ------- 1
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived()" << endl; // -------- 2
    }
};

int main() {
    Derived d;
}

객체 생성 / 소멸 순서

  • 생성 순서: 기본 클래스 멤버 → 기본 클래스 → 파생 클래스 멤버 → 파생 클래스
  • 소멸 순서: 파생 클래스 → 파생 클래스 멤버 → 기본 클래스 → 기본 클래스 멤버

상속이 필요한 경우

코드 중복 제거

  • 상속을 사용하지 않으면 여러 클래스에서 동일한 코드를 반복해야 함
  • 자세히
  • class Image { public: operator string() { return "사진"; } }; class Message { public: Message(int sendTime, string sendName) { this->sendTime = sendTime; this->sendName = sendName; } int GetSendTime() const { return sendTime; } string GetSendName() const { return sendName; } private: // 자식 클래스에서도 이 멤버들에 직접적인 접근 불가 int sendTime; string sendName; }; class TextMessage : public Message { public: TextMessage(int sendTime, string sendName, string text) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출 this->text = text; } string GetText() const { return text; } private: string text; }; class ImageMessage : public Message { public: ImageMessage(int sendTime, string sendName, Image* image) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출 this->image = image; } Image* GetImage() const { return image; } private: Image* image; }; int main() { Image* dogImage = new Image; TextMessage* hello = new TextMessage(10, "두들", "안녕"); ImageMessage* dog = new ImageMessage(20, "두들", dogImage); cout << "보낸 시간 : " << hello->GetSendTime() << endl; cout << "보낸 사람 : " << hello->GetSendName() << endl; cout << " 내 용 : " << hello->GetText() << endl; cout << endl; cout << "보낸 시간 : " << dog->GetSendTime() << endl; cout << "보낸 사람 : " << dog->GetSendName() << endl; cout << " 내 용 : " << (string)*dog->GetImage() << endl; cout << endl; delete dogImage; delete hello; delete dog; }
  • 공통 멤버 변수와 메서드를 Message 클래스에 통합
  • 코드 중복 제거
  • 유지보수가 용이해짐

오버라이드

  • 파생 클래스에서 기본 클래스와 동일한 이름의 멤버를 정의하면 기본 클래스의 멤버가 가려짐
  • 부모 자식에 같은 이름의 멤버 함수가 있다면, 멤버 하이딩 또는 오버라이딩이 발생한다.
    • 멤버 하이딩 :
    • 오버 라이딩 :
class Base {
public:
    int a = 10;
};

class Derived : public Base {
public:
    int a = 20;  // Base의 a를 가림 / 멤버 은닉 / 멤버 하이딩
};

int main() {
    Base b;
    Derived d;

    cout << b.a << endl;  // 10 출력
    cout << d.a << endl;  // 20 출력 (Derived의 a)
}

  • 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
  • 가려진 기본 클래스의 멤버에 접근할 때 유용
  • class Base { public: void Print() { cout << "From Base!" << endl; } }; class Derived : public Base { public: void Print() { // Base의 Print()를 오버라이드 cout << "From Derived!" << endl; } }; int main() { Derived d; d.Print(); // Derived의 Print() 호출 d.Base::Print(); // 명시적으로 Base의 Print() 호출 d.Derived::Print(); // 명시적으로 Derived의 Print() 호출 }
  • 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
  • 가려진 기본 클래스의 멤버에 접근할 때 유용

가상 함수 / 오버라이드 / 동적 바인딩

  • 가상 함수 : 멤버함수 앞에 virtual을 선언한다.
    • 동작에는 차이가 없다.
    • 차이 : 자식 함수에 같은 이름이 있다면 ‘오버라이드’ 되는 것을 의미한다.
  • 오버 라이딩 : 동적 바인딩에 의해 호출되는것이다.
    • 오버 로딩 : 정적 바인딩에 의해 호출되는 것이다.
    • 정적 바인딩 : 컴파일 전에 참조를 결정하는 것이다.
  • 바인딩 : 함수를 호출할때 같은 이름의 함수가 여러개일때 어떤 함수를 호출할지 참조를 확인하는 기능이다.
  • 동적 바인딩 : 호출할 떄 참조를 확인하여 어떤 함수를 호출할 지 정하는 것이다.
  • 부모 클래스의 간접 참조로 자기 자식 객체도 참조할 수 있다.
    • 부모* = 자식 객체 주소
class Weapon {
public:
    Weapon(int power) : power(power) {
        cout << "Weapon(int)" << endl;
    }

    virtual void Use() {  // 가상 함수!!
        cout << "Weapon::Use()" << endl;
    }

protected:
    int power;
};

class Sword : public Weapon {
public:
    Sword(int power) : Weapon(power) {
        cout << "Sword(int)" << endl;
    }

    void Use() {  // 오버라이드
        cout << "Sword::Use()" << endl;
        swing();
    }

private:
    void swing() {
        cout << "Swing sword." << endl;
    }
};

class Magic : public Weapon {
public:
    Magic(int power, int manaCost) : Weapon(power), manaCost(manaCost) {
        cout << "Magic(int, int)" << endl;
    }

    void Use() {  // 오버라이드
        cout << "Magic::Use()" << endl;
        cast();
    }

private:
    void cast() {
        cout << "Cast magic." << endl;
    }

    int manaCost;
};

int main() {
    Sword mySword(10);
    Magic myMagic(15, 7);

    Weapon* currentWeapon; //현재 Weapon, Sword, Magic을 참조할 수 있다.

    currentWeapon = &mySword; //Sword 자식 간접참조 / 동적 바인딩을 위한 참조 할당이다.
    currentWeapon->Use();  // 동적 바인딩에 의해 Sword::Use() 호출
}
  • virtual 키워드로 선언
  • 기본 클래스 포인터로 파생 클래스 객체를 가리킬 때, 실제 객체의 타입에 따라 호출될 함수가 결정됨
  • 동적 바인딩(Dynamic Binding): 실행 시간에 호출할 함수가 결정됨

가상 소멸자

  • 소멸자 앞에 virtual을 붙인것이다.
    • 앞으로 소멸자는 전부 가상 소멸자로 선언해라
    • 부모 소멸자가 호출되면 자식의 소멸자가 호출이 안되는 경우가 있기 때문이다.
class Image {
public:
    operator string() {
        return "사진";
    }
};

class Message {
public:
    Message(int sendTime, string sendName) {
        this->sendTime = sendTime;
        this->sendName = sendName;
    }
    virtual ~Message() {}  // 가상 소멸자

    int GetSendTime() const { return sendTime; }
    string GetSendName() const { return sendName; }
    virtual string GetContent() const { return ""; }

private:
    int sendTime;
    string sendName;
};

class TextMessage : public Message {
public:
    TextMessage(int sendTime, string sendName, string text)
        : Message(sendTime, sendName) {
        this->text = text;
    }

    string GetContent() const { return text; }  // 오버라이드

private:
    string text;
};

class ImageMessage : public Message {
public:
    ImageMessage(int sendTime, string sendName, Image* image)
        : Message(sendTime, sendName) {
        this->image = image;
    }

    string GetContent() const { return (string)*image; }  // 오버라이드

private:
    Image* image;
};

void printMessage(Message* m) {
    cout << "보낸 시간 : " << m->GetSendTime() << endl;
    cout << "보낸 사람 : " << m->GetSendName() << endl;
    cout << "  내 용   : " << m->GetContent() << endl;  // 동적 바인딩
    cout << endl;
}

int main() {
    Image* dogImage = new Image;
    TextMessage* hello = new TextMessage(10, "두들", "안녕");
    ImageMessage* dog = new ImageMessage(20, "두들", dogImage);

    printMessage(hello);
    printMessage(dog);

    delete dogImage;
    delete hello;
    delete dog;
}
  • 기본 클래스 포인터로 파생 클래스 객체를 삭제할 때 올바른 소멸자가 호출되도록 보장
  • 가상 함수를 가진 클래스는 항상 가상 소멸자를 가져야 함

참조를 사용한 다형성

class Image {
public:
    operator string() {
        return "사진";
    }
};

class Message {
public:
    Message(int sendTime, string sendName) {
        this->sendTime = sendTime;
        this->sendName = sendName;
    }
    virtual ~Message() {}  // 가상 소멸자

    int GetSendTime() const { return sendTime; }
    string GetSendName() const { return sendName; }
    virtual string GetContent() const { return ""; }

private:
    int sendTime;
    string sendName;
};

class TextMessage : public Message {
public:
    TextMessage(int sendTime, string sendName, string text)
        : Message(sendTime, sendName) {
        this->text = text;
    }

    string GetContent() const { return text; }  // 오버라이드

private:
    string text;
};

class ImageMessage : public Message {
public:
    ImageMessage(int sendTime, string sendName, Image* image)
        : Message(sendTime, sendName) {
        this->image = image;
    }

    string GetContent() const { return (string)*image; }  // 오버라이드

private:
    Image* image;
};

void printMessage(const Message& m) {  // 참조로 받기
    cout << "보낸 시간 : " << m.GetSendTime() << endl;
    cout << "보낸 사람 : " << m.GetSendName() << endl;
    cout << "  내 용   : " << m.GetContent() << endl;
    cout << endl;
}

int main() {
    Image* dogImage = new Image;
    Message* messages[] = {
        new TextMessage(10, "두들", "안녕"),
        new ImageMessage(20, "두들", dogImage),
        new TextMessage(30, "두들", "잘가")
    };

    // 범위 기반 for문
    for (Message* m : messages) {
		    //실제로 참조하고 있는 클래스에 따라 다른 함수가 호출된다.
        printMessage(*m);
    }

    // 배열의 각 칸마다 동적 할당된 객체를 가리키는 포인터가 들어있으므로 각각 삭제
    for (Message* m : messages) {
        delete m;
    }
    delete dogImage;
}