注:面试过程中整理的学习资料,如有侵权联系我即刻删除。
目录
怎么得到数组的长度?
怎么禁用类中的拷贝构造函数?
介绍一下标准库vector是怎么进行内存管理的?resize和reserve分别有什么作用?
指针操作有什么好处?
交换两个数的值有几种写法
说一下引用和指针的区别。常引用有什么作用呢?将引用作为函数参数有什么好处?拷贝构造函数的参数为什么要用引用呢?
哪些地方会调用拷贝构造函数?
什么时候会重载运算符
虚函数是如何实现的?
C++中为什么虚函数比非虚函数耗费的时间更多?
什么是静态绑定?什么是动态绑定?
为什么我们有些类会定义虚析构函数?如果不定义成虚析构函数会怎么样呢?
为什么构造函数不可以是虚函数?
构造函数中是否可以调用虚函数
C++哪些函数不能声明为虚函数
为什么有了虚析构函数,delete父类的指针(指向子类的对象)就能先调用子类的析构函数?
为什么析构的时候是先调用派生类析构,再调用基类析构?
一个类包含一个普通成员函数、一个虚函数、一个纯虚函数,那么这个类的sizeof()有多大?为什么是4字节?虚表?
虚函数指针的作用?
虚表指针存在对象的什么地方?
虚函数表存放在哪里?
上行转换和下行转换
static_cast 和 dynamic_cast 的区别(RTTI)
基类指针指向子类对象,访问子类中的虚方法,称为多态。那么子类指针指向基类对象呢?
C++对象的内存布局
sizeof(array)/sizeof(array[0]);但是这种计算不适用于数组作为函数参数的时候,当数组名作为函数参数,会退化成指针。解决方法就是将数组引用的数组大小也声明为一个函数参数。动态数组用.size()就可以得到长度。
将拷贝构造函数和重载赋值运算符设为private来禁止拷贝。
(1)vector是一种序列式容器(其中的元素可以排序,但是并未排序)。它和array一样,存储空间是一段连续的内存,因此支持随机访问,但是,和array相比,vector支持动态增加删除数据。
(2)特征量有:size是vector当前所包含的元素个数;capacity是当前可以使用的容量,也就是预分配存储空间的大小,capacity大于等于size;
reserve和resize的区别是:reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素;resize的作用是改变vector中元素的数目且创建对象,调用完resize可以直接引用vector的元素了。如果n比当前的vector元素数目要小,vector的容量要缩减到n,并移除那些超出n的元素同时销毁他们(此情况resize一个参数n)。如果n比当前vector元素数目要大,在vector的末尾扩展需要的元素数目,如果第二个参数val指定了,扩展的新元素初始化为val的副本,否则按类型默认初始化。resize完了之后size就等于n了。注意: 如果n大于当前的vector的容量(capacity),将会引起自动内存分配。
指针可以动态分配内存,很灵活。比如估计不到我们需要多少个变量,这个时候就需要指针。
数据结构离不开指针,链表、树、图都是离不开指针的。
指针传递,直接交换ab所在地址中的值的,但并不改变指针的指向。
二级指针传递,通过交换指针指向的地址来交换两个值。
反正平级传递,是交换不了ab的值的。
(1)指针是实体,需要分配内存空间,引用只是变量的别名,不需要分配内存空间;引用在定义的时候必须进行初始化,并且不能够改变,指针定义的时候不一定初始化,指向的空间可变;指针和引用的自增运算结果不同,指针++是指向下一个空间,引用自增是指引用的变量值加一;sizeof引用得到的是对应变量的大小,sizeof指针就是指针本身的大小。
(2)常引用是 const int &y = x; 常引用是让变量引用具有只读字段,不能通过y去修改x了。常引用的初始化有两种,一种是用变量来初始化常引用,一种是用字面值来初始化常引用。
int x1 = 30;
const int &y1 = x1;//变量初始化常引用
const int &m = 43;//字面值初始化常引用
如果用int &m = 43;会报错,引用是给内存取别名,字面值没有内存,没有内存无法取别名。而字面值初始化常引用,编译器会给字面值分配内存空间。
(3)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,指针传递本质上也是值传递,传递的是一个地址。在被调函数中同样要给形参分配存储单元,且需要重复使用\"*指针变量名\"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
(4)因为,如果假设拷贝构造函数中的参数不是引用类型的话,我们将对象传进去,又会继续触发拷贝构造函数,陷入无限循环,所以只能用引用类型。而用const引用是为了防止传入的对象在拷贝构造函数中被修改,是为了告诉程序员这个变量是只读的。
一个对象去初始化另一个对象;函数以对象作为参数传递;函数(类型)返回某个对象;
· CExample aaa(2); //调用带参构造函数
· CExample bbb(3);
· bbb = aaa; //调用赋值运算符重载函数而不是拷贝构造函数
· CExample ccc = aaa; //调用拷贝构造函数
· bbb.myTestFunc(aaa); //调用拷贝构造函数
· vector
一般是在,需要对非基本类型(比如自定义类)对象,做加减乘除等运算的时候,需要针对这个类做运算符重载。
在类的成员函数前面加个virtual关键字。当类中存在虚函数时,则编译器会在编译期自动的给该类生成一个虚函数表,并在所有该类的对象中生成一个指针变量,指向虚函数表。调用的时候就是通过这个指针变量在虚函数表中找到对应虚函数地址,来完成调用。
虚函数在调用的时候需要去查找虚表,根据指向对象的不同来确定调用的是哪一个函数,而普通函数直接通过函数名就得到了入口地址。所以虚函数耗时更多。
绑定:把一个方法与其所在的类/对象关联起来。
静态绑定:在程序编译的时候就已经知道了方法是属于哪一个类的,编译时就能定位到这个方法。执行速度快。
动态绑定:是在程序运行过程中根据具体的实例对象,才能知道调用的具体是哪一个函数,主要是通过虚函数来实现的。通过指针或者引用才能实现。
动态绑定的缺点:a.动态绑定在函数调用时需要在虚函数表中查找,所以性能比静态绑定低一些。
b.通过基类类型的指针访问派生类自己的虚函数,将发生错误。
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法。
虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类资源的目的,而防止内存泄露。
如果基类析构函数是虚函数的话,基类指针指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,派生类对象没有释放,这样会造成内存泄露。所以,将析构函数声明为虚函数是十分必要的。
因为虚函数是存放在虚表中的,虚表指针又是对象实例化之后才建立的,虚表指针存放在对象的前四个字节中,而构造函数中还没有实例化对象,就没有办法通过虚表指针找到虚函数。所以构造函数不能是虚函数。
而且,构造对象的时候必须知道对象的实际类型,而虚函数是运行的时候才确定实际类型的。
虚函数表是在编译期(compile time)就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组.而对象的隐藏成员--虚函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键。
可以,但是调用的是自己类中的虚函数,而不能调用子类中的虚函数。因为在基类构造函数中,子类对象还未被完全创建,也就不能通过子类的虚表指针来调用子类的虚函数。
两类函数不能被声明为虚函数:
一是不能被继承的函数。
二是不能被重写的函数。
有:普通函数(不属于成员函数)、友元函数(不属于成员函数)、构造函数、内联函数(编译时展开的,虚函数是运行时实现多态的,两个特性相违背)、静态成员函数(编译时确定的,不支持多态)。
delete父类的指针p,程序会去找父类的指针p指向的地址,这个时候该地址就是子类头部虚函数表指针的地址,由指针p找到子类的虚函数表,从而找到子类的虚析构函数。由于子类是从父类继承来的,所以调用完子类的析构函数还会自动调用父类的析构函数。
派生类本身就包含两部分,一部分继承的基类,一部分自己的成员,所以析构派生类的时候编译器也会自动调用父类的析构函数。(不管是不是虚析构,都会自动调用)
程序的运行是将每句指令调入内存中,以压栈的方式存入栈中,对于先声明的成员先进栈必定后析构,即先入后出,比如定义了两个变量human man; student stu; 那么系统先析构stu后析构man。同理对于派生类的对象,先调用了基类的构造后调用了继承类的构造,所以在析构的时候先“弹出”继承类的析构,后“弹出”基类的析构。
一个类中有虚函数,就会有虚函数指针,这个指针指向一个虚函数表,虚函数表中存储的是虚函数的地址。多个虚函数也是只有一个虚表,所以也只有四个字节的大小。程序运行的时候就是在虚函数表中进行查找真正要执行的虚函数的地址。每个类都会维护一张虚表,编译时,编译器根据类的声明创建出虚表,当对象被构造时,虚表的地址就会被写入这个对象内存。
注意:在基类有虚函数的前提下,虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
每一个类的对象都有一个指向虚表的虚指针。虚表是和类对应的,虚表指针是和对象对应的。这个指针指向了对象所属类的虚表。每个对象调用的虚函数都是由虚指针来索引的,在程序运行时,根据对象的类型去初始化虚指针,从而让虚指针正确的指向所属类的虚表。在调用虚函数时,才能够找到正确的函数。
对象的前四个字节设置为虚表指针的地址。
虚函数表是全局共有的,因此是在全局区。
上行转换:子类指针和引用转换为基类指针和引用,可隐式转换。
下行转换:基类指针转成子类指针,只能强制转换。
这两个都是做类型转换的,发生的时间不同,static_cast是编译时,dynamic_cast是运行时。
必须强制转换,这个指针如果调用的是子类继承来的虚函数,那就是调用基类中的函数。如果调用的是子类的普通函数,那就是调用子类的函数。
影响对象大小的因素有:成员变量、虚函数、单一继承、多重继承、虚拟继承、还有字节对齐。
单继承对象的内存布局:
虚表在最前面(被重写的虚函数要在虚表中得到更新)。
成员变量根据继承和声明顺序依次放在后面。
多重继承对象的内存布局:
每个父类都有自己的虚表(子类的虚表放在第一个父类虚表后面)。
父类布局再按照声明顺序来排列。
虚继承对象的内存布局:
多了一个虚基类表来记录虚继承关系,有一个虚基类表指针。