동적 할당 연산자
– new : 변수 할당 받을 때 사용
– new[] : 배열 할당 받을 때 사용
– delete : 변수 할당 받은 메모리 반납
– delete[] : 배열 할당 받은 메모리 반납
data_type* pointer_v = new data_type
delete pointer_v
#include <iostream>
int main() {
int *p = new int; //할당
*p = 200; //사용
delete p; //반납
p = nullptr;
return 0;
}
선언과 동시에 초기화도 가능하다
int *plnt = new int(20); char *pchar = new char('a'); int* hArr = new int[5]{ 1,2,3,4,5 };
class
에서 inheritance
는 기능을 물려받는 다는 의미이다.
클래스는 상속을 거듭할수록 구체화되고, 부모 클래스로 거슬러 올라갈수록 일반화된다고 한다.
아래의 예시 코드로 공부해보자 .
class Base {
std::string s;
public:
Base() : s("기반") {std::cout << "기반 클래스" << std::endl;}
void what() {std::cout << s << std::endl;}
};
class Derived : public Base {
std::string s;
public:
Derived(): s("파생"){std::cout << "파생 클래스" << std::endl;}
void what() {std::cout << s << std::endl;}
/*
base 클래스의 what을 오버라이드 하기 때문에, base클래스의 함수가 출력되는 것이 아니다.
*/
};
클래스들의 관계를 is - a
형식으로 정의할 수 있다.
이렇게 모든 상속관계는 is - a
로 정의될 수 있지만, 당연히 역은 성립하지 않는다.
그리고 보면, Base 클래스의 what함수를 Derived 클래스의 what으로 오버라이딩 한 것을 볼 수 있다.
오버로딩과 오버라이딩이 비슷한 단어처럼 들리기에, 그냥 같은 말을 다르게 하는 것인가 할 수 있지만, 엄연히 다른 개념입니다.
오버로딩
: 오버로딩이 인자의 자료형이나 수가 다른 함수를 같은 이름으로 여러번 중복 정의하는 것입니다. 코드로 확인해보는 것이 빠르겠죠??
class example {
public:
void what()
void what(int a)
void what(int a, int b)
};
아래의 코드처럼 같은 함수명을 인자의 개수에 따라 다르게 인식하는 점을 이용하여 사용할 수 있는 것이 오버로딩입니다.
오버라이딩
: 오버라이딩은 이미 있는 함수를 무시해버리고 새롭게 함수를 재정의하는 것입니다. 아마 제가 수업시간에서 redefine이라고 배운 것 같은데, 정확한 정의는 오버라이딩인 것 같습니다. 오버라이드를 하기 위해서는 두 함수의 꼴이 정확히 같아야합니다.
class Base {
std::string s;
public:
Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }
virtual void incorrect() { std::cout << "기반 클래스 " << std::endl; }
};
class Derived : public Base {
std::string s;
public:
Derived() : Base(), s("파생") {}
void incorrect() const override { std::cout << "파생 클래스 " << std::endl; }
};
위의 형식처럼 부모 클래스의 있는 함수와 조금이라도 형식이 다르다면(const와 같이) 컴파일 오류가 발생하게 됩니다. 반드시 오버라이드를 하고싶다면, 형식을 똑같이 맞추어야합니다.
class example {
public:
void problem() {std::cout << "example class"" << std::endl;}
};
class test : public example {
public:
void problem() {std::cout << "test class" << std endl;}
};
아마 위의 클래스를 만들어서 main함수에서 실행하게 된다면 출력값은
example class
test class
일 것입니다. 이렇듯 부모 클래스에 정의 된 함수를 무시해버리고, 재정의해서 사용하는 것을 말합니다. 그럼 재지정을 한다고 해서 부모클래스의 함수를 사용하지 못하는 것이냐?? 아닙니다.
class example {
public:
void problem() {std::cout << "example class"" << std::endl;}
};
class test : public example {
public:
void problem() {example::problem(); std::cout << "test class" << std endl;}
};
위의 코드처럼 범위지정연산자 ::
을 활용해 함수를 호출하게 된다면, 부모클래스의 함수를 사용할 수는 있다는 점 참고하시면 될 것 같습니다.
int main(){
Base p;
Derived c;
std::cout << "=== 포인터 버전 ===" << std::endl;
Base *p_c = &c;
p_c -> what();
return 0;
}
/*
. 은 클래스의 멤버를 직접 접근
->은 포인터를 통해 멤버를 접근
x->y 은 (*x).y텍스트
*/
예시 코드를 보며 배우자고 한 코드를 위와 같은 main함수에서 실행하게 된다면 출력값은..
이렇게 나올 것이다. Base의 포인터 p_c가 왜 기반이라는 값을 반환하는지 알아보자. 이 내용이 업 캐스팅이다.
Derived is Base
라는 말을 기억하고 있나??
이처럼 객체 c도 어떻게 보면 Base 객체이기 때문에, Base객체를 가리키는 포인터가 c를 가리켜도 무방하다는 말이다. 단, Base의 포인터기에 이와 관련된 내용에만 접근을 할 수 있을 것이라는 추측이 가능하다.
즉 Derived 객체의 Base에 해당하는 정보밖에 가지고 있지 않다는 것이다.
그렇기에 Base의 what을 실행해서, s("기반")에 대한 내용을 출력하는 것이다.
(다운 캐스팅은 컴파일이 안된다. 위의 개념을 잘 이해했다면 왜 안되는지에 대한 설명이가능할 것이다. 댓글로 달아주시면, 확인해드리겠다.)
#include <iostream>
class Base{
private:
int a;
protected:
int b;
public:
int c;
};
class Derived : public A{
};
void main(){
Base A;
A.b = 5; //에러
A.c = 5;
}
접근제어
상속을 public으로 했을 때,
파생 클래스에서 a 멤버로 접근이 불가하다.(private)
파생 클래스에서 b 멤버로 접근이 가능하다.(protected)
파생 클래스에서 c멤버로 접근이 가능하다. (public)
외부에서 protected 멤버로 접근이 불가하다.
외부에서 public 멤버로 접근이 가능하다.
#include <bits/stdc++.h>
class Base {
public:
Base() { std::cout << "기반 클래스" << std::endl; }
virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "파생 클래스" << std::endl; }
virtual void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
int main() {
Base p;
Derived c;
Base *p_c = &c;
Base *p_p = &p;
std::cout << " == 실제 객체는 Base == " << std::endl;
p_p->what();
std::cout << " == 실제 객체는 Derived == " << std::endl;
p_c->what();
return 0;
}
출력결과
이상하다. 분명히 Base 객체의 포인터인 객체 p_p 와 p_c는 우리가 배운 내용에 의하면, 엔드포인트로 파생클래스 객체의 주소를 할당하여도, 알고 있는 내용이 Base 클래스 내용 밖에 없기 때문에, 모두 기반 클래스의 what()이라고 나와야한다.
이것이 가능해진 이유는 바로
class Derived : public Base {
public:
Derived() { std::cout << "파생 클래스" << std::endl; }
virtual void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
virtual
키워드 때문이다.
이 키워드는 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 일을 가리켜서 동적 바인딩(dynamic binding) 이라고 부른다.
그래서 위의 예시코드에서, Derived
의 what
을 실행할지 Base
의 what
을 실행할지는 런타임에서 결정된다는 것이다.
상속시 중요하게 생각해야 하는 부분. 소멸자를 가상함수로 만들어야하는 것이다.
#include <iostream>
#include <string>
class Parent {
public:
Parent() { std::cout << "생성자 호출" << std::endl;}
~Parent() {std::cout << "소멸자 호출" << std::endl;}
};
class Child: public Parent {
public:
Child(): Parent() { std::cout << "생성자 호출" << std::endl;}
~Child() {std::cout << "소멸자 호출" << std::endl;}
};
int main(){
std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
{Child c;}
// c가 중괄호 영역을 빠져나가면 소멸된다.
std::cout << "--- Parent 포인터로 Chile 가리킬 때 ---" << std::endl;
{
Parent *p = new Child();
delete p;
}
return 0;
}
결과는
평범한 child를 만들었을 때는,
Parent -> child -> child소멸자 -> parent 소멸자
순서로 문제없이 불러온다.
하지만 포인터로 가리킬 때는, child 클래스에서 소멸자가 호출되지 않는다. 이를 해결하지 않는다면 메모리 누수가 발생할 수 있기 때문에, 해결하는 것이 좋다. 이를 어떻게 해결하냐? 소멸자를 가상함수로 만들면된다 !!
#include <iostream>
#include <string>
class Parent {
public:
Parent() { std::cout << "생성자 호출" << std::endl;}
virtual ~Parent() {std::cout << "소멸자 호출" << std::endl;}
};
class Child: public Parent {
public:
Child(): Parent() { std::cout << "생성자 호출" << std::endl;}
~Child() {std::cout << "소멸자 호출" << std::endl;}
};
int main(){
std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
{Child c;}
std::cout << "--- Parent 포인터로 Chile 가리킬 때 ---" << std::endl;
{
Parent *p = new Child();
delete p;
}
return 0;
}
문제 없이 작동하는 것을 확인할 수 있다. 그렇기에, 포인터를 통해 동적할당을 할 경우에는, 기반 포인터로 파생 클래스를 가리켜도, 기반 클래스가 알고 있는 부분만 가리키기 때문에, 마찬가지로 소멸자를 호출하더라도 알고 있는 부분의 함수만 사용한다. 그렇기에 이런 경우에는 소멸자 또한 virtual로 만들게 되면 문제를 해결할 수 있다.
#include <iostream>
class Base {
public:
int baseValue;
Base(int value) : baseValue(value) {}
virtual void print() {
std::cout << "Base value: " << baseValue << std::endl;
}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int base, int derived) : Base(base), derivedValue(derived) {}
void print() override {
std::cout << "Derived value: " << derivedValue << std::endl;
}
};
void process(Base obj) {
obj.print();
}
int main() {
Derived derivedObj(10, 20);
process(derivedObj); // 여기서 객체 슬라이싱이 발생
return 0;
}
객체 슬라이싱(Object Slicing)은 주로 C++에서 발생하는 문제로, 다형성(polymorphism)을 사용하는 상황에서 발생한다.
이 문제는 기본 클래스와 파생 클래스 간의 형 변환(conversion) 시 발생하는데, 파생 클래스 객체를 기본 클래스 객체로 변환하면서 데이터가 손실되는 현상을 의미한다.
아래와 같이 Base와 Derived 클래스를 다루는 함수가 있을 때
void process(Base obj) { obj.print(); }
해당 코드에서 문제가 발생하게 되는데
process 함수
에derivedObj
를 전달할 때,Derived 클래스
객체가Base 클래스
로 변환되면서,Base 클래스
에만 있는 멤버 변수와 함수만을 가지고 있는 객체로 변환됩니다. 이때Derived 클래스
에서 추가된 정보나 동작은 손실되는데, 이것이객체 슬라이싱
이라고 불리는 현상입니다.int main() { Derived derivedObj(10, 20); process(derivedObj); // 여기서 객체 슬라이싱이 발생 return 0; }
이를 방지하려면 함수 매개변수를 참조 또는 포인터로 받는 방법을 사용합니다.
void process(const Base& obj) { obj.print(); }
void process(Base* pObj) { pObj->print(); }
#include <bits/stdc++.h>
class Animal{
public:
Animal() {}
virtual ~Animal() {}
virtual void speak() = 0;
// 순수 가상 함수 반드시 자식 클래스를 생성해서 인스턴스를 만들어야함.
};
class Dog : public Animal{
public:
Dog(): Animal() {}
void speak() override {std::cout << "몽몽" << std::endl;}
};
class Cat : public Animal {
public:
Cat(): Animal() {}
void speak() override {std::cout << "냥냥" << std::endl;}
};
int main(){
Animal* dog = new Dog(); //클래스 객체의 주소를 저장
Animal* cat = new Cat();
dog->speak();
cat->speak();
};
virtual void speak() = 0;
다른 함수와 다르게 몸통이 정의되어 있지 않은 특징이 있다. = 0을 붙여서 반드시 Derived 클래스에서 오버라이딩 되도록 만든 것을 순수 가상함수라고 한다. 예제 코드처럼, 동물에 따라 울음소리가 달라지는 경우, 부모 클래스에서 함수를 정의만 해놓고, 각각의 동물클래스에서 해당 함수를 오버라이드 해 상황에 맞게 정의해서 사용하면 된다.
Animal 클래스를 가르키는 포인터 변수 dog
에 동적 바인딩으로 Dog 클래스 객체의 주소를 저장한다. 그래서
dog -> speak();
에서 멤버함수를 호출하게 되면, 원래는 Animal 포인터기에 Animal에 해당하는 speak함수를 호출해야하지만,
virtual
과 override
를 통해, 각 클래스에 함수를 따로 정의를 했으므로
vt table
에서 speak()
함수를 찾아 사용하게 된다.
클래스 포인터에 객체를 할당하려면 동적으로 할당하기 위해 new
를 활용하여 Dog()
를 동적으로 할당하고 포인터 생성.
#include <iostream>
#include <string>
class Unit {
public:
Unit(const std::string& name) : name(name) {}
virtual void attack() const {
std::cout << name << " attacks." << std::endl;
}
virtual void show_status() = 0;
std::string getName() const {
return name;
}
private:
std::string name;
};
class ProtossUnit : public Unit {
public:
ProtossUnit(const std::string& name, int hp, int position_x, int position_y, int damage)
: Unit(name), hp(hp), position_x(position_x), position_y(position_y), damage(damage) {}
void attack() const override {
std::cout << getName() << "(Protoss) attacks with psionic energy" << std::endl;
std::cout << "HP: " << hp << ", Position X: " << position_x << ", Position Y: " << position_y << ", Damage: " << damage << std::endl;
}
void show_status() override {
std::cout << "HP: " << hp << ", Position X: " << position_x << ", Position Y: " << position_y << ", Damage: " << damage << std::endl;
}
private:
int hp;
int position_x;
int position_y;
int damage;
};
class TerranUnit : public Unit {
public:
TerranUnit(const std::string& name, int hp, int position_x, int position_y, int damage)
: Unit(name), hp(hp), position_x(position_x), position_y(position_y), damage(damage) {}
void attack() const override {
std::cout << getName() << "(Terran) attacks with bullets and guns" << std::endl;
std::cout << "HP: " << hp << ", Position X: " << position_x << ", Position Y: " << position_y << ", Damage: " << damage << std::endl;
}
void show_status() override {
std::cout << "HP: " << hp << ", Position X: " << position_x << ", Position Y: " << position_y << ", Damage: " << damage << std::endl;
}
private:
int hp;
int position_x;
int position_y;
int damage;
};
class ZergUnit : public Unit {
public:
ZergUnit(const std::string& name, int hp, int position_x, int position_y, int damage)
: Unit(name), hp(hp), position_x(position_x), position_y(position_y), damage(damage) {}
void attack() const override {
std::cout << getName() << "(Zerg) attacks biological weapons" << std::endl;
std::cout << "HP: " << hp << ", Position X: " << position_x << ", Position Y: " << position_y << ", Damage: " << damage << std::endl;
}
void show_status() override {
std::cout << "HP: " << hp << ", Position X: " << position_x << ", Position Y: " << position_y << ", Damage: " << damage << std::endl;
}
private:
int hp;
int position_x;
int position_y;
int damage;
};
int main() {
ProtossUnit zealot("Zealot", 100, 1, 2, 20);
TerranUnit marine("Marine", 80, 3, 4, 10);
ZergUnit zergling("Zergling", 60, 5, 6, 5);
Unit* units[] = {&zealot, &marine, &zergling};
for (Unit* unit : units) {
unit->attack();
unit->show_status();
}
return 0;
}
원래 기존의 과제는 해당 유닛에 대한 어택 출력만 하면 되는거였지만,
show_status
함수를 unit클래스에서 순수 가상함수로 지정한 후, 자식 클래스에서 override
하여 정의를 내리고, 클래스 포인터가 배열에 저장되어, 해당 자식 클래스를 참조할 때, show_status
함수로 체력, 위치, 데미지 까지 보여주는 기능을 추가했다. 포인터에 대한 예습을 안했으면 큰일날뻔!
private에 체력 위치 데미지의 정보를 넣긴했지만, 만약 프로젝트가 커진다면, protoss unit의 pirvate에 넣으면 안될 것 같기도 하다....
본 내용은 씹어먹는c++을 통해 알게된 내용을 개인적인 공부를 위해 정리한 포스팅입니다. 저작권을 해칠 의도가 없으며, 모든 것은 해당 블로그 저자의 지식재산입니다.
https://modoocode.com/210