C++(1·内置指针、语句、右值引用)
内置指针
- 指针本质:通过间接指向和操作内存,提供了更灵活的操作能力
- 1级指针,2级指针:type** 指针的指针
- 指向:
- 对非常量指针可以指向:非常量左值,将亡值(通过右值引用间接)
- 对常量指针可以指向:非常量左值,常量左值,将亡值(通过右值引用间接)
- 常量指针可以指向:非常量左值
- 指向常量的常量指针可以指向:非常量左值,常量左值,将亡值(通过右值引用间接)
- 指针作用:(间接操作的优势,防止拷贝,增加灵活性)
- 作为函数参数返回值、数组和指针的转换时,指针类型元素的数组(虚表),……使用已分配内存时,可以高效传递对象,避免拷贝的内存消耗
- 可以灵活修改指向
- 非指针只能静态绑定,基类指针由于间接指向内存,可以灵活修改指向,可以动态绑定,实现动态多态
- 使用函数指针实现回调函数,可以接受不同的函数地址,有更高的灵活性
- 动态生命周期管理,指向不同的内存,灵活性较高
1 2 3 4 5
class A { A a;//❌ 错误: 类的定义中不能直接包含同类对象,递归下去,编译器无法计算出 A 类的大小 A* a;//✅ 正确 A& a;//✅ 正确 };
展开
- 在实现链式数据结构时通常需要next(链表,树,图),如果next为类类型成员,内存分配无限循环,不被允许,如果next为类类型指针成员,内存分配有限,被允许
复杂指针阅读
右左法则:从标识符开始,先向右看,然后向左看,每当遇到圆括号时,就调转阅读方向,当括号内的内容解析完毕,就跳出这个括号,重复这个过程直到表达式解析完毕
1 2 3 4 5 6 7 8 9
int i;//从i开始,右侧没有看左侧,则i是int类型 int *a[3];//从a开始,右侧说明a是包含3个元素的数组,左侧说明每个元素是int*类型 int (*a)[3];//从a开始,右侧是括号,左侧说明a是指针,左侧遇到括号,右侧说明a指向包含3个元素的数组,左侧表明每个元素是int类型 int **p;//p是指针,指向的对象类型也是指针(二级指针),此指针指向的类型是int int *foo();//从foo开始,右侧说明foo是函数,左侧说明返回类型是int* int (*foo)();//从foo开始,右侧是括号,左侧说明foo是指针,左侧遇到括号,右侧说明foo指向函数(函数指针),左侧说明返回类型是int int (*(*vtable)[])();//vtable是指针,指向数组,每个元素是指针类型,指向函数(函数指针),左侧说明此元素返回类型是int int *(*p(int))[3];//p是包含int类型参数的函数,返回类型为指针,此指针指向包含3个元素的数组,每个元素类型是指针,指向int类型对象 int (*(*p)(int))(int);//p是指针,指向包含int类型参数的函数,函数返回类型为指针,指针指向包含int类型参数的函数,函数返回类型为int
展开
- int p;
- 从p开始向右看,没有,向左看,p是int类型的变量
- int *p;
- 从p开始向右看,没有,向左看,p是指针,再向左看,p指向int类型元素
- int *p[3];
- 从p开始向右看,p是有3个元素的数组,向左看,元素类型为int*
- int (*p)[3];
- 从p开始向右看,遇到圆括号时,向左看,p是指针,跳出括号,先向右看,p指向有3个元素的数组,向左看,元素类型为int
- int **p;
- 从p开始向右看,没有,向左看,p是指针,向左看,p指向int*
- int p(int);
- 从p开始向右看,p是参数为int类型的函数,向左看,函数返回类型为int
- int (*p)(int);
- 从p开始向右看,遇到圆括号时,向左看,p是指针,跳出括号,先向右看,p指向参数为int类型的函数,向左看,函数返回类型为int
- int (p(int))[3];
- 从p开始向右看,p是参数为int类型的函数,向左看,函数返回类型为指针,跳出括号,先向右看,函数返回的指针指向有3个元素的数组,向左看,元素类型为int*
- int ((p)(int))(int);
- 从p开始向右看,遇到圆括号时,向左看,p是指针,跳出括号,先向右看,p指向参数为int类型的函数,函数返回类型为指针,跳出括号,先向右看,函数返回的指针 指向参数为int类型的函数,函数返回值为int
函数指针
- 定义函数指针:return_type (*p) (parameter_list);
- 函数指针数组(虚表):return_type (*p[n]) (parameter_list)
- 函数指针数组的指针(虚指针):return_type ((p)[n]) (parameter_list)
- 初始化/赋值: = &func_name <==> = func_name
- 调用函数:(*p)(parameter_list) <==> p(parameter_list)
- typedef
- 使用 typedef 定义函数指针,代码看起来更简洁,也更不容易出错, 并且函数指针作为函数返回值,必须使用typedef声明的类型
- 声明函数类型: typedef return_type (func_name)(parameter_list);
- 声明函数指针类型: typedef return_type (*ptr_name)(parameter_list);
- 定义和初始化函数指针:
- func_name* p = &func / func(因为函数会隐式转换);
- ptr_name p = &func / func;
- 调用:p(parameter_list);
静态 vs. 动态
静态:
- 在构建时确定
- 空间小
- 不灵活
- 速度快
- 内存固定
- 自动管理
动态:
- 在运行时(执行指令)确定
- 动态绑定,动态内存分配,动态链接,动态生命周期:
- 动态内存分配作用:
- 静态生命周期对象一直占用内存,造成内存浪费,在不需要时手动释放内存,避免内存浪费
- 自动生命周期对象通常情况下栈区,栈区的大小容不下过于庞大的数据,所以在堆分配内存
- 动态内存分配作用:
- 空间大
- 更灵活
- 速度慢
- 内存可变
- 手动管理
语句
循环:重复执行某段代码
遍历:遍历强调访问集合中的每个元素
迭代:迭代是循环的一次执行过程
- 条件
- if:
- if(条件){语句块}else if(条件){语句块}……else{语句块},如果,否则如果,……,否则
- 可以嵌套
- 优势:条件灵活
- 缺陷:性能较差
- 使用场景:常用于复杂判断时
- switch:
- switch(条件){case关键字 整形常量表达式:语句块;break; ……}
- 条件:允许(整形,enum类型,常量表达式),不允许(浮点,字符串,类类型)
- break:防止接下来的case语句块执行
- 优势:性能较高
- 缺陷:条件有大量限制,只能做==比较
- 使用场景:常用于高效匹配时
- if:
- 迭代
- while:
- while(条件){语句块}
- 当条件为真执行
- 使用场景:循环次数已知/未知,事件等待
- 小技巧:
- n–,执行n次
- –n,执行n-1次
- for:
- for(初始化(只执行一次); 条件(每次循环前检查); 迭代(每次循环后执行)) {语句块}
- 范围for循环:用于迭代容器或序列
- 使用场景:循环次数已知,复杂迭代(比如多变量迭代)
- 小技巧:(执行次数 == 区间个数)
- =n <m / =m >n 则执行m-n次
- =n <=m / =m >=n 则执行m-n + 1次
1 2 3
do { //循环体 } while (条件);
展开
- do while:先执行循环体,再检查条件, 也就是循环体至少执行一次
- while:
- 跳转
- break: 退出本次循环,继续循环外的指令
continue: 跳过本次循环体中剩余的语句,继续执行下一次循环的循环体
1 2 3 4
goto 标签名; // ...这里不执行 标签名: // ```代码
展开
- goto:无条件跳转语句,直接跳转到程序中的指定标签位置
左值,右值,左值引用,右值引用
- 左值:
- 特点:可寻址的内存、表达式结束不会立即销毁、可以修改值,有符号名,可以作为赋值运算符的左右操作数,可以作为递增减运算符的操作数
- 常见形式:字符串字面值,变量,赋值、前置递增递减表达式返回值,……
- const左值:不可以修改值(包括字符串字面值,和被const修饰的变量)
- 右值:
- 特点:不可寻址的内存、表达式结束立即销毁、不可修改值,没有符号名,仅能作为赋值运算符的右操作数,不能作为递增减运算符的操作数
- 常见形式:在除了字符串的其他字面值,变量作为赋值运算符的右操作数,算术、逻辑、关系、位运算、后置递增递减表达式返回值,函数非引用返回值……
- 纯右值:普通右值
- 将亡值:通过std::move转换后的右值,表达式结束不会立即销毁,可以寻址
- const右值:纯右值本身就是常量,它不可被赋值,将亡值可以是非常量,move转换给非常量
- 左值引用:
- type & name = val;
- 定义时必须初始化,否则编译错误
- 可以赋值,但更改的是绑定对象的值,而非绑定另一个对象
- 引用类型和绑定对象类型要严格匹配
- 可以被左值引用绑定的?非常量左值
引用不是对象,不占用内存这句话需要从多种角度理解(实际中会站在使用角度)- 从使用角度/规范上,无法获取引用本身地址,只能获取绑定对象的地址,这句话看起来是正确的
- 但底层实现上,引用其实是int* const r = &a;常量指针,所以它是对象,占用内存
- 引用是左值吗?
- 在规范上,它不是对象,因此没有左值右值之分
- 在使用上,使用的是它绑定的对象,它是左值
- 函数返回值
- 函数非引用返回值是右值,可以理解为type temp = x;
- 函数引用返回值是左值,可以被左值引用绑定,可以理解为type& temp = x;
- 要注意,返回局部变量情况,在函数调用后x生命周期结束被销毁,temp是空悬引用
- 函数对常量的引用返回值是const左值,不能出现在赋值运算符的左侧,可以理解为const type& temp = x;
- 左值引用作用:
- 避免现有对象拷贝,节省内存开销
- 函数引用形参,直接操作传入的对象而非副本
- 函数引用形参,实现多返回值
- 函数引用返回值,实现链式调用
- 对const的左值引用
- 不能通过该引用修改所绑定对象的值
- 引用类型和绑定对象类型允许隐式类型转换
- 可以被对const的左值引用绑定的?常量左值,非常量左值,纯右值,将亡值
- 对const的左值引用绑定右值时,延长右值的生命周期,右值随着引用销毁后销毁
- 右值引用:
- type && name = val;
- 定义时必须初始化,否则编译错误
- 可以赋值,但更改的是绑定对象的值,而非绑定另一个对象
- 可以被右值引用绑定的?
- 右值引用本身不是对象
- 作用:
- 相比于对const的左值引用,右值引用可以避免拷贝临时资源的同时,允许对右值操作
- 移动语义:对于不再需要的对象直接将资源移动到另一个对象,而非拷贝,节省内存开销
使用场景:移动构造,移动赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
class A { private: char* data; int size; public: // 1. 默认构造函数 A() : data(nullptr), size(0) {} // 3. 拷贝构造函数 A(const A& other) {//不会发生自赋值,因为other是已有的对象,要构造新的对象,他们一定是不同的对象 size = other.size; if (other.data) { data = new char[size + 1]; strcpy(data, other.data); } else { data = nullptr; } } // 4. 拷贝赋值运算符 A& operator=(const A& other) { if (this == &other) { return *this; } delete[] data; size = other.size; if (other.data) { data = new char[size + 1]; strcpy(data, other.data); } else { data = nullptr; } return *this; } // 5. 移动构造函数 A(A&& other) noexcept {//同样这个构造也不会发生自赋值 data = other.data; size = other.size; other.data = nullptr; other.size = 0; } // 6. 移动赋值运算符 A& operator=(A&& other) noexcept { if (this == &other) { return *this; } delete[] data; data = other.data; size = other.size; other.data = nullptr; other.size = 0; return *this; } // 7. 析构函数 ~A() { delete[] data; } // 辅助函数 const char* getData() const { return data ? data : ""; } int getSize() const { return size; } };
展开
完美转发:将函数的参数以左值或右值的形式完美传递给另一个函数
1 2 3 4 5 6 7 8 9 10 11
void process(int& i) { std::cout << "处理左值: " << i << std::endl; } void process(int&& i) { std::cout << "处理右值: " << i << std::endl; } template<typename T> void logAndProcess(T&& param) { process(std::forward<T>(param)); }
展开
- 引用折叠
- 规则:(模板类型、实参是左值/左值引用/右值/右值引用、&符号折叠为几个)
- T& -> & == &
- T&& -> & == &
- T& -> && == &
- T&& -> && == &&
- 当实参是左值/左值引用时,T&&为T&,当实参是右值/右值引用时,T&&为T&&
- 触发条件:
- 模板实例化
- 自动类型推导,包括auto关键字和decltype
- 作用:
- 确保组合不会得到引用的引用(不合法)
- 规则:(模板类型、实参是左值/左值引用/右值/右值引用、&符号折叠为几个)
- 万能引用
- T&& 可以绑定到左值或右值,并保证值类别
- std::forward<T>(param):保持param的原始值类别(左值/右值)
- 作用:编写既能接受左值、又能接受右值参数的模板代码
- 引用折叠
- 对const的右值引用
- 不能通过该引用修改所绑定对象的值
- 可以被对const的右值引用绑定的?纯右值,将亡值
- 一般不会使用
std::ref() std cref()
- 引用包装器std::ref,通过reference_wrapper包装,代替原本会被识别的值类型,防止拷贝,但它不能使被包装的对象变成引用
- 比如bind / thread ,如果关联的函数带有引用的形参,我们应该向万能引用参数传递引用,需要用ref / cref 包装
本文由作者按照 CC BY 4.0 进行授权
