分享C++程序员面试八股文(二)

以下是一些 C++ 常见的八股文问题及回答:

  1. 说一下 static 关键字的作用

    • 全局静态变量:在全局变量前加上 static,它将存放在静态存储区,在整个程序运行期间一直存在。未经初始化的全局静态变量会被自动初始化为 0,其作用域是从定义之处开始到文件结尾,在声明它的文件之外不可见。
    • 局部静态变量:位于局部变量之前的 static 使其成为局部静态变量,同样存放在静态存储区。未经初始化的局部静态变量也会被自动初始化为 0,其作用域仍为局部作用域,当定义它的函数或语句块结束时,作用域结束。但局部静态变量离开作用域后不会销毁,仍驻留在内存中,直到该函数再次被调用,且值不变。
    • 静态函数:在函数返回类型前加 static,定义静态函数。该函数只在声明它的文件中可见,不能被其他文件使用,可避免与其他文件中的同名函数产生冲突。
    • 类的静态成员:实现多个对象之间的数据共享,且不会破坏隐藏原则,保证了安全性。它是类的所有对象共享的成员,而不是某个对象的成员。
    • 类的静态函数:和静态数据成员一样属于类的静态成员,不是对象成员,对其引用无需使用对象名。在静态成员函数的实现中不能直接引用类中说明的非静态成员,但可引用静态成员。调用静态成员函数的格式为:<类名>::< 静态成员函数名 >(< 参数表 >)。
  2. 说一下 C++ 和 C 的区别

    • 设计思想:C++ 是面向对象的语言,而 C 是面向过程的结构化编程语言。
    • 语法:C++ 具有封装、继承和多态三种特性;C++ 相比 C 增加了许多类型安全的功能,如强制类型转换;C++ 支持范式编程,例如模板类、函数模板等。
  3. 说一说 C++ 中四种 cast 转换

    • const_cast:用于将 const 变量转换为非 const。
    • static_cast:可用于各种隐式转换,如非 const 转 const、void * 转指针等。它能用于多态向上转换(子类向基类的转换),向下转(基类向子类的转换)能成功但不安全,结果未知。
    • dynamic_cast:用于动态类型转换,只能用于含有虚函数的类,用于类层次间的向上和向下转化,只能转换指针或引用。向下转化时,如果是非法的,对于指针返回 null,对于引用则抛出异常。它通过判断执行到该语句时变量的运行时类型和要转换的类型是否相同来决定是否能够进行向下转换。
    • reinterpret_cast:几乎可以进行任何转换,但可能会出现问题,应尽量少用。
  4. 请说一下 C/C++ 中指针和引用的区别

    • 存储空间:指针有自己的存储空间,而引用只是一个别名,没有独立的存储空间。
    • 大小:使用 sizeof 查看,指针的大小通常是 4 字节,而引用的大小是被引用对象的大小。
    • 初始化:指针可以被初始化为 null,而引用必须被初始化且必须是一个已有对象的引用。
    • 参数传递:作为参数传递时,指针需要解引用才能对对象进行操作,而直接对引用的修改会改变引用所指向的对象。
    • const:可以有 const 指针,但没有 const 引用。
    • 指向变更:指针在使用中可以指向其他对象,但引用只能是一个对象的引用,不能被改变。
    • 多级:指针可以有多级指针(**p),而引用止于一级。
    • ++ 运算符意义:指针和引用使用 ++ 运算符的意义不同。
    • 返回动态内存:如果返回动态内存分配的对象或内存,必须使用指针,使用引用可能引起内存泄漏。
  5. 什么情况下会调用拷贝构造函数

    • 用类的一个对象去初始化另一个对象时。
    • 当函数的参数是类的对象(值传递时),如果是引用传递则不会调用。
    • 当函数的返回值是类的对象(在 C++ 编译器发生 NRV 优化,如果是引用返回的形式则不会调用,如果是值传递的方式依然会发生拷贝构造函数的调用)。
  6. volatile、mutable 和 explicit 关键字的用法

    • volatile:定义变量的值是易变的,每次使用该变量的值时都要重新从其所在的内存读取数据,而不是读取寄存器内的备份。在多线程中,被多个任务共享的变量需要定义为 volatile 类型,可防止优化编译器将变量从内存装入 CPU 寄存器。
    • mutable:如果类的成员函数不会改变对象的状态,通常会声明为 const。但有时需要在 const 函数中修改一些与类状态无关的数据成员,此时该函数应使用 mutable 修饰,并放在关键字 const 之后。
    • explicit:C++ 中,一个参数的构造函数(或除第一个参数外其余参数都有缺省值的多参构造函数)承担了两个角色:用于构建单参数的类对象和隐含的类型转换操作符。被 explicit 修饰的构造函数的类,不能进行相应的隐式类型转换,只能以显式的方式进行类型转换。explicit 关键字只能写在声明中,不能写在定义中,且只对有一个参数的类构造函数有效。如果类构造函数参数大于或等于两个时,不会产生隐式转换,explicit 关键字也就无效了。声明为 explicit 的构造函数不能在隐式转换中使用,只能显式调用去构造类对象。
  7. 内联函数和宏定义的区别

    • 内联函数可以进行参数类型检查(在编译时),且具有返回值;宏只做简单字符串替换(在编译前)。
    • 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销,从而提高执行效率,同时进行参数类型检查,还可实现重载。
    • 宏定义时要注意书写(参数需括起来),否则容易出现歧义,而内联函数不会产生歧义。
  8. 浅拷贝和深拷贝的区别

    • 浅拷贝只是拷贝一个指针,没有开辟新的地址,拷贝的指针和原来的指针指向同一块地址。如果原来的指针所指向的资源被释放,再释放浅拷贝的指针的资源就会出现错误。
    • 深拷贝不仅拷贝值,还会开辟出一块新的空间用来存放新的值。即使原先的对象被析构,释放内存,也不会影响到深拷贝得到的值。
  9. C++ 中的重载、重写(覆盖)和隐藏的区别

    • 重载:在同一范围内,函数名相同,参数类型和数目有所不同,且不能仅依靠返回值不同来区分的函数。重载和函数成员是否为虚函数无关,全局函数和成员函数即使名称和参数相同,也因范围不同而互不干扰。
    • 覆盖(重写):在派生类中覆盖基类中的同名函数,即覆盖函数体,要求基类函数必须是虚函数,且不同范围(派生类和基类)、函数名和参数均相同,基类函数的 virtual 关键字必须存在。在覆盖关系中,调用方法根据对象类型决定。
    • 隐藏:派生类的函数隐藏了基类的同名函数。其特征是不同范围(派生类和基类)且函数名相同;若参数不同,则基类函数被隐藏;若参数相同,且基类没有 virtual 关键字,则也会隐藏。基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。当两个函数参数不同时,无论基类函数是否为虚函数,都会被隐藏。
  10. 简述堆和栈的区别

    • 空间分配:栈由操作系统自动分配释放,用于存放函数的参数值、局部变量的值等;堆一般由程序员分配释放。
    • 缓存方式:栈使用一级缓存,通常在被调用时处于存储空间中,调用完毕立即释放;堆存放在二级缓存中,速度相对较慢。
    • 数据结构:堆类似数组结构;栈类似栈结构,先进后出。
  11. C++ 的内存管理

    • 内存分配方式:C++ 中内存分为堆、栈、自由存储区、全局 / 静态存储区和常量存储区。栈用于在执行函数时创建函数内局部变量的存储单元,函数执行结束时自动释放;堆由 new 分配的内存块,需对应使用 delete 释放;自由存储区由 malloc 等分配的内存块,使用 free 来结束其生命周期;全局变量和静态变量被分配到全局 / 静态存储区;常量存储区存放常量,不允许修改。
    • 常见内存错误及其对策:
      • 内存分配未成功却使用:定义指针时先初始化为 null,使用 malloc 或 new 后立即检查指针值是否为 null。
      • 内存分配成功但未初始化就引用:为数组和动态内存赋初值,防止使用未初始化的内存作为右值。
      • 操作越过内存边界:避免数字或指针的下标越界,注意 “多 1” 或 “少 1” 操作。
      • 忘记释放内存造成内存泄漏:动态内存的申请与释放必须配对,或使用智能指针。
      • 释放了内存却继续使用:用 free 或 delete 释放内存后,立即将指针设置为 null,防止出现 “野指针”。

12.怎么判断数据分配在栈上还是堆上:

  • 从定义方式判断:

    • 如果是局部变量(函数内部定义的非静态变量,包括函数的参数),则通常分配在栈上。例如在函数void func(int num)中,num就是分配在栈上的局部变量;在函数内部定义的int local_var = 10;local_var也在栈上。
    • 如果是通过newmalloc(C++ 中建议使用new)分配的内存,则是在堆上。例如int *p = new int;*p所指向的内存就在堆上;char *str = (char*)malloc(100 * sizeof(char));str所指向的 100 个字节的内存是在堆上。
  • 从变量的生命周期判断:

    • 栈上的变量生命周期与所在的函数或语句块相关,函数执行结束或语句块结束,变量就被销毁。例如在{ int temp = 5; }这个语句块中,temp在语句块结束时就被销毁,这符合栈的特性。
    • 堆上的内存需要程序员手动释放(对于new使用delete,对于malloc使用free),如果不释放,它将一直存在(直到程序结束),并且可以在函数返回后继续使用(只要还有有效的指针指向它)。例如,在一个函数中int *arr = new int[10];,即使函数返回,只要arr指针没有被销毁并且没有越界操作,就可以继续在其他地方使用arr所指向的堆内存。

13.初始化为 0 的全局变量在 bss 还是 data:

  • 在 C++ 中,未初始化或者初始化为 0 的全局变量存放在 bss 段。bss(Block Started by Symbol)段是一种特殊的内存区域,它主要用于存储未初始化的全局变量和静态变量。这些变量在程序运行前会被自动初始化为 0。
  • data 段主要用于存放已经初始化的全局变量和静态变量。例如int global_var = 10;这个已初始化的全局变量就存放在 data 段,而int global_zero;(未初始化,默认初始化为 0)或者int global_init_zero = 0;(初始化为 0)就存放在 bss 段。

14.C++ 中内存对齐的使用场景:

  • 硬件优化:

    • 许多现代计算机体系结构(如 x86、ARM 等)在访问内存时,对于按特定字节对齐的数据访问效率更高。例如,在 32 位系统中,通常 4 字节对齐的整数访问速度更快。如果一个int类型(通常为 4 字节)的数据按照 4 字节对齐的方式存储在内存中,CPU 读取这个数据时可以一次性将其从内存加载到寄存器中,减少了内存访问的次数。
    • 对于结构体或类中的成员,如果按照内存对齐的原则进行排列,可以提高数据的读取和写入速度。比如,一个包含int(4 字节)和double(8 字节)成员的结构体,按照内存对齐原则进行内存布局,能够在访问这些成员时提高性能。
  • 兼容性和移植性:

    • 在跨平台开发中,不同的硬件平台可能有不同的内存对齐要求。遵循内存对齐原则可以确保代码在不同平台上的兼容性。例如,一些嵌入式系统或者特定的硬件设备可能对内存对齐有严格的要求,如果不遵循,可能会导致程序运行错误或者性能下降。
    • 当与外部库或硬件设备进行数据交互时,例如通过网络传输结构体数据或者与硬件寄存器进行数据交互,按照内存对齐原则组织数据结构可以确保数据的正确传输和解释。如果数据没有正确对齐,可能会导致数据的错误解读或者硬件设备的异常操作。
  • 内存布局的可预测性:

    • 在编写与内存布局相关的代码(如序列化和反序列化、内存映射文件等)时,内存对齐有助于确保内存布局的可预测性。按照内存对齐原则,我们可以准确地知道结构体或类中的每个成员在内存中的位置,从而方便进行数据的处理。例如,在将一个结构体写入到文件或者从文件中读取一个结构体时,如果结构体成员按照内存对齐原则存储,我们可以准确地定位每个成员在文件中的位置,进行正确的读写操作。

喜欢的同学可以点点关注!咱们下期见!! 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值