1. 객체 생성 해보기(스타크래프트 마린 생성) - [마린 생성, 배열 방식으로 마린 생성(Heap)]
2. 소멸자(Destructor) - [소멸자가 존재하는 Marine Class, 소멸자 호출]
3. 복사생성자(copy constructor) - [포토캐논 복사 생성하기, 복사생성자 호출 방식, 디폴트 복사생성자, 디폴트 복사생성자의 한계]
1. 객체 생성 해보기(스타크래프트 마린 생성)
일반적인 방식으로 마린 생성
→ 객체의 내부 성질, 상태 관련 변수는 private
→ 객체가 외부에 하는 행동을 함수로 구현해 public
#include <iostream>
class Marine {
int hp; // 마린 체력
int coordX, coordY; // 마린 위치
int damage; // 공격력
bool isDead;
public:
Marine(); // 기본 생성자
Marine(int x, int y); // x, y 좌표에 마린 생성
int Attack(); // 데미지를 리턴
void BeAttacked(int damageEarn); // 입는 데미지
void Move(int x, int y); // 새로운 위치
void ShowStatus(); // 상태를 보여준다.
};
Marine::Marine() {
hp = 50;
coordX = coordY = 0;
damage = 5;
isDead = false;
}
Marine::Marine(int x, int y) {
coordX = x;
coordY = y;
hp = 50;
damage = 5;
isDead = false;
}
void Marine::Move(int x, int y) {
coordX = x;
coordY = y;
}
int Marine::Attack() { return damage; }
void Marine::BeAttacked(int damageEarn) {
hp -= damageEarn;
if (hp <= 0) isDead = true;
}
void Marine::showStatus() {
std::cout << " *** Marine *** " << std::endl;
std::cout << " Location : ( " << coordX << " , " << coordY << " ) "
<< std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
// 생성자 오버로딩을 통해 (2, 3), (3, 5)에 위치한 마린 생성
Marine marine1(2, 3);
Marine marine2(3, 5);
**marine1.ShowStatus();
marine2.ShowStatus();
std::cout << std::endl << "마린 1 이 마린 2 를 공격! " << std::endl;
marine2.BeAttacked(marine1.Attack());
marine1.ShowStatus();
marine2.ShowStatus();
}
→ marine1, marine2 이런 식으로 일일이 이름 붙이고, 몇 개의 마린을 만들지 컴파일 시점에 정해버리는 것도 아니어서, 수십개의 marine을 미리 만들 수도 없는 격이다. 따라서, marine 들을 배열로 정해버리는 식으로 극복해볼 수 있다.
배열 방식으로 마린 생성(Heap)
int main() {
Marine* marines[100]; // Marine 객체를 가리키는 포인터 배열
marines[0] = new Marine(2, 3);
marines[1] = new Marine(3, 5);
// Marine 들의 포인터를 가리키는 배열로, 메소드를 호출 시,
// [.]이 아닌 [->]를 사용해야 한다.
marines[0]->be_attacked(marines[1]->attack());
marines[0]->show_status();
marines[1]->show_status();
// 동적으로 할당한 메모리는 언제나 해제 해주어야 한다.
delete marines[0];
delete marines[1];
}
+) new와 malloc 모두 동적으로 할당하지만, new는 객체를 동적으로 생성하면서 동시에 자동으로 생성자도 호출해준다.
cf.) 동적할당, 정적할당
동적할당: 힙(Heap) 메모리에 객체를 생성해 직접 관리하는 방식
정적할당: 스택(Stack) 메모리에 공간을 할당해 함수가 끝나면 자동으로 삭제되는 방식
+) 동적(Heap) 배열은 크기 변경이 가능하다.
int* arr = new int[5] {1, 2, 3, 4, 5}; // 크기 5인 동적 배열
// 새로운 크기의 배열을 만들기 위해, 기존 데이터를 복사해야 한다.
int newSize = 10;
int* newArr = new int[newSize]; // 크기 10의 새 배열 생성
for (int i = 0; i < 5; i++) {
newArr[i] = arr[i];
}
delete[] arr; // 기존 배열 삭제
arr = newArr; // 새 배열로 포인터 변경
+) 올바르지 않은 방법(가능은 하다.)
marines[0] = new Marine(2, 3);
Marine marine(2, 3);
marines[0] = &marine;
→ marine이 Stack 메모리에 생성된다. marines[0]에 marine의 주소를 저장한다. 하지만, marine이 main() 함수 종료 시 사라지기 때문에, marine[0]이 가리키는 주소가 댕글링(Dangling)이 된다.
+) Stack 메모리 활용한 객체 배열(비권장)
int main() {
Marine marines[100]; // 스택(Stack)에 객체 100개 생성 (new가 필요 없다.)
marines[0] = Marine(2, 3);
marines[1] = Marine(3, 5);
marines[0].ShowStatus();
marines[1].ShowStatus();
}
장점: 1) new/delete 없이 사용 가능해 메모리 관리가 쉽다. 2) 프로그램 종료 시 자동으로 해제된다. 3) 실행 속도가 빠르다(스택 메모리는 CPU에서 관리된다.).
단점: 1) 배열 크기 변경이 불가능하다(위 코드 상 100으로 고정). 2) 너무 큰 배열을 만들 경우, 스택 오버플로우 위험이 있다. 3) 동적인 추가/삭제가 불가능하다.
2. 소멸자(Destructor)
~(클래스명) : 생성자가 클래스명과 똑같이 생겼다면 소멸자는 그 앞에 ~를 붙이면 된다.
- Marine Class에 각 마린에 대한 name을 저장할 수 있는 인스턴스 변수를 추가해보자.
class Marine {
private:
char* name;
//...
public:
Marine();
Marine(int x, int y, const char* marineName);
Marine(int x, int y);
//...
}
//...
Marine::Marine(int x, int y, const char* marineName) {
name = new char[strlen(marineName) + 1];
strcpy(name, marineName);
coordX = x;
coordY = y;
hp = 50;
damage = 5;
isDead = false;
}
int main() {
Marine* marines[100];
marines[0] = new Marine(2, 3, "Marine 2");
//...
delete marines[0];
delete marines[1];
//...
}
→ 이런 식으로 되면, 마린의 이름을 추가할 때, name을 동적으로 생성해서 문자열을 복사하는 형태이다. 이렇게 되면, 동적으로 하당된 char 배열에 대한 delete가 이루어지지 않는다. 저 name은 영원히 메모리 공간 속에 떠다니게 되는 것이다. 이런 식으로 메모리가 쌓이다 보면, 메모리 누수(Memory Leak)이 발생할 수 있다.
→ 여기서, 생성했던 객체가 소멸 될 때, 자동으로 호출되는 함수 같은 것이 있으면 좋지 않을까? → 바로 소멸자(Destructor)이다.
소멸자가 존재하는 Marine Class
#include <string.h>
#include <iostream>
class Marine {
int hp; // 마린 체력
int coordX, coordY; // 마린 위치
int damage; // 공격력
bool isDead;
char* name; // 마린 이름
public:
Marine(); // 기본 생성자
Marine(int x, int y, const char* marineName); // 이름까지 지정
Marine(int x, int y); // x, y 좌표에 마린 생성
~Marine();
int Attack(); // 데미지를 리턴한다.
void BeAttacked(int damageEarn); // 입는 데미지
void Move(int x, int y); // 새로운 위치
void ShowStatus(); // 상태를 보여준다.
};
Marine::Marine() {
hp = 50;
coordX = coordY = 0;
damage = 5;
isDead = false;
name = NULL;
}
Marine::Marine(int x, int y, const char* marineName) {
name = new char[strlen(marineName) + 1];
strcpy(name, marineName);
coordX = x;
coordY = y;
hp = 50;
damage = 5;
isDead = false;
}
Marine::Marine(int x, int y) {
coordX = x;
coordY = y;
hp = 50;
damage = 5;
isDead = false;
name = NULL;
}
void Marine::move(int x, int y) {
coordX = x;
coordY = y;
}
int Marine::Attack() { return damage; }
void Marine::BeAttacked(int damageEarn) {
hp -= damageEarn;
if (hp <= 0) isDead = true;
}
void Marine::ShowStatus() {
std::cout << " *** Marine : " << name << " ***" << std::endl;
std::cout << " Location : ( " << coordX << " , " << coordY << " ) "
<< std::endl;
std::cout << " HP : " << hp << std::endl;
}
Marine::~Marine() {
std::cout << name << " 의 소멸자 호출 ! " << std::endl;
if (name != NULL) {
delete[] name;
}
}
int main() {
Marine* marines[100];
marines[0] = new Marine(2, 3, "Marine 2");
marines[1] = new Marine(1, 5, "Marine 1");
marines[0]->ShowStatus();
marines[1]->ShowStatus();
std::cout << std::endl << "마린 1 이 마린 2 를 공격! " << std::endl;
marines[0]->BeAttacked(marines[1]->Attack());
marines[0]->ShowStatus();
marines[1]->ShowStatus();
//객체가 소멸할 때 소멸자가 호출된다.
//소멸자 호출 관련 메시지가 뜨게 된다.
delete marines[0];
delete marines[1];
}
위 코드에서 소멸자 내용을 보면,
Marine::~Marine() {
std::cout << name << " 의 소멸자 호출 ! " << std::endl;
if (name != NULL) {
delete[] name;
}
}
→ name이 NULL이 아닐 때, 즉, 동적으로 할당 되었을 때만 delete로 name을 삭제한다.
+) name 자체가 char 의 배열로 동적할당 하였으므로, delete 역시 delete [] name 즉, []를 꼭 추가해야 한다.
+) 소멸자는 오버로딩이 되지 않겠다. 인자가 필요 없는 것이 기본이기 때문이다.
소멸자 호출
// 소멸자 호출 확인하기
#include <string.h>
#include <iostream>
class Test {
char c;
public:
Test(char _c) {
c = _c;
std::cout << "생성자 호출 " << c << std::endl;
}
~Test() { std::cout << "소멸자 호출 " << c << std::endl; }
};
void simple_function() { Test b('b'); }
int main() {
Test a('a');
simple_function();
}
→ 위 코드 결과: 생성된 객체는 함수가 종료됨과 동시에 소멸되어 소멸자가 호출된다.
생성자 호출 a
생성자 호출 b
소멸자 호출 b
소멸자 호출 a
+) 따로 생성자를 생성하지 않아도 디폴트 생성자가 있었던 것처럼, 소멸자도 디폴트 소멸자(Default Destructor)가 있다. 물론 디폴트 소멸자 내부에선 아무런 작업도 수행하지 않는다.
+) 추가로, 소멸자가 필요 없는 클래스라면 굳이 소멸자를 쓸 필요는 없다.
3. 복사생성자(copy constructor)
어떤 객체를 일일히 생성자로 생성할 수도 있지만, 1개만 생성해두고, 그 한 개를 통해 나머지를 복사 생성 할 수도 있다.
T(const T& a);
→ 어떤 클래스 T가 있다면, 객체 a를 상수 레퍼런스로 받는다는 것이다. 즉, a가 const여서 복사 생성자 내부에서 a의 데이터를 변경할 수 없고, 새롭게 초기화 되는 인스턴스 변수들에게 복사만 할 수 있게 된다.
포토캐논 복사 생성하기
// 포토캐논
#include <string.h>
#include <iostream>
class PhotonCannon {
int hp, shield;
int coordX, coordY;
int damage;
public:
PhotonCannon(int x, int y);
PhotonCannon(const PhotonCannon& pc);
void show_status();
};
PhotonCannon::PhotonCannon(const Photon_Cannon& pc) {
std::cout << "복사 생성자 호출 !" << std::endl;
hp = pc.hp;
shield = pc.shield;
coordX = pc.coordX;
coordY = pc.coordY;
damage = pc.damage;
}
PhotonCannon::PhotonCannon(int x, int y) {
std::cout << "생성자 호출 !" << std::endl;
hp = shield = 100;
coordX = x;
coordY = y;
damage = 20;
}
void PhotonCannon::ShowStatus() {
std::cout << "Photon Cannon " << std::endl;
std::cout << " Location : ( " << coordX << " , " << coordY << " ) "
<< std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
PhotonCannon pc1(3, 3);
PhotonCannon pc2(pc1);
PhotonCannon pc3 = pc2;
pc1.ShowStatus();
pc2.ShowStatus();
}
위 코드에서 복사 생성자를 보면,
PhotonCannon::PhotonCannon(const Photon_Cannon& pc) {
std::cout << "복사 생성자 호출 !" << std::endl;
hp = pc.hp;
shield = pc.shield;
coordX = pc.coordX;
coordY = pc.coordY;
damage = pc.damage;
}
→ 기존 객체를 이용해서 객체를 초기화할 때 사용되며, 매개 변수가 const로 선언되었다. 따라서, 객체 내부 값을 변경할 수 없다.
→ hp, shield, coordX, coordY, damage 의 값은 새로운 객체의 변수로 변경이 가능하긴 하다.
+) 만약, const 없이 원본 객체를 수정할 수 있게 된다면, 원본 데이터가 손상될 수 있다.
복사생성자 호출 방식
ex1) int x, int y 를 인자로 갖는 생성자가 오버로딩 되었고, pc2가 인자로 pc1을 넘겼으므로, 복사생성자가 호출되었다.
PhotonCannon pc1(3, 3);
PhotonCannon pc2(pc1);
ex2) 직관적인 방식
PhotonCannon pc3 = pc2; // (== Photon_Cannon pc3(pc2);)
+) pc3 = pc2 는 평범한 대입 연산이겠지만, 생성 시 대입하는 연산(ex_2)은 복사생성자가 호출되는 방식이다. 이를 통해 사용자가 직관적인 프로그래밍이 가능하다.
- +) 복사생성자, 생성자 호출 차이
//1. 복사 생성자 1번 호출
PhotonCannon pc3 = pc2;
→ pc3을 pc2를 이용해 “생성”할 때, 복사 생성자가 호출된다.
//2. 생성자 1번 호출, pc3 = pc2; 명령 실행
PhotonCannon pc3;
pc3 = pc2;
→ pc3이 먼저 기본 생성자로 생성된다. 복사 생성자가 아닌, 이미 존재하는 객체를 pc2 의 값을 pc3에 복사하는 동작이다.
즉, 복사생성자는 객체가 생성될 때(= or () 를 이용한 초기화)만 호출된다.
디폴트 복사생성자
- 디폴트 복사생성자가 하는 일(컴파일러가 자동으로 생성하는 코드)
C++ 컴파일러는 복사생성자를 정의하지 않아도 자동으로 디폴트 복사생성자를 생성한다. 해당 디폴트 복사생성자는 객체의 모든(기본 타입의) 멤버변수를 일대일로 복사한다.
ex) 자동으로 생성되는, 동작되는 복사생성자 코드
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
hp = pc.hp;
shield = pc.shield;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
damage = pc.damage;
}
디폴트 복사생성자의 한계
복사생성자로 디폴트 복사생성자를 사용하기로 한 것으로 두고, 아래에서 char *name;을 추가해본다.
class Photon_Cannon {
int hp, shield;
int coord_x, coord_y;
int damage;
char *name;
public:
Photon_Cannon(int x, int y);
Photon_Cannon(int x, int y, const char *cannon_name);
~Photon_Cannon();
void show_status();
};
→ 위와 같이 클래스를 생성하고, 해당 클래스에 맞는 pc1, pc2를 생성하는 코드를 main() 함수에 작성 후, 실행하면 런타임 오류가 발생한다. 분명 디폴트 복사생성자는 일대일로 원소들 간 복사를 수행하는 것 아니었나?
- 위에서 자동으로 생성될 디폴트 복사생성자를 파악해보자
Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
hp = pc.hp;
shield = pc.shield;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
damage = pc.damage;
name = pc.name;
}
→ 여기서, pc1 , pc2는 name까지 모두 같은 값을 갖게 된다. 여기서 name이 같다는 것은 두 개의 포인터가 같은 값을 갖는 다는 것이고, 이는 둘이 같은 주소 값을 가리킨다는 것이 된다.
→ 이 상태에서는 문제가 되지 않지만, 소멸자에서는 문제가 된다. 소멸될 때, 객체들이 파괴되면서 소멸자를 호출하게 되는데, 먼저 pc1이 파괴되었다고 쳐보자. 그러면, pc1에서 가리키던 메모리가 delete가 되었는데, pc2는 해당 메모리를 가리키고 있는 상태가 된다.
pc2의 name은 일단 pc1에서 가리키던 name의 주소값을 가지고 있을 것이다. 즉, NULL은 아니므로(할당은 된 것), delete [] name이 수행된다. 여기서 이미 해제된 메모리에 접근해서 다시 해제하려고 했으므로, 런타임 오류가 발생하게 된다.
- 어떻게 해결할까?
복사생성자에서 name을 그대로 복사하지 않고, 다른 메모리에 동적 할당을 해서 그 내용만 복사하면 된다. 이렇게 메모리를 새로 할당해 내용을 복사하는 것을 깊은 복사(deep copy), 단순히 대입만 해주는 것을 얕은 복사(shallow copy)라고 한다.
컴파일러가 생성하는 디폴트 복사생성자는 얕은 복사 밖에 할 수 없어서, 깊은 복사가 필요한 경우에는 사용자가 직접 복사생성자를 만들어야 한다.
ex) 복사생성자에서 깊은 복사 사용하기
#include <string.h>
#include <iostream>
class PhotonCannon {
int hp, shield;
int coordX, coordY;
int damage;
char *name;
public:
PhotonCannon(int x, int y);
PhotonCannon(int x, int y, const char *cannonName);
PhotonCannon(const Photon_Cannon &pc);
~PhotonCannon();
void ShowStatus();
};
PhotonCannon::PhotonCannon(int x, int y) {
hp = shield = 100;
coordX = x;
coordY = y;
damage = 20;
name = NULL;
}
PhotonCannon::PhotonCannon(const PhotonCannon &pc) {
std::cout << "복사 생성자 호출! " << std::endl;
hp = pc.hp;
shield = pc.shield;
coordX = pc.coordX;
coordY = pc.coordY;
damage = pc.damage;
name = new char[strlen(pc.name) + 1];
strcpy(name, pc.name);
}
PhotonCannon::PhotonCannon(int x, int y, const char *cannonName) {
hp = shield = 100;
coordX = x;
coordY = y;
damage = 20;
name = new char[strlen(cannonName) + 1];
strcpy(name, cannonName);
}
//name이 할당되지 않았다면 그냥 넘어가고,
//할당되어있다면 동적할당 메모리 해제
PhotonCannon::~PhotonCannon() {
if (name) delete[] name;
}
void PhotonCannon::ShowStatus() {
std::cout << "Photon Cannon :: " << name << std::endl;
std::cout << " Location : ( " << coordX << " , " << coordY << " ) "
<< std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
PhotonCannon pc1(3, 3, "Cannon");
PhotonCannon pc2 = pc1;
pc1.ShowStatus();
pc2.ShowStatus();
}
cf.) 위에서 활용했던 문자열을 널 종료 char 배열로 다루는 것은 매우 비권장되는 방식이다. C++에서 문자열은 std::string을 사용하면 된다.
- 문제) 주어져 있는 문자열 클래스를 완성해보자.
→ 주어진 문자열 클래스
class String {
char* str;
int len;
public:
String(char c, int n); // 문자 c 가 n 개 있는 문자열로 정의
String(const char* s);
String(const String& s);
~String();
void AddString(const String& s); // str 뒤에 s 를 붙인다.
void CopyString(const String& s); // str 에 s 를 복사한다.
int StrLen(); // 문자열 길이 리턴
void PrintString() {
std::cout << str << "\\n";
}
};
→ 해결 코드
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
class String {
char* str;
int len;
public:
String(char c, int n); // 문자 c 가 n 개 있는 문자열로 정의
String(const char* s);
String(const String& s);
~String();
void AddString(const String& s); // str 뒤에 s 를 붙인다.
void CopyString(const String& s); // str 에 s 를 복사한다.
int StrLen(); // 문자열 길이 리턴
void PrintString() {
std::cout << str << "\\n";
}
};
String::String(char c, int n) {
len = n;
str = new char[len + 1];
for (int i = 0; i < len; i++) {
str[i] = c;
}
str[len] = '\\0';
}
String::String(const char* s) {
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
}
String::String(const String& s) {
len = s.len;
str = new char[len + 1];
std::strcpy(str, s.str);
}
String::~String() {
if (str) delete[] str;
}
void String::AddString(const String& s) {
if (this == &s) return;
int newLen = len + s.len;
char* newStr = new char[newLen + 1];
std::strcpy(newStr, str);
std::strcat(newStr, s.str);
delete[] str;
str = newStr;
len = newLen;
}
void String::CopyString(const String& s) {
// 본인 복사 방지
if (this == &s) return;
// 원래 있던게 있으면 삭제 시키고 진행한다.
if (str) delete[] str;
len = s.len;
str = new char[len + 1];
std::strcpy(str, s.str);
}
int String::StrLen() {
return len;
}
int main() {
// 아래는 확인용
String str1('a', 5);
std::cout << "str1: ";
str1.PrintString();
String str2("Hello");
std::cout << "str2: ";
str2.PrintString();
String str3 = str2;
std::cout << "str3 (복사된 str2): ";
str3.PrintString();
str1.AddString(str2);
std::cout << "str1 + str2: ";
str1.PrintString();
str3.CopyString(str1);
std::cout << "str3 (복사된 str1): ";
str3.PrintString();
return 0;
}
'공부 > C++' 카테고리의 다른 글
[C++] 초기화 리스트, static 변수/함수, this, 상수(const) 함수, 복사 생략 (0) | 2025.03.24 |
---|---|
[C++] 객체지향, 오버로딩(Overloading), 생성자(Constructor) (0) | 2025.03.20 |
[C++] Static, new/delete, Naming Rules (0) | 2025.03.16 |