C++面试题


1~10

1. 变量的声明和定义有什么区别

变量的定义为变量分配地址和存储空间, 变量的声明不分配地址。一个变量可以在多个地方声明, 但是只在一个地方定义。 加入extern修饰的是变量的声明, 说明此变量将在文件以外或在文件后面部分定义。

int main()
{
    extern int A;
    //这是个声明而不是定义,声明A是一个已经定义了的外部变量
    //注意:声明外部变量时可以把变量类型去掉如:extern A;
    do_sth(); //执行函数
}
int A; //是定义,定义了A为整型的外部变量

2. 简述#ifdef、#else、#endif和#ifndef的作用

  • 利用#ifdef#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏蔽。
#ifdef MATH
#include "math.c"
#endif
  • 在子程序前加上标记,以便于追踪和调试。
#ifdef DEBUG
printf ("In debugging......!");
#endif
  • 应对硬件的限制。由于一些具体应用环境的硬件不一样,限于条件,本地缺乏这种设备,只能绕过硬件,直接写出预期结果。

注意: 虽然不用条件编译命令而直接用if语句也能达到要求,但那样做目标程序长( 因为所有语句都编译),运行时间长( 因为在程序运行时间对if语句进行测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度, 减少运行时间。

3. 结构体可以直接赋值吗

声明时可以直接初始化, 同一结构体的不同对象之间也可以直接赋值, 但是当结构体中含有指针“ 成员” 时一定要小心。

注意:当有多个指针指向同一段内存时, 某个指针释放这段内存可能会导致其他指针的非法操作。因此在释放前一定要确保其他指针不再使用这段内存空间。

4. sizeof和strlen的区别

  • sizeof是一个操作符,strlen是库函数。
  • sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为'\0'的字符串作参数。
  • 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
  • 数组做sizeof的参数不退化,传递给strlen就退化为指针了

5. new/delete、malloc/free

5.1 new/delete、malloc/free 区别

特征 new/delete malloc/free
分配内存的位置 自由存储区
内存分配成功的返回值 完整类型指针 void*
内存分配失败的返回值 默认抛出异常 返回 NULL
分配内存的大小 由编译器根据类型计算得出 必须显式指定字节数
处理数组 有处理数组的 new 版本 new[] 需要用户计算数组的大小后进行内存分配
是否相互调用 可以,看具体的 operator new/delete 实现 不可调用 new
函数重载 允许 不允许
构造函数与析构函数 调用 不调用

5.2 C++有了malloc/free,为什么还需要new/delete?

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

5.3 delete和delete[]的区别

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

5.4 内存泄漏与定位

内存泄漏并非指的是内存在物理上的消失,而是分配某段内存后,失去了对该内存的控制,造成内存的浪费。比如 C++ new 之后没有 delete

定位内存泄露

  1. windows平台下通过CRT中的库函数进行检测;
  2. 在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
  3. Linux下通过工具valgrind检测

6. volatile有什么作用

  • 状态寄存器一类的并行设备硬件寄存器。
  • 一个中断服务子程序会访问到的非自动变量。
  • 多线程间被几个任务共享的变量。

7. 一个参数可以既是const又是volatile吗

可以, 用constvolatile 同时修饰变量, 表示这个变量在程序内部是只读的, 不能改变的, 只在程序外部条件变化下改变, 并且编译器不会优化这个变量。每次使用这个变量时, 都要小心地去内存读取这个变量的值, 而不是去寄存器读取它的备份。

注意: 在此一定要注意const 的意思,const只是不允许程序中的代码改变某一变量, 其在编译期发挥作用, 它并没有实际地禁止某段内存的读写特性。

8. 结构体内存对齐问题

  • 结构体作为一种复合数据类型, 其构成元素既可以是基本数据类型的变量, 也可以是一些复合型类型数据。

  • 对此, 编译器会自动进行成员变量的对齐以提高运算效率。默认情况下, 按自然对齐条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储, 第一个成员的地址和整个结构的地址相同, 向结构体成员中size最大的成员对齐。

  • 每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

  • 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐

9. 简述C、C++程序编译的内存分配情况

  • 从静态存储区域分配:

    内存在程序编译时就已经分配好, 这块内存在程序的整个运行期间都存在。速度快、不容易出错, 因为有系统会善后。例如全局变量, static变量, 常量字符串等。

  • 在栈上分配:

    在执行函数时, 函数内局部变量的存储单元都在栈上创建, 函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中, 效率很高, 但是分配的内存容量有限。

  • 从堆上分配:

    即动态内存分配。程序在运行的时候用mallocnew申请任意大小的内存, 程序员自己负责在何 时用freedelete释放内存。动态内存的生存期由程序员决定, 使用非常灵活。如果在堆上分配了空间, 就有责任回收它, 否则运行的程序会出现内存泄漏, 另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

一个C 、C + + 程序编译时内存分为5大存储区: 堆区、栈区、全局/静态区、常量区、程序代码区。

10. 指针和引用?

10.1 指针和引用的区别

  • 初始化区别:引用必须被初始化,指针不必。

  • 可修改区别:引用初始化以后不能被改变,指针可以改变所指的对象。

  • 非空区别:不存在指向空值的引用,但是存在指向空值的指针。

  • 合法性区别:在使用引用之前不需要测试他的合法性;相反,指针则应该总是被测试,防止其为空。

  • 应用区别

    • 使用指针的情况
      • 考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空)
      • 需要能够在不同时刻指向不同对象(在这种情况下,能够改变指针的指向)
    • 使用引用的情况:总是指向一个对象并且一旦指向一个对象后就不会改变指向

10.2 在什么时候需要使用“常引用”?

如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。

10.3 将“引用”作为函数返回值类型的优点和注意事项

好处:在内存中不产生返回值的副本,提高效率

注意事项

  1. 不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了”无所指”的引用,程序会进入未知状态。
  2. 不能返回函数内部 new 分配的内存的引用。原因是引用所指向的空间就无法释放,造成内存泄漏。
  3. 可以返回类成员的引用,但最好是 const。主要原因是如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
  4. 流操作符和赋值操作符重载返回值申明为引用。
  5. 在另外的一些操作符中,却千万不能返回引用,例如四则运算符。

10.4 句柄和指针的区别与联系

句柄和指针其实是两个截然不同的概念。

  • Windows系统用句柄标记系统资源,隐藏系统的信息。只要知道有这个东西,然后去调用即可,他是一个32bituint
  • 指针则标记某个物理内存地址。

10.5 常量指针和指针常量

  • 常量指针是一个指针,读成常量的指针,指向一个只读变量。如const int *p
  • 指针常量是一个不能给改变指向的指针。指针是个常量,不能中途改变指向,如int *const p

技巧:* 前面的是对被指向对象的修饰,* 后面的是对指针本身的修饰。

11~20

11. typedef和define有什么区别

  • 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。

  • 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。

13. 说一说extern“C”

extern "C" 的主要作用就是为了能够正确实现C + + 代码调用其他C 语言代码。加上extern “ C “ 后, 会指示编译器这部分代码按C 语言( 而不是C + + ) 的方式进行编译。由于C + + 支持函数重载, 因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中, 而不仅仅是函数名; 而C语言并不支持函数重载, 因此编译C语言代码的函数时不会带上函数的参数类型, 一般只包括函数名。

14. 什么是右值引用,跟左值又有什么区别?

  • 左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;
  • 右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象;
  • 左值能寻址,能赋值,可变。右值不能

15. 说一说C++中四种cast转换

C + + 中四种类型转换是:static_cast, dynamic_cast, const_cast,
reinterpret_cast

  1. static_cast
  • 用于各种隐式转换,比如非constconstvoid*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
  1. dynamic_cast

    用于动态类型转换。只能用于含有虚函数的类, 用于类层次间的向上和向下转化。只能转指针或引用。向下转化时如果是非法的,对于指针返回NULL,对于引用抛异常 。要深入了解内部转换的原理。

  • 向上转换:指的是子类向基类的转换

  • 向下转换:指的是基类向子类的转换

    它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

  1. const_cast
  • 用于将const变量转为非const
  1. reinterpret_cast
  • 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
  1. 为什么不使用c++的强制转换?
  • C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

16. C++的空类有哪些成员函数

  • 缺省构造函数。
  • 缺省拷贝构造函数。
  • 缺省析构函数。
  • 缺省赋值运算符。
  • 缺省取址运算符。
  • 缺省取址运算符 const

17. 对C++中的smart pointer四个智能指针的理解

智能指针的作用是管理一个指针, 因为存在以下这种情况: 申请的空间在函数结束时忘记释放, 造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类, 当超出了类的作用域是, 类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间, 不需要手动释放内存空间。

  • auto_ptr(c++98的方案,c++11已经抛弃)
    采用所有权模式。

    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.

    此时不会报错, p2剥夺了p1的所有权, 但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是: 存在潜在的内存崩溃问题!

  • unique_ptr(替换auto_ptr

    unique_ptr实现独占式拥有或严格拥有概念, 保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露( 例如“ 以new创建对象后因为发生异常而忘记调用delete” ) 特别有用。

    采用所有权模式。

    unique_ptr<string> p3 (new string ("auto")); //#4
    unique_ptr<string> p4; //#5
    p4 = p3;//此时会报错!!

    另外unique_ptr还有更聪明的地方: 当程序试图将一个 unique_ptr 赋值给另一个时, 如果源unique_ptr是个临时右值, 编译器允许这么做; 如果源unique_ptr将存在一段时间, 编译器将禁止这么做, 比如:

    unique_ptr<string> pu1(new string ("hello world"));
    unique_ptr<string> pu2;
    pu2 = pu1; // #1 not allowed
    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
  • shared_ptr

    shared_ptr 实现共享式拥有概念。多个智能指针可以指向相同对象, 该对象和其相关资源会在“ 最后一个引用被销毁” 时候释放。从名字shared就可以看出了资源可以被多个指针共享, 它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count() 来查看资源的所有者个数。除了可以通过new 来构造, 还可以通过传入auto_ptr , unique_ptr , weak_ptr 来构造。当我们调用release()时, 当前指针会释放资源所有权, 计数减一。当计数等于0 时, 资源会被释放。

  • weak_ptr

    weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 share_ptr管理的对象。 weak_ptr只是提供了对管理对象的一个访问手段,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况,它只可以从一个share_ptr或另一个weak_ptr对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决share_ptr相互引用时的死锁问题, 如果说两个share_ptr相互引用, 那么这两个指针的引用计数永远不可能下降为0 , 资源永远不会释放。

18. 对虚函数和多态的理解

  • 多态的实现主要分为静态多态和动态多态, 静态多态主要是重载, 在编译的时候就已经确定; 动态多态是用虚函数机制实现的, 在运行期间动态绑定。举个例子: 一个父类类型的指针指向一个子类对象时候, 使用父类的指针去调用子类中重写了的父类中的虚函数的时候, 会调用子类重写过后的函数, 在父类中声明为加了virtual 关键字的函数, 在子类中重写时候不需要加virtual也是虚函数。
  • 虚函数的实现: 在有虚函数的类中, 类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表表中放了虚函数的地址, 实际的虚函数在代码段中。当子类继承了父类的时候也会继承其虚函数表, 当子类重写父类中虚函数时候, 会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数, 会增加访问内存开销, 降低效率。

19. vector的底层原理

  • vector底层是一个动态数组, 包含三个迭代器,startfinish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。
  • 当空间不够装下数据v.push_back(val)时, 会自动申请另一片更大的空间(1.5倍或2倍), 然后把原来的数据拷贝到新的内存空间, 接着释放原来的那片空间。
  • 当释放或者删除v.clear()里面的数据时, 其存储空间不释放, 仅仅是清空了里面的数据。因此, 对vector的任何操作一旦引起了空间的重新配置, 指向原vector的所有迭代器会都失效了。

20. vector中的reserve和resize的区别

  • reserve是直接扩充到已经确定(最大容量)的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。
  • resize()可以改变有效空间(分配元素数量)的大小,也有改变默认值的功能。若size 大于capacity,则capacity的大小也会随着改变。

21~30

21. vector中的size和capacity的区别

  • size表示当前vector中有多少个元素(finish - start);

  • capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start)

22. vector中erase方法与algorithm中的remove方法区别

  • vectorerase方法真正删除了元素,迭代器不能访问了

  • remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。

23. vector迭代器失效的情况

  • 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。

  • 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it)

24. list的底层原理

  • list的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或删除一个元素,就配置或释放一个元素空间。

  • list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取

25. map 、set、multiset、multimap的底层原理

map setmultisetmultimap 的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux 系统中CFS进程调度算法, 也用到红黑树。

红黑树的特性:

  • 每个结点或是红色或是黑色;
  • 根结点是黑色;
  • 每个叶结点是黑的;
  • 如果一个结点是红的,则它的两个儿子均是黑色;
  • 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。

26. map 、set、multiset、multimap的特点

  • setmultiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。

  • mapmultimapkeyvalue组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序),map中元素的key不允许重复,multimap可以重复。

  • mapset的增删改查速度为都是logn,是比较高效的。

27. 为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?

  • 存储的是结点,不需要内存拷贝和内存移动。

  • 插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

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

  • 预处理:预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的;

  • 编译:将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程;命令中-S让编译器在编译之后停止,不进行后续过程。

  • 汇编:将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc汇编过程通过as命令完成。

  • 链接:链接过程将多个目标文件以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。

30. 构造函数为什么一般不定义为虚函数

  • 因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
  • 虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

31~40

31. 为什么析构函数最好声明为虚函数

  • 当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可能存在内存泄露的问题

  • 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

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

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

33. 移动语义

将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制,对象的移动语义需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。


文章作者: Allen Sun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Allen Sun !
评论
  目录