1.什么是多态?静态和动态多态的实现原理?
答:多态指的是一种相同的形式表现出不同行为的概念,分为静态多态和动态多态。代码层面,静态多态通过重载(overload)实现,动态多态通过覆盖(override)实现;原理层面,静态多态通过名称修饰(name mangling)实现,动态多态通过虚函数表实现。
对于函数重载,编译器通过函数名和其参数类型识别重载函数。为了保证类型安全的连接(type-safe linkage),编译器用参数个数和参数类型对每一个函数标识符进行专门编码,这个过程有时称为“名字改编”(name mangling)或“名字修饰”(name decoration)。类型安全的连接使得程序能够调用合适的重载函数并保证了参数传递的一致性。编译器能够检测到并报告连接错误。且函数重载需要满足函数名称相同,但函数参数列表不相同的条件。
对于动态多态,首先要保证该函数是虚函数,如果不是虚函数,而子类父类中存在同名的函数,则二者是无关系的。当有虚函数声明时,编译器会创建一个虚函数表,将当前的虚函数按照声明次序放入虚函数表中,而这个虚函数表实际上就是一个函数指针数组,然后将当前这个虚函数表的地址放入对象模型的最起始位置。所以本质上虚函数表是一个函数指针数组。编译器会为包含虚函数的类生成一个成员变量,该成员变量是一个指向虚函数表的指针,也就是说,如果一个类含有虚表,那么类的每个对象都含有虚表指针。当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址(大部分情况是这样),而这四个字节就可以看作是一个指向虚函数表的指针。
对于单继承,派生类会将基类的虚函数拷贝一份并=放在虚函数表的最开头,然后如果有重写,则将虚函数表中被重写的基类的虚函数替换成派生类的虚函数,在基类的虚函数后面按照声明顺序将派生类的其他虚函数依次添加进虚函数表中。
对于多继承,则按照继承顺序将各基类虚函数表拷贝放到类数据结构开头,如果有重写,则类似于单继承相应替换即可。
对于菱形继承,则一个派生类对象中就有可能存在多个基类对象的成员,例如:
class A
{
A();
virtual void aa();
int a;
}
class B : public A
{
B();
virtual void aa() override;
int b;
}
class C : public A
{
C();
virtual void aa() override;
int c;
}
class D : public B, public C
{
D();
virtual void aa() override;
int d;
}
D d;
d.a
此时提示a的语义不明确
故需要使用虚拟继承的方式完善菱形继承(实际应用中尽量还是避免菱形继承)
class A
{
A();
virtual void aa();
int a;
}
class B : virtual public A
{
B();
virtual void aa() override;
int b;
}
class C : virtual public A
{
C();
virtual void aa() override;
int c;
}
class D : public B, public C
{
D();
virtual void aa() override;
int d;
}
D d;
d.a
虚继承的意义就是将基类的被继承变量并未在子类中拷贝一份,即B类和C类虽然继承了A类,但是B类和C类中并没有存储A类的对象(基类对象只有一份,被存放在了整个对象模型的最后),除了子类新增之外,只有一个指针,这个指针就被称为虚基表指针。 如图所示(图片来源CSDN:德拉库斯)
此时根据继承关系,虚基表会根据虚拟继承了多少个基类生成多少个虚基表指针,而虚表只有一份,一个类的所有对象共享这个虚表。
如果一个类既虚拟继承了其他类,同时重写了基类的虚函数,即既存在虚基表又存在虚表,则其存储结构如下图(图源:CSDN:德拉库斯)
2.static的作用
1)修饰全局变量/函数,表明该变量/函数具有文件作用域
2)修饰成员变量/函数,表示该变量/函数不受对象影响,与对象无关
3)修饰局部变量,表示该变量只会初始化一次,且该值会保存到程序结束(不修改时)
3.NULL和nullptr区别
在主流的编译器中,NULL一般是一个整数常量,通常为0,而nullptr是C++11引入的一个专门表示空指针的关键字,类型为nullptr_t。在C或C++11以前的版本中,初始化指针时一般将其赋值为NULL。但是这么做有一个问题,即当有一个函数执行func(NULL)时,编译器无法判断其想调用的是func(int)还是func(int*)。此外,NULL虽然一般赋值为0,0一般情况下也是一个无效地址,但是存在0地址为特殊用途的情况,而nullptr永远指向一个无效地址。
4.__cdecl和__stdcall区别?
均为MSVC++用于声明函数调用约定的关键字,前者为C++默认调用方式,函数参数从右往左压入栈中,由调用方负责清理堆栈;后者同样从右往左压栈,但是由被调用方负责清理堆栈。二者主要被用于跨语言调用,以确保压栈方式和清理方式一致。
5.malloc和new的区别?
malloc只会按照用户要求分配一段内存,并且由用户决定转换成何种类型;new则先分配一段内存(operator new),然后在这段内存中创建对象(placement new)。
6.malloc实现原理
先从用户态切换到内核态,分配一段空闲物理内存,接着在虚拟内存堆空间或者共享内存空间分配一段虚拟内存,然后填充页表,把虚拟内存映射到物理内存,最后返回用户态。
7.free后内存回收去哪了
free后,内存没有立即释放,而是暂时存于内存中,作为内存池的一部分供下次使用。
8.delete[]是怎么知道数组长度的
无标准实现,一般常用实现方式为,在申请内存时,会在返回的指针前面存放该数组的大小,然后在调用delete[]的时候,就可以知道数组的大小了。
9.重载类的delete运算符时,delete的时候会发生什么?
delete会首先调用类的析构函数,然后释放内存。new的作用就是先分配一段内存,然后在这段内存处创建对象。如果重载全局的new 和 delete运算符,则会修改所有的new和delete操作,如果重载的是类的new/delete运算符,则只会修改这个类的new/delete运算符操作。