深入理解 C++ 内置数组(四十三)

1. 定义和初始化内置数组

数组的声明格式一般为:

type arrayName[dimension];

其中,arrayName 是数组名,dimension 表示数组中元素的个数,必须为一个常量表达式。数组的元素类型可以是任意对象类型(但不包括引用)。

1.1 常规定义

例如:

int arr[10];          // 定义一个含有 10 个 int 类型元素的数组
unsigned sz = 42;
constexpr unsigned csz = 42;
int *parr[csz];       // 定义一个含有 42 个整型指针的数组

注意:

  1. 数组的维度必须是常量表达式。如果使用普通变量(如 sz),则会导致编译错误。
  2. 默认情况下,数组的元素会被默认初始化。对于内置类型,如 int,默认初始化通常不会自动赋值(局部数组未初始化);但如果是全局数组,则所有元素初始化为 0。
  3. 不能使用 auto 关键字让编译器推断数组类型,必须明确指定数组类型和维度。

2. 显式初始化数组元素

可以在数组声明时使用列表初始化来显式地指定每个元素的初始值。如果在声明时省略数组的维度,编译器将根据初始值的个数自动推导出数组大小;反之,如果指定了维度,则初始值的个数不能超过该维度,多余部分会忽略,缺少部分将使用默认值进行填充。

2.1 使用列表初始化

下面的示例展示了不同方式初始化数组的效果:

示例代码 说明
int a1[3] = {0, 1, 2}; 数组 a1 有 3 个元素,分别初始化为 0、1、2。
int a2[] = {0, 1, 2}; 数组 a2 的维度自动推导为 3。
int a3[5] = {0, 1, 2}; 数组 a3 有 5 个元素,前三个元素分别为 0、1、2,后两个元素值默认初始化为 0。
string a4[3] = {"hi", "bye"}; 数组 a4 有 3 个元素,前两个初始化为 “hi” 和 “bye”,第三个元素初始化为空字符串。

例如:

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;

int main() {
    
    
    int a1[3] = {
    
    0, 1, 2};
    int a2[] = {
    
    0, 1, 2};
    int a3[5] = {
    
    0, 1, 2};  // 剩余两个元素初始化为 0

    string a4[3] = {
    
    "hi", "bye"};
    
    cout << "a1: ";
    for (auto x : a1)
        cout << x << " ";
    cout << endl;
    
    cout << "a3: ";
    for (int i = 0; i < 5; ++i)
        cout << a3[i] << " ";
    cout << endl;
    
    cout << "a4: ";
    for (auto &s : a4)
        cout << "\"" << s << "\" ";
    cout << endl;
    
    return 0;
}

3. 字符数组的特殊初始化

字符数组具有额外的初始化方式,可以使用字符串字面值来初始化。例如:

char al[] = {
    
    'C', '+', '+'};  // 列表初始化,没有空字符,维度为 3
char a2[] = {
    
    'C', '+', '+', '\0'};  // 显式包含空字符,维度为 4
char a3[] = "C++";            // 自动在末尾添加空字符,维度为 4

注意:
当用字符串字面值初始化字符数组时,必须预留空间存放结尾的空字符。例如:

const char a4[6] = "Daniel";  // 错误:字符串 "Daniel" 实际上有 7 个字符(包括结尾的空字符),数组大小必须至少为 7。

4. 数组的拷贝和赋值

内置数组与其他对象不同,不能直接将一个数组赋值给另一个数组,也不能用数组作为初始值拷贝另一个数组:

int a[] = {
    
    0, 1, 2};
// int b[] = a;   // 错误:不能使用数组初始化另一个数组
// a = b;         // 错误:不能直接赋值给数组

如果需要将一个数组的内容复制到另一个数组中,可以使用循环或标准库算法(如 std::copy)。

5. 理解复杂的数组声明

数组声明中涉及多种修饰符时,必须特别注意绑定顺序。以下是一些例子:

5.1 存放指针的数组

int *ptrs[10];  // 定义一个数组 ptrs,其中包含 10 个指向 int 的指针

这是最简单的情况:从右向左阅读,ptrs 是一个数组,数组的每个元素都是指向 int 的指针。

5.2 数组的指针和数组的引用

数组的指针和数组的引用声明相对复杂。举例:

  • 数组指针:

    int arr[10];
    int (*Parray)[10] = &arr;
    

    解释:Parray 是一个指针,它指向一个包含 10 个 int 元素的数组。

  • 数组引用:

    int arr[10];
    int (&arrRef)[10] = arr;
    

    解释:arrRef 是一个引用,引用了一个包含 10 个 int 元素的数组。

  • 引用数组(错误示例):
    数组不能直接存放引用,因此下面的写法是错误的:

    // int &refs[10] = /*?*/; // 错误:不存在引用的数组
    
  • 引用数组中的指针:
    可以定义一个引用,该引用引用的是一个数组,其中元素为指针:

    int *ptrs[10];
    int*(&arry)[10] = ptrs; // arry 是一个引用,引用了一个含有 10 个 int* 的数组
    

    从内向外阅读:首先,arry 是一个引用,其引用的对象是一个数组;数组中每个元素是 int*

6. 总结

  • 定义和初始化:

    • 数组声明时必须指定元素个数,且该个数必须为常量表达式。
    • 可以使用列表初始化对数组元素进行显式初始化,若初始值数量少于数组维度,其余元素采用默认初始化。
  • 字符数组特殊性:

    • 使用字符串字面值初始化字符数组时,编译器会自动添加一个空字符作为结束标志,因此数组大小必须足够大以容纳该空字符。
  • 数组赋值与拷贝:

    • 内置数组不支持直接赋值或拷贝,只能通过逐元素复制实现。
  • 复杂声明:

    • 数组的指针和数组的引用声明相对复杂,理解时建议从内向外或从右向左逐步解析修饰符的绑定顺序。

通过掌握数组的这些基本知识和细节,你可以在 C++ 编程中正确使用内置数组,并为后续学习更高级的容器(如 vector)奠定基础。

参考资料

  • cppreference.com 关于数组、初始化和数组指针的详细说明
  • 各大 C++ 编码规范中对数组声明与初始化的建议

希望这篇文章能帮助你全面理解 C++ 内置数组的定义、初始化方法以及复杂声明的技巧,从而更好地运用数组处理数据。