상속(inheritance)란 기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것이다.
상속의 장점
1. 기존에 작성된 클래스를 재활용할 수 있다.
2. 자식 클래스 설계 시 중복되는 멤버(멤버 변수, 멤버 함수)를 부모 클래스에 작성해두면 자식 클래스에서는 해당 멤버를 작성하지 않아도 된다.
3. 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련
C++에서는 JS에서 사용하던 extends와 다르게 public, private등을 통해서 상속하는 방식을 사용한다.
#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;
}
};
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;
}
};
이런식으로 (class 클래스명 : public,private,protected 등 상속받는 부모 클래스 명) 방식을 통해서 부모 클래스를 상속받을 수 있다.
이렇게 상속받은 클래스를 파생 클래스(자식 클래스) 라고도 한다.
여기서 만약 부모 클래스의 접근 제어가 private나 default로 설정된 멤버 변수나 함수는 자식 클래스에서 상속은 받으나 접근할 수는 없다.
이유는
1. 캡슐화: 객체 지향 프로그래밍의 핵심 원칙으로, 불필요한 외부 접근을 제한하는데, 부모 클래스의 private 멤버는 부모 클래스 내부에서만 관리되도록 설계 되어있기 때문이다.
2.클래스 설계 의도 유지: 부모 클래스의 private 멤버는 해당 클래스의 구현 세부사항이며, 자식 클래스가 이를 변경할 수 있다면 부모 클래스의 설계 의도가 깨질 수도 있기 때문이다.
3.유지보수성: 자식 클래스에서 부모 클래스의 세부 사항을 변경할 수 있다면 이를 상속 받는 모든 클래스에 영향이 갈 수 있기 때문이다.
여기서 사용한 protected는 위의 이유에 완벽하게 지켜진다고 보기는 어렵다.
=> 자식 클래스에서 부모 클래스의 protected에 선언된 멤버 함수,변수에 접근하여 수정할 수 있기 때문에 지켜진다고 보기엔 어렵다.
다형성은 하나의 객체가 여러가지 타입을 가질 수 있는 것을 의미한다.
#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;
}
각각 동물들의 클래스를 선언하여 각각 bark라는 기능을 구현하고 print를 각 클래스에 맞추어 별도로 만들어야 했다면,
모든 동물 클래스는 Animal 클래스를 상속받아 별도의 객체를 만들어 각 객체에서 bark를 재정의하여 사용할 수 있게 된다.
부모 클래스에 선언되어 파생 클래스에 의해서 재정의 되는 멤버 함수이다.
이러한 virtual은 몇가지 규칙이 존재한다.
- 클래스의 공개(public) 섹션에 선언합니다.
- 가상 함수는 정적(static)일 수 없으며 다른 클래스의 친구(friend) 함수가 될 수도 없습니다.
- 가상 함수는 실행시간 다형성을 얻기위해 기본 클래스의 포인터 또는 참조를 통해 접근(access)해야 합니다.
- 가상 함수의 프로토타입(반환형과 매개변수)은 기본 클래스와 파생 클래스에서 동일합니다.
- 클래스는 가상 소멸자를 가질 수 있지만 가상 생성자를 가질 수 없습니다.
함수의 정의가 이루어지지 않고 함수만 선언한 것이다.
// 기본 클래스: Animal
class Animal {
public:
// 가상 함수: 자식 클래스에서 재정의 가능
virtual void makeSound() = 0;
};
이렇게 선언된 순수 가상 함수가 존재한다면 이를 추상 클래스라고 부른다.
추상 클래스는 객체로 만들지 못하고 상속으로만 사용이 가능하고, 상속받은 자식 클래스는 무조건 해당 순수 가상 함수를 override 시켜줘야만 한다.(즉 무조건 재정의 해줘야만 한다는 뜻)
기존 가상 함수는 자식 클래스에서 재정의 할지는 선택이었다면, 순수 가상함수는 무조건 자식 클래스에서 재정의 해야만 한다.
이런 순수 가상 함수를 통한 추상 클래스의 구현은 하나의 클래스가 다양한 범위의 객체를 만들 수 있게끔 해주어 더 좋다.
순수 가상함수로만 이루어진 추상 클래스를 인터페이스라고 부른다.
정적 배열(Static array): 일반적인 배열
선언시 현재 저장공간에서 사용중이지 않은 연속적인 메모리들을 미리 예약해둔다.
동적 배열(Dynamic array):
정적 배열은 정해진 저장공간을 가지게 되므로 배열의 크기 변경이 불가능하지만, 동적배열은 정해진 크기 이상의 데이터가 들어올 경우에 그에 맞추어 배열의 크기 변경이 가능하게 만든 것이다.
대표적으로 list 자료 구조가 있다.
#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];
cout << "지금 출력이 되니?" << endl;
// 동적 메모리로 할당시 new에서 생성자가 실행된다.
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;
}
이 코드를 참조했을 때 정적 배열(메모리)로 사용한 문장은 바로 생성자가 실행되고 끝나는 반면 동적 배열(메모리)로 사용한 문장은 new로 선언시에만 작동하여 생성자를 생성하기 때문에 에러를 발생시키지 않는다.
할당 시점:
관리 주체:
수명:
개발자의 해제 책임:
추가로 정적 메모리는 운영체제가 자동으로 정리해주기에 해제할 필요가 없다.
할당 시점:
관리 주체:
수명:
개발자의 해제 책임: