C++面试问题总结

0.面向对象和面向过程的区别
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;

  • 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
  • 缺点:没有面向对象易维护、易复用、易扩展

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
  • 缺点:性能比面向过程低

1. 请用简单的语言告诉我C++ 是什么?
C++是在C语言的基础上开发的一种面向对象编程语言,应用广泛。C++支持多种编程范式 --面向对象编程、泛型编程和过程化编程。 其编程领域众广,常用于系统开发,引擎开发等应用领域,是最受广大程序员受用的最强大编程语言之一,支持类:类、封装、重载等特性!

2. C和C++的区别

  • C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,首要考虑的是如何构造一个契合问题域的对象模型,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。
  • C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。
  • C++支持函数重载,C不支持函数重载
  • C++中有引用,C中不存在引用的概念

3. C++文件编译与执行的四个阶段

1)预处理:根据文件中的预处理指令来修改源文件的内容
2)编译:编译成汇编代码
3)汇编:把汇编代码翻译成目标机器指令
4)链接:链接目标代码生成可执行程序

4. 定义和声明的区别

  • 变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。
  • 变量声明:用于向程序表明变量的类型和名字。
  • 定义也是声明:当定义变量时我们声明了它的类型和名字。
  • extern关键字:通过使用extern关键字声明变量名而不定义它。

5. 指针和引用的区别

  • 指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;
    引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的
  • 引用只有一级,而指针可以有多级
  • 引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
  • 引用不能为NULL,指针可以为NULL
  • 引用变量内存单元保存的是被引用变量的地址
  • “sizeof 引用" = 指向变量的大小 , “sizeof 指针”= 指针本身的大小
  • 引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
  • 引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址;指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作

6. extern关键字的作用
extern置于变量或函数前,用于标示变量或函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。它只要有两个作用:

  • 当它与“C”一起连用的时候,如:extern “C” void fun(int a,int b);则告诉编译器在编译fun这个函数时候按着C的规矩去翻译,而不是C++的(这与C++的重载有关,C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同)
  • 当extern不与“C”在一起修饰变量或函数时,extern int g_Int,他的作用是声明变量或者函数为外部变量。 如:extern int g_Int;它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用。记住它是一个声明不是定义!也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

7. static关键字的作用

  • 修饰局部变量
    static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。静态变量只初始化一次,即便初始化的代码被调用多次。(无论是局部静态还是全局静态)
  • 修饰全局变量
    全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被该包含该定义的文件访问(即改变了作用域)。
  • 修饰函数
    static修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。
  • 修饰成员变量
    用static修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例。因此,static成员必须在类外进行初始化 (初始化格式:int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化(static const int count = 0; // 静态整型常量成员可以在类内初始化,但是 static const float count就不行了 )。
  • 修饰成员函数
    用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量。静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。例如可以封装某些算法,比如数学函数,如ln,sin,tan等等,这些函数本就没必要属于任何一个对象,所以从类上调用感觉更好,比如定义一个数学函数类Math,调用Math::sin(3.14);还可以实现某些特殊的设计模式:如Singleton;
  • 最重要的特性:隐藏
    当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏。
  • 不可以同时用const和static修饰成员函数。C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

8. 解释C++中静态函数和静态变量?
类静态变量

  • 类静态数据成员在编译时创建并初始化:在该类的任何对象建立之前就存在,不属于任何对象,而非静态类成员变量则是属于对象所有的。类静态数据成员只有一个拷贝,为所有此类的对象所共享。
  • static 成员变量实现了同类对象间信息共享。
  • static 成员类外存储,求类大小,并不包含在内。
  • static 成员是命名空间属于类的全局变量,存储在 data 区的rw段。
  • static 成员只能类外初始化。
  • 可以通过类名访问(无对象生成时亦可),也可以通过对象访问。

类静态成员函数

  • 类静态成员函数属于整个类,不属于某个对象,由该类所有对象共享。
  • 静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。
  • 静态成员函数只能访问静态数据成员。原因:非静态成员函数,在调用时 this指针时被当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针。

9.const的作用

  • 定义变量为只读变量,不可修改
  • 修饰函数的参数和返回值(后者应用比较少,一般为值传递)
  • 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不修改成员变量的值
  • const成员函数(只需要在成员函数参数列表后加上关键字const,如char get() const;)可以访问const成员变量和非const成员变量,但不能修改任何变量。在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const成员函数。
  • const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.即对于class A,有const A a;那么a只能访问A的const成员函数。而对于:A b;b可以访问任何成员函数。
  • 使用const关键字修饰的变量,一定要对变量进行初始化。

10. #define和const的区别

  • #define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域
  • 处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。
  • #define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址
  • #define可以定义简单的函数,const不可以定义函数

11. C++中有了malloc / free , 为什么还需要 new / delete

  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。 由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  • 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

12. 结构体struct和共同体union(联合)的区别

  • 结构体:将不同类型的数据组合成一个整体,是自定义类型;共同体:不同类型的几个变量共同占用一段内存
  • 结构体中的每个成员都有自己独立的地址,它们是同时存在的;共同体中的所有成员占用同一段内存,它们不能同时存在;
  • sizeof(struct)是内存对齐后所有成员长度的总和;sizeof(union)是内存对齐后最长数据成员的长度

13. 结构体和类的区别

  • C++结构体内部成员变量及成员函数默认的访问级别是public,而c++类的内部成员变量及成员函数的默认访问级别是private。
  • C++结构体的继承默认是public,而c++类的继承默认是private。
  • 到底默认是public继承还是private继承,取决于子类而不是基类。
  • struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。如下:
    struct A{};class B : A{}; //private继承
    struct C : B{}; //public继承
    注意: struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!!
    详细内容参考:C++中结构体与类的区别(struct与class的区别)

14. delete和delete[]的区别

  • delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数
  • 用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
    15. C++内存分配方式
  1. 从静态存储区分配内存,编译时分配,程序运行的整个周期都存在。
  2. 从栈中分配内存,函数内的局部变量在栈上分配内存,系统自动分配和释放。
  3. 从堆中分配内存,程序员来分配和释放,并指定大小。
    详细内容参考博客:C++内存分配的方式

15. 堆和栈的区别
一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。
注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,
初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区―存放函数体的二进制代码。

5、重载overload,重写(覆盖)overwrite,重定义(隐藏)redefining,这三者之间的区别
先上一张图:
C++面试问题总结_第1张图片

  • 重载:
    在同一个作用域内;
    函数名相同,参数列表不同(参数个数不同,或者参数类型不同,或者参数个数和参数类型都不同),返回值类型可相同也可不同;这种情况叫做c++的重载!
    注意:c语言没有函数重载的机制。
    c++函数重载达到的效果:调用函数名相同的函数,会根据实参的类型和实参顺序以及实参个数选择相应的函数;c++函数重载是一种静态多态(又叫做静态联编,静态绑定,静态决议)

  • 重写(覆盖):
    说重写之前先说一个概念:虚函数:类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。
    重写(覆盖)的前提条件:父类函数为虚函数;
    重写(覆盖)的概念:当在子类中定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的这个虚函数。
    在子类中定义了一个与父类虚函数完全相同的函数,那么这个子类的函数就是重写了父类的虚函数,此时这个子类的函数就是虚函数,如果不显示的加上virtual修饰,编译器也会默认为虚函数;
    覆盖(重写)达到的效果:

    1. 在子类中重写了父类的虚函数,那么子类对象调用该重写函数,调用到的是子类内部重写的虚函数,而并不是从父类继承下来的虚函数;(这其实就是动态多态的实现);
    2. 在子类中重写了父类的虚函数,如果用一个父类的指针(或引用)指向(或引用)子类对象,那么这个父类的指针或用引用调用该重写的虚函数,调用的是子类的虚函数;相反,如果用一个父类的指针(或引用)指向(或引用)父类的对象,那么这个父类的指针或用引用调用该重写的虚函数,调用的是父类的虚函数;

    什么是在子类中定义了一个与父类完全相同的虚函数,有两种情况:
    1. 就是说子类中的虚函数和父类中的虚函数,函数名,参数个数,参数类型,返回值类型都相同;这种情况下子类的这个虚函数重写的父类中的虚函数,构成了重写;
    2. 协变—是说子类中的虚函数和父类中的虚函数,函数名,参数个数,参数类型都相同,只是返回值类型不同;父类的虚函数返回父类的指针或者引用,子类虚函数返回子类的指针或者引用;这种情况下子类的这个虚函数也重写了父类中的虚函数,也构成了重写;——我们把这种特殊的情况叫做协变

  • 隐藏(重定义)
    隐藏(重定义)概念:
    是指在不同的作用域中(分别在父类和子类中),函数名相同,不能构成重写的都是重定义。
    隐藏(重定义)的使用范围:
    重定义的不光是类的成员函数,还可以是类的成员变量;
    隐藏(重定义)的直接效果:
    如果在父类和子类中有相同名字的成员;那么在子类中。会将父类的成员隐藏;隐藏以后的直接效果就是:无论在子类的内部或者外部(通过子类成员)访问该成员;全都是访问子类的同名成员; 如果在子类内部或者外部(通过子类成员)访问同名的成员函数,则需要根据函数调用的规则来调用子类的同名成员函数;否则调用失败;
    详细的示例参考:c++三大概念要分清–重载,隐藏(重定义),覆盖(重写)
    16. 内存对齐的原则以及作用?
    内存对齐的原则

  • 结构体内的成员按自身长度自对齐(32位机器上,如char=1,short=2,int=4,double=8),所谓自对齐是指该成员的起始地址必须是它自身长度的整数倍。如int只能以0,4,8这类地址开始。

  • 结构体的总大小为结构体的有效对齐值的整数倍(默认以结构体中最长的成员长度为有效值的整数倍,当用#pragrma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值)。即sizeof的值,必须是其内部最大成员的整数倍,不足的要补齐。

内存对齐的作用

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。

17. STL库用过吗?常见的STL容器有哪些?算法用过几个?

STL包括两部分内容:容器和算法
容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器
序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist
关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set
算法有排序,复制等,以及各个容器特定的算法
迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。

18. STL中map和set的原理(关联式容器)

map和set的底层实现主要通过红黑树来实现
红黑树是一种特殊的二叉查找树
1)每个节点或者是黑色,或者是红色
2)根节点是黑色
3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
4)如果一个节点是红色的,则它的子节点必须是黑色的
5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。

19. STL中的vector的实现,是怎么扩容的?

vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。
vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。vector的动态增加大小的时候,并不是在原有的空间上持续新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,并释放原空间。在VS下是1.5倍扩容,在GCC下是2倍扩容。在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。

20. STL中unordered_map和map的区别

map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是 O(logN) 。创建一棵有n个节点的红黑树的复杂度是 nlog(n).

unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。

21. C++中vector和list的区别

  • vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
  • list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。
  • vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符
  • list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等
  • vector::iterator和list::iterator都重载了“++”运算符。
  • 总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
    如果需要大量的插入和删除,而不关心随机存取,则应使用list。

22. C++中内存泄漏的几种情况

内存泄漏是指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

1)类的构造函数和析构函数中new和delete没有配套,或者malloc/free没有配套
2)在释放对象数组时没有使用delete[],使用了delete
3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露
4)没有正确的清楚嵌套的对象指针

23. C++的四种强制转换

类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)

    (new-type) expression
    new-type (expression)

隐式类型转换比较常见,在混合类型表达式中经常发生;四种强制类型转换操作符:

static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :编译时期的静态类型检查
static_cast < type-id > ( expression )
该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的
2)dynamic_cast:运行时的检查
用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用
dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。
dynamic_cast如果不能转换返回NULL
源类中必须要有虚函数,保证多态,才能使用dynamic_cast(expression)
3)const_cast
去除const常量属性,使其可以修改 ; volatile属性的转换
4)reinterpret_cast
通常为了将一种数据类型转换成另一种数据类型

24. 深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。

25. 什么情况下会调用拷贝构造函数(三种情况)

系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)

生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数
调用拷贝构造函数的情形:

1)用类的一个对象去初始化另一个对象的时候
2)当函数的参数是类的对象时,又是值传递的时候,如果是引用传递则不会调用
3)当函数的返回值是类的对象或者引用的时候

26. C++的多态性

  • 多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数 。C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
  • 多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
  • 在绝大多数情况下,程序的功能是在编译的时候就确定下来的,我们称之为静态特性。反之,如果程序的功能是在运行时刻才能确定下来的,则称之为动态特性。C++中,虚函数,抽象基类,动态绑定和多态构成了出色的动态特性。
  • 最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
    a、编译时多态性:通过重载函数实现
    b、运行时多态性:通过虚函数实现

27. 虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?

  • 编译器为每一个类维护一个虚函数表(本质是一个函数指针数组,数组里面存放了一系列函数地址 ),每个对象的首地址保存着该虚函数表的指针,同一个类的不同对象实际上指向同一张虚函数表。调用形式:*(this指针+调整量)虚函数在vftable内的偏移。
  • 在类内部添加一个虚拟函数表指针,该指针指向一个虚拟函数表,该虚拟函数表包含了所有的虚拟函数的入口地址,每个类的虚拟函数表都不一样,在运行阶段可以循此脉络找到自己的函数入口。纯虚函数相当于占位符, 先在虚函数表中占一个位置由派生类实现后再把真正的函数指针填进去。 除此之外和普通的虚函数没什么区别。
  • 在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。
    使用这种方式,就可以实现多态的特性。假设我们使用如下语句:
    MyClass*pc=new MyClassA;
    pc->fun();  

因为虚函数表内的函数地址已经被子类重写的fun函数地址覆盖了,因此该处调用的函数正是MyClassA::fun,而不是基类的MyClass::fun。
如果使用MyClassA对象直接访问fun,则不会出发多态机制,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用MyClassA::fun即可。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表
详情可以参考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html

28. 智能指针怎么实现?什么时候改变引用计数?
构造函数中计数初始化为1;
拷贝构造函数中计数值加1;
赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一;
析构函数中引用计数减一;
在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象。
详情参考:https://blog.csdn.net/liuweiyuxiang/article/details/88644818
29. 虚函数是怎么实现的
每一个含有虚函数的类都至少有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),类的示例对象不包含虚函数表,只有虚指针;派生类会生成一个兼容基类的虚函数表。

30. 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

  • 构造函数不能声明为虚函数
    1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
    2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
  • 析构函数最好声明为虚函数
    1)首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
    2)如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

31. 什么时候要用虚析构函数
通过基类的指针来删除派生类的对象时,基类的析构函数应该是虚的。否则其删除效果将无法实现。
一般情况下,这样的删除只能够删除基类对象,而不能删除子类对象,形成了删除一半形象,从而千万内存泄漏。
原因:在公有继承中,基类对派生类及其对象的操作,只能影响到那些从基类继承下来的成员。如果想要用基类对非继承成员进行操作,则要把基类的这个操作(函数)定义为虚函数。那么,析构函数自然也应该如此:如果它想析构子类中的重新定义或新的成员及对象,当然也应该声明为虚的。
注意:如果不需要基类对派生类及对象进行操作,则不能定义虚函数(包括虚析构函数),因为这样会增加内存开销。

32. 静态绑定和动态绑定的介绍
静态绑定和动态绑定是C++多态性的一种特性

  • 对象的静态类型和动态类型
    静态类型:对象在声明时采用的类型,在编译时确定
    动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改
  • 静态绑定和动态绑定
    静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定
    动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定
  • 只有虚函数才使用的是动态绑定,其他的全部是静态绑定

33. 引用是否能实现动态绑定,为什么引用可以实现
可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。

34. Makefile文件的作用?
makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

参考文章:

  1. C++面试常见问题
  2. C++常见面试题
  3. C++面试集锦( 面试被问到的问题 )
  4. 常见C++面试题及基本知识点总结(一)
  5. 常见C++笔试面试题整理
  6. 面向对象与面向过程的本质的区别

你可能感兴趣的:(C++)