作者:杀风之痕 链接:https://www.nowcoder.com/discuss/578910 来源:牛客网
-
说一下C 和C的区别
C 它是面向对象的语言,C是面向过程的结构化编程语言
语法上:
C 重载、继承和多态三个特点
C 相比C,增加许多类型的安全功能,如强制类型转换,
C 支持模板类、函数模板等范式编程
-
引用和指针的区别?
-
指针是需要分配内存空间的实体。引用只是变量的别名,不需要分配内存空间。
-
引用必须在定义时初始化,不能改变。指针在定义时不必初始化,指向的空间可以改变。(注:没有引用值NULL)
-
有多级指针,但没有多级引用,只能有一级引用。
-
指针和引用的自增操作结果不同。(指针指向下一个空间,引用时引用的变量值加1)
-
sizeof 引用所指向的变量(对象)的大小sizeof 指针是指针本身的大小。
-
引用访问是直接访问,而指针访问是间接访问。
-
使用指针前最好做类型检查,防止野生指针的出现;
-
通过指针实现引用底层;
-
作为参数,传指的本质是传值,传值是指针的地址;传引用的本质是传输地址和变量地址。
-
从汇编层解释引用
-
9: int x = 1;
-
00401048 mov dword ptr [ebp-4],1
-
10: int &b = x;
-
0040104F lea eax,[ebp-4]
-
00401052 mov dword ptr [ebp-8],eax
x的地址为ebp-4,b的地址为ebp-8.由于栈内的变量内存由高到低分布。所以b的地址低于x。lea eax,[ebp-4] 这句话将x的地址ebp-4放入eax寄存器mov dword ptr [ebp-8],eax 这条语句将eax将值放入B的地址ebp-8中以上两个汇编的功能是将x的地址存储在变量b中,这和将某个变量的地址存储在指针变量中不一样吗?因此,从汇编层面来看,引用确实是通过指针实现的。
- C 指针参数传输和引用参数传输
-
指针参数传递本质上是值传递,它所传递的是一个地址值。在值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,将在栈中开辟内存空间,存储主调函数传递的实际参数值,形成实际参数的副本(替身)。值传输的特点是,形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量值(形参指针变化,实参指针不变)。
-
在引用参数传输过程中,被调整函数的形式参数也为栈中的局部变量开辟了内存空间,但存储了主调整函数放置的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接地址搜索,即通过堆栈中存储的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
-
引用传输不同于指针传输。虽然它们都是被调整函数栈空间中的局部变量,但引用参数的任何处理都将通过间接搜索到主调整函数中的相关变量。对于指针传输的参数,如果被调函数中的指针地址发生变化,则无法应用主调函数的相关变量。主调函数中的相关变量(地址)要调函数中的相关变量(地址),则必须引用指针或指针。
-
从编译的角度来看,程序在编译过程中将指针和引用添加到符号表中,并记录变量名和变量对应的地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成后不会改变,因此指针可以改变指向对象(指针变量中的值可以改变),而引用对象不能修改。
-
形参与实参的区别?
-
在调用结束时,形参变量只分配内存单元, 立即释放分配的内存单元。因此,形参仅在函数内有效。 该形参变量不能用于函数调用后返回主调函数。
-
实参可以是常量、变量、表达式、函数等, 无论实参是什么样的量,在调用函数时,它们都必须有一定的值, 将这些值传递给形参。 因此,实际参数应提前通过赋值、输入等方式获得确定值,这将产生临时变量。
-
实参和形参的数量、类型、顺序应严格一致, 否则会出现类型不匹配的错误。
-
函数调用中发生的数据传输是单向的。 也就是说,实参的值只能传递给形参,而不能反向传递给实参。 因此,在函数调用过程中,形参值发生变化,实参值不变。
-
当形参和实参不是指针类型时,形参和实参时,形参和实参是不同的变量。它们在内存中处于不同的位置。形参复制了实参的内容当函数运行结束时,形参被释放,实参的内容不会改变。
-
值传递:如果值传递的对象是类对象,则存在形参向函数所属的栈复制数据的过程 或者大的结构对象会消耗一定的时间和空间。(传值)
-
指针传输:还有一个形参向函数所属的栈复制数据的过程,但复制数据是一个固定为4字节的地址。(传输值,传输地址值)
-
引用传输:也有上述数据复制过程,但对于地址,相当于数据所在地址的别名。(地址传输)
-
在效率方面,指针传输和引用传输的比例传输效率较高。一般提倡使用引用传输,代码逻辑更加紧凑和清晰。
-
static用法和作用?
-
全局静态变量
在全局变量前添加关键字static,全局变量定义为全局静态变量.
在整个程序运行过程中,静态存储区一直存在。
初始化:未初始化的全球静态变量将自动初始化为0(自动对象的值是任意的,除非它是显式初始化;
功能域:除了声明他的文件之外,整体静态变量是看不见的,从定义到文件结尾。
- 局部静态变量
在局部变量前添加关键字static,局部变量成为局部静态变量。
内存位置:静态存储区
初始化:未初始化的全球静态变量将自动初始化为0(自动对象的值是任意的,除非它是显式初始化;
功能域:功能域仍然是局部功能域。当定义函数或句子块结束时,功能域结束。然而,当局部静态变量离开功能域时,它并没有被摧毁,而是仍然停留在内存中,但我们不能再访问它,直到函数再次被调用,持不变;
- 静态函数
添加函数返回类型static,函数被定义为静态函数。默认情况下,函数的定义和声明是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
使用函数static修改后,此函数只能在本cpp内部使用,与其他不同cpp同名函数引起冲突;
warning:不要在头文件中声明static的全局函数,不要在cpp内声明非static如果你想要多个全局函数cpp中复用函数,将其声明提到头文件,否则cpp需要添加内部声明static修饰;
- 静态成员
在类别中,静态成员可以实现多个对象之间的数据共享,使用静态数据成员不会破坏隐藏的原则,即确保安全。因此,静态成员是所有类别的对象***享受的成员,而不是某个对象的成员。对于多个对象,静态数据成员只存储一个共享的地方
- 类的静态函数
静态成员函数和静态数据成员一样,属于静态成员,不是对象成员。因此,不需要使用对象名来引用静态成员。
在实现静态成员函数时,不能直接引用类中解释的非静态成员,也不能引用类中解释的静态成员(这非常重要)。如果非静态成员应在静态成员函数中引用,则可以通过对象引用。可以看出,调用静态成员函数使用以下格式:<类名>::<静态成员函数名>(<参数表>);
- static成员函数不能被接受virtual修饰,static成员不属于任何对象或实例,因此添加virtual没有实际意义;静态成员函数没有this虚拟函数的实现是为每个对象分配一个指针vptr指针,而vptr是通过this指针调用,不能用于virtual;调用虚函数的关系,tis->vptr->ctable->virtual function
-
静态变量什么时候初始化
-
初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。
-
静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。
-
而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。
- const?
-
阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
-
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
-
在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
-
对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;
-
对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
-
const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;
-
非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
-
一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。
-
const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;
-
const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
-
对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
- const成员函数的理解和应用?
① const Stock & Stock::topval (②const Stock & s) ③const
①处const:确保返回的Stock对象在以后的使用中不能被修改
②处const:确保此方法不修改传递的参数 S
③处const:保证此方法不修改调用它的对象,const对象只能调用const成员函数,不能调用非const函数 10. 指针和const的用法
-
当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。
-
int const p2中const修饰p2的值,所以理解为p2的值不可以改变,即p2只能指向固定的一个变量地址,但可以通过p2读写这个变量的值。顶层指针表示指针本身是一个常量
-
int const p1或者const int p1两种情况中const修饰p1,所以理解为p1的值不可以改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p赋值不同的地址改变这个指针指向。底层指针表示指针所指向的变量是一个常量。
-
int const *const p;
- mutable
-
如果需要在const成员方法中修改一个成员变量的值,那么需要将这个成员变量修饰为mutable。即用mutable修饰的成员变量不受const成员方法的限制;
-
可以认为mutable的变量是类的辅助状态,但是只是起到类的一些方面表述的功能,修改他的内容我们可以认为对象的状态本身并没有改变的。实际上由于const_cast的存在,这个概念很多时候用处不是很到了。
- extern用法?
- extern修饰变量的声明
如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
- extern修饰函数的声明
如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。
- extern修饰符可用于指示C或者C++函数的调用规范。
比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。
- 为什么C语言不支持重载,C++支持重载呢
比如一个函数声明如下:
void function(float x,float y);
在C语言中,编译器进行编译之后,在库中的名字为:_function
在C++中,编译器在进行编译之后,在库中的名字为:_function _int_int
再来看另一个函数的声明:
void function(float x,float y);
在C语言中,编译器进行编译之后,在库中的名字为:_function
在C++中,编译器在进行编译之后,在库中的名字为:_function_float_float
编译器在链接的阶段,都是找到相应的函数名,进行链接。
在C语言中,两个函数的名字一样,就会在链接时报错。
在C++中,两个函数饿名字不相同,就不会报错。
· C语言,在符号表中的函数标识是函数本身,就会存在两个同名函数。
· C++,不是用原生的函数名,是函数名+参数(c++有函数名修饰规则,函数名+类型一起决定)
- int转字符串字符串转int?strcat,strcpy,strncpy,memset,memcpy的内部实现?
c++11标准增加了全局函数std::to_string
可以使用std::stoi/stol/stoll等等函数
strcpy拥有返回值,有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达, 15. 深拷贝与浅拷贝?
- 浅复制 —-只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
深复制 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。
- 在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
- C++模板是什么,底层怎么实现的?
-
编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
-
这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。
- C语言struct和C++struct区别
-
C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。
-
C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
-
C++中,struct的成员默认访问说明符为public(为了与C兼容),class中的默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。
-
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名
- 虚函数可以声明为inline吗?
不可以
-
虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
-
虚函数要求在运行时进行类型确定,而内联函数要求在编译期完成相关的函数替换;
- 有static/virtual函数的类内存分布
1、static修饰符
1)static修饰成员变量
在数据段分配内存。
对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当做是类的成员,无论这个类被定义了多少个,静态数据成员都只有一份拷贝,为该类型的所有对象所共享(包括其派生类)。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以它不属于特定的类对象,在没有产生类对象前就可以使用。
2)static修饰成员函数
与普通的成员函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上来说,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,只能调用其他的静态成员函数。
Static修饰的成员函数,在代码区分配内存。
2、C++继承和虚函数
C++多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。
- 虚函数在进程内存空间的表现
动态多态实现有几个条件:
(1) 虚函数;
(2) 一个基类的指针或引用指向派生类的对象;
基类指针在调用成员函数(虚函数)时,就会去查找该对象虚表指针所指向的的虚函数表。查找该虚函数表中该函数的指针进行调用。
每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的都指向这同一个虚函数表。
虚函数表中为什么就能准确查找相应的函数指针呢?因为在类设计的时候,虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表中的函数指针就会被替换,因此可以根据指针准确找到该调用哪个函数。
3、virtual修饰符
如果一个类是局部变量则该类数据存储在栈区,如果一个类是通过new/malloc动态申请的,则该类数据存储在堆区。
如果该类是virutal继承而来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。
如果类中成员是virtual属性,会隐藏父类对应的属性。
- 类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?
- 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。
这两种方式的主要区别在于:
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
- 一个派生类构造函数的执行顺序如下:
① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
③ 类类型的成员对象的构造函数(按照初始化顺序)
④ 派生类自己的构造函数。
- 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。
- 成员列表初始化?
- 必须使用成员初始化的四种情况
① 当初始化一个引用成员时;
② 当初始化一个常量成员时;
③ 当调用一个基类的构造函数,而它拥有一组参数时;
④ 当调用一个成员类的构造函数,而它拥有一组参数时;
- 成员初始化列表做了什么
① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的; 23. 构造函数为什么不能为虚函数?析构函数为什么要虚函数?
-
从存储空间角度,虚函数对应虚函数表里存放的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
-
从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3.析构要是虚函数,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
- 当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。
因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。
析构函数为什么要虚函数
直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。 24. 析构函数的作用,如何起作用?
-
构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,就是你不写,编译器也自动调用一次。
-
析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。 当撤销对象时,编译器也会自动调用析构函数。 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。
- 虚析构函数的作用,父类的析构函数是否要设置为虚函数?
-
C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
-
纯虚析构函数一定得定义,因为每一个派生类析构函数会被编译器加以扩张,以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数。因此,缺乏任何一个基类析构函数的定义,就会导致链接失败。因此,最好不要把虚析构函数定义为纯虚析构函数。
- 构造函数析构函数可以调用虚函数吗?
-
在构造函数和析构函数中最好不要调用虚函数;
-
构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别;
-
即使构造函数或者析构函数如果能成功调用虚函数, 程序的运行结果也是不可控的。
- 虚函数的代价?
-
带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类;
-
带有虚函数的类的每一个对象,都会有有一个指向虚表的指针,会增加对象的空间大小;
-
不能再是内联的函数,因为内联函数在编译阶段进行替代,而虚函数表示等待,在运行阶段才能确定到低是采用哪种函数,虚函数不能是内联函数。
- 菱形继承
B继承于A,C继承于A,D多重继承于B和C,则创建D类对象时,就会有基类A的两份拷贝。
2.多继承:即一个派生类可以有两个或多个基类。
3.多重继承:像上图B继承于A,D继承于B,这种继承关系便是多继承。
4.虚继承:虚基类用virtual声明。无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。虚继承可用来解决菱形继承中的问题。 29. 多重继承和虚继承
这里讨论的是最终派生类对象的虚表指针指向的虚表空间
1、直接继承,没有虚函数覆盖
表头代表对象地址空间起始处,他指向虚表,里面的布局是先基类虚函数,再派生类虚函数。
2、直接继承,有虚函数覆盖
被覆盖的基类虚函数替换为派生类虚函数,其他不变。
3、多重继承,无虚函数
按照继承顺序,对象第一个地址空间存储由第一个基类继承来的虚函数表。
第二个地址空间继承第二个基类的虚函数表。
派生类的虚函数都放在第一个虚函数处。
4、多重继承,有虚函数
将所有基类被覆盖的虚函数替换为派生类虚函数,其余不变。 30. 哪些函数不能是虚函数
-
构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
-
内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
-
静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
-
友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
-
普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
- 类对象的大小
-
类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小;
-
内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的;
-
虚函数的话,会在类对象插入vptr指针,加上指针大小;
-
当该该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在在派生类中的空间中,也会对派生类进行扩展。
- 空类的大小是多少?为什么?
-
C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
-
C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
-
带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
-
C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
- 静态函数能定义为虚函数吗?常函数?
1、static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。2. 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.虚函数的调用关系:this -> vptr -> vtable ->virtual function
- 移动构造函数
-
有时候我们会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;
-
拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制;
-
C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;
-
与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作;
Example6 (Example6&& x) : ptr(x.ptr)
{
x.ptr = nullptr;
}
// move assignment
Example6& operator= (Example6&& x)
{
delete ptr;
ptr = x.ptr;
x.ptr=nullptr;
return *this;
} 35. 何时需要合成构造函数
-
如果一个类没有任何构造函数,但他含有一个成员对象,该成员对象含有默认构造函数,那么编译器就为该类合成一个默认构造函数,因为不合成一个默认构造函数那么该成员对象的构造函数不能调用;
-
没有任何构造函数的类派生自一个带有默认构造函数的基类,那么需要为该派生类合成一个构造函数,只有这样基类的构造函数才能被调用;
-
带有虚函数的类,虚函数的引入需要进入虚表,指向虚表的指针,该指针是在构造函数中初始化的,所以没有构造函数的话该指针无法被初始化;
-
带有一个虚基类的类
-
并不是任何没有构造函数的类都会合成一个构造函数
-
编译器合成出来的构造函数并不会显示设定类内的每一个成员变量
- 何时需要合成复制构造函数
有三种情况会以一个对象的内容作为另一个对象的初值:
-
对一个对象做显示的初始化操作,X xx = x;
-
当对象被当做参数交给某个函数时;
-
当函数传回一个类对象时;
-
如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时编译器会为该类合成一个拷贝构造函数;
-
如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;
-
如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;
-
如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;
- 何时需要成员初始化列表?过程是什么?
-
当初始化一个引用成员变量时;
-
初始化一个const成员变量时;
-
当调用一个基类的构造函数,而构造函数拥有一组参数时;
-
当调用一个成员类的构造函数,而他拥有一组参数;
-
编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何显示用户代码前。list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。
- 程序员定义的析构函数被扩展的过程?
-
析构函数函数体被执行;
-
如果class拥有成员类对象,而后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调用;
-
如果对象有一个vptr,现在被重新定义
-
如果有任何直接的上一层非虚基类拥有析构函数,则它们会以声明顺序被调用;
-
如果任何虚基类拥有析构函数
- 构造函数的执行算法?
-
在派生类构造函数中,所有的虚基类及上一层基类的构造函数调用;
-
对象的vptr被初始化;
-
如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做;
-
执行程序员所提供的代码;
- 构造函数的扩展过程?
-
记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并与成员的声明顺序为顺序;
-
如果一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么默认构造函数必须被调用;
-
如果class有虚表,那么它必须被设定初值;
-
所有上一层的基类构造函数必须被调用;
-
所有虚基类的构造函数必须被调用。
- 构造函数析构函数可否抛出异常
-
C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。
-
用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;
-
如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用terminate函数让程序结束;
-
如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。
- 类如何实现只能静态分配和只能动态分配
-
前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建
-
建立类的对象有两种方式:
① 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
② 动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;
- 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有。
- 如果想将某个类用作基类,为什么该类必须定义而非声明?
- 派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。
- 什么情况会自动生成默认构造函数?
-
带有默认构造函数的类成员对象,如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器就为该类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正被需要的时候才会发生;如果一个类A含有多个成员类对象的话,那么类A的每一个构造函数必须调用每一个成员对象的默认构造函数而且必须按照类对象在类A中的声明顺序进行;
-
带有默认构造函数的基类,如果一个没有任务构造函数的派生类派生自一个带有默认构造函数基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数;
-
带有一个虚函数的类
-
带有一个虚基类的类
-
合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化。
- 有关多态的若干问题
-
1、在有继承关系的父子类中,构建和析构一个子类对象时,父子构造函数和析构函数的执行顺序分别是怎样的?
-
2、在有继承关系的类体系中,父类的构造函数和析构函数一定要申明为virtual 吗?如果不申明为virtual 会怎样?
-
3、什么是C++ 多态?C++ 多态的实现原理是什么?
-
4、什么是虚函数?虚函数的实现原理是什么?
-
5、什么是虚表?虚表的内存结构布局如何?虚表的第一项(或第二项)是什么?
-
6、菱形继承(类D同时继承B和C,B和C又继承自A)体系下,虚表在各个类中的布局如何?如果类B和类C同时有一个成员变了m,m如何在D对象的内存地址上分布的?是否会相互覆盖?
- 什么是类的继承?
- 类与类之间的关系
has-A包含关系,用以描述一个类由多个部件类构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类;
use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;
is-A,继承关系,关系具有传递性;
- 继承的相关概念
所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;
- 继承的特点
子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;
- 继承中的访问控制
public、protected、private
-
继承中的构造和析构函数
-
继承中的兼容性原则
- 什么是组合?
-
一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。
-
创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体,析构函数调用顺序相反。
- 抽象基类为什么不能创建对象?
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义: 称带有纯虚函数的类为抽象类。
(2)抽象类的作用: 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意: 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
抽象类是不能定义对象的。一个纯虚函数不需要(但是可以)被定义。
一、纯虚函数定义 纯虚函数是一种特殊的虚函数,它的一般格式如下: class <类名> { virtual <类型><函数名>(<参数表>)=0; … }; 在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。 纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。
二、纯虚函数引入原因 1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔 雀等子类,但动物本身生成对象明显不合常理。 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)。若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个
三、相似概念 1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。 a.编译时多态性:通过重载函数实现 b.运行时多态性:通过虚函数实现。 2、虚函数 虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态重载。 3、抽象类 包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。 49. 多态运行机制
1、定义一个类A,若类中含有虚函数,则编译器自动为该类准备一个虚表存放虚函数指针。类A的对象a首地址为虚表指针,指向数据段存放的类的虚表。
2、类B继承类A,则也将类A虚表一并继承过来。 若类B对对类A中虚函数override(重写或叫覆盖),则修改类B虚表中对应的虚函数指针。若类B新增虚函数,在虚表中新增虚函数指针。则此时类A、类B各自持有一个虚表。
3、类B对象b,编译器为该对象准备一个虚表指针,指向类的虚表。b调用虚函数。
4、先查找子类虚函数表中是否重写了父类的虚函数,若有则为多态,否则为非多态。 50. 类什么时候会析构?
-
对象生命周期结束,被销毁时;
-
delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
-
对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
- 为什么友元函数必须在类内部声明?
- 因为编译器必须能够读取这个结构的声明以理解这个数据类型的大、行为等方面的所有规则。有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。
- 介绍一下C++里面的多态?
(1)静态多态(重载,模板)
是在编译的时候,就确定调用函数的类型。
(2)动态多态(覆盖,虚函数实现)
在运行的时候,才确定调用的是哪个函数,动态绑定。运行基类指针指向派生类的对象,并调用派生类的函数。
虚函数实现原理:虚函数表和虚函数指针。
纯虚函数: virtual int fun() = 0;
函数的运行版本由实参决定,在运行时选择函数的版本,所以动态绑定又称为运行时绑定。
当编译器遇到一个模板定义时,它并不生成代码。只有当实例化出模板的一个特定版本时,编译器才会生成代码。 53. 简述C++虚函数作用及底层实现原理
A: 要点是要答出虚函数表和虚函数表指针的作用。
虚函数是用来实现动态绑定的。
C++中虚函数使用虚函数表和虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地址,假如子类重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的函数的地址(子类中可以不是虚函数,但是必须同名);虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处),它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表。
- 虚函数的内存结构,那菱形继承的虚函数内存结构呢
- 多继承的优缺点,作为一个开发者怎么看待多继承
-
C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。
-
多重继承的优点很明显,就是对象可以调用多个基类中的接口;
-
如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性
-
加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。
-
使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。
- 虚函数与纯虚函数的区别在于
-
纯虚函数只有定义没有实现,虚函数既有定义又有实现;
-
含有纯虚函数的类不能定义对象,含有虚函数的类能定义对象;
子类如果不提供虚函数的实现,那就会自动调用基类的缺省方案。而子类如果不提供纯虚函数的实现,则编译将会失败。基类提供的纯虚函数实现版本,无法通过指向子类对象的基类类型指针或引用来调用,因此不能作为子类相应虚函数的备选方案。
第一,当基类的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供一个备选方案的时候,请将其设计为虚函数
第二,第二,当基类的某个成员方法,必须由子类提供个性化实现的时候,请将其设计为纯虚函数。 57. 隐式转换,如何消除隐式转换?
-
C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换
-
C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。在比如,数值和布尔类型的转换,整数和浮点数的转换等。某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。
-
基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。自定义对象 子类对象可以隐式的转换为父类对象。
-
C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。
-
如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。
-
用C语言实现C++的继承
#include
using namespace std;
//C++中的继承与多态
struct A
{
virtual void fun() //C++中的多态:通过虚函数实现
{
cout<<“A:fun()”<<endl;
}
int a;
};
struct B:public A //C++中的继承:B类公有继承A类
{
virtual void fun() //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加)
{
cout<<“B:fun()”<<endl;
}
int b;
};
//C语言模拟C++的继承与多态
typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承
struct _A //父类
{
FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
int _a;
};
struct _B //子类
{
_A a; //在子类中定义一个基类的对象即可实现对父类的继承
int _b;
};
void _fA() //父类的同名函数
{
printf("_A:_fun()\n");
}
void _fB() //子类的同名函数
{
printf("_B:_fun()\n");
}
void Test()
{
//测试C++中的继承与多态
A a; //定义一个父类对象a
B b; //定义一个子类对象b
A* p1 = &a; //定义一个父类指针指向父类的对象
p1->fun(); //调用父类的同名函数
p1 = &b; //让父类指针指向子类的对象
p1->fun(); //调用子类的同名函数
//C语言模拟继承与多态的测试
_A _a; //定义一个父类对象_a
_B _b; //定义一个子类对象_b
_a._fun = _fA; //父类的对象调用父类的同名函数
_b.a._fun = _fB; //子类的对象调用子类的同名函数
_A* p2 = &_a; //定义一个父类指针指向父类的对象
p2->_fun(); //调用父类的同名函数
p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转
p2->_fun(); //调用子类的同名函数
} 59. 继承机制中对象之间如何转换?指针和引用之间如何转换?
-
向上类型转换
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。Static_cast
-
向下类型转换
将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。 60. 派生类指针转换为基类指针,指针值会不会变
将一