오픈소스와 취업때문에 블로그를 한 3주정도 안썻다 안한게 아니다. 정리한내용을 쭉 보자.
void Date::SetDate(int year, int month, int date)
Date 클래스에 속하는 SetDate 함수라는 의미이고 여기에 구현하면 된다.
우리는 SetDate라는 메서드를 이용해서 생성했다. 이제는 이 방식이 아닌 기본적으로 생성자를 통해 만들어진 객체를 이용해서 객체를 만들어낼 것이다.
Date(int year, int month, int day)
다음과같이 선언하여 생성하는 방식이다.
Date day(2011, 3, 1);
이제 생성하자마자 각 필드들을 초기화를 바로 진행하는 방식이다.
생성자를 생성하는 방식으로는 2가지가 있다.
암시적 방법(implict)
Date day(2011, 3, 1);
명시적 방법(explicit)
Date day = Date(2012, 3, 1);
암시적 방법으로 축약해서 쓸 수 있기 때문에, 주로 선호하는 편이다.
Date day; 로 생성했을때, 생성자가 생성될까? 에 대한 의문이 들겠지만, 생성 된다. 이건 Default constructor로 기본 생성자이다.
Date() {
year_ = 2025;
month_ = 9;
day_ = 22;
}
이런식으로 정의하게 된다면 호출하는 방식은 다음과 같다.
Date day = Date();
Date day2;
Date day3(); // 이 방식은 안된다.
추가로 먼저 정의 후 나중에 구현한다고 하면, 다음과 같이도 쓸 수 있다. 써주는게 좋다.
Date() = default;
우리는 오버로딩을 이용해서 생성자를 여러개로 만들 수 있다.
Date(){
std::cout << "기본 생성자 호출!" << std::endl;
year_ = 2025;
month_ = 9;
day_ = 22;
}
Date(int year, int month, int day){
std::cout << "인자 3개인 생성자 호출!" << std::endl;
year_ = year;
month_ = month;
day_ = day;
}
#include <iostream>
#include <vector>
class Point{
int x, y;
public:
Point(int pox_x, int pos_y);
int GetPointX() const;
int GetPointY() const;
};
Point::Point(int pos_x, int pos_y) {
x = pos_x;
y = pos_y;
};
int Point::GetPointX() const{
return x;
};
int Point::GetPointY() const{
return y;
};
class Geometry {
Point* point_array[100];
int point_counter;
public:
Geometry(Point **point_list);
Geometry();
void AddPoint(const Point &point);
// 모든 점들 간의 거리를 출력하는 함수 입니다.
double GetDistance(const Point &p1, const Point &p2);
void PrintDistance();
// 모든 점들을 잇는 직선들 간의 교점의 수를 출력해주는 함수 입니다.
// 참고적으로 임의의 두 점을 잇는 직선의 방정식을 f(x,y) = ax+by+c = 0
// 이라고 할 때 임의의 다른 두 점 (x1, y1) 과 (x2, y2) 가 f(x,y)=0 을 기준으로
// 서로 다른 부분에 있을 조건은 f(x1, y1) * f(x2, y2) <= 0 이면 됩니다.
std::vector<int> StraightExpr(const Point &p1, const Point& p2);
long long ccw(const Point& p1, const Point& p2, const Point& p3);
bool isIntersect(const Point &a, const Point &b, const Point &c, const Point &d);
void PrintNumMeets();
};
Geometry::Geometry(Point **point_list){
point_counter = 0;
for(int i = 0; i<100 && point_list[i] !=nullptr; i++){
point_array[i] = point_list[i];
point_counter++;
};
}
Geometry::Geometry(){
point_counter = 0;
for(int i = 0; i<100; i++) {
point_array[i] = nullptr;
}
};
void Geometry::AddPoint(const Point &point){
point_array[point_counter++] = new Point(point.GetPointX(), point.GetPointY());
};
double Geometry::GetDistance(const Point &p1, const Point &p2) {
int DistX = p2.GetPointX() - p1.GetPointX();
int DistY = p2.GetPointY() - p1.GetPointY();
return std::sqrt(DistX*DistX + DistY*DistY);
}
void Geometry::PrintDistance(){
for(int i = 0; i<point_counter; i++){
for (int j = i+1; j<point_counter; j++) {
std::cout << GetDistance(*point_array[i], *point_array[j]) << std::endl;;
}
}
};
std::vector<int> Geometry::StraightExpr(const Point &p1, const Point& p2){
std::vector<int> result;
result.push_back(p2.GetPointX()-p1.GetPointX());
result.push_back(p2.GetPointX()-p1.GetPointX());
result.push_back(p1.GetPointX()*p2.GetPointY() - p2.GetPointX()*p1.GetPointY());
return result;
}
long long Geometry::ccw(const Point& p1, const Point& p2, const Point& p3){
return ((p2.GetPointX() - p1.GetPointX()) * (p3.GetPointY() - p1.GetPointY()))
- ((p2.GetPointY() - p1.GetPointY()) * (p3.GetPointX() - p1.GetPointX()));
}
bool Geometry::isIntersect(const Point &a, const Point &b, const Point &c, const Point &d) {
long long ab = ccw(a, b, c) * ccw(a, b, d);
long long cd = ccw(c, d, a) * ccw(c, d, b);
if (ab == 0 && cd == 0) {
if (std::min(a.GetPointX(), b.GetPointX()) <= std::max(c.GetPointX(), d.GetPointX()) &&
std::min(c.GetPointX(), d.GetPointX()) <= std::max(a.GetPointX(), b.GetPointX()) &&
std::min(a.GetPointY(), b.GetPointY()) <= std::max(c.GetPointY(), d.GetPointY()) &&
std::min(c.GetPointY(), d.GetPointY()) <= std::max(a.GetPointY(), b.GetPointY()))
return true; // 일직선 위에서 겹침
return false;
}
return (ab <= 0 && cd <= 0);
}
void Geometry::PrintNumMeets() {
int count = 0;
// 모든 선분 쌍을 검사
for (int i = 0; i < point_counter; i++) {
for (int j = i + 1; j < point_counter; j++) {
for (int k = i + 1; k < point_counter; k++) {
for (int l = k + 1; l < point_counter; l++) {
if (i == k || i == l || j == k || j == l) continue; // 점이 겹치면 skip
if (isIntersect(*point_array[i], *point_array[j], *point_array[k], *point_array[l])) {
count++;
}
}
}
}
}
std::cout << "교차하는 선분 개수: " << count << std::endl;
}
int main() {
Geometry g;
g.AddPoint(Point(0, 0));
g.AddPoint(Point(4, 0));
g.AddPoint(Point(0, 4));
g.AddPoint(Point(4, 4));
std::cout << "==== 거리 출력 ====" << std::endl;
g.PrintDistance();
std::cout << "==== 교차 횟수 출력 ====" << std::endl;
g.PrintNumMeets();
Geometry g2;
g2.AddPoint(Point(0, 0));
g2.AddPoint(Point(1, 1));
g2.AddPoint(Point(2, 2));
g2.AddPoint(Point(3, 3));
std::cout << "\n==== g2 거리 출력 ====" << std::endl;
g2.PrintDistance();
std::cout << "==== g2 교차 횟수 출력 ====" << std::endl;
g2.PrintNumMeets();
}
실습 내용
#include <iostream>
class Marin {
private:
int hp;
int coord_x, coord_y;
int damage;
bool is_dead;
public:
Marin();
Marin(int x, int y);
int attack();
void be_attacked(int damage_earn);
void move(int x, int y);
void show_status();
};
Marin::Marin() {
hp = 50;
coord_x = coord_y = 0;
damage = 5;
is_dead = false;
}
Marin::Marin(int x, int y) {
hp = 50;
coord_x = x;
coord_y = y;
damage = 5;
is_dead = false;
}
int Marin::attack(){
return damage;
}
void Marin::be_attacked(int damage_earn){
hp = hp-damage_earn;
if(hp <= 0) {
is_dead = true;
}
}
void Marin::move(int x, int y){
coord_x -= x;
coord_y -= y;
}
void Marin::show_status(){
std::cout << " *** Marine *** " << std::endl;
std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) " << std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
Marin marin1(2, 3);
Marin marin2(3, 5);
marin1.show_status();
marin2.show_status();
std::cout << std::endl << "마린 1이 마린 2를 공격" << std::endl;
marin2.be_attacked(marin1.attack());
marin1.show_status();
marin2.show_status();
}
마린에 대한 기본적인 코드이다. 객체지향적으로 설계를 잘했나? 괜찮은거 같다. 각 기능별로 분리를 잘 했다. 하지만, 문제가 있다. 마린이 새로 생성되면 또 일일이 이름도 붙여줘야하고 마린이 1, 2마리만 존재하는게 아니다. 그러니까 이걸 관리하기 조금 더 용이하게 해줘야 한다. 일단 main함수만 수정해보자.
int main() {
Marin *marins[100];
marins[0] = new Marin(1, 2);
marins[1] = new Marin(2, 3);
marins[0]->show_status();
marins[1]->show_status();
std::cout << std::endl << "마린 1이 마린 2를 공격! " << std::endl;
marins[1]->be_attacked(marins[0]->attack());
marins[0]->show_status();
marins[1]->show_status();
delete marins[0];
delete marins[1];
}
이런식으로 짜는 코드의 장점을 몇가지 보자.
객체의 할당 해제를 해줄 수 있다.
프리미티브 타입의 경우는 해제하면 힙 영역의 데이터가 아니기 때문에 오류가 발생하는 것을 경험했다.
객체를 배열안에서만 관리하면 된다.
대신에, .으로 객체를 참조하는게 아닌 →키워드로 참조해줘야 한다.
이제 마린이라는 객체를 확장시켜보자.
마린의 이름을 넣어보자.
class Marin {
private:
int hp;
int coord_x, coord_y;
int damage;
bool is_dead;
char* name;
public:
Marin();
Marin(int x, int y, const char* marin_name);
int attack();
void be_attacked(int damage_earn);
void move(int x, int y);
void show_status();
};
Marin::Marin() {
hp = 50;
coord_x = coord_y = 0;
damage = 5;
is_dead = false;
name = NULL;
}
Marin::Marin(int x, int y, const char* marin_name) {
name = new char[strlen(marin_name) + 1];
name = strcpy(name, marin_name);
hp = 50;
coord_x = x;
coord_y = y;
damage = 5;
is_dead = false;
}
마린을 초기화 시키는 과정에서 이름값을 같이 전달하는 방식이다.
marins[0] = new Marin(1, 2, "my");
marins[1] = new Marin(2, 3, "marin");
호출은 다음과 같이 하면 된다.
하지만, 이런식으로 필드를 동적으로 생성하게 되면, char*에 해당하는 name필드는 할당 해제를 할 수 없다. 왜냐하면, JVM이 없다. 그리고 객체의 생명주기에서 사라지는 단계에서 자동으로 사라지지 않는다.
그렇기 때문에, 우리는 이 필드가 영원히 메모리상에 떠다니는 문제를 메모리 누수 (Memory Leak) 라고 얘기한다. 우리는 그러면, 사라지는 단계에서 메모리 누수를 막을 방법을 생각해내야 하는데, C++에 소멸자라는 기능을 제공한다.
Marin::~Marin() {
std::cout << name << " 의 소멸자 호출! " << std::endl;
if(name != NULL) {
delete[] name;
}
}
우리는 name을 char* 로 저장하고 있기 때문에, 하나하나 전부 초기화해줘야 한다. 다시 말해 배열을 해제해줘야 한다.
간단한 실험을 하나 더 해보자.
#include <cstring>
#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');
a.simple_function();
}
생성자 호출 a
생성자 호출 b
소멸자 호출 b
소멸자 호출 a
클래스 내부에 자기 자신 클래스를 호출하는 코드인데, 소멸자를 따로 작성해주지 않아도 메인함수가 종료된다면, 소멸자를 자동으로 호출되는 것을 확인할 수 있다.
#include <iostream>
#include <cstring>
class Photo_Canon {
int hp, shield;
int coord_x, coord_y;
int damage;
public:
Photo_Canon(int x, int y);
Photo_Canon(const Photo_Canon& pc);
void show_status();
};
Photo_Canon::Photo_Canon(int x, int y){
std::cout << "생성자 호출! " << std::endl;
hp = shield = 100;
coord_x = x;
coord_y = y;
damage = 20;
}
Photo_Canon::Photo_Canon(const Photo_Canon& pc){
std::cout << "복사 생성자 호출! " << std::endl;
hp = pc.hp;
shield = pc.shield;
damage = pc.damage;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
}
void Photo_Canon::show_status(){
std::cout << "Photon Cannon " << std::endl;
std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
<< std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
Photo_Canon pc1(3, 3);
Photo_Canon pc2(pc1);
Photo_Canon pc3 = pc2;
pc1.show_status();
pc2.show_status();
pc3.show_status();
}
생성자 호출!
복사 생성자 호출!
복사 생성자 호출!
Photon Cannon
Location : ( 3 , 3 )
HP : 100
Photon Cannon
Location : ( 3 , 3 )
HP : 100
Photon Cannon
Location : ( 3 , 3 )
HP : 100`
지금 보면, 복사 생성자를 생성하는 과정에 대해서 얘기해 볼 필요가 있다. 표준 복사생성자 방식은 pc2 방식과 같다.
Photo_Canon(const Photo_Canon& pc);
// T(const T& a);
추가로, const 키워드에 해당하면 위에서 얘기했던것과 같이 값 변경이 불가능하다.
우리는 pc3을 생성하는 방식은 Photo_canon pc3 = pc2 로 작성했다. 근데 동일하게 복사생성자가 호출되었다.
참고로, 이 두개는 다르다.
Photo_Canon p3 = p2
이 방법은 복사생성자 방식
Photo_Canon p3;
p3 = p2;
이 방법은 p2에 p3라는 별칭을 달아준 것
그러면 우리는 복사생성자의 특징을 하나 알 수 있다. 복사생성자는 호출될때만 생성된다는 특징이다.
추가로 우리는 복사생성자를 직접 구현했지만, 사실 복사생성자는 내부적으로도 구현되어져 있다.
#include <iostream>
#include <cstring>
class Photo_Canon {
int hp, shield;
int coord_x, coord_y;
int damage;
char *name;
public:
Photo_Canon(int x, int y, const char* photo_name);
~Photo_Canon();
void show_status();
};
Photo_Canon::Photo_Canon(int x, int y, const char* photo_name){
std::cout << "생성자 호출! " << std::endl;
hp = shield = 100;
coord_x = x;
coord_y = y;
damage = 20;
name = new char[strlen(photo_name) + 1];
strcpy(name, photo_name);
}
Photo_Canon::~Photo_Canon(){
if(name != NULL) {
delete [] name;
}
}
void Photo_Canon::show_status(){
std::cout << "Photon Cannon " << name << std::endl;
std::cout << " Location : ( " << coord_x << " , " << coord_y << " ) "
<< std::endl;
std::cout << " HP : " << hp << std::endl;
}
int main() {
Photo_Canon pc1(3, 3, "Canon");
Photo_Canon pc2(pc1);
Photo_Canon pc3 = pc2;
pc1.show_status();
pc2.show_status();
pc3.show_status();
}
이 코드는 name을 추가한 것이다. 이걸 실행하면 어떻게 될까?
생성자 호출!
Photon Cannon Canon
Location : ( 3 , 3 )
HP : 100
Photon Cannon Canon
Location : ( 3 , 3 )
HP : 100
Photon Cannon Canon
Location : ( 3 , 3 )
HP : 100
photocanon(63302,0x20cb10c80) malloc: Double free of object 0x12ae05d70
photocanon(63302,0x20cb10c80) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort "/Users/younho/soyeong/C++/"photocanon
이유는 예상이 가겠지만, 복사 생성자 코드를 다시 가져와보자(코드로 구현 안되어져 있어도, 내부 로직은 구현 되어져 있음)
Photo_Cannon::Photo_Cannon(const Photo_Cannon& pc) {
hp = pc.hp;
shield = pc.shield;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
damage = pc.damage;
name = pc.name;
}
name을 추가했으니, name에서 문제가 발생한건 맞다. 하지만 이유를 조금 더 파보자면, 다음과 같다.

pc1, pc2가 가진 name이라는 필드는 포인터 변수이다. 근데 포인터 변수인 name이 이미 할당이 한번 끝났다면, 다음 해제에서는 이미 해제된 포인터가 되는 것인데, 또 해제하려니 문제가 발생하게 된 것이다.
‘Canon’ 이라는 문자열은 heap 메모리에 저장되어져 있고, 그걸 가리키는데 문제가 발생하게 된 것이다.
그러니, 우리는 name문자열을 복사하는 방식이 아니라, 새로 동적으로 할당하는 방식을 사용하면 된다. 우리는 이걸 깊은 복사라고 얘기할 것이다.
우리는 지금까지 단순히 얕은 복사만 진행했다.
얕은 복사는 주소값만 그대로 가져다 저장하는 방식이고
깊은 복사는 주소는 다르지만, 값은 같게 하는 방식이다.
이제 이 문제를 해결하는 방법을 각 name별로 새로 객체를 생성해주면 문제가 해결된다.
우리는 그러기 위해 복사생성자를 새로 정의해주면 된다.
Photo_Canon::Photo_Canon(const Photo_Canon& pc) {
hp = pc.hp;
shield = pc.shield;
damage = pc.damage;
coord_x = pc.coord_x;
coord_y = pc.coord_y;
name = new char[strlen(pc.name) + 1];
strcpy(name, pc.name);
}
이제 복사생성자에서 새로운 캐릭터 배열을 만들어주었기 때문에, 기존 객체와 복사생성자를 통해 생성된 객체의 reference type(참조자, 주소 등등이라고 표현할 수 있음) 의 값이 다르게 된다.
컴파일시 정상적으로 동작하게 된다.
생성자를 만드는 코드를 조금 수정해보자.
Marin::Marin() : hp(50), coord_x(0), coord_y(0), damage(5), is_dead(false) {}
Marin::Marin(int x, int y) : hp(50), coord_x(x), coord_y(y), damage(5), is_dead(false) {}
기존에 하나하나 다 넣는 방식이 아니라, 변수끼리 값을 넣어주는 방식이다. 함수 본체에는 어떤 내용도 들어가지 않는다. (물론 필요시에는 넣어줘야 한다.)
이런식으로 쓰면은 안된다.
Marin::Marin(int coord_x, int coord_y) : hp(50), coord_x(corrd_x), coord_y(coord_y), damage(5), is_dead(false) {}
위에서 얘기했던것처럼, 컴파일러는 가장 가까운 변수를 먼저 보기에, coord_x와 coord_y를 매개변수에 들어온 변수로 인식한다.
그러면 사실 결국 같은 결과를 보이는데 왜 쓰이는가?
두개의 생성자 생성 방식에는 약간의 차이가 존재하는데, 초기화 리스트를 사용한다면, 생성과 초기화를 동시에 진행하게 된다.
하지만, 기존 생성자는 생성을 먼저 하고, 그다음에 대입을 한다.
직관적으로 생각하면 이렇다.
초기화 리스트 방식
int a = 10;
복사 생성자가 호출이 된다.
기존 생성자 방식
int a;
a = 10;
기본 생성자를 생성 한 후, 값을 초기화 하는 방식이다.
그렇기 때문에, 효율적인 측면에서 조금 더 빠르기 때문에 초기화 리스트 방법을 사용하는 것이 조금이나마 효율적으로 프로그램을 작성할 수 있다.
추가적으로 몇가지 예를 더 보자.
const int a;
a = 3;
int& refA;
ref = c;
이런 방식으로 코딩을 하게 되면, ref는 메모리상에 올라가있지도 않은데, 값을 넣으려고 하니 오류가 발생한다.
추가 팁인데, 만약에 레퍼런스 참조 또는 상수를 필드값을 가지면, 생성자에 필수로 넣어줘야 한다.
(왜냐하면, 생성되고 나서 상수를 넣는 방법도 있지만, 안전하지 않고, 래퍼런스는 생성과 동시에 초기화가 되기 때문이다.)
하기전에, 객체를 만약에 새어야 한다면 어떤 방식을 쓰면 괜찮을것인가에 대해서 생각해보자.
1번의 방법의 경우는 크기가 자유자제로 변할 수 있는 array를 담을 공간이 필요하다. 그러면 vector라는 객체를 사용하면 될 것이다. 하지만 vector는 아직 배우지 않았으니 넘어가도록 하고, 만약 2번 방식을 사용하게 된다면, 그 값을 이용하기 위해 인자로 계속 전달해야 하는 귀찮음이 발생한다.
그러면, 전역으로 관리할 수 있게하는 전역변수를 만든다면, 프로젝트의 규모가 커질 수록 변수의 이름이 겹치거나 잘못사용하게 되어 오류가 날 가능성이 다분해진다. (전역 변수는 잘 사용하지 말자)
C++에는 위 문제를 해결할 기능을 하나 제공한다. 변수 같지만, 하나의 클래스 내부에만 살아있는 변수로써 static 멤버 변수를 가질 수 있다.
static 변수의 가장 큰 특징은 같은 클래스의 다른 객체에서도 static 변수는 공유한다는 점이다.
static int total_marin_num;
...
int Marin::total_marin_num = 0;
결국 marin이라는 클래스 내부에 존재하는 변수인 total_marin_num이라는 변수를 클래스 생성에서 초기화 하면 되지 않을까 라는 생각을 할 수 있다.
일반적인 프로세스의 메모리 구조를 생각해보면

static 변수는 bss영역에 저장되고, 컴파일타임에 bss영역을 사용하도록 정해진다. 그렇기 때문에, 바로 초기화되는것은 아니고 결국 런타임때 결정되지만, 프로세스 전체가 공유할 수 있는 bss영역으로 할당된다.
그렇기 때문에, static변수는 marin객체가 존재하지 않아도 이미 존재하기 때문에, 변수를 다시 초기화 하려는 행위가 중복된다. 또한 전체가 공유하는 것이기 때문에 로직상 올바르지도 않다.
또한 static변수는 marin객체만 접근가능할 뿐이지 사실 global 변수이다.
이제 우리는 marin이라는 객체에 대해서 추가적인 연산만 진행해주면 된다. 생성시, +1 해제시 -1의 연산 말고는 다른 연산에 대한 필요는 없다.
*** Marine my ***
Location : ( 1 , 2 )
HP : 50
현재 총 마린 수 : 2
*** Marine marin ***
Location : ( 2 , 3 )
HP : 50
현재 총 마린 수 : 2
*** Marine hhhhh ***
Location : ( 1 , 2 )
HP : 50
현재 총 마린 수 : 3
hhhhh 의 소멸자 호출!
마린 1이 마린 2를 공격!
*** Marine my ***
Location : ( 1 , 2 )
HP : 50
현재 총 마린 수 : 2
*** Marine marin ***
Location : ( 2 , 3 )
HP : 45
현재 총 마린 수 : 2
my 의 소멸자 호출!
marin 의 소멸자 호출!
마린을 찍었을때, hhhhh마린이 생성되었을때, 총 마린수는 3이었는데, 아래에서 추가적으로 더 상태를 봤을때 마린의 수가 2로 줄었는데, hhhhh가 사라졌다. 왜 사라졌을까?
marin3은 지역 객체이기 때문에, create_marin() 호출이 끝나고, 스택프레임이 사라지게 되면 당연히 없어진다.
void create_marin() {
Marin marin(1, 2);
marin.show_status();
}
이게 마린 생성 코드이고, hhhhh도 이 코드로 생성했다.
내가 잘못알고 있던 개념이고, marin(1, 2)를 통해 생성된 객체는 사실 힙영역이 아니라 스택 프레임 내부에 생성되는 것이다. 그렇게 된다면 당연히 없어지게 된다. 힙 영역이 아니기 때문이다. 그러면 어떤식으로 해야하나
void create_marin() {
Marin *marin = new Marin(1, 2);
marin.show_status();
}
이렇게 생성하면 힙영역에 생성되기에 create_marin()이 함수를 종료하게 되더라도 힙영역에 marin이 남아있게 된다.
또한 static변수 뿐만 아니라 static 함수도 정의할 수 있다.
static void show_total_marin();
...
void Marin::show_total_marin(){
std::cout << "전체 마린 수: " << total_marin_num << std::endl;
}
...
int main() {
...
Marin::show_total_marin();
}
void Marin::show_total_marin(){
std::cout << default_damage << std::endl;
std::cout << "전체 마린 수: " << total_marin_num << std::endl;
}
이제 다음으로, default_damage가 뭔지 알 수 없기 때문에 다음 코드는 동작하지 않을 것이다.
그렇기 때문에, 이 문제를 해결할 수 있어야 한다.
Marin& Marin::be_attacked(int damage_earn){
hp -= damage_earn;
if(hp <= 0) is_dead = true;
return *this;
}
이 코드를 보면, be_attacked를 호출한 객체를 대상으로 가리키는 예약어인 this를 사용할 수 있다. 결국 이 코드는 다시 생각해보면 이것과 동일하다.
Marin& Marin::be_attacked(int damage_earn){
this.hp -= damage_earn;
if(this.hp <= 0) this.is_dead = true;
return *this;
}
우리는 static도 공부했으니, static에서 this를 사용할 수 있을까?
정답은 no다. this는 소속 클래스의 객체가 공유하는 변수이기 때문에 this를 사용할 필요도 없고, 사용도 되지 않는다.
근데 코드를 보면 Marin& 을 반환한다고 한다. &이란 뭘까? 이전에 우리는 별명 이라고 얘기했다. 별명을 리턴한다는 의미를 조금 더 생각해보자
#include <iostream>
class A {
int x;
public:
A(int c) : x(c) {}
int& access_x() {return x;}
int get_x() {return x;}
void show_x() {std::cout << x << std::endl; }
};
int main() {
A a(5);
a.show_x();
int& c = a.access_x();
c = 4;
a.show_x();
int d = a.access_x();
d = 3;
a.show_x();
}
이걸 실행시키면,
5
4
4
처럼 결과가 생성된다. 우리는 레퍼런스 공부를 하면서 &의 쓰임을 기억할 것이다. 단지 이름으로써, 메모리에 생성되지 않는다는 것도 알고, 단지 가리키는 것이라고 생각한다. 이게 별명의 의미이다.
그래서 a의 x를 받아오는 행위를 &를 통해서 하냐 안하냐에 따라 값이 바뀔 수 있고 바뀌지 않을 수 있다.
이걸 조금 더 쉽게 풀어서 써보자면, &라는 키워드를 통해서 별명을 얻어낼 수 있었다. 그러면 메모리에 값을 직접적으로 참조할 수 있기 때문에 값을 변경을 직접적으로 할 수 있다.
int a = 10;
int &b = &a;
/*
int &b;
b = a;
는 불가능
*/
b = 13;
std::cout << a << std::endl;
이렇게 되면, a값은 변경되었던 것을 기억할 것이다.
전문적으로 이야기 하면, b는 a의 주소를 복사한 것이기 때문에, 깊은 복사가 발생해서 값을 b를 통해서 재할당 하고, 변경할 수 있다. 사이드 이펙트가 발생하게 되는 것이다.
사이드 이펙트(side effect)란?
어떤 표현식(문장)을 평가(evaluate) 했을 때,
**단순히 값을 “계산하는 것”을 넘어서
프로그램의 상태를 바꾸는 행위**가 발생하는 것
사이드 이펙트는 다시 말해 깊은 복사를 통해 메모리값을 복사하여, 해당 메모리가 참조하는 값을 변경할 수 있는 것을 얘기한다고 생각하자.
다시 우리가 작성한 A클래스 코드를 다시 봐보자.
b라는 별칭 &x를 반환하는 access_x()를 통해서 x 를 가져온것이기 때문에 깊은 복사가 발생했고, A클래스의 x필드를 변경할 수 있게 된 것이다.\
int& e = a.get_x();
e = 2;
a.show_x();
이 코드의 경우에는 x값을 가져오는 x’ 변수를 임시로 생성한 것이기 때문에, get_x()문장이 끝나면 소멸되기 때문에 레퍼런스를 가질 수 없어서 초기화 되지 안흔다.

marin2.be_attacked(marin1.attack()).be_attacked(marin1.attack());
이제 이런식으로 코드를 짜도, 2번의 attack을 다 진행한 결과를 출력하게 된다. 왜그러냐면, 우리는 be_attack()이 주소 자체를 반환하기 때문에, side effect 가 발생해도 또 동일하게 그 객체의 주소를 가리키기 때문에, 값을 계속 변경할 수 있다. 하지만 아래를 보자.
Marin Marin::be_attacked(int damage_earn){
this->hp -= damage_earn;
if(this->hp <= 0) this->is_dead = true;
return *this;
}
이런식으로 코드를 짯다고 하면
marin2.be_attacked(marin1.attack()).be_attacked(marin1.attack());
이 코드는 한번 때리고 나서 복사본을 때리고, 사라지기 때문에 1번의 공격으로만 인식되게 된다.
두 코드를 조금 풀어써보면 이해될 것이다.
// 레퍼런스를 쓰지 않은 경우
Marin marin3 = marin2.be_attacked(marin1.attack()); // Hp 5 감소 후 marin의 복사본을 대상으로 하게 됨
marin3.be_attacked(marin1.attack());
// Marin marin3 = marin2.be_attacked()라고 생각하면 되고,
// marin3.be_attacked(); 이렇게 되는것과 같다.
// 레퍼런스를 쓴 경우
marin2.be_attacked(marin1.attack());
marin2.be_attacked(marin1.attack());
// 동일 객체에 2번 실행
C++에는 변수들의 값을 바꾸지 않고 읽기만 하는 상수 함수로 선언할 수 있다.
예를 들어서 공격력은 바뀌지 않기 때문에, const예약어를 이용해서, 내부에서 값의 변화가 없다는 것을 알려야한다.
const 로 정의한 상수 함수는 오직 객체들의 읽기만 가능해진다.
int attack() const;
...
int Marin::attack() const{
return default_damage;
}
이제 이 함수는 내부에서 값을 변경할 수 없다.
멋져용