继承
初识继承
继承的作用是提高程序的复用性,定义一个基础类,其余类可以继承基础类的成员属性。相信子类继承父类的说法大家已经熟悉了。举一个形象的例子:
class people
{
public:
string 姓名;
int 年龄;
void 共同行为()
{
吃饭睡觉;
}
}; //基类,基础类,父类
class kid : public people
{
public:
void 去学校()
{
}
}; //派生类,子类
class dad : public people
{
public:
void 去工作()
{
}
}; //派生类,子类
在子类继承父类所有public属性的同时,也会保留自己的特有属性。除此之外,还有多继承以及继承限定词(还有访问修饰符?)不要着急,且往下看。
继承限定词
public继承:父类成员访问修饰符保持不变;
protected继承:父类public成员访问修饰符降级为protected以下;
private继承:父类所有可被继承的成员访问修饰符降级为private。
所以继承成员可访问性由低到高 public -> protected -> private。
class mom : protected people
{
public:
void 去买菜()
{}
}; //除了自身成员,父类继承来的成员全部降为protected
注意父类private成员不能被继承,不要与以上内容混淆~
构造,析构与继承
先用无参数的类演示一下构造函数执行顺序:
#include <iostream>
using namespace std;
class father //父类
{
public:
father()
{
cout << "father" << endl;
}
};
class son : public father //子类
{
public:
son()
{
cout << "son" << endl;
}
};
class grandson : public son //孙子类
{
public:
grandson()
{
cout << "grandson" << endl;
}
};
int main()
{
grandson c1; //实例化孙子类
return 0;
}
运行结果如下:
可以看到,最基础的基类,其构造函数首先调用,然后依次调用子类的构造函数,以此类推。
对于有参数的构造函数,父类构造函数如果有参数,那么子类需要在构造函数中为父类传递参数。同时,孙子类只需要为子类传递参数(因为父类的构造函数参数已经由子类传递)。代码如下:
#include <iostream>
using namespace std;
class father
{
public:
father(int a)
{
a = 1;
cout << "father" << endl;
}
};
class son : public father
{
public:
son(int b) : father(1) //初始化列表传递参数
{
b = 1;
cout << "son" << endl;
}
};
class grandson : public son
{
public:
grandson() : son(2) //初始化列表传递参数
{
cout << "grandson" << endl;
}
};
int main()
{
son c1(1);
grandson c2;
return 0;
}
另外,继承的构造函数支持重载,具体取决于参数类型及数量。
析构函数
#include <iostream>
using namespace std;
class father
{
public:
father(int a)
{
a = 1;
cout << "father" << endl;
}
~father()
{
cout << "recycle father" << endl;
}
};
class son : public father
{
public:
son(int b) : father(1)
{
b = 1;
cout << "son" << endl;
}
son() : father(1)
{
cout << "son" << endl;
}
~son()
{
cout << "recycle son" << endl;
}
};
class grandson : public son
{
public:
grandson()
{
cout << "grandson" << endl;
}
~grandson()
{
cout << "recycle grandson" << endl;
}
};
int main()
{
grandson c2;
return 0;
}
运行结果如下:
由此可见,继承时调用顺序为父类构造->子类构造->子类析构->父类构造。
覆盖
父类与子类含有同名成员,覆盖是C++的处理方式。
如果父类与子类均含有数据成员a,在子类作用域下,如果没有通过作用域修饰符声明使用父类的a,则默认采用子类的a。对于函数成员,同样也会覆盖父类的函数执行子类的函数。注意:这里的覆盖不是重载,如果参数列表不一致,编译器不会通过重载的方式匹配父类或子类的成员函数。
多态与虚函数
多态是泛型编程思想的核心,顾名思义,用相同的代码实现不同的功能,常见的用法就是用父类的指针调用子类的函数。其中虚函数是这个概念的语法基础。
class father
{
public:
virtual void fun() //虚函数
{}
};
class son : public father
{
public:
void fun() //函数名字一致,才可以达到虚函数的效果
{}
};
int main()
{
father *p = new son;
p->fun(); //此时如果子类中有实现,则调用子类的实现函数
return 0;
}
需要注意的是虚函数同覆盖一样 ,不是重载,参数列表不同,父类的同名虚函数是无法调用子类的函数实现的。如果有多个子类,那么虚函数调用以具体分配的指针类型为准。
这里需要强调,虚函数究其本质是重写,由子类具体实现重写父类的virtual函数,是虚函数特有的性质,注意同覆盖和重载的区别。林外,子类重写的函数默认也是虚函数。
值得一提的虚函数特点:
- 虚函数不能是内联函数;
- 构造函数不能是虚函数(构造函数也是内联函数);
虚表
利用父类指针新创建对象的时候,对象都会对应一块内存空间,我们把它称为虚表。除了数据成员以外,函数成员以指针的形式存在于虚表中。创建子类对象时,检测到虚函数在子类中有重写,则存放子类成员函数的调用地址,如果没有重写,则按照普通函数成员进行存放。
class father
{
public:
int a; //对应成员地址1
void fun1() //对应成员地址2
{}
virtual void fun2() //对应虚表地址->虚函数地址1
{}
};
根据内存对齐,该类的内存分布可能会有空白但绝不会越界。函数指针存放在对应的虚表地址中。其中1和2的内容是固定的,而3处存放的地址可能是father::fun2()的地址,也可能是son::fun2()的地址(假如子类fun2有重写的话)。
由上图可知,虚表存在于对象地址的第一个4字节(32位机),可以根据这个地址找到虚函数表的地址。虚函数表中每个成员都是一个指针(对于32位机就是4字节)。如果子类重写了虚函数,则定向到子类函数的地址,如果没有,则保持基类的成员函数地址。注意在该对象的成员区,非虚函数成员都存放该成员的具体内容,函数存放指针,数据成员按数据类型(int,float等)对齐排布。
虚析构
class father
{
public:
int a;
virtual void fun2()
{}
virtual ~father
{}
};
class son : public father
{
public:
int a;
void fun2()
~son //由于基类的析构定义成了虚函数,所以子类的析构默认为虚函数
{}
};
这样可以避免父类指针创建子类对象,进行内存回收时,不执行子类析构函数导致回收不撤底的情况。具体是将析构函数声明为虚函数时,如果通过父类指针回收子类对象,会由基类一次执行虚构函数直至子类,避免程序因内存错误而崩溃。最好养成一个习惯:一旦要使用继承,基类析构函数一定加上virtual。
纯虚函数
纯虚函数可以没有具体实现,完全由子类继承重写。
class father
{
public:
int a;
virtual void fun() = 0; //纯虚函数的形式
virtual ~father
{}
};
注意,有纯虚函数的类不能实例化对象。通过father创建对象不会编译通过。只能通过子类继承的方式区创建对象,并且最终至少有一个中间类对纯虚函数进行了重写 有纯虚函数的类叫做抽象类,全部由纯虚函数成员构成的类称为接口类。
在类作为基类的时候,需要将析构函数写为虚函数,如果不作为基类使用,则不需要写为虚函数(会占用额外内存开销,但没有问题 )。
虚继承
为了避免继承中产生的二义性,C++的虚继承可以避免多继承中产生的歧义。
class A
{
public:
int a;
}
class B : public A
{}
class C : public A
{}
class D : public B, public C
{}
// B和C继承A,D继承B和C(多继承)
上述代码会产生歧义,导致对成员a的重复复制,因此采用虚继承以避免这个问题,代码如下:
class A
{
public:
int a;
}
class B : virtual public A //此时A类称为虚基类
{}
class C : virtual public A
{}
class D : public B, public C
{}
虚继承不会对成员a进行多次复制,仅仅是给予子类对基类成员的使用权,可以保证D类从father基类的成员只复制一份。极端情况,如果B和C类都声明了相同的成员x,则D类创建成员的时候依然会产生歧义。