17-7 C 风格数组介绍
既然我们已经介绍了std::vector和std::array,接下来我们将通过讲解最后一种数组类型——C风格数组来完成对数组的全面覆盖。
正如第16.1节 容器与数组介绍 所述,C风格数组源自C语言,是C++核心语言的内置特性(区别于其他数组类型——它们属于标准库容器类)。这意味着使用时无需#include头文件。
顺带一提……
由于它们是语言原生支持的唯一数组类型,标准库中的数组容器类型(如 std::array 和 std::vector)通常采用 C 风格数组实现。
声明C风格数组
由于C风格数组属于核心语言的一部分,因此拥有专属的声明语法。在C风格数组声明中,我们使用方括号([])告知编译器所声明的对象是C风格数组。方括号内可选地提供数组长度,该长度为std::size_t类型的整数值,用于告知编译器数组包含多少个元素。
以下定义创建了一个名为testScore的C风格数组变量,包含30个int类型的元素:
int main()
{
int testScore[30] {}; // Defines a C-style array named testScore that contains 30 value-initialized int elements (no include required)
// std::array<int, 30> arr{}; // For comparison, here's a std::array of 30 value-initialized int elements (requires #including <array>)
return 0;
}

C 风格数组的长度必须至少为 1。若数组长度为零、负数或非整数值,编译器将报错。
进阶读者须知:
在堆上动态分配的 C 风格数组允许长度为 0。
C 风格数组的长度必须是常量表达式
与 std::array 类似,声明 C 风格数组时,数组长度必须是常量表达式(类型为 std::size_t,尽管这通常无关紧要)。
提示:
为兼容C99的可变长度数组variable-length arrays(VLAs)特性,某些编译器可能允许创建长度为非constexpr的数组。可变长度数组在C++中无效,不应在C++程序中使用。若编译器支持此类数组,可能是您忘记禁用编译器扩展功能(参见0.10节——配置编译器:编译器扩展)。
对C风格数组进行下标操作
与std::array类似,C风格数组可通过下标运算符(operator[])进行索引:
#include <iostream>
int main()
{
int arr[5]; // define an array of 5 int values
arr[1] = 7; // use subscript operator to index array element 1
std::cout << arr[1]; // prints 7
return 0;
}

与标准库容器类(仅使用类型为std::size_t的无符号索引)不同,C风格数组的索引可以是任何整数类型(带符号或无符号)的值,或无作用域枚举类型。这意味着C风格数组不会遇到标准库容器类所面临的所有符号转换索引问题!
#include <iostream>
int main()
{
const int arr[] { 9, 8, 7, 6, 5 };
int s { 2 };
std::cout << arr[s] << '\n'; // okay to use signed index
unsigned int u { 2 };
std::cout << arr[u] << '\n'; // okay to use unsigned index
return 0;
}

提示:
C 风格数组可接受带符号或无符号的索引(或无作用域枚举)。
operator[]运算符不会进行任何边界检查,传递超出边界的索引将导致未定义行为。
顺带一提…:
声明数组时(例如 int arr[5]),[] 符号属于声明语法的一部分,并非对下标运算符 operator[] 的调用。
C 风格数组的聚合初始化
与 std::array 类似,C 风格数组属于聚合类型,因此可通过聚合初始化进行初始化。
简而言之,聚合初始化允许我们直接初始化聚合体的成员。为此,我们提供初始化列表——即用大括号括起的、以逗号分隔的初始化值列表。
int main()
{
int fibonnaci[6] = { 0, 1, 1, 2, 3, 5 }; // copy-list initialization using braced list
int prime[5] { 2, 3, 5, 7, 11 }; // list initialization using braced list (preferred)
return 0;
}

这些初始化形式均按顺序初始化数组成员,从元素0开始。
若未为C风格数组提供初始化器,元素将采用默认初始化。多数情况下这会导致元素处于未初始化状态。由于通常需要初始化元素,在定义C风格数组时若未提供初始化器,应采用值初始化(使用空大括号)。
int main()
{
int arr1[5]; // Members default initialized int elements are left uninitialized)
int arr2[5] {}; // Members value initialized (int elements are zero uninitialized) (preferred)
return 0;
}

如果初始化列表中提供的初始化项数量超过定义的数组长度,编译器将报错。如果初始化列表中提供的初始化项数量少于定义的数组长度,则剩余未初始化的元素将按值初始化:
int main()
{
int arr1[5]; // Members default initialized int elements are left uninitialized)
int arr2[5] {}; // Members value initialized (int elements are zero uninitialized) (preferred)
return 0;
}

使用C风格数组的一个缺点是必须显式指定元素类型。CTAD无法工作,因为C风格数组不是类模板。尝试使用auto从初始化列表推断数组元素类型同样行不通:
int main()
{
auto squares[5] { 1, 4, 9, 16, 25 }; // compile error: can't use type deduction on C-style arrays
return 0;
}

省略的长度Omitted length
以下数组定义中存在微妙的冗余。发现了?
int main()
{
const int prime[5] { 2, 3, 5, 7, 11 }; // prime has length 5
return 0;
}

我们明确地告诉编译器该数组长度为5,同时又用5个元素初始化它。当使用初始化列表初始化C风格数组时,我们可以省略数组定义中的长度,让编译器根据初始化项的数量推断数组长度。
以下数组定义的行为完全相同:
int main()
{
const int prime1[5] { 2, 3, 5, 7, 11 }; // prime1 explicitly defined to have length 5
const int prime2[] { 2, 3, 5, 7, 11 }; // prime2 deduced by compiler to have length 5
return 0;
}

这仅在为数组的所有成员显式提供初始化器时才有效。
int main()
{
int bad[] {}; // error: the compiler will deduce this to be a zero-length array, which is disallowed!
return 0;
}

使用初始化列表初始化C风格数组的所有元素时,建议省略长度参数,让编译器自动计算数组长度。这样,当增删初始化项时,数组长度会自动调整,避免因定义的数组长度与提供的初始化项数量不匹配而引发错误。
最佳实践:
当显式为数组每个元素赋值时,建议省略C风格数组的长度参数。
const 和 constexpr C 风格数组
与 std::array 类似,C 风格数组可以是 const 或 constexpr。与其他 const 变量相同,const 数组必须初始化,且元素值之后不可更改。
#include <iostream>
namespace ProgramData
{
constexpr int squares[5] { 1, 4, 9, 16, 25 }; // an array of constexpr int
}
int main()
{
const int prime[5] { 2, 3, 5, 7, 11 }; // an array of const int
prime[0] = 17; // compile error: can't change const int
return 0;
}

C 风格数组的大小sizeof()
在之前的课程中,我们使用 sizeof() 运算符获取对象或类型的字节大小。当应用于 C 风格数组时,sizeof() 会返回整个数组所占用的字节数:
#include <iostream>
int main()
{
const int prime[] { 2, 3, 5, 7, 11 }; // the compiler will deduce prime to have length 5
std::cout << sizeof(prime); // prints 20 (assuming 4 byte ints)
return 0;
}

假设使用4字节整型,上述程序将输出20。素数数组包含5个整型元素,每个占用4字节,因此总计5×4=20字节。
需注意此处不存在额外开销。C风格数组对象仅包含其元素本身,别无其他。
获取C风格数组的长度length
在C++17中,我们可以使用std::size()(定义在
#include <iostream>
#include <iterator> // for std::size and std::ssize
int main()
{
const int prime[] { 2, 3, 5, 7, 11 }; // the compiler will deduce prime to have length 5
std::cout << std::size(prime) << '\n'; // C++17, returns unsigned integral value 5
std::cout << std::ssize(prime) << '\n'; // C++20, returns signed integral value 5
return 0;
}

提示:
std::size() 和 std::ssize() 的标准定义头文件是。但由于这些函数非常实用,许多其他头文件也提供了它们,包括 和 。当对 C 风格数组使用 std::size() 或 std::ssize() 时,我们可能尚未包含其他头文件。此时,按惯例需包含 头文件。 您可在cppreference文档中查阅定义这些函数的完整头文件列表。
获取C风格数组的长度(C++14及更早版本)
在C++17之前,没有标准库函数可用于获取C风格数组的长度。
若使用C++11或C++14,可改用以下函数:
#include <cstddef> // for 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风格数组,并返回表示数组长度的非类型模板参数。
在更早期的代码库中,常会看到通过将整个数组的大小除以数组元素的大小来确定C风格数组的长度:
#include <iostream>
int main()
{
int array[8] {};
std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";
return 0;
}
这将输出:

这是如何实现的?首先,请注意整个数组的大小等于数组长度乘以元素大小。更简洁地表达:数组大小 = 长度 * 元素大小array size = length * element size。
通过代数运算,我们可以重排这个等式:长度 = 数组大小 / 元素大小length = array size / element size。我们通常使用 sizeof(array[0]) 表示元素大小。因此,长度(length) = sizeof(array) / sizeof(array[0])。有时你也会看到这样的写法:sizeof(array) / sizeof(*array),其含义相同。
然而,正如下一课将展示的,该公式在处理衰变数组时极易失效,导致程序意外崩溃。C++17的std::size()与上文示例中的length()函数模板在此情况下均会引发编译错误,故可安全使用。
相关内容:
数组衰变将在下一课17.8节——C风格数组衰变中详述。
C风格数组不支持赋值
或许令人惊讶的是,C++数组不支持赋值:
int main()
{
int arr[] { 1, 2, 3 }; // okay: initialization is fine
arr[0] = 4; // assignment to individual elements is fine
arr = { 5, 6, 7 }; // compile error: array assignment not valid
return 0;
}

从技术上讲,这行不通,因为赋值操作要求左操作数是可修改的左值,而C风格数组不被视为可修改的左值。
若需将新值列表赋值给C风格数组,最佳方案是改用std::vector。替代方案包括:逐元素向C风格数组赋值,或使用std::copy复制现有C风格数组:
#include <algorithm> // for std::copy
int main()
{
int arr[] { 1, 2, 3 };
int src[] { 5, 6, 7 };
// Copy src into 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{}; // allocate 3 ints
显示答案
constexpr int a[3] {}; // allocate 3 ints
问题 #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;
}

显示答案
- 定义数组时,长度必须是编译时常量。在上面的程序中,length 不是 const 类型,因此定义 const int arr[length] 是被禁止的。
- 数组索引范围为0至length-1。因此arr[length]属于越界索引,将导致未定义行为。
- 数组元素类型为“const int”,故无法通过赋值修改arr[0]。
问题 #3
“完全平方数perfect square”是指其平方根为整数的自然数。通过将自然数(包括零)自乘可得到完全平方数。前四个完全平方数为:0, 1, 4, 9。
使用全局常量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 风格数组以查找匹配项。

显示解决方案
#include <iostream>
namespace ProgramData
{
constexpr int squares[] { 0, 1, 4, 9 };
}
bool matchSquare(int input)
{
for (const auto& e : ProgramData::squares)
{
if (input == e)
return true;
}
return false;
}
int main()
{
while (true)
{
std::cout << "Enter a single digit integer, or -1 to quit: ";
int input{};
std::cin >> input;
if (input == -1)
break;
if (matchSquare(input))
std::cout << input << " is a perfect square\n";
else
std::cout << input << " is not a perfect square\n";
}
std::cout << "Bye\n";
return 0;
}



浙公网安备 33010602011771号