C++ 공부 - 모두의 코드 (1)

자훈·2023년 11월 7일
1

C++ / C study

목록 보기
2/8
post-thumbnail

📌 동적 메모리 할당

형식

동적 할당 연산자
– 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 형식으로 정의할 수 있다.

  • 파생클래스는 부모클래스의 기능을 포함한다.
  • 파생클래스는 부모클래스의 기능을 수행할 수 있다.
  • 즉 모든 Derived is Base이다!

이렇게 모든 상속관계는 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 멤버로 접근이 가능하다.

📌 Virtual

#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) 이라고 부른다.

그래서 위의 예시코드에서, Derivedwhat을 실행할지 Basewhat을 실행할지는 런타임에서 결정된다는 것이다.

그렇다면 virtual 함수들은 어떻게 처리될까? 포인터가 어떤 객체를 가리키는지 어떻게 알 수 있는 것일까??

  • virtual 함수들은 VT table 을 통해서 관리되고 VT table에 접근하여 신이 필요한 함수의 주소를 찾아 호출한다.
  • virtual 함수가 있어 vt테이블이 있다면 클래스의 사이즈가 테이블을 가르키는 만큼 증가한다.

🌟Virtual 소멸자

상속시 중요하게 생각해야 하는 부분. 소멸자를 가상함수로 만들어야하는 것이다.

#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로 만들게 되면 문제를 해결할 수 있다.

🌟오브젝트 Slicing problem

#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함수를 호출해야하지만,
virtualoverride를 통해, 각 클래스에 함수를 따로 정의를 했으므로
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

0개의 댓글