1、什么是虚函数?什么是纯虚函数?
虚函数时允许被其子类重新定义的成员函数。
函数的声明:virtual returntype func(parameter); 引入虚函数的目的是为了动态绑定;
纯虚函数的声明:virtual returntype func(parameter) = 0; 引入纯虚函数是为了派生接口。
2、什么是多态?多态有什么用途?
C++多态有两种:静态多态、动态多态。
静态多态:静态多态就是重载,在编译时就可以确定函数地址。
动态多态:通过继承重写基类的虚函数实现的多态,运行时在虚函数表中寻找调用函数的地址。
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
如果对象类型是子类,就调用子类的函数;如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。
class Person
{
public :
virtual void BuyTickets()
{
cout<<" 买票"<< endl;
}
protected :
string _name ; // 姓名
};
class Student : public Person
{
public :
virtual void BuyTickets()
{
cout<<" 买票-半价 "<<endl ;
}
protected :
int _num ; //学号
};
void Fun (Person& p)
{
p.BuyTickets ();
}
void Test ()
{
Person p ;
Student s ;
Fun(p);
Fun(s);
}
运行结果:
买票
买票-半价
多态的实现原理
1、存在虚函数的类都有一个一维的虚函数表叫虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
2、类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
3、虚函数表是一个存储类成员函数指针的数据结构。
4、虚函数表是由编译器自动生成与维护的。
5、virtual成员函数会被编译器放入虚函数表中。
6、当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要再base指针中,找到vptr指针即可)。
7、vptr一般作为对象的第一个成员。
探索虚表(https://blog.csdn.net/qq_40840459/article/details/80195158)
虚表是通过一块连续的内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在这样一张虚函数表,它就像一张地图,指向了实际调用的虚函数。
一些考题
为什么调用普通函数比调用虚函数的效率高?
因为普通函数是静态联编的,而调用虚函数是动态联编的。
联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。
静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。
动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。
明显动态联编要比静态联编做的操作多,肯定就费时间。
为什么要用虚函数表(存函数指针的数组)?
实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数。
同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。
为什么要把基类的析构函数定义为虚函数?
在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏.
为什么子类和父类的函数名不一样,还可以构成重写呢?
因为编译器对析构函数的名字做了特殊处理,在内部函数名是一样的。
3、C++类的大小——sizeof(class)
1)、空类的大小
class A {
};
std::cout << sizeof(A) << std::endl;
结果: 1
原因:类的实例化,所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。
2)、一般非空类大小
class A {
int a;
};
std::cout << sizeof(A) << std::endl;
结果: 4
class A{
char a;
int b;
double c;
};
std::cout << sizeof(A) << std::endl;
结果: 8
char 1字节,int的首地址偏移为4的倍数,所以要补3个字节,所以现在1+3+4=8字节,再计算double,现在首地址为8,所以不用补字节,所以总大小为1+3+4+8=16字节,且16字节为8的倍数。
3)、有虚函数的类
class A {
public:
A();
virtual ~A();
private:
int a;
char *p;
};
std::cout << sizeof(A) << std::endl;
结果: 12
4)、有虚函数类的继承
class A { public: A(); virtual ~A(); private: int a; char *p; }; class B : public A { public: B(); ~B(); private: int b; };
运行:cout<<"sizeof(CChild)="<<sizeof(CChild)<<endl;
输出:sizeof(CChild)=16;
可见子类的大小是本身成员变量的大小加上子类的大小。
5)、虚函数
class C { virtual void FunA(); virtual void FunB(); };
当C++ 类中有虚函数的时候,会有一个指向虚函数表的指针。
6)、静态数据成员
class D { int a; static int b; virtual void FunA(); };
得到结果:8
静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员.但是它不影响类的大小,不管这个类实际产生了多少实例,还是派生了多少新的类,静态成员数据在类中永远只有一个实体存在。
而类的非静态数据成员只有被实例化的时候,他们才存在.但是类的静态数据成员一旦被声明,无论类是否被实例化,它都已存在.可以这么说,类的静态数据成员是一种特殊的全局变量.
所以该类的size为:int a型4字节加上虚函数表指针4字节,等于8字节。
7)、普通成员函数
class A
{
void FuncA();
}
结果:1
类的大小与它的构造函数、析构函数和其他成员函数无关,只已它的数据成员有关。
8)、虚拟继承
当存在虚拟继承时,派生类中会有一个指向虚基类表的指针。所以其大小应为普通继承的大小(12字节),再加上虚基类表的指针大小(4个字节),共16字节。
4、指针与引用的区别
1)、指着有自己的一块空间,而引用只是一个别名;
2)、使用 sizeof 看一个指针的大小为4
字节(32位,如果要是64位的话指针为8字节),而引用则是被引用对象的大小;
3)、指针可以被初始化为 NULL
,而引用必须被初始化且必须是一个已有对象的引用;
4)、作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5)、可以有 const
指针,但是引用不能用 const;
6)、指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变;
7)、指针可以是多级,而引用没有分级;
8)、如果返回动态分配内存的对象或者内存,必须使用指针,引用可能引起内存泄漏。
5、堆栈的区别
(1)堆栈空间分配区别
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈; 堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
(2)堆栈的缓存方式区别
栈:是内存中存储值类型的,大小为2M(window,linux下默认为8M,可以更改),超出则会报错,内存溢出;
堆:内存中,存储的是引用数据类型,引用数据类型无法确定大小,堆实际上是一个在内存中使用到内存中零散空间的链表 结构的存储空间,堆的大小由引用类型的大小直接决定,引用类型的大小的变化直接影响到堆的变化。
(3)堆栈数据结构上的区别
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
6、new 和 delete 是如何实现的,与 malloc 和 free有什么异同
1、属性
new delete 是C++关键字,需要编译器支持;malloc free是库函数,需要头文件支持。
2、参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显示地指定所需内存的尺寸。
3、返回类型
new操作符内存分配成功时,返回的对象类型是指针,类型格式与对象匹配,无须进行类型转换,故new时符合类型安全性的操作符。而malloc内存分配成功则返回void*,需要通过类型转换将void*指针转换成我们需要的类型。
4、分配失败
new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL。
5、自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。 malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
6、重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
7、内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
一般的new操作符可以在堆内存分配块区域,而布局(placement)操作符可以使用指定提供的内存空间。需包含头文件<new>可以使用这种特性来设置内存管理规程或处理需要通过特定地址进行访问的硬件。
7、class和struct有什么区别
1)、C语言的struct与C++的class的区别
C中struct只是作为一种复杂数据类型定义,struct中只能定义成员变量,不能定义成员函数。
2)、C++中的struct与class的区别
对于成员访问权限及继承方式,class中默认的是private,而struct则是public。class还可以用于表示模板类型,struct不行。