第5.5章: C 风格数组
概述
C 风格数组是一个固定大小的元素序列,存储在一整块连续的内存中。 数组是指针算术、数组退化以及 C 风格字符串的基础,因此 理解它们在内存中的行为至关重要。
在本章中,你将学到:
- 如何声明和初始化 C 风格数组
- 数组在内存中如何布局
- 如何安全地访问元素
- 为什么越界访问是未定义行为
- 为什么
::fast_io::array与::fast_io::vector是更安全的选择 - 为什么数组与指针相关但并不相同
1. 声明 C 风格数组
C 风格数组通过指定元素类型与元素个数来声明:
int a[3]; // array of 3 ints
char b[10]; // array of 10 chars
double c[4]; // array of 4 doubles
大小必须在编译期已知,并且必须大于 0。
// int a[0]; // error: C-style arrays cannot have size 0
2. 初始化数组
你可以用花括号初始化来初始化数组:
int a[3]{10, 20, 30};
如果你提供的元素较少,其余元素会被零初始化:
int a[5]{1, 2}; // becomes {1, 2, 0, 0, 0}
如果省略大小,编译器会自行推导:
int a[]{10, 20, 30}; // size is 3
3. 数组在内存中是连续的
数组的所有元素在内存中彼此相邻存放。
int a[3]{10, 20, 30};
Example memory layout (addresses vary):
Address: 1000 1004 1008
Memory: [ 10 ] [ 20 ] [ 30 ]
↑ ↑ ↑
a[0] a[1] a[2]
正是这种连续性让指针算术成为可能(详见第5.6章)。
4. 访问数组元素
使用 [] 运算符访问元素:
int a[3]{10, 20, 30};
int x = a[0]; // 10
int y = a[2]; // 30
a[1] = 99; // a becomes {10, 99, 30}
5. 越界访问是未定义行为
C 风格数组不做边界检查。 访问数组之外的位置是未定义行为。
int a[3]{10, 20, 30};
// a[3]; // undefined behavior — out of bounds
// a[-1]; // undefined behavior
未定义行为意味着:
- 程序可能崩溃
- 也可能破坏内存
- 也可能悄悄产生错误结果
- 也可能看起来一切正常
你不能依赖任何特定结果。
WebAssembly 说明
在未启用内存标记的 WebAssembly 上,越界数组访问通常 不会触发陷阱,使缺陷更难被发现。
6. fast_io::array 与 C 风格数组
::fast_io::array 是 C 风格数组更安全、更灵活的替代品。
- 它对
[]访问执行边界检查。 - 它允许零大小数组,而 C 风格数组不允许。
#include <fast_io_dsal/array.h>
::fast_io::array<int, 0> empty; // valid: zero-sized array
常规元素访问是带检查的:
::fast_io::array<int, 3> arr{10, 20, 30};
int x = arr[1]; // safe: bounds checked
如果你有意需要非检查访问(用于性能关键的代码),
::fast_io::array 提供了:
int x = arr.index_unchecked(1); // no bounds checking
对大多数代码,请优先使用带边界检查的 arr[i]。
7. 作为默认容器的 fast_io::vector
对大多数动态元素序列,请优先使用 ::fast_io::vector。
它自己管理大小与容量,并且同样支持带边界检查的访问。
#include <fast_io_dsal/vector.h>
::fast_io::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
int x = vec[0]; // bounds checked
当你为性能关键的代码需要非检查访问时,
::fast_io::vector 也提供了:
int x = vec.index_unchecked(0); // no bounds checking
对大多数代码,请优先使用带边界检查的 vec[i],仅在仔细审查过的
热点路径中使用 index_unchecked()。
8. 数组与指针相关但并不相同
数组与指针关系密切,但它们并不相同。
数组:
- 是一块固定的内存
- 在编译期就知道自己的大小
- 不能被重新赋值
指针:
- 存储一个地址
- 可以指向不同的对象
- 不知道它所指向数组的大小
我们将在第5.7章:数组退化中详细探讨数组与指针的关系。
9. 获取数组元素的地址
你可以用 ::std::addressof 获取任何元素的地址:
#include <memory>
int a[3]{10, 20, 30};
int *p0 = ::std::addressof(a[0]);
int *p1 = ::std::addressof(a[1]);
int *p2 = ::std::addressof(a[2]);
Addresses (example):
p0 = 1000
p1 = 1004
p2 = 1008
正是这种布局让指针算术有意义(下一章)。
10. 数组不能被拷贝或赋值
C 风格数组不能用 = 拷贝或赋值。
int a[3]{1, 2, 3};
int b[3];
// b = a; // error: arrays cannot be assigned
拷贝数组需要手工逐元素拷贝,或者使用 memcpy
(详见第5.14章)。
要点
- C 风格数组是一块固定大小、连续的内存。
- C 风格数组不能大小为 0。
- 数组元素用
[]访问。 - 越界访问是未定义行为。
- 未定义行为并不保证会崩溃。
::fast_io::array对[]执行边界检查,并支持零大小数组。- 需要时可用
arr.index_unchecked()与vec.index_unchecked()获得非检查访问。 - 优先把
::fast_io::vector作为你的默认容器。 - 优先使用
::fast_io::array而不是 C 风格数组。 - 数组与指针相关但并不相同。
- 本章为指针算术(第5.6章)与数组退化(第5.7章)做准备。