多态
多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。实现:
a.编译时多态性:通过重载函数实现,它在编译的时候就已经确定了,什么情况调用什么样的函数
b 运行时多态性:通过虚函数实现,它在运行期间动态绑定。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。
多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。在C++程序设计中,多态的实现:
函数重载
运算符重载
虚函数
四种指针情况:
直接用基类指针指向基类对象;
直接用派生类指针指向派生类对象;
派生类对象给基类指针赋值;
基类对象给派生类指针赋值。(编译会出错)
通过指针引起的普通成员函数调用,仅仅与指针的类型有关,而与指针正指向什么对象无关。在这种情况下,必须采用显式的方式调用派生类的函数成员**。
本来使用对象指针是为了表达一种动态的性质,即当指针指向不同对象时执行不同的操作,现在看来并没有起到这种作用。要实现这种功能,就需要引入虚函数的概念。
抽象类(接口)
含有纯虚函数的类
抽象类不能被用于实例化对象,它只能作为接口使用。
C# 和 Java 里面有Interface关键字,C++是通过纯虚函数把实现的
C++通过 结构体+纯虚函数 声明接口
struct Ia {
virtual ~Ia() = default;
virtual void method() = 0;
};
class B : public virtual Ia
{
public:
void method() override;
};
void B::method(){/*...*/}
通过 类+纯虚函数 表示接口
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
虚函数
C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,一般在每一层声明该函数时都加virtual,使程序更加清晰。
虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
虚函数表
#include <iostream>
using namespace std;
class A
{
public:
int i;
virtual void func() {}
virtual void func2() {}
};
class B : public A
{
int j;
void func() {}
};
int main()
{
cout << sizeof(A) << ", " << sizeof(B); //输出 8,12
return 0;
}
在 32 位编译模式下,程序的运行结果是:
8, 12
8是int i 4个byte 和 虚函数表4个byte
12是int i 4个byte、int j 4个byte 和 虚函数表4个byte
如果将程序中的 virtual 关键字去掉,输出结果变为:
4, 8
8是int i 4个byte
12是int i 4个byte、int j 4个byte
对比发现,有了虚函数以后,对象所占用的存储空间比没有虚函数时多了 4 个字节。实际上,任何有虚函数的类及其派生类的对象都包含这多出来的 4 个字节,这 4 个字节就是实现多态的关键——它位于对象存储空间的最前端,其中存放的是虚函数表的地址。
每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针(可以认为这是由编译器自动添加到构造函数中的指令完成的)。
虚函数表是编译器生成的,程序运行时被载入内存。一个类的虚函数表中列出了该类的全部虚函数地址。例如,在上面的程序中,类 A 对象的存储空间以及虚函数表(假定类 A 还有其他虚函数)如图 1 所示。
类 B 对象的存储空间以及虚函数表(假定类 B 还有其他虚函数)如图 2 所示。
多态的函数调用语句被编译成根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的一系列指令。
假设 pa 的类型是 A*,则 pa->func() 这条语句的执行过程如下:
-
取出 pa 指针所指位置的前 4 个字节,即对象所属的类的虚函数表的地址(在 64 位编译模式下,由于指针占 8 个字节,所以要取出 8 个字节)。如果 pa 指向的是类 A 的对象,则这个地址就是类 A 的虚函数表的地址;如果 pa 指向的是类 B 的对象,则这个地址就是类 B 的虚函数表的地址。
-
根据虚函数表的地址找到虚函数表,在其中查找要调用的虚函数的地址。不妨认为虚函数表是以函数名作为索引来查找的,虽然还有更高效的查找方法。
如果 pa 指向的是类 A 的对象,自然就会在类 A 的虚函数表中查出 A::func 的地址;如果 pa 指向的是类 B 的对象,就会在类 B 的虚函数表中查出 B::func 的地址。
类 B 没有自己的 func2 函数,因此在类 B 的虚函数表中保存的是 A::func2 的地址,这样,即便 pa 指向类 B 的对象,pa->func2();这条语句在执行过程中也能在类 B 的虚函数表中找到 A::func2 的地址。
- 根据找到的虚函数的地址调用虚函数。
由以上过程可以看出,只要是通过基类指针或基类引用调用虚函数的语句,就一定是多态的,也一定会执行上面的查表过程,哪怕这个虚函数仅在基类中有,在派生类中没有。
多态机制能够提高程序的开发效率,但是也增加了程序运行时的开销。虚函数表、各个对象中包含的 4 个字节的虚函数表的地址都是空间上的额外开销;而查虚函数表的过程则是时间上的额外开销。
虚析构函数
析构函数的作用是在对象撤销之前把类的对象从内存中撤销。通常系统只会执行基类的析构函数,不执行派生类的析构函数。只需要把基类的析构函数声明为虚函数,即虚析构函数,这样当撤销基类对象的同时也撤销派生类的对象,这个过程是动态关联完成的。
如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
最好把基类的析构函数声明为虚函数,这将使所有派生类的析构函数自动成为虚函数,如果程序中显式delete运算符删除一个对象,而操作对象用了指向派生类对象的基类指针,系统会调用相应类的析构函数。
如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。
虚析构函数的声明语法如下:
virtual~类名
为什么析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
https://www.cnblogs.com/yuanch2019/p/11625460.html
继承
派生类是对基类的扩充
共有三种继承方式:(三种方式对应的最终值)
公有继承(public),
私有继承(private,系统的默认值),
保护继承(protected)
派生类继承了基类的全部数据成员和除了构造、析构函数之外的全部函数成员
派生类构造函数的任务应该包括3个部分:
① 对基类数据成员初始化;
② 对子对象数据成员初始化;
③ 对派生类数据成员初始化。
虚函数
声明函数时,在函数前面加virtual
实现的功能:
虚函数的作用是实现动态联编,也就是在函数运行阶段动态的选择合适的成员函数;
在派生类中重写虚函数的时候,要保持重写的函数与原函数的一致性(包括返回值类型、参数个数与类型);
根据什么考虑是否把一个成员函数声明为虚函数?
-
看成员函数所在的类是否会作为基类
-
看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到作为基类而把类中的所有成员函数都声明为虚函数。
应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。有时在定义虚函数时,并不定义其函数体,即纯虚函数。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
说明:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(vtbl),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联的时间开销很少,提高了多态性的效率。
虚继承
菱形的继承关系(虚继承)也是存在的
多继承中的二义性问题()
则需要将这个基类设置为虚基类(即虚继承)。
class A
{
public:
void f();
private:
int a;
};
class B: virtual public A
{
protected:
int b;
};
class C: virtual public A
{
protected:
int c;
};
class D: public B, public C
{
public:
int g();
private:
int d;
纯虚函数
它在该基类中没有定义具体的操作内容。这里就应将该接口说明成一个纯虚函数,其具体操作由各子孙类来定义,带有纯虚函数的类称为抽象类。(java中的接口)
class 类名
{
virtual 类型 函数名(参数表)=0;
//纯虚函数
...
}
模板
模板(Template)是 C++ 语言代码重用和多态性的一个集中表现。模板是提供这样一个转换机制:由程序员定义一种操作或一个类,而该操作或类却可以适应几乎所有的数据类型。在一定意义上,模板类似宏定义或函数重载,但它书定更为简洁,使用更加灵活,适应性更强。
模板分函数模板和类模板。前者为程序员编写通用函数提供了 一种手段;而后者则为程序员设计通用类奠定了基础。
友元关系
友元函数,友元类。
- 友元关系是不能传递的。若声明A类为B类的友元类,则A类的所有的成员函数都可以成为B类的友元函数。
- 友元关系是单向的(不是对称的)
友元的作用主要是为了提高程序的运行效率和方便编程。但随着硬件性能的提高,友元的作用也不明显,相反,友元破坏了类的封装性,所以在使用时,应权衡利弊。
#include <iostream>
using namespace std;
class Box
{
double width;
public:
friend void printWidth( Box box );
void setWidth( double wid );
};
// 成员函数定义
void Box::setWidth( double wid )
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width <<endl;
}
// 程序的主函数
int main( )
{
Box box;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth( box );
return 0;
}
static 所有成员共享
静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。
初始化赋值
不能用构造函数初始化赋值;
不能放在主函数中初始化;
在程序定义该类的任何对象之前,对类中的static数据成员单独初始化;
运算符重载
目的
C++代码更直观,易读,使用更方便。
运算符重载的实质
运算符重载的实质是函数重载。 只不过它重载的是类似“+ - * / =“这样的操作符。
运算符重载函数一般采用下述两种形式之一。
成员函数的形式;
友元函数的形式。
运算符重载函数可以定义为友元函数的形式,格式如下:
返回值类型 operator op (参数表)
{
相对于该类而定义的操作(运算符重载函数体)
}
单目运算符重载,参数表中只有一个形参数;
双目运算符重载,参数表中有两个形参数。
运算符重载为成员函数和友元函数形式的主要区别在于前者有this 指针,后者无this 指针。
explicit构造函数
explicit的主要用法就是放在单参数的构造函数中,防止隐式转换, 导致函数的入口参数, 出现歧义.
explicit 只对构造函数起作用,用来抑制隐式转换。
下面两种写法仍然正确:
String s2 ( 10 ); //OK 分配10个字节的空字符串
String s3 = String ( 10 ); //OK 分配10个字节的空字符串
下面两种写法就不允许了:
String s4 = 10; //编译不通过,不允许隐式的转换
String s5 = ‘a’; //编译不通过,不允许隐式的转换
代理类
内联函数
为提高程序运行效率而引入的。所有函数调用时都会产生一些额外的开销,主要是系统栈的保护、代码的传递、系统栈的恢复以及参数传递等。
把函数代码直接放在函数调用处,取代函数调用,从而提高程序的执行速度
有些小而常用的函数可以使用内联函数。