C++整理

智能指针

为什么要使用智能指针?

​ 为了使指针在生命周期结束时,不仅仅释放存放指针的内存,也释放其指向的内存,防止内存泄漏。因此将其包装为一个对象,这样在生命周期结束时就可以通过调用析构函数来释放内存。

智能指针的四种方案

auto_ptr

auto_ptr 采取所有权的形式来实现,但 C++11 已经抛弃,原因在于如果出现以下情况:

auto_ptr<string> p1 = (new string("hello"));
auto_ptr<string> p2;
p2 = p1;

​ 此时如果再次调用p1就会产生访问空指针的错误,但在编译阶段并不会报错,所以 C++11 采取了替代措施。

unique_ptr

unique_str 保证了同一时间只有一个智能指针可以指向该对象,如果出现如下情况:

unique_ptr<string> p1 = (new string("hello"));
unique_ptr<string> p2;
p2 = p1;

​ 此时在编译阶段就会报错,因为 unique_ptr 不允许这样的操作。

shared_ptr

shared_ptr 则采取计数机制来记录当前有多少个智能指针指向该对象,直到最后一个智能指针被释放时,才会释放分配了的空间。

weak_ptr

weak_ptr 并不控制对象的生命周期,它指向一个 shared_ptr 管理的对象,它主要用于配合 shared_ptr ,解决两个 shared_ptr 互相引用时的死锁。

C++内存模型

​ C++内存分区主要分为堆、栈、代码区、全局区、常量区。

代码区

  • 代码区存放程序的二进制代码供CPU执行
  • 代码区是共享的
  • 代码区是只读的

全局区

​ 全局区分为已初始化和未初始化两个区域,存放全局变量和静态变量。

​ 堆由程序员进行内存维护和释放,通过使用 malloc/freenew/delete 来申请和销毁内存所在的区域。

​ 堆由低地址向高地址延申。

​ 栈由编译器自动分配释放,存放函数的参数值、局部变量、形参等。

​ 函数栈入参顺序:

​ 函数局部变量 $\rarr$ 最右参数 $\rarr$ 中间参数 $\rarr$ 最左参数 $\rarr$ 返回地址 $\rarr$ 运行时参数

常量区

​ 存放常量的区间,一旦初始化后就不能修改。

C++参数传递

值传递

​ 按值传递时,被调函数会创建一个实参的副本来存放传递进来的值,因此在被调函数中做的任何操作都不会对主调函数传递进来的实参有作用。

指针传递

​ 地址传递的本质也是传值,只是它所传递的是指针指向的地址,被调函数同样也会创建一个同样类型的指针副本指向同一个地址,由于现在的操作是通过寻址进行的,所以在被调函数中任何对于存放在该地址的变量的操作都会影响到主调函数传递进来的实参。

引用传递

​ 引用传递传递的是变量本身的地址,同样在被调函数中的操作会影响到主调函数传递进来的实参。

指针传递与引用传递的不同

​ 指针传递本质仍是值传递,它对于形参的操作都不会反映于实参;引用传递是地址传递,它对于形参的操作都会通过间接寻址的方式操作到实参上。

const

const 修饰基本数据类型

  • 基本数据类型,修饰符 const 可以⽤在类型说明符前,也可以⽤在类型说明符后, 其结果是⼀样的。
  • const 可以定义常量,定义的变量只有类型为整数或者枚举,且以常量表达式初始化时才能作为常量表达式。
  • const 定义的常量在程序运行过程中只有一份拷贝
  • const 常量默认为文件具备变量,在不同文件访问时需要显式声明 extern

const 修饰指针与引用

  • const 位于 $*$ 的左侧,就代表用来修饰指针所指向的变量,即指针指向常量
  • const 位于 $*$ 的右侧,就代表用来修饰指针本身,即指针本身是常量
  • 可以将非 const 对象的地址赋给 const 对象的指针,这样指向 const 对象的指针就无法修改这个非 const 对象

const 修饰函数

  • 参数为引用时:如果参数为非内部数据类型,一般需要将参数声明为引用,这样不会创建一个临时对象,花费额外的时间进行对象的构造、复制、析构,如果此时希望该对象不被改变,则可以加上 const 修饰。
  • 参数为指针时:同修饰指针的第三条,如果希望指针所指内容不被修改,可以加上 const 修饰

const 修饰类

  • const 成员变量只在某个对象生命周期内时常量,对于整个类而言时可以改变的。const 成员的初始化只能在类的构造函数中以初始化列表的方式进行。
  • const 成员函数用于防止成员函数修改对象的内容。
  • const 修饰类对象只能调用常量函数,别的成员函数都不能调用。

static

  • static 成员变量属于整个类所有,对类的所有对象只有一份拷贝
  • static 成员函数属于整个类所有,不接受 this 指针,所以只能访问类的 static 成员变量。
  • static 类对象必须要在类外进行初始化,因为 static 修饰的变量先于对象存在。
  • static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual 没有任何实际意义。

重载、重写、重定义

重载

​ 重载是指同⼀可访问区内被声明的几个具有不同参数列表的同名函数,依赖于 C++ 函数名字的修饰会将参数加在后⾯,可以是参数类型,个数,顺序的不同。根据参数列表决定调⽤哪个函数,重载不关心函数的返回类型。

重写

​ 派⽣类中重新定义父类中除了函数体外完全相同的虚函数,被重写的函数不能是 static 的, ⼀定要是虚函数,且其他⼀定要完全相同。要注意,重写和被重写的函数是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派⽣类中重写可以改为 public

重定义

​ 派⽣类重新定义父类中相同名字的非 virtual 函数,参数列表和返回类型都可以不同,即父类中除了定义成 virtual 且完全相同的同名函数才不会被派⽣类中的同名函数所隐藏。

构造函数与析构函数

四种基本构造函数

无参数构造函数

​ 即默认构造函数,如果没有明确写出无参数构造函数,编译器会自动生成默认的无参数构造函数,也可以自己显示定义一个无参数构造函数。

一般构造函数

​ 一般构造函数可以有各种参数形式,即可以是重载的,创建对象时根据传入参数不同调用不同的构造函数。

拷贝构造函数

​ 拷贝构造函数参数为对象本身的引用,用于复制一个已存在对象的副本,会将其中的值一一复制到新创建的对象中。

类型转换构造函数

​ 根据一个指定类型的对象创建一个本类的对象。需要构造函数只有一个参数,如果想要关闭隐式转换,可以加关键字 explict 修饰。

构造函数特性

​ 构造函数一般不定义为虚函数,原因在于虚函数的调用是需要实例化之后对象的虚函数表指针来实现的,如果构造函数是虚函数,那么虚函数表指针是不存在的,无法实现调用。

​ 构造函数的执行顺序

  • 基类构造函数。如果有多个基类则按照派生表的顺序
  • 成员类对象构造函数,如果有多个成员类则按照在类中被声明的顺序
  • 派生类构造函数

析构函数特性

​ 析构函数一般定义为虚函数,原因在于在析构实际对象时,可以根据运行时的类型正确地析构对象,而不是只调用基类的析构函数,导致派生类没有被析构,造成内存泄漏。

​ 析构函数执行顺序:

  • 派生类的析构函数
  • 成员类对象的析构函数
  • 基类的析构函数

虚函数

多态

C++ 的多态,一般指在基类的函数前加上 virtual 关键字,在派生类中重写该函数。在运行时就会根据对象的实际类型来调用相应的函数。

​ 当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址。派生类继承了基类自然也会有自己的虚函数表,因此会为其生成一个虚函数指针指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。

虚函数表

​ 虚函数表的建立步骤:

  • 拷贝基类的虚函数表,如果是多继承就拷贝每个有虚函数基类的虚函数表
  • 查看派生类中是否有重写基类中的虚函数,如果有,就替换成已经重写的虚函数地址;查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中。(派生类的自身内容追加在主基类的内存块后)