cpp

C++ 常见问答题

Continuous update :D

Posted by kevin on March 21, 2020

0x01 C和C++的区别

  1. C 是面向过程的编程,C++ 是面向对象的编程,主要包括:
    • 封装:隐藏了代码细节,只留接口给用户调用
    • 继承:继承父类的数据和方法,扩展已经存在的模块,实现代码重用
    • 多态:通过派生类重写父类的虚函数,实现了接口的重用,也就是 “一个接口,多种实现”
  2. C++ 支持函数重载, C 不支持
  3. C++ 有引用,C 中不存在引用的概念
  4. 动态管理内存方式不同,C 使用 malloc/free,C++ 除此之外还有 new/delete 关键字

0x02 #include 和 #include "file.h" 的区别

#include 先从标准库的路径中寻找文件,找不到再去工作目录

#include”file.h” 是先从工作目录中寻找文件,找不到再去标准库寻找

0x03 C++ 文件编译与执行的四个阶段

  1. 预处理:根据文件中的预处理指令来修改源文件的内容
  2. 编译:把源代码编译成汇编代码
  3. 汇编:把汇编代码翻译成目标二进制机器指令
  4. 链接:链接目标代码生成可执行程序,可执行程序包括源程序的机器码和相关的描述信息,如程序多大,占用多少内存空间等

0x04 #define 和 const 有什么区别

  1. 处理阶段不同:#define 定义的宏变量在预编译时就展开,在有宏变量的地方进行替换;const 定义的变量在编译时确定值
  2. #define 定义的常量没有类型,所给出的是一个立即数;const 定义的常量有类型名字,存放在静态(全局)区域
  3. #define 可以定义简单的函数,const 不可以定义函数
  4. #define 定义的常量不可以用指针去指向,因为不会给这个常量分配内存,const 定义的常量可以用指针去指向它的地址
  5. 能用 const 最好不要用 #define

0x05 C++ 深拷贝和浅拷贝的区别

  1. 对拥有动态成员的对象进行浅拷贝后,一个对象改变,另一个对象也会跟着改变,也就是说,这样的话,两个对象中在的指针成员指向的是同一片内存空间,为避免这种情况,就得用深拷贝,深拷贝后,两个对象的指针成员指向不同的地址,但是地址中的值是一样的
  2. 如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。

0x06 C++构造函数参数列表初始化与直接在函数内部初始化有何区别

这个问题之前也有人问过我,但是我回答不上来,后来在知乎上找到了答案:

当实例化一个类的对象时,实质上会发生下面 5 步:

  1. 分配 memory 给对象
  2. 调用类相应的构造函数
  3. 先进行初始化列表的初始化
  4. 再进入构造函数体内,进行一些赋值什么的
  5. 跳出函数体,控制权还给调用者

因此可以看到,如果写在函数内部的话,它是先用默认构造初始化了一遍,然后又去函数内部赋值,就很影响效率,所以最好优先使用初始化列表。

基类的构造函数只能通过列表初始化来完成! 上次有人问我一般在什么情况下用列表初始化,原来就是这个场景!

0x07 引用与指针有什么区别

  1. 引用必须被初始化,指针不必初始化
  2. 引用初始化之后就不能被改变,指针可以改变指向的对象
  3. 引用不能指向空值,指针可以

0x08 堆和栈有什么区别

  1. 堆是由 new 分配的内存块,需要程序员手动释放,否则程序结束后会被操作系统自动回收; 栈存放函数的参数值,局部变量,由编译器自动分配释放(保护现场就是用栈)
  2. 堆的分配需要使用频繁的 new/delete ,造成内存空间的不连续,会有大量的碎片(操作系统是用链表来储存空闲的内存地址
  3. 堆的生长空间向上,地址越来越大,栈的生长空间向下,地址越来越小
  4. 申请栈的空间大小有限制(类比汇编,栈段的大小不能超过 64 kB),申请空间超过栈的剩余空间时,会提示 overflow ,而堆的大小受限于系统中的有效虚拟内存,因此灵活度高,空间也比较大
int a = 0; //全局初始化区 
char *p1; //全局未初始化区 
main() 
{ 
    int b; //栈 
    char s[] = "abc"; //栈 
    char *p2; //栈 
    char *p3 = "123456"; //123456\0在常量区,p3在栈上。 
    static int c =0 //全局(静态)初始化区 
    p1 = (char *)malloc(10); //堆 
    p2 = (char *)malloc(20);  //堆 
}

0x09 struct 和 union 有什么区别

  1. struct 中的每个成员都有自己独立的地址,它们是同时存在的,union 中的所有成员占用同一段内存,它们不能同时存在,union 里面的数据存放在同一个地址开始的内存单元
  2. struct 的大小是字节对齐之后所有成员长度的总和,union 的大小是内存对齐后最长的数据成员的长度,union 就是为了节省空间才设计出来的
  3. union 的好处是可以用来测试 CPU 是大端模式还是小端模式

0x0A 多态,虚函数,纯虚函数

参考我写的这篇文章

0x0B 关键字static有什么作用

0x0C 哪些函数不能声明成虚函数

C++ 有五种函数不能被声明成虚函数:构造函数,友元函数,非成员函数,静态成员函数,内联成员函数

  1. 内联函数在编译时被展开,而虚函数在运行时才能动态绑定函数
  2. 非成员函数和友元函数不支持继承,没有继承属性的函数不能作为虚函数
  3. 静态成员函数对于每个类只有一个,所有的对象共享一份代码,这是属于类的而不是属于对象的,相反,虚函数必须要知道指针或引用对象的类型才能判断调用哪一个虚函数,因此虚函数是与对象相关的,而静态成员函数是与类相关的

0x0D 哪些成员函数不能被继承

  1. 构造函数(含拷贝构造函数)
  2. 析构函数
  3. 赋值运算符重载函数

0x0E 内存有哪五个区

  1. 堆:存储局部变量和函数参数
  2. 栈:由 new 申请的内存区,需要程序员手动 delete 释放,否则可能会被操作系统回收
  3. 全局存储区(静态存储区):存储全局变量和静态变量(初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放)
  4. 常量存储区:里面存放常量,不允许修改
  5. 自由存储区:由 malloc 申请的内存区,需要程序员手动 free 释放,否则就被操作系统回收
int a = 0;                //静态存储区 
char *p1;                 //静态存储区 
void main()
{
   int b;                //栈 
   char s[] = "abc";     //栈 
   char *p2;             //栈 
   char *p3 = "123456";  //123456:常量存储区,p3:栈 
   static int c = 0;     //静态存储区 
   p1 = new char[10];    //堆
   p1 = "123456";        //123456:常量存储区,编译器将p1与p3所指向的"123456"优化成同一个地方
}

0x0F 如何初始化 const 型数据成员

const 型的数据成员只能通过类构造函数初始化列表来初始化,不能在类里直接初始化

0x10 vector 动态扩容机制

vector<int> v; 这样子的话,一开始 v 里面的容量是 0,如果 v.push_back(1) 放进去一个值,那么 v 的容量就会扩大,不过不是在原内存上扩大,而是将之前内存中的东西拷贝到新申请的内存上,并且释放原来的内存。具体扩大多少倍跟编译器有关,VS 扩大 1.5 倍,GCC 扩大 2 倍。这样子的话就会有时间成本,所以最好一开始就给 vector 预先定义好容量。

0x11 vector emplace_back() 和 push_back() 的区别

都是向 vector 末尾添加一个元素,但是 emplace_back 效率更高,push_back 需要先创建这个元素,然后再将这个元素拷贝或者移动到容器中。而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

#include <vector> 
#include <iostream> 
using namespace std;
class testDemo
{
public:
    testDemo(int num):num(num){
        std::cout << "调用构造函数" << endl;
    }
    testDemo(const testDemo& other) :num(other.num) {
        std::cout << "调用拷贝构造函数" << endl;
    }
    testDemo(testDemo&& other) :num(other.num) {
        std::cout << "调用移动构造函数" << endl;
    }
private:
    int num;
};
int main()
{
    cout << "emplace_back:" << endl;
    std::vector<testDemo> demo1;
    demo1.emplace_back(2);  
    cout << "push_back:" << endl;
    std::vector<testDemo> demo2;
    demo2.push_back(2);
}

运行结果为:

emplace_back: 调用构造函数 push_back: 调用构造函数 调用移动构造函数

在此基础上,将 testDemo 类中的移动构造函数注释掉,再运行程序会发现,运行结果变为:

emplace_back: 调用构造函数 push_back: 调用构造函数 调用拷贝构造函数

由此可以看出,push_back() 在底层实现时,会优先选择调用移动构造函数,如果没有才会调用拷贝构造函数。但是 emplace_back 是 C++11 才有的特性,要兼容的话就用 push_back

0x12 vector 的内存管理是怎样的

vector map 都是通过 stl 的 allocator 进行内存分配的,如果你有注意过模板的话他们都会提供一个默认的 allocator。

所以你用不同的创建方式创建出来的容器对象 (指 vector 本身) 可以在栈或者堆上,而内容数据永远都是在堆上的

C++ 四种强制类型转换

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

  1. const_cast 用于将 const 变量转为非 const

  2. static_cast 用于各种隐式转换,比如非 const 转 const, void 转指针等 static_cast 能用于多态向上转化,如果向下转能成功但是不安全,结果未知

  3. dynamic_cast 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。 向下转化时,如果是非法的对干指针回NULL,对于引用抛异常。要深入了解内部转换的原理。 向上转换:指的是子类向基类的转换 向下转换:指的是基类向子类的转换 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换

  4. reinterpret_cast

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