c와 cpp의 확연한 다른점인 객체지향에 대해 알아보겠습니다.

 

첫번째로 상속에 대해 먼저 알아보겠습니다.

 

자동차를 만든다고 가정해보겠습니다.

자동차마다 엔진이나 서스팬션 같은 세부 스펙은 다 다르지만 모든 자동차가 가지는 속성도 있습니다.

예를 들어 속도나 색상이나 문이라던지 다양합니다.

하지만 이를 class로 표현 할 때 매번 모든 차량의 공통적인 특성을 반복해서 구현하기보다는

하나를 구현해놓고 사용할 수 있으면 관리도 편리하고 코드도 짧아집니다.

 

 

Vehicle -> Bycle/Truck Class 구현

 

#include <iostream>
#include <string>

using namespace std;

class Vehicle {
protected:
    string color;
    int speed;

public:
    Vehicle(string c, int s) : color(c), speed(s) {}

    void move() {
        cout << "The vehicle is moving at " << speed << " km/h." << endl;
    }

    void setColor(string c) {
        color = c;
    }

    string getColor() {
        return color;
    }
};

Vehicle 에 대한 클래스를 생성해보았습니다.

멤버변수를 protected로 선언해주고

public 에 생성자를 만들어주고 멤버함수들도 놓아줍니다. void move는 몇 키로로 움직이는 지 프린트로 나타주는 함수이고 void setColor 은 color = c 로 초기화해주는 함수입니다.

 

이제 Bicycle와 Truck이라는 파생 클래스(자식)를 만들어주겠습니다.

// 파생 클래스 1: 자전거
class Bicycle : public Vehicle {
private:
    bool hasBasket;

public:
    Bicycle(string c, int s, bool basket) : Vehicle(c, s), hasBasket(basket) {}

    void ringBell() {
        cout << "Bicycle bell: Ring Ring!" << endl;
    }

};

// 파생 클래스 2: 트럭
class Truck : public Vehicle {
private:
    int cargoCapacity;

public:
    Truck(string c, int s, int capacity)
        : Vehicle(c, s), cargoCapacity(capacity) {
    }

    void loadCargo() {
        cout << "Truck loading cargo. Capacity: " << cargoCapacity << " tons."
            << endl;
    }
};

자 이제 메인함수에서 이를 사용해봅시다.

 

int main()
{
    Bicycle b("Yellow", 30, true);
    Truck t("Blue", 40, 95);

    b.ringBell();
    t.loadCargo();

    return 0;
}

자전거 클래스 소속인 b를 만들어주고 트럭클래스 소속인 t를 만들어줍니다. 또한 클래스 내부의 멤버함수도 이용하여 호출해봅니다.

 

이제 다형성에 대해 알아보겠습니다.

 

예로 모든 동물들은 울음소리가 있습니다.

 

사자의 울음소리,늑대의 울음소리...등등

 

이를 클래스로 정의하여 울음소리에 해당되는 문자열을 받고 출력하는 동물클래스를 제작해보겠습니다.

 

또한 이를 바탕으로 클래스를 인자로 받고 울음소리를 출력하는 함수를 제작해보겟습니다.

 

먼저 다형성이 적용되지않은 클래스를 살펴보겠습니다.

#include <iostream>
#include <string>

using namespace std;

class Lion
{
public:
	Lion(string word):m_word(word){}
	void bark() { cout << "Lion" << " " << m_word << endl; }
private:
	string m_word;
};

class Wolf
{
public:
	Wolf(string word) :m_word(word) {}
	void bark() { cout << "Wolf" << " " << m_word << endl; }

private:
	string m_word;
};

void print(Lion lion)
{
	lion.bark();
}

void print(Wolf wolf)
{
	wolf.bark();
}
int main()
{
	Lion lion("ahaaaaaa!");
	Wolf wolf("ohhhhh");

	print(lion);
	print(wolf);

	return 0;
}

여기서 dog class가 하나 더 만들어졌다고 해봅시다. 주목할 점은 사용자가 알아야 할 `class`가 하나 더 늘어났으며, 외부 함수도 하나 더 필요해졌다는 것 입니다.

 

#include <iostream>
#include <string>

using namespace std;

class Lion
{
public:
	Lion(string word):m_word(word){}
	void bark() { cout << "Lion" << " " << m_word << endl; }
private:
	string m_word;
};

class Wolf
{
public:
	Wolf(string word) :m_word(word) {}
	void bark() { cout << "Wolf" << " " << m_word << endl; }

private:
	string m_word;
};

class Dog
{
public:
	Dog(string word) :m_word(word) {}
	void bark() { cout << "Dog" << " " << m_word << endl; }

private:
	string m_word;
};

void print(Lion lion)
{
	lion.bark();
}

void print(Wolf wolf)
{
	wolf.bark();
}

void print(Dog dog)
{
	dog.bark();
}


int main()
{
	Lion lion("ahaaaaaa!");
	Wolf wolf("ohhhhh");
	Dog dog("oooooooooooooops");

	print(lion);
	print(wolf);
	print(dog);

	return 0;
}

 

새로운 동물이 생길때마다 관리해야 할 class가 많이 생기는 이유는 아래와 같습니다. Lion, Wolf, Dog 모두 타입이 다르기 때문에 전부 따로 관리 해야 합니다. 여기서 필요한 개념이 다형성 입니다. 다형성은 대표 class를 만들어서 정의만 하고, 실제 구현은 파생 클래스에서 하는 기법 입니다. 이 때 실제 호출시 파생 클래스를 확인해라 라는 의미로 함수앞에 virtual을 붙입니다. 이러한 모양의 함수를 가상함수라고 합니다.

 

 

다형성을 정리하기 전! 마지막으로 다형성을 이해하는 데 필요한 문법들이 표함된 예시코드 몇개를 같이 보겠습니다

기본 클래스에 일반 가상함수 적용된 코드 입니다. 일반 가상함수의 정의는 필수가 아닙니다. Animal 타입 앞에 *이 붙었습니다. 이걸 포인터 타입 이라고 합니다. 일반 변수가 값을 담는 다면, 포인터변수는 말 그대로 변수를 가르킬 수 있다고 보시면 됩니다. 대상이 되는 변수는 앞에 &를 붙이면 됩니다.

#include <iostream>
using namespace std;

// 기본 클래스: Animal
class Animal {
public:
    // 가상 함수: 자식 클래스에서 재정의 가능
    virtual void makeSound() {
        cout << "Animal makes a sound." << endl;
    }
};

// 파생 클래스: Dog
class Dog : public Animal {
public:
    void makeSound() {
        cout << "Dog barks: Woof! Woof!" << endl;
    }
};

// 파생 클래스: Cat
class Cat : public Animal {
public:
    void makeSound() {
        cout << "Cat meows: Meow! Meow!" << endl;
    }
};

int main() {
    // Animal 타입 포인터로 다양한 객체를 가리킴
    Animal* myAnimal;
    Dog myDog;
    Cat myCat;

    // Dog 객체 가리키기
    myAnimal = &myDog;
    myAnimal->makeSound();  // Dog의 makeSound() 호출

    // Cat 객체 가리키기
    myAnimal = &myCat;
    myAnimal->makeSound();  // Cat의 makeSound() 호출

    return 0;
}

 포인터가 여기서 무슨 역할을 하는지 세세하게 살펴보겠습니다.

 

 

1. 포인터의 기본 구조

 
Animal* myAnimal;
  • Animal*: Animal 타입의 포인터라는 뜻.
    • 포인터란?
      • 다른 변수(또는 객체)의 메모리 주소를 저장하는 변수.
      • 예: myAnimal은 Animal 객체의 주소를 저장할 수 있음.
  • myAnimal:
    • 이 포인터 변수는 실제 Animal 객체의 주소를 가리키는 역할을 합니다.

2. 왜 포인터가 필요할까?

  1. 객체의 주소를 저장하기 위해
    • 포인터는 객체를 직접 복사하지 않고, 객체의 주소를 저장합니다.
    • 이를 통해, 같은 객체를 여러 곳에서 참조할 수 있습니다.
  2. 다형성을 구현하기 위해
    • 부모 클래스의 포인터(Animal*)로 자식 클래스 객체(Dog, Cat)를 가리킬 수 있습니다.

3. 코드 분석

 
Animal* myAnimal; Dog myDog; Cat myCat;

3.1 Animal* myAnimal;

  • myAnimal은 Animal 타입의 포인터입니다.
  • myAnimal은 Animal 객체나 이를 상속받은 자식 클래스 객체의 주소를 저장할 수 있습니다.

3.2 Dog myDog;

  • myDog은 Dog 클래스의 객체입니다. 메모리에 직접 생성된 객체로, myAnimal이 이 객체를 가리킬 수 있습니다.

3.3 Cat myCat;

  • myCat은 Cat 클래스의 객체입니다. 이 역시 메모리에 생성된 객체이며, myAnimal이 이 객체를 가리킬 수 있습니다.

4. 포인터의 동작

포인터의 역할

  1. 포인터는 메모리 주소를 저장:
    • myAnimal은 객체(myDog 또는 myCat)의 주소를 저장합니다.
  2. 포인터로 객체에 접근:
    • myAnimal->makeSound()를 호출하면, 실제로 myAnimal이 가리키는 객체의 makeSound 함수가 실행됩니다.

5. 동작 예제

 
myAnimal = &myDog; // myDog 객체의 주소를 myAnimal에 저장 myAnimal->makeSound(); // myDog의 makeSound() 호출

실행 흐름

  1. myAnimal = &myDog;
    • myAnimal 포인터가 myDog 객체의 주소를 가리킵니다.
  2. myAnimal->makeSound();
    • 포인터 myAnimal이 가리키는 객체(myDog)의 makeSound() 함수가 호출됩니다.

6. 코드에서 중요한 점

  1. 포인터는 변수 이름과 붙어있다
    • Animal* myAnimal에서 *는 포인터 변수인 myAnimal에 붙습니다.
    • 쉽게 말해, *는 "이 변수는 포인터다"라는 것을 나타냅니다.
  2. 타입은 Animal
    • Animal*는 "Animal 타입 객체의 주소를 저장할 수 있는 포인터"를 뜻합니다.
    • 따라서, Dog와 Cat 객체도 저장할 수 있습니다(다형성).

 

자 이제 가상함수를 순수가상함수로 바꿔보겠습니다.

순수 가상함수가 적용된 코드 입니다.

  • 가상함수에 0을 대입하는 것 같은 문법입니다. 순수가상함수는 해당 함수를 파생 클래스에서 반드시 구현해야 합니다.
  • 순수가상함수를 포함한 클래스는 그 자체로 변수가 될수 없습니다.(인스턴스화 한다 라고도 합니다.) 따라서 변수로 선언시 에러가 발생 합니다.
#include <iostream>
using namespace std;

// 기본 클래스: Animal
class Animal {
public:
    // 가상 함수: 자식 클래스에서 재정의 가능
    virtual void makeSound() = 0;
};

// 파생 클래스: Dog
class Dog : public Animal {
public:
    void makeSound() {
        cout << "Dog barks: Woof! Woof!" << endl;
    }
};

// 파생 클래스: Cat
class Cat : public Animal {
public:
    void makeSound() {
        cout << "Cat meows: Meow! Meow!" << endl;
    }
};

int main() {
    // Animal 타입 포인터로 다양한 객체를 가리킴
    Animal* myAnimal;
    Dog myDog;
    Cat myCat;

    // Dog 객체 가리키기
    myAnimal = &myDog;
    myAnimal->makeSound();  // Dog의 makeSound() 호출

    // Cat 객체 가리키기
    myAnimal = &myCat;
    myAnimal->makeSound();  // Cat의 makeSound() 호출

    return 0;
}

 

 

이제 new 연산자를 활용한 코드를 살펴보겠습니다.

  • new 연산자를 하지 않고 클래스 배열을 만들게 되면, 생성과 동시에 기본생성자가 모두 호출 됩니다. 원하는 시점에 객체가 생성되게 하기 위해서 new 연산자를 사용합니다.
  • new 연산자로 생성한 객체는 사용자가 직접 메모리 관리를 해줘야 합니다. 출력값을 보고 어떤 함수가 어떤 순서로 호출되었는지 유심히 봐주세요.
#include <iostream>
using namespace std;

// 기본 클래스: Employee
class Employee {
public:
    Employee() {
        cout << "Employee 기본 생성자 호출!" << endl;
    }

    virtual void work() {
        cout << "Employee is working." << endl;
    }

    virtual ~Employee() {
        cout << "Employee 소멸자 호출!" << endl;
    }
};

// 파생 클래스: Developer
class Developer : public Employee {
public:
    Developer() {
        cout << "Developer 기본 생성자 호출!" << endl;
    }

    void work() {
        cout << "Developer is coding." << endl;
    }

    ~Developer() {
        cout << "Developer 소멸자 호출!" << endl;
    }
};

// 파생 클래스: Manager
class Manager : public Employee {
public:
    Manager() {
        cout << "Manager 기본 생성자 호출!" << endl;
    }

    void work() {
        cout << "Manager is planning." << endl;
    }

    ~Manager() {
        cout << "Manager 소멸자 호출!" << endl;
    }
};

int main() {
    cout << "=== 정적 배열 사용 ===" << endl;

    // Employee 배열 (기본 생성자 호출됨)
    Employee team_static[2];  // 기본 생성자만 호출됨
    team_static[0].work();    // Employee의 work() 호출
    team_static[1].work();    // Employee의 work() 호출

    cout << "=== 동적 배열 사용 ===" << endl;

    // 동적 메모리 할당
    Employee* team_dynamic2[2];
    team_dynamic2[0] = new Developer();  // Developer 객체 생성
    team_dynamic2[1] = new Manager();    // Manager 객체 생성

    for (int i = 0; i < 2; i++) {
        team_dynamic2[i]->work();  // 다형성 적용, 각각의 work() 호출
    }

    // 동적 메모리 해제
    for (int i = 0; i < 2; i++) {
        delete team_dynamic2[i];
    }

    return 0;
}

동적메모리를 사용할때에는 꼭 메모리 누수가 일어나지않게 메모리 해제를 해주세요!