이 글은 https://modoocode.com/ 님의 블로그를 보고 학습한 내용을 정리한 글입니다.
C에서 Struct를 통해 무언가를 묶어놨다면, CPP에서는 Class를 통해 무언가를 묶어둘 수 있다. 예를 들어 스타크래프트라는 게임을 보겠다. 그 안에 마린이라는 유닛이 존재하는데, 해당 유닛은 생성될 때 마다 같은 스펙을 공유한다. 이러한 상황에서 마린이 100마리 나온다고 똑같은 코드를 100줄씩 쓸 필요는 없을것이다.
Class를 다음과 같이 정의했다고 해보자
class Marine {
int hp;
int coord_x, coord_y;
int damage;
bool is_dead;
char *name;
public:
Marine();
Marine(int x, int y, const char *marine_name);
Marine(int x, int y);
~Marine(); // destructor
int attack();
void be_attacked(int damage_earn);
void move(int x, int y);
void show_status();
};
이 안에는 private으로 선언된 변수들, 생성자와 소멸자 그리고 Marine이라는 객체가 수행할 함수들이 적혀있는 공간이다.
이런식으로 Class를 선언하고 생성자를 통해 객체를 생성할 수 있다.
우선 생성자부터 시작하겠다.
생성자란, 자바를 해보셨으면 알겠지만, 객체를 생성하는 것이다.
Public 아래 3종류의 함수가 생성자 함수이다.
이는 단순히 선언한것이고 각 코드들의 내용은 다음과 같다.
Marine::Marine(){
hp = 50;
coord_x = coord_y = 0;
damage = 5;
is_dead = false;
name = NULL;
}
Marine::Marine(int x, int y, const char *marine_name){
name = new char[strlen(marine_name) + 1];
strcpy(name, marine_name);
hp = 50;
coord_x = coord_y = 0;
damage = 5;
is_dead = false;
char *name;
}
Marine::Marine(int x, int y) {
coord_x = x;
coord_y = y;
hp = 50;
damage = 5;
is_dead = false;
name = NULL;
}
int main() {
Marine marine1 = new Marine();
Marine marine2 = new Marine(2, 3 "Marine 2");
Marine marine3 = new Marine(4, 5);
}
첫번쨰 생성자는 기본생성자이다.
아무런 값을 주지 않고 생성하면 다음과 같은 stat을 가진 마린 하나가 생성되게 된다.
두번째는 이름을 가진 객체를 생성하는 생성자이다. 이렇게 만든 마린은 Marine 2라는 이름을 가지게 된다.
마지막은 좌표만을 가지고 만들어진 마린이다.
하지만 이렇게 여러개를 생성해야 하는데 일일히 이름을 붙이면서 타이핑하는건 조금 귀찮을 수 있다. 이때 배열을 이용하여 여러개를 관리할 수 있다.
int main() {
Marine* marines[100];
marines[0] = new Marine(2, 3, "Marine 2");
marines[1] = new Marine(1, 5, "Marine 1");
marines[0]->show_status();
marines[1]->show_status();
cout << endl << "Marine 1 attacks Marine 2" << endl;
marines[0]->be_attacked(marines[1]->attack());
marines[0]->show_status();
marines[1]->show_status();
delete marines[0];
delete marines[1];
}
이렇게 Marine 타입의 배열을 통해 마린을 여러개 생성하고 관리할 수 있다.
여기서 우리는 마지막줄에 delete marines[0]라는 이상한 코드를 볼 수 있는데 바로 다음에 설명할 소멸자이다.
우선 소멸자는 다음과 같이 선언하고 다음과 같이 사용한다.
class Marine {
int hp;
int coord_x, coord_y;
int damage;
bool is_dead;
char *name;
public:
~Marine(); // destructor
};
Marine::~Marine() {
cout<< name << " is destructed! " << endl;
if (name != NULL) {
delete[] name;
}
}
그럼 소멸자를 왜 쓰는 것일까?
아까 2번째 생성자에서 우리는 이름을 '배열'로 생성하고 집어넣었다. 그럼 이 배열은 언제 사라질까? 파이썬이나 자바처럼 가비지 컬렉션이 이루어진다면 언젠가는 사라지겠지만, cpp은 그렇지 않은 모양이다.
이렇게 작은 데이터들이 사라지지 않고 계속 메모리 공간에 남아있게되면 결국 메모리에 부담이 되고 이는 우리가 흔히 메모리 누수라고 불리는 현상을 낳게 된다. (대부분의 온라인겜이 가지고 있다는 그거 맞다.)
따라서 우리는 생성한 객체가 사라지면, 생성한 모든것들도 같이 사라지도록하는 소멸자를 사용하기로 한 것이다.
소멸자 내부의 코드를 살펴보면, name이 NULL이 아닌경우 name을 지우도록 설정해둔것을 알 수 있다.
# include <iostream>
# include <string.h>
using namespace std;
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(const Photon_Cannon &pc);
~Photon_Cannon();
void show_status();
};
Photon_Cannon::Photon_Cannon(int x, int y) {
// cout << "Normal Constructor " << endl;
hp = shield = 100;
coord_x = x;
coord_y = y;
damage = 20;
name = NULL;
}
Photon_Cannon::Photon_Cannon(const Photon_Cannon &pc) {
// cout << "copy constructor" << endl;
hp = pc.hp;
shield = pc.shield;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
damage = pc.damage;
name = new char[strlen(pc.name) + 1];
strcpy(name, pc.name);
}
Photon_Cannon::Photon_Cannon(int x, int y, const char *cannon_name) {
// cout << "Normal Constructor " << endl;
hp = shield = 100;
coord_x = x;
coord_y = y;
damage = 20;
name = new char[strlen(cannon_name) + 1];
strcpy(name, cannon_name);
}
Photon_Cannon::~Photon_Cannon() {
if (name) delete[] name;
}
void Photon_Cannon::show_status() {
cout << "Photon Cannon "<< name << endl;
cout << " Location : ( " << coord_x << " , "<< coord_y << " ) "<<endl;
cout << " HP " << hp << endl;
cout << " Shield " << shield << endl;
}
int main() {
Photon_Cannon pc1(3, 3, "Cannon");
Photon_Cannon pc2 = pc1;
pc1.show_status();
pc2.show_status();
}
말 그대로 '복사' 해서 생성해주는 생성자이다.
위의 코드를 살펴보면, 광자포를 생성해주는 코드임을 알 수있는데, 중간에 &pc를 인자로 받는, 즉 주소값을 받는 생성자가 있음을 알 수있다. 이것이 바로 복사생성자이다.
이러한 복사생성자는 특정 객체의 정보들을 받는것만이 가능하고, 새로 써주는 행위는 불가능하다. 즉
coord_x = 3;
같은 잘못된 코드는 사용이 불가능 하다는것이다.
복사생성자는
int main() {
Photon_Cannon pc1(3, 3, "Cannon");
Photon_Cannon pc2 = pc1;
pc1.show_status();
pc2.show_status();
}
다음과 같이 pc2 = pc1을 함으로써 호출이 된다.
복사생성자를 따로 선언하지 않더라도 pc3 = pc1은 알아서 복사를 하긴 한다. 이런 경우에서 우리가 이름배열을 쓴다고 가정해보자. 두 생성자 모두 특정 주소에 있는 이름배열을 포인팅해서 사용할 것이다. 이때 만약 pc1 객체가 사라진다면 어떻게 될까? 소멸자에 의해 이름 배열은 주소채로 사라지게 되고, pc3 객체는 허공을 포인팅하게 되는 치명적인 오류가 발생하게 된다. 따라서 항상 복사생성자도 만들어줘야한다.
마린 생성자는 다음과 같이 기본값을 가진상태로 생성할 수도 있다.
Marine::Marine()
: hp(50), coord_x(0), coord_y(0), default_damage(5), is_dead(false) {}
Marine::Marine(int x, int y)
: coord_x(x), coord_y(y), hp(50), default_damage(5), is_dead(false) {}
Marine::Marine(int x, int y, int default_damage)
: coord_x(x),
coord_y(y),
hp(50),
default_damage(default_damage),
is_dead(false) {}
이러한 상태로 생성을 하게 되면
1번의 경우 저러한 정보들을 가진 객체가 생성이 된다.
2번의 경우 x, y값을 인자로 받으며 그 값들이 coord_x, coord_y에 들어가게 된다.
3번 역시 마찬가지로 작동한다.
이는 const int default_damage = default_damage
랑 같은 동작을 하는 코드이다.
Static의 경우 자바의 static과 동일한 기능이다.
static변수를 선언할 경우 어디에서든 사용가능한 변수가 나오는것이고,
static 함수를 선언하면, 별도의 객체선언없이 바로 사용가능한 함수가 나오는것이다.
객체지향 프로그래밍에서 뭐가 제일 어렵냐 하면 아마 this아닐까 싶다. 말로는 나 자신을 가르키는것이라 하기에 매우 쉬워 보이지만, 실제로 써보면 이게 뭐지 싶은 그런 단어이다.
cpp에서 this는 함수를 호출하는 객체 자기 자신을 의미한다.
Marine& Marine::be_attacked(int damage_earn) {
hp -= damage_earn;
if (hp <= 0) is_dead = true;
return *this;
}
다음과 같은 코드가 있다고하자. 이 코드는 실제로는
Marine& Marine::be_attacked(int damage_earn) {
this->hp -= damage_earn;
if (this->hp <= 0) this->is_dead = true;
return *this;
}
와 같은 역할을 한다고 할 수 있다. 그렇다면 여기서 return *this는 무엇일까? 호출한 marine 의 레퍼런스? (marine&)
이 부분은 링크를 읽어보는게 좋을것같다.
#include <iostream>
class Test {
int number;
char* String_cont;
public:
Test(int number);
Test(const char* String);
};
Test::Test(int number) {
number = number;
String_cont = "A";
std::cout << number << std::endl;
}
Test::Test(const char* String) {
number = 0;
String_cont = "A";
std::cout << String_cont << std::endl;
}
int main() {
Test(3);
}
다음과 같은 클래스가 존재한다고 하면, main에서 Test(3)은 자동적으로 Test(int number)을 실행시키게 된다.
만약 이떄
void AnyMethod(Test t) {
}
AnyMethod(Test("A"));
같은 함수가 있다고 하고 이를 실행시키면 어떤일이 생길까?
Test("A") 생성자가 객체를 생성하고 이를 인자로 전달하여 실행된다.
만약 여기서
AnyMethod("A");
생성자 없이 A만 실행된다면 어떨까?
이런 경우, 컴파일러가 자동적으로 Test 클래스 중에서 "A"를 가지고 생성할만한 생성자를 찾고 그 생성자를 이용하여 자동으로 생성해서 실행해준다.
이를 암시적 변환 (implict conversion)이라고 한다.
하지만 중요한 생성자(문자의 길이 등을 지정) 하는 생성사자가 이러한 암시적 변환을 겪게 되면 사용자가 의도하지 않은 암시적 변환이 일어나게 된다.
AnyMethod(3);
이는 사용자가 실수로 입력을 잘못넣은 것이지만, 컴파일러는 이를 암시적 변환처리하여 Test(int number) 가 작동하도록 바꿔버린다. 이를 방지하기 위해선
#include <iostream>
class Test {
int number;
char* String_cont;
public:
explict Test(int number);
Test(const char* String);
};
Test::Test(int number) {
number = number;
String_cont = "A";
std::cout << number << std::endl;
}
Test::Test(const char* String) {
number = 0;
String_cont = "A";
std::cout << String_cont << std::endl;
}
int main() {
Test(3);
}
암시적 변환을 원치 않는 컴파일러 앞에 explict를 붙이면 된다.
이러한 explict가 걸린 생성자의 경우, 복사생성자로도 호출이 되지 않는다.
const 함수로 붙은 값은 바꿀수 없다고 다들 잘 알고있을것이다. 하지만 mutable을 선언한 변수는 const 함수여도 바꿀수 있다.
// > 출처 : https://modoocode.com/
#include <iostream>
class A {
mutable int data_;
public:
A(int data) : data_(data) {}
void DoSomething(int x) const {
data_ = x;
}
void PrintData() const { std::cout << "data: " << data_ << std::endl; }
};
int main() {
A a(10);
a.DoSomething(3);
a.PrintData();
}
위의 코드에서 보이는것처럼 data_에 mutable을 붙이는것으로 const 함수에서도 변경이 가능하다.
그런데 이럴거면 그냥 const를 안쓰면 되는거 아닐까?
const함수란, 이 함수는 객체 내부를 건드리지 않는 함수라고 명시적으로 표기해주는 방법이다. 예를 들어, read-only 함수들처럼 읽어오기만 하고 값 변경은 없는 경우 const를 이용하는것이다.
하지만 가끔 const 안에서 값을 바꿔줘야 하는 경우가 생길 수 도 있다. 물론 함수를 하나 더 선언해서 해도 되겠지만, 그냥 그러지말고 const와 mutable을 써주면 된다고 한다.
class Server {
// > 출처 : https://modoocode.com/
// .... (생략) ....
mutable Cache cache; // 캐쉬!
// 이 함수는 데이터베이스에서 user_id 에 해당하는 유저 정보를 읽어서 반환한다.
User GetUserInfo(const int user_id) const {
// 1. 캐쉬에서 user_id 를 검색
Data user_data = cache.find(user_id);
// 2. 하지만 캐쉬에 데이터가 없다면 데이터베이스에 요청
if (!user_data) {
user_data = Database.find(user_id);
// 그 후 캐쉬에 user_data 등록
cache.update(user_id, user_data); // <-- 불가능
}
// 3. 리턴된 정보로 User 객체 생성
return User(user_data);
}
};
읽어오면서 동시에 캐쉬에 써주는 함수인데, 읽는 함수이기 때문에 const이지만, 써야해서 cache를 mutable로 선언해야한다.