基础
程序
- ISO C++标准中定义了两种实体:
- 核心语言特性.i.e.内置类型(char,int).循环(for,while)
- 标准库组件.i.e.容器(vector,map).输入输出操作(<<,getline)
- C++是一种静态语言。这意味着,每一个实体(对象、值、名称和表达式等)被使用的那一刻,编译器必须知道其准确的类型。
1 | int main(){} //最小的cpp程序 |
1 | //import std; 是cpp20的特性 |
Hello, World!
函数
1 | double sqrt(double d); //函数声明的时候可以包含参数的名称 |
类型、变量和运算
- unsigned一般用于位运算
- 数字可以是浮点数或者整数:
- 浮点字面量含有小数点或者指数符号
- 0b二进制整数 //0b0101010
0x十六进制整数 //0x3f3f3f3f
0八进制整数 //0334
- 使用单引号(’)作位数字分隔符
0x3.243F’6A88’85A3’08D3
- 初始化:
- = 的形式是C语言传统的形式,会发生隐式类型转换
- {} 更通用,而且可以省略’=’,不会发生隐式类型转换
- 常量被声明时必须被初始化,用户自定义类型可以在定义时被隐式初始化
- 如果变量的类型可以从初始化符号中推导出来,就无需显式指定类型
- 使用 auto 常用在泛型编程中,以避免书写冗长的类型名称及重复代码
1 | double d1 = 2.3; |
1 | auto y = 3.14; |
作用域和生命周期
- 声明语句把一个名字引入作用域:
- 局部作用域:在函数/匿名函数中定义的名字叫做局部名字(包括函数参数的名字),以语句块({})结尾.
- 类作用域:不在函数、匿名函数或 enum class 中但在类的内部.
- 命名空间作用域:不是局部名字和类名字且在命名空间的内部.
- 生命周期:
- 对象必须先被构造(初始化)才能被使用,并且在退出作用域时被销毁。
- 命名空间对象在程序结束时被销毁。
- 对象成员的销毁时间点取决于所属对象的销毁时间点。
- 一个用new创建的对象则可以持续生存,直到用delete将其销毁。
常量
- const
用来说明接口,可以用指针或者引用的方式传入参数而不用担心被改变
编译器负责强制执行 const 承诺
const声明的值可以在运行时计算 - constexpr
声明常量,在只读内存中,提高性能
constexpr 的值必须由编译器运算
const double s1 = sum(v); //可行:允许在运行时运算
constexpr double s2 = sum(v);
1 | double sum1(const double&); |
- 若要使函数在常量表达式(上文代码)中使用,这个函数必须被定义为 constexpr 或 consteval
- constexpr 函数
- 在输入非常量参数时,输出的不是常量表达式
- 在输入常量参数时,输出的才是常量表达式
- consteval 函数可以让输入变量只能为常量表达式,即可以省略输入的常量表达式语法,还可以使输出为常量表达式。
1 | constexpr double squre(double x){return x*x;} |
1 | consteval double square2(double x){return x*x;} |
总结
这里讨论的有三个部分,函数的返回值类型(函数的声明),函数的输入参数类型,数据类型,均有两种写法,注意分辨
函数的输入参数类型
- (数据类型 变量名):可以是变量,也可以是常量表达式
- (const 数据类型& 变量名):函数不会修改他它的参数
数据类型
- const:说明接口,意为“只读”
- cosntexpr:声明常量,赋值给他的只能是常量表达式
函数的返回值类型
- constexpr:不指定输入类型,输入和输出的不变性一致
- consteval:输入和输出均为常量表达式
注意const只具有接口性质,不是实际上的常量
函数的声明为常量(constexpr,consteval)可以实现C++的纯函数,即数学意义上的函数,不会有副作用,不能修改函数的输入参数
指针、数组和引用
1 | T a[n]; //数组 |
注意*在定义时是定义指针,在指针前使用是解引用
空指针
空指针用nullptr表达,整数用0或NULL表达,在检验时是兼容的
不能引用空指针
映射到硬件
- 赋值:
- p和q同时指向y的地址
1 | int y = 3; |
- 地址r1,r2不同,但是使不同的地址具有相同的值
1 | int x = 2; |
这里直接用引用的方法操作地址,没有使用指针
- 初始化:没有未经初始化的引用
使用 = 来初始化一个引用,注意这不是赋值
int& r = x; //r指代x
补充在函数中引用的两种方法
void sort(vector<double>&v)
若只想减少复制参数的开销,则:
double sum(const vector<double>&)
这样就不能改变函数传入的参数
用户自定义类型
- 内置类型:用基本类型、const操作符和声明操作符构造出来的类型
- 用户自定义类型:利用C++的抽象机制从其他类型构造出来的类型,包括类和枚举类型
结构 struct
1 | struct Vector { |
访问结构体的成员有两种方式
- 用 . 表示通过名字或者引用访问
- 用 -> 表示通过指针访问
1 | void f(Vector v, Vector& rv, Vector* pv){ |
类 class
把类型的接口(所有代码都可使用的部分)与其实现(可访问外部不可访问的数据)分离开来的语言机制被称为类
1 | class Vector { |
- 构造函数在初始化类对象时一定会被调用
- 错误处理暂时没有涉及
- 思考通过new获取的double数组如何归还?
struct和class没有本质区别,struct是默认public的
枚举
enum class
- 枚举类型用于表示少量整数数值的集合
- 通过符号名称替代整数
- 后面的class表示这个枚举类型是强类型,并且具备独立作用域
不能混用不同类的枚举值
无法隐式地混用枚举类型与整数类型的值
- 默认情况下,一个enum class定义仅定义赋值操作符、初始化函数及比较操作符,也可以自定义其他操作符
1 | enum class Color { red, blue, green }; |
Color x = red
Color y = traffic_light::red
int i = Color::red
Color c = 2
Color z = Color::red
auto z = Color::red
Color x = Color{5}
Color y {6}
enum
- 普通enum中的枚举值进入与enum自身同级的作用域
- 可以被隐式转换为整数数值
用枚举表示一组命名的常量
为枚举定义操作来简化并保证安全
优先使用enum class以避免很多麻烦
1 | enum Color {red, green, blue}; |
联合 union
- 实际占用空间就是它最大成员所占的空间,可以用来节约空间
- 同一时刻,union中只能保存一个成员的值
1 | enum class Type { ptr, num }; |
避免使用“裸”union;将其与类型字段一起封装在一个类中
使用标准库类型variant可以消除大多数需要直接使用union
variant更简单,安全
1 | struct Entry { |
模块化
一个C++程序包含许多独立开发的部分,例如函数、用户自定义类型、类层次、 模板
为了让这些独立开发的部分在其他地方使用(类似于函数的声明),我们使用模块化的方法
区分声明(用作接口)和定义(用作实现)
分离编译
头文件
- 在C++中,接口文件后缀名为.h,里面应该包括函数的声明
- 实现接口函数的文件后缀名为.cpp,为了帮助编译器保持一致性,同样应该包含提供接口的.h文件
- 要使用这个头文件的函数,就需要在头文件中包含"xxx.h"
使用#include及头文件实现模块化是一种传统方法,它具有明显的缺点。
- 编译时间:不同的文件每一次include不同的头文件编译器都要处理一次
- 依赖顺序:先include的头文件的定义与宏可能会影响后一个的代码的含义
- 不协调:可能会出现类似下面的问题
- 我们有意无意地在两个源文件中定义了同一个实体的时候
- 不同源文件引用头文件的顺序不一致的时候
- 传染性:一个文件(1)包含的头文件(2)会包含其他这个文件(1)不需要的头文件,以此类推会导致代码膨胀且冗余
避免在头文件中定义非内联函数
不要在头文件中使用 using 指令
模块
- 写法1:将class export并表示成声明的形式,就和.h写法差不多
1 | export module Vector; |
-
写法2:函数export的就是可见的,没有export的就是不可见的(类似java)
-
module Vector接口形式,后缀名为.cppm
-
import Vector使用Vector
-
export module Vector定义Vector
模块在维护性与编译时间方面的改进非常显著
命名空间
用 namespace 命名空间名 {}
用 :: 访问
- 表达某些声明是属于一个整体的
- 表明它们的名字不会与其他命名空间中的名字冲突
真正的main()(不是自己定义的命名空间里的)
用 using 声明将命名空间中的名字放进作用域,可以降低可读性
1 | void my_code(vector<int>& x, vector<int>& y) { |
使用标准库命名空间中所有名称的访问权
using namespace std;
用以上方式使用命名空间指令不会影响使用模块的用户,影响仅限于模块内部
函数参数与返回值
函数之间传递信息有三种路径:
参数
全局变量
类对象中的共享状态
在函数的信息传递中,我们应考量:
- 对象是被复制的还是共享的?
- 这个共享对象是否可被修改?
- 这个对象是否被移动,从而留下了一个空对象?
参数传递
- 默认情况复制(传值)/直接指向引用(传引用)/直接只读const&
- 拥有默认值的函数参数可以达到和重载函数一样的效果
1 | void print(int value, int base = 10); |
1 | void print(int value, int base){ |
返回值
- 返回值的默认行为是复制
- 返回引用的情况只应当出现在返回的内容不属于函数局部的时候(注意数组)
- 局部变量在函数返回的时候消失,因此我们不应当返回它的引用或者指针
- 对于较大的对象构造移动方法来极大地减少复制开销
- 编译器会优化复制行为,叫做省略复制优化
使用指针返回大对象的代码性能不会比使用引用返回的函数好,不要使用这样的代码
1 | Matrix* add(const Matrix& x, const Matrix& y) { |
返回类型推导
使用 auto 关键字
auto mul(int i, double d) { return i*d; }
返回类型后置
有的时候,我们需要先看到参数,然后决定返回值的类型。
这包括但不限于返回类型推导这种情形,这个问题与命名空间、匿名函数、概念都有一定的联系
使用 auto 关键字,表示返回值会在后面提到
auto mul(int i, double d) -> double { return i*d; }
使用这种记法能够更有效地实现代码对齐
auto next_elem -> Elem*;
auto exit(int) -> void;
auto sqrt(double) -> double;
不要过度使用返回类型推断
结构化绑定
把类对象成员赋予局部变量名称的机制
使用 {s, i} 构造
使用 [n, v] 读取(“解包”)
1 | struct Entry { |
对完全没有私有数据的类使用结构化绑定时,绑定行为是明显的:
提供的名称数量必须与数据成员的数量相等,每一个绑定名称对应一个成员变量
1 | map<string, int> m; |
结构化绑定也可以用于处理需要通过成员函数访问对象数据的类(?)
complex<double> z = {1,2};
auto [re,im] = z+2;
不要过度使用结构化绑定