C++로 많은 코드를 작성해왔지만, 강력한 기능 중 하나인 클래스를 활용하지 못했다. 이번 기회를 통해 클래스를 잘 정리할 수 있는 기회가 되면 좋겠다.
class Car
{
public:
int num; // 자동차의 상태 (1)
double gas; // 자동차의 상태 (2)
void show(); // 자동차의 기능 (1)
private:
int accident_cnt = 0; // 자동차의 상태 (3)
};
void Car::show()
{
cout << "차량 번호는 " << num << "\n";
cout << "연료량은 " << gas << " 입니다.\n";
}
int main()
{
Car car1; // 객체 선언
car1.num = 1111;
car1.gas = 20.5; // . 연산자를 사용해서 데이터 멤버에 값 대입
car1.show(); // 멤버 함수 호출
Car* pCar; // Car 클래스를 가리킬 포인터 준비
pCar = new Car; // 객체를 동적으로 생성한 후, 포인터에 그 주소를 대입
pCar->num = 2222;
pCar->gas = 30.0; // -> 연산자를 사용해서 멤버에 접근
delete pCar; // 객체 소멸
return 0;
}
구조체와 클래스 모두 변수와 함수를 하나로 묶을 수 있지만, 함수를 하나로 묶어야 할 경우에는 구조체 대신 클래스를 주로 사용한다.
클래스로부터 객체가 생성될 때, 자동적으로 호출되는 특수한 멤버 함수.
인수의 개수와 형(type)이 다르다면 오버로드를 통해 여러 개의 생성자를 정의할 수 있다.
Car::Car()
{
num = 0;
gas = 0.0;
cout << "자동차 생성\n";
}
Car::Car(int n, double g)
{
num = n;
gas = g;
cout << "차량 번호는 " << num << ", 연료량은 " << gas < "인 자동차 생성\n";
Car car1; // Car() 호출
Car car2(1111,20.5); // Car(int n, double g) 호출
생성자를 정의하지 않았을 때, 컴파일러에 의해 인수 없는 생성자(default constructor) 가 호출된다.
기존의 객체와 데이터 멤버, 멤버 함수는 각각의 객체에 값이 저장되어 연결되어 있었지만, 객체에 연결되지 않은 멤버를 가질 수도 있다.
이를 클래스 전체에 연결되었다. 라고 표현하고, 정적 멤버 라고 부른다.
각각의 객체에 데이터 멤버에 값을 저장할 수 있었는데, 이를 데이터 멤버가 해당 클래스에 연결되었다 고 표현한다.
멤버 함수도 객체를 생성하고 나서 호출할 수 있으므로, 멤버 함수도 객체와 연결되었다 고 볼 수 있다.
객체에 연결되지 않은 멤버를 가질 수도 있고, 이를 클래스 전체에 연결되었다 고 부른다. 이 때, 클래스에 연결된 멤버를 정적 멤버 라고 부른다.
class Car
{
private:
int num;
double gas;
public:
static int sum; // 정적 데이터 멤버
Car();
void show();
static void show_sum(); // 정적 멤버 함수
};
Car::Car()
{
num = 0;
gas = 0.0;
sum++;
cout << "자동차 생성\n";
}
void Car::show()
{
cout << "차량 번호는 " << num << ", 연료량은 " << gas << "\n";
}
void Car::show_sum() // 해당 정적 멤버 함수는 객체를 생성하지 않더라도 호출 가능
{
cout << "자동차는 모두 " << sum << "\n";
}
Car::show_sum(); // 객체를 생성하지 않아도 클래스 이름을 붙여서 호출 가능
이미 설계된 클래스를 바탕으로 새로운 클래스를 만들도록 지원하는 기능. 클래스를 파생한다 (extends) 라고 한다.
class 파생 클래스 명 : 접근 지정자 기본 클래스 명
{
파생 클래스에 추가할 멤버 선언
};
기본 클래스를 확장하여 파생 클래스를 선언할 수 있고, 생성된 파생 클래스는 기본 클래스의 멤버를 상속 받는다.
class Car // 기본 클래스 (base class)
{
private:
int num;
double gas;
public:
Car();
Car(int n,double g);
...
};
class RacingCar : Car // 파생 클래스 (derived class)
{
private:
int course;
public:
RacingCar();
RacingCar(int n, double g, int c)
...
};
Car::Car()
{
num = 0;
gas = 0.0;
cout << "자동차 생성\n";
}
Car::Car(int n,double g)
{
num = n;
gas = g;
cout << "차량 번호는 " << num << ", 연료량은 " << gas << " 인 자동차 생성\n";
}
RacingCar::RacingCar()
{
course = 0;
cout << "레이싱 카 생성\n";
}
RacingCar::RacingCar(int n,double g,int c) : Car(n,g) // 2.기본 클래스의 생성자 호출
{
// 3. 파생 클래스의 생성자 실행
course = c;
cout << "코스 번호가 " << c << " 인 레이싱카 생성\n";
}
int main()
{
RacingCar rc1(1234,20.5,5); // 1.파생 클래스의 생성자 호출
return 0;
}
출력 결과
차량 번호는 1234, 연료량은 20.5 인 자동차 생성
코스 번호가 5 인 레이싱카 생성
class Car // 기본 클래스 (base class)
{
private:
int num;
double gas;
public:
Car();
Car(int n,double g);
...
};
class RacingCar : Car // 파생 클래스 (derived class)
{
private:
int course;
public:
RacingCar();
RacingCar(int n, double g, int c)
...
};
위 코드 경우, 기본 클래스의 private 멤버는 파생 클래스조차 접근할 수 없다. private 대신 protected 접근 지정자를 사용하여 파생 클래스에서 기본 클래스 멤버에 접근할 수 있다.
class Car
{
protected: // 파생 클래스에서 해당 멤버에 접근 가능
int num;
double gas;
...
};
파생 클래스와 기본 클래스 바깥에서 파생 클래스가 소유한 멤버에 접근하는 경우, 파생 클래스가 어떻게 상속받는가 에 달려있다.
class RacingCar : (public | private | protected) Car { ... };
| 기본 클래스에서 접근 지정 | 상속 방법 | 파생 클래스에서 이용할 때 | 클래스 바깥에서 이용할 때 |
|---|---|---|---|
| public | 가능 | 가능 | |
| protected | public | 가능 | 불가능 |
| private | 불가능 | 불가능 | |
| ------------------ | ----------- | ------------------ | ------------------ |
| public | 가능 | 불가능 | |
| protected | protected | 가능 | 불가능 |
| private | 불가능 | 불가능 | |
| ------------------ | ----------- | ------------------ | ------------------ |
| public | 가능 | 불가능 | |
| protected | private | 가능 | 불가능 |
| private | 불가능 | 불가능 |
파생 클래스에 정의된 멤버 함수가 기본 클래스에 정의된 함수 대신 동작하는 것을 오버라이드 라고 한다.
class Car
{
protected:
int num;
double gas;
public:
Car();
void setCar(int n,double g);
viud show(); // 기본 클래스의 show() 멤버 함수
};
class RacingCar : public Car
{
private:
int course;
public:
RacingCar();
void setCourse(int c)
void show(); // 파생 클래스의 show() 멤버 함수
};
Car::Car()
{
num = 0;
gas = 0.0;
cout << "자동차 생성\n";
}
void Car::setCar(int n,double g)
{
num = n;
gas = g;
cout << "차량 번호는 " << num << ", 연료량은 " << gas << " 로 설정\n";
}
void Car::show()
{
cout << "차량 번호는 " << num << " 입니다.\n";
cout << "연료량은 " << gas << " 입니다.\n";
}
RacingCar::RacingCar()
{
course = 0;
cout << "레이싱 카 생성\n";
}
RacingCar::setCourse(int c)
{
course = c;
cout << "코스 번호를 " << c << " 로 설정\n";
}
void RacingCar::show()
{
cout << "레이싱 카의 차량 번호는 " << num << " 입니다.\n";
cout << "연료량은 " << gas << " 입니다.\n";
cout << "코스 번호는 " << course << " 입니다.\n";
}
int main()
{
RacingCar rc1;
rc1.setCar(1234, 20.5);
rc1.setCourse(5);
rc1.show();
return 0;
}
출력 결과
자동차 생성
레이싱 카 생성
차량 번호는 1234, 연료량은 20.5로 설정
코스 번호를 5로 설정
레이싱 카의 차량 번호는 1234 입니다.
연료량은 20.5 입니다.
코스 번호는 5 입니다.
기본 클래스의 멤버 함수 show() 가 아닌, 파생 클래스의 멤버 함수 show() 가 호출되는 것을 오버라이드 (override) 라고 한다.
기본 클래스형 포인터를 사용하여 기본 클래스의 객체 뿐 아니라 파생 클래스의 객체도 가리킬 수 있다.
int main()
{
Car* pCars[2]; // 기본 클래스형 포인터 선언
Car car1; // 기본 클래스 객체 생성
RacingCar rccar1; // 파생 클래스 객체 생성
pCars[0] = &car1;
pcars[0]->setCar(1234,20.5);
pCars[1] = &rccar1;
pCars[1]->setCar(4567,30.5);
pCars[0]->show();
pCars[1]->show();
return 0;
}
출력 결과
자동차 생성 // 기본 클래스 생성자
자동차 생성
레이싱카 생성
차량 번호는 1234, 연료량은 20.5로 설정
차량 번호는 4567, 연료량은 30.5로 설정
차량 번호는 1234 입니다.
연료량은 20.5 입니다.
차량 번호는 4566 입니다.
연료량은 30.5 입니다.
모두 기본 클래스의 show() 함수가 호출되었다.
기본 클래스형의 포인터를 사용하여 기본 클래스의 객체, 파생 클래스의 객체를 다룰 때, 기본 클래스의 멤버 함수가 호출된다.
기본 클래스형 포인터를 사용하여 멤버 함수를 호출하였을 때, 기본 클래스의 멤버 함수가 호출된다. 그러나 파생 클래스에서 새로 정의된(오버라이드) 함수를 호출하기 위해서는 기본 클래스의 멤버 함수 선언 시 virtual 이라는 지정자를 붙여준다.
class Car
{
...
public:
...
virtual void show();
};
class RacingCar :: public Car
{
...
};
...
int main()
{
Car* pCar[2];
... // 위 예시 코드와 동일
pCars[0]->show();
pCars[1]->show();
return 0;
}
출력 결과
...
차량 번호는 1234 입니다.
연료량은 20.5 입니다.
레이싱 카의 차량 번호는 4567 입니다.
연료량은 30.5 입니다.
코스 번호는 0 입니다.
기본 클래스 멤버 함수에 virtual 을 붙여, pCars[1]->show() 를 호출하여 새로운 파생 클래스에 정의된 멤버 함수가 호출하였다.
멤버 함수를 가상 함수로 만들면, 포인터가 가리키는 객체의 형(type)과 맞아 떨어지는 멤버 함수가 호출된다.
오버라이드 : 함수명과 인수가 모두 동일한 함수를 새로 정의하는 기능
오버로드 : 함수명은 같지만, 인수의 형과 개수가 다른 함수를 정의하는 기능
추상 클래스는 객체를 가질 수 없지만, 파생 클래스들을 한 곳에 모아 쉽게 제어할 수 있다는 장점이 있다.
가상 함수 선언 마지막에 = 0 을 붙인 멤버 함수. 본체가 없으며, 상속 받은 파생 클래스에서 오버라이드로 구현한다.
추상 클래스 (abstract class)
- 순수 가상 함수가 하나라도 존재하는 클래스
- 객체를 생성할 수 없다.
파생 클래스
- 추상 클래스에서 상속 받은 순수 가상 함수의 몸체를 오버라이드로 구현
class Vehicle
{
protected:
int speed;
public:
void setSpeed(int s);
virtual void show() = 0; // 순수 가상 함수
};
class Car : public Vehicle
{
private:
int num;
double gas;
public:
Car(int n, double g);
void show(); // 추상 클래스의 show 를 오버라이드
};
class Plane : public Vehicle
{
private:
int flight;
public:
Plane(int f);
void show(); // 추상 클래스의 show 를 오버라이드
};
...
void Car::show() // show() 함수의 몸체를 정의
{
cout << "차량 번호는 " << num << " 입니다.\n";
cout << "연료량은 " << gas << " 입니다.\n";
cout << "속도는 " << speed << " 입니다.\n";
}
void Plane::show() // show() 함수의 몸체를 정의
{
cout << "비행기 번호는 " << flight << " 입니다.\n";
cout << "속도는 " << speed << " 입니다.\n";
}
int main()
{
Vehicle* pVc[2];
Car car1(1234,20.5);
pVc[0] = &car1;
pVc[0]->setSpeed(60);
Plane pln1(232);
pVc[1] = &pln1;
pVc[1]->setSpeed(500);
pVc[0]->show(); // 객체 고유의 Car::show() 를 호출
pVc[1]->show(); // 객체 고유의 Plane::show() 를 호출
return 0;
}
추상 클래스로 객체는 만들 수 없지만, 그 클래스를 가리킬 수 있는 포인터를 사용하여 파생 클래스를 가리키도록 만들 수 있다.
추상 클래스의 순수 가상 함수는 반드시 파생 클래스에서 오버라이드 되어야한다. 즉, 추상 클래스를 상속받은 모든 파생 클래스는 추상 클래스의 순수 가상 함수와 같은 이름을 가진 함수가 반드시 존재한다.
추상 클래스로 다양한 파생 클래스와 객체가 생성되어 한꺼번에 조작해야 한다. 하나의 형(추상 클래스)으로 다른 형(파생 클래스)들을 조작해야 한다.
#include <iostream>
#include <typeinfo> // typeid 연산자를 사용하기 위해
using namespace std;
...
int main()
{
Vehicle* pVc[2];
Car car1(1234,20.5);
Plane pln1(232);
pVc[0] = &car1;
pVc[1] = &pln1;
for(int i=0;i<2;++i)
{
if(typeid(*pVc[i]) == typeid(Car))
cout << (i+1) << "번째 객체는 " << typeid(Car).name() << "의 객체입니다.\n";
else
cout << (i+1) << "번째 객체는 " << typeid(Car).name() << "의 객체가 아닙니다. << typeid(*pVc[i]).name() << "의 객체입니다.\n";
}
}
출력 결과
1번째 객체는 class Car입니다.
2번째 객체는 class Car의 객체가 아닙니다. class Plane의 객체입니다.
기본 클래스에서 파생되는 파생 클래스 뿐만 아니라, 파생 클래스로 파생 클래스를 만들 수도 있다. 즉, 클래스의 단계별 상속이 구현된다.
2개 이상의 클래스로부터 상속을 받아야하는 경우
class Base1
{
protected:
int bs1;
public:
Base1(int b1 = 0) {bs1 = b1;}
void showBs1();
};
class Base2
{
protected:
int bs2;
public:
Base2(int b2 = 0) {bs2 = b2;}
void showBs2();
};
class Ereived : public Base1, public Base2 // 클래스 다중상속
{
protected:
int dr;
public:
Derived(int d = 0) {dr = d;}
void showDr();
};
void Base1::showBs1()
{
cout << "bs1은 " << bs1 << " 입니다.\n";
}
void Base2::showBs2()
{
cout << "bs2는 " << bs2 << " 입니다.\n";
}
void Derived::showDr()
{
cout << "dr은 " << dr << " 입니다.\n";
}
int main()
{
Derived drv;
drv.showBs1();
drv.showBs2();
drv.showDr();
return 0;
}
출력 결과
bs1은 0 입니다.
bs2는 0 입니다.
dr은 0 입니다.
위와 같은 다중상속 구조에서 showBs1() 함수와 showBs2() 함수의 이름 같을 경우, Derived 클래스는 상속받은 showBs() 함수 호출할 수 없다.
// drv.showBs(); 컴파일 X
drv.Base1::showBs();
drv.Base2::showBs();
와 같은 방식을 통해 누구에게서 상속받은 함수를 호출할 것인지 명시적으로 선택하여 모호성을 해결해주자.
다중상속 시 발생하는 모호함이 또 존재한다.
기본 클래스0에서 파생된 기본 클래스1 과 기본 클래스2 모두를 상속받는 파생 클래스가 있을 경우, 해당 파생 클래스는 기본 클래스1과 기본 클래스2를 통해 기본 클래스0의 멤버를 2개 가지게 되는 모호함이 발생한다.
이러한 경우, 기본 클래스0 을 가상 기본 클래스 (virtual base class) 라고 하고, 기본 클래스0 에 virtual 키워드를 붙인다.
class Base0
{
protected:
int bs0;
public:
Base0(int b0=0) {bs0 = b0;}
void showBs0();
};
class Base1 : public virtual Base0 // Base0 을 가상 기본 클래스로 상속받음
{
protected:
int bs1;
public:
Base1(int b1=0) {bs1 = b1;}
void showBs1();
};
class Base2 : public virtual Base0 // Base0 을 가상 기본 클래스로 상속받음
{
protected:
int bs2;
public:
Base2(int b2=0) {bs2 = b2;}
void showBs2();
};
class Derived : public Base1, public Base2
{
protected:
int dr;
public:
Derived(int d=0) {dr = d;}
void showDr();
};
클래스의 선언부터 기능, 상속에 대한 부분을 정리해보았다.
C에서 C++ 로 넘어오면서 STL의 편리성에 눈이 멀어 클래스라는 강력한 기능을 놓친 것 같다.
공부할 내용도 많지만, 앞으로 공부할 언리얼 엔진이나 대규모 프로그램을 만들 때, 매우 유용한 기능이기 때문에 확실하게 잡고 가야겠다.