目录
一.前言
二.继承机制
三.父子类赋值兼容规则
四.继承中的作用域
五.子类的默认成员函数
1.构造函数
2.拷贝构造函数
3.赋值重载函数
4.析构函数
一.前言
继承作为C++的三大特性之一,其重要性不言而喻,只有学好了继承才能为后面的多态打下基础。本文会列出继承的一些重要知识点,带你快速掌握要点。
二.继承机制
这里就不画那个3*3的列表了,直接说怎么理解记忆:
- 父类的所有成员都会被子类继承下来,但是能不能用就不一定了
- 父类的private成员子类不可见
- 父类的其它成员在子类中的访问限定符 = 成员在父类的访问限定符和继承方式中权限小的那一个(public < protected < private)
- 继承方式不写,默认就是私有继承(struct 默认是公有继承)
注:
1.不建议使用private/protected继承,因为继承过来的成员只能在类里面访问
2.私有成员和保护成员的区别是什么?
protected这个访问限定符是专门为继承设计的,父类protected成员在子类中是可以访问的,而private成员虽然子类继承下来了,但是不可见
三.父子类赋值兼容规则
不同类型赋值时,如果是相近类型,可以隐式类型转换,会产生临时对象,但是C++父子类赋值时比较特殊:
在公有继承的基础上(注意必须是公有继承),父类和子类是 "is a" 的关系,子类对象可以赋值给父类对象或父类的引用,父类指针可以指向子类对象。
并且中间不会产生临时对象,这个叫做父子类赋值兼容规则,也叫"切片"
1.如果是对象赋值,则会将子类对象中父类的那部分成员拷贝过去(内置类型值拷贝,自定义类型调用拷贝构造)2.如果是引用或指针,则引用或指向子类对象中父类的那部分成员
Student s;
Person p =s; //对象赋值
Person& rp = s;//引用
Person* pp = &s;//指针
四.继承中的作用域
- 从父类继承下来的成员和自己定义的成员在不同作用域中,因此父类和子类可以有重名成员
- 当使用这个重名的标识符时,访问的是子类的成员,这种现象叫做子类同名成员隐藏了父类同名成员
- 如果想要访问父类成员,可以用使用指定作用域的方式
看一个题目:
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
//b.fun(10); //默认是访问子类的成员
//b.A::fun(); //指定作用域访问父类的成员
};
//以上两个fun函数构成隐藏,不是重载!!!因为两个函数不在同一个作用域
B继承了A的fun,同时实现了自己的fun,请问两个fun构成什么关系?
答:以上两个fun函数构成隐藏,不是重载!!!因为两个函数不在同一个作用域。成员函数满足函数名相同就构成隐藏。
五.子类的默认成员函数
一个擂有六个默认成员函数,但重要的就以下四个:
1.构造函数
- 编译器自动生成的默认构造函数会先调用父类的构造函数以初始化父类的成员,然后再初始化自己的成员
- 如果自己实现构造函数,父类的成员不能单独初始化,只能使用父类的构造函数整体初始化
- 当然,如果你没有调用父类构造函数,编译器也会隐式的调用父类的默认构造函数,自己的自定义类型成员也是同理
Student(int stuid = 111)
:Person()
,_stuid(stuid)//自己的内置类型成员
{}
2.拷贝构造函数
- 编译器自动生成的拷贝构造函数会先调用父类的拷贝构造函数,然后初始化自己的成员(内置类型值拷贝,自定义类型调用拷贝构造)
- 如果自己实现拷贝构造函数,如何显式调用父类的拷贝构造,怎么传参?哪来的父类对象?——直接传子类对象,因为子类对象可以赋给父类引用(切片)
- 当然,如果你没有调用父类拷贝构造函数,编译器也会隐式调用父类拷贝构造函数,自己的自定义类型成员也是同理.但如果你没有处理自己的内置类型成员,编译器是不会帮你值拷贝的
Student(const Student& s)
:Person(s)
,_stuid(s._stuid)
{}
下面谈一下我对构造函数(普通构造和拷贝构造)的理解,这些可能会对你有所帮助:
1)以上的所有动作都是在初始化列表完成的
2)一个类的的成员变量可以分为三类:从父类继承下来的,自己的自定义类型,自己的内置类型。在初始化列表,编译器有一些隐藏的动作:调用父类构造和自定义类型的构造、可能初始化内置类型(当你声明内置类型时给了一个缺省值的情况下就会帮你初始化,因为这个缺省值实际上给了初始化列表)。
3)当你自己做了这些事,编译器就不再帮你做了
4)注意一下默认拷贝构造和自己实现的拷贝构造的差异,对于内置类型,默认拷贝构造会完成值拷贝,而自己实现的编译器就不再管了,交给你自己去把握。
3.赋值重载函数
- 编译器自动生成的赋值重载函数会先调用父类的赋值重载函数,然后处理自己的成员(内置类型值拷贝,自定义类型调用赋值重载)
- 如果自己实现赋值重载函数,调用父类的赋值重载函数会出现问题,为什么?因为自己的赋值重载函数把父类的赋值重载函数隐藏了,编译器认为你调用的是自己的赋值重载,所以形成了死递归
- 所以需要指定类域才能调用父类的赋值重载函数
Student& operator=(const Student& s)
{
Person::operator=(s);
_stuid = s._stuid;
return *this;
}
4.析构函数
先不谈默认析构函数做了哪些工作,我们自己实现一个,先掉掉坑再说:
~Student()
{
~Person();
cout << "Student析构" << endl;
}
想法:因为父类是先创建的,所以先把父类析构了,然后再释放子类自己申请的资源,这里没有申请资源,打印一句话意思一下。OK编译,错辣!!!
由于多态的原因,编译器会将一个类的析构函数名字都处理成“destructor”(后面再讲),所以子类的析构函数隐藏了父类的析构函数,编译器认为你调用的是子类的析构函数,形成死递归。解决方案:指定类域。
//-----------这是父类Person的析构-----------
~Person()
{
cout << "Person的析构" << endl;
}
//---------------------------------------
~Student()
{
Person::~Person();
cout << "Student析构" << endl;
}
int main()
{
Student s;
return 0;
}
再次编译,没问题,运行。嘶~结果有问题
父类的析构函数怎么调用了两次啊!!!
因为编译器在末尾会调用父类的析构函数,你自己显式调用一遍,编译器再调用一遍,所以一共调用了两次。
编译器这样特殊处理的原因是确保先析构子类,再析构父类。
因为子类析构函数中可以访问父类的成员,但若在执行这种语句之前前父类成员就被析构了,就会出问题;反过来父类的析构函数是访问不到子类成员的.,所以先把子类成员析构就没有影响。
编译器这样处理,就是告诉你,父类析构函数你不要显式调用啦,你只管先把子类申请的资源释放就OK,父类析构我帮你调用,这样就保证了析构安全.
//-----------这是父类Person的析构-----------
~Person()
{
cout << "Person的析构" << endl;
}
//---------------------------------------
~Student()
{
cout << "Student析构" << endl;
}
int main()
{
Student s;
return 0;
}
默认析构函数:先调用自定义类型的析构函数,然后调用父类的析构函数
值得注意的是,当你自己实现析构函数时,不允许显式调用自定义类型成员的析构函数,编译器会帮你隐式调用。
下节预告:菱形继承与虚继承。