c++从C语言中继承了数组,在C语言中数组几乎随处可见。c++提供了更容易使用且更不容易出错的抽象(c++ 98开始使用std::vector<T>, c++ 11开始使用std::array<T, n>),因此对数组的需求不像C那样经常出现。然而,当您阅读遗留代码或与用C编写的库交互时,您应该对数组的工作方式有一个坚实的把握。

本FAQ分为五个部分:

类型级别上的数组和访问元素 数组创建和初始化 赋值和参数传递 多维数组和指针数组 使用数组时常见的陷阱

如果你觉得这个常见问题中缺少了一些重要的内容,请写下答案并将其链接到这里作为附加部分。

在下面的文本中,“array”指的是“C数组”,而不是类模板std::array。假定具有C声明器语法的基本知识。注意,下面演示的手动使用new和delete在遇到异常时非常危险,但这是另一个常见问题的主题。


(注意:这是Stack Overflow的c++常见问题解答的一个条目。如果你想批评在这个表单中提供FAQ的想法,那么在meta上开始这一切的帖子将是这样做的地方。这个问题的答案在c++聊天室中被监控,FAQ的想法最初就是在那里开始的,所以你的答案很可能会被想出这个想法的人读到。)


当前回答

5. 使用数组时常见的陷阱。

5.1陷阱:信任类型不安全链接。

好的,你已经被告知,或者你自己已经发现,globals (namespace 可以在翻译单元外访问的范围变量) 邪恶™。但是你知道他们有多邪恶吗?考虑到 下面的程序,由两个文件[main.cpp]和[numbers.cpp]组成:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

在Windows 7中,这个编译和链接与mingwg++ 4.4.1和 Visual c++ 10.0。

由于类型不匹配,程序在运行时崩溃。

形式解释:程序具有未定义行为(UB),而不是 因此,它可以挂起来,或者什么都不做,或者它 可以向美国、俄罗斯、印度的总统发送威胁邮件, 中国和瑞士,让鼻子里的恶魔飞出来。

实际解释:在main.cpp中,数组被视为一个指针,被放置 和数组的地址一样。对于32位可执行文件,这意味着第一个 数组中的Int值,被视为指针。例如,在main.cpp中 数字变量包含(int*)1。这导致了 访问内存的程序在地址空间的最下面,也就是 传统的保留和陷阱造成。结果:你会撞车。

编译器完全有权利不诊断这个错误, 因为c++ 11§3.5/10规定了兼容类型的要求 对于声明,

[N3290§3.5/10) 违反此类型标识规则不需要进行诊断。

同一段详细说明了允许的变化:

数组对象的声明可以指定数组类型 区别在于是否存在主数组边界(8.3.4)。

这种允许的变化不包括将名称声明为其中的数组 翻译单元,以及作为另一个翻译单元的指针。

5.2陷阱:过早优化(memset和朋友)。

还没写

5.3陷阱:使用C语言来获取元素的个数。

有丰富的C语言经验,很自然地会写……

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

由于数组在需要时衰减为指向第一个元素的指针,因此 表达式sizeof(a)/sizeof(a[0])也可以写成 sizeof (a) / sizeof(*)。意思都一样,不管怎么说 它是用C语言编写的,用于查找数组的数字元素。

主要缺陷:C习惯用法不是类型安全的。例如,代码 ...

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

将一个指针传递给N_ITEMS,因此很可能产生一个错误 结果。在Windows 7中编译为32位可执行文件,生成…

7个元素,调用显示… 1的元素。

编译器将int const a[7]重写为int const a[]。 编译器将int const a[]重写为int const* a。 因此,用指针调用N_ITEMS。 对于32位可执行文件,sizeof(数组)(指针的大小)则为4。 Sizeof (*array)等价于Sizeof (int),对于32位可执行文件也是4。

为了在运行时检测这个错误,您可以执行…

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7个元素,调用显示… 断言失败:("N_ITEMS需要一个实际的数组作为参数",typeid(a) != typeid(&*a)),文件runtime_detect . (&*a)) 离子。cpp,第16行 此应用程序请求运行时以一种不寻常的方式终止它。 请联系应用程序的支持团队以获得更多信息。

运行时错误检测比不检测好,但它会浪费一些时间 处理器时间,可能还有更多的程序员时间。更好的检测 编译时间!如果你愿意在c++ 98中不支持局部类型数组, 然后你可以这样做:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

用g++编译第一个完整的程序, 我有…

M:\count> g++ compile_time_detection.cpp 编译时间检测.cpp:在函数void display(const int*): 编译时间检测.cpp:14:错误:没有匹配函数调用'n_items(const int*&)' M: \数> _

它是如何工作的:数组通过引用传递到n_items,所以它做 而不是衰减到指向第一个元素的指针,函数可以返回 类型指定的元素个数。

在c++ 11中,你也可以将它用于局部类型的数组,它是类型安全的 c++习惯用法,用于查找数组的元素数量。

5.4 c++ 11和c++ 14的缺陷:使用constexpr数组大小函数。

在c++ 11和以后的版本中,这是很自然的,但正如你将看到的那样危险!, 替换c++ 03函数

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

with

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

其中重要的变化是使用constexpr,这允许 此函数生成一个编译时间常数。

例如,相对于c++ 03函数,这样的编译时间常数 可以用来声明一个与另一个相同大小的数组:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

但是考虑使用constexpr版本的代码:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

缺陷:截至2015年7月,上述编译与MinGW-64 5.1.0 -pedantic-errors, 在gcc.godbolt.org/上使用在线编译器进行测试,也使用clang 3.0 和clang 3.2,但不与clang 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1或 3.7(实验)。对于Windows平台来说,重要的是它不能编译 Visual c++ 2015。原因是c++ 11/ c++ 14关于使用的语句 constexpr表达式中的引用:

C++11 C++14 $5.19/2 nine

th

dash

A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:         ⋮ an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either it is initialized with a constant expression or it is a non-static data member of an object whose lifetime began within the evaluation of e;

一个人总是可以写得更详细

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

但是当Collection不是一个原始数组时,这就失败了。

类的可重载性来处理非数组的集合 N_items函数,但是为了在编译时使用,需要一个编译时 数组大小的表示。以及经典的c++ 03解决方案,它工作得很好 同样在c++ 11和c++ 14中,是让函数不以值的形式报告其结果 而是通过它的函数结果类型。比如这样:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

关于static_n_items返回类型的选择:这段代码没有使用std::integral_constant 因为用std::integral_constant表示结果 直接作为constexpr值,重新引入原来的问题。而不是 对于Size_carrier类,1可以让函数直接返回 引用数组。然而,并不是每个人都熟悉这种语法。

关于命名:这个解决方案的一部分是constexpr-invalid-due-to引用 问题是要明确地选择编译时间常数。

希望“哎哟,在你的constexpr中有一个引用”的问题将被修复 但在此之前,像上面STATIC_N_ITEMS这样的宏会产生可移植性, 例如,clang和Visual c++编译器,保留类型安全。

相关:宏不尊重作用域,因此为了避免名称冲突,可以使用 使用名称前缀是个好主意,例如MYLIB_STATIC_N_ITEMS。

其他回答

5. 使用数组时常见的陷阱。

5.1陷阱:信任类型不安全链接。

好的,你已经被告知,或者你自己已经发现,globals (namespace 可以在翻译单元外访问的范围变量) 邪恶™。但是你知道他们有多邪恶吗?考虑到 下面的程序,由两个文件[main.cpp]和[numbers.cpp]组成:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

在Windows 7中,这个编译和链接与mingwg++ 4.4.1和 Visual c++ 10.0。

由于类型不匹配,程序在运行时崩溃。

形式解释:程序具有未定义行为(UB),而不是 因此,它可以挂起来,或者什么都不做,或者它 可以向美国、俄罗斯、印度的总统发送威胁邮件, 中国和瑞士,让鼻子里的恶魔飞出来。

实际解释:在main.cpp中,数组被视为一个指针,被放置 和数组的地址一样。对于32位可执行文件,这意味着第一个 数组中的Int值,被视为指针。例如,在main.cpp中 数字变量包含(int*)1。这导致了 访问内存的程序在地址空间的最下面,也就是 传统的保留和陷阱造成。结果:你会撞车。

编译器完全有权利不诊断这个错误, 因为c++ 11§3.5/10规定了兼容类型的要求 对于声明,

[N3290§3.5/10) 违反此类型标识规则不需要进行诊断。

同一段详细说明了允许的变化:

数组对象的声明可以指定数组类型 区别在于是否存在主数组边界(8.3.4)。

这种允许的变化不包括将名称声明为其中的数组 翻译单元,以及作为另一个翻译单元的指针。

5.2陷阱:过早优化(memset和朋友)。

还没写

5.3陷阱:使用C语言来获取元素的个数。

有丰富的C语言经验,很自然地会写……

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

由于数组在需要时衰减为指向第一个元素的指针,因此 表达式sizeof(a)/sizeof(a[0])也可以写成 sizeof (a) / sizeof(*)。意思都一样,不管怎么说 它是用C语言编写的,用于查找数组的数字元素。

主要缺陷:C习惯用法不是类型安全的。例如,代码 ...

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

将一个指针传递给N_ITEMS,因此很可能产生一个错误 结果。在Windows 7中编译为32位可执行文件,生成…

7个元素,调用显示… 1的元素。

编译器将int const a[7]重写为int const a[]。 编译器将int const a[]重写为int const* a。 因此,用指针调用N_ITEMS。 对于32位可执行文件,sizeof(数组)(指针的大小)则为4。 Sizeof (*array)等价于Sizeof (int),对于32位可执行文件也是4。

为了在运行时检测这个错误,您可以执行…

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7个元素,调用显示… 断言失败:("N_ITEMS需要一个实际的数组作为参数",typeid(a) != typeid(&*a)),文件runtime_detect . (&*a)) 离子。cpp,第16行 此应用程序请求运行时以一种不寻常的方式终止它。 请联系应用程序的支持团队以获得更多信息。

运行时错误检测比不检测好,但它会浪费一些时间 处理器时间,可能还有更多的程序员时间。更好的检测 编译时间!如果你愿意在c++ 98中不支持局部类型数组, 然后你可以这样做:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

用g++编译第一个完整的程序, 我有…

M:\count> g++ compile_time_detection.cpp 编译时间检测.cpp:在函数void display(const int*): 编译时间检测.cpp:14:错误:没有匹配函数调用'n_items(const int*&)' M: \数> _

它是如何工作的:数组通过引用传递到n_items,所以它做 而不是衰减到指向第一个元素的指针,函数可以返回 类型指定的元素个数。

在c++ 11中,你也可以将它用于局部类型的数组,它是类型安全的 c++习惯用法,用于查找数组的元素数量。

5.4 c++ 11和c++ 14的缺陷:使用constexpr数组大小函数。

在c++ 11和以后的版本中,这是很自然的,但正如你将看到的那样危险!, 替换c++ 03函数

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

with

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

其中重要的变化是使用constexpr,这允许 此函数生成一个编译时间常数。

例如,相对于c++ 03函数,这样的编译时间常数 可以用来声明一个与另一个相同大小的数组:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

但是考虑使用constexpr版本的代码:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

缺陷:截至2015年7月,上述编译与MinGW-64 5.1.0 -pedantic-errors, 在gcc.godbolt.org/上使用在线编译器进行测试,也使用clang 3.0 和clang 3.2,但不与clang 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1或 3.7(实验)。对于Windows平台来说,重要的是它不能编译 Visual c++ 2015。原因是c++ 11/ c++ 14关于使用的语句 constexpr表达式中的引用:

C++11 C++14 $5.19/2 nine

th

dash

A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:         ⋮ an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either it is initialized with a constant expression or it is a non-static data member of an object whose lifetime began within the evaluation of e;

一个人总是可以写得更详细

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

但是当Collection不是一个原始数组时,这就失败了。

类的可重载性来处理非数组的集合 N_items函数,但是为了在编译时使用,需要一个编译时 数组大小的表示。以及经典的c++ 03解决方案,它工作得很好 同样在c++ 11和c++ 14中,是让函数不以值的形式报告其结果 而是通过它的函数结果类型。比如这样:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

关于static_n_items返回类型的选择:这段代码没有使用std::integral_constant 因为用std::integral_constant表示结果 直接作为constexpr值,重新引入原来的问题。而不是 对于Size_carrier类,1可以让函数直接返回 引用数组。然而,并不是每个人都熟悉这种语法。

关于命名:这个解决方案的一部分是constexpr-invalid-due-to引用 问题是要明确地选择编译时间常数。

希望“哎哟,在你的constexpr中有一个引用”的问题将被修复 但在此之前,像上面STATIC_N_ITEMS这样的宏会产生可移植性, 例如,clang和Visual c++编译器,保留类型安全。

相关:宏不尊重作用域,因此为了避免名称冲突,可以使用 使用名称前缀是个好主意,例如MYLIB_STATIC_N_ITEMS。

数组创建和初始化

与任何其他类型的c++对象一样,数组可以直接存储在命名变量中(那么大小必须是编译时常量;c++不支持VLAs),或者它们可以匿名存储在堆上,并通过指针间接访问(只有这样才能在运行时计算大小)。

自动数组

每次控制流经过非静态局部数组变量定义时,都会创建自动数组(位于“堆栈上”的数组):

void foo()
{
    int automatic_array[8];
}

初始化按升序进行。注意,初始值取决于元素类型T:

如果T是POD(如上面例子中的int),则不进行初始化。 否则,T的默认构造函数初始化所有元素。 如果T没有提供可访问的默认构造函数,则程序不会编译。

或者,初始值可以在数组初始化式中显式指定,数组初始化式是一个以逗号分隔的列表,用花括号括起来:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

由于在这种情况下,数组初始化器中的元素数量等于数组的大小,因此手动指定大小是多余的。它可以由编译器自动推导:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

也可以指定数组的大小并提供一个更短的数组初始化式:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

在这种情况下,其余元素是零初始化的。注意,c++允许空数组初始化式(所有元素都是零初始化的),而C89不允许(至少需要一个值)。还要注意,数组初始化器只能用于初始化数组;以后不能在作业中使用它们。

静态数组

静态数组(位于“数据段中”的数组)是用Static关键字定义的本地数组变量和命名空间范围内的数组变量(“全局变量”):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(注意,名称空间范围内的变量是隐式静态的。将static关键字添加到它们的定义中具有完全不同的、不推荐的含义。)

下面是静态数组与自动数组的不同之处:

Static arrays without an array initializer are zero-initialized prior to any further potential initialization. Static POD arrays are initialized exactly once, and the initial values are typically baked into the executable, in which case there is no initialization cost at runtime. This is not always the most space-efficient solution, however, and it is not required by the standard. Static non-POD arrays are initialized the first time the flow of control passes through their definition. In the case of local static arrays, that may never happen if the function is never called.

(以上都不是特定于数组的。这些规则同样适用于其他类型的静态对象。)

数组数据成员

数组数据成员是在创建其所属对象时创建的。不幸的是,c++ 03没有提供在成员初始化列表中初始化数组的方法,因此初始化必须用赋值来伪造:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

或者,你可以在构造函数体中定义一个自动数组,然后复制元素:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

在c++ 0x中,由于统一初始化,数组可以在成员初始化器列表中初始化:

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

这是唯一适用于没有默认构造函数的元素类型的解决方案。

动态数组

动态数组没有名称,因此访问它们的唯一方法是通过指针。因为它们没有名字,所以从现在开始我将它们称为“匿名数组”。

在C语言中,匿名数组是通过malloc和friend创建的。在c++中,匿名数组是使用新的T[size]语法创建的,该语法返回指向匿名数组第一个元素的指针:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

如果内存大小在运行时被计算为8,下面的ASCII图描述了内存布局:

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

显然,匿名数组比命名数组需要更多的内存,因为必须单独存储额外的指针。(免费商店也有一些额外的开销。)

Note that there is no array-to-pointer decay going on here. Although evaluating new int[size] does in fact create an array of integers, the result of the expression new int[size] is already a pointer to a single integer (the first element), not an array of integers or a pointer to an array of integers of unknown size. That would be impossible, because the static type system requires array sizes to be compile-time constants. (Hence, I did not annotate the anonymous array with static type information in the picture.)

对于元素的默认值,匿名数组的行为类似于自动数组。 通常,匿名POD数组不会初始化,但是有一个特殊的语法可以触发值初始化:

int* p = new int[some_computed_size]();

(请注意分号前的最后一对括号。)同样,c++ 0x简化了规则,并允许为匿名数组指定初始值,这要归功于统一的初始化:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

如果你用完了匿名数组,你必须把它释放回系统:

delete[] p;

你必须释放每个匿名数组一次,然后永远不要再碰它。完全不释放它会导致内存泄漏(或者更普遍地,根据元素类型,会导致资源泄漏),并且尝试多次释放它会导致未定义的行为。使用非数组形式的delete(或free)而不是delete[]来释放数组也是未定义的行为。

程序员经常混淆多维数组和指针数组。

多维数组

大多数程序员都熟悉命名多维数组,但许多人不知道多维数组也可以匿名创建。多维数组通常被称为“数组的数组”或“真正的多维数组”。

命名多维数组

当使用命名多维数组时,所有维度必须在编译时已知:

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

这是命名多维数组在内存中的样子:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

注意,像上面这样的2D网格只是有用的可视化。从c++的观点来看,内存是一个“扁平的”字节序列。多维数组的元素按行长顺序存储。即connect_four[0][6]和connect_four[1][0]是内存中的邻居。实际上,connect_four[0][7]和connect_four[1][0]表示同一个元素!这意味着你可以将多维数组视为大型一维数组:

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

匿名多维数组

对于匿名多维数组,除第一个维度外的所有维度必须在编译时已知:

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

这是一个匿名多维数组在内存中的样子:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

注意,数组本身仍然被分配为内存中的单个块。

指针数组

您可以通过引入另一层间接来克服固定宽度的限制。

指针的命名数组

这是一个由五个指针组成的命名数组,初始化时使用不同长度的匿名数组:

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

下面是它在内存中的样子:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

由于现在每一行都是单独分配的,因此将2D数组视为1D数组不再有效。

指针的匿名数组

这是一个5个(或任何其他数量)指针的匿名数组,初始化时使用不同长度的匿名数组:

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

下面是它在内存中的样子:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

转换

数组到指针的衰减自然扩展到数组的数组和指针的数组:

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

然而,从T[h][w]到T**没有隐式转换。如果这种隐式转换确实存在,那么结果将是指向指向T的h个指针数组的第一个元素的指针(每个指针都指向原始2D数组中一行的第一个元素),但是这个指针数组在内存中还不存在。如果你想要这样的转换,你必须手动创建和填充所需的指针数组:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

注意,这将生成原始多维数组的视图。如果你需要一个副本,你必须创建额外的数组并自己复制数据:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;

赋值

没有特定的原因,数组不能被分配给另一个数组。使用std::copy代替:

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

这比真正的数组赋值更灵活,因为可以将较大数组的切片复制到较小的数组中。 copy通常专门用于基本类型,以提供最大的性能。std::memcpy不太可能执行得更好。如果有疑问,就测量一下。

虽然不能直接分配数组,但可以分配包含数组成员的结构体和类。这是因为数组成员是由赋值操作符逐个复制的,而赋值操作符是编译器提供的默认值。如果为自己的结构体或类类型手动定义赋值操作符,则必须为数组成员返回到手动复制。

参数传递

数组不能按值传递。你可以通过指针或者引用来传递它们。

传递指针

由于数组本身不能按值传递,通常将指向数组第一个元素的指针按值传递。这通常被称为“传递指针”。由于数组的大小不能通过该指针获取,所以必须传递第二个参数来指示数组的大小(经典的C解决方案)或第二个指针指向数组的最后一个元素(c++迭代器解决方案):

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

作为一种语法替代,你也可以将参数声明为T p[],它只在参数列表的上下文中表示与T* p完全相同的事情:

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

你可以认为编译器只是在参数列表的上下文中将T p[]重写为T *p。这个特殊的规则是造成数组和指针混淆的部分原因。在其他上下文中,将某个对象声明为数组或指针会产生巨大的差异。

不幸的是,你也可以在数组参数中提供一个大小,但编译器会默默地忽略它。也就是说,以下三个签名是完全等价的,正如编译器错误所指出的那样:

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

通过引用传递

数组也可以通过引用传递:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

在这种情况下,数组大小非常重要。因为编写一个只接受8个元素的数组的函数用处不大,程序员通常把这样的函数写成模板:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

请注意,只能使用实际的整数数组调用这样的函数模板,而不能使用指向整数的指针。数组的大小会自动推断出来,对于每个大小为n的数组,都会从模板实例化一个不同的函数。您还可以编写非常有用的函数模板,从元素类型和大小中抽象出来。

类型级别上的数组

数组类型表示为T[n],其中T是元素类型,n是正大小,即数组中元素的数量。数组类型是元素类型和大小的乘积类型。如果其中一种或两种成分不同,你就会得到一种不同的类型:

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

请注意,大小是类型的一部分,也就是说,不同大小的数组类型是不兼容的类型,彼此之间绝对没有任何关系。sizeof(T[n])等价于n * sizeof(T)。

Array-to-pointer衰变

T[n]和T[m]之间唯一的“联系”是这两种类型都可以隐式转换为T*,转换的结果是指向数组第一个元素的指针。也就是说,在任何需要T*的地方,你都可以提供T[n],编译器会默默地提供该指针:

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

This conversion is known as "array-to-pointer decay", and it is a major source of confusion. The size of the array is lost in this process, since it is no longer part of the type (T*). Pro: Forgetting the size of an array on the type level allows a pointer to point to the first element of an array of any size. Con: Given a pointer to the first (or any other) element of an array, there is no way to detect how large that array is or where exactly the pointer points to relative to the bounds of the array. Pointers are extremely stupid.

数组不是指针

编译器将在数组的第一个元素被认为有用时静默生成一个指针,也就是说,当一个操作在数组上失败而在指针上成功时。从数组到指针的转换很简单,因为得到的指针值只是数组的地址。注意,指针不存储为数组本身的一部分(或内存中的其他任何地方)。数组不是指针。

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

数组不会衰减为指向其第一个元素的指针的一个重要上下文是&操作符应用于该数组时。在这种情况下,&操作符产生一个指向整个数组的指针,而不仅仅是指向数组第一个元素的指针。尽管在这种情况下,值(地址)是相同的,指向数组第一个元素的指针和指向整个数组的指针是完全不同的类型:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

下面的ASCII图解释了这种区别:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

注意,指向第一个元素的指针只指向一个整数(表示为一个小方框),而指向整个数组的指针则指向一个包含8个整数的数组(表示为一个大方框)。

同样的情况在课堂上也会出现,而且可能更加明显。指向对象的指针和指向其第一个数据成员的指针具有相同的值(相同的地址),但它们是完全不同的类型。

如果你不熟悉C声明器语法,int(*)[8]类型中的圆括号是必不可少的:

Int(*)[8]是一个指向8个整数数组的指针。 Int *[8]是一个包含8个指针的数组,每个指针的类型都是Int *。

访问元素

c++提供了两种语法变体来访问数组的各个元素。 这两种东西都没有优劣之分,你应该都熟悉。

指针的算术

给定指向数组第一个元素的指针p,表达式p+i将生成指向数组第i个元素的指针。通过之后对该指针的解引用,可以访问单个元素:

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

如果x表示一个数组,那么数组到指针的衰减就会发生,因为添加一个数组和一个整数是没有意义的(数组上没有加号操作),但添加一个指针和一个整数是有意义的:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(注意,隐式生成的指针没有名称,所以我写了x+0来标识它。)

另一方面,如果x表示指向数组的第一个(或任何其他)元素的指针,则数组到指针的衰减是不必要的,因为i要加到的指针已经存在:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

请注意,在所描述的情况下,x是一个指针变量(通过x旁边的小方框可以分辨出来),但它也可以是返回指针的函数的结果(或任何其他类型的T*表达式)。

索引操作符

由于语法*(x+i)有点笨拙,c++提供了另一种语法x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;

由于加法是可交换的,下面的代码做的完全相同:

std::cout << 3[x] << ", " << 7[x] << std::endl;

索引操作符的定义导致以下有趣的等价:

&x[i]  ==  &*(x+i)  ==  x+i

然而,&x[0]通常不等同于x。前者是指针,后者是数组。只有当上下文触发数组到指针的衰减时,x和&x[0]才能互换使用。例如:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

在第一行,编译器检测到从一个指针到另一个指针的赋值操作,该操作成功。在第二行,它检测从数组到指针的赋值。因为这是没有意义的(但是指针到指针的赋值是有意义的),数组到指针的衰减像往常一样开始了。

范围

类型为T[n]的数组有n个元素,索引从0到n-1;没有元素n。然而,为了支持半开放范围(其中开头是包含的,结尾是不包含的),c++允许计算指向(不存在的)第n个元素的指针,但取消引用该指针是非法的:

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

例如,如果你想对一个数组排序,下面两种方法都同样有效:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

请注意,提供&x[n]作为第二个参数是非法的,因为这相当于&*(x+n),并且子表达式*(x+n)在技术上调用c++中未定义的行为(但在C99中不是)。

还要注意,您可以简单地提供x作为第一个参数。对我来说,这有点太简洁了,而且它也使编译器更难推断模板参数,因为在这种情况下,第一个参数是一个数组,而第二个参数是一个指针。(同样,数组到指针的衰减开始了。)