C 风格数组简介
作者:Alex
日期:2024 年 10 月 13 日

在已经介绍了 std::vectorstd::array 之后,我们将通过讲解最后一种数组类型——C 风格数组——来完成对数组的全面探讨。

如第 16.1 课《容器与数组简介》所述,C 风格数组继承自 C 语言,并内置于 C++ 核心语言之中(其余数组类型均为标准库容器类)。因此,使用 C 风格数组无需包含任何头文件。

附注
由于 C 风格数组是语言原生支持的惟一数组类型,标准库中的数组容器(如 std::arraystd::vector)通常以 C 风格数组作为其底层实现。

声明 C 风格数组

作为核心语言的一部分,C 风格数组拥有专用的声明语法。声明时,使用方括号 [] 告知编译器该对象为 C 风格数组;方括号内可放置一个整型常量表达式,其类型为 std::size_t,用以指定数组元素个数。

下列语句定义了一个名为 testScore 的 C 风格数组,包含 30 个 int 元素:

int main()
{
    int testScore[30] {};      // 定义含有 30 个值初始化 int 元素的 C 风格数组(无需包含头文件)

//  std::array<int, 30> arr{}; // 作为对比:std::array 同样拥有 30 个值初始化 int 元素,但需 #include <array>

    return 0;
}

C 风格数组长度必须 ≥ 1。若长度为 0、负数或非整型值,编译器将报错。

进阶说明
在堆上动态分配的 C 风格数组允许长度为 0。

数组长度必须是常量表达式

std::array 相同,C 风格数组在声明时,其长度必须是常量表达式(类型为 std::size_t,通常可忽略)。

提示
某些编译器为了兼容 C99 的变长数组(VLA)特性,可能允许非常量长度。
变长数组并非合法的 C++ 特性,不应在 C++ 程序中使用。若编译器允许此类数组,可能是未关闭编译器扩展(参见 0.10 — 配置编译器:编译器扩展)。

C 风格数组的下标操作

std::array 类似,C 风格数组可使用下标运算符 operator[] 进行索引:

#include <iostream>

int main()
{
    int arr[5]; // 定义含 5 个 int 的数组

    arr[1] = 7; // 使用下标运算符访问第 1 个元素
    std::cout << arr[1]; // 输出 7

    return 0;
}

与标准库容器(仅接受 std::size_t 类型的无符号索引)不同,C 风格数组的索引可为任何整型(含符号或无符号)或无作用域枚举值,因此不会遇到符号转换导致的索引问题。

#include <iostream>

int main()
{
    const int arr[] { 9, 8, 7, 6, 5 };

    int s { 2 };
    std::cout << arr[s] << '\n'; // 有符号索引合法

    unsigned int u { 2 };
    std::cout << arr[u] << '\n'; // 无符号索引同样合法

    return 0;
}

提示
C 风格数组接受有符号、无符号索引或无作用域枚举值。
operator[] 不做边界检查,越界访问将导致未定义行为。

附注
在声明语句中(如 int arr[5]),[] 属于声明语法的一部分,而非调用下标运算符。

C 风格数组的聚合初始化

std::array 一样,C 风格数组是聚合体,可使用聚合初始化。
简要回顾:聚合初始化通过大括号包围的逗号分隔值列表直接初始化聚合体成员。

int main()
{
    int fibonnaci[6] = { 0, 1, 1, 2, 3, 5 }; // 复制列表初始化
    int prime[5] { 2, 3, 5, 7, 11 };         // 列表初始化(推荐)

    return 0;
}

初始化值按元素顺序自 0 开始依次赋值。

若未提供初始化列表,元素将默认初始化;大多数情况下,元素保持未初始化状态。因此,无初始化列表时应使用值初始化(空大括号):

int main()
{
    int arr1[5];    // 元素默认初始化(int 元素保持未初始化)
    int arr2[5] {}; // 元素值初始化(int 元素零初始化)(推荐)

    return 0;
}

若初始化器数量多于数组长度,编译器报错;若少于长度,剩余元素执行值初始化:

int main()
{
    int a[4] { 1, 2, 3, 4, 5 }; // 编译错误:初始化器过多
    int b[4] { 1, 2 };          // arr[2] 与 arr[3] 值初始化

    return 0;
}

C 风格数组的不足之一:必须显式指定元素类型。CTAD 不适用,且无法使用 auto 根据初始化列表推导元素类型:

int main()
{
    auto squares[5] { 1, 4, 9, 16, 25 }; // 编译错误:C 风格数组无法进行类型推导

    return 0;
}

省略长度

以下定义存在隐式冗余:

int main()
{
    const int prime[5] { 2, 3, 5, 7, 11 }; // 显式指定长度 5

    return 0;
}

当使用初始化列表时,可省略长度,由编译器根据初始化器数量推导:

int main()
{
    const int prime1[5] { 2, 3, 5, 7, 11 }; // 显式长度 5
    const int prime2[] { 2, 3, 5, 7, 11 };  // 编译器推导长度 5

    return 0;
}

此技巧仅在所有元素均提供初始化器时有效。

int main()
{
    int bad[] {}; // 错误:编译器推导为零长度数组,非法!

    return 0;
}

当使用初始化列表显式初始化全部元素时,建议省略长度,以便增删元素时长度自动调整,避免长度与初始化器数量不匹配。

最佳实践
若所有元素均由初始化列表给定,优先省略 C 风格数组长度。

const 与 constexpr C 风格数组

std::array 类似,C 风格数组可声明为 constconstexpr
与其他 const 变量一样,const 数组必须初始化,且元素值不可再更改:

#include <iostream>

namespace ProgramData
{
    constexpr int squares[5] { 1, 4, 9, 16, 25 }; // constexpr int 数组
}

int main()
{
    const int prime[5] { 2, 3, 5, 7, 11 }; // const int 数组
    prime[0] = 17; // 编译错误:无法修改 const int

    return 0;
}

C 风格数组的 sizeof

前文曾使用 sizeof() 运算符获取对象或类型所占字节数。
对 C 风格数组使用 sizeof() 返回整个数组占用的字节总数:

#include <iostream>

int main()
{
    const int prime[] { 2, 3, 5, 7, 11 }; // 编译器推导长度为 5

    std::cout << sizeof(prime); // 输出 20(假设 int 为 4 字节)

    return 0;
}

假设 int 占 4 字节,则 5 * 4 = 20 字节。
注意:C 风格数组无额外开销,对象仅包含元素本身。

获取 C 风格数组长度

  • C++17:可使用 <iterator> 头文件中的 std::size(),返回 std::size_t 类型的无符号长度。
  • C++20:可使用 std::ssize(),返回带符号整数(通常为 std::ptrdiff_t)。
#include <iostream>
#include <iterator> // std::size 与 std::ssize

int main()
{
    const int prime[] { 2, 3, 5, 7, 11 };   // 编译器推导长度 5

    std::cout << std::size(prime) << '\n';  // C++17,返回无符号整数 5
    std::cout << std::ssize(prime) << '\n'; // C++20,返回带符号整数 5

    return 0;
}

提示
std::size()std::ssize() 的规范定义位于 <iterator>。由于这两个函数极为常用,<array><vector> 等头文件也将其引入。若仅对 C 风格数组使用,习惯上包含 <iterator>

完整支持列表参见 cppreference 中 size 函数的文档。

C++14 及更早版本获取长度

在 C++17 之前,标准库未提供获取 C 风格数组长度的函数。
C++11/14 可使用以下函数模板:

#include <cstddef> // std::size_t
#include <iostream>

template <typename T, std::size_t N>
constexpr std::size_t length(const T(&)[N]) noexcept
{
    return N;
}

int main() {
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << length(array) << " elements\n";

    return 0;
}

该模板以引用方式接收 C 风格数组,并通过非类型模板形参 N 返回长度。

在更旧代码中,可能见到以下写法:

#include <iostream>

int main()
{
    int array[8] {};
    std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";

    return 0;
}

输出:

The array has: 8 elements

原理:sizeof(array) 等于 length * sizeof(array[0]),故
length = sizeof(array) / sizeof(array[0])
亦可见 sizeof(array) / sizeof(*array),效果相同。

然而,正如下一课所示,当数组发生退化(decay)时,此公式极易出错,导致程序异常。
C++17 的 std::size() 及上述 length() 模板会在退化场景下触发编译错误,因此更安全。

相关内容
下一课(17.8 — C 风格数组退化)将讨论数组退化。

C 风格数组不支持赋值

令人意外的是,C++ 数组不可直接整体赋值:

int main()
{
    int arr[] { 1, 2, 3 }; // 合法:初始化
    arr[0] = 4;            // 合法:元素赋值
    arr = { 5, 6, 7 };     // 编译错误:数组整体赋值非法

    return 0;
}

根本原因在于:赋值要求左操作数为可修改左值,而 C 风格数组并非可修改左值。

若需整体赋予新值,应改用 std::vector;亦可逐个元素赋值,或使用 std::copy

#include <algorithm> // std::copy

int main()
{
    int arr[] { 1, 2, 3 };
    int src[] { 5, 6, 7 };

    // 将 src 拷贝至 arr
    std::copy(std::begin(src), std::end(src), std::begin(arr));

    return 0;
}

测验

问题 1
将以下 std::array 定义转换为等价的 constexpr C 风格数组定义:

constexpr std::array<int, 3> a{}; // 分配 3 个 int

[显示答案]

问题 2
以下程序存在哪三处错误?

#include <iostream>

int main()
{
    int length{ 5 };
    const int arr[length] { 9, 7, 5, 3, 1 };

    std::cout << arr[length];
    arr[0] = 4;

    return 0;
}

[显示答案]

问题 3
完全平方数是指平方根为整数的自然数。自然数(含 0)自乘即可得到完全平方数。前 4 个完全平方数为:0, 1, 4, 9。
请使用全局 constexpr C 风格数组保存 0 到 9(含)之间的所有完全平方数。
反复提示用户输入一位整数,或输入 -1 退出。输出该整数是否为完全平方数。
输出格式应与以下一致:

Enter a single digit integer, or -1 to quit: 4
4 is a perfect square

Enter a single digit integer, or -1 to quit: 5
5 is not a perfect square

Enter a single digit integer, or -1 to quit: -1
Bye

提示:使用范围 for 循环遍历 C 风格数组以查找匹配项。

[显示答案]

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"