继承是一种面向对象编程的重要特性,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和行为。基类中定义的成员变量和成员函数可以在派生类中使用,从而实现代码重用和层次化设计。
并不是所有的类都能存在继承关系。必须满足: is a 判定
大千世界,不是什么东西都能产生继承关系。只有存在某种联系,才能表示有继承关系。如:哺乳动物是动物,猫是哺乳动物,因此,猫是动物,等等。所以在学习继承后面的内容之前,先说说两个术语 is A
和 has A
。
is A
是一种继承关系,指的是类的父子继承关系。表达的是一种方式:这个东西是那个东西的一种。例如:西红柿是蔬菜的一种。 西红柿:子类,蔬菜:父类
has a
是一种组合关系,是关联关系的一种(一个类中有另一个类型的实例),是整体和部分之间的关系(比如电脑和GPU之间),并且代表的整体对象负责构建和销毁部分对象,代表部分的对象不能共享。
#include
#include
using namespace std;
//父类
class Vegetable{
public:
string name;
bool delicious;
};
//子类
class Cabbage:public Vegetable{
};
int main(){
//子类虽然没有声明name 和 delicious ,但是继承了Vegetable类,等同于自己定义的效果一样
Cabbage ca;
ca.name = "Chinese cabbage";
ca.delicious = true;
cout << ca.name << " " << ca.delicious << endl;
return 0 ;
}
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是在继承基类时指定的。 如: class DerivedClass : public BaseClass
。 我们几乎不使用 protected 或 private 继承,通常使用 public继承。
表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口
表示私有成员,该成员仅在类内可以被访问,在类的外面无法访问;
表示保护成员,保护成员在类的外面同样是隐藏状态,无法访问。但是可以在子类中访问。
#include
using namespace std;
class BaseClass{
public:
string name;
protected:
int no;
private:
int num;
};
class DerivedClass: public BaseClass{
void print(){
name = "aili";
no = 2;
num = 45; //编译错误! 无法访问 num
}
};
int main(){
DerivedClass de;
de.name = "baidu" ;
de.no = 12; //编译错误! 无法访问 no
de.num = 18 ; //编译错误! 无法访问 num
return 0 ;
}
#include
using namespace std;
class BaseClass{
public:
string name;
protected:
int no;
private:
int num;
};
class DerivedClass: private BaseClass{
void print(){
name = "aili";
no = 2;
num = 45; //编译错误! 无法访问 num
}
};
int main(){
DerivedClass de;
de.name = "baidu" ;//编译错误! 无法访问 name
de.no = 12; //编译错误! 无法访问 no
de.num = 18 ; //编译错误! 无法访问 num
return 0 ;
}
#include
using namespace std;
class BaseClass{
public:
string name;
protected:
int no;
private:
int num;
};
class DerivedClass: protected BaseClass{
void print(){
name = "aili";
no = 2;
num = 45; //编译错误! 无法访问 num
}
};
int main(){
DerivedClass de;
de.name = "baidu" ;//编译错误! 无法访问 name
de.no = 12; //编译错误! 无法访问 no
de.num = 18 ; //编译错误! 无法访问 num
return 0 ;
}
构造函数是对象在创建时调用,析构函数是对象在销毁时调用。但是在继承关系下,无论在对象的创建还是销毁,都会执行父类和子类的构造和析构函数。 它们一般会有以下规则:
- 子类对象在创建时会首先调用父类的构造函数;父类构造函数执行完毕后,执行子类的构造函数;
- 当父类的构造函数中有参数时,必须在子类的初始化列表中显示调用;
- 析构函数执行的顺序是先调用子类的析构函数,再调用父类的析构函数
#include
using namespace std;
class Base{
public:
Base(){
cout<< "基类的构造..." <<endl;
}
};
class Derived : public Base {
public:
Derived(){
cout<< "派生类的构造..." <<endl;
}
};
int main() {
Base b ;
cout << "------------------------" << endl;
Derived d;
return 0;
}
运行结果:
基类的构造...
------------------------
基类的构造...
派生类的构造...
创建派生类对象的时候,会先执行基类的构造,然后在执行派生类的构造。
因为派生类里面有可能使用到了基类的成员(例如指针成员),这些成员应该在基类先完成初始化,然后派生类才能使用他们。
#include
using namespace std;
class Base{
public:
Base(){
cout<< "基类的构造..." <<endl;
}
~Base(){
cout<< "基类的析构..." <<endl;
}
};
class Derived : public Base {
public:
Derived(){
cout<< "派生类的构造..." <<endl;
}
~Derived(){
cout<< "派生类的析构..." <<endl;
}
};
int main() {
//Base b ;
cout << "------------------------" << endl;
Derived d;
return 0;
}
运行结果:
------------------------
基类的构造...
派生类的构造...
派生类的析构...
基类的析构...
先执行子类的析构,然后在执行父类的析构
确保子类完全释放了之后,再走父类的释放回收工作,有可能子类里里面使用到了父类的一些成员。
3. 继承和组合
如果在继承状态下,子类中的成员又含有其他类的对象属性,那么他们之间的构造很析构调用顺序,遵循以下原则:
a. 先调用父类的构造函数,再调用组合对象的构造函数,最后调用自己的构造函数;
b. 先调用自己的析构函数,再调用组合对象的析构函数,最后调用父类的析构函数。
代码:
#include
using namespace std;
class Base{
public:
Base(){
cout<< "基类的构造..." <<endl;
}
~Base(){
cout<< "基类的析构..." <<endl;
}
};
class Other{
public:
Other(){
cout<< "Other的构造..." <<endl;
}
~Other(){
cout<< "Other的析构..." <<endl;
}
};
class Derived : public Base {
public:
Other o;
Derived(){
cout<< "派生类的构造..." <<endl;
}
~Derived(){
cout<< "派生类的析构..." <<endl;
}
};
int main() {
//Base b ;
cout << "------------------------" << endl;
Derived d;
return 0;
}
运行结果:
------------------------
基类的构造...
Other的构造...
派生类的构造...
派生类的析构...
Other的析构...
基类的析构...
继承关系下,子类的默认构造函数会隐式调用父类的默认构造函数,假设父类没有默认的无参构造函数,那么子类需要使用参数初始化列表方式手动调用父类有参构造函数。
在创建子类对象前,就必须完成父类对象的创建工作,也就是在执行子类构造函数之前,必须先执行父类的构造函数。c++ 使用初始化列表来完成这个工作
#include
using namespace std;
class Base{
public:
string name;
int num;
Base(){
cout<< "基类的无参构造..." <<endl;
}
Base(string name,int num){
cout<< "基类的有参构造..." <<endl;
}
};
class Derived : public Base {
public:
Derived(){
cout<< "派生类的无参构造..." <<endl;
}
Derived(string name,int num){
cout<< "派生类的有参构造..." <<endl;
}
};
int main() {
Derived d;
cout << "------------------------" << endl;
Derived d1("libai",24);
return 0;
}
运行结果:
基类的无参构造...
派生类的无参构造...
------------------------
基类的无参构造...
派生类的有参构造...
默认情况下,子类的所有构造永远会调用父类的无参构造。如果父类没有无参构造,只有有参构造
#include
using namespace std;
class Base{
public:
string name;
int num;
Base(){
cout<< "基类的无参构造..." <<endl;
}
Base(string name,int num){
cout<< "基类的有参构造..." <<endl;
}
};
class Derived : public Base {
public:
Derived(){
cout<< "派生类的无参构造..." <<endl;
}
Derived(string name,int num):Base(name,num){
cout<< "派生类的有参构造..." <<endl;
}
};
int main() {
Derived d;
cout << "------------------------" << endl;
Derived d1("libai",24);
return 0;
}
运行结果:
基类的无参构造...
派生类的无参构造...
------------------------
基类的有参构造...
派生类的有参构造...
创建子类对象,必须要先走父类的构造函数,才能走子类的构造函数
在子类里面手动调用父类的有参构造,需要使用初始化列表的方式来完成,初始化列表的工作是在子类构造函数体调用之前就执行了。