%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # C++中的数组 C++中的数组分为两种: - **静态数组**: - **静态数组**本身是一种**对象**,其**类型**为 C++类型系统中的 **"==数组类型=="(array type)**。 - 因此,也允许定义 **==指向数组的指针==以及==数组的引用==**。 - **静态数组的大小(及内存分配)==在编译时==确定**,存储在**程序的栈上(局部静态数组)** 或者**在全局/静态存储区(全局静态数组)** - **动态数组**: - **动态数组==通过指针实现==,其大小==在运行时==确定,通过 `new` 关键字为其动态分配内存**。 - 动态数组 **通过"指针"** 来进行指代,**没有数组名**。 > [!caution] 数组不支持直接拷贝和赋值。 > > **(静态) 数组不支持拷贝和赋值**。**不能用一个数组直接对另一个数组初始化或赋值**,只能: > > - **通过循环逐个元素复制**; > - 或者**使用标准库提供的 `std::copy` 函数**。 > > 当数组作为**函数形参**时,发生的不是**数组的拷贝**,而是**数组名被隐式转换为 "==指向其首元素的指针==**",这个**指针被传递给函数**。 > 因此,实际上**传递的是==数组首元素的地址==**,而非整个数组的拷贝。 > [!tip] 理解 "**数组**" 声明的含义,最好是从 "**数组名字**" 开始**由内到外的顺序阅读**。[^1](P103) > > ```cpp > T *pd[3] // an array of 3 pointers. 类型为`T*[3]`. > T (*pd)[3] // a pointer to an array of 3 `T` elements. 类型为`T (*)[3]`. > T *(*pd)[3] // a pointer to an array of 3 `T*` pointers. 类型为`T *(*)[3]`. > ``` <br><br> # 静态数组 ## 静态数组的类型 **静态数组**的类型 (type) 为 C++类型系统中的 **"==数组类型=="(array type)**,属于复合类型。 > [!important] 静态数组的大小属于 "==数组类型==" 的一部分,因此必须在编译时已知,即数组大小必须是一个 "==常量表达式=="。 - 对于静态数组 `int arr[5];`,其类型为 `int [5]`; - 对于二维静态数组 `int arr[3][7];`,其类型为 `int [3][7]`。 ```cpp title:array_type.cpp int arr[5]; assert(typeid(arr) == typeid(int[5])); int arr_2d[3][7]; assert(typeid(arr_2d) == typeid(int[][7])); ``` 因此,可以定义 "**指向数组类型的指针**" 或 "**数组的引用**"。 <br> ## 静态数组的大小 C++标准中**不支持 "变长静态数组"**,最早于 C++98、C++03 标准中规定 **==静态数组的大小==** 必须是一个 **==编译时常量==**。 > [!caution] **==gcc/g++ 编译器==**支持在 **C++中使用变长静态数组**,但这**==仅属于编译器自身的扩展==**,而非 C++标准的一部分。 > [!NOTE] > > C99 标准中引入了**变长数组**(Variable Length Arrays, VLAs),但其并不是 C++标准的一部分,**标准 C++ 不支持变长静态数组**。 > > 为支持变长静态数组,编译器需要在函数栈上产生 "**==变长栈帧==**"(从而实现**在栈上存储变长静态数组**)。 > 变长栈帧依赖于使用 **`%rbp` 寄存器**作为**帧指针**。 > <br> ## 静态数组的数组名 > 动态数组在动态时创建,只能由指针所指代,没有数组名。 **静态数组的数组名**实际上是 **==数组首元素的地址==的一个常量表示**。 静态数组的数组名用在==**绝大多数==表达式中**会被**隐式转换**为 **"==指向数组首元素的指针=="**: - 其 "**值**" 为 **==数组中首项元素的地址==** ; - **指针类型**为 "**==指向静态数组内元素类型的指针==**" 。 该指针**存储了==数组首元素的地址==**,通过**指针算术操作**,**可以访问数组中的不同元素**。 **下标访问符 `array[idx]`** 即是基于 **"静态数组名" 所对应的==首项元素的地址**== 及其**指针类型**(**==指向数组元素的指针类型==**)来**计算地址偏移量**,从而进行访问。 ```cpp title:array_name_as_pointer.cpp int arr[5] = {1, 2, 3, 4, 5}; int* p = arr; for (int i = 0; i < 5; ++i) { cout << *(p + i) << ' '; // 等价于下标访问符`arr[i]` 以及`p[i]`. } // 使用方括号"数组表示法"等同于"间址运算符": p[i] 被解释为 *(p+i) int *pt = new int[10]; *pt = 5; pt[0] = 6; // 等同于*pt pt[9] = 44; // 等同于*(pt + 9) int coast[10]; *(coast + 4) = 12; // 等同于coast[4] ``` > [!caution] 数组名使用的例外情况 > > - 用于 `sizeof` 时,将返回整个数组的长度(**单位为字节,而非元素数量**); > - 用于 `decltype`、`typeid` 时,取得 "**数组类型**",例如 `int [3]`。(而数组名用于 `auto` 时,得到的仍是**指向数组元素的指针类型**) > - 用于取址运算符 `&` 时,取得 "**指向==数组类型==的指针**",虽然**指针所指的地址仍然是数组内首个元素的地址**。 > - 用作字符串字面量,初始化字符数组时。 #### 数组名的隐式类型转换 ```cpp title:array_name_use_example.cpp const int N = 3, M = 4; int A[N]; int B[N][M]; // 数组名是"指向数组元素类型的指针", 该指针指向数组内首个元素. int* Aptr = A; // 数组A中的元素类型是int. int (*Bptr) [M] = B; // 二维数组B本质是嵌套数组, B中元素是`int [M]`类型 // 对数组取地址得到 "指向数组类型的"指针 int (*Aptr2) [N] = &A; int (*Bptr2) [N][M] = &B; // 对比 int (*Bptr3) [M] = B; // `B`是二维数组, 数组名隐式转为"指向数组首元素(大小为M的一维数组)的指针" int* Bptr4 = B[0]; // `B[0]`是一维数组, 数组名隐式转为"指向数组首元素(int)的指针" int* Bptr5 = &B[0][0]; // `&B[0][0]`是指向数组元素的指针 cout << A << ' ' << A + 1 << endl; // `A`隐式转为"指向数组首元素的指针", 算术运算时步长为数组元素大小 cout << &A << ' ' << (&A) + 1 << endl; // `&A`为"指向数组类型的指针", 算术运算时步长为整个数组大小 static_assert(std::is_same<decltype(A), int [3]>::value); // 上面的一个可能的输出: // 0x2f21bffde4 0x2f21bffde8 // 0x2f21bffde4 0x2f21bffdf0~ ``` <br> ## 静态数组的别名 注意 **`typedef` 声明** 静态数组别名时,**数组大小放在最后**: ```cpp typedef int int_array[4]; // `int_array` 是 `int[4]`类型的别名 int_array a; // a是一个含有4个int元素的数组类型 // 等价声明: using int_array = int[4]; ``` <br> ## 静态数组的初始化 ```cpp int array[10]{}; // 值初始化, 全0. int array[10] = {}; // 值初始化, 全0. int array[10] = {1, 2, 3}; // 除指定值外, 剩余元素值初始化, 全0; int array[] = { 1, 2, 3, 4}; // 数组大小自动推断为4. ``` **静态数组大小可以省略**,由编译器根据 "**初始化列表**" 中元素数量进行推断。 ```cpp struct Foo { static int Array[]; // 静态成员变量, 类中仅是声明而未定义, 故可省略数组大小. }; // 类外对静态数据成员进行定义, 此处由编译器根据初始化列表推断数组大小. int Foo::Array[] = { 1, 2, 3, 4, 5}; ``` <br> ## 指向静态数组的指针 通常 "**指向==数组==的指针**" 实际指的是 "**指向==数组内首项元素==的指针**" ,**通过该指针能够访问数组中元素**。 (数组名即是被隐式转换为这一类型的指针) 而 "**指向==整个数组==的指针**" 也即 "**指向==静态数组类型==的指针** ",所指向的是 "**==数组类型==**", 例如, `int (*p)[5]`:指向 "**包含 5 个 `int` 元素的数组类型**"的指针 区别说明: ```cpp title:pointer_to_array_type.cpp // 一维数组 int arr[5]; int* p1 = arr; // `arr`隐式转为"指向数组首元素的指针" int (*pt_arr)[5] = &arr; // `&arr`为"指向数组类型的指针". // 对二维数组 int arr_2d[3][4]; int (*p2)[4] = arr_2d; // `arr_2d`为二维数组名, 隐式转为"指向数组首元素(大小为4的一维数组)的指针" int* p3 = arr_2d[0]; // `arr_2d[0]`为一维数组名, 隐式转为"指向数组首元素(int)的指针" int (*pt_arr_2d)[3][4] = &arr_2d; // `&arr_2d`为"指向数组类型的指针". ``` ## 静态数组的引用 ```cpp title:reference_of_array // 对一维数组类型的引用 int arr[5] = {1, 2, 3, 4, 5}; int (&ref)[5] = arr; // 对"包含5个int元素的数组类型"的引用 // 对二维数组类型的引用 int arr_2d[3][4]; int (&ref_2d)[3][4] = arr_2d;  // 对"包含3个含有4个int元素的数组类型"的引用 ``` <br> ## 静态数组作为函数形参 C++中不允许拷贝数组。当**数组名**被用作**形参**时,会自动发生从"**数组**"到"**指向数组首元素的指针**"的 **==隐式类型转换==**。 因此,当函数需要接收一个数组时,需要以 "**指向==数组内元素类型==的指针**" 作为形参。 在函数形参中,可以**用 `[]` 来代替 `*` 标识指针**,例如 `int []` 等价于 `int*`,可更清晰地**标识该形参为 "==数组形参=="**。 示例: - `void fun(int []);` 等价于 `void func(int*);` - 形参类型为"**指向 int 型元素的指针**",可接受**一维静态数组名作为实参**。 - `void func(int [][4]);` 等价于 `void func(int (*)[4]);` - 形参类型为 "**指向包含 4 个 int 型元素的数组的指针**",可接受**二维数组名作为实参**。 > [!caution] 函数形参中使用 `[]` 代替 `*` 时,其形参类型是 "**指向元素类型的指针**",故与"**数组大小**"无关,**不声明数组大小(==声明了也没有任何实际作用==)** > > 但是,对于**多维数组**来说,**==数组第二维及其后所有维度的大小==** 都是 **内层的==数组类型的一部分==**,因而不能省略。 > 例如 `void func(int arr[][4]);` ,其中 `int arr[][4]` 即可接受一个二维数组,**第二维是 size 为 4 的数组**。 示例:下列三种形式的形参等价,可接受数组实参: ```cpp // 下面三种形参等价, 形参都是`const int*`类型 void func(const int*); void func(const int []); // `[]`在函数形参中即表示指针类型 void func(const int [10]); // 这里的`10`并不限定实际数组元素, 只是表达形参的期望值, 因此完全可省略, 同上一句. ``` 示例:下列三种形式的形参等价,可接受二维数组实参,其中第二维 size 固定。 ```cpp // 对于二维数组, 下面三种形参等价, 形参都是`int (*)[4]`类型 void func21(int (*arr)[4]) {} // void func22(int arr[][4]) {} // void func23(int arr[10][4]) {} // 第一维度10可省略, 没有任何实际作用. ``` 使用示例: ```cpp template<typename T, int dim> class Point { public: Point(); Point(T coords[]) { for (int i = 0; i < dim; ++i) { _coords[i] = coords[i]; } } Point(T (*coords)[dim]) { // 形参是"指向具有dim个T类型元素的数组的指针" for (int i = 0; i < dim; ++i) { _coords[i] = (*coords)[i]; // 先对指针解引用, 然后再取下标, 否则下标偏移是dim*sizeof(T)字节. } } Point(std::initializer_list<T> list) { assert(list.size() >= dim); for (int i = 0; i < dim; ++i) { _coords[i] = *(list.begin() + i); } } T& operator[](int idx) { return const_cast<T&>(static_cast<const Point&>(*this)[idx]); } const T& operator[](int idx) const { assert(idx >= 0 && idx < dim); return _coords[idx]; } private: T _coords[dim]; }; template<typename T, int dim> inline std::ostream& operator<<(std::ostream& os, Point<T, dim>& pt) { os << "("; for (int i = 0; i < dim - 1; ++i) { os << pt[i] << ", "; } os << pt[dim - 1] << ")"; return os; } int main() { double array[3] { 0.15, 3.42, 1.27 }; Point<double, 1> pt1(array); Point<double, 2> pt2({2.14, 1.72}); Point<double, 3> pt3(&array); cout << pt1 << endl; cout << pt2 << endl; cout << pt3 << endl; } ``` <br> ### 静态数组引用形参 C++允许将变量定义为**数组的引用**,因此**形参也可以是数组的引用**,但必须 **==明确地指定数组大小==**,且 **==要求实参数组大小匹配==**。 ```cpp void func(int (&arr)[10]); // arr是对一个"含有10个int元素的数组"的引用 int arr[5] = {1, 2, 3, 4, 5}; int (&ref)[5] = arr; int *(&arr2)[5]; // `arr2`是对一个"含有10个int*元素的指针数组"的引用 ``` <br><br> ## 指向静态数组的指针或引用作为函数返回类型 函数形式: - **返回指向静态数组的指针**的函数:`Type (*function(param_list)) [dimension]` - **返回静态数组引用**的函数:`Type (&function(param_list)) [dimension]` 其中,`(*function(param_list))` 两端括号必须有存在,`[dimension]` 指定数组维度。 必须在 **==被定义的函数名==** 及其 ==**形参列==表之后**声明**数组的维度**。 推进改写为**尾置返回类型**,会更清晰明了,例如: ```cpp int (*func(int i)) [10]; // 函数返回一个指向包含10个int元素的静态数组的指针 int (&func(int i)) [10]; // 函数返回一个指向包含10个int元素的静态数组的引用 // auto func(int i) -> int(*)[10]; auto func(int i) -> int(&)[10]; ``` <br><br> ## 静态数组遍历 对于静态数组,可以由以下几种方式进行遍历。 其中,"方式三" 和 "方式四" 即**基于两个指针进行遍历**,而方式二和方式四则 **==无需明确知道数组大小==**。 ```cpp title:traverse_array.cpp // 遍历一维数组 void traverse_array() { int arr[5] = {1, 2, 3, 4, 5}; // 方式一: 根据数组大小明确指定循环访问的次数 for (int i = 0; i < 5; ++i) { cout << arr[i] << ' '; } cout << endl; // 方式二: 使用范围for循环 for (int& elem : arr) { cout << elem << ' '; } cout << endl; // 方式三: 使用指针遍历 int* p_begin = arr; // 指向数组首元素的指针 int* p_end = &arr[5]; // 或 = arr + 5; 尾后指针, 指向数组尾元素之后的内存地址     for (; p_begin != p_end; ++p_begin) {         cout << *p_begin << ' ';     }     cout << endl; // 方式四: 使用标准库函数begin()和end() // 定义于头文件<iterator>中, 用于返回指向数组首元素和尾元素之后的指针 for (int* p = begin(arr); p != end(arr); ++p) { cout << *p << ' '; } cout << endl; } ``` ```cpp title:traverse_multi_dim_array.cpp // 遍历多维数组 void traverse_multi_dim_array() { constexpr size_t rowCnt = 3, colCnt = 4; int array[rowCnt][colCnt] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; // 方式一: 下标访问 for (int i = 0; i < rowCnt; ++i) { for (int j = 0; j < colCnt; ++j) { cout << array[i][j] << " "; } cout << endl; } cout << endl; // 方式二: 使用范围for循环 for (auto& row : array) { // 外层循环变量必须要声明为引用类型, 从而推导为int(&)[4]; 否则row会被解释为指针int*, 内层col会报错. for (auto col : row) { cout << col << " "; } cout << endl; } cout << endl; // 方式三: 使用指针遍历(利用begin和end()获取指针 for (auto p = begin(array); p != end(array); ++p) { // p是int(*)[colCnt] 类型的指针. for (auto q = begin(*p); q != end(*p); ++q) { // q是int*类型的指针. cout << *q << " "; } cout << endl; } cout << endl; } ``` > [!NOTE] 使用范围 `for` 循环处理多维数组时,除了最内层的循环外,其他所有循环的控制变量都应声明为引用类型 > > > 参见 [^1] (P114) > ```cpp > constexpr size_t rowCnt = 3, colCnt = 4; > int array[rowCnt][colCnt]; > for (auto& row: array) { // 外层的循环变量row必须声明为引用, 否则内层循环被自动转换为指针类型, 而无法遍历 > for (auto col : row) { > cout << col << " "; > } > cout << endl; > } > ``` > > [!NOTE] C++11 标准库提供了 `begin` 和 `end` 两个函数 > > 类似于 STL 容器的成员函数,为静态数组提供了获取 **==指向首项元素的指针==** 以及 **==尾后指针==** 的功能。 > 其中,**==尾后指针==指向静态数组的==尾元素的后一内存地址==(而不是尾元素)**。 > > [!example] 示例:利用指针遍历二维静态数组,修改对角线元素的值 > > ```cpp > const int N = 16; > void fix_set_diag(int A[N][N], int val) { > int* Aptr = A[0]; // 等价于&A[0][0]. > int* Aend = &A[N][N]; // 作为边界的地址, 无效但实际不会访问, 因此不用担心越界访问错误. > while (Aptr != Aend) { > *Aptr = val; > Aptr += N + 1; > } > } > ``` > #### 在函数中遍历形参数组的几种方式 ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-数组相关.assets/IMG-cpp-数组相关-3F314B557802F87EE1ACD69F3B6209F2.png|550]] ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-数组相关.assets/IMG-cpp-数组相关-94800FA5374644E9A3072E6E094FCD37.png|550]] ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-数组相关.assets/IMG-cpp-数组相关-9226D6DD32908683B6671D983281F57D.png|550]] <br><br><br> ## 多维静态数组 C++中并没有真正的多维数组,所谓多维数组其实是**嵌套数组**——**数组的数组**。 对于多维数组,**一个维度表示数组本身大小**,**后一个维度表示其==元素(也是数组)的大小==**,以此类推,**从紧跟数组名的第一维度开始逐步解析**。 以 `int arr[3][4][5]` 为例:`arr` 是一个大小为 3 的数组,其每个元素都是大小为 4 的数组,这些数组的元素又是含有 5个 `int` 元素的数组。 ```cpp // 示例一: 二维嵌套数组 int A[5][3]; // 等价于下述声明: typedef int row3_t [3]; row3_t A[5]; // 示例二: 三维嵌套数组 int A[3][4][5]; // 等价于下述声明 typedef int row3_t [3]; typedef row3_t row4_3_t [4]; row4_3_t A[5]; ``` <br><br> #### 多维静态数组的内存空间布局 在这种**嵌套声明**方式下,**数组元素**在**内存**中将按照 "**行优先的顺序排列**": ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-数组相关.assets/IMG-cpp-数组相关-9A9ADF6818FFABF6E4B9B123840FE63C.png|399]] > [!example] > ![[Excalidraw/Excalidraw-Solution/数组相关.excalidraw#^group=ti13Fcwj|402]] <br><br><br> # 动态数组 > [!NOTE] **动态数组是通过指针动态创建的,因此其类型并非 C++类型系统中的 "arrary type",==而就是指针类型==**。 对于动态数组,其每一个维度下的数组都由**一个指针**来表示。因此: ```cpp title:dynamic_array.cpp // 申请一维动态数组 int *p = new int[10]; // 释放一维动态数组 delete[] p; // 申请二维动态数组: 一个[5][3]的二维动态数组. // p2是指向指针的指针, 即"指向int*类型的指针", 用其指向一个指针数组. int **p2 = new int*[5]; // 申请行指针: 创建一个长度为5的指针数组. for (int i = 0; i < 5; ++i) { p2[i] = new int[3]; // 申请每一行的空间: 对指针数组里的每个指针, 都动态申请一个长度为10的一维数组. } // 释放二维动态数组 for (int i = 0; i < 5; ++i) { delete[] p2[i]; // 释放每一行的空间 } delete[] p2; // 释放行指针 ``` <br><br><br> # 字符数组 字符数组相较于其他数组具有一些特殊性质[^1](P102)。 > [!NOTE] 任何可使用 "字符串字面量" 的地方,均可由使用 "以 `\0` 结尾的字符数组"。 > > 例如: 可用于 `cout <<` 中; 可用字符数组初始化 `std::string` 对象或为其赋值。 > [!caution] 字符数组中必须存在空字符 `\0` 作为结尾字符 > > 对于一个合法的**字符数组**,在其**数组范围内==必须==要确保有一个==空字符== `\0` 作为==结尾字符==**,标志着 **==字符串的结束==**。 > > 对于 `std::cout`、`strlen()`、`strcpy()` 等字符串操作而言,**空字符 `\0` 是必须的**,这些操作**根据该空字符来确定字符串的结束位置**。 <br> ## 字符数组初始化 几种初始化方式: - 直接使用 "**==字符串字面量==**" 对字符数组进行**初始化**,**编译器会==自动在末尾加上空字符== `\0`**。 - 使用**花括号初始化列表**包裹一个 "**字符串字面量**",效果同上; - 使用**花括号初始化列表**包裹一个 "**字符列表**"时,**==必须手动添加空字符== `\0` 作为终止字符**。 ```cpp title:init_char_array.cpp // 直接使用"字符串字面量" 对字符数组进行初始化,编译器会自动在末尾加上空字符`\0`。 char str[] = "Hello"; // 实际分配了6个字符的空间,包括末尾的'\0' char str2[] = {"Hello"}; // 同上 // 以"字符"列表的形式初始化字符数组时, 必须手动添加'\0'结束符 char str3[] = {'H', 'e', 'l', 'l', 'o', '\0'}; ``` 对字符数组初始化之后,==**不允许==直接将另一个字符串字面量**赋值给字符数组,**需要使用字符串拷贝函数 `strcpy`**。 > [!NOTE] > - **可以**用 "**字符串字面量**" **==初始化==** 字符数组 > - **不能**将 "**字符串字面量**" **==赋值==** 给字符数组 <br> ## 字符数组的数组名 在大多数 C++表达式中, **`char` 数组名、`char` 指针、字符串字面量** 都被解释为 **==字符串中第一个字符的地址==**。 但是,在 `std::cout` 中,上述三者都会被输出为 "**==字符串内容==**"。 如果要输出 **`char` 数组 / `char` 指针所指的地址**(即第一个字符的地址),需要: - **将==数组名/指针==进行==显式类型转换==**,可转换为 `void*` 指针类型,从而实现。 - 或者,对于**字符数组**,可用 **`&` 直接取其地址**。 ```cpp title:cout_the_address_of_char.cpp char char_arr[] = "Hello"; const char* p = "World"; cout << char_arr << '\t' <<(void*)char_arr<< endl; cout << char_arr << '\t' << &char_arr<< endl; // 与上等效 cout << p << '\t' << (void*)p <<endl; ``` <br> ## 字符数组操作 **字符数组**、**指向字符串的 `char` 指针**支持一系列相同的字符串相关操作。 参见 [[02-开发笔记/02-c/c-字符串处理|c-字符串处理]] ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-数组相关.assets/IMG-cpp-数组相关-EA0CEE44FD7755F64DE5317C44BEECEE.png|650]] ![[_attachment/02-开发笔记/01-cpp/类型相关/cpp-数组相关.assets/IMG-cpp-数组相关-29A990EF72CB4C5A8E3A1C2547D98F70.png|660]] <br><br> # 参考资料 # Footnotes [^1]: 《C++ Primer》