详细介绍:C++中的数组

大家好!我是大聪明-PLUS


我们继续推出“C++ 深度解析”系列文章。本系列旨在尽可能详细地介绍各种语言特性,其中一些特性相当专业。这是本系列的第四篇文章;

本文主要讨论数组。数组是 C++ 最古老的底层机制之一,其历史可以追溯到 C 语言的早期版本。尽管存在一些限制,数组如今已成为 C++ 面向对象类型系统的一部分。程序员了解这些特性对于避免潜在错误至关重要。本文还将探讨 C 语言的另一个遗留问题——平凡类型和未初始化变量。C++11、C++14 和 C++17 中的一些新特性会影响数组的处理,本文也将详细介绍这些新特性。那么,让我们来全面了解一下数组吧。

1. 总则

数组是最简单的聚合类型。它模拟了一组排列在连续内存空间中的相同元素。几乎所有编程语言都以某种形式支持数组,因此数组出现在 C 语言的早期版本中,后来又成为 C++ 的一部分,也就不足为奇了。

1.1 声明数组

如果在编译时对T某些类型、N常量或表达式求值,则该语句

T a[N];

声明一个a类型为“数组”的变量,数组N元素类型为T“” 。该类型必须隐式转换为类型,并且其值(称为数组大小)必须大于零。数组分配在一块连续的内存块中,数组中的每个元素分配一个字节。因此,保存整个数组所需的内存为字节。此值受平台和编译器的限制。数组类型用表示,表示它包含元素类型和数组大小。因此,具有相同元素类型但大小不同的数组将具有不同的类型。NTNstd::size_tsizeof(T)N*sizeof(T)T[N]

这类数组也称为常规数组,以区别于其他类型的数组。术语“数组”在编程中广泛使用,包括 C++。
以下是一些有效的数组声明示例:

const int N = 8;
constexpr int Square(int n) { return n * n; }
int a1[1];
int a2[N];
int a3['Q'];
int a4[Square(2)];

以下是一些数组声明错误的示例:

int n;
int b1[0];
int b2[n];
int b3["Q"];

数组元素通过索引器访问,索引值范围从 00N-11。以下是一个示例:

int a[4];
a[0] = 42;
int t = a[3];

未检查数组越界,因此可能会出现错误,导致未定义行为。

可以在一条语句中声明多个数组,但必须为每个数组指定大小。

int a[4], b[8];

数组类型可以设置别名。可以使用传统的关键字方法typedef

typedef int I4[4];

或者使用更现代的(C++11)关键字using

using I4 = int[4];

之后,这些数组被声明为简单变量:

I4 a, b;

这将与

int a[4], b[4];


1.2. 数组操作运算符和标准函数

要处理数组,可以使用运算符sizeof以及几个标准函数和宏。

该运算符sizeof返回数组的总大小(以字节为单位),即元素大小乘以数组大小。

该宏_countof()(位于 MSVS 头文件中<cstdlib>)返回数组大小,即元素数量。C++17 引入了一个标准函数模板,可以实现std::size()相同的功能(并且还有一个重载版本,用于确定标准容器的大小)。

int a[4];
std::cout << sizeof(a) << ' ' << std::size(a) << '\n';

结论:

16 4

在 C++11 中,标准库引入了自由(非成员)函数模板 `__returns__`std::begin()和 ` std::end()__returns__`。当对数组调用 `__returns__` 时,std::begin()它会返回指向数组第一个元素std::end()或倒数第二个元素的指针。(还有 `__const__` 和std::cbegin()` __const__` 版本std::cend()。)这使得数组可以用于范围操作for

int a[4]{ 4, 3, 2, 1 };
for (auto t : a)
{
    std::cout << t << ' ';
}

在标准算法中也是如此:

std::sort(std::begin(a), std::end(a));


1.3 存储器布局

如果数组是静态声明的——即在全局作用域、命名空间作用域或作为静态类成员——它将被分配到静态内存中。局部声明的数组则被分配到栈上。(当然,在选择局部数组的大小时,必须考虑栈的容量限制。)非静态类成员被分配到类实例内部。动态数组(参见第 6 节)被分配到动态内存中。


1.4. 数组元素类型的限制

你不能声明一个元素类型为 的数组void

你不能声明一个引用数组。

int u, v;
int &rr[2] = { u, v };

也可以使用常量指针数组来代替。

int * const rr[2] = { &u, &v };

(数组初始化的语法将在第 3.2 节中讨论。)

C++11 引入了模板std::reference_wrapper<>。它模拟了引用接口,但实例化可以存储在容器和内置数组中。然而,对引用接口的模拟并不完全完善;有时必须使用成员函数get()。以下是一个示例。

int u = 42, v = 5;
std::reference_wrapper rr[2] = { u, v };
std::cout << rr[0] << ' ' << rr[1] << '\n';
++rr[0];
rr[1].get() = 125;
std::cout << u << ' ' << v << '\n';

你不能声明一个函数数组。

int ff[2](double);

也可以使用函数指针数组。

int (*ff[2])(double);

可以使用函数类型来指定模板std::reference_wrapper<>,但它与指针相比几乎没有任何优势——函数本身就可以通过指针调用而无需解引用,而且指针也可以用函数名初始化而无需使用 `@` 运算符&。模拟函数数组的另一种方法是使用模板std::function<>,但这又是另一个话题了。

不能使用关键字声明数组auto

auto x[2] = {1, 2}

该限定符const不适用于数组类型,而仅适用于其元素的类型。

using I4 = int[4];
const I4 ci;

2. 数组的扁平化和复制

本节讨论数组的特性,这些特性使数组区别于一般的 C++ 类型系统。

2.1 总结

如上所述,数组大小是数组类型的一个组成部分,但在某些情况下会丢失,导致数组类型在某种意义上“不完整”。这种丢失称为衰减(数组到指针的衰减)。(“衰减”一词有时也被翻译为“缩减”,有时也使用“分解”。)衰减的本质是,在几乎所有情况下,数组都会被转换为指向第一个元素的指针,并且大小信息丢失。例外情况包括 `\n` 运算符sizeof、`\n` &(地址)运算符和数组引用初始化。`\n` 运算符sizeof已在 1.2 节中讨论过;指针和数组引用将在第 4 节中详细讨论。使用 `\n` 关键字声明decltype也能正确定义数组类型,而不会发生衰减。

当然,数组和指针之间的密切关系不容忽视。以下是一种处理数组所有元素的标准(C 风格)方法:

const int N = 100;
int a[N];
for (int *d = a, *end = d + N; d < end; ++d)
{
    *d = rand();
}

然而,这种简化仍然可以被视为 C 风格的古体,必须谨慎小心地使用,否则可能会遇到一些不愉快的意外。

以下是扁平化如何影响函数声明。函数

void Foo(int a[4]);
void Foo(int a[]);
void Foo(int *a);

它们并非重载函数——它们是同一回事。必须将大小作为附加参数传递,或者必须使用确定大小的特殊约定(例如,字符串以空字符结尾)。

外部阵列连接也会导致阵列扁平化。

// file 1
int A[4];
// file 2
extern int A[]

对于尺寸,您还需要使用额外的变量或使用特殊的约定来确定尺寸。

使用关键字声明变量auto也会导致代码简化。

int a[4];
auto da = a;

指定函数模板时

template
void Foo(T t);

如果参数是数组,则模板函数参数的类型也会被推断为指针。

使用继承时,字符串拼接会引发额外的问题。(毕竟,C 语言没有继承。)我们来看一个例子。

class B {/* ... */};
class D : public B {/* ... */};
void Foo(B[], int size);

以下代码编译时没有错误或警告。

D d[4];
Foo(d, _countof(d));

但是,如果这样做,则数组元素的偏移量(当然,零除外)sizeof(B) < sizeof(D)在函数体中将被错误地确定,因此几乎总是会出错。所以,通过指向基类的指针以多态方式操作数组是不可能的。Foo()dFoo()

2.2 复制

除了归约(以及与之密切相关的)之外,数组类型还有另一个特性使其在某种程度上显得“不完整”。数组不支持基于复制语义的常见初始化和赋值语法:

using I4 = int[4];
I4 a;
I4 b = a;
I4 b{a};
I4 b(a);
I4 b2;
b2 = a;

此外,该函数不能返回数组。

I4 Foo();

但是,如果数组是类/结构/联合体的成员,则编译器生成的复制构造函数和相应的赋值运算符将对该数组执行元素级复制。

struct X
{
    int A[4];
};
X x = { {1, 2, 3, 4} };
X x2 = x;
X x3{x};
X x4;
x4 = x;
X Foo()
{
    return { {4, 3, 2, 1} };
}

另一种会发生数组复制的情况是在 lambda 表达式中按值捕获数组时。

intptr_t x[4];
auto f = [x]() { return sizeof(x) / sizeof(x[0]); };
std::cout << f() << '\n';

结论:

4

但如果使用初始化器捕获(C++14),则会发生扁平化。

intptr_t u[4];
auto g = [x = u]() { return sizeof(x) / sizeof(x[0]); };
std::cout << g() << '\n';

结论:

1


3. 数组初始化

为了描述数组初始化的规则,有必要简要讨论一下平凡类型。


3.1 平凡类型和未初始化变量

构造函数和析构函数是 C++ 对象模型的关键要素。构造函数在对象创建时总是被调用,析构函数在对象销毁时被调用。然而,由于与 C 语言的兼容性问题,C++ 引入了一种例外情况,称为平凡类型。平凡类型旨在模拟 C 类型和 C 变量的生命周期,而无需强制调用构造函数和析构函数。如果 C 代码在 C++ 中编译和执行,其行为应与 C 代码相同。平凡类型包括数值类型、指针、枚举,以及由平凡类型构成的类、结构体、联合体和数组。类和结构体必须满足某些附加条件:不能包含用户自定义的构造函数、析构函数、复制、赋值或虚函数。

除非使用显式初始化方法,否则平凡类型的变量将处于未初始化状态。对于平凡类,编译器可能会生成默认构造函数和析构函数。默认构造函数会重置对象,而析构函数不执行任何操作。但是,只有在使用显式初始化方法时,才会生成并使用此构造函数;否则,变量将保持未初始化状态。

未初始化的变量结构如下:如果它在命名空间(全局)中声明,则所有位都设置为零;如果它在局部或动态声明,则其位将随机排列。显然,使用这样的变量会导致程序行为不可预测。数组通常类型简单,因此这个问题对它们尤为突出。

编译器可以检测到未初始化的平凡类型常量,有时还可以检测到其他未初始化的变量,但静态代码分析器在这方面做得更好。

C++11 标准库包含名为类型属性(头文件<type_traits>)的模板。其中一个属性允许您确定一个类型是否为平凡类型。该表达式的std::is_trivial<Т>::value值为真表示类型为平凡类型,true否则为假。Tfalse

3.2 数组初始化语法

3.2.1. 总则

如果没有显式初始化,非平凡数组保证会为每个元素调用默认构造函数。当然,在这种情况下,这样的构造函数必须存在,否则会发生错误。但是,对于平凡数组,或者当缺少默认构造函数或默认构造函数不合适时,必须使用显式初始化。

自 C 语言起,数组可以使用聚合初始化语法进行初始化:

int a[4] = { 1, 2, 3, 4 };

C++11 引入了统一初始化,现在你可以这样初始化:

int a[4]{ 1, 2, 3, 4 };

对于通用初始化,也可以使用 =,区分这两种初始化类型并不总是容易的,而且可能也不是很有必要。

数组大小可以省略,在这种情况下,数组大小将由初始化器的数量决定。

int a[] { 1, 2, 3, 4 };

如果指定了数组大小,则初始化器的数量不得超过数组大小。如果数组大小大于初始化器的数量,则保证对剩余元素(包括平凡类型)调用默认构造函数(当然,默认构造函数必须存在)。因此,通过指定一个空的初始化器列表,我们可以保证对平凡类型数组的所有元素调用默认构造函数。

int a[4]{};

平凡类型常量数组需要一个强制初始化列表。

const int a[4] = { 3, 2, 1 };

初始化器的数量可能小于数组的大小,在这种情况下,剩余的元素将由默认构造函数进行初始化。

字符数组可以用字符串字面量进行初始化。

const char str[] = "meow";
const wchar_t wstr[] = L"meow";

这样的数组的大小将比字符串中的字符数多 1;必须存储一个终止空字符。


3.2.2. 类成员的初始化

C++11 引入了初始化非静态类成员数组的功能。这可以通过两种方式实现:直接在声明时初始化,或者在定义构造函数时在成员初始化列表中初始化。

class X
{
    int a[4]{ 1, 2, 3, 4 };
    int b[2];
// ...
public:
    X(int u, int v) : b{ u, v }
    {}
// ...
};

但是,在这种情况下,数组大小必须始终显式指定;不允许通过初始化列表隐式定义大小。

静态数组和以前一样,只能在定义时进行初始化;数组大小可以通过初始化列表确定。

class X
{
    static int A[];
// ...
};
int X::A[] = { 1, 2, 3, 4 };

C++17 引入了将静态成员(包括数组)声明为 `static` 的功能inline。此类成员可以在声明时进行初始化;无需定义。

class X
{
    inline static int A[]{ 1, 2, 3, 4 };
// ...
};


3.2.3. 初始化器的要求

初始化列表中的表达式会在初始化之前立即求值;它们不需要在编译时已知(当然,声明为 `int` 的数组除外constexpr)。初始化列表元素的要求与具有与数组元素相同类型的参数的函数参数的要求相同——必须存在从初始化列表元素类型到数组元素类型的隐式转换。假设我们有一个数组声明:

T a[] = {x1 /*, ... */};

或者

T a[]{x1 /*, ... */};

所需转换的存在等同于指令的正确性

T t = x1;

初始化列表元素本身也可以是初始化列表。在这种情况下,该指令的正确性也保证了数组元素的正确初始化。

我们来看一个例子。

class Int
{
    int m_Value;
public:
    Int(int v) : m_Value(v) {}
// ...
};
// ...
int x, y;
// ...
Int rr[] = { x, y };

如果我们把构造函数声明Intexplicit,那么最后的声明就会出错。在这种情况下,我们需要这样写:

Int rr[] = { Int(x), Int(y) };

这个例子也演示了如何使用初始化列表为没有默认构造函数的类型创建数组。但是,在这种情况下,初始化列表的数量必须与数组的大小相匹配。


4. 指向数组的指针和引用


4.1 指向数组的指针

假设我们声明了一个数组。

T a[N];

指向该数组的指针声明并初始化如下:

T(*pa)[N] = &a;

要获取指针,可以使用传统的运算符&。指向数组的指针类型表示为T(*)[N]

注意括号的使用;如果没有括号,我们将得到一个N指向指针类型的元素数组的声明T

指向数组的指针并非指向数组首元素的指针(尽管它们按位相同);它本身并不包含任何信息。它是一个完整的类型,能够“知道”数组的大小。因此,在初始化时,数组的大小必须匹配。

int a[4];
int(*pa)[4] = &a;
int(*p2)[2] = &a;

递增时,数组指针增加的是整个数组的大小,而不是元素的大小。

要通过指针访问数组元素,必须使用运算符*和索引器。

(*pa)[3] = 42;

通过使用别名,您可以实现更熟悉的语法来声明指向数组的指针。

using I4 = int[4];
I4 a{ 1, 2, 3, 4 };
I4 *pa = &a;

你也可以使用auto,编译器会根据初始化器的类型正确地推断变量类型为指向数组的指针。

int a[4];
auto pa = &a;

理解数组指针对于正确处理多维数组至关重要,我们将在后面详细讨论多维数组。

4.2. 数组引用

假设我们声明了一个数组。

T a[N];

该数组的引用声明和初始化方式如下:

T(&ra)[N] = a;

与任何引用一样,数组引用类型的变量必须进行初始化。数组引用类型用 表示T(&)[N]

此外,可以使用指向数组的解引用指针来初始化对数组的引用。

T(*pa)[N] = &a;
T(&ra)[N] = *pa;

与指针类似,引用“知道”数组的大小。因此,初始化时数组的大小必须匹配。

int a[4];
int(&ra)[4] = a;
int(&r2)[2] = a;

通过引用访问数组元素的方式与通过数组标识符访问数组元素的方式相同。

ra[3] = 0;

数组引用正是绕过归约的手段。

功能

void Foo(T(&a)[N]);

它接受类型为的参数T[N],指针不适用于此。

通过使用别名,您可以实现更熟悉的数组引用声明语法。

using I4 = int[4];
I4 a{ 1, 2, 3, 4 };
I4 &ra = a;

你也可以使用auto,编译器会将变量类型推断为数组引用。

int a[4];
auto &ra = a;

&注意后缀的存在auto,如果没有它,将会发生缩减,类型ra将输出为int*

指定函数模板时

template
void Foo(T& t);

如果参数是数组,则模板函数参数的类型也会被推断为数组引用。

使用能够显示数组类型和大小的模板尤其方便。

template
void Foo(T(&a)[N]);

实例化此类模板时,编译器会推断元素类型T和数组大小N(保证大于零)。只有数组才能用作参数;指针会被拒绝。此技术用于宏_countof()和函数模板的实现std::size(),以及函数模板std::begin()std::end(),它们为数组提供了范围实现for,使算法操作更加便捷。第 5 节提供了一个此类模板的示例实现。


5. 多维数组

C++ 不支持真正的多维数组,因此该表达式a[N, M]无效,但多维性被建模为“数组的数组”,因此您可以使用该表达式a[N][M]

如果T某些类型NM表达式可用于确定数组的大小,则该语句

T a[N][M];

声明a一个数组的数组,即一个N元素数组,其中每个元素都是一个M类型为 `<T>` 的元素数组T。这样的数组被称为二维数组。表达式 `<T>` a[i][j],其中` <T>` 到i` <T>` ,`<T> ` 到`<T> `,用于访问此数组的元素。第一个索引从数组的数组中选择一个数组,第二个索引选择此数组中的一个元素。值 `<T>`可以称为二维数组的外维度,`<T>` 可以称为内维度。多维数组的类型表示为 `<T>` 。0N-1j0M-1NMT[N][M]

该表达式是一个类型为 的元素a[i]数组。因此,可以对其进行简化,可以获取其地址,也可以用于初始化引用。

T *dai = a[i];
T(*pai)[M] = &a[i];
T(&rai)[M] = a[i];

归约操作将数组转换为指向元素的指针。对于二维数组,元素本身也是一个数组,这意味着二维数组被归约成一个指向数组的指针。

T a[N][M];
T(*da)[M] = a;

因此,当向函数传递二维数组时,声明相应参数的以下选项是等效的:

void Foo(T a[N][M]);
void Foo(T a[][M]);
void Foo(T(*a)[M]);

这意味着二维数组的外维度丢失了,必须作为单独的参数传递。

通过使用别名,您可以实现更简洁的二维数组声明语法。

using I4 = int[4];
I4 b[2];

这是一回事。

int b[2][4];

二维数组的初始化方式如下:

int b[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};

如果只需要保证默认初始化,可以使用空的初始化列表{}。初始化列表的大小只能根据外部大小来确定。

int b[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
int b[][] = {{1, 2, 3, 4}, {5, 6, 7, 8}};

你可以获取指向二维数组的指针:

T a[N][M];
T(*pa)[N][M] = &a;

你还可以获取引用。以下是使用二维数组引用的示例。

template
void Print2dArray(T(&a)[N][M])
{
    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < M; ++j)
        {
            std::cout << a[i][j] << ' ';
        }
        std::cout << '\n';
    }
}
// ...
int b[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
Print2dArray(b);

结论:

1 2 3 4
5 6 7 8

二维数组非常适合表示数学矩阵。在声明中

T mtx[N][M];

N可以解释为矩阵的行数M和列数。那么,mtx[i][j]它是位于i第 n 行和j第 n 列交点处的矩阵元素,mtx[i]这是一个大小为 n 的数组M,表示i矩阵的第 n 行。因此,这样的矩阵在内存中是逐行排列的。然而,在数学中,通常从 1 而不是 0 开始对行和列进行编号。

6. 动态数组

C++ 没有“动态数组”类型。它只有用于创建和删除动态数组的操作符,这些操作符通过指向数组开头的指针来访问(类似于完全归约)。此类数组的大小必须单独存储。最好将动态数组封装在 C++ 类中。


6.1 创建和删除动态数组

如果存在T某种类型的n变量,其值可以在程序执行期间确定,则该指令

T *pa = new T[n];

在动态内存中创建一个数组。变量的类型n必须强制转换为`int` std::size_t,其值可以为零。存储数组所需的内存大小(即 `int` n*sizeof(T))受平台和编译器的限制。变量 `i`pa指向数组的第一个元素。

如果类型T是平凡的,则元素将具有随机值;否则,将使用默认构造函数初始化元素。

C++11 引入了使用初始化列表的功能。

int *pa = new int[n]{1, 2, 3, 4};

如果初始化器的数量大于数组的大小,则多余的初始化器不会被使用(如果n编译时已知数组大小,编译器可能会报错)。如果数组的大小大于初始化器的数量,则保证对剩余元素(包括平凡类型)调用默认构造函数。因此,通过指定一个空的初始化器列表,我们可以保证对平凡类型数组的所有元素调用默认构造函数。

运算符new[]首先为整个数组分配内存。如果分配成功,则(如果T类型非平凡或存在初始化列表)会从零开始,为每个数组元素调用构造函数。如果任何构造函数抛出异常,则会按照构造函数调用的相反顺序,为所有已创建的数组元素调用析构函数,然后释放已分配的内存。如果请求无法满足,标准内存分配函数会抛出异常std::bad_alloc

动态数组通过运算符删除delete[],该运算符应用于运算符返回的指针new[]

delete[] pa;

在这种情况下,如果在创建数组时使用了构造函数,则会按照与构造函数调用相反的顺序对数组的所有元素调用析构函数(析构函数不应该抛出异常),然后释放已分配的内存。

在其他方面pa,运算符返回的指针new[]仅仅是指向数组开头的指针。它不能(至少在法律上)用于获取数组的大小;数组的大小必须单独存储。因此,范围数组不能与动态数组一起使用。C for/C++ 中的指针支持索引器(内置运算符 `\n` []),因此访问动态数组的元素与访问常规数组的元素相同;索引没有有效性检查。

6.2 动态数组和智能指针

标准智能指针std::unique_ptr<>可用于管理动态数组的生命周期。它有一个针对数组的部分特化(参见第 7 节),该特化重载了 `std::remove` 运算符而不是 `std::remove`和 ` []std:: remove` 运算符,并且使用 `std::remove` 运算符作为默认删除器。以下是一个示例:->*delete[]

int n = 100;
std::unique_ptr aptr(new int[n]);
for (int i = 0; i < n; ++i)
{
    aptr[i] = i;
}

此支持并不完整:未存储有关数组大小的信息,因此无法控制索引的正确性,不支持标准容器和范围的接口for

在 C++14 中,可以std::unique_ptr<>使用以下方法创建动态数组并初始化一个实例std::make_unique<>

auto aptr = std::make_unique(n);

这样可以保证数组元素默认被初始化,包括简单类型。

智能指针std::shared_ptr<>支持仅在 C++17 中出现,而std::make_shared<>该特化的使用仅在 C++20 中出现。

作为使用智能指针的替代方案,我们可以推荐std::vector<>……


6.3 多维动态阵列

动态数组不能是多维的,这意味着表达式 `a + b` (其中 `a`和new T[n][m]`b` 的值都在运行时确定)是无效的。但是,我们可以创建一个动态数组,其中每个元素都是一个内置数组,其大小在编译时已知。如果表达式 `a + b` 可以用于确定数组大小,则以下语句可以创建这样一个数组:

T(*pa)[M] = new T[n][M];

该运算符new[]返回指向数组的指针。该数组的元素将通过表达式访问pa[i][j],该表达式本身pa[i]又是一个M类型为的元素数组T

使用别名可以实现更简洁的语法。

using I4 = int[4];
I4 *pa = new I4[n];

利用运算符重载,[]可以轻松创建一个将数据存储在一维数组中,但仍提供多维数组接口的类。以下是一个极其简化的矩阵类示例。

template
class MatrixView
{
    T * const m_Buff;
    int const m_RowCount;
    int const m_ColCount;
public:
    MatrixView(T* buff, int rowCount, int colCount)
        : m_Buff(buff)
        , m_RowCount(rowCount)
        , m_ColCount(colCount)
    {}
    T *operator[](int rowInd) const
    {
        return m_Buff + rowInd * m_ColCount;
    }
};
template
class DynBuffer // buffer owner
{
    T* const m_Buff;
protected:
    T* Buff() const { return m_Buff; };
    DynBuffer(int length) : m_Buff(new T[length]{}) {}
    ~DynBuffer() { delete[] m_Buff; }
    DynBuffer(const DynBuffer&) = delete;
    DynBuffer& operator=(const DynBuffer&) = delete;
};
template
class MatrixSimple
    : DynBuffer, public MatrixView
{
    using Buff = DynBuffer;
    using View = MatrixView;
public:
    MatrixSimple(int rowCount, int colCount)
        : Buff(rowCount * colCount)
        , View(Buff::Buff(), rowCount, colCount)
    {}
};

以下是一个使用示例:

MatrixSimple mtx(3, 3);
mtx[1][2] = 42;

更高级的矩阵类可能会使用一个特殊的嵌套代理类来表示行,例如RowProxy`Match`,索引器会返回该类的实例。这样的类可以控制索引值,提供成员函数 `getIndex()`、`getIndex() begin()`end()等。类似的解决方案也可用于列。

7. 在模板中使用数组

数组类型可以用作模板参数,并用于类模板的特殊化。

你可以为数组定义一个类模板的部分特化,而无需指定数组大小——也就是说,可以针对“一般”数组进行特化。为此,请使用 `<array>` 作为特化类型T[]。当然,你也可以为具有给定大小的数组定义部分特化。以下是一个示例。

template 
struct U
{
    const char* Tag() const { return "primary"; }
};
template 
struct U
{
    const char* Tag() const { return "pointer"; }
};
template 
struct U
{
    const char* Tag() const { return "array"; }
};
template 
structs U
{
    const char* Tag() const { return "array[N]"; }
};
U u1;
U u2;
U u3;
U u4;
std::cout << u1.Tag() << ' ' << u2.Tag() << ' ' << u3.Tag() ' ' << u4.Tag();

结论:

主指针数组[N]


在标准库中,智能指针std::unique_ptr<>std::shared_ptr<>数组的部分特化用于管理动态数组的生命周期,更多详情请参见第 6.2 节。

对于使用数组作为模板参数的编程模板,标准库(头文件<type_traits>)提供了几个类型属性:std::is_array<>`int`、std::extent<>`int`、`int` std::rank<>、`int` 。以下是它们的使用示例(这些示例利用了std::remove_extent<>C++17 中引入的用后缀_v代替成员的功能value):

std::cout <<
    std::is_array_v << ' ' <<
    std::is_array_v << ' ' <<
    std::is_array_v << ' ' <<
    std::is_array_v << '\n';
std::cout <<
    std::extent_v << ' ' <<
    std::extent_v << ' ' <<
    std::extent_v << ' ' <<
    std::extent_v << '\n';
std::cout <<
    std::rank_v << ' ' <<
    std::rank_v << ' ' <<
    std::rank_v << ' ' <<
    std::rank_v << '\n';

结论:

1 1 1 0
0 4 8 0
1 1 2 0

作为这些类型属性的实际应用示例,以下是std::make_unique<>数组函数模板重载版本的简化定义(参见第 6.2 节):

template  && extent_v == 0, int> s = 0>
unique_ptr make_unique(size_t size) {
    using elem_t = remove_extent_t; // тип элемента массива
    return unique_ptr(new elem_t[size]{});
}

函数模板不支持部分特化,因此使用了一种称为模板禁用的技术。对于任何类型与 `T` 不同的模板参数,该模板将被禁用,这意味着它不会被实例化。T[]相应地,其他类型模板参数的重载版本std::make_unique<>也会被禁用T[]


8. 数组的标准替代方案

标准库提供了一些类(或者更确切地说是类模板),建议使用这些类来代替数组。

建议使用 `Array` 模板而不是内置数组std::array<>。)该模板是对内置数组的对象包装;它有两个模板参数:元素类型和大小。大小必须在编译时确定,但与内置数组不同的是,它可以为零。以下是一个示例:

std::array a{1, 2, 3, 4};

此模板支持索引器和传统标准容器接口。

for (int i = 0; i < a.size(); ++i)
{
    std::cout << a[i] << ' ';
}
for (auto it = a.begin(); it != a.end(); ++it)
{
    std::cout << *it << ' ';
}
for (auto t : a)
{
    std::cout << t << ' ';
}

建议使用此std::vector<>模式。这种模式对程序员来说是众所周知的,并且在文献中也有详细描述(标准容器#1),因此无需赘述。

此外,还有一个比较特殊且不太流行的模板std::valarray<>。它允许你模拟多维数组的接口。

posted @ 2025-12-27 12:15  gccbuaa  阅读(3)  评论(0)    收藏  举报