|
数组&指针&引用
强烈建议在本人博客查看!知乎不能兼容我的文档格式原文链接
食用说明:这是一个基于 C++ 11 标准的博文,文中提及的一致初始化器、auto 关键字等,在 C++ 11 以前是无法使用的。 §1 指针
看到指针,就必须想到它是什么东西的地址 1)指针:存储另一个变量地址的变量。
2)创建:以指向一个整数的指针为例 int * p
3)初始化:
int a = 0;
int *p; // 初始值随机
p = &a; // 保存 a 的地址,也就是说,「指向 a」
int *q = &a; // 可以直接初始化
p = q // 重绑定: p 保存 q 保存的地址,结果是 「p,q 都指向 a」
//注意,书写时候,*紧跟在变量前,不要紧跟在 int 后,否则如果写出以下代码
//int* p, q;
//q 是一个 int 变量,而 p 是一个 int 指针,还一眼看不出来
4)使用:设有某个整数变量的指针 p
- 取值:*p 取得 p 指向的变量上的值
- 改值:*p = 1 将修改 p 指向的变量的值
- 析构:
cpp delete (p) //销毁 p 指向的变量/对象 p = nullptr; //之后请立即清理野指针
5)空指针:nullptr
6)指针的指针: int **pp = &p 创建了一个指向指针 p 的指针,此时保存的值为 p 的所在地址(不是 p 所保存的地址)
7)函数指针:一般形式 return_type (* pointer_name) (parameter_type),具体举例如 int (* p) (int) 创建了一个指向某函数的指针,该函数接受一个 int 变量,返回一个 int 变量
- 赋值:
- auto p = &f; 或者 auto p = f 其中 f 是一个函数名,推荐使用 auto 直接获取对应的类型
- 调用:p(parameters) 或者 (*p)(parameters)
**lambda 表达式指针**:lambda 表达式是一个匿名函数,也可以被指针绑定 wzxhzdk:1 **回调**:正常的函数调用在当前函数体内发生,但是,也可以交给其他函数体调用,方法就是创建一个函数指针交给调用方。这种方式可以实现模块解耦。例如在 priority_queue 的创建中,允许用户自定义优先级函数,从而实现不同优先级的堆,而避免自己重新改写堆全部代码。
§2 数组
1)数组:一系列顺序存储的元素集合
2)创建:以 32 位整数数组为例
- 一维:
```cpp int arr[n]; // 静态数组,长度不可变更
vector arr; // 空数组,动态数组,可增加、删除元素
vector arr(n); // 预分配 n 个单元空间
auto arr = new int[n]; // 动态申请 ```
STL:代码中提及的 vector 是 C++ 标准库(Standard Library,STL)当中给出的一种实现(相当于别人帮你写好的东西,你需要引入相关的依赖)。为了使得上述代码能正常运行,还需要在代码开头引入头文件 #include <vector>,正常情况下是使用 std::vector 来进行声明,但是每次都写 std 很麻烦,就可以在一开头使用 using namespace std 来告知编译器,如果找不到变量,优先在 std 这一「命名空间」当中寻找。(至于命名空间,这是区分不同库代码当中同名定义的手段)
- 多维:
```cpp int mat[m][n];
vector> mat;
auto arr = new int[m][n]; // 动态申请 ```
数学家常说把一维的结论推广到多维情况的事情是很危险的事。所以不如只记住 2 到 3 维。创建的数组哪怕再多一维,都是代码风格有问题。
3)初始化:
vector<int> arr[n]; // 无初始化值
vector<int> arr(n); // 初始化为 0
vector<int> arr(n, a); // 初始化为 a
int arr[]{1, 2, 3}; // 初始化为 [1, 2, 3]
int arr[n] {1} ; // 剩余元素为 0
int arr[][3]{{1, 2, 3}, {4, 5, 6}};
// 初始化为 [[1, 2, 3], [0, 0, 0]],只有第一维大小可以省略
// 使用一致构造初始化构造函数
vector<int> arr {1, 2, 3};
vector<int> arr = {1, 2, 3};
vector<vector<int>> arr { { 1, 2}, {1, 3} };
// 一致构造初始化器还可以接受迭代器的写法
set<int> s; // 建立一个集合
vector<int> arr = {s.begin(), s.end()}; // 将集合中的元素以数组形式导出
// 直接写内存
memset(arr, val, size_type * num_units)
4)索引:
- 静态数组:使用下标方式 arr[idx] 访问
- 动态数组:
运算符重载:对于 vector<int> arr; 方式创建的数组,之所以可以通过 [idx] 的方式来进行索引,是因为其底层实现当中重载了运算符 [];
- 使用 iterator(迭代器)进行访问
```cpp vector::iterator ptr = arr.begin(); while( ptr ! =arr.end()){ ++ptr; // iterator 可以通过加减运算进行移动 cout << *ptr; // 通过 * 来进行值索引 }
// 等价于以下代码 for(auto a: arr){ cout << a; } ```
**越界访问**:c++ 当中为了保证效率,在索引数组内容时没有做长度检查,而是直接返回对应位置的值。举例而言,对于一个长度为 10 的数组,访问第 11 个元素,将忠实地返回内存当中,距离数组开头第 11 个单位长度的位置的值。这是一个**非常危险**的行为,如果内存当中存在隐私信息,有可能以这种方式窃取。 除此之外,使用 `vector::back()` 函数也必须进行检查 wzxhzdk:3
5)增删:动态数组可以添加或删除元素
vector<int> arr = 0;
if(!arr.empty()){
arr.pop_back(); // 移除最后一个元素
}
arr.push_back(); // 向末尾添加元素
**扩容**:vector 又一个内部变量 capicity,表示存储的最大长度。当存储的元素量超过这个值时,就会发生扩容。vector 重新申请一个 1.5 倍或者 2 倍 capacity 长度的空间,然后将旧的数据拷贝到新位置。 **STL pop**:STL 标准库当中的容器多数实现了 pop 方法。在使用这类方法前一定要检查是否是空容器!否则,编译器不会报错,但是一定会发生一个**运行时错误**。以 vector 为例,其源码实现是直接 `--__size__`,从而发生越界。 **遍历时删除**:在遍历一个容器时,删除其中的元素是一个危险操作,因为我们希望遍历时容器的各个状态是稳定不变的,比方说它的长度、存储的连续性。多数博文止步于此,但现实当中我们有时就是有过滤掉非法元素的需求。那么,可以尝试使用 lambda 表达式风格的写法: wzxhzdk:5 ****迭代器失效****:为了进一步强调遍历过程中改变容器有多危险,这里给出一个例子: wzxhzdk:6 迭代器失效是非常常见的现象,在遍历过程中 push、pop,都可能引发这一机制。
6)数组与指针:
- 数组本质上是一个指针,保存的是该数组保存的首地址
```cpp int a[10]; // 相当于 int * 类型的指针
int a[10][11]; // 相当于 int * 类型的指针,具体来说,是 int()[11] 类型, // 这表明其是一个指针,指向一个 int[11] 类型的数组 ```
- 可以通过将数组名视为首地址进行数组访问
```cpp int a[] {1, 2, 3}; cout << *(a + 1); // 2
int b[][3]{{1, 2, 3}, {4, 5, 6}}; cout << ((b + 1) + 1); // 5 ```
7)参数传递:
- 指针方式:void f(int a*, int *b, int (*c)[10], int ** d);。不是值传递,因此对数组元素的修改会影响原数组
- 数组方式:void f(int a[], int b[10], int c[][10], int d[10][10]);。不是值传递,因此对数组元素的修改会影响原数组
只能省略第一维度的长度
- vector 方式:使用 vector 方式创建的数组只能使用 vector 传参,如果想要避免不必要的拷贝,注意使用引用传参 void f(vector<int> &arr);
§3 引用
看到引用,就必须想到它是什么东西的别名 1)引用:另一个变量的别名。因此,必须是先存在了某个变量,才存在这个变量的引用
2)创建:
int a;
int &p = a; // 创建同时必须绑定到变量
int &q = p; // 或者绑定到另一个引用
// 无法重绑定!也就是
// p = b; 的实际效果,是将 b 的值赋给 p 所绑定的变量 a。
**含引用类型成员变量的类**:正常情况下,引用创建同时必须绑定到变量,但有一个例外: wzxhzdk:8
3)使用:假设有某个整数变量的引用 p
- 取值:p 取得 p 保存的地址上的值
- 改值:p = 1 将修改 p 指向的变量的值
- 析构:delete (p) 销毁 p 指向的变量/对象
4)常量引用:const 修饰定义的引用,不能修改绑定对象的值
int b = 1;
const int & a = b;
a = 2; // 编译报错
5)右值引用:绑定到一个右值的引用
- 右值:通常而言在一个赋值语句当中,出现在等号左边的,是一个带地址的变量,是有名字的,称为左值。而出现在等号右边的,可能是一个字面量,临时对象,或者函数调用的结果等,其值是没有名字的,称为右值。左值可以取地址,而右值不可以取地址,因为右值可能保存在寄存器当中。
- 创建:int &&a = 1 创建了一个右值引用
- 转化:move(a) a 是一个左值引用,move 后变成右值引用。
- 特性:
- 左值引用绑定到左值,非常量左值引用不能绑定到右值。
- 只有类类型的非常量右值引用可以再次被赋值,此时调用了重载赋值运算符
- 右值引用作为函数返回值时,它是右值,直接声明定义时,是左值。
- 作用:
- 更灵活的引用传参
cpp int f(int & a){ return a; } int g(int && a){ return a; } int h(const int && a){ return a; } f(1); // 编译报错 g(1); // 返回 1 h(1); // 返回 1
- 避免拷贝:在进行值传参、值返回时,会调用拷贝构造函数。若定义形参为右值引用的拷贝构造函数(称为移动构造函数),且实参使用右值时,将避免拷贝(尤其是深拷贝)。使用时要注意,被拷贝变量应当销毁。
默认移动构造函数:若用户没有自定义拷贝构造函数,将默认生成一个
一文带你详细介绍c++中的std::move函数
- 完美转发:模版编程中使用,日常接触不到。
6)没有空引用!回顾第一点,必须是先存在了某个变量,才存在这个变量的引用。 |
|