# 基础一
#include<iostream>C++ 的头文件,包含了程序必须的相关信息main()的完整写法main(int argc, const char* argv[])using namespace std;使用标准命名空间endl英语意思是end of line一行输出结束,然后输出下一行。它的作用相当于在末尾添加换行符 \n- 指数
13e+8表示:13*10^8
16 进制0xN(N 为任意数)
2 进制0bN(N 为任意数)
8 进制0N(N 为任意数) - float 型数值后应加 f
float 有 7 位有效数字,注意:3.14 是 3 位有效数字 - 只有在类中的函数才需要声明访问级别
- 左值(lvalue)
有名字,有内存位置,可以出现在赋值号 = 的左边,在表达式结束后依然存在
例如:int a = 10; // a 是左值 - 右值(rvalue)
没有名字,不能取地址,一般只能出现在 = 的右边,生命周期只在当前语句
右值能绑定到const T&
例如:int x = 10; // 10 是右值
# 指针
- 格式
int a = 1; int* p; int* p = &a;
指针(地址)前加 * 代表解引用,找到指针(地址)指向内存中的数据
p 与 &a 同意,输出结果是 a 的地址
* p 与 *&a 同意,输出结果为 1 - 不论什么数据类型,指针在 32 位系统中占用 4 字节,64 位系统中占用 8 字节
- 空指针(使用 nullptr 而不是 NULL)
在 C++ 中,NULL 其实就是一个整数常量 0,而不是一个独立的 “空指针类型”。
当存在函数重载时,NULL 容易引起二义性,形参是指针还是整数,编译器无法判断。
nullptr 是一个可以隐式转换为任意指针类型的特殊类型
0 - 255 之间的内存编号是系统占用的,因此不可访问(nullptr 对应的的编号是 0)
空指针和野指针都不是我们申请的空间,因此不要访问 - const 与 指针
- const 修饰指针:常量指针
const int* p = &a;
指针的指向可以修改,但指针指向的值不可修改(不可通过 *p 修改指针指向的常量) - const 修饰常量:指针常量
int* const p = &a;
指针的指向不可修改,但指针指向的值可以修改(不可通过 *p 修改指针的指向) - const 即修饰指针,又修饰常量
- 口诀
const 在 * 左边:指向的值不能改
const 在 * 右边:指针本身不能改
左右都有:都不能改
- const 修饰指针:常量指针
- 函数指针是指向函数的指针变量,指针函数是返回值为指针的函数
- 不要滥用指针,否则会出现各种糟糕的情况,例如:指针指向的指针指向的函数指针指向的指针函数返回了一个指针
# 结构体
- C 语言中结构体不允许定义函数成员,且没有访问控制属性的概念。
C++ 为 C 语言中的结构体引入了成员函数、访问控制权限、继承、包含多态等面向对象特性。 - 结构体与类的区别只有一个:结构体中成员的默认保护级别为公开(public),而类中成员默认保护级别为私有(private)
- C++ 之所以要引入结构体,是为了保持和 C 程序的兼容性
- C 语言中,空结构体的大小为 0,而 C++ 中空结构体(属于空类)的大小为 1
- C++ 中空类的大小为 1 的原因:
空类也可以实例化,类实例化出的每个对象都需要有不同的内存地址,为使每个对象在内存中的地址不同,所以在类中会加入一个隐含的字节。
- C++ 中空类的大小为 1 的原因:
# 内存分区
代码区、全局 / 静态区、常量区、栈区、堆区
严格来说 常量不属于全局区,而是单独划分为常量区,只不过一些教材会简化处理,把它们也算进全局区
- 代码区
存放函数体的二进制代码,由操作系统进行管理 - 全局 / 静态区
存放全局变量和静态变量
存放:全局变量、静态变量(无论是否 const,但不包括字符串常量)
数据段:
已初始化数据段 .data
未初始化数据段 .bss - 常量区
字符串常量、const 全局常量(通常编译器放在只读段)
数据段:只读数据段 .rodata - 栈区
由编辑器自动分配释放,存放函数的参数值和局部变量等
不要返回局部变量的地址 - 堆区
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
用 new(或 malloc 等动态分配函数)创造的一定在堆区,返回的是变量在堆中的地址 - 虚拟内存区域分布
- 地址由低到高:代码区 → 常量区 (.rodata) → 全局区 (.data/.bss) → 堆 → 栈
- 代码区、常量区、全局区地址邻近
- 堆 在中间,从低地址往上长
- 栈 在高地址,从高地址往下长
# 引用
- 引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字
一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量 - 引用很容易与指针混淆,它们之间有三个主要的不同:
- 不存在空引用,引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象,而指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化,指针可以在任何时间被初始化。
- 不要返回局部变量的引用
函数的调用可以作为左值,对返回值进行操作 - 引用的本质是指针常量。
int& ref在内部会自动转换为int* const ref - 常量引用:用来修饰形参,防止误操作 (可以防止引用指向的值被修改)
# 高级函数
- 函数的默认参数
形参可以有默认值,从第一个有默认值的形参开始,从左往右,必须全都有默认值
如果函数声明有默认参数,那么函数实现就不能有默认参数
函数声明和函数实现中只能有一个默认值 - 函数占位参数
保留位置(兼容以后扩展)
区分函数重载
占位参数可以有默认参数,例:void func(int a, int, int = 10)
运算符重载时的特殊标记,例:class Counter
{private:
int value;
public:
Counter(int v = 0) : value(v) {}
// 前置 ++ (无参数)Counter& operator++()
{++value;
return *this;
}// 后置 ++ (int 占位参数区分)Counter operator++(int)
{Counter temp = *this; // 保存当前值
value++;
return temp; // 返回修改前的值
}void show() const
{cout << value << endl;
}};
- 函数重载:不同条件下解决同一类问题,使函数的命名相同参数不同,调用者只记住一个函数名即可。例如:
void Test();void Test(int a);
# 基础二
面向对象的三大特性为:封装、继承、多态
# 类和对象 — 封装
- 封装:将属性和行为作为一个整体,并增加权限控制
- 类中的属性和行为统称为:成员
- 属性 又称之为:成员属性、成员变量
- 行为 又称之为:成员函数、成员方法
- 三种访问权限
- 公共权限:public 成员在类的内部和外部都可以访问
- 保护权限:protected 成员在类的内部和继承这个类的其他类可以访问
- 私有权限:private 成员仅在类的内部可以访问
- 设置属性的权限:
- 将属性设置为 私有,然后用 方法 控制读或写的权限
# 类和对象 - 对象特性
- 默认情况下 C++ 至少给类添加四个函数
- 构造函数:无参数,空函数体
- 析构函数:无参数,空函数体
- 拷贝构造函数:参数为 const T&,拷贝方式为浅拷贝
- 赋值运算符函数:operator= 参数为 const T&,赋值方式为浅拷贝
- 如果用户定义有参构造函数,编译器不提供默认无参构造函数,但是会提供默认拷贝构造函数
- 如果用户定义拷贝构造函数,编译器不提供其他构造函数
- 在栈中创建对象时,不能用 () 调用默认构造函数,编译器会认为是函数声明
- 匿名对象在栈中创建,当前行结束后立即回收
- 编译器认为,匿名对象 T (s); 等同于 T s;,这可能会导致重复定义
- 析构函数中必须将堆中创建的内存释放,释放后不需要将指针置空
- 直接 delete 对象不会释放对象内部创建的堆内存
- 只需要释放堆中创建的内存,栈中的内存会自动释放
- 拷贝构造函数,分为浅拷贝和深拷贝,如果不写拷贝构造函数,默认是浅拷贝
- 浅拷贝会导致两个对象指向同一块内存,在析构时会重复 delete,导致程序崩溃
- 深拷贝复制内容,而不是复制指针
- 不写 const 就无法拷贝临时对象(右值)
- 临时对象只能绑定到 const T& 或 T&&,不能绑定到 T
- 对象作为函数形参时,会调用拷贝构造函数
- 通过拷贝构造函数创建对象时,如果用户定义拷贝构造函数,只有手动拷贝的变量会被拷贝
- 其他类的对象作为成员变量(直接在栈中的创建的对象),构造时会先构造成员变量,析构顺序与构造顺序相反
- 静态成员变量
- 所有的对象共享同一份数据
- 在编译阶段分配内存
- 在类内声明,类外初始化
- 静态成员函数
- 所有的对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 静态成员函数没有 this 指针
- 成员变量 和 成员函数 分开储存
- 空对象占用 1 字节内存,为了区分不同对象
- 只有非静态成员变量占用对象内存,其他的变量和函数都不占用对象内存
- this 指针
- 成员函数隐含的参数,指向调用该成员函数的对象
- this 指针的本质是指针常量,不可修改 T* const this
- 解决名称冲突
- 返回对象本身
- 所有非静态成员变量在使用时默认添加 this 指针
- 调用 a 实际上是 this->a
- 常函数
- const 修饰的是 this 指针,使 this 指针指向的值不可修改 const T* const this
- const 对象仅能调用 const 函数
- mutable 修饰可变成员变量,在常函数中也可以修改
- 常对象
- 常对象只能调用常函数
- 常对象不可修改成员变量
- 可变成员变量在常函数中可以修改
# 类和对象 - 友元
- 友元可以访问私有成员
- 友元类的所有成员函数都可以访问私有成员
- 在声明友元的同时也实现了函数的声明(引入声明)
# 运算符重载
- 内置数据类型的运算符不能被重载
- 不要滥用运算符重载
- 成员函数 重载 + 本质上是 p1.operator+(p2)
全局函数 重载 + 本质上是 operator+(p1, p2) - 运算符重载也可以发生函数重载
- 无法用成员函数 重载 << 的方法实现 cout << p;(p 是某个类的对象)
- 重载 ++ 需要 int 占位参数区分
- 无占位参数是 前置 ++
- 有占位参数是 后置 ++
- 这是系统规定的
- 重载 赋值运算符 =
- 默认的赋值运算符是浅拷贝,所有变量全部赋值
- 重载的目的之一就是将它改为深拷贝
- 仅对有需要的变量实现赋值逻辑,其他变量不会改变
- 重载 函数调用运算符 ()
- 将对象当作函数使用
- 使用方法非常像函数调用,所以称为 仿函数
# 类和对象 - 继承
- 父类中成员的权限是它能达到的最高权限
- 继承方式决定了父类成员在子类中的权限,但是无法超过父类中成员本身的权限
- 子类的大小 ≈ 父类的大小(所有非静态成员) + 子类新增成员的大小
- 检测方法 1:
- 打开 开发人员命令提示符,进入项目目录,运行
cl /d1 reportSingleClassLayout<类名> "源文件名.cpp" - 例如:
cl /d1 reportSingleClassLayoutHusky "A4 Inheritance.cpp"
- 打开 开发人员命令提示符,进入项目目录,运行
- 检测方法 2:鼠标移动到类名上,点击内存布局
- 一个类的大小不仅仅是 父类大小 + 子类新增成员大小,还要考虑编译器的 内存对齐
- x86 默认最大对齐是 4 字节
- x64 下很多成员(尤其是指针、long long、double 等)需要 8 字节对齐
- 一个类的大小必须是其 最大对齐单位的整数倍
- 检测方法 1:
- 如果存在同名成员(包括静态成员),父类的成员会被隐藏
如果存在同名函数(包括静态函数),父类中所有的同名函数都会被隐藏
解决方法:使用 作用域限定符 来访问被隐藏的成员或函数 - 构造顺序:父类构造 -> 子类构造
析构顺序:子类析构 -> 父类析构 - 多继承
- 如果多个父类中存在同名成员或函数,子类必须使用 作用域限定符 来访问
- 如果多个父类中存在同名的虚函数,子类重写时不需要使用 作用域限定符
- 实际开发中尽量避免多继承
- 菱形继承
- 父类(A) -> 两个子类(B、C) -> 一个类继承这两个子类(D)
- B、C 中相同的成员在 D 中会出现二义性
- D 中有两份 A 的成员
- 使用虚继承可以解决菱形继承的问题
- 将 B、C 改为虚继承
- 在 D 的内存布局中,继承的不是 A 的数据,而是 vbptr 指针
- vbptr 表示 虚基类表指针(virtual base class pointer) 指向 vbtable(虚基类表)
- vbtable 记录了 虚基类(A) 在 菱形继承 最底层类(D)中的 偏移位置
- 而 A 的数据只会在 D 中存在一份
- vbtable 储存在常量区
- 父类(A) -> 两个子类(B、C) -> 一个类继承这两个子类(D)
# 类和对象 - 多态
- 静态多态:函数重载、运算符重载
动态多态:派生类、虚函数 - 抽象类
- 包含纯虚函数的类是 抽象类
- 子类继承抽象类后,必须重写所有纯虚函数,否则子类也是抽象类
- 抽象类通常需要一个虚析构函数,这样可以确保在通过基类指针删除派生类对象时能够正确调用派生类的析构函数
- 通过 抽象类 或 Concept 来实现接口
- __interface 是 MSVC 的专用语法,本质上就是一个 只允许纯虚函数的抽象类,不能跨平台,不建议使用
- 虚析构函数 和 纯虚析构函数
- 纯虚析构函数必须有函数体,类内声明,类外初始化
- 父类指针 / 引用指向子类对象,删除时不会调用子类的析构函数
解决方法:将父类的析构函数改为虚析构函数或纯虚析构函数
- 如果父类有虚函数,父类和子类的内存布局中会有 vfptr 指针
- 子类中的指针是继承过来的,不是额外增加的
- 单继承:子类的 vfptr 覆盖并继承了父类的那个位置,内存里只有一个
- 多重继承或虚继承:子类会出现多个 vfptr(每个继承分支有自己的虚表)
- vfptr 表示 虚函数指针(virtual function pointer) 指向 vftable(虚函数表)
- vftable 记录的内容:
- 指向 RTTI(运行时类型信息)结构 的指针
- 偏移量(从当前子对象到最顶层基类的偏移)
对于单继承的简单类,它可能就是 0
如果是复杂继承(多重继承、虚拟继承),这个值会变成一个非零偏移,用来帮编译器在调用时调整 this 指针 - 虚函数的函数地址
- vftable 储存在常量区
- 父类指针或引用指向子类对象并调用虚函数时,因为对象是子类的,所以找到的是子类的 vfptr,然后找到子类的 vftable,最后执行子类的虚函数
流程:父类指针 / 引用 -> 子类对象 -> 子类 vfptr -> 子类 vftable -> 子类虚函数
# 文件操作
- 文件打开方式
ios::in只读ios::out只写ios::ate初始位置为文件尾ios::app追加写入ios::trunc如果存在先删除,再创建ios::binary二进制 - EOF 表示文件尾
- 不要将整个对象写入文件(或从文件读取到对象),应当手动操作每个成员变量的读写
# 命名空间
- 命名空间处在相同文件时,先创建后声明
- 命名空间处在不同文件时,用头文件声明
- 非静态变量:
- 具有 外部链接(external linkage)
- 可以被其他 .cpp 文件通过 extern 声明访问
- 所有文件共享一份
- 静态变量和静态函数:
- 具有 内部链接(internal linkage)
- 只在 定义它的那个 .cpp 文件里可见,不能 extern
- 每个文件独享一份,外部不可见
- 其他文件想要访问,只能依靠访问接口(写一个函数或方法,在内部进行操作,对外提供接口)
- 非静态变量:
# 基础三
# 模版
学习模版不是为了写模版,而是为了在 STL 中运用系统提供的模版。
- C++ 模版分为函数模版和类模版
- template <typename T> 和 template <class T>
typename 和 class 完全等价。一开始只有 class,为了语义更清晰,引入了 typename
建议使用 typename - 多个泛型参数参与运算时,必须保证类型一致
模版必须得到具体的数据类型才能使用,即使没有用到某个参数,也必须提供类型 - 隐式类型转换
- 普通函数可以发生隐式类型转换
- 函数模版 自动类型推导时,不可以发生隐式类型转换
- 函数模版 显示指定类型时,可以发生隐式类型转换
- 建议使用显示指定类型的方式调用函数模版
- 特别情况,不论是函数模版还是普通函数,如果形参是引用,那么实参类型必须完全匹配,不能隐式转换
- 调用规则
普通函数与函数模版都可以实现时,优先调用普通函数
如果函数模版有更好的匹配,则调用函数模版
通过空模版参数列表可以强制调用函数模版
函数模版可以重载 - 模版的局限性
泛型比大小传入自定义类型、泛型数组赋值等无法实现
通过模版具体化,可以解决自定义类型的通用化 - 函数模版与类模版的区别
- 类模版不能自动类型推导
- 类模版的模版参数列表可以设置默认参数,有默认参数的情况下,创建对象时可以写空模版参数列表
- 普通类中的成员函数一开始就会创建
类模版中的成员函数在调用时才会创建
分文件编写类模版时,会遇到链接错误- 方法一:包含 .cpp 文件,而不是仅包含 .h 文件
- 方法二(常用):将声明和实现都写在 .hpp 文件中
- 约定俗成:一般来说 .hpp 文件表示模版文件
- 类模版对象做函数参数
- 指定传入类型(常用)
- 参数模版化
- 类模版化
- 子类继承类模版时,必须指定参数类型或将子类也变成类模板
# STL
- 标准模板库 STL(Standard Template Library)
几乎所有的 STL 组件都是用模版实现的
广义上分为:容器、算法、迭代器,容器和算法通过迭代器连接
细分为六大组件:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
容器:存放数据 vector、list、deque、set、map
算法:操作数据 sort、find、copy、for_each
迭代器:连接容器和算法,类似指针
仿函数:可以像函数一样调用的对象,可作为算法的某种策略
适配器:对容器或迭代器进行改造 stack、queue、priority_queue
空间配置器:分配内存 - 容器将运用广泛的数据结构实现出来
分为 序列式容器 和 关联式容器
序列式容器:按照元素插入的顺序来存储数据
关联式容器:自动排序并组织数据,底层一般基于平衡二叉搜索树(红黑树)实现 - 算法用于解决逻辑或数学问题
分为 质变算法 和 非质变算法
质变算法:运算过程中会修改容器中的元素
非质变算法:运算过程中不会修改容器中的元素 - 迭代器是连接容器和算法的桥梁
迭代器类似于指针,可以遍历容器中的元素
迭代器是概念上对指针行为的抽象,但不一定由指针实现
迭代器分为五种类型:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器
不同的容器支持不同类型的迭代器 - 仿函数是可以像函数一样调用的对象
仿函数本质上是一个类,重载了 () 运算符
仿函数可以有自己的状态
仿函数可以作为算法的某种策略 - 适配器是对容器或迭代器进行改造
容器适配器:stack、queue、priority_queue
迭代器适配器:insert_iterator、reverse_iterator、stream_iterator - 空间配置器负责内存分配
STL 默认的空间配置器是 std::allocator
可以自定义空间配置器以满足特殊需求 - string 容器是 char* 的封装类
- vector 和数组很相似,也称为单端数组
头部插入和删除效率低,访问元素效率高
容量超上限时,会创建一个新的更大的空间,拷贝原数据,删除原空间
插入和删除操作可能会使原有迭代器失效 - deque 是双端数组
支持在头尾高效插入和删除,访问元素效率较低
deque 内部使用中控器管理多个缓冲区,中控器中存放缓冲区的地址
deque 的迭代器支持随机访问 - const_iterator 是只读迭代器
用于遍历 const 容器,不能修改容器中的元素 - 所有支持随机访问的容器,都可以用 sort 排序
- list 容器是双向链表
支持在任意位置高效插入和删除,不支持随机访问,访问元素效率低
链表由节点组成,每个节点包含数据和指向前后节点的指针
list 支持双向迭代器
插入和删除操作不会使原有迭代器失效
list 有自己独特的排序方法 sort
forward_list 是单向链表
与 CSharp 做对比:CSharp 中的 List<T> 是一个基于数组实现的动态顺序表,类似于 C++ 中的 vector,而 LinkedList<T> 是一个双向链表 - 不支持随机访问的迭代器不能用标准算法,他们的内部会提供算法
- set/multiset 容器是关联式容器
自动排序并组织数据
set 不允许有重复元素,multiset 允许有重复元素
元素是常量,不能修改(不能通过迭代器或引用直接修改 set 中元素的值)
支持双向迭代器,不支持随机访问 - map/multimap 容器是关联式容器
所有元素都是 pair 键值对
按 key 值自动排序并组织数据
map 不允许有重复 key,multimap 允许有重复 key
key 不能修改,value 可以修改
支持双向迭代器,不支持随机访问
使用 map [key] 时需要注意:如果 key 输入错误,内部会自动创建一个键值对 - 重载 () 操作符的类,称为函数对象,也称为仿函数
可以像函数一样调用,但本质上是一个类
函数对象可以作为参数
返回 bool 的仿函数称为谓词,根据参数的数量分为一元谓词和二元谓词 - 内置函数对象
算术仿函数:加、减、乘、除、取模、取反
关系仿函数:等于、不等于、大于、大于等于、小于、小于等于
逻辑仿函数:与、或、非 - 算法头文件
algorithm 最常用,包含大部分算法
numeric 包含小部分算法 - find 算法搜索自定义数据类型时,类需要重载 == 运算符(const 参数)
- 随机种子
不设置随机种子会导致每次随机的结果都相同
用时间作为随机种子 srand ((unsigned int) time (nullptr)); - 注意部分算法要求有序元素
# lambda 表达式
- 结构
[捕获列表](参数) { 函数体 } - 捕获列表
[] 不捕获任何外部变量。Lambda 只能使用自己参数里的东西,比如 (int a)
[=] 以值捕获所有在外层作用域中可见的变量。就是复制一份进来,函数体里能读不能改
[&] 以引用捕获所有外层变量。可以修改外层变量
[x] 只以值捕获变量 x
[&x] 只以引用捕获变量 x
[&, x] 混合捕获。默认引用捕获,但 x 例外用值捕获