코딩 학습

C++ 기초 - 객체지향 프로그래밍

이개 2026. 3. 9. 11:17

C++의 클래스는 C언어의 struct를 확장하여 객체지향적 특성을 추가한 개념이다.

하나의 기본 클래스를 정의하고 공통 속성을 구현하여 재사용성을 높인다.

 

상속

  • 상속하는 클래스의 경우는 변수를 protected로 해서 상속한 클래스가 데이터를 접근할 수 있어야함.
    • protected는 외부 접근은 막고, 파생 클래스에서는 접근 가능
    • private으로 설정하면 상속받은 클래스에서 접근을 못하며 public은 전체 접근 가능하므로 쓰지 않음
  • 멤버 초기화 리스트
    • Vehicle(string c, int s) : color(c), speed(s) {} 
    • 각 인자를 변수에 넣어줌.
    • 멤버 초기화 리스트는 생성자의 코드 부분보다 먼저 실행됨.
    • Class 호출 -> 멤버 초기화 리스트 실행  -> Class 코드 실행
  • 자식 클래스의 생성자는 부모 클래스의 생성자 호출 가능
  • 생성자를 명시하지 않으면 인자가 없는 기본 생성자가 호출됨
  • 직접 정의한 생성자는 기본값을 넣을 경우 일부 인자만 넣어서 호출 가능하고, 기본값이 없으면 전체를 넣어줘야만 작동
    • 기본값은 뒤에서부터 넣을 수 있음.
    • 생성자 오버로딩 가능
  • Base 함수에 virtual 키워드를 붙이면 인터페이스가 된다.
    • 동적 바인딩을 통해 실체 호출된 객체의 타입에 따라 파생 클래스의 함수가 실행되도록 하려면 함수 앞에 virtual 키워드를 붙인다.
    • virtual을 붙이지 않으면, 파생 클래스에서는 원본 클래스의 함수를 호출한다.
  • 코드 예시
#include <iostream>

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;
	}
};

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;
	}

};

int main()
{
	Bicycle b("Yellow", 20, true);

	b.move();
	b.ringBell();
	
	return 0;
}

 

  • 접근제어 - class Derived : public Base
    • public -> 공개 상속. Base의 public은 Derived에서도 public 
    • protected -> Derived에서는 protected
    • private -> 접근 불가
    • 인터페이스를 유지하면서 상속하고 싶을 때 public 사용
    • class 상속은 기본이 private, struct 상속은 기본이 public
      • public 상속하려면 꼭 public 붙여줘야함.
class Base {
public:
    int x;
protected:
    int y;
private:
    int z;
};

class Derived : public Base {
public:
    void func() {
        x = 1; // OK (public)
        y = 2; // OK (protected)
        // z = 3; // ❌ private 접근 불가
    }
};
class Derived : private Base {
    // Base의 public, protected 모두 private로 변환
};
  • private이나 protected는 내부에서만 Base의 멤버를 사용하고 싶을 때 사용
  • private과 protected의 차이
#include <iostream>
using namespace std;

class Base {
public:
    void hello() { cout << "Hello from Base\n"; }
};

class PrivateDerived : private Base {
public:
    void callHello() { hello(); } // 내부에서는 접근 가능
};

class PublicDerived : public PrivateDerived {
public:
    void test() {
        // hello(); // ❌ 불가능, Base 멤버는 PublicDerived에서 접근 불가
        callHello(); // ✅ 가능, PrivateDerived의 public 멤버 접근
    }
};

int main() {
    PublicDerived pd;
    // pd.hello(); // ❌ 불가능
    pd.callHello(); // ✅ 가능
}

 

  • 상속 시 접근 지정 붙이는 것은 Base 클래스의 접근성을 다시 지정해주는 것에 가깝다.

 

다형성

 

다형성이 적용된 코드

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
	Animal() {}
	virtual void bark() {};
};

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

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

private:
	string m_word;
};

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

private:
	string m_word;
};

void print(Animal* animal)
{
	animal->bark();
}

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

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

	return 0;
}

 

위와 같이 원본 클래스 포인터를 함수에 전달해서 사용하는 것도 가능하다.

가상 함수의 경우는 파생 클래스에서 정의한 함수가 실행된다.

 

가상함수와 순수 가상함수

  • Animal 클래스를 정의하고, makeSound()라는 멤버 함수를 만든다고 할 때,
    • void makeSoudn() {} 라고 하면 일반 멤버 함수. 파생 클래스에서 호출해도 이 내용이 실행된다.
    • virtual void makeSound() {} 라고 하면 가상함수.
    • virtual void makeSound() = 0; 이라고 하면 순수 가상함수가 된다.
  • 순수 가상함수는 반드시 파생 클래스에서 구현해야함.
  • 추상 클래스란 하나 이상의 순수 가상함수를 가진 클래스로, 객체 생성이 불가능.

 

일반 함수와 가상함수의 차이

  • 일반 함수는 정적 바인딩(컴파일 단계에서 결정), 가상 함수는 정적 바인딩(실행 시 결정)
class Animal {
public:
    void speak() {
        std::cout << "Animal sound\n";
    }
};

class Dog : public Animal {
public:
    void speak() {
        std::cout << "Woof\n";
    }
};

int main() {
    Animal* a = new Dog();
    a->speak();
}

출력 : Animal sound

 

왜냐하면

  • a의 타입이 Animal* 이기 때문에
  • 컴파일할 때 이미 Animal::speak()로 결정
class Animal {
public:
    virtual void speak() {
        std::cout << "Animal sound\n";
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof\n";
    }
};

int main() {
    Animal* a = new Dog();
    a->speak();
}

출력 : Woof

 

왜냐하면

  • 실제 객체는 Dog
  • 실행 시점에 Dog::speak() 호출

 

가상함수가 있어야 가상함수 테이블이 만들어지고, 실행시 파생 클래스의 함수가 실행된다.

 

가상함수 사용 예시

vector<Animal*> animals;
animals.push_back(new Dog());
animals.push_back(new Cat());

for (auto a : animals)
    a->speak();

* auto는 컴파일러가 타입을 자동으로 추측하게 하는 키워드.

  • 초기값이 반드시 필요
  • vector<string>::iterator it = v.begin(); -> auto it = v.begin();

 

가상함수를 사용하는 경우

  • Is-a를 적용해본다.
  • 동물은 사자다(X) 사자는 동물이다(O) 처럼 Is-a 관계일 때 다형성을 사용한다.

 

객체 포인터로 가리키는 객체의 멤버에 접근할 때는 -> 연산자를 쓰는데, 원본 클래스의 포인터로 다양한 객체를 가리킬 수 있다.

'코딩 학습' 카테고리의 다른 글

C++ 기초 - 참조  (0) 2026.04.04