공부/C++

생성자, 소멸자, 생성자 오버로딩, 멤버 초기화 리스트, static, 정적 멤버 및 변수, new, delete, 상수형 메서드, 메서드 선언과 정의 분리, 연산자 오버로딩, friend, 구조체 및 객체 매개변수 작성법

월러비 2025. 6. 19. 18:22

짧은 설명

  • 클래스의 구성요소 : 멤버 변수, 멤버 함수
  • 비교 연산자 : 같은 메모리를 ‘복사’ 해서 하나하나 비교한다.
  • 각 객체의 ID가 필요한 이유
    • 프로그램 종료 후 재시작할때 마지막 좌표를 유지해야하는것과 유지안해도 되는것을 구별하기 위해 사용할 수 있다.
  • 앞으로 파일을 분할시켜서 예제를 풀어야한다.
    • main
    • 클래스가 구현된 헤더파일
    • cpp 파일

객체와 클래스

객체의 생성 및 소멸 시기

class MyClass {
public:
    MyClass() {  // 생성자
        cout << "생성자가 호출되었음!!" << endl; //1
    }
    ~MyClass() {  // 소멸자
        cout << "소멸자가 호출되었음!!" << endl; //2
    }
};

void testLocalObj() {
    cout << "=== testLocalObj 함수 시작 ===" << endl; //3
    MyClass localObj;  // 지역 객체 생성
    cout << "=== testLocalObj 함수 끝 ===" << endl; //4
}  // 여기서 localObj 소멸

int main() {
    cout << "=== main 함수 시작 ===" << endl; //5
    testLocalObj();
    cout << "=== main 함수 끝 ===" << endl; //6
}

  • 5 - 3 - 1 - 4 - 2 - 6 순서로 호출된다.
  • 생성자 : 어떤 클래스의 객체든 간에 메모리에 올라가는 순간 초기화가 진행되는 함수다.
    • 생성자 규칙
      • 함수와 유사하지만 반환형을 적지 않는다.
      • 함수의 이름을 클래스와 동일하게 선언한다.
      • 생성자는 ‘오버로딩’이 가능하다.
        • 여러개 선언 가능
      • 기본 생성자가 없다면 ‘컴파일 에러’가 일어난다.
  • 소멸자 : 클래스 객체가 소멸할때 호출되는 함수다.
    • 함수든 프로그램이든 끝나는곳에 객체가 있으면 소멸한다.
    • 소멸자 규칙
      • ~을 쓰고 클래스 이름과 동일하게 작성한다.
      • 소멸자는 ‘오버로딩’이 없다.
  • 생성자든 호출자든 private로 선언하면 객체를 생성할때는 문법적으로 틀린것이 없지만 객체 생성 이후에 생성자에 들어가지 못하게 되기에 ‘컴파일 에러’가 일어난다.
  • 생성자와 소멸자를 작성하지 않았더라도 프로그램이 실행될 때 자동을 생성해서 동작한다.
  • 함수와 생성자 차이점
    • 함수 : 사용자가 호출하고 싶을때 호출된다.
    • 생성자 : 객체가 선언될 때 1번만 호출된다.

생성자 오버로딩

class Character {
private:
    int hp;
    int power;
    string name;

public:
    // 기본 생성자
    Character() {
        hp = 100;
        power = 10;
        name = "무명";
    }

    // 매개변수가 있는 생성자
    Character(string n, int h, int p) {
        name = n;
        hp = h;
        power = p;
    }

    void showInfo() {
        cout << "이름: " << name << endl;ㅂ
        cout << "HP: " << hp << endl;
        cout << "공격력: " << power << endl;
    }
};

int test(Character i)
{
	return 0;
}

int main() {
    Character hero1;  // 기본 생성자 호출
    Character hero2("전사", 150, 20);  // 매개변수 생성자 호출

    hero1.showInfo();
    cout << "----------" << endl;
    hero2.showInfo();
}

  • 기본생성자 호출 : 일반적으로 객체를 선언하면 호출된다.
  • 매개변수 생성자 호출 : 객체를 선언할때 함수에 매개변수를 넣는것처럼 호출한다.
    • 이름없이 클래스 이름과 생성자만 호출할 수 있다.
      • 임시객체 라고 부른다.
      • 리터럴 상수처럼 사용할 수 있다.
      • Character char1 = Character(”이름”, 1, 2); 형태다.

생성자 - 멤버 초기화 리스트 (멤버 이니셜라이저)

class Game {
private:
    const int maxLevel;    // 상수 멤버
    int& currentLevel;     // 참조 멤버
    int score;

public:
    // 멤버 초기화 리스트 사용
    Game(int max, int& level)
        : maxLevel(max), currentLevel(level), score(0) {
        // 생성자 본문
    }

    void showInfo() {
        cout << "최대 레벨: " << maxLevel << endl;
        cout << "현재 레벨: " << currentLevel << endl;
        cout << "점수: " << score << endl;
    }
};

int main() {
    int level = 1;
    Game myGame(99, level);
    myGame.showInfo();
}
  • 상수 멤버와 참조 멤버는 반드시 초기화 리스트에서 초기화
    • 변수를 만들때 초기화와는 다르다.
      • 변수 : 변수 선언 후 초기화 진행 (2단계)
      • 초기화 리스트 : 생성 되자마자 초기화 진행 (1단계)
    • 매개변수에서 초기화를 하면 기본 생성자와 동일한 효과가 있지만 멤버 이니셜라이저를 사용하는것이 더 좋다.
    • 클래스명(매개변수) : 멤버변수(매개변수) {} 형태로 사용한다.
  • 성능상 이점 (직접 초기화)
  • 참조 멤버는 멤버 이니셜라이저로만 초기화할 수 있다.

정적 멤버 (static)

  • 멤버는 static과 instance로 나뉘어진다.
    • 기본 ; instance
      • 객체별로 존재하는 멤버들
    • 정적 : static
      • 클래스마다 단 하나 존재하는 멤버들이다.
      • 인스턴스의 유무와 상관 없이 프로그램 시작할때 만들어져서 프로그램 끝날때 소멸한다.
      • 인스턴스 없이 접근할 수 있다.
  • 한정자 라고 부른다.
    • 상수도 한정자다.

정적 멤버 변수 및 메서드

class Player {
private:
    static int totalCount;  // 전체 플레이어 수
    string name;

public:
    Player(string n) : name(n) {
        totalCount++;
    }

    ~Player() {
        totalCount--;
    }

    static int getTotalCount() {
        return totalCount;
    }
};

// static 멤버 변수 초기화 (클래스 외부에서)
int Player::totalCount = 0;

int main() {
    cout << "현재 플레이어 수: " << Player::getTotalCount() << endl;

    Player* p1 = new Player("전사");
    Player* p2 = new Player("마법사");

    cout << "현재 플레이어 수: " << Player::getTotalCount() << endl;

    delete p1;
    cout << "현재 플레이어 수: " << Player::getTotalCount() << endl;

    delete p2;
    cout << "현재 플레이어 수: " << Player::getTotalCount() << endl;
}
  • 특징
    • 모든 객체가 공유하는 변수
    • 객체 생성 전에도 존재
    • 클래스명::변수명 으로 접근 가능
      • 클래스명을 네임스페이스를 사용하는 문법처럼 사용한다.
    • 인스턴스 객체를 통해서도 접근할 수 있다.
      • p1→getTotalCount();
    • static 함수 내부에는 인스턴스 멤버는 들어갈 수 없다.
      • static만 들어갈 수 있다.
    • this 포인터도 static이 클래스 단위로 있기 때문에 접근할 수 없다.
    • 전역 영역에서 ‘초기화’ 해야한다.
  • static 사용처
    • 인스턴스마다 있지않고 하나로 사용해도 괜찮은 값

정적 추가 설명

Vector2::Add(vec1, vec2);
  • 이걸 보면 static 함수를 선언해야겠다고 떠올려야한다.
  • 이유 : 클래스 자체 호출로 객체 선언 없이 함수를 호출했다.
    • 왜냐하면? static으로 선언하면 함수 자체를 호출할 수 있으니까

new 키워드

  • 동적할당 키워드다.
  • 사용자가 원할떄 객체를 생성하는 키워드다.
  • new 데이터형(생성자에 매개변수가 필요하면 넣는다.);

delete 키워드

  • 동적으로 객체를 삭제하는 키워드다.

상수형 메서드 const

  • const 앞 : 상수형을 반환한다.
  • const 뒤 : const 함수라는 것이다.
  • 일반 상수보다 제약이 1개 더 있다.
class Item {
private:
    string name;
    int price;

public:
    Item(string n, int p) : name(n), price(p) {}

    // const 메서드 - 멤버 변수를 수정하지 않음
    int getPrice() const {
        // price = 100;  // 에러! const 메서드에서는 수정 불가
        return price;
    }

    string getName() const {
        return name;
    }

    // 일반 메서드
    void setPrice(int p) {
        price = p;
    }
};

int main() {
    const Item sword("용의 검", 5000);

    // const 객체는 const 메서드만 호출 가능
    cout << sword.getName() << ": " << sword.getPrice() << "원" << endl;

    // sword.setPrice(3000);  // 에러! const 객체는 일반 메서드 호출 불가
}
  • const 함수 내부에서 멤버 변수를 수정할 수 없다.
  • const 함수를 호출하면 객체 데이터가 변경되지 않는다는 보장이 가능하다.
    • 기본 함수는 호출하면 객체의 데이터가 변경될 수 있다.
  • 읽기 전용 get 함수는 전부 붙여서 쓰는게 좋다.
  • 같은 이름의 함수를 써도 컴파일 에러가 안난다.
    • 오버로딩 된 것이다.
  • 일반객체도 const 함수를 호출할 수 있지만, 일반 함수도 같이 있을경우 일반 객체는 일반 함수만 호출된다.

상수형 오버로딩 사용 조건

  1. 매개변수의 갯수 및 데이터형
  2. const가 붙었는가 안붙었는가

메서드 선언과 정의 분리

// Character.h 파일 내용
class Character {
private:
    int hp;
    int mp;

public:
    Character();
    void attack();
    void useSkill();
    int getHp() const;
};

// Character.cpp 파일 내용
#include <iostream>
using namespace std;

Character::Character() {
    hp = 100;
    mp = 50;
}

void Character::attack() {
    cout << "공격!" << endl;
}

void Character::useSkill() {
    if (mp >= 10) {
        cout << "스킬 사용!" << endl;
        mp -= 10;
    }
}

int Character::getHp() const {
    return hp;
}
  • 컴파일 시간 단축
  • 구현부 숨기기 가능
  • 코드 관리 용이
  • 파일이 분할하더라도 해당 클래스, 해당 선언이 있는 파일 내부에 선언되어있다면 접근이 가능하다.

연산자 오버로딩

  • 기본 기능으로 만들어진 연산자를 매개변수를 바꿔서 구현할 수있다.
  • 필요한 연산자 만 재정의해서 쓰면 된다.
class Vector2D {
private:
    float x, y;

public:
    Vector2D(float x = 0, float y = 0) : x(x), y(y) {}

    // + 연산자 오버로딩
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }

    void print() const {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

int main() {
    Vector2D pos1(10, 20);
    Vector2D pos2(30, 40);

    Vector2D pos3 = pos1 + pos2;  // operator+ 호출

    cout << "pos1: "; pos1.print();
    cout << "pos2: "; pos2.print();
    cout << "pos3: "; pos3.print();
}

해석

Vector2 operator+(const Vector2& vec1);
vec1.operator+(vec2); == vec1 + vec2
  • 연산자 : operator기호 → 표준 함수 중의 산술 연산자 함수를 의미한다.
  • 좌항을 오퍼레이터를 호출하는 인스턴스, 매개변수를 우항을 의미한다.
	friend Vector2 operator*(float lhs, const Vector2& rhs);

	void Print();

};

Vector2 operator*(float lhs, const Vector2& rhs);
  • 오버로딩이 좌우 매개변수로 필요하다면 전역함수로 구현해야한다.
    • 전역함수로 구현해야하는 상황이 아니면 반드시 멤버 함수로 선언해야한다.
  • 전역함수로 클래스 private 변수에 접근하는 방법
    1. get 함수를 써서 넣는다.
    2. friend 키워드 사용

freind 키워드

  • 클래스의 멤버가 아닌 함수 또는 별도의 클래스에 있는 모든 멤버에게 멤버 수준의 액세스 권한을 부여하는 키워드다.
  • private 접근 지정자를 해칠 수 있어서 사용을 자제해야한다.
  • 전역 함수 또는 클래스 이름 자체가 올 수 있다.
    • 해당 클래스의 private 또는 protected에 접근이 가능하게 된다.

구조체 및 객체 매개변수 작성법

Vector2 Vector2::Add(const Vector2& vec1, const Vector2& vec2)
  • &를 붙이면 값 복사 과정이 스킵되어 더 빨라진다.
  • const 를 붙이면 값 변경이 안된다.
  • 결론 : 연산이 더 빨라지고 값 변경이 일어나지않아 안전하다.
  • 매개변수로 구조체나 클래스 객체를 넣는 매개 변수는 이렇게 작성해야한다.
    • float같은 단일 변수를 갖고있는건 그냥 써도 된다.