C++基础学习--随记
参考“C++基础与深度解析”
一、预备知识
// c++常用工具
/usr/bin/time //查看程序用了多少时间(Linux自带)
$ sleep 1
$ /usr/bin/time sleep 1
valgrind //分析是否有内存泄漏(软件)
cppreference.com //"百科全书"(网站)
compiler explorer //直接生成汇编代码,但是只能粘贴一个代码(网站)
c++ insights //将代码“翻译”成另外的版本(网站)
- cppreference.com切换为中文网站
// 创建软连接
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-17 30 --slave /usr/bin/clang++ clang++ /usr/bin/clang++-17
// 移除软连接
sudo update-alternatives --remove-all clang
- 编译器使用版本如下:
--pedantic-errors
// codelite编译器引入参数,使编译器符合C++标准
-g;-O0;-Wall;--std=c++2a;--pedantic-errors
// 生成翻译单元(gcc编译c,g++编译c++)
gcc -E ./main.cpp -o ./main.i
// 生成汇编代码
g++ main.i -S -o main.s
// 直接编译和链接
g++ ./main.cpp -o ./main
// 罗列所有的外部链接
nm ./main.cpp.o
// 只能在编译期中执行(c++20发布的功能)
consteval
// 返回程序运行的返回值
$ echo $?
C++是一门大小写敏感的语言
// C++的编译/加工模型
源代码->“编译”-目标文件->“链接”-可执行文件->
C++默认只会给小写的“main”函数补加返回值0,其他函数必须自己定义返回值
新版的C++规定“main”函数返回值类型必须为int
C++规定“main”函数的形参有两种形式
① 为空:int main()
② int main(int argc, char* argv[])
// 查看系统的 cache line
cd /sys/devices/system/cpu/cpu0/cache/index0/
cat coherency_line_size
一个字节byte占4位bit
sizeof()可以获取类型存储所需要的尺寸
取值空间 numeric_limits // 超过范围可能产生溢出
std::numeric_limits<>::min();
std::numeric_limits<>::max();
对齐信息 alignof
std::cout << alignof(int) << std::endl;
// 从操作系统整个的环境变量下查找(库所在路径)
// 一般用于引用C++标准库提供的头文件
// 不加后缀.h
#include <iostream>
// 从当前目录下(main.cpp所在路径)开始查找头文件
// 一般用于引用自己编写的头文件
// 一般加上后缀.h
#include "myheader.h"
// "=" 赋值表达式:赋值后并返回所赋的值
x = y = 42
二、类型
类型分为“基本类型”与“复杂类型”
---基本(内建)类型:C++语言中所支持的类型
--数值类型
-字符类型 char, wchar_t, char16_t, char32_t
-整数类型
带符号整数类型 short, int, long, long long
无符号整数类型 unsigned+带符号整数类型
-浮点类型 float, double, long double
--void
---复杂类型:由基本类型组合、变种所产生的类型
可能是标准库引入,或自定义类型
// C++中各个类型具体占多少字节要根据不同编译器自己查看
short占2个字节,16位bit
int占4个字节,32位bit
long占8个字节,64位bit
long long占8个字节,64位bit
float占4个字节,324位bit
double占8个字节,64位bit
long double占16个字节,128位bit
char占1个字节,8位bit (可以表示256种字符 //ASCII码)(单引号‘’引入的才是字符)
wchar_t占2个字节 (宽字符类型)
char16_t占2个字节(C++11引入)
char32_t占4个字节(C++11引入)
C++是强类型语言
类型是一个编译器概念,可执行文件中不存在类型的概念
C++将double类型变为int类型,采用直接舍去小数点及以后的数字,10.5-->10
int x = 10.5; --> x = 10
unsigned 后面不加任何别的类型,默认就是 unsigned int
// 查看类型
#include <type_traits>
std::cout << std::is_same_v<decltype(ptr), const int* const> << std::endl;
// 类型别名 typedef / using
① typedef int MyInt;
② using MyInt = int; (从C++11开始)(推荐)
using IntPtr = int*;
const IntPtr ptr = &x;
====> int* const ptr = &x;
// 类型自动推导(从C++11开始)
// 自动推导类型并不意味着弱化类型,对象还是强类型
- auto
会引入类型退化(const int& --> int)
const int 作为右值时,会出现类型退化,变为 int 类型
const int x = 3
auto y = x; --> x退化为int类型
- const auto / constexpr auto
- auto&
推导出引用类型,避免类型退化
const int x = 3
auto& y = x; --> x不会退化为int类型,仍旧是const int类型
- decltype(exp)
返回exp表达式的类型,左值加引用&
int x = 3;
int* ptr = &x;
ptr -> int*
*ptr -> int
decltype(*ptr) -> int&
decltype(x) -> int
decltype((x)) -> int&
- decltype(val)
返回val的类型
- decltype(auto)(从C++14开始,简化decltype的使用,结合auto和decltype各自的优点,不会产生任何类型的退化)
decltype(3.5 + 15l) x = 3.5 + 15l;
简化为 decltype(auto) x = 3.5 + 15l;
- concept auto(不是某种类型,是引入的一个概念,如int,short...都属于同一系列的类型integral)(从C++20开始,表示一系列类型)
std::integral auto x = 3;
与类型相关的标准未定义的部分
① char 是否有符号(可以通过 signed 或 unsigned 强制定义是否有符号)
② 整数中内存的保存方式(大端、小端)
③ 每种类型的大小(间接影响取值范围)
(C++11中引入了固定尺寸的整数类型,如 int32_t)
无符号与带符号比较,std::cmp_XXX(C++20引入)
指针
void* 指针类型可以转换成任意的其他类型的指针类型(int* char* ...)
但是 void* 丢掉了对象的尺寸信息
// 指针 V.S. 对象
指针复制成本低,读写成本高
// 指针的问题
--可以为空
--地址信息可能非法
// 解决方法:引用
-- int& ref = val;
- 是对象的别名,不能绑定字面值
- 构造时绑定对象,在其生命周期内不能绑定其他对象(赋值操作会改变对象内容)
- 不存在空引用,但可能存在非法引用(但总的来说比指针安全)
- 属于编译期概念,在底层还是通过指针实现
// 指针的引用
指针是对象,因此可以定义引用
int* p = &val;
int*& ref = p;
类型信息从右向左解析
常量
常量 const 是编译期概念(没有底层硬件支持)
- 防止非法操作
- 优化程序逻辑
int* const ptr = &x; ==> 指针所指的地址不能修改(顶层常量)
(ptr = &y; 会报错)
const int* ptr = &x; ==> 不能使用指针改变地址对应对象的值(底层常量)
(*ptr = 3; 会报错,但是可以 ptr = &y;)
// 常量引用 const int&
- 可以绑定变量
- 可读但不可写
- 主要用于函数形参
- 可以绑定字面值
// 常量表达式(const expression)(从C++11开始)
- 使用 constexpr 声明
- 声明的是编译期常量
- 类似一种指示符,是一种限定符,不代表类型(constexpr int x = 3; --> x的类型是 const int)
- 编译器可以利用其进行优化
- 常量表达式指针:constexpr位于*的左侧,表示指针是常量表达式
(constexpr const int* ptr = nullptr;
constexpr 修饰的是 ptr
ptr 指向的是 const int* 类型的对象
ptr 的类型是 const int* const)
三、系统I/O
// iostream 标准库所提供的IO接口,用于与用户交互
输入流:cin (一般通过键盘输入)
输出流:cout / cerr / clog (cerr-立即刷新缓冲区,clog-不立即刷新缓冲区)
缓冲区(刷新):std::flush; 或 std::endl;(endl不仅刷新缓冲区,还会添加换行符)
// C语言IO
#include <cstdio>
printf("111");
// C++20引入的新的IO解决办法(暂时不支持)
// std::format
#include <format>
std::cout << std::format("Hello, {}!\n", "World");
四、命名空间
// 命名空间:用于防止名称冲突
std 名字空间
// 访问名字空间中元素的三种方式
① 域解析符 ::
② using语句(不推荐)
③ 名字空间别名
namespace ns1 = NameSpace1;
ns1::fun();
// 名字空间与名字改编 name mangling
// c++filt -t 将C++外部链接后改编的名称重新变为原来的名称
// 不管是 mangling,还是 demangling,都不会改动 main 的名称
nm ./main.cpp.o | c++filt -t
五、数组
// 数组
不能使用 auto 来声明数组类型
auto b = {1, 3, 4};
std :: cout << typeid(b).name() << std :: endl; // St16initializer_listIiE
std::cout<<std::is_same_v<decltype(b), int[3]><<std::endl;
数组不能复制
元素个数必须是一个常量表达式(编译期可计算的值)
字符串数组的特殊性
// C和C++会隐式地在这种形式的字符串最后加一个 ‘\0’--取值为0的特殊的字符,用来表示字符串的结束
// 等价于 char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str[] = "Hello"; // char[6]
std::cout<<std::is_same_v<decltype(str), char[6]><<std::endl;
char str[] = {'H', 'e', 'l', 'l', 'o'}; // char[5]
std::cout<<std::is_same_v<decltype(str), char[5]><<std::endl;
// 打印类型名称
#include <typeinfo>
int b[10];
std :: cout << typeid(b).name() << std :: endl;
// A10_i
// demangling处理
~/cpp_learning/demo/array_demo/Debug$ ./array_demo | c++filt -t
// int [10]
数组类型退化,会退化为指针
C++中字符串需要用双引号“”引起来
// 指针的数组
// a是一个数组,由三个指针类型的元素构成
int* a[3]; --> int*[3]
// 数组的指针
// a是一个指针,解引用之后,指向一个数组,该数组由三个int类型的元素构成
int (*a)[3]; --> int(*)[3]
// 数组的引用
// a是一个引用,是b这个数组的别名,b这个数组由三个int类型的元素构成
int b[3];
int (&a)[3] = b; --> int(&)[3]
// 不能定义引用的数组
// 引用是对象的别名,但是本身不是对象
// 数组的元素必须是对象
int x1;
int x2;
int x3;
int& a[3] = {x1, x2, x3}; (会报错)
数组是一个左值 l-value
// 验证一个对象是否是左值
// 查看类型中是否含有引用&
const int x = 3;
std :: cout << std :: is_same_v<decltype((x)), const int&> << std :: endl;
int a[3] = {1, 2, 3};
std :: cout << std :: is_same_v<decltype((a)), int(&)[3]> << std :: endl;
// 当数组被当做右值使用时,会发生隐式转换
int a[3] = {1, 2, 3};
auto b = a; // 数组不能直接复制,所以要用auto,此时 a --> int*(隐式转换)
// b的类型是 int*,指向数组a中包含的第一个元素
std :: cout << std :: is_same_v<decltype(b), int*> << std :: endl;
std :: cout << *b << std :: endl;
std :: cout << a[0] << std :: endl;
std :: cout << &(a[0]) << std :: endl;
auto& b = a; // b --> int(&)[3]
// C++中,[ ]的实际意义
x[y] --> *(x+y) // 先把x转换成指针,指向x数组的第一个元素,再将指针往后移动y个b中元素类型的大小,再解引用
int a[3] = {1, 2, 3};
auto b = a; --> b 是 int*
b[1] --> *(b+1) --> b本身已经是指针了,不用转换了,指向a数组的第一个元素,然后该指针往后移动1个int*的大小,即4个字节(int占4个字节),再解引用
a[1] -- 1[a] -- *(a+1) -- *(1+a)
//不要使用 extern 指针来声明数组
extern int* array; (错误用法,会直接把数组的值当作数组的地址)
// 正确声明数组(并且避免了直接将数组大小写死)
// Unknown Bounded Array 声明
extern int array[]; // 其中 int array[] 又称为incomplete type,不完整的类型
// 获得指向数组开头与结尾的指针
int a[3] = {1, 2, 3};
std :: cout << a << ' ' << &(a[0]) << ' ' << std :: begin(a) << std :: endl; // 取开头第一个元素的地址 begin --> int* 读写
std :: cout << a << ' ' << &(a[0]) << ' ' << std :: cbegin(a) << std :: endl; // cbegin --> const int* 只读
std :: cout << a + 3 << ' ' << &(a[3]) << ' ' << std :: end(a) << std :: endl; // 取结尾最后一个元素的地址 end --> int* 读写
std :: cout << a + 3 << ' ' << &(a[3]) << ' ' << std :: cend(a) << std :: endl; // cend --> const int* 只读
auto& b = a;
std :: cout << std :: begin(b) << std :: endl;
std :: cout << std :: end(b) << std :: endl;
// 数组的加减
int a[3] = {1, 2, 3};
auto ptr = a; // int*
auto ptr2 = a + 2;
std :: cout << *ptr << std :: endl;
std :: cout << *ptr2 << std :: endl;
ptr = ptr - 1;
ptr = ptr + 2;
std :: cout << *ptr << std :: endl;
ptr = ptr - 1;
std :: cout << *ptr << std :: endl;
// 数组的比较(两个指针只有指向同一个数组时,才能作比较)
std :: cout << (ptr == ptr2) << std :: endl;
std :: cout << (ptr != ptr2) << std :: endl;
std :: cout << (ptr > ptr2) << std :: endl;
std :: cout << (ptr < ptr2) << std :: endl;
std :: cout << (ptr >= ptr2) << std :: endl;
std :: cout << (ptr <= ptr2) << std :: endl;
// 求距离
int a[3] = {1, 2, 3};
auto ptr = a; // int*
auto ptr2 = a + 2;
std :: cout << ptr2 - ptr << std :: endl;
// 解引用
int a[3] = {1, 2, 3};
auto ptr = a; // int*
auto ptr2 = a + 2;
std :: cout << *ptr << std :: endl;
std :: cout << *ptr2 << std :: endl;
std :: cout << *a << std :: endl;
// 指针索引
std :: cout << ptr[2] << std :: endl; // *(ptr + 2)
// 求元素的个数(前提:数组给定了大小)
① sizeof //(C语言常用方式)(书写麻烦,并且可能存在不能及时报错的情况,并且将类型写死了)
int a[3] = {1, 2, 3};
std :: cout << sizeof(a) << std :: endl;
std :: cout << sizeof(int) << std :: endl;
std :: cout << "数组元素的个数为:" << sizeof(a) / sizeof(int) << " 个" << std :: endl;
② std :: size // 推荐使用
int a[3] = {1, 2, 3};
std :: cout << "数组元素的个数为:" << std :: size(a) << " 个" << std :: endl;
③ (c)end - (c)begin // 运行期的方法(不推荐)
int a[3] = {1, 2, 3};
std :: cout << "数组元素的个数为:" << std :: end(a) - std :: begin(a) << " 个" << std :: endl;
std :: cout << "数组元素的个数为:" << std :: cend(a) - std :: cbegin(a) << " 个" << std :: endl; // 只读方式,更推荐
// 元素遍历
① 基于元素个数
int a[4] = {2, 3, 5, 7};
size_t index = 0;
while (index < std :: size(a))
{
std :: cout << a[index] << std :: endl;
index = index + 1;
}
② 基于 (c)begin / (c)end
int a[4] = {2, 3, 5, 7};
auto ptr = std :: cbegin(a);
while (ptr != std :: cend(a)) // 或者 while (ptr < std :: cend(a))
{
std :: cout << *ptr << std :: endl;
ptr = ptr + 1;
}
③ 基于 range-based for 循环 // C++11开始引入的(语法糖)(与方法②while,没有本质的区别)
int a[4] = {2, 3, 5, 7};
for (int x : a)
{
std :: cout << x << std :: endl;
}
多维数组
// 多维数组 从里往外看
#include <type_traits>
int x2[3][4]; // x2数组中有三个元素,每一个元素是一个包含四个元素的数组
std::cout << sizeof(x2[0]) << std::endl;
std::cout << sizeof(int) << std::endl;
std::cout << std :: is_same_v<decltype(x2[0]), int(&)[4]> << std::endl; // x2[0]是一个表达式,当decltype中的表达式是一个左值时(能够定位它的地址时),会返回该对象类型的引用&
// 定义一个多维数组时候,最多只能缺省最高位的维度(聚合初始化)
int x[][2][4] = {1, 2, 3};
// 多维数组的遍历
① 基于 range-based for 循环
int x2[3][4] = {1, 2, 3, 4, 5};
for (auto& p : x2) // 避免p由于隐式转换变为int*
{
for (auto q : p)
{
std::cout << q << '\n';
}
}
② 基于 while 循环
int x2[3][4] = {1, 2, 3, 4, 5};
size_t index0 = 0;
while (index0 < std :: size(x2))
{
size_t index1 = 0;
while (index1 < std :: size(x2[index0]))
{
std::cout << x2[index0][index1] << '\n';
index1 = index1 + 1;
}
index0 = index0 + 1;
}
// 多维数组和指针
多维数组可以隐式转换为指针,但只有最高维会进行转换,其他维度的信息会被保留
// 可以使用类型别名来简化多维数组的声明
using A2 = int[4][5];
int x2[3][4][5];
A2* ptr = x2;
// 可以使用指针来遍历多维数组
int x2[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
auto ptr = std :: begin(x2);
while (ptr != std :: end(x2))
{
auto ptr2 = std :: begin(*ptr);
while (ptr2 != std :: end(*ptr))
{
std::cout << *ptr2 << std :: endl;
ptr2 = ptr2 + 1;
}
ptr = ptr + 1;
}
六、C字符串
C字符串本质上也是数组
C语言提供了额外的函数来支持C字符串相关的操作
// 计算C字符串长度 strlen
#include <cstring>
char str[] = "Hello"; // ‘\0’ null-terminated string
auto ptr = str;
std :: cout << strlen(str) << std :: endl;
std :: cout << strlen(ptr) << std :: endl;
七、Vector
// Vector(序列容器)
- 是C++标准库中定义的一个类模板
- 与内建数组相比,更侧重易用性(可复制、可在运行期动态改变元素的个数)
- 以牺牲程序的性能为代价
// vector初始化
#include <vector>
std :: vector<int> x = {1, 2, 3}; // 类比数组
std :: vector<int> x1(3); // 与数组不同,默认会全部为零
std :: vector<int> x2(3,1); // vector中有三个元素,每个元素的值都是1
std :: vector<int> x3{1, 2, 3}; // vector为(1, 2, 3)
// 获取元素个数
#include <vector>
std :: vector<int> x1(3, 1);
std :: cout << x1.size() << std :: endl; // 函数调用 -- 方法
// 判断是否为空
#include <vector>
std :: vector<int> x1(3, 1);
std :: cout << x1.empty() << std :: endl;
// 插入元素
#include <vector>
std :: vector<int> x1(3, 1);
std :: cout << x1.size() << std :: endl;
x1.push_back(2); // 运行期执行,把元素2插入vector结尾处
std :: cout << x1.size() << std :: endl;
// 删除元素
#include <vector>
std :: vector<int> x1(3, 1);
std :: cout << x1.size() << std :: endl;
x1.pop_back(); // 运行期执行,把vector结尾处的元素删除
std :: cout << x1.size() << std :: endl;
// 比较(从第一个元素开始比较,如果相同,再比较下一个)(字典排序)
#include <vector>
std :: vector<int> x1 = {1, 2, 3};
std :: vector<int> x2 = {1, 3, 2};
std :: cout << (x1 == x2) << std :: endl;
std :: cout << (x1 > x2) << std :: endl;
std :: cout << (x1 < x2) << std :: endl;
// 元素的索引
#include <vector>
std :: vector<int> x1 = {1, 2, 3};
std :: cout << x1[2] << std :: endl;
std :: cout << x1.at(2) << std :: endl; // 能够有效避免越界的情况,保证系统的安全
// 元素的遍历
① while 循环
#include <vector>
std :: vector<int> x1 = {1, 2, 3};
auto bb = x1.begin(); // auto b = std :: begin(x1);
// 此时b不是指针了,是iterator迭代器
auto ee = x1.end(); // auto e = std :: end(x1);
while (bb != ee)
{
std :: cout << *bb << std :: endl;
bb = bb + 1;
}
② range-based for 循环
#include <vector>
std :: vector<int> x1 = {1, 2, 3};
for (auto val : x1)
{
std :: cout << val << std :: endl;
}
迭代器
- 模拟指针的行为
- 包含多种类别,每种类别支持的操作不同
// vector对应“随机访问迭代器”
/*
#include <vector>
std :: vector<int> x1 = {1, 2, 3};
auto bb = x1.begin();
auto ee = x1.end();
*/
- 解引用 // *bb;
- 下标访问 // bb[1];
- 移动 // bb = bb + 1;
- 两个迭代器相减求距离 // ee-bb
- 两个迭代器比较 // == < > ,两个迭代器要比较,必须满足指向同一个vector
// 添加元素可能使迭代器失效 iterator invalidation
多维vector
// 定义和初始化多维vector都比数组麻烦
// 但是多维vector的元素使用聚合的方式定义时,可以是不同大小
// std :: vector<std :: vector<int>> x{{1, 2, 3}, {4, 5}};
#include <vector>
std :: vector<std :: vector<int>> x;
x.push_back(std :: vector<int>());
x[0].push_back(1);
std :: cout << x[0][0] << std :: endl;
->操作符
// 语法糖 . 变为 ->操作符
// 调用对象的方法,但是->的左边必须是指针
#include <vector>
std :: vector<int> x;
std :: cout << x.size() << std :: endl;
std :: vector<int>* ptr = &x;
std :: cout << (*ptr).size() << std :: endl;
std :: cout << ptr -> size() << std :: endl;
// vector内部定义的类型
- size_type
- iterator
- const_iterator
八、string
- 是C++标准库中定义的一个类模板特化别名(std::basic_string<char>),用于内建字符串的替代品
- 与内建字符串相比,更侧重于易用性(可复制、可在运行期动态改变字符的个数)
#include <string>
std :: string x = "Hello world";
std :: string y = x;
y = y + '!';
// 构造
#include <string>
std :: string x = "Hello world";
std :: string x(3, 'a'); // 构造了三个字符,每个字符是a的字符串
// 初始化
#include <string>
std :: string x = "Hello world";
std :: string y = x; // 用复制的方式进行初始化
std :: string y(x);
std :: string z{"Hello"};
// 尺寸相关方法
#include <string>
std :: string x = "Hello world";
std :: cout << x.size() << std :: endl;
std :: cout << x.empty() << std :: endl;
// 比较
#include <string>
std :: string x = "Hello world";
std :: string y("Hello");
std :: cout << (y == x) << std :: endl;
std :: cout << (y > x) << std :: endl;
std :: cout << (y < x) << std :: endl;
// 赋值
#include <string>
std :: string x = "Hello world";
std :: string y("Hello");
y = "New String";
// 拼接
#include <string>
std :: string x = "Hello world";
std :: string y("Hello");
std :: cout << y << std :: endl;
y = y + ' ' + "New String"; // string和内建字符串进行拼接
std :: cout << y << std :: endl;
y = y + ' ' + x;
std :: cout << y << std :: endl;
// y = "hello_world" + "_hello_cplus" + x; // 这样会报错,因为有两个内建字符串在进行拼接
y = std :: string("hello_world") + "_hello_cplus" + x; // std :: string("hello_world") 创建一个临时的对象,类型是string,参数是"hello_world",没有名称
// 索引
#include <string>
std :: string y("Hello");
std :: cout << y[3] << std :: endl;
// 转换为C字符串
#include <string>
std :: string y("Hello");
y = y + " world";
auto ptr = y.c_str(); // ptr是一个char*类型的指针,指向一个字符串,这个字符串的内容是Hello
std :: cout << ptr << std :: endl; // cout会把char*类型的指针以字符串形式输出
九、表达式
表达式基础
表达式由一到多个操作数组成,可以求值并(通常会)返回求值结果
- 最基本的表达式:变量、字面值
- 通常来说,表达式会包含操作符(运算符)operator
操作符的特性
- 接收几个操作数:一元、二元、三元
- 操作数的类型 -- 类型转换
- 操作数是左值还是右值
- 结果的类型
- 结果是左值还是右值
- 优先级与结合性,可以通过小括号来改变运算顺序
- 操作符的重载--不改变操作数的个数、优先级与结合性
// 操作数求值顺序的不确定性(为了保证性能)(但是某种情况下可能会令程序比较危险)
int x = 0;
fun(x = x + 1, x = x + 1);
// 避免方式
// 拆成多条语句
int x = 0;
x = x + 1;
x = x + 1;
fun(x, x);
左值与右值
传统的左值与右值划分
- 来源于C语言:左值可能放在等号的左边;left-value -- l-value
右值只能放在等号的右边。right-value -- r-value
- 在C++中,左值也不一定能放在等号左边;(非亡值的泛左值)
右值也不一定能放在等号右边。(纯右值或亡值)
所有的划分都是针对表达式的,不是针对对象或数值(具体查阅 https://en.cppreference.com/w/cpp/language/value_category)
- glvalue(generalized-泛左值): 标识一个对象、位或函数
- prvalue(pure-纯右值): 用于初始化对象或作为操作数
- xvalue(expiring-将亡值): 标识其资源可以被重新使用
// 左值与右值的转换
- 左值转换为右值(lvalue -> rvalue)
int x = 3; // x是左值
int y = x; // x被转换为右值
- 临时具体化(Temporary Materialization)(prvalue -> xvalue)
// e.g.1
struct Str
{
int x;
};
Str().x // Str()是纯右值,但是.操作符将其变为将亡值
// e.g.2
void fun(const int& par)
{
}
int main()
{
fun(3); // 3本身是纯右值,但是C++内部将其转换为了将亡值
}
再论 decltype
- prvalue -> type
// 若表达式的值的类型为纯右值,则decltype返回T
decltype(3) x; <--> int x;
- lvalue -> type&
// 若表达式的值的类型为左值,则decltype返回T&
int x;
decltype(x) y; <--> int y;
// 将x改为表达式(x)
decltype((x)) y = x; <--> int y;
- xvalue -> type&&
// 若表达式的值的类型为亡值,则decltype返回T&&(右值引用)
#include <utility>
int x;
decltype(std :: move(x)) y = std :: move(x); <--> int&& y;
类型转换
隐式类型转换
- 自动发生
- 实际上是一个(有限长度的)转型序列【https://en.cppreference.com/w/cpp/language/implicit_conversion】
显式类型转换(少用,有风险)
-- 显式引入的转换
- static_cast // 编译期完成,性能好,但是安全性较差
// e.g.1
static_cast<double>(3) + 0.5; // 3 + 0.5;
// e.g.2
int x = 3;
int y = 4;
std :: cout << (x / y) << std :: endl;
std :: cout << (x / static_cast<double>(y) ) << std :: endl;
// e.g.3(C语言的一种使用方法,C++一般使用函数重载完成相同的功能)
void fun(void* par, int t)
{
if (t == 1)
{
int* ptr = static_cast<int*>(par);
}
else if (t == 2)
{
int* ptr = static_cast<double*>(par);
}
}
- const_cast // 改变所定义变量的常量性,比较危险(行为不确定),要尽量绑定在变量上
// e.g.1
const int* ptr;
const_cast<int*>(ptr);
// e.g.2
int x = 3;
const int& ref = x;
int& ref2 = const_cast<int&>(ref);
ref2 = 4;
std :: cout << x << std :: endl;
- dynamic_cast // 运行期完成,更安全,但性能更差
- reinterpret_cast
int x = 3;
int* ptr = &x;
double* ptr2 = reinterpret_cast<double*>(ptr); // 把int类型x对应的内存空间强行解释成一个double
std :: cout << *ptr << std :: endl;
std :: cout << *ptr2 << std :: endl; // 把int类型强行解释成一个double,值就会发生改变(int和double所占的字节数不同--具体占多少字节需要看电脑)
- C形式的类型转换(主要在C语言中使用,不建议在C++中使用)
(double)3;
表达式详述
算术操作符
优先级(均为左结合)
① + - (一元) // 7 * + 3; 是合法的,此处的+是一元的,代表正号
② * / %
③ + - (二元)
● 通常来说,操作数与结果均为算数类型的右值;但加减法与一元 + 可接收指针
// e.g.1
int a[3] = {1, 2, 3};
int* ptr = a;
ptr = ptr + 1;
std :: cout << ptr << std :: endl;
std :: cout << *ptr << std :: endl;
ptr = ptr - 1;
std :: cout << ptr << std :: endl;
std :: cout << *ptr << std :: endl;
std :: cout << std :: cend(a) - std :: cbegin(a) << std :: endl;
// e.g.2(应用场景较少)
int a[3] = {1, 2, 3};
// const auto& x = a; // int const (&x)[3] = a;
const auto& x = +a; // int *const & x = +a;
// +不能应用于数组,只能应用于指针,所以会强行进行类型转换
● 一元 + 操作符会产生 integral promotion
short x = 3;
// auto y = x; // short y = x;
auto y = +x; // int y = +static_cast<int>(x);
// +x是一个表达式,求值时x首先是一个short类型,有了+后,会产生类型提升
● 整数相除会产生整数,向 0 取整
std :: cout << 4 / 3 << std :: endl;
● 求余只能接收整数类型操作数,结果符号与第一个操作数相同
3 % 4;
● 满足 (m / n) * n + m % n == m
逻辑与关系操作符
● 关系操作符接收算术或指针类型操作数
● 逻辑操作符接收可转换为 bool 值的操作数
true && true;
● 操作数与结果均为右值(结果类型为 bool )
● 除逻辑非外,其它操作符都是左结合的
● 逻辑与、逻辑或具有短路特性
● 逻辑与的优先级高于逻辑或
● 通常来说,不能将多个关系操作符串连
int a = 3;
int b = 4;
int c = 5;
// std :: cout << (c > b > a) << std :: endl; // 0
std :: cout << ((c > b) && (b > a)) << std :: endl; // 1
● 不要写出 val == true 这样的代码
int a = 3;
if (a == true) // ==是逻辑关系符,true会被隐式转换成int的字面值1(a == 1)
{
}
// C++20引入的 <=>
● Spaceship operator: <=> // “宇宙飞船”操作符
/*
// 普通情况下,如果a和b是比较复杂的表达式,会耗费较多的时间
// 在计算完 a>b 的结果后,还得重新再计算 a<b 的结果,影响整个程序的性能
if (a > b)
{
}
else if (a < b)
{
}
else
{
}
*/
int a = 4;
int b = 5;
auto res = (a <=> b);
if (res > 0) // if (res == std :: strong_ording :: greater)
{
}
else if (res < 0) // else if (res == std :: strong_ording :: less)
{
}
else // else if (res == std :: strong_ording :: equal)
{
}
区别于C语言会返回1、-1或0,C++会返回以下三种情况
– strong_ordering
– weak_ordering(给类似 3*5和5*3比较 这种情况使用)
– partial_ordering(给 NaN 使用)
位操作符
● 接收右值,进行位运算,返回右值
● 除取反外,其它运算符均为左结合的
● 注意计算过程中可能会涉及到 integral promotion
● 注意这里没有短路逻辑
● 移位操作在一定情况下等价于乘(除) 2 的幂,但速度更快
● 注意整数的符号与位操作符的相关影响
– integral promotion 会根据整数的符号影响其结果
– 右移保持符号,但左移不能保证
// ~ 按位取反
signed char x = 3; // 3<->00000011
std :: cout << ~x << std :: endl; // ~按位取反,11111100 <-> 对应二进制的补码形式就是 -4
// 补码概念参考:https://blog.csdn.net/u011429167/article/details/129264625
// 移位操作符
signed char x = 3; // 0000 0101
std :: cout << (x << 1) << std :: endl; // 0000 1010
赋值操作符
● 左操作数为可修改左值;右操作数为右值,可以转换为左操作数的类型
● 赋值操作符是右结合的
● 求值结果为左操作数
● 可以引入大括号(初始化列表)以防止收缩转换( narrowing conversion )
● 小心区分 = 与 ==
● 复合赋值运算符
x = x + 2; <=> x += 2;
y = y * 3; <=> y *+ 3;
// 不能提到性能,但能节省一块内存
int a = 2;
int b = 3;
a ^= b ^= a ^= b; // a ^= b --> 10 ^= 11 --> 01 --> a=1;
// a ^= b ^= a;
// b ^= a --> 11 ^= 01 --> 10 --> b=2;
// a ^= b;
// a ^= b --> 01 ^= 10 --> 11 --> a=3;
std :: cout << a << '\n'; // 3
std :: cout << b << '\n'; // 2
自增与自减运算符
● ++; --
++x; <=> x = x + 1;
● 分前缀与后缀两种情况
int a = 3;
int b;
b = a++; // 后缀时,变量自增后,返回变量的原始值
std :: cout << a << '\n'; // 4
std :: cout << b << '\n'; // 3
int a = 3;
int b;
b = ++a; // 前缀时,变量自增后,返回变量变化后的值
std :: cout << a << '\n'; // 4
std :: cout << b << '\n'; // 4
● 操作数为左值;前缀时返回左值;后缀时返回右值
++(++x); // 合法
(x++)++; // 不合法
● 建议使用前缀形式
// 后缀形式需要付出一定成本,构造一个临时变量,用于返回原始值
// 但是不要因噎废食
int x = 3;
int y = x;
x = x + 1;
||
int x = 3;
int y = x++;
其它操作符
● 成员访问操作符: . 与 ->
– -> 等价于 (*).
– . 的左操作数是左值(或右值),返回左值(或右值 xvalue )
– -> 的左操作数指针,返回左值
struct Str
{
int x;
};
int main()
{
Str a;
a.x;
Str* ptr = &a;
ptr -> x; // (*ptr).x;
}
● 条件操作符 ? : // 不推荐使用
– 唯一的三元操作符(C/C++中)
– 接收一个可转换为 bool 的表达式与两个类型相同的表达式,只有一个表达式会被求值
– 如果表达式均是左值,那么就返回左值,否则返回右值
– 右结合
// e.g.1
std :: cout << (true ? 3 : 5) << std :: endl; // 3
// e.g.2
int score = 100;
int res = (score > 0) ? 1 :(score == 0) ? 0 : -1;
std :: cout << res << std :: endl; // 1
int res = ((score > 0) ? 1 :(score == 0)) ? 0 : -1;
std :: cout << res << std :: endl; // 0
● 逗号操作符
– 确保操作数会被从左向右求值
– 求值结果为右操作数
– 左结合
std :: cout << (2, 3) << std :: endl; // 3
std :: cout << (2, 3, 4, 5) << std :: endl; // 5
// 注意! fun(2, 3); 不是使用逗号操作符,这是在向函数内部传递参数
● sizeof 操作符
– 操作数可以是一个类型或一个表达式
– 并不会实际求值,而是返回相应的尺寸
int* ptr = nullptr;
// (*ptr); // 由于指向空指针,所以直接解引用会报错
sizeof(*ptr); // 只是假装求值,只是返回相应的尺寸,因此可以运行
● 其它操作符
– 域解析操作符 ::
int x = 1;
namespace ABC
{
int x = 2;
}
int main()
{
int x = 3;
int y = x; // 访问的是局部的x
int z = :: x; // 访问的是全局的x
int k = ABC :: x;
std :: cout << y << std :: endl; // 3
std :: cout << z << std :: endl; // 1
std :: cout << k << std :: endl; // 2
}
– 函数调用操作符 ()
– 索引操作符 []
– 抛出异常操作符 throw
C++17对表达式的求值顺序限定
● 以下表达式在 C++17 中,可以确保 e1 会先于 e2 被求值
– e1[e2]
– e1.e2
– e1.*e2
– e1→*e2
– e1<<e2
– e1>>e2
– e2 = e1 / e2 += e1 / e2 *= e1… (赋值及赋值相关的复合运算)
● new Type(e) 会确保 e 会在分配内存之后求值
十、语句
语句基础
语句的常见类别
– 表达式语句:表达式后加分号,对表达式求值后丢弃,可能产生副作用
– 空语句:仅包含一个分号的语句,可能与循环一起工作
– 复合语句(语句体):由大括号组成,无需在结尾加分号,形成独立的域(语句域)
顺序语句与非顺序语句
– 顺序语句
● 从语义上按照先后顺序执行
● 实际的执行顺序可能产生变化(编译器优化、硬件乱序执行)
● 与硬件流水线紧密结合,执行效率较高
– 非顺序语句
● 在执行过程中引入跳转,从而产生复杂的变化
● 分支预测错误可能导致执行性能降低
非顺序语句 goto
● 最基本的非顺序语句: goto
– 通过标签指定跳转到的位置
int x = 3;
if (x) goto label;
x = x + 1;
label:
return 0;
● 具有若干限制
– 不能跨函数跳转
– 向前跳转时不能越过对象初始化语句
– 向后跳转可能会导致对象销毁与重新初始化
● goto 本质上对应了汇编语言中的跳转指令
– 缺乏结构性的含义
– 容易造成逻辑混乱
– 除特殊情况外,应避免使用
分支语句 if
● 语法: https://zh.cppreference.com/w/cpp/language/if
int x = 3;
if (x > 2)
{
std :: cout << "x > 2" << std :: endl;
}
else
{
std :: cout << "x <= 2" << std :: endl;
}
● 使用语句块表示复杂的分支逻辑
● 从 if 到 if-else
– 实现多重分支
– else 会与最近的 if 匹配
– 使用大括号改变匹配规则
● if V.S. if constexpr—— 运行期与编译期分支
constexpr int grade = 80;
if constexpr (grade < 60)
{
std :: cout << "fail\n";
}
else
{
std :: cout << "pass\n";
}
// 编译器会进行翻译(优化)
constexpr int grade = 80;
std :: cout << "pass\n";
● 带初始化语句的 if
int x = 3;
if (int y = x * 3; y > 100) // 使得定义的y只在if-else语句中存在
{
std :: cout << y << '\n';
}
else
{
std :: cout << -y << '\n';
}
int y = 3; // 可以再重新初始化y
std :: cout << y << '\n';
分支语句 switch
● 语法: https://zh.cppreference.com/w/cpp/language/switch
● 条件部分应当能够隐式转换为整形或枚举类型,可以包含初始化的语句
● case/default 标签
– case 后面跟常量表达式 , 用于匹配 switch 中的条件,匹配时执行后续的代码 (不加break,会出现 fall through 现象)
– 可以使用 break 跳出当前的 switch 执行
– default 用于定义缺省情况下的逻辑
– 在 case/default 中定义对象要加大括号
int x;
switch (std :: cin >> x; x)
{
case 3:
std :: cout << "Hello\n";
break;
case 4:
{
int y = 4;
std :: cout << "World\n";
break;
}
default:
std :: cout << "China\n";
break;
}
● [[fallthrough]] 属性(C++17引入的)
case 3:
std :: cout << "Hello\n";
[[fallthrough]];
-Wimplicit-fallthrough
在编译器中添加参数 -g;-O0;-Wall;--std=c++2a;-Wimplicit-fallthrough
可以在出现 fall through 现象后抛出警告
● 与 if 相比的优劣
– 分支描述能力较弱
– 在一些情况下能引入更好的优化(跳表)
循环语句 while
● 语法: https://zh.cppreference.com/w/cpp/language/while
int x = 0;
while (x <= 3)
{
std :: cout << x << std :: endl;
x = x + 1;
}
● 处理逻辑:
– 1. 判断条件是否满足,如果不满足则跳出循环
– 2. 如果条件满足则执行循环体
– 3. 执行完循环体后转向步骤 1
● 注意:在 while 的条件部分不包含额外的初始化内容
循环语句 do-while
● 语法: https://zh.cppreference.com/w/cpp/language/do
– 注意结尾处要有分号,表示一条语句的结束
- 至少执行一次
int x = 0;
do
{
std :: cout << x << std :: endl;
x = x + 1;
}while (x <= 3);
● 处理逻辑:
– 1. 执行循环体
– 2. 判断条件是否满足,如果不满足则跳出循环
– 3. 如果条件满足则转向步骤 1
● 注意:在 while 的条件部分不包含额外的初始化内容
循环语句 for
● 语法: https://zh.cppreference.com/w/cpp/language/for
for (int x = 0; x < 5; ++x)
{
std :: cout << x << std :: endl;
}
● 处理逻辑
– 1. 初始化语句会被首先执行
– 2. 条件部分会被执行,执行结果如果为 false ,则终止循环
– 3. 否则执行循环体
– 4. 迭代表达式会被求值,之后转向 2
● 在初始化语句中声明多个名字,需要它们能够拥有相同的声明说明符序列
● 初始化语句、条件、迭代表达式可以为空
for ( ; ; )
{
}
循环语句 基于范围的for循环
● 语法: https://zh.cppreference.com/w/cpp/language/range-for
#include <vector>
std :: vector<int> arr{1, 2, 3, 4, 5};
for (int v : arr)
std :: cout << v << '\n';
● 本质:语法糖,编译器会转换为 for 循环的调用方式
● 转换形式的衍化: C++11 / C++17 / C++20
● 使用常量左值引用读元素
#include <vector>
#include <string>
std :: vector<std :: string> arr{"h", "e", "l", "l", "o"};
for (std :: string v : arr) // 创建了一个对象,比较耗时
std :: cout << v << '\n';
#include <vector>
#include <string>
std :: vector<std :: string> arr{"h", "e", "l", "l", "o"};
for (const std :: string& v : arr) // 添加了 const 和 &,更高效
std :: cout << v << '\n';
● 使用 “ 万能引用( universal reference ) ” 修改元素
std :: vector<int> arr{1, 2, 3};
for (auto& v : arr) // 非常量的左值引用
v = v + 1;
for (auto v : arr)
{
std :: cout << v << std :: endl;
}
std :: vector<bool> arr{true, false, true};
for (auto&& v : arr) // 修改一个范围里的元素,通常使用万能引用
v = false;
for (auto v : arr)
{
std :: cout << v << std :: endl;
}
循环语句 break/continue
● 含义(转自 cpp reference )
– break: 导致外围的 for 、范围 for 、 while 或 do-while 循环或 switch 语句终止
– continue: 用于跳过整个 for 、 while 或 do-while 循环体的剩余部分。
● 注意这二者均不能用于多重嵌套循环,多重嵌套循环的跳转可考虑 goto 语句
达夫设备
使用循环展开提升系统性能
处理无法整除的情形
额外增加一个循环语句
将 switch 与循环结合
// 原始代码
constexpr size_t buffer_count = 1000;
std :: vector<size_t> buffer(buffer_count);
for (size_t i = 0; i < buffer_count; ++i)
{
buffer[i] = i;
}
size_t max_value = buffer[0];
for (size_t i = 0; i < buffer_count; ++i) // 比较耗费资源
{
max_value = (max_value > buffer[i]) ? max_value : buffer[i]; // 希望把时间、资源都放在这一步,处理具体的逻辑
}
std :: cout << max_value << std :: endl;
// 第一次优化
constexpr size_t buffer_count = 1000; // 但是改为1001后会出错,出现内存访问越界
std :: vector<size_t> buffer(buffer_count);
for (size_t i = 0; i < buffer_count; ++i)
{
buffer[i] = i;
}
size_t max_value = buffer[0];
for (size_t i = 0; i < buffer_count; i += 8)
{
max_value = (max_value > buffer[i]) ? max_value : buffer[i];
max_value = (max_value > buffer[i+1]) ? max_value : buffer[i+1];
max_value = (max_value > buffer[i+2]) ? max_value : buffer[i+2];
max_value = (max_value > buffer[i+3]) ? max_value : buffer[i+3];
max_value = (max_value > buffer[i+4]) ? max_value : buffer[i+4];
max_value = (max_value > buffer[i+5]) ? max_value : buffer[i+5];
max_value = (max_value > buffer[i+6]) ? max_value : buffer[i+6];
max_value = (max_value > buffer[i+7]) ? max_value : buffer[i+7];
}
std :: cout << max_value << std :: endl;
// 第二次优化
constexpr size_t buffer_count = 1001;
std :: vector<size_t> buffer(buffer_count);
for (size_t i = 0; i < buffer_count; ++i)
{
buffer[i] = i;
}
size_t max_value = buffer[0];
for (size_t i = 0; i + 8 <= buffer_count; i += 8)
{
max_value = (max_value > buffer[i]) ? max_value : buffer[i];
max_value = (max_value > buffer[i+1]) ? max_value : buffer[i+1];
max_value = (max_value > buffer[i+2]) ? max_value : buffer[i+2];
max_value = (max_value > buffer[i+3]) ? max_value : buffer[i+3];
max_value = (max_value > buffer[i+4]) ? max_value : buffer[i+4];
max_value = (max_value > buffer[i+5]) ? max_value : buffer[i+5];
max_value = (max_value > buffer[i+6]) ? max_value : buffer[i+6];
max_value = (max_value > buffer[i+7]) ? max_value : buffer[i+7];
}
for (size_t i = buffer_count / 8 * 8; i < buffer_count; ++i)
{
max_value = (max_value > buffer[i]) ? max_value : buffer[i];
}
std :: cout << max_value << std :: endl;
// 第三次优化(使用指针,格式统一,并使用switch)
constexpr size_t buffer_count = 1001;
std :: vector<size_t> buffer(buffer_count);
for (size_t i = 0; i < buffer_count; ++i)
{
buffer[i] = i;
}
size_t max_value = buffer[0];
auto ptr = buffer.begin();
for (size_t i = 0; i + 8 <= buffer_count; i += 8)
{
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
}
switch (buffer_count % 8)
{
case 7 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 6 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 5 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 4 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 3 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 2 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 1 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
}
std :: cout << max_value << std :: endl;
// 第四次优化
constexpr size_t buffer_count = 1000;
std :: vector<size_t> buffer(buffer_count);
for (size_t i = 0; i < buffer_count; ++i)
{
buffer[i] = i;
}
size_t max_value = buffer[0];
auto ptr = buffer.begin();
switch (buffer_count % 8) // 数组个数不能为零
{
case 0 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]]; // 重点
case 7 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 6 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 5 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 4 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 3 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 2 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 1 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
}
for (size_t i = 0; i < (buffer_count - 1) / 8; ++i)
{
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
}
std :: cout << max_value << std :: endl;
// 典型的达夫设备
constexpr size_t buffer_count = 1001;
std :: vector<size_t> buffer(buffer_count);
for (size_t i = 0; i < buffer_count; ++i)
{
buffer[i] = i;
}
size_t max_value = buffer[0];
auto ptr = buffer.begin();
size_t i = 0;
// 达夫设备 switch语句里面套循环
switch (buffer_count % 8) // 数组个数不能为零
for (; i < ((buffer_count - 1) + 8) / 8; ++i)
{ [[fallthrough]];
case 0 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]]; // 重点
case 7 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 6 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 5 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 4 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 3 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 2 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr; [[fallthrough]];
case 1 : max_value = (max_value > *ptr) ? max_value : *ptr; ++ptr;
}
std :: cout << max_value << std :: endl;
十一、函数
函数基础
函数
封装了一段代码,可以在一次执行过程中被反复调用。
#include <iostream>
int Add(int x, int y)
{
return x + y;
}
int main()
{
std :: cout << Add(2, 3) << std :: endl;
}
函数头
● 函数名称 —— 标识符,用于后续的调用
● 形式参数 —— 代表函数的输入参数
● 返回类型 —— 函数执行完成后所返回的结果类型
函数体
● 为一个语句块( block ),包含了具体的计算逻辑
函数声明与定义
– 函数声明只包含函数头,不包含函数体,通常置于头文件中
– 函数声明可出现多次,但函数定义通常只能出现一次(存在例外)
函数调用
– 需要提供函数名与实际参数
– 实际参数拷贝初始化形式参数
– 返回值会被拷贝给函数的调用者
– 栈帧结构(栈:后进先出)
https://www.codeguru.com/visual-studio/function-calls-part-3-frame-pointer-and-local-variables/
拷贝过程的(强制)省略
– 返回值优化
– C++17 强制省略拷贝临时对象
函数的外部链接
extern "C" // 标注这是一个C类型函数,不支持函数重载,具有C语言外部链接的形式,不会出现mangling
int Add(int x, int y)
{
return x + y;
}
函数详解
参数
● 函数可以在函数头的小括号中包含零到多个形参
– 包含零个形参时,可以使用 void 标记
void fun(void) // 也可以省略void
{
}
– 对于非模板函数来说,其每个形参都有确定的类型,但形参可以没有名称
void fun(int) // 预留之后可能用到的参数,作为接口
{
}
– 形参名称的变化并不会引入函数的不同版本
– 实参到形参的拷贝求值顺序不定
– C++17 强制省略复制临时对象
// verison 1
#include <iostream>
struct Str
{
Str() = default;
Str (const Str&)
{
std :: cout << "Copy constructor is called. \n";
}
};
void fun(Str par)
{
}
int main()
{
Str val;
fun(val); // 会打印 Copy constructor is called.
}
// verison 2
#include <iostream>
struct Str
{
Str() = default;
Str (const Str&)
{
std :: cout << "Copy constructor is called. \n";
}
};
void fun(Str par)
{
}
int main()
{
fun(Str{}); // 此时改为传入临时变量,不会打印 Copy constructor is called.
}
● 函数传值、传址、传引用
// 传值
void fun(int par)
{
++par;
}
int main()
{
int arg = 3;
fun(arg);
std :: cout << arg << '\n'; // 3
}
// 传址
void fun(int* par)
{
++(*par);
}
int main()
{
int arg = 3;
fun(&arg);
std :: cout << arg << '\n'; // 4
}
// 传引用
void fun(int& par)
{
++par;
}
int main()
{
int arg = 3;
fun(arg);
std :: cout << arg << '\n'; // 4
}
● 函数传参过程中的类型退化
// 还是可以通过加引用&的方式避免退化
void fun(int (&par)[3])
{
}
int main()
{
int a[3];
fun(a);
}
● 变长参数
– initializer_list // 类型都得相同
#include <initializer_list>
void fun(std :: initializer_list<int> par)
{
}
int main()
{
fun({1, 2, 3, 4, 5});
}
– 可变长度模板参数 // 可以传入不同类型的参数
– 使用省略号表示形式参数(C语言方法--如printf)(C++中不推荐使用)
● 函数可以定义缺省实参
void fun(int x = 0)
{
std :: cout << x << '\n';
}
int main()
{
fun(); // 0
fun(1); // 1
}
– 如果某个形参具有缺省实参,那么它右侧的形参都必须具有缺省实参
– 在一个翻译单元中,每个形参的缺省实参只能定义一次
– 具有缺省实参的函数调用时,传入的实参会按照从左到右的顺序匹配形参
– 缺省实参为对象时,实参的缺省值会随对象值的变化而变化
● main 函数的两个版本
– 无形参版本
– 带两个形参的版本
int main(int argc, char* argv[]) // argc-实参个数(非负数) argv-实参具体数值
// build以下程序,并在终端运行
// ~/cpp_learning/demo/main_demo/Debug$ ./main_demo
#include <iostream>
int main(int argc, char* argv[])
{
std :: cout << "argc = " << argc << std :: endl;
for (int i = 0; i < argc; ++i)
{
std :: cout << argv[i] << '\n';
}
}
函数体
● 函数体形成域:
– 其中包含了自动对象(内部声明的对象以及形参对象)
int x = 3;
– 也可包含局部静态对象
// 生存周期是从首次进入函数执行初始化语句开始,到整个程序执行结束为止,而不是到函数返回return为止。
static int x = 3;
● 函数体执行完成时的返回
– 隐式返回
// main函数出现隐式返回时,编译器不会给出警告
– 显式返回关键字: return
-- return; 语句
-- return 表达式 ;
-- return 初始化列表 ;
– 小心返回自动对象的引用或指针
– 返回值优化(RVO-return value optimism) —— C++17 对返回临时对象的强制优化
-- 具名返回值优化(返回值有具体的名称,return x;)
-- 非具名返回值优化(返回值没有具体的名称,return Str{};)
-fno-elide-constructors
编译器中加入参数-fno-elide-constructors,避免对返回临时对象的强制优化
#include <iostream>
#include <initializer_list>
struct Str
{
Str() = default;
Str(const Str&)
{
std :: cout << "Copy constructor is called\n";
}
};
Str fun()
{
Str x;
return x;
}
int main()
{
Str res = fun();
}
返回类型
● 返回类型表示了函数计算结果的类型,可以为 void
● 返回类型的几种书写方式
– 经典方法:位于函数头的前部
void fun()
{
}
– C++11 引入的方式:位于函数头的后部
auto fun() -> int
{
}
// 用于简化特殊情况下的函数定义
auto S :: fun() -> MyInt // S :: MyInt S :: fun()
{
}
– C++14 引入的方式:返回类型的自动推导
-- 使用 constexpr if 构造 “ 具有不同返回类型 ” 的函数
// 编译期具有不同返回类型,一旦执行到具体的return语句,就会退化到运行期只具有一种返回类型的函数。
constexpr bool value = true;
auto fun()
{
if constexpr (value)
{
return 1;
}
else
{
return 3.14;
}
}
● 返回类型与结构化绑定( C++ 17 )(语法糖)
struct Str
{
int x;
int y;
};
Str fun()
{
return Str{};
}
int main()
{
auto [v1, v2] = fun();
v1;
v2;
}
● [[nodiscard]] 属性( C++ 17 )
// 提醒使用者注意函数返回值的重要性
// 当使用者仅仅是调用函数,而未接收/使用函数返回值时,编译器会抛出警告
[[nodiscard]] int fun (int a, int b)
{
return a+b;
}
int main()
{
fun(2, 3);
}
函数重载与重载解析
● 函数重载:使用相同的函数名定义多个函数,每个函数具有不同的参数列表
#include <iostream>
int fun(int x)
{
return x+1;
}
double fun(double x)
{
return x+1;
}
int main()
std :: cout << fun(3) << std :: endl;
std :: cout << fun(3.5) << std :: endl;
}
– 不能基于不同的返回类型进行重载
int fun(int x)
{
return x+1;
}
double fun(int x)
{
return x+1;
}
- 函数重载与 name mangling
● 编译器如何选择正确的版本完成函数调用?
– 参考资源: Calling_Functions: A Tutorial
https://www.youtube.com/watch?v=GydNMuyQzWo&list=PLHTh1InhhwT6DdPY3CPxayypP5DXek_vG&index=7
名称查找
– 限定查找( qualified lookup )与非限定查找( unqualified lookup )
// 限定查找(对查找的区域进行了限定)
#include <iostream>
void fun()
{
std :: cout << "global fun is called. \n";
}
namespace MyNS
{
void fun()
{
std :: cout << "MyNS fun is called. \n";
}
}
int main()
{
:: fun(); // 全局域
MyNS :: fun();
}
– 非限定查找会进行域的逐级查找 —— 名称隐藏( hiding )
#include <iostream>
void fun()
{
std :: cout << "global fun is called. \n";
}
namespace MyNS
{
void fun()
{
std :: cout << "MyNS fun is called. \n";
}
void g()
{
fun(); // 非限定查找
}
}
int main()
{
MyNS :: g();
}
– 查找通常只会在已声明的名称集合中进行(函数模板是特殊情况)
#include <iostream>
void fun(int)
{
std :: cout << "global fun is called. \n";
}
namespace MyNS
{
void fun(double); // 声明的使用
void g()
{
fun(3); // 非限定查找
}
void fun(double)
{
std :: cout << "MyNS fun is called. \n";
}
}
int main()
{
MyNS :: g();
}
– 实参依赖查找( Argument Dependent Lookup: ADL )
-- 只对自定义类型生效
#include <iostream>
namespace MyNS
{
struct Str
{
};
void g(Str x)
{
}
}
int main()
{
MyNS :: Str obj;
g(obj); // 非限定性查找,参数是名字空间内定义的结构体
}
(狭义的)重载解析
在名称查找的基础上进一步选择合适的调用函数
https://zh.cppreference.com/w/cpp/language/overload_resolution
– 过滤不能被调用的版本 (non-viable candidates)
-- 参数个数不对
-- 无法将实参转换为形参
-- 实参不满足形参的限制条件
#include <iostream>
#include <string>
void fun(int x)
{
}
void fun(std :: string x)
{
}
int main()
{
fun(3);
}
– 在剩余版本中查找与调用表达式最匹配的版本,匹配级别越低越好(有特殊规则)
-- 级别 1 :完美匹配 或 平凡转换(比如加一个 const )
-- 级别 2 : promotion 或 promotion 加平凡转换
-- 级别 3 :标准转换 或 标准转换加平凡转换
-- 级别 4* :自定义转换 或 自定义转换加平凡转换 或 自定义转换加标准转换
-- 级别 5* :形参为省略号的版本
-- 函数包含多个形参时,所选函数的所有形参的匹配级别都要优于或等于其它函数
函数相关的其它内容
递归函数
在函数体中调用其自身的函数
通常用于描述复杂的迭代过程
内联函数(优化机制)
inline 说明符
// main.cpp
#include <iostream>
#include "header.h"
int main()
{
fun();
}
// source.cpp
#include <iostream>
#include "header.h"
void fun2()
{
fun();
}
// header.h
#include <iostream>
inline void fun() // inline---满足“翻译单元”的一处定义原则
{
std :: cout << "Hello World \n";
}
constexpr 函数(C++11 起 )
#include <iostream>
constexpr int fun() // 使函数的调用可以在编译期完成(要保证函数内部的语句都能在编译期完成才能使用)
{
return 3;
}
int main()
{
constexpr int x = fun(); // 在编译期完成
return x;
}
consteval 函数 (C++20 起 )
#include <iostream>
consteval int fun(int x) // 标注该函数只能在编译期完成
{
return x+1;
}
int main()
{
constexpr int y = 4;
return fun(y);
}
函数指针
从C语言继承过来的概念
函数类型
用处较少
int fun(int x) // 函数类型为 int(int)
{
return x+1;
}
// 函数声明
using K = int(int);
K fun; // 但是不能写成 int(int) fun;
函数指针类型
// 基础用法
#include <iostream>
int inc(int x)
{
return x+1;
}
int dec(int x)
{
return x-1;
}
using K = int(int);
int main()
K* fun = &inc; // 变成构造了一个变量,这个变量叫做fun,这个变量是一个指针,它指向一个函数,这个函数可以接收一个int类型的形参,返回一个int类型的数值
std :: cout << (*fun)(100) << std :: endl;
}
// 作为高阶函数使用,接收或返回另一个函数
#include <iostream>
int inc(int x)
{
return x+1;
}
int dec(int x)
{
return x-1;
}
using K = int(int);
int Twice(K* fun, int x)
{
int tmp = (*fun)(x);
return tmp*2;
}
int main()
{
std :: cout << Twice(&inc, 100) << std :: endl;
std :: cout << Twice(&dec, 100) << std :: endl;
}
// 泛型算法
#include <iostream>
#include <vector>
#include <algorithm>
#include <iostream>
int inc(int x)
{
return x+1;
}
int dec(int x)
{
return x-1;
}
using K = int(int);
int Twice(K* fun, int x)
{
int tmp = (*fun)(x);
return tmp*2;
}
int main()
{
std :: vector<int> a{1, 2, 3, 4, 5};
std :: transform(a.begin(), a.end(), a.begin(), &inc); // 泛型算法
for (int i = 0; i < 5; ++i)
{
std :: cout << a[i] << std :: endl;
}
std :: transform(a.begin(), a.end(), a.begin(), &dec);
std :: transform(a.begin(), a.end(), a.begin(), &dec);
for (int i = 0; i < 5; ++i)
{
std :: cout << a[i] << std :: endl;
}
}
// 函数也不能复制
#include <iostream>
#include <vector>
#include <algorithm>
#include <iostream>
int inc(int x)
{
return x+1;
}
int dec(int x)
{
return x-1;
}
using K = int(int);
int Twice(K* fun, int x)
{
int tmp = (*fun)(x);
return tmp*2;
}
int main()
{
auto fun = inc; // 和数组一样,函数也是不能复制的,会退化为函数指针类型
}
函数指针与重载
#include <iostream>
#include <vector>
#include <algorithm>
void fun(int)
{
}
void fun(int, int)
{
}
int main()
{
using K = void(int); // 显式地给出函数的类型
K* x = fun;
}
函数指针作为函数参数
#include <iostream>
#include <vector>
#include <algorithm>
int dec(int x)
{
return x-1;
}
std :: vector<int> a{1, 2, 3, 4, 5};
std :: transform(a.begin(), a.end(), a.begin(), &dec);
将函数指针作为函数返回值
#include <iostream>
#include <vector>
#include <algorithm>
int inc(int x)
{
return x+1;
}
int dec(int x)
{
return x-1;
}
auto fun(bool input)
{
if (input)
{
return inc;
}
else
{
return dec;
}
}
int main()
{
std :: cout << (*fun(true))(100) << std :: endl;
std :: cout << (*fun(false))(100) << std :: endl;
}
Most vexing parse
最令人苦恼的解析
// 大括号内部不能放类型,只能放对象
TimeKeeper time_keeper(Timer{}); // 构造Timer对象,将其赋给time_keeper
TimeKeeper time_keeper{Timer()}; // Timer()可能是函数调用,也可能是Timer对象的构造,但是Timer()返回的一定是个对象,用这个对象初始化time_keeper
TimeKeeper time_keeper{Timer{}}; // 构造Timer的临时对象,用这个对象初始化time_keeper
十二、深入 IO
IOStream 概述
● IOStream 采用流式 I/O 而非记录 I/O ,但可以在此基础上引入结构信息
● 所处理的两个主要问题
– 表示形式的变化:使用格式化 / 解析在数据的内部表示与字符序列间转换
– 与外部设备的通信:针对不同的外部设备(终端、文件、内存)引入不同的处理逻辑
● 所涉及到的操作
– 格式化 / 解析
– 缓存
– 编码转换
– 传输
● 采用模板来封装字符特性,采用继承来封装设备特性
– 常用的类型实际上是类模板实例化的结果
输入与输出
● 输入与输出分为格式化与非格式化两类
● 非格式化 I/O :不涉及数据表示形式的变化
– 常用输入函数: get / read / getline / gcount
– 常用输入函数: put / write
#include <iostream>
int main()
{
int x;
std :: cin.read(reinterpret_cast<char*>(&x), sizeof(x));
std :: cout << x << std :: endl;
}
● 格式化 I/O :使用移位操作符来进行的输入 (>>) 与输出 (<<)
– C++ 通过操作符重载以支持内建数据类型的格式化 I/O
– 可以通过重载操作符以支持自定义类型的格式化 I/O
● 格式控制
– 可接收位掩码类型( showpos )、字符类型( fill )与取值相对随意( width )的格式化参数
– 注意 width 方法的特殊性:触发后被重置
// e.g.1
#include <iostream>
int main()
{
char x = '0';
std :: cout.setf(std :: ios_base :: showpos); // set flag(位) show positive
std :: cout << x << std :: endl; // 字符,不存在正负
int y = static_cast<int>(x);
std :: cout << y << std :: endl;
}
// e.g.2
#include <iostream>
int main()
{
int x = 101;
std :: cout.width(10); // 输出需要占到十个字符,触发后会重置
std :: cout << x << std :: endl;
}
// e.g.3
#include <iostream>
int main()
{
int x = 101;
std :: cout.width(10); // 输出需要占到十个字符,触发后会重置
std :: cout.fill('.'); // 空白在缺省情况下填充 . (默认是空格)
std :: cout << x << std :: endl;
}
● 操纵符
– 简化格式化参数的设置
– 触发实际的插入与提取操作
#include <iostream>
#include <iomanip>
int main()
{
char x = '1';
int y = static_cast<int>(x);
std :: cout << std :: showpos
<< std :: setw(10)
<< std :: setfill('.') << x << '\n'
<< std :: setw(10) << y << '\n';
}
● 提取会放松对格式的限制(只对部分类型有效)
#include <iostream>
#include <iomanip>
int main()
{
int x;
std :: cin >> x;
std :: cout << x << std :: endl;
}
● 提取 C 风格字符串时要小心内存越界
#include <iostream>
#include <iomanip>
#include <string>
int main()
char x[5]; // 类比C语言的字符串
// std :: cin >> x;
std :: cin >> std :: setw(5) >> x; // 避免内存越界,会自动在结尾插入结束符'\0'
std :: cout << x << std :: endl;
}
文件与内存操作
● 文件操作
– basic_ifstream / basic_ofstream / basic_fstream
– 文件流可以处于打开 / 关闭两种状态,处于打开状态时无法再次打开,只有打开时才能 I/O
// 输出流
#include <iostream>
#include <fstream>
int main()
{
std :: ofstream outFIle("my_file");
outFIle << "Hello\n";
}
// 输入流
#include <iostream>
#include <fstream>
#include <string>
int main()
{
std :: ifstream inFIle("my_file");
std :: string x;
inFIle >> x;
std :: cout << x << std ::endl;
}
// open / close
int main()
{
std :: ofstream outFIle;
std :: cout << outFIle.is_open() << std :: endl;
outFIle.open("my_file");
std :: cout << outFIle.is_open() << std :: endl;
outFIle << "Hello\n";
outFIle.close();
std :: cout << outFIle.is_open() << std :: endl;
std :: ifstream inFIle("my_file");
std :: string x;
inFIle >> x;
std :: cout << x << std ::endl;
}
● 文件流的打开模式(图引自 C++ IOStream 一书)
– 每种文件流都有缺省的打开方式
– 注意 ate 与 app 的异同
// 使用app打开的文件,其指针不能更改位置,只能一直在文件结尾处写入内容
– binary 能禁止系统特定的转换
– 避免意义不明确的流使用方式(如 ifstream + out )
#include <iostream>
#include <fstream>
#include <string>
int main()
{
// 以写的方式打开文件,并将之前文件里的内容删除
std :: ofstream outFile("my_file", std :: ios_base :: out | std :: ios_base :: trunc);
outFile << "World\n";
}
● 合法的打开方式组合(引自 C++ IOStream 一书)
#include <iostream>
#include <fstream>
#include <string>
int main()
{
// 以"附加"写的方式打开文件
std :: ofstream outFile("my_file", std :: ios_base :: out | std :: ios_base :: app);
outFile << "China\n";
}
● 内存流: basic_istringstream / basic_ostringstream / basic_stringstream
// 输出流
#include <iostream>
#include <sstream>
#include <iomanip>
int main()
{
std :: ostringstream obj1;
obj1 << std :: setw(10) << std :: setfill('.') << 1234; // 写入的是1234,一个整数,完成了从整数到字符串的隐式类型转换
std :: string res = obj1.str(); // 获取底层所对应的内存
std :: cout << res << std :: endl; // 读出的是"1234",一个字符串
}
// 输入流
#include <iostream>
#include <sstream>
#include <iomanip>
int main()
{
std :: ostringstream obj1;
obj1 << 1234; // 写入的是1234,一个整数,完成了从整数到字符串的隐式类型转换
std :: string res = obj1.str(); // 获取底层所对应的内存
std :: istringstream obj2(res); // 从一块内存中进行读取,作为输入流
int x;
obj2 >> x;
std :: cout << x << std :: endl;
}
● 也会受打开模式: in / out / ate / app 的影响
● 使用 str() 方法获取底层所对应的字符串
– 小心避免使用 str().c_str() 的形式获取 C 风格字符串
std :: ostringstream buf2("test");
buf2 << '1';
auto c_res = buf2.str().c_str();
std :: cout << c_res << std :: endl;
// buf2.str()返回的是右值,buf2.str().c_str() / c_res 指向内存的首地址
// 由于是右值,auto c_res = buf2.str().c_str(); 语句执行完之后, buf2 会被释放掉,此后 c_res 就会指向一块被释放的内存,变成未定义
// 但是可以改为 std :: cout << buf2.str().c_str() << std :: endl; 在一条语句内完成操作
● 基于字符串流的字符串拼接优化操作
#include <iostream>
#include <sstream>
#include <iomanip>
int main()
{
std :: string x;
x += "Hello";
x += " World";
x += " Hello";
x += " World";
std :: cout << x << std :: endl;
std :: ostringstream obj;
obj << "Hello";
obj << " World";
obj << " Hello";
obj << " World";
std :: cout << obj.str() << std :: endl; // 性能好
}
流的状态、定位与同步
流的状态
● iostate
– failbit / badbit / eofbit / goodbit // 位掩码类型
// badbit 不可恢复的流错误
#include <iostream>
#include <fstream>
int main()
{
std :: ofstream outFile;
outFile << 10;
}
// failbit 输入/输出操作失败(格式化或提取错误)-- 可以恢复的错误
#include <iostream>
#include <fstream>
int main()
{
int x;
std :: cin >> x; // 终端输入“hello”
}
// eofbit (end of file)关联的输入序列已抵达文件尾,没办法再继续读取了(对终端流、内存流也都是有效的)
在终端等待输入界面中,键盘按下 Ctrl+D(Windows输入Ctrl+C)(表示在终端中的输入达到了终端的结尾,后面不会再有终端的输入了)
● 检测流的状态
– good( ) / fail() / bad() / eof() 方法
– 转换为 bool 值( 参考 cppreference )
#include <iostream>
#include <fstream>
int main()
{
int x;
std :: cin >> x;
std :: cout << std :: cin.good() << '\n';
std :: cout << std :: cin.fail() << '\n';
std :: cout << std :: cin.bad() << '\n';
std :: cout << std :: cin.eof() << '\n';
std :: cout << static_cast<bool>(std :: cin) << '\n';
}
● 注意区分 fail 与 eof
– 可能会被同时设置,但二者含意不同
– 转换为 bool 值时不会考虑 eof
```cpp
● 通常来说,只要流处于某种错误状态时,插入 / 提取操作就不会生效
● 复位流状态
– clear :设置流的状态为具体的值(缺省为 goodbit )
– setstate :将某个状态附加到现有的流状态上
● 捕获流异常:exceptions方法
流的定位
● 获取流位置
– tellg() / tellp() 可以用于获取输入 / 输出流位置 (pos_type 类型 )
– 两个方法可能会失败,此时返回 pos_type(-1)
// tellp()
#include <iostream>
#include <sstream>
int main()
{
std::ostringstream s;
std::cout << s.tellp() << '\n';
s << 'h';
std::cout << s.tellp() << '\n';
s << "ello, world ";
std::cout << s.tellp() << '\n';
s << 3.14 << '\n';
std::cout << s.tellp() << '\n' << s.str();
}
// tellg()
#include <iostream>
#include <sstream>
#include <string>
int main()
{
std::string str = "Hello, world";
std::istringstream in(str);
std::string word;
in >> word;
std::cout << "After reading the word \"" << word
<< "\" tellg() returns " << in.tellg() << '\n';
}
● 设置流位置
– seekg() / seekp() 用于设置输入 / 输出流的位置
– 这两个方法分别有两个重载版本:
-- 设置绝对位置:传入 pos_type 进行设置
-- 设置相对位置:通过偏移量(字符个数 ios_base::beg ) + 流位置符号的方式设置
--- ios_base::beg
--- ios_base::cur
--- ios_base::end
// seekg()
#include <iostream>
#include <sstream>
#include <string>
int main()
{
std::string str = "Hello, world";
std::istringstream in(str);
std::string word1, word2;
in >> word1;
in.seekg(0); // 回溯到字符串最开始的位置
in >> word2;
std::cout << "word1 = " << word1 << '\n'
<< "word2 = " << word2 << '\n';
}
// seekp()
#include <iostream>
#include <sstream>
int main()
{
std::ostringstream os("hello, world");
os.seekp(7);
os << 'W';
os.seekp(0, std::ios_base::end);
os << '!';
os.seekp(0);
os << 'H';
std::cout << os.str() << '\n';
}
流的同步
● 基于 flush() / sync() / unitbuf 的同步
– flush() 用于“输出流”同步,刷新缓冲区
std::cout << "What's your name" << std :: endl;
// 等同于std::cout << "What's your name\n" << std :: flush;
– sync() 用于“输入流”同步,其实现逻辑是编译器所定义的
– 输出流可以通过设置 unitbuf 来保证每次输出后自动同步
● 基于绑定 (tie) 的同步
– 流可以绑定到一个输出流上,这样在每次输入 / 输出前可以刷新输出流的缓冲区
– 比如: cin 绑定到了 cout 上
● 与 C 语言标准 IO 库的同步
– 缺省情况下, C++ 的输入输出操作会与 C 的输入输出函数同步
– 可以通过 sync_with_stdio 关闭该同步
#include <cstdio>
#include <iostream>
int main()
{
std::ios::sync_with_stdio(false);
std::cout << "a\n";
std::printf("b\n");
std::cout << "c\n";
}
十三、动态内存管理
动态内存基础
栈内存 V.S. 堆内存
https://www.linkedin.com/pulse/what-where-stack-heap-maxim-malisciuc
– 栈内存的特点:更好的局部性,对象自动销毁
– 堆内存的特点:运行期动态扩展,需要显式释放
// e.g.1
#include <iostream>
int main()
{
int x; // 栈内存
x = 2;
std :: cout << x << std :: endl;
int* y = new int(2); // 使用new分配了一块堆内存,占int的大小,
// 把2保存在这块内存中,把指向这块内存的首地址返回回来
std :: cout << *y << std :: endl;
}
// e.g.2
#include <iostream>
int main()
{
int* y = new int(2);
std :: cout << *y << std :: endl;
delete y; // y所对应的那块内存不再使用,供其他new使用
}
// e.g.3
#include <iostream>
int* fun()
{
int* y = new int(2);
return y;
}
int main()
{
int* res = fun();
std :: cout << *res << std :: endl;
delete res;
}
对象的构造、销毁
-
在 C++ 中通常使用 new 与 delete 来构造、销毁对象
-
对象的构造分成两步:分配内存与在所分配的内存上构造对象;对象的销毁与之类似
new 的几种常见形式
构造单一对象 / 对象数组
int* y = new int(2); // 开辟了4个字节
int* x = new int[5]{1, 2, 3, 4, 5}; // 开辟了20个字节
std :: cout << x[2] << std :: endl;
delete[] x;
nothrow new
#include <iostream>
#include <new>
int main()
{
int* y = new (std :: nothrow) int[5]{1, 2, 3, 4, 5};
if (y == nullptr)
{
// ...
}
std :: cout << y[2] << std :: endl;
delete[] y;
}
placement new
已经有了一块内存,不再需要重新分配内存,只要在这块内存上构造对象
#include <iostream>
#include <new>
int main()
{
char ch[sizeof(int)]; // 在栈当中开辟了一块内存,这块内存是一个char类型的数组,
// 包含了sizeof(int)个元素
int* y = new (ch)int(4); // ch先进行隐式转换,从char数组转换为char*指针
// 在new之后再次进行隐式转换,从char*指针转换为void*指针
std :: cout << *y << std :: endl;
}
new auto
int* y = new auto(3);
new 与对象对齐
#include <iostream>
#include <new>
struct alignas(256) Str{}; // Str开辟了一块内存,这块内存的首地址一定得是256的整数倍
// 以256个字节对齐为单位
int main()
{
Str* ptr = new Str();
std :: cout << ptr << std :: endl; // 打印出来,最后两位一定是0
}
delete 的常见用法
销毁单一对象 / 对象数组
int* ptr = new int;
delete ptr;
int* ptr = new int[5];
delete[] ptr;
placement delete
只销毁对象,不把内存归还给系统
使用 new 与 delete 的注意事项
– 根据分配的是单一对象还是数组,采用相应的方式销毁
– delete nullptr
int* x = 0; // 或者 int* x = nullptr;
delete x; // 指针指向的内存如果是0或者nullptr,delete什么都不做
– 不能 delete 一个非 new 返回的内存
– 同一块内存不能 delete 多次
调整系统自身的 new / delete 行为
不要轻易使用
智能指针
使用 new 与 delete 的问题:内存所有权不清晰,容易产生不销毁,多销毁的情况
C++ 的解决方案:智能指针
– auto_ptr ( C++17 删除)
– shared_ptr / uniuqe_ptr / weak_ptr
shared_ptr
基于引用计数的共享内存解决方案
基本用法
#include <iostream>
#include <memory>
int main()
{
std :: shared_ptr<int> x(new int(3)); // 类模板,传入int,
// 代表这个智能指针可以指向int类型的对象
// 使用new分配了一块内存,占int的大小
// 使用3初始化这块内存,再把这块内存的地址返回给x
// int* x = new int(3);
}
#include <iostream>
#include <memory>
std :: shared_ptr<int> fun()
{
std :: shared_ptr<int> res(new int(3));
return res;
}
int main()
{
std :: shared_ptr<int> x = fun();
}
get 方法
#include <iostream>
#include <memory>
std :: shared_ptr<int> fun()
{
std :: shared_ptr<int> res(new int(3));
return res;
}
void fun2(int* x)
{
std :: cout << *x << std :: endl;
}
int main()
{
std :: shared_ptr<int> x = fun();
fun2(x.get()); // get方法返回传入参数的指针,用于兼容没有使用智能指针的程序
// 与C语言使用裸指针的程序结合
}
reset 方法
#include <iostream>
#include <memory>
std :: shared_ptr<int> fun()
{
std :: shared_ptr<int> res(new int(3));
return res;
}
void fun2(int* x)
{
std :: cout << *x << std :: endl;
}
int main()
{
std :: shared_ptr<int> x = fun();
x.reset(new int(4)); // 接受一个新的指针
// 首先判断原先这个shared_ptr智能指针是否关联了一个对象
// 如果关联了某个对象,直接调用delete,删除对象
// 把传入的对象重新进行关联
fun2(x.get()); // get方法返回传入参数的指针,用于兼容没有使用智能指针的程序
}
#include <iostream>
#include <memory>
std :: shared_ptr<int> fun()
{
std :: shared_ptr<int> res(new int(3));
return res;
}
int main()
{
std :: shared_ptr<int> x = fun();
x.reset(); // 等价于 x.reset((int*)nullptr);
// 删除 x 的内存,并且暂时不指向任何地址
}
指定内存回收逻辑
#include <iostream>
#include <memory>
void fun(int* ptr)
{
std :: cout << "Call deleter fun\n";
delete ptr;
}
int main()
{
std :: shared_ptr<int> x(new int(3), fun);
}
#include <iostream>
#include <memory>
void dummy(int*)
{
}
std :: shared_ptr<int> fun()
{
static int res = 3;
return std :: shared_ptr<int>(&res, dummy);
}
int main()
std :: shared_ptr<int> x = fun();
}
std::make_shared
#include <iostream>
#include <memory>
int main()
{
auto x = std :: make_shared<int>(3); // 系统将用来保存对象的内存和用来保存引用计数的内存开辟得尽量得近
}
支持数组
C++17 支持 shared_ptr<T[]>
#include <iostream>
#include <memory>
int main()
{
std :: shared_ptr<int[]> x(new int[5]);
}
C++20 支持 make_shared 分配数组
#include <iostream>
#include <memory>
int main()
{
auto x = std :: make_shared<int[5]>();
}
注意
shared_ptr 管理的对象不要调用 delete 销毁
unique_ptr
独占内存的解决方案
基本用法
#include <iostream>
#include <memory>
int main()
{
std :: unique_ptr<int> x(new int(3)); // x拥有了new int(3)这块内存,不能再写y=x了,不能共享内存了
}
不支持复制,但可以移动
#include <iostream>
#include <memory>
int main()
{
std :: unique_ptr<int> x(new int(3));
std :: cout << x.get() << std :: endl;
std :: unique_ptr<int> y = std :: move(x); // 基于x构造一个将亡值,将x里面所包含的这块地址复制给y
// 同时把x所对应的这块地址清空
std :: cout << x.get() << std :: endl;
std :: cout << y.get() << std :: endl;
}
#include <iostream>
#include <memory>
std :: unique_ptr<int> fun()
{
std :: unique_ptr<int> res(new int(3));
return res;
}
int main()
{
std :: unique_ptr<int> x = fun();
}
#include <iostream>
#include <memory>
int main()
{
auto x = std :: make_unique<int>(3);
}
为 unique_ptr 指定内存回收逻辑
#include <iostream>
#include <memory>
void fun(int* ptr)
{
std :: cout << "Fun is called\n";
delete ptr;
}
int main()
{
std :: unique_ptr<int, decltype(&fun)> x(new int(3), fun);
}
weak_ptr
防止循环引用而引入的智能指针
不会改变内部引用计数的值
基于 shared_ptr 构造
// shared_ptr版
#include <iostream>
#include <memory>
struct Str
{
std :: shared_ptr<Str> nei;
~Str()
{
std :: cout << "~Str is called\n";
}
};
int main()
{
std :: shared_ptr<Str> x(new Str);
std :: shared_ptr<Str> y(new Str);
x -> nei = y;
y -> nei = x; // x和y对应的两块内存不会被释放 ==> 循环引用(环)
}
// weak_ptr版
#include <iostream>
#include <memory>
struct Str
{
std :: weak_ptr<Str> nei;
~Str()
{
std :: cout << "~Str is called\n";
}
};
int main()
{
std :: shared_ptr<Str> x(new Str);
std :: shared_ptr<Str> y(new Str);
x -> nei = y;
y -> nei = x;
}
lock 方法
防止由于没有改变内部引用计数的值,而访问已经销毁的内存
#include <iostream>
#include <memory>
struct Str
{
std :: weak_ptr<Str> nei;
~Str()
{
std :: cout << "~Str is called\n";
}
};
int main()
{
std :: shared_ptr<Str> x(new Str);
{
std :: shared_ptr<Str> y(new Str);
x -> nei = y;
}
if (auto ptr = x -> nei.lock(); ptr)
{
std :: cout << "true branch\n";
}
else
{
std :: cout << "false branch\n";
}
}
动态内存的相关问题
sizeof 不会返回动态分配的内存大小
动态分配是在运行期完成的
sizeof 是在编译期完成的
编译期先于运行期完成
#include <iostream>
#include <memory>
int main()
{
int* ptr = new int[5];
std :: cout << sizeof(ptr) << std :: endl; // 返回的是指针本身的大小
}
使用分配器( allocator )来分配内存【推荐】
#include <iostream>
int main()
{
std :: allocator<int> al; // 基于类模板,初始化一个内存分配器,但是只能用于分配传入的类型的内存,如此处只能分配int类型的内存
int* ptr = al.allocate(3); // 分配一堆内存,要求这堆内存可以放下三个int,将这堆内存的地址返回
al.deallocate(ptr, 3); // 回收(释放)内存
}
使用 malloc / free 来管理内存
基于C语言
只能分配内存,不能构造对象
不能分配对齐内存(重大的缺陷)
// https://zh.cppreference.com/w/c/memory/malloc
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p1 = malloc(4*sizeof(int)); // 足以分配 4 个 int 的数组
int *p2 = malloc(sizeof(int[4])); // 等价,直接命名数组类型
int *p3 = malloc(4*sizeof *p3); // 等价,免去重复类型名
if(p1) {
for(int n=0; n<4; ++n) // 置入数组
p1[n] = n*n;
for(int n=0; n<4; ++n) // 打印出来
printf("p1[%d] == %d\n", n, p1[n]);
}
free(p1);
free(p2);
free(p3);
}
使用 aligned_alloc 来分配对齐内存
C语言引入的
// https://zh.cppreference.com/w/cpp/memory/c/aligned_alloc
#include <cstdio>
#include <cstdlib>
int main()
{
int* p1 = static_cast<int*>(std::malloc(10*sizeof *p1));
std::printf("默认对齐地址: %p\n", static_cast<void*>(p1));
std::free(p1);
int* p2 = static_cast<int*>(std::aligned_alloc(1024, 10*sizeof *p2));
std::printf("1024 字节对齐地址: %p\n", static_cast<void*>(p2));
std::free(p2);
}
动态内存与异常安全
如遇到处理异常的情况,可能会直接跳出某个正在执行的函数,导致没有正确释放内存,造成内存泄漏
异常安全:程序在抛出异常的情况下,也能保证程序(内存)安全
// 不安全
#include <iostream>
int main()
{
int* ptr = new int(3);
// 有可能经过复杂过程,也可能会出现抛出异常的情况
delete ptr;
}
// 异常安全
#include <iostream>
#include <memory>
int main()
{
std :: shared_ptr<int> x(new int(3));
// ...
}
C++ 对于垃圾回收的支持
欲拒还迎
垃圾回收需要额外开一个线程去定时地检测(工作量很大,影响程序的性能)
不推荐使用
十四、序列与关联容器
容器概述
容器
一种特殊的类型,其对象可以放置其它类型的对象(元素)
- 需要支持的操作(通常):对象的添加、删除、索引、遍历
- 有多种算法可以实现容器,每种方法各有利弊
容器分类
- 序列容器:其中的对象有序排列,使用整数值进行索引
- 关联容器:其中的对象顺序并不重要,使用键进行索引
- 适配器:调整原有容器的行为,使得其对外展现出新的类型、接口或返回新的元素
- 生成器:构造元素序列
迭代器
用于指定容器中的一段区间,以执行遍历、删除等操作
获取迭代器
(c)begin/(c)end ; (c)rbegin/(c)rend
#include <iostream>
#include <vector>
int main()
{
std :: vector<int> x{1, 2, 3};
auto b = x.begin();
auto e = x.end(); // b和e构造了一个前闭后开的区间 [b, e)
for (auto ptr = b; ptr < e; ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
#include <iostream>
#include <vector>
int main()
{
std :: vector<int> x{1, 2, 3};
auto b = x.rbegin(); // reverse 反向迭代 rbegin -> end往前一个
auto e = x.rend(); // rend -> begin往前一个 (b, e]
for (auto ptr = b; ptr < e; ++ptr)
{
std :: cout << *ptr << std :: endl; // 3 2 1
}
}
迭代器分类
分成 5 类( category ),不同的类别支持的操作集合不同
序列容器
C++ 标准库中提供了多种序列容器模板
- array :元素个数固定的序列容器(不支持对象的添加、删除)
- vector :元素连续存储的序列容器
- forward_list / list :基于链表 / 双向链表的容器
- deque : vector 与 list 的折衷
- basic_string :提供了对字符串专门的支持
需要使用元素类型来实例化容器模板,从而构造可以保存具体类型的容器。
不同的容器所提供的接口大致相同,但根据容器性质的差异,其内部实现与复杂度不同。
对于复杂度过高的操作,提供相对较难使用的接口或者不提供相应的接口。
array 容器模板
具有固定长度的容器,其内部维护了一个内建数组,与内建数组相比提供了复制操作
#include <iostream>
#include <array>
int main()
{
std :: array<int, 3> a; // 构造一个容器,这个容器包含了3个int
std :: array<int, 3> b = a;
}
提供的接口
构造
#include <iostream>
#include <array>
int main()
{
std :: array<int, 3> b;
std :: array<int, 3> a = {1, 2, 3};
}
成员类型
value_type 等
#include <iostream>
#include <array>
#include <type_traits>
int main()
{
std :: array<int, 3> a;
std :: cout << std :: is_same_v<std :: array<int, 3> :: value_type, int> << std :: endl;
}
元素访问
[] , at , front , back , data
#include <array>
#include <iostream>
int main()
{
std::array<int, 4> numbers{2, 4, 6, 8};
std::cout << "Second element: " << numbers[1] << '\n';
numbers[0] = 5;
std::cout << "All numbers:";
for (auto i : numbers)
std::cout << ' ' << i;
std::cout << '\n';
}
#include <iostream>
#include <array>
#include <type_traits>
int main()
{
std :: array<int, 3> a = {1, 2, 3};
std :: cout << a[100] << std :: endl; // 行为是未定义的
std :: cout << a.at[100] << std :: endl; // 系统会直接崩溃,防止出现内存访问的无效
}
#include <iostream>
#include <array>
#include <type_traits>
int main()
{
std :: array<int, 3> a = {1, 2, 3};
std :: cout << a.front() << std :: endl; // 返回第一个元素
std :: cout << a.back() << std :: endl; // 返回最后一个元素
}
// 如果容器当中的元素是在内存当中连续保存的,通常会提供data接口
#include <iostream>
#include <array>
#include <type_traits>
void fun(int* ptr) // 偏向于C语言的传统接口
{
}
int main()
{
std :: array<int, 3> a = {1, 2, 3};
std :: cout << a.data() << std :: endl; // 返回int*指针,指向数组当中的第一个元素
std :: cout << *(a.data()) << std :: endl;
fun(a.data());
}
容量相关(平凡实现)
empty , size , max_size
#include <iostream>
#include <array>
int main()
{
std :: array<int, 3> a = {1, 2, 3};
std :: cout << a.empty() << std :: endl; // 判断是否为空
}
#include <iostream>
#include <array>
int main()
{
std :: array<int, 3> a = {1, 2, 3};
std :: cout << a.size() << std :: endl; // 返回array中包含了多少个元素
}
#include <iostream>
#include <array>
int main()
{
std :: array<int, 3> a = {1, 2, 3};
std :: cout << a.max_size() << std :: endl; // 返回array中最多可以包含多少个元素
}
填充与交换
fill , swap
#include <iostream>
#include <array>
int main()
{
std :: array<int, 3> a;
a.fill(100);
std :: cout << a[0] << ' '
<< a[1] << ' '
<< a[2] << ' '
<< std :: endl;
}
#include <array>
#include <iostream>
template<class Os, class V> Os& operator<<(Os& os, const V& v)
{
os << '{';
for (auto i : v)
os << ' ' << i;
return os << " } ";
}
int main()
{
std::array<int, 3> a1{1, 2, 3}, a2{4, 5, 6};
auto it1 = a1.begin();
auto it2 = a2.begin();
int& ref1 = a1[1];
int& ref2 = a2[1];
std::cout << a1 << a2 << *it1 << ' ' << *it2 << ' ' << ref1 << ' ' << ref2 << '\n';
a1.swap(a2); // 交换两个array的元素
std::cout << a1 << a2 << *it1 << ' ' << *it2 << ' ' << ref1 << ' ' << ref2 << '\n';
}
比较操作
< = >
要求类型一致才能比较
迭代器
vector 容器模板
元素的数目可变
提供的接口
与 array 很类似,但有其特殊性
#include <iostream>
#include <vector>
int main()
{
std :: vector<int> a{1};
std :: cout << a.size() << std :: endl;
std :: cout << a.max_size() << std :: endl;
}
#include <iostream>
#include <vector>
int main()
{
std :: vector<int> a{1};
std :: vector<int> b{0, 1, 2};
std :: cout << (a < b) << std :: endl; // 逐个元素比较
}
容量相关接口
capacity / reserve / shrink_to_fit
#include <iostream>
#include <vector>
int main()
{
std :: vector<int> a;
a.reserve(1024); // 提高性能,一开始就开辟1024个元素(事先知道要放多少个元素的时候才有用)
for (int i = 0; i < 1024; ++i)
{
a.push_back(i);
}
}
附加元素接口
push_back / emplace_back
都是在结尾插入元素
#include <iostream>
#include <vector>
#include <string>
int main()
{
std :: vector<std :: string> a;
a.push_back("hello"); // 首先根据C字符串来构造一个string,再调用push_back传入vector
a.emplace_back("world"); // 直接使用world在vector的内存中构造string,减少了一次对象的拷贝/移动
std :: cout << a[0] << std :: endl;
std :: cout << a[1] << std :: endl;
}
元素插入接口
性能较差
insert / emplace
元素删除接口
pop_back / erase / clear
注意
vector 不提供 push_front / pop_front ,可以使用 insert / erase 模拟,但效率不高
在 vector 中使用 swap 的效率较高,只需交换 size, cap, buffer
写操作(插入元素)可能会导致迭代器失效
list 容器模板
双向链表
#include <iostream>
#include <list>
int main()
{
std :: list<int> a{1, 2, 3};
for (auto ptr = a.begin(); ptr != a.end(); ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
与 vector 相比
- 插入、删除成本较低,但随机访问成本较高(没有提供[])
- 提供了 pop_front / splice 等接口
- 写操作通常不会改变迭代器的有效性
forward_list 容器模板
单向链表
#include <iostream>
#include <forward_list>
int main()
{
std :: forward_list<int> a{1, 2, 3};
for (auto ptr = a.begin(); ptr != a.end(); ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
- 目标:一个成本较低的线性表实现
- 其迭代器只支持递增操作,不能进行反向遍历,因此无 rbegin/rend
- 不支持 size
- 不支持 pop_back / push_back
- XXX_after 操作
deque容器模板
double-ended queue 双端容器
vector 与 list 的折衷
http://cpp-tip-of-the-day.blogspot.com/2013/11/how-is-stddeque-implemented.html
- push_back / push_front 速度较快
- 在序列中间插入、删除速度较慢
- 大部分情况下不会使用deque
basic_string 容器模板
实现了字符串相关的接口
- 使用 char 实例化出 std::string
- 提供了如 find , substr 等字符串特有的接口
- 提供了数值与字符串转换的接口
- 针对短字符串的优化( short string optimization: SSO )
关联容器
使用键进行索引
- set / map / multiset / multimap
- unordered_set / unordered_map / unordered_multiset / unordered_multimap
set / map / multiset / multimap 底层使用红黑树实现
unordered_xxx 底层使用 hash 表实现
#include <iostream>
#include <map>
int main()
{
std :: map<char, int> m{{'a', 3}, {'b', 4}};
std :: cout << m['a'] << std :: endl;
}
set
一个集合,包含了一系列元素
元素无序、唯一
基于红黑树实现
#include <iostream>
#include <set>
int main()
{
std :: set<int> s{100, 3, 56, 7}; // 键:所包含的元素,值:bool值(查找的“键”是否在集合内)
}
比较大小
通常来说,元素需要支持使用 < 比较大小
#include <iostream>
#include <set>
int main()
{
std :: set<int> s{100, 3, 56, 7}; // 键:所包含的元素,值:bool值(查找的“键”是否在集合内)
for (auto ptr = s.begin(); ptr != s.end(); ++ptr)
{
std :: cout << *ptr << std :: endl; // 会使用树的中序遍历
}
}
#include <iostream>
#include <set>
int main()
{
std :: set<int, std :: greater<int>> s{100, 3, 56, 7}; // 从大到小排序
for (auto ptr = s.begin(); ptr != s.end(); ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
或者采用自定义的比较函数来引入大小关系
#include <iostream>
#include <set>
struct Str
{
int x;
};
bool MyComp(const Str& val1, const Str& val2)
{
return val1.x < val2.x;
}
int main()
{
std :: set<Str, decltype(&MyComp)> s({Str{3}, Str{4}}, MyComp);
}
插入元素: insert / emplace / emplace_hint
#include <iostream>
#include <set>
struct Str
{
int x;
Str(int value) : x(value) {} // 为了规避隐式行为差异,可以显式添加一个接受 int 的构造函数
};
bool MyComp(const Str& val1, const Str& val2)
{
return val1.x < val2.x;
}
int main()
{
std :: set<Str, decltype(&MyComp)> s({Str{3}, Str{4}}, MyComp);
s.insert(Str{100});
s.emplace(200);
}
删除元素: erase
#include <iostream>
#include <set>
struct Str
{
int x;
Str(int value) : x(value) {}
};
bool MyComp(const Str& val1, const Str& val2)
{
return val1.x < val2.x;
}
int main()
{
std :: set<Str, decltype(&MyComp)> s({Str{3}, Str{4}}, MyComp);
s.insert(Str{100});
s.emplace(200);
s.erase(200); // 在容器里面查找200这个元素,将其删除
s.erase(s.begin()); // 删除容器里面的第一个元素
}
访问元素: find / contains
#include <iostream>
#include <set>
int main()
{
std :: set<int> s{1, 3, 5};
auto ptr = s.find(3); // ptr是一个迭代器,返回的是红黑树的一个节点
// std::_Rb_tree_const_iterator<int> ptr = s.find(3);
if (ptr != s.end())
{
std :: cout << *ptr << std :: endl;
}
}
#include <iostream>
#include <set>
int main()
{
std :: set<int> s{1, 3, 5};
std :: cout << s.contains(3) << std :: endl;
std :: cout << s.contains(20) << std :: endl;
}
修改元素: extract
注意: set 迭代器所指向的对象是 const 的,不能通过其修改元素
防止破坏红黑树的数据结构
C++17 引入 extract,返回的是 node_type(性能优于 erase)
https://zh.cppreference.com/w/cpp/container/set/extract
#include <algorithm>
#include <iostream>
#include <string_view>
#include <set>
void print(std::string_view comment, const auto& data)
{
std::cout << comment;
for (auto datum : data)
std::cout << ' ' << datum;
std::cout << '\n';
}
int main()
{
std::set<int> cont{1, 2, 3};
print("Start:", cont);
// 提取节点句柄并改变键
auto nh = cont.extract(1);
nh.value() = 4;
print("After extract and before insert:", cont);
// 将节点句柄插回去
cont.insert(std::move(nh));
print("End:", cont);
}
map
本质也是基于红黑树实现的
键和值的映射
使用键构造红黑树
树中的每个结点存储了一个 std::pair 对象
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto ptr = m.begin(); ptr != m.end(); ++ptr)
{
auto p = *ptr; // std :: pair<const int, bool>
std :: cout << p.first << ' ' << p.second << std :: endl; // ptr是一个迭代器,不能直接 *ptr
}
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto p : m)
{
std :: cout << p.first << ' ' << p.second << std :: endl; // ptr是一个迭代器,不能直接 *ptr
}
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto& [k, v] : m) // 加了&之后可以避免拷贝,只取地址,提高性能
{
std :: cout << k << ' ' << v << std :: endl;
}
}
比较大小
键 (pair.first) 需要支持使用 < 比较大小
或者采用自定义的比较函数来引入大小关系
#include <iostream>
#include <map>
struct Str
{
};
int main()
{
std :: map<int, Str> m{{3, Str{}}, {4, Str{}}, {1, Str{}}};
}
插入元素
insert
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
m.insert(std :: pair<int, bool>(5, true));
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
}
删除元素
erase
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
m.insert(std :: pair<int, bool>(5, true));
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
m.erase(4);
std :: cout << std :: endl;
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
}
访问元素
find / contains / [] / at
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
std :: cout << std :: endl;
auto ptr = m.find(3);
std :: cout << ptr->first << ' ' << ptr->second << std :: endl;
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
std :: cout << std :: endl;
auto ptr = m.find(3);
std :: cout << ptr->first << ' ' << ptr->second << std :: endl;
std :: cout << m.contains(4) << std :: endl;
std :: cout << m.contains(40) << std :: endl;
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
std :: cout << std :: endl;
std :: cout << m[3] << std :: endl;
std :: cout << m[30] << std :: endl; // 往树中插入一个新的节点,这个节点的key就是30,值如果类型是类,会在类初始化后进行赋值,否则值默认为0
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, bool> m{{3, true}, {4, false}, {1, true}};
for (auto& [k, v] : m)
{
std :: cout << k << ' ' << v << std :: endl;
}
std :: cout << std :: endl;
std :: cout << m[3] << std :: endl;
std :: cout << m.at(3) << std :: endl;
std :: cout << m.at(30) << std :: endl;
}
注意
map 迭代器所指向的对象是 std::pair ,其键是 const 类型(会进行隐式转换)
[] 操作不能用于常量对象
#include <iostream>
#include <map>
void fun(const std :: map<int, int>& m)
{
auto ptr = m.find(3); // 不能使用m[3] ---> [] 操作不能用于常量对象
// []的行为是如果这个键存在,就返回这个键所对应的值的对象
// 如果这个键不存在,就往m中插入一个相应的对象
// 而判断键存不存在,是在运行期的行为
// 但在解析m[3]这个代码时,是在编译期的行为
// 编译器知道传入的是const的引用,不能保证运行期m中一定会存在键为3的元素
// 如果编译器通过了这段代码,则在m中至少会存在一个元素{3, 0},就改变了m中的内容,与const相违背了
if (ptr != m.end())
{
std :: cout << ptr->second << std :: endl;
}
}
int main()
{
std :: map<int, int> m;
m.insert(std :: pair<const int, int>(3, 100));
fun(m);
}
multiset / multimap
与 set / map 类似,但允许重复键
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: set<int> s{1, 3, 1};
for (auto i : s)
{
std :: cout << i << std :: endl;
}
}
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
for (auto i : s)
{
std :: cout << i << std :: endl;
}
}
元素访问
find
返回首个查找到的元素
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
auto ptr = s.find(1); // 返回的是首个键为1的元素
++ptr; // 查找下一个键为1的元素
}
count
返回元素个数
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
std :: cout << s.count(1) << std :: endl; // 返回包含1这个节点的个数
}
lower_bound / upper_bound / equal_range
返回查找到的区间
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
for (auto ptr = s.begin(); ptr != s.end(); ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
auto b = s.lower_bound(1);
auto e = s.upper_bound(1);
for (auto ptr = b; ptr != e; ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
auto p = s.equal_range(1);
for (auto ptr = p.first; ptr != p.second; ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
#include <iostream>
#include <map>
#include <set>
int main()
{
std :: multiset<int> s{1, 3, 1};
auto [b, e] = s.equal_range(1);
for (auto ptr = b; ptr != e; ++ptr)
{
std :: cout << *ptr << std :: endl;
}
}
unordered_set / unordered_map / unordered_multiset / unordered_multimap
基于哈希表实现快速查找
与 set / map 相比查找性能更好
但插入操作一些情况下会慢
其键需要支持两个操作
- 转换为 hash 值
- 判等
#include <iostream>
#include <unordered_set>
int main()
{
std :: unordered_set<int> s{3, 1, 5, 4, 1}; // C++为int提供了转换成哈希值的函数,同时int支持判等操作
for (auto p : s)
{
std :: cout << p << std :: endl;
}
}
除 == , != 外,不支持容器级的关系运算,但 ==, != 速度较慢
#include <iostream>
#include <unordered_set>
int main()
{
std :: unordered_set<int> s1{3, 1, 5, 4, 1};
std :: unordered_set<int> s2{3, 1, 5};
std :: cout << (s1 == s2) << std :: endl;
std :: cout << (s1 != s2) << std :: endl;
}
自定义 hash 与判等函数
#include <iostream>
#include <unordered_set>
struct Str
{
int x;
};
size_t MyHash(const Str val) // 自定义哈希函数
{
return val.x;
};
bool MyEqual(const Str& val1, const Str& val2) // 自定义判等函数
{
return val1.x == val2.x;
};
int main()
{
// decltype(&MyHash) -> size_t (*) (const Str val) 函数指针
std :: unordered_set<Str, decltype(&MyHash), decltype(&MyEqual)> s1{1, MyHash, MyEqual};
s1.insert(Str{3});
s1.insert(Str{4});
s1.insert(Str{5});
s1.insert({Str{13}, Str{14}});
for (auto p : s1)
{
std :: cout << p.x << std :: endl;
}
}
#include <iostream>
#include <unordered_set>
struct Str {
int x;
//此函数用于unordered_set比较
// Str元素。
bool operator==(const Str& t) const
{
return (this->x == t.x);
}
};
class MyHashFunction{
public:
size_t operator()(const Str& t) const // 可以使用MyHashFunction缺省构造出一个对象,
// 然后使用这个对象()的方式传入一个类型t,从而返回哈希函数
{
return t.x;
}
};
int main()
{
// MyHashFunction mf;
// mf(Str{3}); // MyHash(Str{3});
std :: unordered_set<Str, MyHashFunction> s1;
s1.insert(Str{3});
s1.insert(Str{4});
s1.insert(Str{5});
s1.insert({Str{13}, Str{14}});
for (auto p : s1)
{
std :: cout << p.x << std :: endl;
}
}
适配器与生成器
类型适配器
把不同类型统一到相同的类型进行处理
basic_string_view ( C++17 )
- 可以基于 std::string , C 字符串,迭代器构造
- 提供成本较低的操作接口,仅需要调整迭代器的指针,不用开辟新的内存
- 只是作为原始string的一个窗口,读取字符串的信息,不可进行写操作
- 大部分情况下,string_view是作为函数的输入参数,不会作为函数的返回值
#include <iostream>
#include <string>
#include <string_view>
void fun(std :: string_view str)
{
std :: cout << str << std :: endl;
}
int main()
{
fun("12345");
std :: string s("12345");
fun(std :: string_view(s.begin(), s.begin() + 3));
}
#include <iostream>
#include <string>
#include <string_view>
std :: string_view fun(std :: string_view input)
{
return input.substr(0, 3);
}
int main()
{
std :: string s = "12345";
auto res = fun(s);
std :: cout << res << std :: endl;
}
span ( C++20 )
- 支持非字符串的视图
- 可基于 C 数组、 array 等构造
- 可读写
#include <iostream>
#include <span>
#include <vector>
void fun(std :: span<int> input)
{
for (auto p : input)
{
std :: cout << p << std :: endl;
}
}
int main()
{
std :: vector<int> s{1, 2, 3};
fun(s);
std :: cout << std :: endl;
int a[3] = {1, 2, 3};
fun(a);
}
#include <iostream>
#include <span>
#include <vector>
void fun(std :: span<int> input)
{
input[0] = 3;
}
int main()
{
std :: vector<int> s{1, 2, 3};
fun(s);
for (auto p : s)
{
std :: cout << p << std :: endl;
}
}
接口适配器
- stack / queue / priority_queue
- 对底层序列容器进行封装,对外展现栈、队列与优先级队列的接口
- priority_queue 在使用时其内部包含的元素需要支持比较操作
#include <iostream>
#include <stack>
int main()
{
std :: stack<int> p;
}
#include <iostream>
#include <stack>
#include <vector>
void stack_cout(std::stack<int, std::vector<int>> p)
{
std::stack<int, std::vector<int>> temp = p;
while (!temp.empty())
{
std::cout << temp.top() << ' ';
temp.pop();
}
std::cout << temp.top() << std::endl;
}
int main()
{
std :: stack<int, std :: vector<int>> p;
p.push(3);
p.push(5);
p.push(11);
p.push(91);
stack_cout(p);
p.push(2);
stack_cout(p);
p.pop();
stack_cout(p);
p.top();
stack_cout(p);
}
数值适配器 (c++20)
- std::ranges::XXX_view / std::ranges::views::XXX(std::views::XXX)
- 可以将一个输入区间中的值变换后输出
- 数值适配器可以组合,引入复杂的数值适配逻辑
std::ranges::XXX_view
#include <iostream>
#include <vector>
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
for (auto val : v)
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <vector>
#include <ranges>
bool isEven(int i)
{
return i % 2 == 0;
}
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
for (auto val : std :: ranges :: filter_view(v, isEven)) // filter_view(原有容器, 预测器)
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <vector>
#include <ranges>
int Square(int i)
{
return i * i;
}
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
for (auto val : std :: ranges :: transform_view(v, Square))
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
std::ranges::views::XXX(std::views::XXX)
#include <iostream>
#include <vector>
#include <ranges>
bool isEven(int i)
{
return i % 2 == 0;
}
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
auto x = std :: views :: filter(isEven); // filter中此时没有传入原有容器,此时x就是预测器
for (auto val : x(v))
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <vector>
#include <ranges>
bool isEven(int i)
{
return i % 2 == 0;
}
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
auto x = std :: views :: filter(isEven);
for (auto val : v | x) // “按位或”运算符的重载,对原有容器v使用预测器x进行处理
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <vector>
#include <ranges>
bool isEven(int i)
{
return i % 2 == 0;
}
int Square(int i)
{
return i * i;
}
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
auto x = std :: views :: filter(isEven);
auto y = std :: views :: transform(Square);
for (auto val : v | x | y) // 从左往右,v先经过x,再经过y
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <vector>
#include <ranges>
bool isEven(int i)
{
return i % 2 == 0;
}
int Square(int i)
{
return i * i;
}
int main()
{
std :: vector<int> v{1, 2, 3, 4, 5};
auto x = std :: views :: filter(isEven) | std :: views :: transform(Square);
for (auto val : v | x)
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
生成器 (c++20)
- std::ranges::itoa_view / std::ranges::views::itoa(std::views::itoa)
- 可以在运行期生成无限长或有限长的数值序列
#include <algorithm>
#include <iostream>
#include <ranges>
struct Bound
{
int bound;
bool operator==(int x) const { return x == bound; }
};
int main()
{
for (int i : std::ranges::iota_view{1, 10})
std::cout << i << ' ';
std::cout << '\n';
for (int i : std::views::iota(1, 10))
std::cout << i << ' ';
std::cout << '\n';
for (int i : std::views::iota(1, Bound{10}))
std::cout << i << ' ';
std::cout << '\n';
for (int i : std::views::iota(1) | std::views::take(9)) // iota(1),只传入一个数时,会生成无限长的序列,take(9)只取这个容器前9个数
std::cout << i << ' ';
std::cout << '\n';
std::ranges::for_each(std::views::iota(1, 10),
[](int i){ std::cout << i << ' '; });
std::cout << '\n';
}
十五、泛型算法与 Lambda 表达式
泛型算法
泛型算法:可以支持多种类型的算法
这里重点讨论 C++ 标准库中定义的算法
- <algorithm> C++中基本的一些泛型算法(sort...)
- <numeric> 数值的算法(accumulate...)
- <ranges> C++20新引入的库
初学者推荐阅读
为什么要引入泛型算法而不采用方法的形式
- 内建数据类型不支持方法(方法:;类内部定义的函数。 算法:一般意义下定义的普通函数)
- 计算逻辑存在相似性,避免重复定义
如何实现支持多种类型
- 使用迭代器作为算法与数据的桥梁(迭代器的本质:模拟指针--更加泛化的指针)
- 通过迭代器操作容器
#include <iostream>
#include <algorithm>
int main()
{
int x[100];
std :: sort(std :: begin(x), std :: end(x)); // 数组是内建数据类型,不支持方法
}
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std :: vector<int> x(100);
std :: sort(x.begin(), x.end()); // vector不是内建的数据类型,是C++提供的标准的类模板,其中定义了begin和end方法
}
泛型算法通常来说都不复杂,但优化足够好
一些泛型算法与方法同名,实现功能类似,此时建议调用“方法”而非算法
为了支持类型的泛化,牺牲了一定的性能(不是优化不好的问题)
- std::find V.S. std::map::find
泛型算法的分类
读算法
给定迭代区间,读取其中的元素并进行计算
accumulate / find / count
写算法
向一个迭代区间中写入元素
单纯写操作
fill / fill_n
读 + 写操作
transform / copy
注意
写算法一定要保证目标区间足够大
排序算法
改变输入序列中元素的顺序
sort / unique
unique需要sort之后才有用
泛型算法使用迭代器实现元素访问
迭代器的分类
一些算法会根据迭代器类别的不同引入相应的优化:如 distance 算法
输入迭代器
可读,可递增 —— 典型应用为 find 算法
输出迭代器
可写,可递增 —— 典型应用为 copy 算法
前向迭代器
可读写,可递增 —— 典型应用为 replace 算法
双向迭代器
可读写,可递增递减 —— 典型应用为 reverse 算法
随机访问迭代器
可读写,可增减一个整数 —— 典型应用为 sort 算法
一些特殊的迭代器
插入迭代器
在没有保证有足够的目标存储空间的情况下,支持插入的操作
back_insert_iterator / front_insert_iterator / insert_iterator
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std :: vector<int> x;
std :: fill_n(std :: back_insert_iterator<std :: vector<int>>(x), 10, 3);
for (auto val : x)
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std :: vector<int> x;
std :: fill_n(std :: back_inserter(x), 10, 3);
for (auto val : x)
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <algorithm>
#include <list>
int main()
{
std :: list<int> x;
std :: fill_n(std :: front_inserter(x), 10, 3);
for (auto val : x)
{
std :: cout << val << ' ';
}
std :: cout << std :: endl;
}
流迭代器
istream_iterator / ostream_iterator
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
int main()
{
std :: istringstream str("1 2 3 4 5");
std :: istream_iterator<int> x(str);
std :: cout << *x << std :: endl;
++x;
std :: cout << *x << std :: endl;
}
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
int main()
{
std :: istringstream str("1 2 3 4 5");
std :: istream_iterator<int> x(str);
std :: istream_iterator<int> y{}; // 作为流迭代器的结尾位置(成对使用)
for (; x != y; ++x)
{
std :: cout << *x << std :: endl;
}
}
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <numeric>
int main()
{
std :: istringstream str("1 2 3 4 5");
std :: istream_iterator<int> x(str);
std :: istream_iterator<int> y{};
int res = std :: accumulate(x, y, 0);
std :: cout << res << std :: endl;
}
反向迭代器
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <numeric>
#include <vector>
int main()
{
std :: vector<int> x{1, 2, 3, 4, 5};
std :: copy(x.begin(), x.end(), std :: ostream_iterator<int>(std :: cout, " "));
std :: cout << std :: endl;
std :: copy(x.rbegin(), x.rend(), std :: ostream_iterator<int>(std :: cout, " "));
std :: cout << std :: endl;
}
移动迭代器
move_iterator
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <numeric>
#include <string>
int main()
{
std :: string x = "abc";
auto y = std :: move(x);
std :: cout << x << std :: endl;
}
迭代器与哨兵( Sentinel )
并发算法( C++17 / C++20 )
SIMD
- std::execution::seq
- std::execution::par
- std::execution::par_unseq
- std::execution::unseq
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <numeric>
#include <execution>
#include <random>
#include <chrono>
int main()
{
std :: random_device rd;
std :: vector<double> vals(10000000);
for (auto& d : vals)
{
d = static_cast<double>(rd());
}
for (int i = 0; i < 5; ++i)
{
using namespace std :: chrono;
std :: vector<double> sorted(vals);
const auto startTime = high_resolution_clock :: now();
std :: sort(sorted.begin(), sorted.end());
const auto endTime = high_resolution_clock :: now();
std :: cout << "Latency: "
<< duration_cast<duration<double, std :: milli>>(endTime - startTime).count()\
<< std :: endl;
}
}
-O3;-ltbb
添加配置,以使用并发算法
-O3
--std=c++2a
-ltbb
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <numeric>
#include <execution>
#include <random>
#include <chrono>
int main()
{
std :: random_device rd;
std :: vector<double> vals(10000000);
for (auto& d : vals)
{
d = static_cast<double>(rd());
}
for (int i = 0; i < 5; ++i)
{
using namespace std :: chrono;
std :: vector<double> sorted(vals);
const auto startTime = high_resolution_clock :: now();
std :: sort(std :: execution :: unseq, sorted.begin(), sorted.end());
const auto endTime = high_resolution_clock :: now();
std :: cout << "Latency: "
<< duration_cast<duration<double, std :: milli>>(endTime - startTime).count()\
<< std :: endl;
}
}
#include <iostream>
#include <algorithm>
#include <iterator>
#include <sstream>
#include <numeric>
#include <execution>
#include <random>
#include <chrono>
int main()
{
std :: random_device rd;
std :: vector<double> vals(10000000);
for (auto& d : vals)
{
d = static_cast<double>(rd());
}
for (int i = 0; i < 5; ++i)
{
using namespace std :: chrono;
std :: vector<double> sorted(vals);
const auto startTime = high_resolution_clock :: now();
std :: sort(std :: execution :: par, sorted.begin(), sorted.end());
const auto endTime = high_resolution_clock :: now();
std :: cout << "Latency: "
<< duration_cast<duration<double, std :: milli>>(endTime - startTime).count()\
<< std :: endl;
}
}
bind 与 lambda 表达式
很多算法允许通过可调用对象自定义计算逻辑的细节
- transform / copy_if / sort…
#include <iostream>
#include <algorithm>
#include <vector>
bool MyPredict(int val)
{
return val > 3;
}
int main()
{
std :: vector<int> x{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std :: vector<int> y;
std :: copy_if(x.begin(), x.end(), std :: back_inserter(y), MyPredict);
for (auto p : y)
{
std :: cout << p << ' ';
}
std :: cout << std :: endl;
}
可调用对象
- 函数指针:概念直观,但定义位置受限(C++不支持在函数内部定义函数)
- 类:功能强大,但书写麻烦
- bind :基于已有的逻辑灵活适配,但描述复杂逻辑时语法可能会比较复杂难懂
- lambda 表达式:小巧灵活,功能强大
bind
通过绑定的方式修改可调用对象的调用方式
早期的 bind 雏形: std::bind1st / std::bind2nd
https://en.cppreference.com/w/cpp/utility/functional/bind12
- 具有了 bind 的基本思想,但功能有限
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
int main()
{
std :: vector<int> x{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std :: vector<int> y;
std :: copy_if(x.begin(), x.end(), std :: back_inserter(y), std :: bind1st(std :: greater<int>(), 3));
for (auto p : y)
{
std :: cout << p << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
int main()
{
std :: vector<int> x{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std :: vector<int> y;
std :: copy_if(x.begin(), x.end(), std :: back_inserter(y), std :: bind2nd(std :: greater<int>(), 3));
for (auto p : y)
{
std :: cout << p << ' ';
}
std :: cout << std :: endl;
}
std::bind ( C++11 引入):更加灵活的解决方案
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
bool MyPredict(int val1, int val2)
{
return val1 > val2;
}
int main()
{
using namespace std :: placeholders;
std :: vector<int> x{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std :: vector<int> y;
std :: copy_if(x.begin(), x.end(), std :: back_inserter(y), std :: bind(MyPredict, _1, 3));
for (auto p : y)
{
std :: cout << p << ' ';
}
std :: cout << std :: endl;
}
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
bool MyPredict(int val1, int val2)
{
std :: cout << "第一个参数:" << val1 << '\n';
std :: cout << "第二个参数:" << val2 << '\n';
return val1 > val2;
}
int main()
{
using namespace std :: placeholders;
auto x = std :: bind(MyPredict, _1, 3); // bind调用MyPredict,依次按顺序传入,先传入_1,再传入3
// _1指调用x时传入的第一个参数
std :: cout << x(50) << '\n'; // 使用50调用MyPredict,_1指调用x时传入的第一个参数,
// 此时50会作为第一个参数传入,3作为第二个参数传入
// auto x = std :: bind(MyPredict, 3, _1);
// std :: cout << x(50) << '\n';
// _1指调用x时传入的第一个参数,
// 此时50会作为第二个参数传入,3作为第一个参数传入
}
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
bool MyPredict(int val1, int val2)
{
std :: cout << "第一个参数:" << val1 << '\n';
std :: cout << "第二个参数:" << val2 << '\n';
return val1 > val2;
}
int main()
{
using namespace std :: placeholders;
auto x = std :: bind(MyPredict, _2, 3); // bind调用MyPredict,依次按顺序传入,先传入_2,再传入3
// _2指调用x时传入的第二个参数
std :: cout << x("hello", 50) << '\n'; // 使用50调用MyPredict,_2指调用x时传入的第二个参数,
// 此时50会作为第一个参数传入,3作为第二个参数传入
// auto x = std :: bind(MyPredict, 3, _1);
// std :: cout << x(50) << '\n';
// _2指调用x时传入的第二个参数,
// 此时50会作为第二个参数传入,3作为第一个参数传入
}
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
bool MyPredict(int val1, int val2)
{
std :: cout << "第一个参数:" << val1 << '\n';
std :: cout << "第二个参数:" << val2 << '\n';
return val1 > val2;
}
int main()
{
using namespace std :: placeholders;
auto x = std :: bind(MyPredict, _2, _1);
std :: cout << x(3, 4) << '\n';
}
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
bool MyPredict(int val1, int val2)
{
std :: cout << "第一个参数:" << val1 << '\n';
std :: cout << "第二个参数:" << val2 << '\n';
return val1 > val2;
}
bool MyAnd(bool val1, bool val2)
{
return val1 && val2;
}
int main()
{
using namespace std :: placeholders;
auto x = std :: bind(MyPredict, _1, 3); // 大于3
auto y = std :: bind(MyPredict, 10, _1); // 小于10
auto z = std :: bind(MyAnd, x, y); // 3 < * < 10
std :: cout << z(5) << '\n';
}
调用 std::bind 时,传入的参数会被复制,这可能会产生一些调用风险
可以使用 std::ref 或 std::cref 避免复制的行为
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
void Proc(int& x)
{
++x;
}
int main()
{
int x = 0;
auto b = std :: bind(Proc, std :: ref(x));
b();
std :: cout << x << '\n';
}
std::bind_front ( C++20 引入): std::bind 的简化形式
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
bool MyPredict(int val1, int val2)
{
std :: cout << "第一个参数:" << val1 << '\n';
std :: cout << "第二个参数:" << val2 << '\n';
return val1 > val2;
}
int main()
{
using namespace std :: placeholders;
auto y = std :: bind_front(MyPredict, 3);
std :: cout << y(2) << '\n';
}
lambda 表达式
为了更灵活地实现可调用对象而引入
C++11 ~ C++20 持续更新
- C++11 引入 lambda 表达式
- C++14 支持初始化捕获、泛型 lambda
- C++17 引入 constexpr lambda , *this 捕获
- C++20 引入 concepts ,模板 lambda
lambda 表达式会被编译器翻译成类进行处理
lambda 表达式的基本组成部分
参数与函数体
#include <iostream>
int main()
{
// lambda表达式构造的是一个对象,因此用一个对象接收后,后续可以使用
// [](参数){函数体}
// auto x = [](int val)
// {
// return val > 3;
// };
auto x = [](int val){return val > 3;};
std :: cout << x(5) << std :: endl;
}
C++本身会把lambda表达式解析为类
返回类型
#include <iostream>
int main()
{
auto x = [](int val) -> float // 指定返回类型
{
if (val > 3)
{
return 3.0; // double类型
}
else
{
return 1.5f; // float类型
}
};
std :: cout << x(5) << std :: endl;
}
捕获
针对函数体中使用的局部自动对象进行捕获
#include <iostream>
int main()
{
int y = 10;
auto x = [y](int val) // []用于捕获
{
return val > y;
};
std :: cout << x(5) << std :: endl;
}
值捕获
#include <iostream>
int main()
{
int y = 10;
auto x = [y](int val) mutable // [y]含义是y会被复制到lambda语句的内部
{
++y; // 对y的修改没有传递到lambda语句外部
return val > y;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
#include <iostream>
int main()
{
int y = 10;
int z = 3;
auto x = [=](int val) // [=]: 接下来编译器在编译lambda内部函数体的时候,
// 会自动分析用到了哪些局部自动对象,
// 会自动捕获没有显式声明的局部自动对象
{
return val > z;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
引用捕获
#include <iostream>
int main()
{
int y = 10;
auto x = [&y](int val) // [&y]
{
++y; // 对y的修改传递到了lambda语句外部
return val > y;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
#include <iostream>
int main()
{
int y = 10;
int z = 3;
auto x = [&](int val) // [&]: 接下来编译器在编译lambda内部函数体的时候,
// 会自动分析用到了哪些局部自动对象,
// 会自动捕获没有显式声明的局部自动对象
{
++y;
return val > z;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
混合捕获
#include <iostream>
int main()
{
int y = 10;
int z = 3;
auto x = [&y, z](int val)
{
++y;
return val > z;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
#include <iostream>
int main()
{
int y = 10;
int z = 3;
auto x = [&, z](int val) // 告诉编译器,lambda内部的局部自动对象通常使用引用捕获的方式
// 但是z是例外,要采用值捕获的方式
{
++y;
return val > z;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
#include <iostream>
int main()
{
int y = 10;
int z = 3;
auto x = [=, &y](int val) // 告诉编译器,lambda内部的局部自动对象通常使用值捕获的方式
// 但是y是例外,要采用引用捕获的方式
{
++y;
return val > z;
};
std :: cout << x(5) << std :: endl;
std :: cout << y << std :: endl;
}
this 捕获
#include <iostream>
struct Str
{
auto fun()
{
int val = 3;
auto lam = [val, this]() // this: 如果构造一个Str对象,对Str对象调用fun函数
// this是一个指针,指向Str这个对象
{
return val > x;
};
return lam();
}
int x;
};
int main()
{
}
初始化捕获( C++14 )
#include <iostream>
int main()
{
int x = 3;
auto lam = [y = x](int val) // [y = x]: 构造一个自动对象y,把x的值赋予y,
// y后续可以在整个lambda语句内部使用
// 可以引入更复杂的逻辑
// 在一定程度上提高系统的性能
{
return val > y;
};
std :: cout << lam(100) << std :: endl;
}
#include <iostream>
#include <string>
int main()
{
std :: string a = "hello";
auto lam = [y = std :: move(a)]() // 在构造lambda时,会把a里面的内容移动到y里面,a就空了
{
std :: cout << y << std :: endl;
};
std :: cout << a << std :: endl;
lam();
}
#include <iostream>
int main()
{
int x = 3;
int y = 10;
auto lam = [z = x + y](int val) // z = x + y在构造lambda表达式时被执行了一次
// 后面每次调用lambda表达式时,只要调用z就行,
// 不需要重新计算x+y,一定程度上提高了程序的性能
{
return val > z;
};
std :: cout << lam(14) << std :: endl;
}
#include <iostream>
int main()
{
int x = 3;
auto lam = [x = x](int val) // x = x,构造一个对象,这个对象叫做x,这个对象将用于lambda表达式
// 这个对象会使用等号右边的x进行初始化
{
return val > x;
};
std :: cout << lam(14) << std :: endl;
}
*this 捕获( C++17 )
优点:确保调用lambda表达式时的行为是安全的(防止出现“Dangling pointer - 悬挂的指针”,访问一块被销毁的内存的情况)
缺点:进行了一次复制,会消耗更多的资源
#include <iostream>
struct Str
{
auto fun()
{
int val = 3;
auto lam = [val, this]() // this是一个指针,指向了wrapper中的s对象
{
return val > x;
};
return lam;
}
int x = 5;
};
auto wrapper()
{
Str s; // s是一个局部自动对象,在调用完wrapper后会被自动销毁
return s.fun();
}
int main()
{
auto lam = wrapper(); // 此时lam接收的是一个“悬挂的指针”,指向了一个已经被销毁的对象
std :: cout << lam() << std :: endl; // 此时执行lam()的行为是未定义的
}
#include <iostream>
struct Str
{
auto fun()
{
int val = 3;
auto lam = [val, *this]() // this是一个指针,指向了wrapper中的s对象
// *this解引用之后,本身就是一个Str的对象
// *this会把s中所有的内容复制到lambda内部
{
return val > x;
};
return lam;
}
int x = 5;
};
auto wrapper()
{
Str s; // s是一个局部自动对象,在调用完wrapper后会被自动销毁
return s.fun();
}
int main()
{
auto lam = wrapper();
std :: cout << lam() << std :: endl; // 此时执行lam()的行为是安全的
}
说明符
mutable / constexpr (C++17) / consteval (C++20)……
// 对比
#include <iostream>
int main()
{
int y = 3;
auto lam = [y](int val)
{
return val > y;
};
}
#include <iostream>
int main()
{
int y = 3;
auto lam = [y](int val) mutable
{
++y;
return val > y;
};
}
#include <iostream>
int main()
{
auto lam = [](int val) constexpr // 保证该表达式可以在编译期调用
{
return val + 1;
};
constexpr int val = lam(100);
std :: cout << val << std :: endl;
}
#include <iostream>
int main()
{
auto lam = [](int val) consteval // constexpr定义的函数既能在编译期调用,也能在运行期调用
// consteval定义的函数只能在编译期调用
{
return val + 1;
};
constexpr int val = lam(100);
std :: cout << val << std :: endl;
}
模板形参( C++20 )
#include <iostream>
int main()
{
auto lam = []<typename T>(T val) // 可以接收任意类型的参数,只要可以支持+1操作就行
{
return val + 1;
};
constexpr int val = lam(100);
std :: cout << val << std :: endl;
}
lambda 表达式的深入应用
捕获时计算( C++14 )
#include <iostream>
int main()
{
int x = 3;
int y = 5;
auto lam = [z = x + y]()
{
return z;
};
lam();
}
即调用函数表达式
( Immediately-Invoked Function Expression, IIFE )
通常lambda表达式是先构造再调用
#include <iostream>
int main()
{
int x = 3;
int y = 5;
const auto val = [z = x + y]() // 避免了再另外定义一个函数,来进行初始化(具有复杂逻辑的初始化)
{
return z;
}();
std :: cout << val << std :: endl;
}
使用 auto 避免复制( C++14 )
#include <iostream>
int main()
{
auto lam = [](auto x)
{
return x + 1;
};
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, int> m{{2, 3}};
auto lam = [](const std :: pair<const int, int>& p)
{
return p.first + p.second;
};
std :: cout << lam(*m.begin()) << std :: endl;
}
#include <iostream>
#include <map>
int main()
{
std :: map<int, int> m{{2, 3}};
auto lam = [](const auto& p)
{
return p.first + p.second;
};
std :: cout << lam(*m.begin()) << std :: endl;
}
Lifting ( C++14 )
一种技巧
#include <iostream>
#include <functional>
auto fun(int val)
{
return val + 1;
}
auto fun(double val) // 函数重载
{
return val + 1;
}
int main()
{
auto lam = [](auto x) // 支持函数重载
{
return fun(x);
};
std :: cout << lam(3) << std :: endl;
std :: cout << lam(1.5) << std :: endl;
}
递归调用( C++14 )
#include <iostream>
#include <functional>
int factorial(int n)
{
return n > 1 ? n * factorial(n - 1) : 1;
}
int main()
{
std :: cout << factorial(5) << std :: endl;
}
#include <iostream>
#include <functional>
int main()
{
auto factorial = [](int n)
{
auto f_impl = [](int n, const auto impl) -> int
{
return n > 1 ? n * impl(n - 1, impl) : 1;
};
return f_impl(n, f_impl);
};
std :: cout << factorial(5) << std :: endl;
}
泛型算法的改进—— ranges
可以使用容器而非迭代器作为输入
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std :: vector<int> x{1, 2, 3, 4, 5};
auto iter = std :: find(x.begin(), x.end(), 3);
std :: cout << *iter << std :: endl;
}
- 通过 std::ranges::dangling 避免返回无效的迭代器
#include <iostream>
#include <algorithm>
#include <vector>
#include <ranges>
int main()
{
std :: vector<int> x{1, 2, 3, 4, 5};
auto iter = std :: ranges :: find(x, 3);
std :: cout << *iter << std :: endl;
}
引入映射概念,简化代码编写
#include <iostream>
#include <algorithm>
#include <map>
#include <ranges>
int main()
{
std :: map<int, int> m{{2, 3}};
auto iter = std :: ranges :: find(m, 3, &std :: pair<const int, int> :: second); // 使用类的成员指针:&std :: pair<const int, int> :: second
std :: cout << iter -> first << ' ' << iter -> second << std :: endl;
}
引入 view ,灵活组织程序逻辑
- view 模糊了算法与容器的概念
从类型上区分迭代器与哨兵
十六、类(基础)
结构体与对象聚合
结构体
对基本数据结构进行扩展,将多个对象放置在一起视为一个整体
- 结构体的声明与定义(注意定义后面要跟分号来表示结束)
#include <iostream>
struct Str; // 结构体的声明
struct Str // 结构体的定义,与C语言兼容的结构体
{
int x;
int y;
};
int main()
{
Str m_str;
m_str.x = 3;
m_str.y = 4;
std :: cout << m_str.x << ' ' << m_str.y << '\n';
}
#include <iostream>
typedef struct Str
{
int x;
int y;
} MStr; // 为结构体赋予别名(语言中的典型用法)
int main()
{
MStr m_str;
m_str.x = 3;
m_str.y = 4;
std :: cout << m_str.x << ' ' << m_str.y << '\n';
}
- 仅有声明的结构体是不完全类型( incomplete type )
#include <iostream>
struct Str1; // 不完全类型
struct Str
{
int x;
int y;
};
int main()
{
Str m_str;
Str1* m_str1; // 可以用指针指向不完全类型的结构体
}
- 结构体(以及类)的一处定义原则:翻译单元级别
数据成员(数据域)的声明与初始化
- ( C++11 )数据成员可以使用 decltype 来声明其类型,但不能使用 auto
#include <iostream>
struct Str
{
decltype(3) x; // 3是一个表达式,当decltype中是一个表达式的时候,会返回这个表达式的类型
int y;
};
int main()
{
}
- 数据成员声明时可以引入 const 、引用等限定
#include <iostream>
struct Str
{
const int x = 3;
int y;
};
int main()
{
Str m_str;
std :: cout << m_str.x << std :: endl;
}
- 数据成员会在构造类对象时定义
- ( C++11 )类内成员初始化
- 聚合初始化:从初始化列表到指派初始化器
#include <iostream>
struct Str
{
int x;
int y;
};
int main()
{
Str m_str{4, 5};
std :: cout << m_str.x << ' ' << m_str.y << std :: endl;
}
#include <iostream>
struct Str
{
int x;
int y;
};
int main()
{
Str m_str{.y=4, .x=5};
std :: cout << m_str.x << ' ' << m_str.y << std :: endl;
}
mutable 限定符
#include <iostream>
struct Str
{
mutable int x = 0; // 关键字mutable:可修改的(绕开const)
int y = 1;
};
int main()
{
const Str m_str;
m_str.x = 3;
std :: cout << m_str.x << std :: endl;
}
静态数据成员
多个对象之间共享的数据成员
#include <iostream>
struct Str
{
static int x; // 声明
int y = 1;
};
int Str :: x; // 定义
int main()
{
Str m_str1;
Str m_str2;
m_str1.x = 100;
std :: cout << m_str1.x << std :: endl;
std :: cout << m_str2.x << std :: endl;
}
定义方式的衍化
C++98 :类外定义, const 静态成员的类内初始化
// header.h
struct Str
{
const static int array_Size = 100; // 构造编译期常量
int buffer[array_Size];
};
// str.cpp
const int Str :: array_Size; // 定义
C++17 :内联静态成员的初始化
// header.h
void fun();
struct Str
{
inline static int array_Size = 100;
};
// source.cpp
#include "header.h"
#include <iostream>
void fun()
{
Str m_str2;
m_str2;
std :: cout << "m_str2.array_Size: " << m_str2.array_Size << std :: endl;
}
// main.cpp
#include <iostream>
#include "header.h"
int main()
{
Str m_str1;
m_str1.array_Size = 50;
std :: cout << "m_str1.array_Size: " << m_str1.array_Size << std :: endl;
std :: cout << "&(m_str1.array_Size): " << &(m_str1.array_Size) << std :: endl;
fun();
}
可以使用 auto 推导类型
// header.h
void fun();
struct Str
{
inline static auto array_Size = 100;
};
静态数据成员的访问
“.” 与 “ ->” 操作符
“::” 操作符
// source.cpp
#include "header.h"
#include <iostream>
void fun()
{
Str m_str2;
m_str2;
std :: cout << "m_str2.array_Size: " << m_str2.array_Size << std :: endl;
}
// header.h
void fun();
struct Str
{
inline static auto array_Size = 100;
};
// main.cpp
#include <iostream>
#include "header.h"
int main()
{
Str m_str1;
m_str1.array_Size = 50;
Str* ptr = &m_str1;
std :: cout << "m_str1.array_Size: " << m_str1.array_Size << std :: endl;
std :: cout << "ptr -> array_Size: " << ptr -> array_Size << std :: endl;
std :: cout << "Str :: array_Size: " << Str :: array_Size << std :: endl;
fun();
}
在类的内部声明相同类型的静态数据成员
#include <iostream>
struct Str
{
static Str x; // 不能使用inline,因为系统执行到这一行还无法将Str完整识别出来(incomplete type)
};
Str Str :: x; // Str :: x是一个对象,对象的类型是Str,这个对象所处的域也是Str
int main()
{
Str m_str;
std :: cout << &(m_str.x) << std :: endl;
}
#include <iostream>
struct Str
{
static Str x;
};
inline Str Str :: x; // Str :: x是一个对象,对象的类型是Str,这个对象所处的域也是Str
int main()
{
Str m_str;
std :: cout << &(m_str.x) << std :: endl;
}
成员函数(方法)
// C语言的方法
#include <iostream>
struct Str
{
int x = 3;
};
void fun(Str obj)
{
std :: cout << obj.x << std :: endl;
}
int main()
{
Str m_str;
fun(m_str);
}
可以在结构体中定义函数,作为其成员的一部分
对内操作数据成员,对外提供调用接口
#include <iostream>
struct Str
{
int x = 3;
void fun()
{
std :: cout << x << std :: endl;
}
};
int main()
{
Str m_str;
m_str.fun();
}
- 在结构体中将数据与相关的成员函数组合在一起将形成类,是 C++ 在 C 基础上引入的概念
- 关键字 class
- 类可视为一种抽象数据类型,通过相应的接口(成员函数)进行交互
- 类本身形成域,称为类域
#include <iostream>
class Str
{
public:
int x = 3;
void fun()
{
std :: cout << x << std :: endl;
}
};
int main()
{
Str m_str;
m_str.fun();
}
成员函数的声明与定义
类内定义(隐式内联--防止重复定义)
// header.h
#include <iostream>
struct Str
{
int x;
void fun()
{
std :: cout << x << std :: endl;
}
};
// main.cpp
#include "header.h"
int main()
{
Str m_str;
m_str.x = 3;
m_str.fun();
}
// source.cpp
#include "header.h"
void fun2()
{
Str m_str;
m_str.fun();
}
类内声明 + 类外定义
// header.h
#include <iostream>
struct Str
{
int x;
void fun();
};
// main.cpp
#include "header.h"
int main()
{
Str m_str;
m_str.x = 3;
m_str.fun();
}
// source.cpp
#include "header.h"
void fun2()
{
Str m_str;
m_str.fun();
}
// str.cpp
#include "header.h"
void Str :: fun() // 标明fun所属的作用域是Str
{
std :: cout << x << std :: endl;
}
类与编译期的两遍处理
成员函数与尾随返回类型( trail returning type )
// header.h
#include <iostream>
struct Str
{
using MyRes = int;
MyRes fun();
int x;
};
// main.cpp
#include "header.h"
int main()
{
Str m_str;
m_str.x = 3;
m_str.fun();
}
// source.cpp
#include "header.h"
void fun2()
{
Str m_str;
m_str.fun();
}
// str.cpp
#include "header.h"
Str :: MyRes Str :: fun()
{
return x;
}
改良后
// str.cpp
#include "header.h"
auto Str :: fun() -> MyRes // 系统会自动尝试从Str中查找MyRes
{
return x;
}
成员函数与 this 指针
使用 this 指针引用当前对象
// header.h
#include <iostream>
struct Str
{
void fun(int x = 8) // 隐式传入了 Str * const this(指针本身不能修改,指针指向的内容是可以修改的)
{
std :: cout << this << std :: endl;
std :: cout << x << std :: endl;
std :: cout << this -> x << std :: endl;
std :: cout << Str :: x << std :: endl;
}
int x;
};
// main.cpp
#include "header.h"
int main()
{
Str m_str;
m_str.x = 3;
std :: cout << &(m_str) << std :: endl;
m_str.fun(); // fun(&m_str);
Str m_str2;
m_str2.x = 4;
std :: cout << &(m_str2) << std :: endl;
m_str2.fun();
}
基于 const 的成员函数重载(C++98)
e.g.1
// header.h
#include <iostream>
struct Str
{
void fun() const // this的类型变为 const Str * const,指针本身不能修改,指针所指向的内容也不能修改
{
}
int x;
};
// main.cpp
#include "header.h"
int main()
{
Str m_str;
m_str.x = 3;
std :: cout << &(m_str) << std :: endl;
m_str.fun(); // fun(&m_str);
Str m_str2;
m_str2.x = 4;
std :: cout << &(m_str2) << std :: endl;
m_str2.fun();
}
e.g.2
#include <iostream>
struct Str
{
void fun(int x = 8) const // const Str * const this 用于函数重载的技巧
{
}
void fun(int x = 8) // Str * const this
{
}
int x;
};
成员函数的名称查找与隐藏关系
- 函数内部(包括形参名称)隐藏函数外部
- 类内部名称隐藏类外部
- 使用 this 或域操作符引入依赖型名称查找
静态成员函数
在静态成员函数中返回静态数据成员
#include <iostream>
struct Str
{
static int size() // 构建静态函数后,不用构建对象就能直接访问
{
return 100;
}
int x[100];
};
int main()
{
std :: cout << Str :: size() << std :: endl;
}
#include <iostream>
#include <vector>
struct Str
{
static auto& instance() // 单例模式雏形
{
static Str x;
return x;
}
};
int main()
{
Str :: instance();
}
成员函数基于引用限定符的重载( C++11 )
访问限定符与友元
使用 public/private/protected 限定类成员的访问权限
- 访问权限的引入使得可以对抽象数据类型进行封装
- 类与结构体缺省访问权限的区别
#include <iostream>
struct Str
{
void fun()
{
std :: cout << x << std :: endl;
}
public:
int y;
private:
int x;
protected:
int z;
};
int main()
{
Str m_str;
std :: cout << m_str.y << std :: endl;
}
struct中什么限定类成员的访问权限都不写默认是public
class中什么限定类成员的访问权限都不写默认是private
使用友元打破访问权限限制 —— 关键字 friend
声明某个类或某个函数是当前类的友元 —— 慎用!
#include <iostream>
int main(); // main函数的声明
class Str
{
friend int main(); // 标明main是Str的一个友元,main可以访问Str内部被标记为私有的数据成员或者成员函数
inline static int x;
};
int main()
{
Str m_str;
std :: cout << m_str.x << std :: endl;
}
在类内首次声明友元类或友元函数
- 注意使用限定名称引入友元并非友元类(友元函数)的声明
友元函数 -- 类外定义与类内定义
隐藏友元( hidden friend )
常规名称查找无法找到
https://www.justsoftwaresolutions.co.uk/cplusplus/hidden-friends.html
- 好处:减轻编译器负担,防止误用
- 改变隐藏友元的缺省行为:在类外声明或定义函数
#include <iostream>
class Str
{
inline static int x = 10;
friend void fun(const Str& val)
{
std :: cout << val.x << std :: endl;
}
};
int main()
{
Str m_str;
fun(m_str); // 实参类型的依赖查找
}
构造、析构与复制成员函数
构造函数
构造对象时调用的函数
- 名称与类名相同,无返回值,可以包含多个版本(重载)
#include <iostream>
class Str
{
public:
Str() // 构造函数
{
std :: cout << "Constructor is called." << std :: endl;
}
Str(int input) // 构造函数的重载
{
x = input;
std :: cout << "Str(int input) is called." << ' ' << "input = " << x << std :: endl;
}
private:
int x;
};
int main()
{
Str n; // 系统会自动调用构造函数的逻辑,完成对象的构造
Str m(3);
}
- ( C++11 )代理构造函数
避免代码的重复
#include <iostream>
class Str
{
public:
Str() : Str(3) // 代理构造函数会先被执行 -> 先Str(3),再Str()
{
std :: cout << "here1" << std :: endl;
}
Str(int input)
{
x = input;
std :: cout << "here2" << std :: endl;
}
void fun()
{
std :: cout << x << std :: endl;
}
private:
int x;
};
int main()
{
Str m;
m.fun();
}
初始化列表
区分数据成员的初始化与赋值
通常情况下可以提升系统性能
#include <iostream>
#include <string>
class Str
{
public:
Str(const std :: string& val) : x(val), y(0) // 初始化列表,规避了缺省初始化,提高性能
{
std :: cout << "Pre-assignment: " << x << std :: endl;
std :: cout << "Post-assignment: " << x << std :: endl;
std :: cout << "y: " << y << std :: endl;
}
private:
std :: string x;
int y;
};
int main()
{
Str m("abc");
}
一些情况下必须使用初始化列表(如类中包含引用成员)
引用必须初始化
#include <iostream>
#include <string>
class Str
{
public:
Str(const std :: string& val, int& p_i)
: x(val)
, y(0)
, ref(p_i)
{
std :: cout << "Pre-assignment: " << x << std :: endl;
std :: cout << "Post-assignment: " << x << std :: endl;
std :: cout << "y: " << y << std :: endl;
std :: cout << "ref: " << ref << std :: endl;
ref = 3; // 赋值,不是初始化
}
private:
std :: string x;
int y;
int& ref;
};
int main()
{
int val = 10;
Str m("abc", val);
std :: cout << val << std :: endl;
}
注意元素的初始化顺序与其声明顺序相关,与初始化列表中的顺序无关
用于保证系统性能
#include <iostream>
#include <string>
class Str
{
public:
Str(const std :: string& val)
: x(val)
, y(x.size())
{
std :: cout << x << std :: endl;
std :: cout << y << std :: endl;
}
private:
std :: string x;
size_t y;
};
int main()
{
Str m("abc");
}
使用初始化列表覆盖类内成员初始化的行为
#include <iostream>
class Str
{
public:
Str()
: x(5)
{
std :: cout << "x=" << x << ' ' << "y=" << y << std :: endl;
}
private:
size_t x = 3;
size_t y = 4;
};
int main()
{
Str m;
}
缺省构造函数
不需要提供实际参数就可以调用的构造函数
#include <iostream>
class Str
{
public:
Str(int input = 3)
: x(5)
{
std :: cout << "x=" << x << ' ' << "y=" << y << std :: endl;
std :: cout << "input=" << input << std :: endl;
}
private:
size_t x = 3;
size_t y = 4;
};
int main()
{
Str m;
}
如果类中没有提供任何构造函数,那么在条件允许的情况下,编译器会合成一个缺省构造函数
合成的缺省构造函数会使用缺省初始化来初始化其数据成员
调用缺省构造函数时避免 most vexing parse
Str m(); // 画蛇添足,会被识别为函数声明
// 或者写为 Str m{};
使用 default 关键字定义缺省构造函数
#include <iostream>
#include <string>
class Str
{
public:
Str() = default; // 缺省构造函数内部的逻辑和编译器所合成出来的缺省构造函数内部的逻辑是一样的
Str(const std :: string& input)
: x(input)
{
}
std :: string x;
};
int main()
{
Str m;
std :: cout << "字符串为:" << m.x << std :: endl;
}
单一参数构造函数
可以视为一种类型转换函数
#include <iostream>
class Str
{
public:
Str(int x)
: val(x)
{
}
private:
int val;
};
int main()
{
Str m = 3; // Str m(3); 使用3来构造m;把3转换成m的类型(隐式类型转换)
}
#include <iostream>
class Str
{
public:
Str(int x)
: val(x)
{
}
private:
int val;
};
void fun(Str m)
{
}
int main()
{
fun(3);
}
可以使用 explicit 关键字避免求值过程中的隐式转换
#include <iostream>
class Str
{
public:
explicit Str(int x) // 显式的,规避类型的隐式转换
: val(x)
{
}
private:
int val;
};
void fun(Str m)
{
}
int main()
{
Str m_str(3);
fun(m_str);
}
拷贝构造函数
接收一个当前类对象的构造函数
会在涉及到拷贝初始化的场景被调用,比如:参数传递。因此要注意拷贝构造函数的形参类型
#include <iostream>
class Str
{
public:
Str() = default;
Str(const Str& x) // 避免无限递归,并且不会对传入的值进行修改
: val(x.val)
{
std :: cout <<"Copy constructor is called." << std :: endl;
}
private:
int val = 3; // 类内初始化
};
int main()
{
Str m;
Str m2(m);
}
如果未显式提供,那么编译器会自动合成一个,合成的版本会依次对每个数据成员调用拷贝构造
#include <iostream>
#include <string>
struct Str
{
int val;
std :: string a;
};
int main()
{
Str m;
Str m2(m);
}
#include <iostream>
#include <string>
struct Str
{
Str() = default;
Str(const Str&) = default;
int val;
std :: string a;
};
int main()
{
Str m;
Str m2(m);
}
移动构造函数 (C++11)
接收一个当前类右值引用对象(将亡值)的构造函数
区别于拷贝构造函数,移动完后,会销毁原对象,提高性能
- 可以从输入对象中 “ 偷窃 ” 资源,要确保传入对象在“偷窃”之后处于合法状态
#include <iostream>
#include <string>
struct Str
{
Str() = default;
Str(const Str&) = default;
Str(Str&& x) // 传入右值引用
: val(x.val)
, a(std :: move(x.a))
{
}
void fun()
{
std :: cout << "val=" << val << std :: endl;
std :: cout << "a=" << a << std :: endl;
}
int val = 3;
std :: string a = "abc";
};
int main()
{
Str m;
m.fun();
Str m2 = std :: move(m);
m.fun();
m2.fun();
}
- 当某些特殊成员函数(如拷贝构造)未定义时,编译器可以合成一个
#include <iostream>
#include <string>
struct Str2
{
Str2() = default;
// 有移动调移动,没移动调拷贝
Str2(const Str2&) // 拷贝构造函数
{
std :: cout << "Str2's copy constructor is called." << std :: endl;
}
// Str2(const Str2&&) // 移动构造函数
// {
// std :: cout << "Str2's move constructor is called." << std :: endl;
// }
};
struct Str
{
Str() = default;
Str(const Str&) = default;
Str(Str&& x) = default;
int val = 3;
std :: string a = "abc";
Str2 m_str2;
};
int main()
{
Str m;
Str m2 = std :: move(m);
}
- 通常声明为不可抛出异常的函数
#include <iostream>
#include <string>
struct Str2
{
Str2() = default;
Str2(const Str2&)
{
std :: cout << "Str2's copy constructor is called." << std :: endl;
}
Str2(const Str2&&) noexcept // 移动构造函数通常不抛出异常
{
std :: cout << "Str2's move constructor is called." << std :: endl;
}
};
struct Str
{
Str() = default;
Str(const Str&) = default;
Str(Str&& x) noexcept= default; // 不抛出异常
int val = 3;
std :: string a = "abc";
Str2 m_str2;
};
int main()
{
Str m;
Str m2 = std :: move(m);
}
- 注意右值引用对象用做表达式时是左值!
#include <iostream>
#include <string>
struct Str2
{
Str2() = default;
Str2(const Str2&)
{
std :: cout << "Str2's copy constructor is called." << std :: endl;
}
Str2(const Str2&&) noexcept
{
std :: cout << "Str2's move constructor is called." << std :: endl;
}
};
struct Str
{
Str() = default;
Str(const Str&) = default;
Str(Str&& x) noexcept
{
std :: string tmp = x.a; // x是一个右值引用对象,对其进行求值,用做表达式时是左值(x有一块地址)
// 此时没有“偷”
std :: cout << x.a << std :: endl;
std :: string tmp2 = std :: move(x.a); // 此时“偷”了,x.a是左值,std :: move(x.a)又变为右值
std :: cout << x.a << std :: endl;
}
int val = 3;
std :: string a = "abc";
Str2 m_str2;
};
int main()
{
Str m;
Str m2 = std :: move(m);
}
拷贝赋值与移动赋值函数( operator = )
注意赋值函数不能使用初始化列表
通常来说返回当前类型的引用
#include <iostream>
#include <string>
struct Str
{
Str() = default;
Str(const Str&) = default;
Str(Str&& x) noexcept = default;
Str& operator=(const Str& x) // 拷贝赋值运算
// 返回引用,避免开辟临时对象产生的开销
{
val = x.val;
a = x.a;
std :: cout << "Copy assignment is called." << std :: endl;
return *this;
}
int val = 3;
std :: string a = "abc";
};
int main()
{
Str m;
Str m2;
m2 = m; // 这不是构造,这是赋值
}
#include <iostream>
#include <string>
struct Str
{
Str() = default;
Str(const Str&) = default;
Str(Str&& x) noexcept = default;
Str& operator=(Str&& x) // 移动赋值运算
{
val = std :: move(x.val);
a = std :: move(x.a);
std :: cout << "Move assignment is called." << std :: endl;
return *this;
}
int val = 3;
std :: string a = "abc";
};
int main()
{
Str m;
Str m2;
m2 = std :: move(m); // 这不是构造,这是赋值
}
注意处理给自身赋值的情况
if (&x == *this)
{
return *this;
}
在一些情况下编译器会自动合成
析构函数(销毁)
函数名: “ ~ ” 加当前类型,无参数,无返回值
#include <iostream>
#include <string>
struct Str
{
~Str()
{
std :: cout << "Destructor is called." << std :: endl; // 销毁顺序:m3 m2 m (栈帧结构)
}
int val = 3;
std :: string a = "abc";
};
int main()
{
Str m;
Str m2;
Str m3;
}
用于释放资源
注意内存回收是在调用完析构函数时才进行
除非显式声明,否则编译器会自动合成一个,其内部逻辑为平凡的
#include <iostream>
#include <string>
struct Str
{
Str()
{
std :: cout << "Constructor is called." << std :: endl;
}
~Str()
{
std :: cout << "Destructor is called." << std :: endl;
std :: cout << val << std :: endl;
std :: cout << a << std :: endl;
a.clear();
std :: cout << a << std :: endl;
}
int val = 3;
std :: string a = "abc";
int* ptr;
};
int main()
{
Str* m = new Str(); // ①分配内存 ②调用构造函数 -- 显式地分配内存必须显示地进行内存回收
delete m; // 首先调用类的析构函数,然后将相应的内存回收
}
析构函数通常不能抛出异常
~Str() noexcept = default;
注意
- 一个类如果需要定义析构函数,那么也需要定义拷贝构造与拷贝赋值函数
- 一个类如果需要定义拷贝构造函数,那么也需要定义拷贝赋值函数
- 一个类如果需要定义拷贝构造(赋值)函数,那么也要考虑定义移动构造(赋值)函数
包含指针的类
#include <iostream>
class Str
{
public:
Str() : ptr(new int()) {}
~Str() {delete ptr;}
Str(const Str& val) // 拷贝构造函数
: ptr(new int())
{
std :: cout << "Copy constructor is called." << std :: endl;
*ptr = *(val.ptr);
}
Str& operator=(const Str& val) // 拷贝赋值函数
{
std :: cout << "Copy is called." << std :: endl;
*ptr = *(val.ptr);
return *this;
}
Str(Str&& val) noexcept // 移动构造函数
: ptr(val.ptr)
{
std :: cout << "Move constructor is called." << std :: endl;
val.ptr = nullptr;
}
int& Data()
{
return *ptr;
}
private:
int* ptr;
};
int main()
{
Str a;
a.Data() = 3;
std :: cout << a.Data() << std :: endl;
Str b(a);
Str c = std :: move(a);
Str d;
d = c;
}
default 关键字
只对特殊成员函数有效
delete 关键字
对所有函数都有效
注意其与未声明的区别
注意不要为移动构造(移动赋值)函数引入 delete 限定符
- 如果只需要拷贝行为,那么引入拷贝构造即可
- 如果不需要拷贝行为,那么将拷贝构造声明为 delete 函数即可
- 注意 delete 移动构造(移动赋值)对 C++17 的新影响
#include <iostream>
class Str
{
public:
Str() = default;
Str(const Str& val) = default;
Str(Str&& val) noexcept = delete; // C++17会强制编译器去除移动初始化的优化
};
void fun(Str val)
{
}
int main()
{
fun(Str{});
}
特殊成员的合成行为列表
红框表示支持但可能会废除的行为
字面值类
可以构造编译期常量的类型
- 其数据成员需要是字面值类型
- 提供 constexpr / consteval 构造函数 (小心使用 consteval )
- 平凡的析构函数
- 提供 constexpr / consteval 成员函数 (小心使用 consteval )
- 注意:从 C++14 起 constexpr / consteval 成员函数非 const 成员函数
#include <iostream>
class Str
{
public:
constexpr Str(int val)
: x(val)
{
}
~Str() = default;
private:
int x = 3;
};
constexpr Str a(3);
int main()
{
int x;
Str b(x);
}
#include <iostream>
class Str
{
public:
constexpr Str(int val)
: x(val)
{
}
constexpr int fun() const
{
return x + 1;
}
private:
int x = 3;
};
constexpr Str a(3);
int main()
{
std :: cout << a.fun() << std :: endl;
}
成员指针
数据成员指针类型示例
int A::*;
成员函数指针类型示例
int (A::*)(double);
#include <iostream>
#include <type_traits>
class Str
{
};
class Str2
{
};
int main()
{
int Str :: * ptr; // 数据成员指针
void (Str :: * ptr_fun)(); // 函数成员指针
int Str2 :: * ptr2;
std :: cout << std :: is_same_v<decltype(ptr), decltype(ptr2)> << std :: endl;
}
成员指针对象赋值
auto ptr = &A::x;
#include <iostream>
class Str
{
public:
int x;
int y;
void fun(){};
void fun(double){};
};
int main()
{
int Str :: * ptr = &Str :: x; // 赋值 int* ptr = &x;
auto ptr2 = &Str :: y;
void (Str :: * ptr_fun)() = &Str :: fun; // 当出现函数重载时,成员函数指针的类型不能使用 auto
// auto ptr_fun = &Str :: fun;
}
- 注意域操作符子表达式不能加小括号(否则 A::x 一定要有意义)
成员指针的使用
- 对象 .* 成员指针
- 对象指针 ->* 成员指针
#include <iostream>
class Str
{
public:
int x;
int y;
void fun(){};
void fun(double){};
};
int main()
{
int Str :: * ptr = &Str :: x;
Str obj;
obj.x = 3;
Str obj2;
obj2.x = 5;
std :: cout << obj.*ptr << std :: endl; // 要给出对象以后才能进行解引用
std :: cout << obj2.*ptr << std :: endl;
Str obj3;
Str* ptr_obj3 = &obj3;
ptr_obj3 -> x = 6;
std :: cout << ptr_obj3->*ptr << std :: endl;
}
bind 交互
使用 bind + 成员指针构造可调用对象
#include <iostream>
#include <functional>
class Str
{
public:
int x;
int y;
void fun(double x)
{
std :: cout << x << std :: endl;
};
};
int main()
{
auto ptr = &Str :: fun; // 成员函数指针
Str obj;
(obj.*ptr)(100);
auto x = std :: bind(ptr, obj, 100); // 要将对象作为第二个参数传入
x();
}
注意这种方法也可以基于数据成员指针构造可调用对象
#include <iostream>
#include <functional>
class Str
{
public:
int x;
};
int main()
{
Str obj;
auto ptr = &Str :: x;
obj.*ptr = 3;
std :: cout << obj.*ptr << std :: endl;
auto x = std :: bind(ptr, obj);
std :: cout << x() << std :: endl;
}
十七、类(进阶)
运算符重载
使用 operator 关键字引入重载函数
// 对照版本
#include <iostream>
struct Str
{
int val;
};
Str Add(Str x, Str y)
{
Str z;
z.val = x.val + y.val;
return z;
}
int main()
{
Str x;
x.val = 4;
Str y;
y.val = 5;
Str z = Add(x, y);
std :: cout << z.val << std :: endl;
}
// 引入重载函数
#include <iostream>
struct Str
{
int val;
};
auto operator + (Str x, Str y)
{
Str z;
z.val = x.val + y.val;
return z;
}
int main()
{
Str x;
x.val = 4;
Str y;
y.val = 5;
Str z = x + y;
std :: cout << z.val << std :: endl;
}
重载不能发明新的运算,不能改变运算的优先级与结合性,通常不改变运算含义
函数参数个数与运算操作数个数相同,至少一个为类类型
除 operator() 外其它运算符不能有缺省参数
#include <iostream>
struct Str
{
int val;
auto operator () (int y = 8)
{
return val + y;
}
};
int main()
{
Str x;
x.val = 5;
int y = 10;
std :: cout << x() << std :: endl;
std :: cout << x(y) << std :: endl;
}
可以选择实现为成员函数与非成员函数
- 通常来说,实现为成员函数会以 *this 作为第一个操作数(注意 == 与 <=> 的重载)
根据重载特性,可以将运算符进一步划分
可重载且必须实现为成员函数的运算符( =,[],(),-> 与转型运算符)
可重载且可以实现为非成员函数的运算符
可重载但不建议重载的运算符( &&, ||, 逗号运算符)
- C++17 中规定了相应的求值顺序但没有方式实现短路逻辑
不可重载的运算符(如 ? :运算符)
对称运算符通常定义为非成员函数以支持首个操作数的类型转换
#include <iostream>
struct Str
{
Str(int x)
: val(x)
{}
auto operator + (Str input)
{
std :: cout << "Operator + is called. \n";
return Str(val + input.val);
}
int val;
};
int main()
{
Str x = 3;
Str y = 4;
Str z = x + 4; // 但是此时不支持 Str z = 4 + x;
}
// 修改后
#include <iostream>
struct Str
{
Str(int x)
: val(x)
{}
int val;
};
auto operator + (Str input1, Str input2)
{
std :: cout << "Operator + is called. \n";
return Str(input1.val + input2.val);
}
int main()
{
Str x = 3;
Str y = 4;
Str z = 4 + x;
}
// 优化后
#include <iostream>
struct Str
{
Str(int x)
: val(x)
{}
friend auto operator + (Str input1, Str input2) // 友元函数,位于全局域,可以访问private成员
{
std :: cout << "Operator + is called. \n";
return Str(input1.val + input2.val);
}
private:
int val;
};
int main()
{
Str x = 3;
Str z = 4 + x;
}
移位运算符一定要定义为非成员函数,因为其首个操作数类型为流类型
#include <iostream>
struct Str
{
Str(int x)
: val(x)
{}
friend auto operator + (Str input1, Str input2)
{
std :: cout << "Operator + is called. \n";
return Str(input1.val + input2.val);
}
friend auto& operator << (std :: ostream& ostr, Str input) // 输出流不支持拷贝,只能返回引用
{
ostr << input.val;
return ostr;
}
private:
int val;
};
int main()
{
Str x = 3;
Str z = 4 + x;
std :: cout << x << z << std :: endl;
}
赋值运算符也可以接收一般参数
与“拷贝赋值运算”作比较
作为成员函数时,只有一个参数,其会自带一个隐式参数
#include <iostream>
#include <string>
struct Str
{
Str(int x)
: val(x)
{}
Str& operator=(const std ::string& input)
{
val = static_cast<int>(input.size()); // size_t类型转换为int
return *this;
}
int val;
};
int main()
{
Str x = 3;
x = "123456";
std :: cout << x.val << std :: endl;
}
operator [] 通常返回引用
#include <iostream>
struct Str
{
Str(int x)
: val(x)
{}
int& operator[](int id) // 返回引用才能既支持读,又支持写
{
return val;
}
int operator[](int id) const // 有const情况下的函数重载,支持读const Str,不允许写
{
return val;
}
int val;
};
int main()
{
Str x = 3;
std :: cout << x[5] << std :: endl;
x[0] = 100;
std :: cout << x[5] << std :: endl;
const Str cx = 66;
std :: cout << cx[5] << std :: endl;
}
自增、自减运算符的前缀、后缀重载方法
#include <iostream>
struct Str
{
Str(int x)
: val(x)
{}
Str& operator++() // 前缀自增
{
++val;
return *this;
}
Str operator++(int input) // 后缀自增,input一般情况下都是0
{
Str tmp(*this); // 将当前对象拷贝为临时对象,可能会非常耗时,有些时候还可能不支持拷贝
// 因此尽量使用前缀自增
++val;
return tmp;
}
int val;
};
int main()
{
int x = 3;
std :: cout << (++x) << std :: endl;
Str s(5);
std :: cout << (++s).val << std :: endl;
int y = 7;
std :: cout << (y++) << std :: endl;
std :: cout << y << std :: endl;
Str t(9);
std :: cout << (t++).val << std :: endl;
std :: cout << t.val << std :: endl;
}
使用解引用运算符( * )与成员访问运算符( -> )模拟指针行为
- 注意 “ .” 运算符不能重载
- “→” 会递归调用 “→” 操作
#include <iostream>
struct Str
{
Str(int* p)
: ptr(p)
{}
int& operator*() // 返回引用,支持写
{
return *ptr;
}
int operator*() const
{
return *ptr;
}
private:
int* ptr;
};
int main()
{
int x = 100;
Str ptr(&x);
std :: cout << *ptr << std :: endl;
*ptr = 222;
std :: cout << *ptr << std :: endl;
}
#include <iostream>
struct Str
{
Str(int* p)
: ptr(p)
{}
Str* operator->()
{
return this;
}
int val = 666;
private:
int* ptr;
};
int main()
{
int x = 10;
Str ptr(&x);
std :: cout << ptr -> val << std :: endl;
}
#include <iostream>
struct Str2
{
Str2* operator->()
{
return this;
}
int blabla = 22;
};
struct Str
{
Str(int* p)
: ptr(p)
{}
Str2 operator->()
{
return Str2{};
}
int val = 666;
private:
int* ptr;
};
int main()
{
int x = 10;
Str ptr(&x);
std :: cout << ptr -> blabla << std :: endl;
}
使用函数调用运算符构造可调用对象
函数调用符的参数列表的个数是不定的
#include <iostream>
struct Str
{
Str(int p)
: val(p)
{}
int operator()()
{
return val;
}
int operator()(int x, int y, int z)
{
return val + x + y + z;
}
private:
int val;
};
int main()
{
Str obj(100);
std :: cout << obj() << std :: endl;
std :: cout << obj(1, 2, 3) << std :: endl;
}
本质就是为了构造可调用对象(函数泛化)
类型转换运算符
函数声明为 operator type() const
#include <iostream>
struct Str
{
Str(int p)
: val(p)
{}
operator int() const // 通常加个const,不改变传入对象本身的内容
{
return val;
}
private:
int val;
};
int main()
{
Str obj(100);
std :: cout << int(obj) << std :: endl;
int v = obj;
std :: cout << v << std :: endl;
}
与单参数构造函数一样,都引入了一种类型转换方式
#include <iostream>
struct Str
{
Str(int p)
: val(p)
{}
operator int() const // 通常加个const,不改变传入对象本身的内容
{
return val;
}
private:
int val;
};
int main()
{
Str obj(100);
std :: cout << int(obj) << std :: endl;
int v = obj;
std :: cout << v << std :: endl;
static_cast<Str>(100);
std :: cout << static_cast<int>(obj) << std :: endl;
}
注意避免引入歧义性与意料之外的行为
- 通过 explicit 引入显式类型转换
#include <iostream>
#include <type_traits>
struct Str
{
explicit Str(int p) // 不允许隐式转换,将int转换为Str
: val(p)
{}
operator int() const
{
return val;
}
friend auto operator+(Str a, Str b)
{
return Str(a.val + b.val);
}
private:
int val;
};
int main()
{
Str obj(100);
std :: cout << obj + 3 << std :: endl; // 不能通过隐式转换将3,int转换为Str,因此只能通过隐式转换将obj,Str转换为int
}
std :: cout << std :: is_same_v<decltype(obj+3), int> << std :: endl;
explicit bool 的特殊性:用于条件表达式时会进行隐式类型转换
#include <iostream>
struct Str
{
Str(int p)
: val(p)
{}
explicit operator bool() const // explicit bool
{
return (val == 0);
}
private:
int val;
};
int main()
{
Str obj(100);
// std :: cout << obj << std :: endl;
auto var = obj ? 1 : 0; // 作为条件,相当于使用 static_cast<bool>(obj),进行显式类型转换
std :: cout << "var: " << var << std :: endl;
if (obj)
{
std :: cout << 1 << std :: endl;
}
else
{
std :: cout << 0 << std :: endl;
}
}
C++ 20 中对 == 与 <=> 的重载
优化:只需要定义两种关系运算符,就能定义所有关系运算符
通过 == 定义 !=(反过来不行,不能通过定义!=来定义==)
#include <iostream>
struct Str
{
Str(int p)
: val(p)
{}
friend bool operator==(Str obj1, Str obj2)
{
return obj1.val == obj2.val;
}
private:
int val;
};
int main()
{
Str obj1(100);
Str obj2(100);
std :: cout << (obj1 == obj2) << std :: endl;
std :: cout << (obj1 != obj2) << std :: endl; // 系统会自动推到出 !=
}
通过 <=> 定义多种比较逻辑
隐式交换操作数
注意 <=> 可返回的类型: strong_ordering, week_ordering, partial_ordering
#include <iostream>
#include <compare>
struct Str
{
Str(int p)
: val(p)
{}
auto operator<=>(int x)
{
return val <=> x;
}
private:
int val;
};
int main()
{
Str obj(100);
std :: cout << (100 < obj) << std :: endl;
}
类的继承
通过类的继承(派生)来引入 “ 是一个 ” 的关系
通常采用 public 继承( struct V.S. class )
#include <iostream>
struct Base
{
};
struct Derive : public Base // struct中如果缺省继承类型,默认会使用public公有继承,
// class中如果缺省继承类型,默认会使用private公有继承,
{
};
int main()
{
}
注意:继承部分不是类的声明
使用基类的指针或引用可以指向派生类对象
#include <iostream>
struct Base
{
};
struct Derive : public Base
{
};
int main()
{
Derive d;
Base& ref = d;
Base* ptr = &d;
}
静态类型 V.S. 动态类型
- 静态类型是在编译期知道的类型,不可以发生改变
- 动态类型是在运行期知道的类型,可以在运行期发生改变
protected 限定符:派生类可访问
类的派生会形成嵌套域
派生类所在域位于基类内部
派生类中的名称定义会覆盖基类
使用域操作符显式访问基类成员
在派生类中调用基类的构造函数
#include <iostream>
struct Base
{
Base()
{
std :: cout << "Base constructor is called." << std :: endl;
}
};
struct Derive : public Base
{
Derive()
{
std :: cout << "Derive constructor is called." << std :: endl;
}
};
int main()
{
Derive d;
}
#include <iostream>
struct Base
{
Base(int)
{
std :: cout << "Base constructor is called." << std :: endl;
}
};
struct Derive : public Base
{
Derive(int a)
: Base(a) // 显式调用基类构造函数(默认只会调用缺省构造函数,其他构造函数需要显式调用)
{
std :: cout << "Derive constructor is called." << std :: endl;
}
};
int main()
{
Derive d(3);
}
虚函数
通过虚函数与引用(指针)实现动态绑定
使用关键字 virtual 引入
非静态、非构造函数可声明为虚函数
虚函数会引入vtable结构
- https://www.avabodh.com/cxxin/multivirtual.html
- dynamic_cast
- dynamic_cast的转换是在运行期执行的,要判断动态内存是否满足条件,比较花费时间
#include <iostream>
struct Base
{
virtual void baseMethod(){}
int baseMember;
};
struct myCLassDerived : public Base
{
virtual void derivedMethod(){}
int derivedMember;
};
struct myCLassDerived2 : public myCLassDerived
{
virtual void derivedMethod2(){}
int derivedMember2;
};
int main()
{
myCLassDerived2 d;
Base& b = d;
Base* ptr = &d;
myCLassDerived2& d2 = dynamic_cast<myCLassDerived2&>(b); // dynamic_cast:如果是从基类引用转换为派生类引用,且这个基类引用确实绑定到相应派生类的引用的话,就会执行成功,否则会抛出异常
myCLassDerived2* ptr2 = dynamic_cast<myCLassDerived2*>(ptr); // dynamic_cast:如果是从基类指针转换为派生类指针就会执行成功,否则会返回空指针
}
动态多态
#include <iostream>
struct Base // struct 默认的限定符是public
{
virtual void fun() // 虚函数--支持重写,在派生类覆盖基类的行为
// 没有虚函数,就没有vtable,就不存在多态了
// 没有虚函数,所有的行为就都是在编译期调用了,编译期只看静态类型
{
std :: cout << "Base :: fun() is called." << std :: endl;
}
};
struct Derive : Base
{
void fun()
{
std :: cout << "Derive :: fun() is called." << std :: endl;
}
};
int main()
{
Derive d;
d.fun();
Base& b = d;
b.fun();
}
#include <iostream>
struct Base // struct 默认的限定符是public
{
virtual void fun()
{
std :: cout << "Base :: fun() is called." << std :: endl;
}
};
struct Derive : Base
{
void fun()
{
std :: cout << "Derive :: fun() is called." << std :: endl;
}
};
void proc(Base& b)
{
b.fun();
}
int main()
{
Derive d;
proc(d);
Base b;
proc(b);
}
虚函数在基类中的定义
引入缺省逻辑
可以通过 = 0 声明纯虚函数,相应地构造抽象基类
不能使用抽象基类初始化一个对象
virtual void fun() = 0; // 需要在派生类中实现这个接口
虚函数在派生类中的重写( override )
函数签名保持不变(返回类型可以是原始返回指针 / 引用类型的派生指针 / 引用类型)
虚函数特性保持不变
override 关键字
#include <iostream>
struct Base // struct 默认的限定符是public
{
virtual void fun()
{
std :: cout << "Base :: fun() is called." << std :: endl;
}
};
struct Derive : Base
{
void fun() override // 表示这个函数是原始函数的重写,让编译器能够及时抛出错误
{
std :: cout << "Derive :: fun() is called." << std :: endl;
}
};
void proc(Base& b)
{
b.fun();
}
int main()
{
Derive d;
proc(d);
Base b;
proc(b);
}
由虚函数所引入的动态绑定属于运行期行为,与编译期行为有所区别
虚函数的缺省实参只会考虑静态类型
#include <iostream>
struct Base
{
virtual void fun(int x = 3)
{
std :: cout << "Base: " << x << std :: endl;
}
};
struct Derive : Base
{
void fun(int x = 4) override
{
std :: cout << "Derive: " << x << std :: endl;
}
};
void proc(Base& b) // b的静态类型是Base
{
b.fun();
}
int main()
{
Derive d;
proc(d); // Derive: 3
Base b;
proc(b); // Base: 3
}
虚函数的调用成本高于非虚函数
- final 关键字
#include <iostream>
struct Base
{
virtual void fun(int x = 3)
{
std :: cout << "Base: " << x << std :: endl;
}
};
struct Derive : Base
{
void fun(int x = 4) override final // final相当于告诉编译器,接下来会有些类派生于Derive,
// 但是所有派生于Derive的新的类,都不会对fun函数进行重写了
{
std :: cout << "Derive: " << x << std :: endl;
}
};
void proc(Base& b)
{
b.fun();
}
int main()
{
Derive d;
proc(d);
Base b;
proc(b);
}
#include <iostream>
struct Base
{
virtual void fun(int x = 3)
{
std :: cout << "Base: " << x << std :: endl;
}
};
struct Derive final : Base // 这个类里面所有的虚函数都不会再被重写了
{
void fun(int x = 4) override
{
std :: cout << "Derive: " << x << std :: endl;
}
};
void proc(Base& b)
{
b.fun();
}
int main()
{
Derive d;
proc(d);
Base b;
proc(b);
}
为什么要使用指针(或引用)引入动态绑定
在构造函数中调用虚函数要小心
派生类的析构函数会隐式调用基类的析构函数
通常来说要将基类的析构函数声明为 virtual 的
#include <iostream>
struct Base
{
~Base()
{
std :: cout << "Base" << std :: endl;
}
};
struct Derive final : Base
{
~Derive()
{
std :: cout << "Derive" << std :: endl;
}
};
int main()
{
Derive* d = new Derive();
delete d; // 先打印Derive,再打印Base ==> 栈帧结构(先构建的Base,再构建的Derive)
std :: cout << '\n';
Base* b = d;
delete b; // 只打印了Base,Derive的析构函数没有调用
}
#include <iostream>
#include <memory>
struct Base
{
virtual ~Base()
{
std :: cout << "Base" << std :: endl;
}
};
struct Derive final : Base
{
~Derive() // 虽然名称和Base中的不同,但是都是析构函数,仍会继承Base中析构函数的行为
{
std :: cout << "Derive" << std :: endl;
}
};
int main()
{
std :: shared_ptr<Base> ptr(new Derive());
}
virtual ~Base() = default; // 仅用于将析构函数变为虚函数virtual
在派生类中修改虚函数的访问权限
- 判断访问权限是在编译期的行为,编译器只关注静态类型
继承与特殊成员函数
派生类合成的缺省构造函数会隐式调用基类的缺省构造函数
派生类合成的拷贝构造函数将隐式调用基类的拷贝构造函数
派生类合成的赋值函数将隐式调用基类的赋值函数
派生类使用default也会调用基类相应的函数
派生类的析构函数会调用基类的析构函数
派生类的其它构造函数将隐式调用基类的缺省构造函数
所有的特殊成员函数在显式定义时都可能需要显式调用基类相关成员
可以通过初始化列表的方式调用基类的其他构造函数
struct Derive : Base
{
Derive(const Derive& input)
: Base(input)
{
}
};
struct Derive : Base
{
Derive& operator=(const Derive& val)
{
Base :: operator = val;
return *this;
}
}
构造与销毁顺序
- 基类的构造函数会先调用,之后才涉及到派生类中数据成员的构造
- 派生类中的数据成员会被先销毁,之后才涉及到基类的析构函数调用
public 与 private 继承
https://www.programiz.com/cpp-programming/public-protected-private-inheritance
- public 继承:描述 “ 是一个 ” 的关系(大部分情况使用)
- private 继承:描述 “ 根据基类实现出 ” 的关系
- protected 继承:几乎不会使用
using 与继承
使用 using 改变基类成员的访问权限
#include <iostream>
struct Base
{
public:
int x;
private:
int y;
protected:
int z;
};
struct Derive : public Base
{
public:
using Base :: z;
private:
using Base :: x;
};
int main()
{
Derive d;
d.z;
}
#include <iostream>
struct Base
{
protected:
void fun(){}
};
struct Derive : public Base
{
public:
using Base :: fun; // 只要给出函数名称即可,不用给出定义
};
int main()
{
Derive d;
d.fun();
}
派生类可以访问该成员
派生类得有权限访问基类成员,才能进行修改
无法改变构造函数的访问权限
#include <iostream>
struct Base
{
protected:
Base(int val){} // 构造函数
};
struct Derive : public Base
{
public:
using Base :: Base;
};
int main()
{
// Derive d(100); // 由于Base基类的构造函数权限不能被修改,因此Derive派生类的对象不能访问Base基类中protected权限的构造函数
}
使用 using 继承基类的构造函数逻辑
#include <iostream>
struct Base
{
public:
Base(int val){} // 构造函数
};
struct Derive : public Base
{
public:
using Base :: Base;
};
int main()
{
Derive d(100); // 基类中public权限的构造函数可以正常访问
}
using 与部分重写
#include <iostream>
struct Base
{
protected:
virtual void fun()
{
std :: cout << "Base :: fun()" << std :: endl;
}
virtual void fun(int)
{
std :: cout << "Base :: fun(int)" << std :: endl;
}
};
struct Derive : public Base
{
public:
using Base :: fun; // Base基类中的fun函数从protected变为public并不会改变fun是虚函数的性质
// using的优先级低于override重写的优先级
void fun(int) override
{
std :: cout << "Derive :: fun(int)" << std :: endl;
}
};
int main()
{
Derive d;
d.fun();
d.fun(2);
}
#include <iostream>
struct Base
{
protected:
Base() // Base基类采用缺省构造函数
{
std :: cout << "Base :: constructor is called." << std :: endl;
}
Base(const Base&) // Base基类采用缺省构造函数
{
std :: cout << "Base :: copy constructor is called." << std :: endl;
}
};
struct Derive : public Base
{
public:
using Base :: Base;
// Derive() = default; // 编译器会自动合成一个缺省构造函数,这个缺省构造函数会调用Base基类的缺省构造函数
// Derive(const Derive&) = default;
};
int main()
{
Derive d; // 此时可以构建Derive对象
Derive d1(d);
}
继承与友元
不能在派生类中声明基类的友元,否则会让访问限制“形同虚设”
#include <iostream>
struct Base
{
friend void fun(const Base&);
private:
int x = 10;
};
struct Derive : public Base
{
private:
int y = 20;
};
void fun(const Base& val)
{
std :: cout << val.x << std :: endl;
}
int main()
{
Base b;
fun(b);
Derive d;
fun(d);
}
友元关系无法继承,但基类的友元可以访问派生类中基类的相关成员
#include <iostream>
struct Derive; // 先引入派生类的声明
struct Base
{
friend void fun(const Derive&);
private:
int x = 10;
};
struct Derive : public Base
{
private:
int y = 20;
};
void fun(const Derive& val)
{
std :: cout << val.x << std :: endl; // 友元函数是Base的友元,其中只能访问隶属于Base的相关成员
// std :: cout << val.y << std :: endl; // 虽然传入的是Derive的引用,但是该友元不是Derive的友元,因此不能访问Derive中的相关成员
}
int main()
{
Derive b;
fun(b);
}
通过基类指针实现在容器中保存不同类型对象
#include <iostream>
#include <vector>
#include <memory>
struct Base
{
virtual double GetValue() = 0;
virtual ~Base() = default;
};
struct Derive : public Base
{
Derive(int x)
: val(x)
{}
double GetValue() override
{
return val;
}
int val;
};
struct Derive2 : public Base
{
Derive2(double x)
: val(x)
{}
double GetValue() override
{
return val;
}
double val;
};
int main()
{
// Base* ptr = &d; // 可以使用基类指针指向派生类地址
// 某些情况下需要定义一个数组,数组当中保存不一样类型的元素
// 利用派生引入一个间接的层
std :: vector<std :: shared_ptr<Base>> vec;
vec.emplace_back(new Derive{1});
vec.emplace_back(new Derive2{3.14});
std :: cout << vec[0]->GetValue() << std :: endl;
std :: cout << vec[1]->GetValue() << std :: endl;
}
多重继承与虚继承
#include <iostream>
struct Base1
{
virtual ~Base1() = default;
};
struct Base2
{
virtual ~Base2() = default;
};
struct Derive : public Base1, public Base2 // 多重继承 -- 典型应用于输入输出流
{
};
int main()
{
}
#include <iostream>
struct Base
{
virtual ~Base() = default;
int x;
};
struct Base1 : virtual Base // 虚继承,只保存一个Base的实例,防止同时从Base1和Base2里面一起查找,造成多重查找的问题
{
virtual ~Base1() = default;
};
struct Base2 : virtual Base
{
virtual ~Base2() = default;
};
struct Derive : public Base1, public Base2
{
};
int main()
{
Derive d;
d.x;
}
空基类优化与 [[no_unique_address]] 属性
对对象的尺寸要求比较严格,要求尺寸比较小
C++20以前
#include <iostream>
struct Base // 空的类
{
};
struct Derive : Base
{
};
int main()
{
std :: cout << sizeof(Base) << std :: endl; // 1
}
#include <iostream>
struct Base // 定义了一个函数
{
void fun()
{
}
};
struct Derive : public Base
{
};
int main()
{
std :: cout << sizeof(Base) << std :: endl; // 1
}
#include <iostream>
struct Base
{
void fun()
{
}
};
struct Derive
{
int x; // 占4个字节
Base b; // 占1个字节
};
int main()
{
std :: cout << sizeof(Base) << std :: endl; // 1
std :: cout << sizeof(Derive) << std :: endl; // 8 5+3(其他信息占的内存)
}
#include <iostream>
struct Base
{
void fun()
{
}
};
struct Derive : public Base // 空基类(这个基类中不包含任何数据成员)优化
{
int x; // 占4个字节
};
int main()
{
std :: cout << sizeof(Base) << std :: endl; // 1
std :: cout << sizeof(Derive) << std :: endl; // 4
}
C++20
#include <iostream>
struct Base
{
void fun()
{
}
};
struct Derive
{
int x; // 占4个字节
[[no_unique_address]] Base b; // 只是想要调用b当中的一些成员,但是不显式为这个b分配空间去保存,
// Derive与Base之间没有关系
};
int main()
{
std :: cout << sizeof(Base) << std :: endl; // 1
std :: cout << sizeof(Derive) << std :: endl; // 4
}
十八、模板
函数模板
函数模板不是函数
使用 template 关键字引入模板
template<typename T> void fun(T) {...}
函数模板的声明与定义
#include <iostream>
template<typename T>
void fun(T); // 函数模板的声明
template<typename T>
void fun(T) // 函数模板的定义
{
}
int main()
{
}
typename 关键字可以替换为 class ,含义相同
函数模板中包含了两对参数:函数形参 / 实参;模板形参 / 实参
#include <iostream>
template<typename T> // T是模板形参,要在编译期赋予相应的实参,便可以将函数模板实例化为一个函数
void fun(T input) // input是函数形参,主要是在运行期调用的,在运行期传入实参,触发相应的运行期调用,给出结果
{
std :: cout << input << std :: endl;
}
int main()
{
}
函数模板的显式实例化
fun<int>(3)
#include <iostream>
template<typename T>
void fun(T input)
{
std :: cout << "template fun : " << input << std :: endl;
}
int main()
{
fun<int>(3); // int为模板实参,对应模板形参T,3为函数实参,对应函数形参input
}
实例化会使得编译器产生相应的函数(函数模板并非函数,不能调用)
编译期的两阶段处理
- 模板语法检查
- 模板实例化
模板必须在实例化时可见 —— 放松了翻译单元的一处定义原则
注意与内联函数的异同
函数模板的重载
#include <iostream>
template<typename T>
void fun(T input)
{
std :: cout << "template fun(T input) : " << input << std :: endl;
}
template<typename T, typename T2>
void fun(T input, T2 input2)
{
std :: cout << "template fun(T input, T2 input2) : " << input << ' ' << input2 << std :: endl;
}
int main()
{
fun<int>(3);
fun<int>(2, 4);
}
#include <iostream>
template<typename T>
void fun(T input)
{
std :: cout << "template fun(T input) : " << input << std :: endl;
}
template<typename T>
void fun(T* input)
{
std :: cout << "template fun(T* input) : " << *input << std :: endl;
}
int main()
{
fun<int>(3);
int x = 10;
fun<int>(&x);
}
模板实参的类型推导
如果函数模板在实例化时没有显式指定模板实参,那么系统会尝试进行推导
推导是基于函数实参(表达式)确定模板实参的过程,其基本原则与 auto 类型推导相似
函数形参是左值引用 / 指针
template<typename T>
void fun(T& input)
忽略表达式类型中的引用
将表达式类型与函数形参模式匹配以确定模板实参
函数形参是万能引用
template<typename T>
void fun(T&& input) // 此处不是右值引用,是万能引用
如果实参表达式是右值,那么模板形参被推导为去掉引用的基本类型
如果实参表达式是左值,那么模板形参被推导为左值引用,触发引用折叠
- int& && -> int&
函数形参不包含引用
template<typename T>
void fun(T input)
忽略表达式类型中的引用
忽略顶层 const
- const int* const -> const int*
数组、函数转换成相应的指针类型
模板实参并非总是能够推导得到
如果模板形参与函数形参无关,则无法推导
template<typename T, typename Res>
Res fun(T input)
即使相关,也不一定能进行推导,推导成功也可能存在因歧义而无法使用
template<typename T>
Res fun(typename std :: remove_reference<T> :: type input)
#include <iostream>
template<typename T>
void fun(T i1, T i2)
{
}
int main()
{
fun(3, 5.0); // deduced conflicting types for parameter 'T' ('int' vs. 'double')
}
在无法推导时,编译器会选择使用缺省模板实参
template<typename T = int>
Res fun(typename std :: remove_reference<T> :: type input)
可以为任意位置的模板形参指定缺省模板实参 —— 注意与函数缺省实参的区别
#include <iostream>
template<typename T1, typename Res = double, typename T2>
Res fun(T1 x, T2 y)
{
}
int main()
{
fun(3, 5);
}
显式指定部分模板实参
显式指定的模板实参必须从最左边开始,依次指定
模板形参的声明顺序会影响调用的灵活性
函数模板制动推导时会遇到的几种情况
函数形参无法匹配 —— SFINAE (替换失败并非错误)
模板与非模板同时匹配,匹配等级相同,此时选择非模板的版本
多个模板同时匹配,此时采用偏序关系确定选择 “ 最特殊 ” 的版本
#include <iostream>
template<typename T, typename T2>
void fun(T x, T2 y)
{
std :: cout << "void fun(T x, T2 y)" << std :: endl;
}
template<typename T>
void fun(T x, float y) // 匹配等级相同的情况下,优先匹配更特殊的情况,该情况下y只匹配float的情况
{
std :: cout << "void fun(T x, float y)" << std :: endl;
}
int main()
{
fun(3, 5.0f);
}
函数模板的实例化控制
显式实例化定义
template void fun
(int) / template void fun(int)
// header.h
#include <iostream>
template <typename T>
void fun(T x)
{
std :: cout << x << std :: endl;
}
template
void fun<int>(int x); // 显式引入定义
// main.cpp
#include <iostream>
#include "header.h"
int main()
{
int x = 3;
fun<int>(x);
}
显式实例化声明(C++11)
extern template void fun
(int) / extern template void fun(int)
// header.h
#include <iostream>
template <typename T>
void fun(T x)
{
std :: cout << x << std :: endl;
}
// source.cpp
#include "header.h"
template // 显式实例化定义
void fun<int>(int x);
// main.cpp
#include <iostream>
#include "header.h"
extern template // 显式实例化声明
// 要求编译器不要在main.cpp中产生定义
// 编译器会在链接的翻译单元中找到fun函数的显式实例化定义,即在source.cpp文件中生成定义
void fun<int>(int x);
int main()
{
int x = 3;
fun<int>(x);
}
注意
注意一处定义原则
- https://stackoverflow.com/questions/52664184/why-does-explicit-template-instantiation-not-break-odr
- 虽然编译器对于模板放松了要求,但是还是尽量保证满足一处定义原则,以保证程序具有良好的可移植性
注意实例化过程中的模板形参推导
#include <iostream>
template <typename T>
void fun(T x)
{
std :: cout << "void fun(T x)" << std :: endl;
}
template <typename T>
void fun(T* x)
{
std :: cout << "void fun(T* x)" << std :: endl;
}
int main()
{
int x = 3;
fun<int*>(&x); // void fun(T x)
}
函数模板的 ( 完全 ) 特化
template<> void f
(int) / template<> void f(int)
#include <iostream>
template <typename T>
void fun(T x)
{
std :: cout << "x" << std :: endl;
}
template <> // 不能漏了<>,否则就不是完全特化,变为显式实例化定义了
void fun(int x)
{
}
int main()
{
}
并不引入新的(同名)名称,只是为某个模板针对特定模板实参提供优化算法
注意与重载的区别
注意特化过程中的模板形参推导
避免使用函数模板的特化
函数重载解析调用过程
查找函数名称->模板实参推到->重载解析->检查调用函数是否允许访问->函数模板特化->(如果是虚函数)进行虚函数的派发->(如果该函数被标记为delete)执行相应的delete行为
https://www.youtube.com/watch?v=GydNMuyQzWo&list=PLHTh1InhhwT6DdPY3CPxayypP5DXek_vG&index=8
不参与重载解析,会产生反直觉的效果
#include <iostream>
template <typename T>
void fun(T x)
{
std :: cout << "void fun(T x)" << std :: endl;
}
template <typename T>
void fun(T* x)
{
std :: cout << "void fun(T* x)" << std :: endl;
}
template <>
void fun(int* x)
{
std :: cout << "void fun(int* x)" << std :: endl;
}
int main()
{
int x;
fun(&x);
}
通常可以用重载代替
一些不便于重载的情况:无法建立模板形参与函数形参的关联
使用 if constexpr 解决
#include <iostream>
#include <type_traits>
template <typename Res, typename T>
Res fun(T x)
{
if constexpr(std :: is_same_v<Res, int>)
{
std :: cout << "1\n";
}
else
{
std :: cout << "2\n";
}
return Res{};
}
int main()
{
int x;
fun<float>(&x);
}
引入 “ 假 ” 函数形参
#include <iostream>
#include <type_traits>
template <typename Res, typename T>
Res fun(T x, const Res&)
{
std :: cout << "1\n";
return Res{};
}
template <typename T>
int fun(T x, const int&) // 函数重载
{
std :: cout << "2\n";
return int{};
}
int main()
{
int x;
fun(&x, int{});
}
通过类模板特化解决
函数模板的简化形式(C++20)
使用 auto 定义模板参数类型
优势:书写简捷
劣势:在函数内部需要间接获取参数类型信息
#include <iostream>
void fun(auto x)
{
}
int main()
{
int x;
fun(&x);
}
类模板与成员函数模板
使用 template 关键字引入模板
template<typename T> class B {…};
类模板的声明与定义 —— 翻译单元的一处定义原则
#include <iostream>
template <typename T>
class B; // 类模板的声明
template <typename T>
class B // 类模板的定义
// 函数模板不是函数,类模板也不是类
{
};
int main()
{
B<int> x;
B<char> y;
}
成员函数只有在调用时才会被实例化
#include <iostream>
template <typename T>
class B
{
public:
void fun(T input)
{
std :: cout << input << std :: endl;
}
};
int main()
{
B<int> x;
x.fun(3);
}
类内类模板名称的简写
#include <iostream>
template <typename T>
class B
{
public:
auto fun()
{
return B{}; // return B<T>{};
}
};
int main()
{
B<int> x;
x.fun();
}
类模板成员函数的定义(类内、类外)
#include <iostream>
template <typename T>
class B
{
public:
void fun();
};
template <typename T>
void B<T> :: fun() // 类外定义成员函数
{
}
int main()
{
B<int> x;
x.fun();
}
成员函数模板
类的成员函数模板
#include <iostream>
class B
{
public:
template <typename T> // 类的成员函数模板(类内定义)
void fun()
{
}
};
int main()
{
B x;
x.fun<int>();
}
#include <iostream>
class B
{
public:
template <typename T>
void fun();
};
template <typename T> // 类的成员函数模板(类外定义)
void B :: fun()
{
}
int main()
{
B x;
x.fun<int>();
}
类模板的成员函数模板
#include <iostream>
template <typename T>
class B
{
public:
template <typename T1> // 类模板的成员函数模板(类内定义)
void fun()
{
T1 tmp1;
T tmp;
}
private:
T m_data;
};
int main()
{
B<int> x; // int--T
x.fun<float>(); // float--T1
}
#include <iostream>
template <typename T>
class B
{
public:
template <typename T1>
void fun();
};
template <typename T> // 要有两层的template,类模板的成员函数模板(类外定义)
template <typename T1>
void B<T> :: fun()
{
}
int main()
{
B<int> x; // int--T
x.fun<float>(); // float--T1
}
友元函数(模板)
可以声明一个函数模板为某个类(模板)的友元
#include <iostream>
template <typename T1>
void fun(); // 函数模板的声明(此时还不是友元函数)
template <typename T>
class B
{
template <typename T1>
friend void fun(); // 友元函数的函数模板(此处才定义为友元函数,且为友元函数模板的声明)
int x = 66;
};
template <typename T1>
void fun() // 友元函数的定义
{
B<int> tmp1;
std :: cout << tmp1.x << std :: endl;
B<char> tmp2;
std :: cout << tmp2.x << std :: endl;
}
int main()
{
fun<float>();
}
常用操作
#include <iostream>
template <typename T>
class B
{
friend void fun(B input) // fun是一个普通的函数,接收B<T>类型的对象,且依赖于B的存在而存在
{
std :: cout << input.x << std :: endl;
}
int x = 3;
};
int main()
{
B<int> val;
fun(val);
}
#include <iostream>
template <typename T>
class B
{
friend auto operator+(B input1, B input2)
{
B res;
res.x = input1.x + input2.x;
return res;
}
int x = 3;
public:
void MyCout()
{
std :: cout << this -> x << std :: endl;
}
};
int main()
{
B<int> val1;
B<int> val2;
B<int> b = val1 + val2;
b.MyCout();
}
C++11 支持声明模板参数为友元
#include <iostream>
template <typename T>
class B
{
friend T;
};
int main()
{
}
类模板的实例化
与函数实例化很像
可以实例化整个类,或者类中的某个成员函数
类模板的(完全)特化 / 部分特化(偏特化)
特化版本与基础版本可以拥有完全不同的实现
(完全)特化
#include <iostream>
template <typename T>
struct B
{
void fun()
{
std :: cout << "origin" << std :: endl;
}
};
template <>
struct B<int>
{
void fun2()
{
std :: cout << "B<int>" << std :: endl;
}
};
int main()
{
B<float> y;
y.fun();
B<int> x;
x.fun2();
}
部分特化(偏特化)
#include <iostream>
template <typename T, typename T2>
struct B
{
void fun()
{
std :: cout << "origin" << std :: endl;
}
};
template <typename T2>
struct B<int, T2>
{
void fun2()
{
std :: cout << "B<int>" << std :: endl;
}
};
int main()
{
B<float, int> y;
y.fun();
B<int, double> x;
x.fun2();
}
#include <iostream>
template <typename T>
struct B
{
void fun()
{
std :: cout << "origin" << std :: endl;
}
};
template <typename T>
struct B<T*> // 也是B<T>的部分特化
{
void fun2()
{
std :: cout << "B<int>" << std :: endl;
}
};
int main()
{
B<int> y;
y.fun();
B<int*> x;
x.fun2();
}
类模板的实参推导(从 C++17 开始)
基于构造函数的实参推导
#include <iostream>
template <typename T>
struct B
{
B(T input)
{}
void fun()
{
std :: cout << "origin" << std :: endl;
}
};
int main()
{
B x(3);
}
用户自定义的推导指引
#include <iostream>
#include <utility>
int main()
{
std :: pair x{3, 3.14};
}
注意:引入实参推导并不意味着降低了类型限制!
C++ 17 之前的解决方案:引入辅助模板函数
#include <iostream>
#include <utility>
template <typename T1, typename T2>
std :: pair<T1, T2> make_pair(T1 val1, T2 val2) // C++11不支持类模板的推导,但是支持函数模板的推导
{
return std :: pair<T1, T2>(val1, val2);
}
int main()
{
auto x = make_pair(3, 3.14);
}
Concepts
模板的问题
没有对模板参数引入相应的限制
参数是否可以正常工作,通常需要阅读代码进行理解
编译报错友好性较差 (vector<int&>)
Concepts( C++20 )
编译期谓词,基于给定的输入,返回 true 或 false
-fconcepts
编译器加上 -fconcepts 参数
#include <iostream>
#include <type_traits>
template <typename T>
concept IsAvail = std :: is_same_v<T, int> || std :: is_same_v<T, float>;
int main()
{
std :: cout << IsAvail<int> << std :: endl; // IsAvail<int>是在编译期求值的
std :: cout << IsAvail<char> << std :: endl;
}
与 constraints ( require 从句)一起使用限制模板参数
通常置于表示模板形参的尖括号后面进行限制
#include <iostream>
#include <type_traits>
template <typename T>
concept IsAvail = std :: is_same_v<T, int> || std :: is_same_v<T, float>;
template <typename T>
requires IsAvail<T> // 引入约束
void fun(T input) // 函数模板
{
std :: cout << "fun() is called. " << std :: endl;
}
int main()
{
fun(3);
}
Concept 的定义与使用
包含一个模板参数的 Concept
- 使用 requires 从句
- 直接替换 typename
#include <iostream>
#include <type_traits>
template <typename T>
concept IsIntOrFloat = std :: is_same_v<T, int> || std :: is_same_v<T, float>;
template <IsIntOrFloat T>
void fun(T input)
{
std :: cout << "IsIntOrFloat<T>: " << IsIntOrFloat<T> << std :: endl;
std :: cout << "fun() is called. " << std :: endl;
}
int main()
{
fun(3);
}
包含多个模板参数的 Concept
#include <iostream>
#include <type_traits>
template <typename T, typename T2>
concept IsSameType = std :: is_same_v<T, T2>;
template <typename T, typename T2>
requires IsSameType<T, T2>
void fun(T input, T2 input2)
{
std :: cout << "IsSameType<T, T2>: " << IsSameType<T, T2> << std :: endl;
std :: cout << "fun(T input, T2 input2) is called. " << std :: endl;
}
int main()
{
fun(3.2, 4.5);
}
- 用做类型 constraint 时,少传递一个参数,推导出的类型将作为“首个”参数
#include <iostream>
#include <type_traits>
template <typename T, typename T2>
concept NotSameType = !std :: is_same_v<T, T2>;
template <typename T>
requires NotSameType<T, int>
void fun(T input)
{
std :: cout << "NotSameType<T, int>: " << NotSameType<T, int> << std :: endl;
std :: cout << "fun(T input) is called. " << std :: endl;
}
int main()
{
fun(3.2);
}
requires 表达式
简单表达式
表明可以接收的操作
#include <iostream>
template <typename T>
concept Addable =
requires (T a, T b)
{
a + b;
};
template <Addable T>
auto fun(T x, T y)
{
return x + y;
}
int main()
{
fun(3, 5);
}
类型表达式
表明是一个有效的类型
#include <iostream>
template <typename T>
concept Avail =
requires
{
typename T :: inter; // 必须包含类型inter
};
template <Avail T>
auto fun(T x)
{
}
struct Str
{
using inter = int;
};
int main()
{
fun(Str{});
}
复合表达式
表明操作的有效性,以及操作返回类型的特性
#include <iostream>
#include <concepts>
template <typename T>
concept Avail =
requires (T x)
{
{x + 1} -> std :: same_as<int>;
};
template <Avail T>
auto fun(T x)
{
}
int main()
{
fun(3);
}
嵌套表达式
包含其它的限定表达式
注意区分 requires 从句与 requires 表达式
requires 从句会影响重载解析与特化版本的选取
只有 requires 从句有效而且返回为 true 时相应的模板才会被考虑(SFINAE)
#include <iostream>
#include <concepts>
#include <type_traits>
template <typename T>
requires std :: is_same_v<T, int>
void fun(T)
{
std :: cout << "is_same_v<T, int>" << std :: endl;
}
template <typename T>
requires std :: is_same_v<T, double>
void fun(T)
{
std :: cout << "is_same_v<T, double>" << std :: endl;
}
int main()
{
fun(3.5);
}
requires 从句所引入的限定具有偏序特性,系统会选择限制最严格的版本
特化小技巧
在声明中引入 “ A||B” 进行限制,之后分别针对 A 与 B 引入特化
#include <iostream>
#include <type_traits>
template <typename T>
requires std :: is_same_v<T, int> || std :: is_same_v<T, float>
class B;
template <>
class B<int> // 完全特化
{
public:
void fun()
{
std :: cout << "class B<int>" << std :: endl;
}
};
template <>
class B<float>
{
public:
void fun()
{
std :: cout << "class B<float>" << std :: endl;
}
};
int main()
{
B<float> x;
x.fun();
}
模板相关内容
数值模板参数与模板模板参数
模板可以接收(编译期常量)数值作为模板参数
template
class Str;
template <typename T, T value> class Str;
(C++ 17) template <auto value> class Str;
(C++ 20) 接收字面值类对象与浮点数作为模板参数
- 目前 clang 12 不支持接收浮点数作为模板参数
#include <iostream>
template <float x>
void fun()
{
}
int main()
{
fun<3.14f>();
constexpr float x = 10000 - 10000 + 3.14;
fun<x>();
constexpr float x2 = 10000 + 3.14 - 10000;
fun<x2>();
}
接收模板作为模板参数
template <template
class C> class Str;
#include <iostream>
template <typename T>
class C{};
template <template <typename T> class T2>
void fun()
{
T2<int> tmp;
}
int main()
{
fun<C>();
}
(C++17) template <template
typename C> class Str;
#include <iostream>
template <typename T>
class C{};
template <template <typename> typename T2>
void fun()
{
T2<int> tmp;
}
int main()
{
fun<C>();
}
C++17 开始,模板的模板实参考虑缺省模板实参( clang 12 支持程度有限)
- Str<vector> 是否支持?
#include <iostream>
template <typename T, typename T2 = int>
class C2{};
template <template <typename> typename T2>
void fun()
{
T2<int> tmp;
}
template<>
void fun<C2>()
{
}
int main()
{
fun<C2>();
}
别名模板与变长模板
可以使用 using 引入别名模板
为模板本身引入别名
#include <iostream>
template <typename T>
using MyType = T;
int main()
{
MyType<int> x; // 构建一个对象x,类型是MyType,通过别名后,x类型是int
}
#include <iostream>
template <typename T>
using AddPointer = T*;
int main()
{
AddPointer<int> x; // x的类型是int*的指针
}
template<class T>
struct Alloc {};
template<class T>
using Vec = vector<T, Alloc<T>>; // 类型标识为 vector<T, Alloc<T>>
Vec<int> v; // Vec<int> 等同于 vector<int, Alloc<int>>
为类模板的成员引入别名
#include <iostream>
template <typename T>
struct B
{
using TP = T*;
};
template <typename T>
using MyPointer = typename B<T> :: TP;
int main()
{
MyPointer<int> x; // x的类型是int*的指针
}
别名模板不支持特化,但可以基于类模板的特化引入别名,以实现类似特化的功能
- 注意与实参推导的关系
#include <iostream>
#include <type_traits>
/*
定义一个别名模板,传入除int类型外的所有类型时,都返回指针
但是传入int类型时,返回int&
*/
template <typename T>
struct B
{
using type = T*;
};
template <>
struct B<int>
{
using type = int&;
};
template <typename T>
using PointerOrIntRef = B<T> :: type;
int main()
{
PointerOrIntRef<float> x;
std :: cout << std :: is_same_v<decltype(x), float*> << std :: endl;
int value = 10;
PointerOrIntRef<int> z = value;
std :: cout << std :: is_same_v<decltype(z), int&> << std :: endl;
}
变长模板( Variadic Template )
变长模板参数与参数包
变长模板参数可以是数值、类型或模板
类型 ... 数值
#include <iostream>
template <int... value> // 形参包,传入的int类型的数值
void fun()
{
// 使用折叠表达式(C++17引入的)打印所有值
((std::cout << value << " "), ...);
std::cout << std::endl;
}
int main()
{
fun<1, 2, 3>();
}
typename ... 类型
#include <iostream>
template <typename... type> // 形参包,传入的类型名称
void fun()
{
}
int main()
{
fun<int, double, char>();
}
template ... 类模板
#include <iostream>
template <template<typename> class... ClassTemplate> // 形参包,需要传入类模板
void fun()
{
// 展开模板参数包并实例化,调用每个模板类的成员函数
(ClassTemplate<int>()(666), ...);
}
template <typename T>
struct fun1
{
void operator()(T x1)
{
std::cout << "fun1: " << x1 << std::endl;
}
};
template <typename T>
struct fun2
{
void operator()(T x2)
{
std::cout << "fun2: " << x2 << std::endl;
}
};
int main()
{
fun<fun1, fun2>();
}
pack-name ... pack-param-name
#include <iostream>
template <typename... T>
void fun(T... args)
{
// 使用折叠表达式(C++17引入的)打印所有值
((std::cout << args << " " << '\n'), ...);
}
int main()
{
fun<int, double, char>(3, 5.3, 'c');
}
sizeof... 操作
包展开
#include <iostream>
template <typename... T>
void fun(T... args)
{
std::cout << sizeof...(T) << std :: endl; // 3:表示传入了是3个参数
}
int main()
{
fun<int, double, char>(3, 5.3, 'c');
}
#include <iostream>
template <typename... T>
void fun(T... args)
{
std::cout << sizeof...(args) << std :: endl;
}
int main()
{
fun<int, double, char>(3, 5.3, 'c');
}
注意变长模板参数的位置
在主类模板中,模板形参包必须是模板形参列表的最后一个形参。
在函数模板中,模板参数包可以在列表中更早出现,只要其后的所有形参都可以从函数实参推导或拥有默认实参即可。
#include <iostream>
template <typename... T> class C; // 类模板的声明
template <typename T1, typename T2> class B;
template <typename... T, typename T2> // 引入B的特化
class B<C<T...>, T2> // T...是包展开
{
};
int main()
{
}
包展开与折叠表达式(重要技术)
(C++11) 通过包展开技术操作变长模板参数
A pattern followed by an ellipsis, in which the name of at least one pack appears at least once, is expanded into zero or more instantiations of the pattern, where the name of the pack is replaced by each of the elements from the pack, in order.
模式后随省略号且其中至少有一个形参包的名字的模式会被展开 成零个或更多个逗号分隔的模式实例,其中形参包的名字按顺序被替换成包中的各个元素。
template<class... Us>
void f(Us... pargs) {}
template<class... Ts>
void g(Ts... args)
{
f(&args...); // “&args...” 是包展开
// “&args” 是它的模式
}
g(1, 0.2, "a"); // Ts... args 会展开成 int E1, double E2, const char* E3
// &args... 会展开成 &E1, &E2, &E3
// Us... 会展开成 int* E1, double* E2, const char** E3
包展开语句可以很复杂,需要明确是哪一部分展开,在哪里展开
如果两个形参包在同一模式中出现,那么它们同时展开而且长度必须相同
template<typename...>
struct Tuple {};
template<typename T1, typename T2>
struct Pair {};
template<class... Args1>
struct zip
{
template<class... Args2>
struct with
{
typedef Tuple<Pair<Args1, Args2>...> type;
// Pair<Args1, Args2>... 是包展开
// Pair<Args1, Args2> 是模式
};
};
typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
// Pair<Args1, Args2>... 会展开成
// Pair<short, unsigned short>, Pair<int, unsigned int>
// T1 是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>
// typedef zip<short>::with<unsigned short, unsigned>::type T2;
// 错误:包展开中的形参包包含不同长度
#include <iostream>
void fun()
{
std :: cout << "end\n";
}
template <typename U, typename... T>
void fun(U u, T... args)
{
std :: cout << u << std :: endl;
fun(args...);
}
int main()
{
fun(1, 2, "hello", 'c');
}
(C++17) 折叠表达式
基于逗号的折叠表达式应用
#include <iostream>
template <typename... T>
void fun(T... args)
{
((std :: cout << args << std :: endl), ...);
}
int main()
{
fun(1, 2, "hello", 'c');
}
折叠表达式用于表达式求值,无法处理输入(输出)是类型与模板的情形
#include <iostream>
template <typename... Args>
int sum(Args&&... args)
{
return (args + ... + (2 * 5));
}
int main()
{
std :: cout << sum(1, 2, 3, 4, 5) << std :: endl;
}
完美转发与 lambda 表达式模板
(C++11) 完美转发
std::forward 函数
通常与万能引用结合使用
同时处理传入参数是左值或右值的情形
#include <iostream>
#include <utility>
void g(int&)
{
std :: cout << "l-reference\n";
}
void g(int&&)
{
std :: cout << "r-reference\n";
}
// 类似Python的“装饰器”
template <typename T>
void wrapper(T&& input) // T&&不是右值引用,是万能引用
{
std :: cout << "Hello\n";
g(std::forward<T>(input)); // 转发为左值或右值,依赖于 T
}
int main()
{
int x = 3;
wrapper(x);
wrapper(3);
}
#include <iostream>
#include <utility>
void g(int&, int&)
{
std :: cout << "l-reference\n";
}
void g(int&&, int&&)
{
std :: cout << "r-reference\n";
}
void g(int&, int&&)
{
std :: cout << "l&r-reference\n";
}
void g(int&&, int&)
{
std :: cout << "l&r-reference\n";
}
template <typename... T> // 多个参数
void wrapper(T&&... args) // 包含万能引用的包
{
std :: cout << "Hello\n";
g(std::forward<T>(args)...);
}
int main()
{
int x = 3;
wrapper(x, x);
wrapper(3, 3);
wrapper(x, 3);
wrapper(3, x);
}
(C++20) lambda表达式模板
消除歧义与变量模板
使用 typename 与 template 消除歧义
#include <iostream>
template <typename T>
void fun()
{
T :: internal* p; // 第一种理解
// internal是一个类型,是一个T所关联的类型
// *表明,现在要声明一个T :: internal类型的指针
// 根据这个类型的指针声明了一个变量p
// 第二种理解(编译器的选择)
// internal是T所关联的一个静态的数据成员
// *可能不是指针的意思,是“乘”的意思
// 是T :: internal要与一个变量p相乘
}
int main()
{
}
#include <iostream>
struct Str
{
inline const static int internal = 3;
};
int p = 5;
template <typename T>
void fun()
{
std :: cout << T :: internal* p << std :: endl;
}
int main()
{
fun<Str>();
}
使用 typename 表示一个依赖名称是类型而非静态数据成员
#include <iostream>
struct Str
{
using internal = int;
};
int p = 5;
template <typename T>
void fun()
{
typename T :: internal* p; // 相当于 Str :: internal* p;
}
int main()
{
fun<Str>();
}
使用 template 表示一个依赖名称是模板
#include <iostream>
struct Str
{
template <typename T>
static void internal() // 静态成员
{
}
};
template <typename T>
void fun()
{
T :: template internal<int>(); // 表明internal是一个模板
}
int main()
{
fun<Str>();
}
template 与成员函数模板调用
#include <iostream>
struct Str
{
template <typename T>
void internal() // 普通成员
{
}
};
template <typename T>
void fun()
{
T obj;
obj.template internal<int>(); // 声明internal是一个模板
}
int main()
{
fun<Str>();
}
变量模板(C++14)
为变量定义一个模板
template <typename T> T pi = (T)3.1415926;
#include <iostream>
template <typename T>
T pi = (T)3.1415926;
int main()
{
std :: cout << pi<float> << std :: endl;
std :: cout << pi<int> << std :: endl;
}
#include <iostream>
template <typename T, unsigned v>
unsigned MySize = (sizeof(T) == v);
int main()
{
std :: cout << MySize<float, 3> << std :: endl;
std :: cout << MySize<int, 4> << std :: endl;
}
其它形式的变量模板
十九、元编程
元编程的引入
从泛型编程到元编程
泛型编程 —— 使用一套代码处理不同类型
对于一些特殊的类型需要引入额外的处理逻辑 —— 引入操纵程序的程序
元编程与编译期计算
第一个元程序示例( Erwin Unruh )
http://www.erwin-unruh.de/primorig.html
没法放到现在的编译器中运行
在编译错误中产生质数
使用编译期运算辅助运行期计算
运行期和编译期各自都是“图灵完备”的,要合理利用运行期和编译期各自的优点
不是简单地将整个运算一分为二
详细分析哪些内容可以放到编译期,哪些需要放到运行期
- 如果某种信息需要在运行期确定,那么通常无法利用编译期计算
元程序的形式
模板, constexpr 函数,其它编译期可使用的函数(如 sizeof )
#include <iostream>
template <int x>
struct M
{
constexpr static int val = x + 1;
};
int main()
{
return M<3> :: val;
}
通常以函数为单位,也被称为函数式编程(主流)
#include <iostream>
constexpr int fun(int x)
{
return x + 1;
}
constexpr int val = fun(3);
int main()
{
return val;
}
元数据
基本元数据:数值、类型、模板
数组
元程序的性质
输入输出均为 “ 常量 ”
函数需要能够在编译期被调用,且无副作用(对相同的输入产生相同的输出)
type_traits元编程库(重点)
C++11 引入到标准中,用于元编程的基本组件
顺序、分支、循环代码的编写方式(编译期代码)
顺序代码的编写方式
类型转换示例:为输入类型去掉引用并添加 const
#include <iostream>
#include <type_traits>
template <typename T>
struct Fun
{
using RemRef = typename std :: remove_reference<T> :: type;
using type = typename std :: add_const<RemRef> :: type;
};
int main()
{
Fun<int&> :: type x = 3; // const int x = 3;
}
代码无需至于函数中
- 通常置于模板中,以头文件的形式提供
更复杂的示例:
- 以数值、类型、模板作为输入
- 以数值、类型、模板作为输出
#include <iostream>
#include <type_traits>
template <typename T, unsigned S>
struct Fun
{
using RemRef = typename std :: remove_reference<T> :: type;
constexpr static bool value = (sizeof(T) == S);
};
int main()
{
constexpr bool res = Fun<int&, 4> :: value;
std :: cout << res << std :: endl;
}
引入限定符防止误用
通过别名模板简化调用方式
#include <iostream>
#include <type_traits>
template <typename T, unsigned S>
struct Fun
{
private:
using RemRef = typename std :: remove_reference<T> :: type;
public:
constexpr static bool value = (sizeof(T) == S);
};
template <typename T, int S>
constexpr auto Fun2 = Fun<T, S> :: value;
int main()
{
constexpr bool res = Fun2<int&, 4>;
std :: cout << res << std :: endl;
}
分支代码的编写方式
基于 if constexpr 的分支
便于理解,只能处理数值,同时要小心引入运行期计算
#include <iostream>
constexpr int fun(int x)
{
if (x > 3)
{
return x * 2;
}
else
{
return x - 100;
}
}
constexpr int x = fun(100);
template <int x>
int fun()
{
if constexpr (x > 3)
{
return x * 2;
}
else
{
return x - 100;
}
}
int main()
{
int y = fun<100>();
}
基于(偏)特化引入分支
常见分支引入方式,但书写麻烦
#include <iostream>
template <int x>
struct Imp
{
constexpr static int value = x * 2;
};
template <>
struct Imp<100>
{
constexpr static int value = 100 - 3;
};
constexpr int x = Imp<100> :: value;
int main()
{
std :: cout << x << std :: endl;
}
#include <iostream>
#include <type_traits>
template <int x>
struct Imp
{
constexpr static int value = x * 2;
using type = int;
};
template <>
struct Imp<100>
{
constexpr static int value = 100 - 3;
using type = double;
};
constexpr int x = Imp<100> :: value;
using my_type = Imp<99> :: type;
int main()
{
std :: cout << x << std :: endl;
my_type y;
std :: cout << std :: is_same_v<decltype(y), int> << std :: endl;
}
#include <iostream>
#include <type_traits>
template <int x>
struct Imp;
template <int x>
requires (x < 100)
struct Imp<x>
{
constexpr static int value = x * 2;
using type = int;
};
template <int x>
requires (x >= 100)
struct Imp<x>
{
constexpr static int value = 100 - 3;
using type = double;
};
constexpr auto x = Imp<46> :: value;
int main()
{
std :: cout << x << std :: endl;
}
基于 std::conditional 引入分支
语法简单(类似三元表达式),但应用场景受限
#include <iostream>
#include <type_traits>
#include <typeinfo>
int main()
{
using Type1 = std::conditional<true, int, double>::type;
using Type2 = std::conditional<false, int, double>::type;
using Type3 = std::conditional<sizeof(int) >= sizeof(double), int, double>::type;
std::cout << typeid(Type1).name() << '\n';
std::cout << typeid(Type2).name() << '\n';
std::cout << typeid(Type3).name() << '\n';
}
基于 SFINAE 引入分支
基于 std::enable_if 引入分支
语法不易懂,但功能强大
函数版本
#include <iostream>
#include <type_traits>
template <int x, std :: enable_if_t<(x < 100)>* = nullptr> // void*
constexpr auto fun()
{
return x * 2;
}
template <int x, std :: enable_if_t<(x >= 100)>* = nullptr>
constexpr auto fun()
{
return x - 3;
}
constexpr auto x = fun<46>();
int main()
{
std :: cout << x << std :: endl;
}
类版本
#include <iostream>
#include <type_traits>
template <int x, typename = void>
struct Imp;
template <int x>
struct Imp<x, std :: enable_if_t<(x < 100)>> // 偏特化
{
constexpr static int value = x * 2;
using type = int;
};
template <int x>
struct Imp<x, std :: enable_if_t<(x >= 100)>>
{
constexpr static int value = x - 3;
using type = int;
};
constexpr auto x = Imp<100> :: value;
int main()
{
std :: cout << x << std :: endl;
}
注意用做缺省模板实参不能引入分支!
通过 “ 无效语句 ” 触发分支
基于 std::void_t 引入分支(C++17)
#include <iomanip>
#include <iostream>
#include <map>
#include <type_traits>
#include <vector>
// Variable template that checks if a type has begin() and end() member functions
template<typename, typename = void>
constexpr bool is_iterable = false;
template<typename T>
constexpr bool is_iterable<
T,
std::void_t<decltype(std::declval<T>().begin()),
decltype(std::declval<T>().end())
>
> = true;
// An iterator trait those value_type is the value_type of the iterated container,
// supports even back_insert_iterator (where value_type is void)
template<typename T, typename = void>
struct iterator_trait : std::iterator_traits<T> {};
template<typename T>
struct iterator_trait<T, std::void_t<typename T::container_type>>
: std::iterator_traits<typename T::container_type::iterator> {};
class A {};
#define SHOW(...) std::cout << std::setw(34) << #__VA_ARGS__ \
<< " == " << __VA_ARGS__ << '\n'
int main()
{
std::cout << std::boolalpha << std::left;
SHOW(is_iterable<std::vector<double>>);
SHOW(is_iterable<std::map<int, double>>);
SHOW(is_iterable<double>);
SHOW(is_iterable<A>);
using container_t = std::vector<int>;
container_t v;
static_assert(std::is_same_v<
container_t::value_type,
iterator_trait<decltype(std::begin(v))>::value_type
>);
static_assert(std::is_same_v<
container_t::value_type,
iterator_trait<decltype(std::back_inserter(v))>::value_type
>);
}
基于 concept 引入分支(C++20)
可用于替换 enable_if
基于三元运算符引入分支
std::conditional 的数值版本
#include <iostream>
template <int x>
constexpr auto fun = (x < 100) ? x * 2 : x - 3;
constexpr auto x = fun<56>;
int main()
{
std :: cout << x << std :: endl;
}
循环代码的编写方式
计算二进制中包含 1 的个数
#include <iostream>
template <int x>
constexpr auto fun = (x % 2) + fun<x / 2>;
template <>
constexpr auto fun<0> = 0; // 特化版本(作为结束)
constexpr auto x = fun<99>;
int main()
{
std :: cout << x << std :: endl; // 4
}
fun<(1100011b)>
1 + fun<(110001b)>
1 + 1 + fun<(11000b)>
1 + 1 + 0 + fun<(1100b)>
1 + 1 + 0 + 0 + fun<(110b)>
1 + 1 + 0 + 0 + 0 + fun<(11b)>
1 + 1 + 0 + 0 + 0 + 1 + fun<(1b)>
1 + 1 + 0 + 0 + 0 + 1 + 1 + fun<(0b)>
1 + 1 + 0 + 0 + 0 + 1 + 1 + 0 = 4
使用递归实现循环
任何一种分支代码的编写方式都对应相应的循环代码编写方式
使用循环处理数组
获取容器中 id=0,2,4,6... 的元素
#include <iostream>
#include <type_traits>
#include <vector>
#include <string>
// 定义一个容器模板,可以存放任意数量的类型参数
template <typename...> class Container;
// 定义一个输入类型,包含了多种类型
using Input = Container<int, char, double, bool, void, std::vector<int>, std::string>;
// 定义递归模板 Imp,负责处理类型序列
template <typename Res, typename Rem>
struct Imp;
// 模板偏特化:当容器中包含多个类型时,递归处理
// Processed 是已经处理过的类型集合,TRemain 是剩下的类型
template <typename... Processed, typename T1, typename T2, typename... TRemain>
struct Imp<Container<Processed...>, Container<T1, T2, TRemain...>>
{
// 将当前类型 T1 加入到已处理的类型集合中
using type1 = Container<Processed..., T1>;
// 继续递归处理剩余的类型 TRemain...
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 模板偏特化:当容器中只剩下一个类型时,直接将该类型加入到已处理的类型集合中(终止条件1)
template <typename... Processed, typename T1>
struct Imp<Container<Processed...>, Container<T1>>
{
using type = Container<Processed..., T1>;
};
// 模板偏特化:当容器中没有剩余类型时,递归结束,返回已处理的类型集合(终止条件2)
template <typename... Processed>
struct Imp<Container<Processed...>, Container<>>
{
using type = Container<Processed...>;
};
// 使用 Imp 模板处理 Input 类型,得到最终的输出类型 Output
using Output = Imp<Container<>, Input>::type;
int main()
{
// 检查 Output 是否与 Container<int, double, void, std::string> 类型相同
std::cout << std::is_same_v<Output, Container<int, double, void, std::string>> << std::endl;
}
获取容器中最后三个元素
#include <iostream>
#include <type_traits>
#include <vector>
#include <string>
// 定义一个容器模板,可以存放任意数量的类型参数
template <typename...> class Container;
// 定义一个输入类型,包含了多种类型
using Input = Container<int, char, double, bool, void, std::vector<int>, std::string>;
// 定义递归模板 Imp,负责处理类型序列
template <typename Res, typename Rem>
struct Imp;
// 模板偏特化:当容器中有三个类型时,递归处理并保留类型 U2 和 U3,同时将 T 添加到类型集合中
template <typename U1, typename U2, typename U3, typename T, typename... TRemain>
struct Imp<Container<U1, U2, U3>, Container<T, TRemain...>>
{
// 在已处理的类型中,将 U2 和 U3 保留,并将 T 加入
using type1 = Container<U2, U3, T>;
// 继续递归处理剩余的类型 TRemain...
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 模板偏特化:当容器中只有两个类型时,递归处理并将 T 添加到已处理的类型集合中
template <typename U1, typename U2, typename T, typename... TRemain>
struct Imp<Container<U1, U2>, Container<T, TRemain...>>
{
// 将 T 加入到已处理的类型集合中
using type1 = Container<U1, U2, T>;
// 继续递归处理剩余的类型 TRemain...
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 模板偏特化:当容器中只有一个类型时,递归处理并将 T 添加到已处理的类型集合中
template <typename U1, typename T, typename... TRemain>
struct Imp<Container<U1>, Container<T, TRemain...>>
{
// 将 T 加入到已处理的类型集合中
using type1 = Container<U1, T>;
// 继续递归处理剩余的类型 TRemain...
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 模板偏特化:当容器为空时,将第一个类型 T 添加到已处理的类型集合中
template <typename T, typename... TRemain>
struct Imp<Container<>, Container<T, TRemain...>>
{
// 将 T 加入到已处理的类型集合中
using type1 = Container<T>;
// 继续递归处理剩余的类型 TRemain...
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 模板偏特化:当容器已经没有剩余类型时,递归结束,返回已处理的类型集合
template <typename... TProcessed>
struct Imp<Container<TProcessed...>, Container<>>
{
using type = Container<TProcessed...>;
};
// 使用 Imp 模板处理 Input 类型,得到最终的输出类型 Output
using Output = Imp<Container<>, Input>::type;
int main()
{
// 检查 Output 是否与 Container<void, std::vector<int>, std::string> 类型相同
std::cout << std::is_same_v<Output, Container<void, std::vector<int>, std::string>> << std::endl;
}
简化版
#include <iostream>
#include <type_traits>
#include <vector>
#include <string>
// 定义一个模板类 Container,接受多个类型参数
template <typename...> class Container;
// 定义一个输入类型 Input,包含多个类型,包括内置类型和 STL 容器类型
using Input = Container<int, char, double, bool, void, std::vector<int>, std::string>;
// 定义递归模板 Imp,用于处理类型序列
template <typename Res, typename Rem>
struct Imp
{
// 基础情况:返回已处理的类型集合 Res
using type = Res;
};
// 当容器包含三个类型时,处理并将 T 添加到已处理的类型集合中
template <typename U1, typename U2, typename U3, typename T, typename... TRemain>
struct Imp<Container<U1, U2, U3>, Container<T, TRemain...>>
{
// 在已处理的类型中,保留 U2 和 U3,并将 T 加入
using type1 = Container<U2, U3, T>;
// 继续递归处理剩余的类型 TRemain
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 当容器只包含两个类型时,将 T 添加到已处理的类型集合中
template <typename U1, typename U2, typename T, typename... TRemain>
struct Imp<Container<U1, U2>, Container<T, TRemain...>>
{
// 将 T 添加到已处理的类型集合中
using type1 = Container<U1, U2, T>;
// 继续递归处理剩余的类型 TRemain
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 当容器只包含一个类型时,将 T 添加到已处理的类型集合中
template <typename U1, typename T, typename... TRemain>
struct Imp<Container<U1>, Container<T, TRemain...>>
{
// 将 T 添加到已处理的类型集合中
using type1 = Container<U1, T>;
// 继续递归处理剩余的类型 TRemain
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 当容器为空时,将第一个类型 T 添加到已处理的类型集合中
template <typename T, typename... TRemain>
struct Imp<Container<>, Container<T, TRemain...>>
{
// 将 T 添加到已处理的类型集合中
using type1 = Container<T>;
// 继续递归处理剩余的类型 TRemain
using type = typename Imp<type1, Container<TRemain...>>::type;
};
// 当容器已经没有剩余类型时,递归结束,返回已处理的类型集合
template <typename... TProcessed>
struct Imp<Container<TProcessed...>, Container<>>
{
using type = Container<TProcessed...>;
};
// 使用 Imp 模板处理 Input 类型,得到最终的输出类型 Output
using Output = Imp<Container<>, Input>::type;
int main()
{
// 使用 std::is_same_v 检查 Output 是否与 Container<void, std::vector<int>, std::string> 相同
std::cout << std::is_same_v<Output, Container<void, std::vector<int>, std::string>> << std::endl;
}
减少实例化的技巧
为什么要减少实例化
提升编译速度,减少编译所需内存
#include <iostream>
template <size_t A>
struct Wrap_ // 类模板1
{
template <size_t ID, typename TDummy = void> // TDummy:假参数,没有实际作用
struct imp // 类模板2
{
constexpr static size_t value = ID + imp<ID - 1> :: value;
};
template <typename TDummy>
struct imp<0, TDummy> // 部分特化
{
constexpr static size_t value = 0;
};
template <size_t ID>
constexpr static size_t value = imp<A + ID> :: value;
};
int main()
{
std :: cout << Wrap_<3> :: value<2> << std :: endl; // 3+2=5 -> 5+4+3+2+1+0=15
// Wrap_<3> :: imp<5>, Wrap_<3> :: imp<4>, Wrap_<3> :: imp<3>, Wrap_<3> :: imp<2>, Wrap_<3> :: imp<1>, Wrap_<3> :: imp<0>
std :: cout << Wrap_<10> :: value<2> << std :: endl; // 10+2=12 -> 12+11+10+9+8+7+6+5+4+3+2+1+0=78
// Wrap_<10> :: imp<12>, Wrap_<10> :: imp<11>, ... , Wrap_<10> :: imp<1>, Wrap_<10> :: imp<0>
}
相关技巧
提取重复逻辑以减少实例个数
#include <iostream>
template <size_t ID, typename TDummy = void>
struct imp
{
constexpr static size_t value = ID + imp<ID - 1> :: value;
};
template <typename TDummy>
struct imp<0, TDummy>
{
constexpr static size_t value = 0;
};
template <size_t A>
struct Wrap_
{
template <size_t ID>
constexpr static size_t value = imp<A + ID> :: value;
};
int main()
{
std :: cout << Wrap_<3> :: value<2> << std :: endl; // 3+2=5 -> 5+4+3+2+1+0=15
// imp<5>, imp<4>, imp<3>, imp<2>, imp<1>, imp<0>
std :: cout << Wrap_<10> :: value<2> << std :: endl; // 10+2=12 -> 12+11+10+9+8+7+6+5+4+3+2+1+0=78
// 新实例化:imp<12>, imp<11>, ... , imp<7>, imp<6>
// 重复使用已经存在的对象:imp<5>, imp<4>, imp<3>, imp<2>, imp<1>, imp<0>
}
conditional 使用时避免实例化
原版本
#include <iostream>
#include <type_traits>
using Res = std :: conditional_t<false,
std :: remove_reference_t<int&>,
std :: remove_reference_t<double&>>;
int main()
{
std :: cout << std :: is_same_v<Res, double> << std :: endl;
}
优化版
#include <iostream>
#include <type_traits>
template <typename T>
struct Identity
{
using type = T;
};
using Res = std :: conditional_t<true,
Identity<double&>, // 规避了true时,直接写double&因为没有type成员的报错
std :: remove_reference<double&>> :: type; // 使用remove_reference,而不使用remove_reference_t是为了避免实例化
int main()
{
std :: cout << std :: is_same_v<Res, double&> << std :: endl;
}
引入短路逻辑
|| 等短路逻辑是在运行期执行的
使用 std::conjunction(逻辑与and) / std::disjunction(逻辑或or) 引入短路逻辑
原版本
#include <iostream>
#include <type_traits>
template <typename T>
constexpr bool intORdouble = std :: is_same_v<T, int> || std :: is_same_v<T, double>;
int main()
{
std :: cout << intORdouble<char> << std :: endl;
std :: cout << intORdouble<int> << std :: endl;
std :: cout << intORdouble<double> << std :: endl;
}
优化后
#include <iostream>
#include <type_traits>
template <typename T>
constexpr bool intORdouble = std :: disjunction<std :: is_same<T, int>, std :: is_same<T, double>> :: value;
int main()
{
std :: cout << intORdouble<char> << std :: endl;
std :: cout << intORdouble<int> << std :: endl;
std :: cout << intORdouble<double> << std :: endl;
}
其它技巧介绍
减少分摊复杂度的数组元素访问操作
#include <iostream>
template <typename... T> class Container;
template <typename T, unsigned id>
struct At;
template <typename TCur, typename... T, unsigned id>
struct At<Container<TCur, T...>, id>
{
using type = typename At<Container<T...>, id - 1> :: type;
};
template <typename TCur, typename... T>
struct At<Container<TCur, T...>, 0>
{
using type = TCur;
};
using res = At<Container<double, int, char, bool>, 2> :: type;
// 系统会产生大量实例
// At<Container<double, int, char, bool>, 2>
// At<Container<int, char, bool>, 1>
// At<Container<char, bool>, 0>
using res2 = At<Container<double, int, char, bool>, 3> :: type;
// 系统又会产生大量实例
// At<Container<double, int, char, bool>, 3>
// At<Container<int, char, bool>, 2>
// At<Container<char, bool>, 1>
// At<Container<bool>, 0>
int main()
{
std :: cout << std :: is_same_v<res, char> << std :: endl;
std :: cout << std :: is_same_v<res, double> << std :: endl;
std :: cout << std :: is_same_v<res2, bool> << std :: endl;
std :: cout << std :: is_same_v<res2, int> << std :: endl;
}
二十、其它的工具与技术
异常处理
用于处理程序在调用过程中的非正常行为
传统的处理方法:传返回值表示函数调用是否正常结束
- 不得不修改整个返回链上的返回值
C++ 中的处理方法:通过关键字 try/catch/throw 引入异常处理机制
#include <iostream>
void f1()
{
throw 1;
}
void f2()
{
f1();
}
void f3()
{
f2();
}
int main()
{
try
{
f3();
}
catch(int)
{
std :: cout << "exception is occurred.\n";
}
}
异常触发时的系统行为 —— 栈展开
抛出异常后续的代码不会被执行
局部对象会按照构造相反的顺序自动销毁
系统尝试匹配相应的 catch 代码段
- 如果匹配则执行其中的逻辑,之后执行 catch 后续的代码
- 如果不匹配则继续进行栈展开,直到 “ 跳出 ” main 函数,触发 terminate 结束运行
异常对象
系统会使用抛出的异常拷贝初始化一个临时对象,称为异常对象
异常对象会在栈展开过程中被保留,并最终传递给匹配的 catch 语句
try / catch 语句块
一个 try 语句块后面可以跟一到多个 catch 语句块
每个 catch 语句块用于匹配一种类型的异常对象
catch 语句块的匹配按照从上到下进行
使用 catch(...) 匹配任意异常
在 catch 中调用 throw 继续抛出相同的异常
#include <iostream>
struct Base
{
virtual ~Base() = default;
virtual std::string what() const = 0; // 提供异常信息
};
struct Derive : Base
{
std::string message;
Derive(const std::string& msg = "") // 拷贝构造函数
: message(msg) {}
std::string what() const override
{
return "Derive exception: " + message;
}
};
void f1()
{
throw 10.5;
}
void f2()
{
try
{
f1();
}
catch(double e)
{
std :: cout << "exception is catched in f2-double." << " error: " << e << std :: endl;
throw Derive{"404"}; // 系统会使用抛出的异常“拷贝”初始化一个临时对象
}
catch(int e)
{
std :: cout << "exception is catched in f2-int." << " error: " << e << std :: endl;
throw; // 在捕获内部,抛出已经捕获到的异常
}
}
void f3()
{
try
{
f2();
}
catch(Derive& e) // 异常捕获中,系统只会考虑(数组-指针)(函数-指针)(基类-派生类)
{
std :: cout << "exception is catched in f3-Derive&. " << e.what() << std :: endl;
throw 66;
}
}
int main()
{
try
{
f3();
}
catch(...) // 可以捕获任意类型的异常,通常放在多个catch语句块的最后
{
std :: cout << "exception is catched in main." << std :: endl;
}
}
在一个异常未处理完成时抛出新的异常会导致程序崩溃
不要在析构函数或 operator delete 函数重载版本中抛出异常
通常来说, catch 所接收的异常类型为引用类型
- 避免触发“拷贝构造函数”,防止出现新的异常
异常与构造、析构函数
使用 function-try-block保护初始化逻辑
#include <iostream>
struct Str
{
Str()
{
throw 111;
}
};
class Cla
{
public:
Cla()
try : m_men()
{
}
catch(int e)
{
std :: cout << "exception is catched in Cla." << " error: " << e << std :: endl;
// 系统会隐式地加一个throw;
}
private:
Str m_men;
};
int main()
{
try
{
Cla obj;
}
catch(int e)
{
std :: cout << "exception is catched in main." << " error: " << e << std :: endl;
}
}
在构造函数中抛出异常
已经构造的成员会被销毁,但类本身的析构函数不会被调用
#include <iostream>
struct Str2
{
~Str2()
{
std :: cout << "~Str2()" << std :: endl;
}
};
struct Str
{
Str()
{
throw 111;
}
};
class Cla
{
public:
Cla()
try : m_men()
{
}
catch(int e)
{
std :: cout << "exception is catched in Cla." << " error: " << e << std :: endl;
// 系统会隐式地加一个throw;
}
~Cla()
{
std :: cout << "~Cla()" << std :: endl;
}
private:
Str m_men;
Str2 m_2; // 顺序不同,最后的结果也不同
};
int main()
{
try
{
Cla obj;
}
catch(...)
{
std :: cout << "exception is catched in main." << std :: endl;
}
}
描述函数是否会抛出异常
- 如果函数不会抛出异常,则应表明以为系统提供更多的优化空间
throw() / throw(int, char)【C++98】
- C++20中已经被移出标准了
noexcept / noexcept(false)【C++11】
- 相比C++98,把一些编译期行为移到了运行期
noexcept
限定符
接收 false / true 表示是否会抛出异常
#include <iostream>
void fun() noexcept // 相当于 noexcept(true),指fun函数不会抛出异常
{
}
int main()
{
}
操作符
接收一个表达式,根据表达式是否可能抛出异常返回 false/true
#include <iostream>
void fun()
{
}
int main()
{
std :: cout << noexcept(fun()) << std :: endl; // 返回 fun() 函数是否会抛出异常,
// 如果 fun() 函数后面没有显式标注 noexcept / noexcept(false)
// 默认为 noexcept(false),即返回 false,指fun函数会抛出异常
}
#include <iostream>
void fun2()
{
}
void fun() noexcept(noexcept(fun2()))
{
// ...
fun2();
// ...
}
int main()
{
}
程序终止
在声明了 noexcept 的函数中抛出异常会导致 terminate 被调用,程序终止
注意
不作为函数重载依据
但函数指针、虚拟函数重写时要保持形式兼容
标准异常
#include <iostream>
#include <stdexcept>
void fun()
{
throw std :: runtime_error("Invalid input");
}
int main()
{
try
{
fun();
}
catch(std :: runtime_error& e)
{
std :: cout << e.what() << std :: endl;
}
}
正确对待异常处理
不要滥用:异常的执行成本非常高
不要不用:对于真正的异常场景,异常处理是相对高效、简洁的处理方式
编写异常安全的代码
枚举与联合
枚举(enum)
-
一种取值受限的特殊类型
分为无作用域枚举与有作用域枚举( C++11 起)两种
#include <iostream>
enum class Color // class表示这是一个有作用域的枚举
{
Red,
Green,
Yellow
};
int main()
{
Color x = Color :: Red;
}
枚举项缺省使用 0 初始化,依次递增,可以使用常量表达式来修改缺省值
#include <iostream>
enum Color
{
Red,
Green,
Yellow
};
int main()
{
Color x = Red;
std :: cout << x << std :: endl;
std :: cout << Red << std :: endl;
Color y = Green;
std :: cout << y << std :: endl;
std :: cout << Green << std :: endl;
Color z = Yellow;
std :: cout << z << std :: endl;
std :: cout << Yellow << std :: endl;
}
#include <iostream>
enum Color
{
Red = 100,
Green,
Yellow
};
int main()
{
Color x = Red;
std :: cout << x << std :: endl;
std :: cout << Red << std :: endl;
Color y = Green;
std :: cout << y << std :: endl;
std :: cout << Green << std :: endl;
Color z = Yellow;
std :: cout << z << std :: endl;
std :: cout << Yellow << std :: endl;
}
可以为枚举指定底层类型,表明了枚举项的尺寸
#include <iostream>
enum Color : char
{
Red = 'a',
Green,
Yellow
};
int main()
{
Color x = Red;
std :: cout << x << std :: endl;
std :: cout << Red << std :: endl;
Color y = Green;
std :: cout << y << std :: endl;
std :: cout << Green << std :: endl;
Color z = Yellow;
std :: cout << z << std :: endl;
std :: cout << Yellow << std :: endl;
}
无作用域枚举项可隐式转换为整数值;也可用 static_cast 在枚举项与整数值间转换
#include <iostream>
enum class Color
{
Red,
Green,
Yellow
};
int main()
{
Color x = Color :: Red;
std :: cout << static_cast<int>(x) << std :: endl;
Color y = Color :: Green;
std :: cout << static_cast<int>(y) << std :: endl;
Color z = Color :: Yellow;
std :: cout << static_cast<int>(z) << std :: endl;
}
注意区分枚举的定义与声明
#include <iostream>
enum Color : char; // 枚举的声明
enum Color : char
{
Red = 'a',
Green,
Yellow
};
int main()
{
}
#include <iostream>
enum class Color : char; // 枚举的声明
enum class Color : char
{
Red = 'a',
Green,
Yellow
};
int main()
{
}
联合( union )
- 将多个类型合并到一起以节省空间
#include <iostream>
union Uni
{
int x;
int y;
};
int main()
{
std :: cout << sizeof(Uni) << std :: endl;
}
通常与枚举一起使用
#include <iostream>
enum Type
{
Char,
Int
};
union Uni
{
char x;
int y;
};
struct S
{
Type t;
Uni obj;
};
int main()
{
S s;
s.t = Char;
s.obj.x = 'c';
}
#include <iostream>
struct S
{
enum Type
{
Char,
Int
};
union Uni
{
char x;
int y;
};
Type t;
Uni obj;
};
int main()
{
S s;
s.t = S :: Char;
s.obj.x = 'c';
}
匿名联合
#include <iostream>
struct S
{
enum Type
{
Char,
Int
};
union
{
char x;
int y;
};
Type t;
};
int main()
{
S s;
s.t = S :: Char;
s.x = 'c';
}
在联合中包含非内建类型( C++11 起)
嵌套类与局部类
嵌套类
在类中定义的类
#include <iostream>
class Out
{
public:
class In
{
};
};
int main()
{
Out :: In obj;
}
嵌套类具有自己的域,与外围类的域形成嵌套关系
- 嵌套类中的名称查找失败时会在其外围类中继续查找
#include <iostream>
class Out
{
public:
class In
{
public:
inline static int val = 3;
};
};
int main()
{
std :: cout << Out :: In :: val << std :: endl;
}
#include <iostream>
class Out
{
using MyInt = int;
public:
class In
{
public:
inline static MyInt val = 3;
};
};
int main()
{
std :: cout << Out :: In :: val << std :: endl;
}
嵌套类与外围类单独拥有各自的成员
局部类
以在函数内部定义的类
#include <iostream>
void fun()
{
struct Helper
{
int x;
int y;
};
Helper h;
}
int main()
{
fun();
}
可以访问外围函数中定义的类型声明、静态对象与枚举
局部类可以定义成员函数,但成员函数的定义必须位于类内部
局部类不能定义静态数据成员
嵌套名字空间与匿名名字空间
嵌套名字空间
名字空间可以嵌套,嵌套名字空间形成嵌套域
#include <iostream>
namespace Out
{
namespace In
{
namespace In2
{
}
}
}
int main()
{
}
注意同样的名字空间定义可以出现在程序多处,以向同一个名字空间中增加声明或定义
#include <iostream>
namespace Out
{
int x;
}
namespace Out
{
int y;
}
int main()
{
Out :: x;
Out :: y;
}
C++17 开始可以简化嵌套名字空间的定义
// 原始版本
#include <iostream>
namespace Out
{
int y;
}
namespace Out
{
namespace In
{
int z;
}
}
int main()
{
Out :: y;
Out :: In :: z;
}
// 简化版本
#include <iostream>
namespace Out
{
int y;
}
namespace Out :: In
{
int z;
}
int main()
{
Out :: y;
Out :: In :: z;
}
匿名名字空间
仅具有内部链接
用于构造仅翻译单元可见的对象
#include <iostream>
namespace
{
int y;
}
int main()
{
}
可用 static 代替
#include <iostream>
static int y;
int main()
{
}
可作为嵌套名字空间
#include <iostream>
namespace MyNs
{
namespace
{
int x;
}
}
int main()
{
MyNs :: x;
}
位域与 volatile 关键字
位域
#include <iostream>
struct A
{
bool b1 : 1; // 表示只占1位
bool b2 : 1;
};
int main()
{
std :: cout << sizeof(A) << std :: endl;
}
显示表明对象尺寸(所占位数)
在结构体 / 类中使用
多个位域对象可能会被打包存取
声明了位域的对象无法取地址,因此不能使用指针或非常量引用进行绑定
但是可以使用常量引用进行绑定
#include <iostream>
struct A
{
bool b1 : 1;
bool b2 : 1;
};
int main()
{
A obj;
const bool& ref = obj.b1;
}
尺寸通常会小于对象类型所对应的尺寸,否则取值受类型限制
volatile关键字
表明一个对象的可能会被当前程序以外的逻辑修改
相应对象的读写可能会加重程序负担
注意慎重使用 —— 一些情况下可以用 atomic 代替