同名隐藏
派生类中与基类同名的成员会将基类的所有同名成员隐藏。
可以通过Base::i的方式访问被隐藏的基类成员。
同名成员函数会被全部隐藏。
例如:基类中重载了多个名为func的函数,派生类中有一个func()函数,对于派生类,它只知道一个名为func的无参函数,所有基类的其他有参重载版本都得通过Base::func(i)的方式调用。
代码如下
class Base
{
public:
void func();
void func(char c);
void func(int i);
void func(string s);
};
class Derived :public Base
{
public:
void func();
};
int main()
{
Derived d;
d.func(1); //报错“参数太多”
d.Base::func(1);
cin.get();
return 0;
}
派生类在调用构造函数前先会调用基类的构造函数。派生类对象可以当做基类对象处理,反之则不行。
以函数为例:
void func1(Base& base);
void func2(Derived& derived);
Base base; Derived derived
base和derived都可传入func1(Base),但base不能传入func2(Derived)
实例化一个派生类对象时是否会同时实例化一个基类对象?
(来自https://bbs.youkuaiyun.com/topics/80127442)
“派生类只是在基类的基础上增加了一些东西,好比在一列火车上再加一个车箱,因此生成一个派生类并未生成一个独立的基类对象,但除了被添加的一些东西之外,把剩余的东西仍做基类来处理也是可以的。如果伴随派生类对象的创建而产生了一个独立的基类对象,势必造成资源浪费。”
继承方式
派生类可以直接访问基类的public和protected权限成员。
继承方式:public、protected、private
public:不会修改基类的成员访问权限;
protected:将所有public访问权限修改为protected;
private:将所有public、protected访问权限修改为private。
(总之继承方式会缩小权限范围)
protect成员:通过派生类访问相当于public,通过实例化后的对象访问相当于private。
派生类的构造函数和析构函数
如果基类没有默认构造函数,派生类得在所有构造函数中显式调用基类的任一构造函数。
构造函数的调用顺序:
1、基类(如果派生类多继承,按声明顺序(从左到右)调用基类构造函数);
2、成员对象(按声明顺序);
3、派生类的构造函数。
析构函数的调用顺序相反。
多继承导致的二义性问题
派生类继承的多个基类中拥有同名成员,或派生类的多个基类派生自同一个基类。
解决方法
1、在派生类中重写成员,将产生二义性的基类成员隐藏。
2、使用“::”明确指出是使用哪一个基类的成员。注意:对于图中右侧的情况,不能使用“obj.Base::data”,只能用“obj.Derived11::data”或“obj.Derived12::data”。
由于二义性原因,一个基类不能被一个派生类多次继承。
二义性检查在访问控制权限或类型检查之前进行,所以不能通过修改访问控制权限或修改成员变量类型或函数重载解决二义性问题。
虚基类(二义性的另一种解决方法)
以虚拟的方式继承基类class Derived :virtual public Base。之后可将Base称为虚基类(Base类本身没有任何改变)。
以虚方式继承一个基类的派生类组成一个集合,该集合中的多个类被一个类继承时,它们的虚基类只构造一次。(相当于集合中的类都做了一个声明:“我愿意与集合中的其他类共享虚基类。”)
虚基类调用的构造函数 、 最派生类
最派生类:使用虚基类继承的层次可能很深,实例化的对象可能是层次中的任何一个类。将实例化对象时所指定的类称为最派生类。
例如有四个类:
Base
Derived1:virtual public Base
Derived2:virtual public Base
Derived :public Derived1, public Derived2
当Derived d;时,对于对象d,Derived是最派生类。同理Derived1 d1时,对于对象d1,Derived1就是最派生类。
曾经误解过的知识点
“最派生类的构造函数初始化列表中必须列出对虚基类构造函数的调用,否则虚基类会调用默认构造函数”
例子说明:在上一例的继承关系中,四个类中的一个构造函数如下所示(只给出构造函数):
Base(int seta)
:a(seta)
{}
=============================================================
Derived1()
:i(1),Base(11111)
{
cout << "Default Con Derived1. and set Base.a as: " << 11111 << endl;
}
=============================================================
Derived2()
:i(1),Base(22222)
{
cout << "Default Con Derived2. and set Base.a as: " << 22222 << endl;
}
=============================================================
Derived()
{
cout << "Default Con Derived." << endl;
}
实例化一个Derived对象Derived d;
使用普通方式继承
Derived会调用Derived1和Derived2的无参构造函数,而Derived1和Derived2会根据自己无参构造函数的设置分别以参数“11111”和参数“22222”各调用Base类的Base(int)构造函数1次。
使用虚基类
因为此时的最派生类Derived没有显式调用Base类的构造函数,所以Base类会调用默认构造函数。
补充:实例化Derived对象时,仍会调用Derived1类和Derived2类的构造函数,而具体调用Base类的哪个构造函数,由当前的最派生类Derived决定,且只会调用一次。
如果改为实例化一个Derived1对象Derived1 d1;,则会以参数“11111”调用Base类的Base(int)构造函数。
基于以上特点,建议在所有虚基类的派生类中都显式调用虚基类的构造函数。
虚函数与多态(polymorphism)性
引入多态的意义:实现一组具有相同基本语义方法能在同一接口下为不同的对象服务。
C++多态性可分为两种:编译时的多态(静态多态)、运行时的多态(动态多态)。
静态多态:函数重载和模板。在编译阶段确定函数地址。
动态多态:虚函数。运行阶段确定函数地址。
虚函数
在非静态成员函数声明前加上“virtual”修饰符,把该函数声明为虚函数。
虚函数自身也能被调用,当基类的指针(或引用)指向了派生类对象时,会调用派生类的、重写了基类虚函数的成员函数。
派生类可以重写(overwrite)从基类继承的虚函数,重写时返回类型、函数名、参数列表都必须与原函数保持一致。
函数重载:除了函数名外,其他部分都可修改(参数列表必须修改)。
函数重写:仅函数体{}内的代码可修改,其他部分都不变。
派生类重写后的虚函数仍是虚函数,可不必加上virtual修饰(最好还是加上)。
补充:派生类重写虚函数时,可以加上“override”修饰符(在const修饰函数的位置)。
加上override的好处:
1、提醒阅读代码的人该函数是对虚函数的重写;
2、能起到排错的作用,如果在重写虚函数时粗心将返回类型、函数名、形参表中的任一项写错,或基类忘记将函数声明为虚函数,编译器都会直接报语法错误。
总之,只有当基类中声明了一模一样的虚函数时,派生类中被override修饰的函数声明才不会报错。
函数的调用
1、非多态调用:
不借助指针或引用的直接调用。通过成员访问运算符“.”进行。
2、多态调用
借助指针或引用的调用。一个基类的指针(或引用)可以指向它的派生类对象,此时通过指针(或引用)调用虚函数时,调用的是该指针(或引用)所指对象所在类的重写版本。
Base* pb; Derived d;
pb = &d;
pb->show();//调用的是Derived类的show()函数
派生类中可以重载基类函数,重载后的函数都是实函数,实函数都是非多态的。
构造函数不能声明为虚函数
多态原理
class Animal
{
public:
void speak(); //只声明会报编译错误
};
class Animal
{
public:
virtual void speak()
{} //“无法解析的外部命令”
};
前者sizeof() 得1,后者 sizeof() 得4。
类中出现一个虚函数后,就会生成一个指向该类的虚函数表的指针,表中记录了该类中所有虚函数的地址(如 &Animal::speak)
若派生类的基类中有虚函数,派生类也有一个指向自己的虚函数表的指针。分两种情况:
1、 派生类未重写虚函数
派生类虚函数表记录的是基类的虚函数地址;
2、 派生类重写了虚函数
虚函数表中记录派生类的虚函数地址。
基类虚函数必须实现的原因:
因为声明了虚函数后就会有一个虚函数表,如果只声明,不给出虚函数的定义,则无法找到虚函数的地址。
补充:未实现虚函数的基类在实例化任何一个对象之前,不会报错(“1个无法解析的外部命令”)。
虚析构函数
通常需要对析构函数进行专门定义时,也要将析构函数声明为虚析构函数。
纯虚函数与抽象类
意义:基类无法确定一个虚函数的具体操作方式,只能依靠派生类来提供各个具体的实现版本。virtual void func() = 0;
纯虚函数无法定义
拥有纯虚函数的类成为抽象类。抽象类无法实例化。
继承抽象类的派生类如果不重写所有纯虚函数,则派生类也是抽象类。
析构函数也可声明为纯虚析构函数,但作为纯虚函数,纯虚析构函数必须拥有定义。
析构函数声明为纯虚函数的意义
(来自https://stackoverflow.com/questions/1219607/why-do-we-need-a-pure-virtual-destructor-in-c)
提问者:
使用纯虚析构函数是为了将该类声明为抽象类。但将任何成员函数声明为纯虚函数也能做到这一点。
问题:
1、 什么时候应该将析构函数声明为纯虚函数?
2、 在抽象类中声明析构函数为纯虚函数有什么好处?
赞同数最高回答:
对于两个问题:
1、大概是因为:如果要禁止析构函数为纯虚函数,则需要对C++多做一条规则限制,但纯虚析构函数并没有什么负面效果,所以就没必要。
2、创建一个抽象类使用虚析构函数或纯虚析构函数都一样。
想创建一个抽象类且不想强迫其他人重写任何特定虚函数时,可以将析构函数声明为纯虚函数。即使抽象类的派生类没有实现析构函数,编译器也会自动生成默认析构函数,所以仅将基类的析构函数声明为纯虚函数,只会将基类变成抽象类,不会对派生类产生任何影响。
另一种推测:可能所有派生类都需要有自己的清理代码,基类使用纯虚析构函数能起到提醒作用,但这方式太做作(contrived)且非强制。
(Visual Studio测试:基类使用了纯虚析构函数,派生类不重写析构函数也能实例化)
赞同数第二的回答:
抽象类中至少要有一个纯虚函数,而析构函数是所有类中都有的成员函数,所以它是一个很好的候选对象;进一步,纯虚析构函数没有副作用,因此,很多“style guides”推荐将抽象类的析构函数设为纯虚析构函数,方便其他程序员阅读代码:只要查看析构函数就能判断该类是否是抽象类。
与本文无关的一些补充
Base& b = *new Derived();
delete& b;
第一行:*new Derived()对new返回的指针解引用,得到的是一个Derived类对象;b相当于一个Base对象;
第二行:常用的方法是delete p;(p为指针类型,本质上是代表地址的数字)。取地址的&b也相当于p(指针),所以该写法可行。
犯过的错:cout << k-3 ;//输出k-3后的值,并不改变k本身