整理一下与C++相关的一些常考知识点。

关键字

static

隐藏

未加static的全局变量和函数是全局可见的,当同时编译多个文件时,文件之间的变量或者函数可能会有命名冲突的情况,可以使用static来隐藏,使得变量或函数只能在该文件可见。

改变变量的存储方式和生命周期

static关键字会使得它修饰的变量存储在静态存储区,在程序开始运行是就初始化,并且会一直存在直到程序结束,并且不会被再次初始化。

对类成员声明static

同样有上述的几种功能,对该成员函数or静态变量进行隐藏以及改变其生命周期,并且所有的对象共用一个静态成员函数或静态变量。另外静态内部类也是实现单例模式的一种方法,其优点在于外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE。

const

const用来表示常量,即不允许他修饰的变量被修改,在进行参数传递的时候,也常常使用const修饰引用来传参,既保证了不复制参数,又防止对象被修改。
C++中还可以用const修饰成员函数和数据成员来达到封装的目的,不允许使用const修饰的成员函数来修改数据成员。

extern C

extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用(只是声明,不分配内存)

与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

被extern “C”修饰的变量和函数是按照C语言方式编译和链接的

之所以要如此是因为编译器在将C++源代码编译成目标文件时,会将函数和变量的名称进行修饰,形成符号名,目标文件中所使用的符号名就是修饰后名称,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数(重载)。因此有时候在调用一些库函数的时候,会出现无法链接的情况(因为函数签名不同)所以对于C++来说,必须使用extern “C”来声明这些函数。

底层原理

RTTI

RTTI是运行时类型检测,也是C++11的新特性,主要是在dynamic_casttypeid中使用,在进行类型转换前或者获取类型时,会查询type_info指针来进行类型检测。

cast转换

  • const_cast,用于将const变量转换为非const
  • static_cast,用于隐式转换,如非const转const,void*转指针等,能用于多态向上转化,向下不安全
  • static_cast,动态类型转换,可以用于层次间的向上向下转化,只能转指针和引用
  • reinterpret_cast,什么都可以转,可能会出问题

虚函数(多态)

多态及其实现条件

c++中的多态就是在父类的函数前加上virtual关键字,在子类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是子类,就调用子类的函数,如果对象类型是父类,就调用父类的函数。

多态的实现需要两个条件:

  • 虚函数重写,设置不同的状态
  • 对象调用虚函数时必须是指针或者引用

虚函数与重写

  • 虚函数是带有virtual关键字的成员函数
  • 子类有个和父类完全相同(函数名,形参,返回值都相同,协变和析构函数除外)的虚函数,就称子类虚函数重写父类虚函数

多态的原理

  • 多态是用虚函数表来实现的
  • 有虚函数的类都会生成一个虚函数表,在编译的时候使用
  • 虚函数表是一个存储虚函数地址的数组
  • 生成子类的虚函数表需要经过一下步骤
    • 将父类的虚函数表拷贝
    • 将子类中重写的虚函数覆盖掉父类中的虚函数
    • 如果有新增加的虚函数则放到表的最后
  • 在调用时会根据对象中的虚表指针来找实际应该调用的函数

其他问题

  • 虚函数和普通函数一样在代码段,虚表在只读常量区
  • inline函数(内联函数)没有地址,无法放到虚函数表中
  • 静态成员不能是虚函数,因为静态成员函数没有this指针,因为有this指针才能访问到虚表指针,有虚表指针才能找到虚表从而调用实际应该调用的函数
  • 构造函数不能是虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
  • 析构函数可以是虚函数,并且最好把基类的析构函数定义成虚函数,当父类指针指向子类对象时,如果析构函数不是虚函数,析构就只会释放父类对象,造成内存泄漏
  • 普通对象访问普通函数和访问虚函数是一样快的(不会触发多态);指针对象或者是引用对象,调用普通函数更快一些,因为构成了多态,运行时调用虚函数要先到虚函数表中去查找。这样然后才拿到韩式的地址,这样就不如直接可以拿到函数地址的普通函数快。

内存管理与内存泄露

可以通过静态扫描来检测内存泄露,具体的操作为对象计数;重载new/delete,记录分配点等

解决内存泄露可以使用智能指针,但要小心循环引用的问题。

C++11

auto

auto实际上很早就被引入到C++,用来进行类型推导,比如用在迭代器中。

nullptr

传统的C++无法区分NULL和0,有的编译器会把NULL定义为void*的指针,有的直接就定义为0了,这就会导致一系列的问题,比如在重载时会发生混乱,所以C++11引入了nullptr来区分空指针和0。

基于范围的for循环

进行遍历的时候可以像python一样写auto iter:arr来遍历。

初始化列表

提供了统一的语法来初始化任意的对象,这两个特性就让人感觉C++越来越智能而简洁了。

智能指针

C++11引入智能指针主要是因为C++没有自动的内存回收机制,所以当业务逻辑过于复杂的时候难免会出现内存没有delete的现象,这就容易造成内存泄漏的问题,智能指针主要就是被用来对这些资源进行动态管理,并及时释放无用的资源。

C++11提供四种智能指针,有unique_ptr,shared_ptr, weak_ptr,以及auto_ptr,不过auto_ptr已经被弃用。

shared_ptr允许多个指针指向相同的对象,每次被引用就内部计数+1,析构则-1,当引用计数为0时则释放内存。unique_ptr只允许一个指针指向给定的对象,weak_ptr最大的作用在于协助shared_ptr,可以观测目标对象的引用计数。

智能指针会遇到的最大问题是循环引用,就像是类之间的循环引用一样,两个指针互相引用对方就会造成都无法析构的现象。循环引用的现象可以通过让引用链上的一方持用普通指针或弱智能指针(weak_ptr)来解决。

实现智能指针可以通过引入辅助类或者使用句柄。
辅助类实现即单独使用一个类来存储引用计数,每次在构造和析构智能指针类的时候对辅助类进行操作。
而句柄实现则是把指针封装起来,通过重载来重新定义指针的行为,将计数内置进去。

右值引用

C++11引入右值引用的目的是解决拷贝临时对象时的性能低下问题,在C++11之前,进行临时对象拷贝时会多调用一次构造函数并复制,这就使得性能低下。实际上直接移动变量的指向就会快得多,但C++11之前右值是不能被取地址的,所以没法进行右值引用。

从本质上来说,左值引实际上是用const指针来实现的,它保存的是一个指向数据地址的指针,而如果进行反汇编可以发现右值引用是通过取地址来实现的,一个变量的右值引用存储的就是它的地址。

而对于传入的引用,在不知道是左值还是右值的情况下,可以用move函数来将其转换为右值,也就是实现了取地址的操作。

新增STL容器array以及tuple(元组)

array保存在栈内存中,相比堆内存中的vector,速度更快,但array长度无法更改,没有vector灵活。

而新增了tuple感觉就是C++在像python看齐