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