跳转到内容

C++ 编程/运算符/数组

来自维基教科书,开放的书籍,开放的世界

一个数组存储一组大小恒定的连续块,每个块包含一个所选类型的数值,这些数值都存储在同一个名称下。数组通常可以有效率且直观地组织数据集合。

最简单的理解方式就是将一个数组看作是一个简单的列表,每个数值都作为列表中的一个项目。通过数组中的位置来访问单个元素,称为索引,也称为下标。数组中的每个项目都有一个从 0 到(数组大小)-1 的索引,表示它在数组中的位置。

数组的优点包括:

  • 随机访问时间复杂度为 O(1) (大 O 符号)
  • 易于使用/移植:集成到大多数现代语言中

缺点包括:

  • 大小恒定
  • 数据类型恒定
  • 需要一个大的连续空闲块来容纳大型数组
  • 当用作非静态数据成员时,元素类型必须允许默认构造
  • 数组不支持复制赋值(你不能写 arraya = arrayb
  • 数组不能用作标准容器的数值类型
  • 使用语法不同于标准容器
  • 数组和继承不能混合(派生类的数组不是基类的数组,但很容易被误认为是基类的数组)

注意
如果复杂度允许,你应该考虑使用容器(如 C++ 标准库中的容器)。你可以使用例如std::vector它在大多数情况下与数组一样快,可以动态调整大小,支持迭代器,并且可以让你像数组一样处理向量存储。

在 C++11 中,std::array提供了一个固定大小的数组,它保证与旧式数组一样高效,但具有一些优势,例如可以查询其大小,使用与其他容器相同的迭代器,并且拥有复制赋值运算符。

(现代 C 允许使用 VLA,即变长数组,但这些在 C++ 中没有使用,因为 C++ 已经提供了可调整大小的数组功能,即std::vector.)

指针运算符,正如你将看到,它与数组运算符类似。


例如,以下是一个名为 List 的整数数组,它有 5 个元素,编号为 0 到 4。数组的每个元素都是一个整数。与其他整数变量一样,数组的元素在初始化之前是未初始化的。这意味着在我们将任何内容赋值给它之前,它充满了未知的值。(记住 C 中的基本类型不会初始化为 0。)

索引 数据
00 未指定
01 未指定
02 未指定
03 未指定
04 未指定

由于一个数组存储的是数值,因此在声明数组时必须定义存储的数值类型和数量,以便可以分配所需的内存。数组的大小必须是const大于零的整型表达式。这意味着你不能使用用户输入来声明一个数组。你需要分配内存(使用operator new[]),因此数组的大小必须在编译时已知。连续存储方法的另一个缺点是必须存在一个足够大的连续空闲块来容纳数组。如果你有一个 500,000,000 个块的数组,每个块长 1 字节,那么你需要大约 500 兆字节的连续空间可用;这有时需要对内存进行碎片整理,这需要很长时间。

要声明一个数组,你可以这样做

int numbers[30]; // creates an array of 30 integers

或者

char letters[4]; // create an array of 4 characters

等等...

在声明数组时进行初始化,你可以使用

int vector[6]={0,0,1,0,0,0};

这不仅会创建一个包含 6 个 int 元素的数组,还会将它们初始化为给定的值。

如果你在初始化数组时提供的元素数量少于数组的总元素数量,则剩余的元素将被设置为默认值 - 对于数字来说是零。

int vector[6]={0,0,1}; // this is the same as the example above

如果你在声明数组时完全初始化了它,则可以允许编译器计算数组的大小

int vector[]={0,0,1,0,0,0};  // the compiler can see that there are 6 elements
赋值和访问数据
[编辑 | 编辑源代码]

你可以使用数组的名称后跟索引来给数组赋值。

例如,要将数字 200 赋值给数组中索引为 2 的元素

 
List[2] = 200;

将得到

索引 数据
00 未指定
01 未指定
02 200
03 未指定
04 未指定

你可以用相同的方式访问数组中元素的数据。

std::cout << List[2] << std::endl;

这将打印 200。

基本上,操作数组中的单个元素与操作普通变量没有什么不同。

正如你所看到的,访问存储在数组中的值非常容易。再看另一个例子

int x;
x = vector[2];

上面的声明将赋值x变量vector中索引为 2 的存储的值,即 1。

数组的索引从 0 开始,而不是从 1 开始。上面数组的第一个元素是vector[0]. 数组中最后一个值的索引是数组的大小减 1。在上面的例子中,下标从 0 到 5。C++ 不会对数组访问进行边界检查。编译器不会抱怨以下代码

char y;
int z = 9;
char vector[6] = { 1, 2, 3, 4, 5, 6 };
  
// examples of accessing outside the array. A compile error is not raised
y = vector[15];
y = vector[-4];
y = vector[z];

在程序执行期间,数组访问越界并不总是会导致运行时错误。你的程序可能在从vector[-1]中检索值后仍然愉快地继续执行。为了缓解索引问题,sizeof 表达式通常在编写处理数组的循环时使用。

int ix;
short anArray[]= { 3, 6, 9, 12, 15 };
  
for (ix=0; ix< (sizeof(anArray)/sizeof(short)); ++ix) {
  DoSomethingWith( anArray[ix] );
}

请注意,在上面的示例中,数组的大小没有明确指定。编译器知道它的大小是 5,因为初始化列表中有五个值。在列表中添加一个额外的值会导致它的大小变为 6,并且由于sizeof 表达式位于for 循环中,代码会自动适应这种变化。

多维数组
[编辑 | 编辑源代码]

你也可以使用多维数组。最简单的类型是二维数组。这将创建一个矩形数组 - 每一行都有相同数量的列。要获取一个包含 3 行 5 列的 char 数组,我们可以写...

char two_d[3][5];

要访问/修改此数组中的值,我们需要两个下标

char ch;
ch = two_d[2][4];

或者

two_d[0][0] = 'x';

还有一些奇怪的表示法

int a[100];
int i = 0;
if (a[i]==i[a])
  printf("Hello World!\n");

a[i]i[a]指向同一个位置。在了解指针之后,你会更好地理解这一点。

要获取一个数组大小不同,你必须使用realloc, malloc, memcpy等显式地处理内存。

为什么从 0 开始?
[编辑 | 编辑源代码]

大多数编程语言从 0 开始对数组进行编号。这在数组与指向数组第一个元素的指针可以互换使用的语言中很有用。在 C++ 中,数组中元素的地址可以从 (第一个元素的地址) + i 计算得出,其中 i 是从 0 开始的索引 (a[1] == *(a + 1))。请注意,这里的 “(第一个元素的地址) + i” 不是对数字的字面加法。不同类型的数据具有不同的大小,编译器会正确地考虑这一点。因此,如果索引从 0 开始,指针运算会更简单。

为什么数组索引没有边界检查?
[编辑 | 编辑源代码]

C++ 允许但不要求边界检查实现,在实践中很少或根本没有进行检查。它会影响存储需求(需要“胖指针”)并影响运行时性能。但是,std::vector模板类,我们之前提到了它,我们将在后面详细介绍(一个模板类容器,它代表一个数组,它提供了at()方法),它确实执行边界检查。同样,在许多实现中,标准容器在调试模式下包含了非常完整的边界检查。它们可能不支持这些检查,因为任何容器类相对于内置数组的性能下降都可能阻止程序员从数组迁移到更现代、更安全的容器类。

注意
一些编译器或外部工具可以帮助检测语言规范之外的问题,即使是自动化的方式。有关更多信息,请参见有关调试的部分。

华夏公益教科书