C/C++ 知识总结(更新中)

  1. 1. 前言
  2. 2. 大纲
  3. 3. 基本语法
    1. 3.1. 数据类型与声明
    2. 3.2. 关键字和标识符
    3. 3.3. 常量与变量
      1. 3.3.1. static 关键字的作用
      2. 3.3.2. const 关键字的作用
      3. 3.3.3. extern 关键字的作用
      4. 3.3.4. volatile 关键字的作用
      5. 3.3.5. 变量作用域
    4. 3.4. 运算符
  4. 4. 基本词法
    1. 4.1. 数组
    2. 4.2. 字符串
    3. 4.3. 引用与指针
      1. 4.3.1. 指针
      2. 4.3.2. 引用
      3. 4.3.3. this 指针
    4. 4.4. 内存管理
      1. 4.4.1. malloc 和 free 函数
      2. 4.4.2. new 和 delete 操作符
  5. 5. 基本句法
    1. 5.1. 赋值、判断、循环
    2. 5.2. 结构体
      1. 5.2.1. 内存对齐
    3. 5.3. 函数
      1. 5.3.1. inline 关键字
    4. 5.4. 类和对象
      1. 5.4.1. 概述
      2. 5.4.2. 构造函数和析构函数
      3. 5.4.3. 访问修饰符
      4. 5.4.4. 友元函数
      5. 5.4.5. struct 和 class 的区别
    5. 5.5. 命名空间
  6. 6. 高级语法
    1. 6.1. 类型转换
      1. 6.1.1. 隐式类型转换
      2. 6.1.2. 强制类型转换
    2. 6.2. 异常处理
    3. 6.3. I/O 设计
    4. 6.4. 文件操作
    5. 6.5. 多线程
  7. 7. C++ 编程
    1. 7.1. C 和 C++ 的区别
    2. 7.2. 重载
      1. 7.2.1. 函数重载
      2. 7.2.2. 运算符重载
    3. 7.3. 深拷贝与浅拷贝
    4. 7.4. 封装
    5. 7.5. 继承
    6. 7.6. 多态
      1. 7.6.1. 概述
      2. 7.6.2. 虚函数
      3. 7.6.3. 纯虚函数
    7. 7.7. 模板编程
  8. 8. C++ 标准库
    1. 8.1. STL 容器
    2. 8.2. STL 的 sort 函数
    3. 8.3. 仿函数
    4. 8.4. 迭代器
    5. 8.5. 适配器
  9. 9. C++ 11
    1. 9.1. auto 关键字
    2. 9.2. 智能指针
      1. 9.2.1. auto_ptr
      2. 9.2.2. unique_ptr
      3. 9.2.3. shared_ptr
      4. 9.2.4. weak_ptr
    3. 9.3. 右值引用
    4. 9.4. 列表初始化
    5. 9.5. Lambda 表达式

前言

  关于 C/C++ 知识体系的总结。

大纲

  • 基本语法
    • 数据类型与声明
    • 关键字和标识符
    • 常量与变量
    • 运算符
    • 表达式
  • 基本词法
    • 数组
    • 字符串
    • 各种指针
    • 引用
    • 内存管理
  • 基本句法
    • 赋值、判断、循环
    • 结构体
    • 函数
    • 类和对象
    • 命名空间
  • 高级语法
    • 类型转换
    • 异常处理
    • I/O 设计
    • 文件操作
    • 多线程
  • C++ 编程
    • C 和 C++ 的区别
    • 运算符重载
    • 深拷贝与浅拷贝
    • 封装
    • 继承
    • 多态
    • 模板编程
  • C++ 标准库
    • STL 容器
    • 仿函数
    • 迭代器
    • 适配器
  • C++ 11
    • auto 关键字
    • 智能指针
    • 右值引用
    • 列表初始化
    • Lambda 表达式

基本语法

数据类型与声明

  • 基本数据类型
    • bool 布尔型
    • char 字符型
    • int 整型
    • float 浮点型
    • double 双精度浮点型
    • void 无类型
    • wchar_t 宽字符型
  • 类型修饰符(一些基本类型可以使用一个或多个类型修饰符进行修饰)
    • signed
    • unsigned
    • short
    • long
  • 变量声明
    • type variable_list;
    • type variable_name = value;

关键字和标识符

  • 常用关键字
    • 上面提到的基本数据类型和类型修饰符
    • class / struct / union 类 / 结构体 / 联合体
    • const / static / extern / volatile 变量修饰符
    • return 函数返回
    • true / false 布尔值真 / 假
    • if / else / switch / case / default 条件语句
    • for / while / do / continue / break 循环语句
    • enum 枚举类型
    • try / throw / catch 异常处理
    • new / delete 动态内存分配
    • using / namespace 命名空间
    • nullptr 空指针
    • sizeof 返回数据对象所占空间大小
    • public / protected / private 访问说明符
    • template / typename 模板类
    • virtual 虚类
    • typedef 类型别名
    • operator 运算符重载
  • 标识符
    • 标识符是程序员给变量、类、函数或其他实体的唯一名称。
    • 标识符命名规则
      • 标识符可以由字母,数字和下划线字符组成。
      • 它对名称长度没有限制。
      • 它必须以字母或下划线开头。
      • 区分大小写。
      • 我们不能将关键字用作标识符。
    • 我们应该为有意义的标识符提供有意义的名称。

常量与变量

  • 变量
    • 在编程中,变量是用于保存数据的容器(存储区)。
    • 为了表示存储区域,应该为每个变量赋予唯一的名称(标识符)。
    • 变量的值可以更改。
  • 字面量
    • 字面量用于表示固定值的数据。它们可以直接在代码中使用。
    • 整型字面量、浮点字面量、字符字面量、转义字符字面量、字符串字面量。
  • 常量
    • 我们使用 const 关键字来定义一个常量。
    • 我们还可以使用 #define 预处理器指令创建常量。
    • 常量的值不能更改。

static 关键字的作用

  • 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  • 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  • 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  • 修饰成员函数,表示该函数属于一个类而不是属于此类的任何特定对象,类和对象不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

const 关键字的作用

  • 修饰变量,说明该变量不可以被改变;
  • 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
  • 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  • 修饰成员函数,说明该成员函数内不能修改成员变量。
  • const 的指针与引用
    • 指针
      • 指向常量的指针(pointer to const)const type* p;
      • 自身是常量的指针(常量指针,const pointer)type* const p;
    • 引用
      • 指向常量的引用(reference to const)const type& p;
      • 没有 const reference,因为引用只是对象的别名,不是对象,不能用 const 修饰

extern 关键字的作用

  • extern 用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
  • extern 声明不是定义,即不分配存储空间。
  • 我们也可以在头文件中声明一个变量,在用的时候包含这个头文件就声明了这个变量。为什么还要使用 extern 关键字呢?因为用 extern 引用另一个文件的范围更小,会加速程序的编译过程,这样能节省时间。
  • 在 C++ 中 extern 还有另外一种作用,用于指示 C 或者 C++ 函数的调用规范。比如在 C++ 中调用 C 库函数,就需要在 C++ 程序中用 extern "C" 声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用 C 函数规范来链接。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。

volatile 关键字的作用

  • C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的内存屏障。
  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。
  • 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
  • 一般说来,volatile 用在如下的几个地方:
    • 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
    • 多任务环境下各任务间共享的标志应该加 volatile;
    • 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义。
  • volatile 并不能保证线程安全,因为 volatile 并不能保证变量操作的原子性。

变量作用域

  • 作用域是程序的一个区域,一般来说有三个地方可以定义变量:
    • 在函数或一个代码块内部声明的变量,称为局部变量。
    • 在函数参数的定义中声明的变量,称为形式参数。
    • 在所有函数外部声明的变量,称为全局变量。
  • 局部变量:只能被函数内部或者代码块内部的语句使用。
  • 全局变量:
    • 全局变量的值在程序的整个生命周期内都是有效的。
    • 全局变量可以被任何函数访问。全局变量一旦声明,在整个程序中都是可用的。
    • 在程序中,局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。

运算符

  • 运算符类型
    • 算术运算符:+、-、*、/、%、++、—
    • 赋值运算符:=、+=、-+、*=、/=、%=
    • 关系运算符:==、!=、>、<、>=、<=
    • 逻辑运算符:&&、||、!
    • 按位运算符:&、|、^、~、<<、>>
    • 其他运算符:sizeof、?:、,、.、->、&(地址)、*(指针)

基本词法

数组

字符串

引用与指针

指针

  • 指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。
  • 所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。
  • 不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

引用

  • 引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
  • 引用很容易与指针混淆,它们之间有三个主要的不同:
    • 不存在空引用。引用必须连接到一块合法的内存。
    • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
    • 引用必须在创建时被初始化。指针可以在任何时间被初始化。
  • 把引用作为参数:C++ 支持把引用作为参数传给函数,这比传一般的参数更安全。
  • 把引用作为返回值:可以从 C++ 函数中返回引用,就像返回其他数据类型一样。

this 指针

  • this 指针是隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。
  • 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。
  • 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
  • this 指针被隐含地声明为 ClassName *const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为 const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作)。
  • this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
  • 在以下场景中,经常需要显式引用 this 指针:
    • 为实现对象的链式引用;
    • 为避免对同一对象进行赋值操作;
    • 在实现一些数据结构时,如 list。

内存管理

malloc 和 free 函数

  • 在 C 语言中只能通过 malloc() 和其派生的函数进行动态的申请内存,而实现的根本是通过系统调用实现的。free() 函数释放由其分配的内存。
  • malloc 函数是从堆里面获得了空间。
  • 此外,堆中的内存块总是成块分配的,并不是申请多少字节,就拿出多少个字节的内存来提供使用。堆中内存块的大小通常与内存对齐有关。
  • free(p) 释放的是指针变量 p 所指向的内存,而不是指针变量 p 本身。指针变量 p 并没有被释放,仍然指向原来的存储空间,这时候 p 是个野指针。

new 和 delete 操作符

  • new 和 delete 是 C++ 中关于管理内存的操作符。
  • malloc / free 只是动态分配内存空间 / 释放空间。而 new / delete 除了分配空间还会调用构造函数和析构函数进行初始化与清理(清理成员)。
  • 它们都是动态管理内存的入口。
  • malloc / free是 C/C++ 标准库的函数,new / delete 是 C++ 操作符。
  • malloc / free 需要手动计算类型大小且返回值为 void*(需要进行强制类型转换),new / delete 可自动计算类型的大小,返回对应类型的指针。
  • malloc / free 管理内存失败会返回 0,new / delete 等的方式管理内存失败会抛出异常。
  • 实际上 new 和 delete 只是 malloc 和 free 的一层封装。
  • 他们都需要配套使用。

基本句法

赋值、判断、循环

结构体

内存对齐

  • 结构体中可以存放不同类型的数据,它的大小也不是简单的各个数据成员大小之和,限于读取内存的要求,而是每个成员在内存中的存储都要按照一定偏移量来存储,根据类型的不同,每个成员都要按照一定的对齐数进行对齐存储,最后整个结构体的大小也要按照一定的对齐数进行对齐。
  • 对齐规则
    • 第一个成员在与结构体变量偏移量为 0 的地址。
    • 其他成员变量要对齐到对齐数(对齐数 = 编译器默认的一个对齐数与该成员大小的较小值)的整数倍的地址处。
    • Linux 中默认对齐数为 4,vs 中的默认值为 8。
    • 结构体总大小为最大对齐数的整数倍(每个成员变量除了第一个成员都有一个对齐数)。
    • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数的整数倍(含嵌套结构体的对齐数)。
  • 对齐特点:
    • 每个成员的偏移量 % 自己的对齐数 = 0。
    • 结构体整体大小 % 所有成员最大对齐数 = 0。
    • 结构体的对齐数是自己内部成员的对齐数中的最大对齐数。
  • 为什么要内存对齐?
    • 平台移植型好。
    • CPU 处理效率高。

函数

inline 关键字

  • 为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。
  • 引入内联函数的主要原因是用它替代 C 中表达式形式的宏定义。
  • 宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受 C++ 编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
  • 在 C++ 中引入了类及类的访问控制,这样,如果一个操作或者说一个表达式涉及到类的保护成员或私有成员,你就不可能使用这种宏定义来实现(因为无法将 this 指针放在合适的位置)。
  • inline 的使用是有所限制的,inline 只适合函数体简单的函数使用,不能包含复杂的结构控制语句例如 while、switch,并且内联函数本身不能是直接递归函数。
  • inline 对于编译器只是一个建议,编译器会根据实际情况选择是否设置当前函数为内联函数。

类和对象

概述

  • 定义一个类(class),本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。
  • 类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。
  • 类成员函数:指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。
  • 构造函数:一种特殊的函数,在创建一个新的对象时调用。
  • 析构函数:一种特殊的函数,在删除所创建的对象时调用。
  • 拷贝构造函数:一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
  • 友元函数:可以访问类的 private 和 protected 成员。
  • 内联函数:通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。
  • 指向类的指针:如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。

构造函数和析构函数

  • 构造函数
    • 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
    • 默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。
  • 析构函数
    • 析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
  • 构造函数不能是虚函数。
    • 若构造函数是虚的,则需要通过虚函数表来调用,若对象还未实例化,即还未分配内存空间,无法找到虚函数表。
    • 虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数就没有实际意义。
  • 析构函数应当是虚函数。
    • 析构时将调用相应对象类型的析构函数。
    • 如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

访问修饰符

  • public:公有成员。任何位置都可以访问。
  • protected:保护成员。只有类自己和子类的成员函数以及友元函数才能访问。
  • private:私有成员。只有类自己的成员函数和友元函数才能访问。
  • C++ 的限定符是限定类的,不是限定对象的,只要是类型相同就能相互访问。

友元函数

  • 类的友元函数是定义在类外部,但有权访问类的所有私有成员和保护成员的函数。
  • 尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
  • 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
  • 如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。

struct 和 class 的区别

  • 默认的继承访问权限。struct 是 public 的,class 是 private 的。
  • struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
  • 总的来说,struct 更适合看成是数据结构的实现体,class 更适合看成是对象的实现体。

命名空间

  • 使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突。
  • 在 C++ 中,变量、函数和类都是大量存在的。如果没有命名空间,这些变量、函数、类的名称将都存在于全局命名空间中,会导致很多冲突。
  • 命名空间的定义使用关键字 namespace,后跟命名空间的名称。
    • namespace name { // 代码声明 }
  • 为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称。
    • name::code; // code 可以是变量或函数
  • 可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
    • using namespace std; 使用 C++ 标准库。

高级语法

类型转换

隐式类型转换

  • 当运算符的操作数具有不同的数据类型时,C++ 会自动将它们转换为相同的数据类型。
  • 当一个值被转换为更髙的数据类型时,称之为升级。反之,降级则意味着将其转换为更低的数据类型。
  • char、short 和 unsigned short 值自动升级为 int 值。
  • 当运算符使用不同数据类型的两个值时,较低排名的值将被升级为较高排名值的类型。
  • 当表达式的最终值分配给变量时,它将被转换为该变量的数据类型。
  • 如果接收值的变量的数据类型低于接收的值,值将被降级为变量的类型。如果变量的数据类型没有足够的存储空间来保存该值,则该值的一部分将丢失。
  • 当变量值的数据类型更改时,它不会影响变量本身。

强制类型转换

  • static_cast
    • 用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。
    • static_cast <type_id> (expression)
    • 用于类层次结构中基类和派生类之间指针或引用的转换。
      • 进行上行转换(把派生类的指针或引用转换成基类表示),是安全的。
      • 进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的。
      • 只能在有相互联系的类型中进行相互转换,不一定包含虚函数。
    • 用于基本数据类型之间的转换。这种转换的安全需要开发人员来保证。
    • 把空指针转换成目标类型的空指针。
    • 把任何类型的表达式转换为 void 类型。
    • static_cast 不能转换掉表达式的 const、volitale 或者 __unaligned 属性。
  • const_cast
    • 用来移除变量的 const 或 volatile 限定符。
    • const_cast <type_id> (expression)
    • 不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
    • 常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
    • 经过转换并修改值之后,常量的值不会被修改,指针取值会修改,而它们指向的地址是相同的。(这里我黑人问号,不是很懂为什么会这样,找到的资料都是说这样是无奈之举,并没有说为什么可以地址一样值不一样。)
    • 移除 const 限定后通过指针修改值是一种未定义行为,具体操作取决于编译器。
    • 所以说,这种用法最好只用在迫不得已的地方(比如函数参数类型不一样,只能转换),尽量不要去修改值。
  • reinterpret_cast

异常处理

I/O 设计

文件操作

多线程

C++ 编程

C 和 C++ 的区别

重载

  • 在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
  • 重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。

函数重载

  • 在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。
  • 不能仅通过返回类型的不同来重载函数。

运算符重载

  • 大部分 C++ 内置的运算符都可以被重定义或重载。
  • 重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
  • 不可重载的运算符列表:
    • .:成员访问运算符
    • .*, ->*:成员指针访问运算符
    • :::域运算符
    • sizeof:长度运算符
    • ?::条件运算符
    • #: 预处理符号

深拷贝与浅拷贝

封装

  • 封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。
  • 数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
  • 通常情况下,我们都会设置类成员状态为私有,除非我们真的需要将其暴露,这样才能保证良好的封装性。
  • 这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。

继承

  • 继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
  • 当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
  • 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。
  • 一个派生类继承了所有的基类方法,但下列情况除外:
    • 基类的构造函数、析构函数和拷贝构造函数。
    • 基类的重载运算符。
    • 基类的友元函数。
  • 一般形式:class 派生类名: 访问修饰符 基类名
  • 如果未使用访问修饰符,则默认为 private。
  • 我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
    • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
    • 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
    • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

多态

概述

  • 多态按字面的意思就是多种形态,可以简答地概括为 “一个接口,多种方法”。调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
  • 静态多态
    • 静态多态也称为编译时多态。
    • 静态多态通过重载和模板实现。
  • 动态多态
    • 动态多态也称为运行时多态。
    • 动态多态通过虚函数实现。

虚函数

  • 虚函数的实现是由两个部分组成的,虚函数指针与虚函数表。
  • 虚函数指针
    • 虚函数指针从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。
    • 虚函数指针是确实存在的数据类型,在一个被实例化的对象中,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。与对象的成员不同,虚函数指针对外部是完全不可见的,除非通过直接访问地址的做法或者在 DEBUG 模式中,否则它是不可见的也不能被外界调用。
  • 虚函数表
    • 每个类的实例化对象都会拥有虚函数指针并且都排列在对象的地址首部。而它们也都是按照一定的顺序组织起来的,从而构成了一种表状结构,称为虚函数表。
    • 虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面,子类覆盖父类的函数直接放到父类函数原来的位置中。
    • 对于多重继承,每个父类都有自己的虚表,子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是按照声明顺序来判断的)。
  • 函数的声明与定义要求非常严格,只有在子函数中的虚函数与父函数一模一样的时候(包括限定符)才会被认为是真正的虚函数,不然的话就只能是重载。这被称为虚函数定义的同名覆盖原则,意思是只有名称完全一样时才能完成虚函数的定义。

纯虚函数

  • 虚函数的定义形式:virtual {method body}
  • 纯虚函数的定义形式:virtual { } = 0;
  • 在虚函数和纯虚函数的定义中不能有 static 标识符,原因很简单,被 static 修饰的函数在编译时候要求前期 bind,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
  • 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
  • 虚函数可以被直接使用,也可以被子类重载以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类只有声明而没有定义。
  • 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
  • 虚函数和纯虚函数通常存在于抽象基类中,被继承的子类重载,目的是提供一个统一的接口。
  • 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。
  • 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。
  • 抽象类的子类也可以是抽象类。

模板编程

  • 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
  • 函数模板
1
2
3
4
template <typename type>
ret-type func-name (parameter list) {
// 函数的主体
}
  • 类模板
1
2
3
4
template <class type>
class class-name {
// 类主体
}

C++ 标准库

STL 容器

STL 的 sort 函数

  • 对于STL中的sort()算法:
    • 当数据量大时,将会采用 Quick Sort(快排),分段递归进行排序。
    • 一旦分段后的数据量小于某个阈值,为了避免快排的递归带来过大的额外的开销,sort()算法就自动改为 Insertion Sort(插入排序)。
    • 如果递归的层次过深,还会改用 Heap Sort(堆排序)。
  • 简单来说,sort并非只是普通的快速排序,除了对普通的快排进行优化,它还结合了插入排序和堆排序。
  • 根据不同的数量级以及不同的情况,能够自动选择合适的排序算法。

仿函数

迭代器

适配器

C++ 11

auto 关键字

  • auto 可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型。
  • auto 的自动类型推断发生在编译期,所以使用 auto 并不会造成程序运行时效率的降低。
  • auto 声明的变量必须要初始化,否则编译器不能判断变量的类型。
  • auto 不能被声明为返回值,不能作为形参,不能被修饰为模板参数。
  • 定义在一个 auto 序列的变量必须始终推导成同一类型。
  • 使用 auto 关键字做类型自动推导时,依次施加以下规则:
    • 如果初始化表达式是引用,则去除引用语义。
    • 如果初始化表达式为 const 或 volatile(或者两者兼有),则除去 const / volatile 语义。
    • 如果 auto 关键字带上 & 号,则不去除 const 语意。
    • 初始化表达式为数组时,auto 关键字推导类型为指针。
    • 若表达式为数组且 auto 带上 &,则推导类型为数组类型。

智能指针

  • 四个智能指针: auto_ptrunique_ptrshared_ptrweak_ptr
  • 包含在头文件 <memory>
  • 使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
  • 智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
  • 所有智能指针都重载了 operator-> 操作符,直接返回对象的引用,用以操作对象。

auto_ptr

  • C++98的方案,C++11 已经抛弃。
  • 采用所有权模式。
  • operator= 会剥夺先前对象的内存管理所有权。
  • release() 函数不会释放对象,仅仅归还所有权。
  • 缺点是存在潜在的内存崩溃问题。

unique_ptr

  • 实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。
  • 对于避免资源泄露特别有用。
  • 当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。
  • 可以通过 move() 函数转让所有权。
  • 可以通过 release() 函数释放所有权。

shared_ptr

  • 实现共享式拥有概念,多个智能指针可以指向相同对象。
  • 使用引用计数来表明资源被几个指针共享,每一个 shared_ptr 的拷贝都指向相同的内存。
  • 每使用它一次,内部的引用计数加 1,每析构一次,内部的引用计数减 1。
  • 该对象和其相关资源会在“最后一个引用被销毁”时候释放。
  • 可以通过成员函数 use_count() 来查看资源的所有者个数。
  • 可以指定类型,传入指针通过构造函数初始化。也可以使用 make_shared() 函数初始化。
  • 不能将指针直接赋值给一个智能指针,一个是类,一个是指针。
  • 注意不要用一个原始指针初始化多个 shared_ptr,否则会造成二次释放同一内存。
  • share_ptr 智能指针还是有内存泄露的情况,当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
share_ptr的简单实现

作者:code_peak
链接:https://www.cnblogs.com/WindSun/p/11444429.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
template <class T>
class WeakPtr; //为了用weak_ptr的lock(),来生成share_ptr用,需要拷贝构造用

template <class T>
class SharePtr
{
public:
SharePtr(T *p = 0) : _ptr(p)
{
cnt = new Counter();
if (p)
cnt->s = 1;
cout << "in construct " << cnt->s << endl;
}
~SharePtr()
{
release();
}

SharePtr(SharePtr<T> const &s)
{
cout << "in copy con" << endl;
_ptr = s._ptr;
(s.cnt)->s++;
cout << "copy construct" << (s.cnt)->s << endl;
cnt = s.cnt;
}
SharePtr(WeakPtr<T> const &w) //为了用weak_ptr的lock(),来生成share_ptr用,需要拷贝构造用
{
cout << "in w copy con " << endl;
_ptr = w._ptr;
(w.cnt)->s++;
cout << "copy w construct" << (w.cnt)->s << endl;
cnt = w.cnt;
}
SharePtr<T> &operator=(SharePtr<T> &s)
{
if (this != &s)
{
release();
(s.cnt)->s++;
cout << "assign construct " << (s.cnt)->s << endl;
cnt = s.cnt;
_ptr = s._ptr;
}
return *this;
}
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
friend class WeakPtr<T>; //方便weak_ptr与share_ptr设置引用计数和赋值

protected:
void release()
{
cnt->s--;
cout << "release " << cnt->s << endl;
if (cnt->s < 1)
{
delete _ptr;
if (cnt->w < 1)
{
delete cnt;
cnt = NULL;
}
}
}

private:
T *_ptr;
Counter *cnt;
};

weak_ptr

  • 为了配合 shared_ptr 而引入的一种智能指针,因为它不具有普通指针的行为,没有重载 *->,它的最大作用在于协助 shared_ptr 工作,像旁观者那样观测资源的使用情况。
  • weak_ptr 可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造,获得资源的观测权。
  • weak_ptr 没有共享资源,它的构造不会引起指针引用计数的增加。
  • weak_ptr 可以通过调用 lock 函数来获得 shared_ptr。
weak_ptr的简单实现

作者:code_peak
链接:https://www.cnblogs.com/WindSun/p/11444429.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
template <class T>
class WeakPtr
{
public: //给出默认构造和拷贝构造,其中拷贝构造不能有从原始指针进行构造
WeakPtr()
{
_ptr = 0;
cnt = 0;
}
WeakPtr(SharePtr<T> &s) : _ptr(s._ptr), cnt(s.cnt)
{
cout << "w con s" << endl;
cnt->w++;
}
WeakPtr(WeakPtr<T> &w) : _ptr(w._ptr), cnt(w.cnt)
{
cnt->w++;
}
~WeakPtr()
{
release();
}
WeakPtr<T> &operator=(WeakPtr<T> &w)
{
if (this != &w)
{
release();
cnt = w.cnt;
cnt->w++;
_ptr = w._ptr;
}
return *this;
}
WeakPtr<T> &operator=(SharePtr<T> &s)
{
cout << "w = s" << endl;
release();
cnt = s.cnt;
cnt->w++;
_ptr = s._ptr;
return *this;
}
SharePtr<T> lock()
{
return SharePtr<T>(*this);
}
bool expired()
{
if (cnt)
{
if (cnt->s > 0)
{
cout << "empty" << cnt->s << endl;
return false;
}
}
return true;
}
friend class SharePtr<T>; //方便weak_ptr与share_ptr设置引用计数和赋值

protected:
void release()
{
if (cnt)
{
cnt->w--;
cout << "weakptr release" << cnt->w << endl;
if (cnt->w < 1 && cnt->s < 1)
{
//delete cnt;
cnt = NULL;
}
}
}

private:
T *_ptr;
Counter *cnt;
};

右值引用

  • 左值和右值
    • 左值:英文简写为 lvalue,是 loactor value 的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据。
    • 右值:英文简写为 rvalue,是 read value 的缩写,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。
  • 判断某个表达式是左值还是右值的方法。
    • 可位于赋值号左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
    • 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
  • C++98/03 标准中的引用使用 “&” 表示,正常情况下只能引用左值,无法对右值添加引用。因此这种引用方式也称为左值引用。
  • 注意,虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值。
  • C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
  • 和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。
  • 和常量左值引用不同的是,右值引用还可以对右值进行修改。
  • C++ 语法上是支持定义常量右值引用的,但这种定义出来的右值引用并无实际用处。
  • 移动语义
    • 我们一般在使用函数管理含有资源的类的时候,一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享,不然会因为指向同一个区域,一个析构了另一个也不能使用。
    • 当我们将一个右值(比如一个字符串)作为函数参数传入时,本来就已经需要构造这个右值了,传进去之后却又要拷贝一遍,并且这个右值只是一个临时对象,拷贝完也没有什么用了,这就造成了很多不必要的资源申请和释放。
    • 实现移动语义需要增加两个函数:移动构造函数和移动赋值构造函数。
    • 移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是常量左值引用,而移动构造的参数是右值引用。
    • 移动构造函数并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是”偷”了过来,将自己的指针指向别人的资源,然后将别人的指针修改为空。
    • 有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11 为了解决这个问题,提供了std::move() 方法来将左值转换为右值,从而方便应用移动语义。
  • 通用引用
    • 当右值引用和模板结合的时候,T&& 并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。
    • 它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。
    • 引用折叠
      • 所有的右值引用叠加到右值引用上仍然使一个右值引用。
      • 所有的其他引用类型之间的叠加都将变成左值引用。
  • 完美转发
    • 所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
    • c++ 中提供了一个 std::forward() 模板函数将拥有名字的右值依然保持为右值。
    • 配合上面提到的通用引用,我们可以将左值引用、常量左值引用、右值引用、常量右值引用都保持原样转发给另一个函数,也即实现完美转发。
右值引用例子及理解

列表初始化

Lambda 表达式

  • C++11 提供了对匿名函数的支持,称为 Lambda 函数(也叫 Lambda 表达式)。
  • Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
  • Lambda 表达式本质上与函数声明非常类似。
  • 一般形式:[capture](parameters) -> return-type {body}
  • [capture] 可能值
    1
    2
    3
    4
    5
    6
    []      // 沒有定义任何变量。使用未定义变量会引发错误。
    [x, &y] // x以传值方式传入(默认),y以引用方式传入。
    [&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
    [=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
    [&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
    [=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。