C++基础问题

1、在 main 函数执行之前和之后的代码可能是什么

main 函数执行之前,初始化系统相关资源:

  • 设置栈指针
  • 初始化 static 变量和 global 变量
  • 未初始化的全局变量赋初值
  • 全局对象初始化,这里会调用构造函数,这是可能会调用的代码
  • 将 main 函数的参数传递给 main 函数,之后开始真正的运行 main 函数

main 函数执行之后:

  • 全局对象的析构函数
  • atexit 注册的函数

2、结构体内存对齐

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同
  • 未特殊说明,按结构体中 size 最大的成员对齐(例如,如果有 double 成员,则按照 8 字节对齐)
  • alignof,计算出结构体的对齐方式;alignas,指定结构体的对齐方式。若 alignas 小于最小的对齐单位,会被忽略
  • vector 的第一个元素的地址和 整个 vector 的地址是不同的

3、指针的引用的区别

  • 指针是一个变量,存储地址;引用和原来的变量本质上是同一个东西,是变量的别名
  • 指针可以多级,引用只能一级
  • 指针可以为空,引用不可以为 NULL,且在定义时必须初始化
  • 指针初始化后可以改变指向,引用初始化后不可以改变
  • sizeof,指针得到的是指针的大小,引用得到的是变量的大小
  • 指针作为参数时,传递的是指针的拷贝,和实参指向同一个内存,在函数中改变指针,不影响实参的指向,但是在函数中改变形参指向的变量,会改变实参指向的变量;引用在函数中改变,会改变实参的值
  • 引用在声明时必须初始化为另一个变量;指针声明 和 定义 可以分开

4、传递函数参数时,指针和引用如何选择

  • 对栈空间大小敏感(如递归)时使用引用,因为使用引用不会创建临时变量,开销更小
  • 类对象作为参数时,使用引用,这是 C++ 的标准方式

5、堆和栈的区别

  • 申请方式不同:
    • 堆要手动申请和释放
    • 栈自动申请和释放
  • 大小限制不同:
    • 栈大小固定,一般为 4MB
    • 堆大熊可以灵活调整,是不连续的内存,一般为1-4 G
  • 申请效率不同:
    • 栈块,没有碎片
    • 堆慢,有碎片

6、new/delete 和 malloc/free 的异同

  • 前者是运算符,后者是库函数
  • new 自动计算分配空间大小,malloc 需要手动计算大小
  • new 是类型安全的,malloc 不是
  • 前者有构造函数和析构函数,后者没有;后者需要库函数的支持
  • new 封装了 malloc,直接 free 不会报错,但这样只是释放了内存,没有析构对象
  • 前者可以看做是后者的再封装
  • 前者返回的是具体类型的指针,后者是 void 类型的指针,必须进行强制类型转换
  • 引入前者的原因是为了创建和销毁类对象的时候,涉及到对象的构造和析构
  • 被 free 的内存不是立即返回给系统,而是 ptmalloc 会使用双向链表保存起来,下一次申请的时候,会从这里面找,避免频繁的系统调用。另外,ptmalloc 还会尝试把碎片内存合并

7、宏定义和 typedef/函数的区别

  • 宏定义在预处理阶段就替换了,相当于直接插入代码;函数在运行时会跳转到具体的函数
  • 宏定义的参数没有类型,不进行类型检查;函数参数相反
  • 宏定义用于定义常量和复杂文本内容;typedef 用于定义类型别名
  • 宏定义替换在编译之前,typedef 在编译时
  • 宏定义不是语句,没有分号;typedef 是语句,有分号

8、变量声明和定义的区别

  • 声明只是把变量的声明的位置和类型告知编译器,不分配牛内存;定义会在定义的时候分配内存
  • 相同变量可以在多处声明,但只能有一处定义

9、strlen 和 sizeof 的区别

  • sizeof 是运算符,结果在编译时得到;strlen 是函数,结果在运行时得到
  • sizeof 的参数可以是任何类型,strlen 的参数只能是字符指针,且所指字符串必须以 \0 结尾
  • 由于 sizeof 在编译时确定,因此不可以用来得到动态分配的空间的大小,即运行时分配的空间

10、指针常量和常量指针

  • 指针常量:指向常量(只读)的指针
  • 常量指针:不可以修改指向的指针,指针是常量,必须初始化

11、C++ struct 和 class 的区别

  • 两者都拥有成员函数、共有、私有部分
  • 成员:struct 默认 public,class 默认 private
  • 继承:struct 默认 public,class 默认 private
  • C 和 C++ 中 struct 的区别:
    • C 语言中 struct 是用户自定义数据类型;C++ 中是抽象数据类型,支持成员函数,可以继承,支持多态
    • C 中 struct 没有权限,struct 只是一些变量的集合,可以封装数据,但是不可以隐藏数据;C++ 中增加了权限
    • C 中必须在结构标记前加 struct ,才可以作为数据类型名;C++ 中可以直接使用结构标记作为数据类型名

12、define 宏定义和 const 的区别

  • 宏定义在编译之前的预处理阶段起作用;const 在编译时和 运行时起作用
  • 宏定义只做替换,内存中会产生多分相同的备份;const 在程序运行中只有一份备份,且可以进行常量折叠
  • 宏定义的数据没有分配内存空间,只做文本替换;const 变量需要分配空间

13、数组名和指针的区别

  • 都可以通过增减偏移量来访问数组中的元素
  • 数组名不是真正的指针,可以理解为常指针,因此数组名无法进行自增、自减
  • 数组名作为形参时,在函数内退化为一般指针

14、final 和 override 关键字

  • override,指针了子类的这个虚函数是重写的父类的虚函数
  • final,不允许被继承的类的这个虚函数被重写

15、初始化和幅值的区别

  • 基本类型:没有区别
  • 类和复杂类型:需要考虑构造函数

16、extern “C”

在程序中加上 extern “C”,告诉编译器这部分代码是 C 语言写的,要按照 C 语言进行编译

17、野指针和悬空指针

  • 野指针,没有被初始化过而使用的指针
  • 悬空指针,指针指向的内存已经被释放的指针

18、C++ 重载、重写和隐藏的区别

  • 重载,在同一范围内的同名函数,参数不同
  • 重写,在派生类中,覆盖基类中的同名函数。重写就是重写函数体,要求基类函数必须是虚函数,且:
    • 与基类的虚函数有相同的参数
    • 与基类的返回值类型相同
  • 隐藏,在某些情况下,派生类中的函数屏蔽了基类中的同名函数:
    • 两个函数相同,但是基类函数不是虚函数
    • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏

19、C++ 中的构造函数

  • 默认构造函数
  • 初始化化构造函数
  • 拷贝构造函数
  • 移动构造函数(move 和 右值引用)
  • 委托构造函数
  • 转换构造函数,用于将其他类型的变量隐式转换为本类对象

20、内联函数和宏定义的区别

  • 内联函数在编译时替换;宏定义在编译之前替换

21、拷贝构造函数在什么情况下会被调用

  • 用类的一个实例化对象去初始化另一个对象
  • 函数的参数是类的对象,且值传递
  • 函数的返回值是函数体内局部对象的类的对象,返回方式为值传递

22、static_cast 和 dynamic_cast 的区别

  • static_cast 一般用在自定义类型转换中,会做静态类型检查,编译时
  • dynamic_cast 会做动态类型检查,运行时,需要打开编译的 rtti 支持才可以

23、C++ 中有几种 new

  • plain new,普通的 new,但是在内存分配失败的时候,抛出异常 std::bad_alloc,而不是返回 NULL
  • nothrow new,在分配空间失败的时候,不抛出异常,而是返回 NULL
  • placement new,允许在一块分配成功的内存上重新构造对象或者对象数组。不需要担心内存分配失败,因为他实际上没有分配内存,只是调用对象的构造函数
    • placement new,可以反复使用一块较大的动态分配的内存来构造不同类型的对象或者相应的数组
    • placement new 构造的数组,必须显式调用相应的析构函数来释放内存,不可以是直接使用 delete,这是因为 placement new 构造的对象或者数组的大小不一定等于原来分配的内存的大小,delete 会造成内存泄漏相关的错误。

24、C++ 的异常处理

  • 常见的异常:
    • 数组下标越界
    • 0 作为除数
    • 动态分配内存时空间不足
  • 关键字:try、throw、catch
  • C++ 标准异常类 exception
    • bad_typeid
    • bad_cast
    • bad_alloc
    • ios_base::failure
    • logic_error --> out_of_range

25、static 的用法和作用

  • 隐藏,多文件编译的时候,所有未加 static 的全局变量和函数具有全局可见性
  • 保证变量的内容持久,static 和 全局变量都存储在静态数据区,在程序开始运行的时候完成初始化。
  • static 变量的默认初始化值为 0
  • C++ 类成员的 static
    • 函数内的 static 变量,在下次调用时,不会再初始化,还会维持上次的值
    • 模块内的 static 变量,只可以被模块内的函数访问
    • 模块内的 static 函数,只会被这一模块内的其它函数调用,这个函数的使用范围只在声明它的模块内
    • 类中的 static 成员变量归类所有,类的所有对象只有一份拷贝
    • 类中的 static 成员函数属于整个类所有,这个函数不接收 this 指针,只能访问类的 static 成员变量。这个是因为 static 成员函数属于整个类所有,不属于对象,而 this 指针是指向本对象的指针,因此 static 成员函数不接收 this 指针,所以也无法访问非 static 的成员变量
    • static 修饰的变量先于对象存在,因此 static 成员变量必须在类外进行初始化
    • static 成员函数不可以被 virtual 修饰。因为 static 成员函数属于整个类所有,不属于对象,加 virtual 修饰没有意义。虚函数的实现是为每一个对象分配一个 vptr 指针,而 vptr 指针是通过 this 指针调用的。虚函数的调用关系:this --> vptr --> ctable --> virtual function

26、指针和 const

  • int* const p,const 修饰 p,那么指针 p 的值不可以修改,即指针指向的内存不可以修改,但是指针指向的内存的内容是可以次改的
  • int const *p,const 修饰的是 *p,那么指针 p 指向的内存的内容不可以修改,但是可以通过修改指针 p 的值来修改指针指向的内存

27、形参和实参的区别

  • 形参只有在被调用时才会分配内存,调用结束时,就会在释放内存
  • 实参在函数调时必须要有具体确定的值
  • 形参和实参在数量、类型、顺序上都必须保持完全一致

28、值传递、指针传递和引用传递的区别

  • 值传递,有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象或者是大的结构体对象,耗时耗内存
  • 指针传递,有一个形参向函数所属的栈拷贝数据的过程,但是拷贝的是地址
  • 引用传递,有一个形参向函数所属的栈拷贝数据的过程,但是拷贝是针对地址的,相当于是给该数据所在的地址取了一个别名
  • 效率上,指针传递和引用传递的效率比值传递高

29、什么是类的继承

  • 类的继承,就是一个类继承了了另一个类的属性和方法
  • 子类拥有父类所有的属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用

30、new 和 delete 的实现原理,delete 是如何知道释放内存的大小的

  • new:

    • 对于简单类型,new 直接调用 operator new 分配内存;对于复杂类型,new 先调用 operator new‘ 分配内存,再在分配的内存上调用构造函数

    • 对于简单类型,new[] 计算好大小之后,调用 operator new 分配内存;对于复杂类型,new[] 先调用 operator newp[] 分配内存,然后在分配的内存的前四个字节写入数组的大小 n,之后调用 n 次构造函数

  • delete:

    • 对于简单类型,只是调用 free 函数;复杂类型,先调用析构函数,再调用 operator delete;
    • 对于简单类型,delete 和 delete[] 相同;对于复杂类型,delete 会直接释放指针指向的内存,而 delete[] 会释放 p-4 的内存,因为前面的 4 字节被用来存放数组长度,系统没有记录

31、malloc、realloc、calloc 的区别

  • malloc 需要设定分配内存的大小
  • calloc 不需要设定分配内存的大小,且空间大小默认是 0
  • realloc,给动态分配的空间分配额外的空间,用于扩充容量

32、trival destructor

  • 用户没有定义析构函数的时候,由系统自动生成的析构函数

33、迭代器,++it 和 it++ 的区别

  • 前者返回一个引用,后者返回一个对象
  • 前者不会产生临时对象,后者必须产生临时对象,临时对象会导致效率降低

34、左值引用和右值引用

  • 左值:可以获取地址的表达式,可以出现在赋值语句的左边,对该表达式进行赋值
  • 右值:无法获取地址的对象,如常量值、函数返回值、lambda 表达式等
  • 左值引用:传统的 C++ 引用就是左值引用
  • 右值引用:右值引用关联到右值时,右值被存储到特定位置,右值引用指定该特定位置。右值无法获取地址,但是右值引用是可以获取地址的,该地址表示临时变量的存储位置

35、什么是内存泄漏,如何避免和检测

  • 内存泄漏:一般指堆内存的泄露。堆内存是程序从堆中分配,大小任意,使用完必须释放。如果使用完没有释放,这块内存就不能被再次使用,即内存泄漏
  • 避免内存泄露的方法:
    • 计数法,使用 new 或者 malloc 时,该计数 +1,delete 或者 free 时,-1,程序结束时打印这个数,如果不为 0 则表示内存泄漏
    • 基类的析构函数一定要声明为虚函数
    • 对象数组的释放一定要用 delete[]
    • 有 new 就有 delete,有 malloc 就有 free=,保证成对出现
  • 检测工具:
    • Linux 下 valgrind 工具
    • Windows 下 CRT 库

36、对象复用,零拷贝

  • 对象复用:其本质是一种设计模式:flyweight 享元模式。通过将对象存储到对象池中实现对象的重复利用,这样可以避免多次创建对象的开销,节约系统资源
  • 零拷贝:一种避免CPU将数据从一块内存拷贝到另一块内存的技术,可以减少数据拷贝和共享总线操作的次数
  • C++ 的 vector 的成员函数 emplace_back() 和 push_back(),都是将一个元素插入容器尾部,但是 push_back() 需要调用拷贝构造函数和转移构造函数,而 emplace_back() 插入的元素原地构造,不需要出发拷贝构造和转移构造

37、面向对象三大特性

  • 继承:让某种类型对象获得另一个类型对象的属性和方法。常见的继承方式:
    • 实现继承:使用基类的属性和方法而无需额外编码
    • 接口继承:仅使用属性和方法名称,但是子类需要单独实现
    • 可视继承:子类使用基类的外观和代码实现
  • 封装:数据和方法捆绑在一起,比那面外界干扰和不确定性访问
  • 多态:同一事物表现出不同的能力,即面对同一信息,不同的对象在接收时会产生不同的行为
    • 重载实现编译时多态,虚函数实现运行时多态
    • 多态是一种允许把父类对象设置成和一个或者多个子对象相等的技术,赋值之后,父对象可以根据当前赋值根据子对象的特性进行不同的运行方式。即允许将子类类型的指针赋值给父类类型的指针
    • 实现多态的方式:
      • 覆盖,子类重新定义父类虚函数的方式
      • 重载,允许存在多个同名函数,这些函数的参数表不同

38、为什么成员初始化列表的方式初始化会更快呢

  • 成员初始化列表,在类的构造函数中,不在函数体内对成员变量赋值,而是在构造函数的花括号前面使用冒号和初始化列表赋值
  • 更快的原因:对于自定义类型,少了一次调用构造函数的过程;对于内置类型,没有差别。在构造函数内进行赋值,如果赋值对象是自定义类型,等于是一次构造函数和一次赋值,而初始化列表只做一次赋值操作

39、C++ 四种强制类型转换 reinterpret_cast/const_cast/static_cast/dynamic_cast

  • reinterpret_cast,reinterpret_cast (expression)
    • type-id 必须是一个指针、引用、算术类型、函数指针、成员指针,用于类型之间的强制转换
  • const_cast,const_cast (expression)
    • 用于修改类型的 const 或者 volition 属性,除了 const 或者 volition 修饰之外,type-id 和 expression 的类型是一样的,具体用法:
    • 常量指针被转换为非常量指针,并且仍然指向原来的对象
    • 常量引用被转换为非常量的引用,并且仍然指向原来的对象
    • const_cast 一般用来修改底指针,如 const char* p 形式
  • static_cast,static_cast (expression)
    • 用于把 expression 转换为 type-id 类型,但没有运行时类型检查来保证转换的安全性,主要用法:
      • 用于类层次结构中基类和派生类之间指针或者引用的转换
        • 上行转换(派生类转换成基类表示)是安全的
        • 下行转换(基类转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
      • 用于基本数据类型之间的类型转换,转换的安全与否由开发人员来保证
      • 用于空指针转换成目标类型的空指针
      • 把任何类型的表达式转换成 void 类型
      • static_cast 不能转换掉 expression 的 const、volition、__unaligned 属性
  • dynamic_cast,dynamic_cast (expression)
    • 有类型检查,下行转换比较安全,上行转换不太安全
    • 把 expression 转换成 type-id 的类型,type-id 必须是类的指针、引用、void*,且 expression 和 type-id 的形式应该一致,例如都是 指针
    • 在执行时确定真正的类型,那么 expression 必须是多态类型的
    • 如果下行转换是安全的,那么会返回一个转换过类型的指针;如果不安全,会返回空指针
    • 类型层次之间的上行转换时,和 static_cast 的效果是一样的;下行转换时,dynamic_cast 有动态类型检查,比 static_cast 更加安全

40、函数调用的压栈过程

  • 当入口函数 main 函数开始执行时,编译器会把操作系统的运行状态、main 函数的返回地址、main 的参数、main 函数中的变量等依次压栈
  • 当main 函数开始调用 func() 函数时,编译器会将 main 函数的运行状态进行压栈,再将 func() 函数的返回地址,func() 函数的参数从右到左、func() 定义变量依次压栈
  • 函数调用过程:
    • 从栈空间分配存储空间
    • 从实参的存储空间复制值到形参栈空间
    • 进行运算
  • 形参在函数未被调用之前都还是没有分配存储空间的;函数调用结束之后,形参弹出栈空间,清除形参空间
  • 数组作为参数的函数调用,传递方式是地址传递,形参和实参都指向相同的内存空间。调用完成之后,形参指针被销毁,但是所指向的内存空间不受影响

41、coredump 报错信息

  • coredump 是程序由于异常或者 bug 在运行时异常退出或者终止,在一定的条件下生成的一个叫做 core 的文件,这个 core 文件会记录程序在运行时的内存、寄存器状态、内存指针、函数堆栈信息

42、C++ 将临时变量作为返回值时的处理过程

  • 函数调用过程中,产生的临时变量被压入程序进程的栈中,函数退出时,临时变量出栈,临时变量此时被销毁。
  • 函数调用退出时,返回值被临时存放在寄存器中,没有放到堆或者栈中,即函数的返回值此时与内存没有关系了,在寄存器中,还可以使用。

43、如何获取结构体成员变量相对于结构体开头的字节偏移量

  • 使用 头文件中的 offsetof 宏

    struct S {
        int x;
        char y;
        int z;
        double a;
    };
    int main() {
        std::cout << offsetof(S, x) <<std::endl;	// 0
        std::cout << offsetof(S, y) <<std::endl;	// 4
        std::cout << offsetof(S, z) <<std::endl;	// 8
        std::cout << offsetof(S, a) <<std::endl;	// 12
        return 0
    }
    

44、静态类型、动态类型,静态绑定、动态绑定

  • 静态类型,对象在声明时指定的类型,在编译期间已经确定
  • 动态类型,通常指一个指针或者引用母亲所指向对象的类型,是在运行期间决定的
  • 静态绑定,绑定的是静态类型,所对应的函数或者属性依赖于对象的静态类型,发生在编译期间
  • 动态绑定,绑定的是动态类型,所对应的函数或者属性依赖于对象的动态类型,发生在运行期间
  • 非虚函数一般都是静态绑定,虚函数都是动态绑定。静态绑定不能实现多态,动态绑定能够实现多态,但是存在性能损失
  • 对象的静态类型不可以更改,对象的动态类型可以更改
  • 在继承体系中,只有虚函数使用的是动态绑定,其余都是静态绑定
  • 不要重新定义继承而来的非虚函数,因为这样会导致该函数调用在对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,会带来莫名其妙的 bug
  • 引用可以实现动态绑定。引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。这里只能是调用虚函数

45、指针加减运算

  • 指针加减运算本质上是对其所指的地址的移动,移动的步长和指针的类型有关系,要避免加多或者减多导致指针指向一块未知的内存地址

46、如何判断两个浮点数是否相等

  • 不可以使用 == 来判断,因为会出错。对于两个相等的数,直接 == 比较可能会返回 FALSE。
  • 两个浮点数的比较只能通过相减取绝对值并与预设的精度比较
  • 浮点数与 0 比较也需要有相同的注意,这些都和浮点数的表示方式有关

47、方法调用的原理,栈、汇编

  • 计算机使用栈来传递过程参数、存储返回信息、保存寄存器用以恢复以及本地存储。
  • 为单个过程分配的那部分栈为帧栈,帧栈可以认为是程序栈的一段,它有两个端点,分别标识起始地址、结束地址,两个指针:开始地址指针ebp 和 结束地址指针esp
  • 由一系列栈帧构成,这些栈帧对应一个过程,并且每一个帧指针 +4 的位置存储函数返回地址
  • 每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放
  • 由于栈帧是向地址递减的方向延伸的,因此将栈指针减去一定的值,相当于给栈帧分配了一定空间的内存;如果将栈指针加上一定的值,就相当于压缩了栈帧的长度,即释放了一定空间的内存。
  • 调用过程实现:
    • 备份原来的栈帧指针,调整当前的栈帧指针到栈指针位置
    • 建立起来的栈帧是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存
    • 使用建立好的栈帧,例如读取和写入,使用 mov、push、pop 等指令
    • 恢复被调用者寄存器当中的值,这一过程实际上是从栈帧中将备份的值恢复到寄存器中
    • 释放被调用者的栈帧,具体做法一般是将栈指针指向栈帧指针
    • 回复调用者的栈帧,实际上就是调整栈帧两端,使当前栈帧的区域恢复到原始位置
    • 弹出返回地址,跳出当前过程,继续执行调用者的代码

48、引用传递和指针传递的区别

  • 指针传递:本质是指针的值传递,被调函数会在栈中开辟内存空间来存放由主调函数传递进来的实参值
    • 被调函数对形参(指针)的操作,不会影响到实参(指针)的值
    • 可以通过形参(指针)来访问实参指向的内存,实参和形参是不同的两个地址变量,指向相同的内存
  • 引用传递,被调函数也会在栈中开辟内存空间,但是这时存放的是由主调函数放进来的是参变量的地址
    • 被调函数对形参的任何操作都会通过间接寻址,即通过栈 中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的实参本体)
    • 被调函数中对形参的任何操作都会影响到主调函数中的实参
  • 从编译器的角度来看,程序在编译时分别把指针和引用添加到符号表上,符号表中记录的是变量名即变量名所对应的地址
    • 指针变量在符号表上的地址值为指针变量的地址值,引用在符号表中的地址值为引用对象的地址(变量名和实参不同,地址相同)
    • 符号表生成之后不会再改变,因此指针变量可以改变其指向的对象,而引用对象不可以修改

49、类如何实现只能静态分配和只能动态分配

  • 前者,把 new、delete 重载为 private 属性
  • 后者,把构造函数、析构函数设为 protect 属性,再用子类来动态创建

50、如果某个类要被使用为基类,为什么这个类必须定义,而只是声明不可以

  • 派生类中包含并且可以使用他从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么

51、C++ 中的组合,相比于继承,有什么优点

  • 继承,优点是子类可以重写父类的方法来方便地实现对父类的拓展,缺点:
    • 父类内部细节对子类可见
    • 子类从父类继承的方法在编译的时候就确定了,无法在运行期间改变从父类继承的方法的行为
    • 如果父类的方法修改了,则子类的方法也必须做相应的修改。子类和父类之间高耦合
  • 组合,设计类的时候,把要组合的类的对象加入到该类中作为自己的成员变量,优点:
    • 当前对象只能通过所包含的那个对象去调用其方法,所以包含的对象的内部细节对当前对象是不可见的
    • 当前对象和包含的对象是低耦合的关系,如果修改包含对象中的类的代码,当前对象的类的diamante不需要修改
    • 当前对象可以在运行时动态绑定所包含的对象,可以通过 set 方法给所包含的对象赋值
    • 组合的缺点:
      • 容易产生过多的对象
      • 为了能够组合多个对象,必须仔细定义接口

52、函数指针

  • 函数指针,指向函数类型的指针。函数的类型由其返回的数据类型和参数列表共同决定的,函数名不是其类型的一部分。大部分情况下,函数的名字在后面不跟调用符号(括号)的时候,这个名字就是该函数的指针
  • 函数指针的声明
    • int (*pf) (const int&, const int&); 这里的 pf 就是一个函数指针
  • 函数与数据相似,函数也有地址
  • 一个函数名就是一个函数指针,它指向函数的代码。函数的地址就是该函数的入口,即调用函数的地址。函数的调用可以通过函数名,也可以通过函数指针。函数指针还允许将函数作为变元传递给其他函数
  • 函数指针赋值两种方法:
    • 指针名 = 函数名;
    • 指针名 = &函数名;

53、内存对齐的原因

  • 分配内存的顺序是按照声明顺序进行的
  • 每个变量相对于起始位置的偏移量必须是该类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
  • 最后整个结构体的大小必须是里面变量类型系最大的整数倍
  • 添加了 #pragma pack(n) 后规则变为:
    • 偏移量要是 n 和当前最大变量中较小值的整数倍
    • 整体大小是 n 和最大变量中较小值的整数倍
    • n 必须是 1,2,4,8,···,为其他值的时候,就按照默认规则进行分配

54、结构体变量比较是否相等

  • 重载 “==” 运算符

    struct foo {
        int a;
        int b;
        bool operator==(const foo& rhs) {
            return (a == rhs.a && b == rhs.b);
        }
    };
    
  • 逐个成员比较

  • 指针直接比较,如果保存的是同一个实例的地址,return (p1 == p2)

55、define、const、typedef、inline 的区别

  • #define 定义的是个常数不带类型;const 定义的常量是变量带类型
  • #define 只在预处理阶段起作用,简单的文本替换;const 在编译、连接过程中起作用;typedef 在编译阶段生效,有类型检查功能;inline 在编译阶段进行替换
  • #define 只是简单的字符串替换,没有类型检查;const 是有数据类型的,有类型检查,进行判断
  • #define 预处理后,占用代码段空间;const 占用数据段空间
  • #define 可以通过 #undef 取消某个符号的定义,进行重定义;const 不能重定义
  • #define 还可以用来防止文件重复引用
  • #define 可以用来为类型取别名,还可以定义常量、变量、编译开关;typedef用来定义类型别名,定义与平台无关的数据类型
  • #define 没有作用域的限制,只要是之前预定义过的宏,在后面的代码中都可以使用;typedef 有自己的作用域
  • inline 函数有类型检查,相比于宏定义更安全

56、printf 函数的实现原理

  • 在 C/C++ 中,对函数参数的扫描是从后向前的。printf 函数第一个找到的参数就是那个字符串指针,即被双引号括起来的那部分。函数通过判断字符串里控制参数的个数来判断参数的个数,以及每个参数的类型,进而计算出数据需要的堆栈指针的偏移量

57、为什么模板类一般都是放在一个 h 文件中

  • 模板定义比较特殊,有 template<…> 处理的东西,编译器都会不会在编译时为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器中,有相应的机制会去掉指定模板的多重定义
  • 在分离式编译环境下,编译器编译某一个 cpp 文件的时候,不知道另一个 cpp 文件的存在,也不会去查找,当遇到未决符号的时候编译器会寄希望于连接器。由于模板仅仅在需要的时候才会实例化出来,因此这种模式在没有模板的时候没问题,但是在遇到模板时就不行了。
  • 因此,当编译器只看到模板的声明的时候,它不能实例化模板,只能创建一个具有外部链接的符号并期待连接器能够将符号的地址给出来

58、cout 和 printf 的区别

  • cout << 是一个函数,并且针对各种数据类型进行了重载,因此后面可以跟各种类型,并自动识别数据的类型
  • cout<< 的输出过程会首先将字符输出到缓冲区,然后输出到屏幕,即 cout 有输出缓冲区
  • printf 是行缓冲输出,不是无缓冲输出

59、运算符重载

  • 只能重载已有的运算符
  • 对于重载的运算符,其优先级和结合律与内置运算符一致
  • 不可以改变运算符的操作数个数
  • 两种重载方式:
    • 成员运算符,成员运算符少一个操作数。下标运算符和箭头运算符必须是成员运算符
    • 非成员运算符

60、声明和定义的区别

  • 变量,声明不会分配内存,定义会分配内存

61、全局变量和static 变量的区别

  • 全局变量和静态变量都是静态存储,两者存储方式一致
  • 非静态全局变量的作用域是整个程序,而静态全局变量的作用域是所在的这个文件内

62、静态成员变量和普通成员变量的区别

  • 生命周期
    • 前者从类被加载到被卸载,一直存在
    • 后者只有在类创建对象之后才开始存在,对象结束,它的生命周期结束
  • 共享方式
    • 前者全类共享
    • 后者每个对象单独享用
  • 定义位置
    • 前者存储在静态全局区
    • 后者存储在堆栈中
  • 初始化位置
    • 前者在类外初始化
    • 后者在类内初始化
  • 可以使用静态成员变量作为默认实参

63、explicit 关键字

  • 在构造函数声明的时候,通过添加 explicit 关键字,能够禁止隐式转换
  • 如果构造函数只有一个参数,那么这个构造函数实际上是定义了一种转换为此类的隐式转换机制。可以在声明构造函数的时候,添加 explicit 关键字,来禁止隐式类型转换。这种默认的隐式转换只有在构造函数只有一个参数的时候存在。

64、不使用额外空间,交互两个数

// 算术
x = x + y;
y = x - y;
x = x - y;

// 异或,只能是 int 和 char
x = x ^ y;
y = x ^ y;
x = x ^ y;
x ^= x;
y ^= x;

65、strcpy 和 memcpy 的区别

  • 前者只可以复制字符串,后者可以复制任意内容
  • strcpy 不需要指定长度,它在遇到 ‘\0’ 的时候结束,容易溢出;后者根据第三个参数类指定复制的长度

66、空类会默认添加哪些函数

  • 默认构造函数
  • 拷贝构造函数
  • 析构函数
  • 重载赋值运算符

67、如何设计一个能够计算子类对象个数的类

  • 为类添加一个静态成员变量用于计数
  • 类定义结束之后初始化计数变量
  • 在构造函数、拷贝构造函数、赋值运算符重载中对计数变量进行 +1
  • 在析构函数中对计数变量进行 -1

68、如何阻止一个类被实例化

  • 将类定义为抽象类或者将构造函数声明为 private

69、如何禁止程序自动生成拷贝构造函数

  • 通过手动重写这个函数
  • 某些情况下,为了避免拷贝构造函数和拷贝赋值函数被调用,可以将他们设置为 private

70、模板 写一个比较大小的函数

#include
using namespace std;
template<typename type1, typename type2>	// 函数模板
type1 my_max(type1 a, type2 b) {
	return a > b ? a : b;
}
  • 上面的模板有个 bug,只有在 type1 和 type2 可以进行类型转换的时候才可以进行比较,否则 a > b 会报错。解决方法是重载 > ,这一步会比较繁琐

71、成员函数里面,memset(this, 0, sizeof(this*))

  • 在类里面定义了很多的 int,char,struct 等类型的变量的时候,习惯性将他们初始化为 0,但是逐个初始化太麻烦,可以在构造函数中直接使用 memset(this, 0, sizeof(this*)),将整个对象的内存全部置为 0。但是在下面的情况下不可以这么使用:
    • 类含有虚函数表,这么做会破坏虚函数表,后续对虚函数的调用会处出现异常
    • 类中包含 C++ 类型的对象,因为在构造函数体的代码之前,就对 C++ 类型的对象完成了初始化,此时该类型的构造函数给该对象分配了内存,这么做会对该对象的内存造成破坏

72、一致性哈希

  • 一致性哈希算法,就是在移除或者增加一个节点时,能够尽可能小的改变已经存在的映射关系
  • 一致性哈希将整个哈希值空间组织成一个虚拟的圆环,思想是使用相同的算法将数据和节点都映射到环形哈希空间中

73、C++ 从代码到可执行文件经历了什么过程

  • 预编译,主要处理源文件中 # 开头的预编译指令
    • 删除所有的 #define,展开所有的宏
    • 处理所有的条件预编译指令
    • 处理 #include 预编译指令,将文件内容替换到它的位置,这个过程是递归进行的
    • 删除所有注释
    • 保留所有的 #pragma 编译器指令,编译器需要用到这些指令,例如 #pragma once
    • 添加行号和文件标识,便于编译器编译时产生调试用的行号信息,编译时产生编译错误和警告等
  • 编译,把预编译之后的文件,进行一系列词法分析、语法分析、语义分析及优化之后,生成相应的汇编代码
    • 词法分析,利用类似于有限状态机的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系类的记号
    • 语法分析,语法分析器对由扫描器产生的记号进行语法分析,产生语法树。语法树是一种以表达式为节点的树
    • 语义分析,语法分析完成了对表达式语法层面的分析,语义分析器对表达式是否有意义进行判断,其分析的语义是静态语义,即在编译期间能够分析的语义,对应的动态语义是在运行期间才能确定的语义
    • 优化,源代码级别的优化过程
    • 目标代码生成,由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列,即汇编语言表示
    • 目标代码优化,目标代码优化器对上述的目标机器代码进行优化,寻找合适的寻址方式、使用移位来代替乘法、删除多余指令等
  • 汇编,将汇编代码转成机器位i可以执行的指令,即机器码文件。
    • 汇编过程是使用汇编指令和机器指令对照表一一翻译,生成目标文件
  • 链接,将不同的源文件产生的目标文件进行链接,从而形成一个可执行程序,分为静态链接和动态链接
    • 静态链接,函数和数据被编译进一个二进制文件。静态链接时吗,编译链接可执行文件的时候,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来,创建最终的可执行文件。
      • 浪费空间,更新困难,但是运行速度快
    • 动态链接,基本思想是把程序按照模块拆分成各个相对独立的部分,在程序运行时,再把他们链接在一起,形成一个完整的程序
      • 共享库节省空间,更新方便,性能损耗

74、静态编译和静态编译

  • 静态编译,在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,链接到可执行文件中,使可执行文件在运行时不需要依赖动态链接库
  • 动态编译,动态编译的可执行文件需要附带一个动态链接库,在执行时,只需要对用其对应的动态链接库中的命令进行调用。
    • 缩小了可执行文件的体积
    • 加快编译
    • 需要附带一个很大的完整的动态编译库
    • 如果运行程序的计算机上没有对应的库,就无法运行了

75、几种简单的锁

  • 读写锁
    • 多个读者可以同时读
    • 写者必须互斥,即只允许同时一个写者,读写不可同时进行
    • 写者优先于读者
  • 互斥锁
    • 一次只能有一个线程拥有互斥锁,其他线程只能等待
  • 自旋锁
    • 如果进程无法取得锁,进程不会立刻放弃 CPU时间片,而是一直尝试获取锁,是到获取到为止

76、为什么不能把所有函数都写成内联函数

  • 内联函数以代码复杂为代价,通过省去函数调用的开销来提高执行效率,以下情况不适合内联:
    • 函数体内代码比较长,将导致内存消耗大
    • 函数体内有循环,函数执行时间比函数调用开销大

77、虚函数表

  • 虚函数的实现有两部分:虚函数指针、虚函数表
    • 虚函数指针,本质是指向函数的指针。当子类调用虚函数的时候,实际上是通过该虚函数指针来找到接口。在一个实例化对象中,虚函数指针总是被放在该对象的地址的首位,且虚函数指针对外部完全不可见
    • 虚函数表,每个类的实例化对象都会拥有虚函数指针,并且都排列在对象的地址首部。他们按照一定的顺序组织起来,构成一种标桩结构,即虚函数表
      • 基类的虚函数表只记录自己定义的虚函数
      • 对于一半覆盖继承的子类,子类会对基类的虚函数进行覆盖继承,此时基类的虚函数表项依然保留,而正确继承的虚函数指针会被覆盖,子类的虚函数会在虚函数表后面延长虚函数表。当多重继承的时候,表项会增多,顺序体现为继承的顺序,并且子函数自己的虚函数将跟在第一个表项后面
      • C++ 的一个类公用一个虚函数表,基类有基类的虚函数表,子类有子类的虚函数表

你可能感兴趣的:(CPP,c++,开发语言)