• 文章
  • 使用序列容器提高类型安全性和安全性
发布
2010年3月14日 (最后更新: 2011年7月13日)

使用序列容器提高类型安全性和安全性

评分: 3.2/5 (13 票)
*****
组织结构
第一部分) 将数组传递给函数

第二部分) 从函数返回数组

如果您在理解任何示例时遇到困难,请考虑回顾数组和模板教程。对于模板教程,您只需要阅读关于函数模板的第一部分。

http://www.cplusplus.com/doc/tutorial/arrays/
http://www.cplusplus.com/doc/tutorial/templates/
术语
使用“数组”一词会立即导致一些原因的混淆。

1) 其他语言内置了智能数组类型,其工作方式与 C/C++ 数组不同。术语“数组”在许多字典中都有定义,因此存在一个数组的广义概念,这在讨论 C++ 或其他语言定义的特定类型的数组时会导致混淆。

2) C++ 标准将 std::vector 描述为序列容器,但 C++ 程序员通常称 std::vector 为动态数组。事实上,任何提供随机访问的标准序列容器都可以放入“数组”一词的更一般的定义中。

例如,考虑这些定义:
dictionary.com
计算机。 一组相关的 [相关] 数据元素,每个元素通常由一个或多个下标标识。

Merriam Webster
(1): 按行和列排列的若干数学元素 (2): 一种数据结构,其中相似的数据元素排列在表中 b: 一系列按类别大小顺序排列的统计数据

当我使用“数组”一词时,我指的是字典中更通用的定义。当引用 C++ 标准 8.3.4 节描述的“数据结构”时,我将使用“C 数组”一词。以下示例展示了一个 C 数组的示例。这种数据结构存在于 C 语言中,并且必须得到 C++ 编译器的支持。我将使用大量示例来解释为什么有时最好考虑使用标准序列容器之一。

1
2
3
const int SIZE(5);
int data[SIZE];
std::generate(data, data + SIZE, rand);


第一部分 - 将数组传递给函数
编译并执行程序。它包含一个缺陷,在您分析输出时会显现出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

void printArray(int data[])
{
    for(int i = 0, length = sizeof(data); i < length; ++i)
    {
        std::cout << data[i] << ' ';
    }
    std::cout << std::endl;
}

int main()
{
    int data[] = { 5, 7, 8, 9, 1, 2 };
    printArray(data);
    return 0;
}


您将看到只打印了 C 数组的前 4 个元素。 sizeof(data) 的调用返回值为 4!这恰好是指向打印数组的指针的大小。这有几方面的影响。首先,数组没有被复制。指向数组第一个元素的指针被复制了。C 数组没有复制构造函数、赋值运算符或功能接口。在以下示例中,您将看到使用 C++ 标准模板库提供的动态序列容器 std::vector、std::deque 和 std::list 的示例。这不是一个完整的容器教程,但它们被用来展示对现有有缺陷程序的改进的灵活性。

让我们看另一个例子。在这个例子中,我创建了多个重载的 printArray 函数,以便展示多种解决方案。然后,我将分析每个解决方案,并解释它们的优缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>
#include <vector>
#include <deque>
#include <list>

// Method 1: works but very little security.  It is impossible to validate
// the inputs since the size of data still cannot be validated. If length is too large
// undefined behavior will occur.
void printArray(int data[], int length)
{
    for(int i(0); i < length; ++i)
    {
        std::cout << data<i> << ' ';
    }
    std::cout << std::endl;
}

// Method 2: Type safe and more generic.  Works with any container that supports forward iterators.
// Limitation - cannot validate iterators so caller could pass null or invalid pointers.  Typesafe - won't
// allow you to pass inconsistent iterator types.  Allows you to pass any valid range of a container.
template <class ForwardIteratorType> 
void printArray(ForwardIteratorType begin, ForwardIteratorType end)
{
    while(begin != end)
    {
        std::cout << *begin << ' ';
        ++begin;
    }
    std::cout << std::endl;
}

// Method 3 - This implementation is as typesafe and secure as you can get but
// does not allow a subrange since the entire container is expected.  It could
// be useful if you want that extra security and know that you want to operate
// on the entire container.
template <class ContainerType> 
void printArray(const ContainerType& container)
{
    ContainerType::const_iterator current(container.begin()), end(container.end());
    for( ; 
        current != end; 
        ++current)
    {
        std::cout << *current << ' ';
    }
    std::cout << std::endl;
}

int main()
{
    // Method 1.
    const int LENGTH(6);
    int data[LENGTH] = { 5, 7, 8, 9, 1, 2 };
    printArray(data, LENGTH);

    // Method 2.
    printArray(data, data + LENGTH);
    std::vector<int> vData(data, data + LENGTH);
    printArray(vData.begin(), vData.end());
    std::list<int> lData(data, data + LENGTH);
    printArray(lData.begin(), lData.end());
    std::deque<int> dData(data, data + LENGTH);
    printArray(dData.begin(), dData.end());
    // won't compile if caller accidentally mixes iterator types.
    //printArray(dData.begin(), vData.end());

    // method 3.
    printArray(vData);
    printArray(dData);
    printArray(lData);
	return 0;
}


方法 2 是独一无二的,因为它允许您指定数组的任何范围,而方法 1 和 2 完成了打印整个容器的相同目标。如果这正是您的意图,那么我认为方法 3 是最好的。它最安全,类型最安全。调用者指定无效参数的可能性非常小。空容器不会导致任何问题。该函数根本不会打印任何值。

需要注意的是,C 数组不能通过方法 3 传递。方法 3 要求使用容器,例如 std::vector。C 数组是 C 语言的遗留物,没有功能接口。如果您处理的是 C 数组,则需要使用方法 1 或 2。我确定还有其他方法,但这取决于您来确定哪种方法最适合您的项目。

人们可以制作数百个示例程序来进一步证明这些观点,但我会将它留给读者复制程序并构建其他类型的示例。模板的美妙之处在于它减少了重复的编程任务。定义一次函数,这样就可以多次调用该函数,每次指定不同的类型。这仅仅是确保类型支持函数最低要求的问题。方法 3 的 printArray 函数要求 ContainerType 具有 begin() 和 end() 成员函数,它们返回前向迭代器,并且容器内的对象是支持 operator<< 函数的类的实例。operator<< 也可以为用户定义的类型定义,因此方法 3 不仅限于内置类型容器。
第二部分 - 从函数返回数组
下面是一个包含从函数返回数组的两个典型问题的示例。根据记录,我认为从函数返回数组没有必要。将函数的结果返回似乎很自然,但并非必要。您可以使用指针或引用通过 out 参数向函数提供数据。

以下程序使用 MS Visual Studio C++ Express 2008 产生此输出。
13 8 9 10 11 12
-858993460 -858993460 -858993460 -858993460 -858993460 3537572
41 18467 6334 26500 19169 15724
41 18467 6334 26500 19169 15724
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <algorithm>
#include <iostream>

// Prints out array elements. Method 2 from PART I.
template <class ForwardIteratorType> 
void printArray(ForwardIteratorType begin, ForwardIteratorType end)
{
    while(begin != end)
    {
        std::cout << *begin << ' ';
        ++begin;
    }
    std::cout << std::endl;
}

// This function is a poor design which will lead to undefined behavior when the caller
// tries to use the pointer that is returned.  data is allocated on the stack and destroyed
// after the function returns.  The pointer to the memory is returned but it is a dangling
// pointer to memory that has already been released.
{
    int data[6] = { 13, 8, 9, 10, 11, 12 };
    int* pointer = data;
    printArray(pointer, pointer + 6);
    return pointer;
}

// The *& symbol means reference to a pointer so that modification of the array 
// results in modification of lotteryNumbers back in main.  In this case the pointer
// updated back in main is valid but the caller has to remember to release the memory
// at some point.  Therefore this approach is error prone.
void generateArray(int *&array, int length)
{
    int* pointer = new int[length];
    // std::generate requires the <algorithm> header
    std::generate(pointer, pointer + length, rand);
    printArray(pointer, pointer + length);
    array = pointer;
}

int main()
{
    int* lotteryNumbers = generateArray();
    printArray(lotteryNumbers, lotteryNumbers + 6);

    const int LENGTH(6);
    generateArray(lotteryNumbers, LENGTH);
    printArray(lotteryNumbers, lotteryNumbers + 6);
    delete lotteryNumbers;
    return 0;
}


第一次调用 printArray 发生在返回值的 generateArray 版本中。那时,名为 data 的数组是有效的,并且自从在函数内创建以来,它已从堆栈内存中分配。一旦 generateArray 返回,内存就会返回到堆栈,供程序用于其他目的。因此,返回到 main 的指针指向可以也将会被覆盖的内存,并且第二行输出是垃圾。该行为未定义。无法预测此类程序的行为。我所见的输出可能不是您在使用另一编译器和/或运行时环境时看到的输出。

同一个 generateArray 版本还有另一个问题。该函数只能返回一个值。即使数组是使用堆内存正确构造的,main 如何知道数组的大小?在这种情况下,编写这两个函数的程序员进行了编码假设,这是一个糟糕的设计。

请注意,还有一个 generateArray 版本,它接受两个参数并且返回类型为 void。第一个参数是指向指针的引用,以便 main 的 lotteryNumbers 指针被修改。第二个参数是长度,我要求调用者提供。尽管函数可以成功完成任务,但这是最好的方法吗?在复杂的大型应用程序中,内存泄漏可能导致严重问题,而您可能很难自己管理内存。

我认为我们可以做得更好。一个出现的问题是,为什么您会想要一个构建数组的函数?您可以轻松地在原地实例化一个数组。让我创建一个函数来读取控制台输入,并为用户填充数组。下面的示例允许函数构建数组,而调用者不必担心内存泄漏或堆栈与堆内存分配。有很多方法可以做到这一点。在这种情况下,我选择允许调用者传递任何大小的数组,函数将简单地向其添加元素。它可以是空的,但不必如此。std::vector 正在管理内存,因此当 main 函数退出时,它会被销毁,而程序员不必担心垃圾回收。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <vector>
#include <iostream>
#include <limits>

// Prints out array elements. Method 2 from PART I.
template <class ForwardIteratorType> 
void printArray(ForwardIteratorType begin, ForwardIteratorType end)
{
    while(begin != end)
    {
        std::cout << *begin << ' ';
        ++begin;
    }
    std::cout << std::endl;
}

// The caller must decide whether to pass an empty container.  This function will 
// add to it.  
void readScores(std::vector<int>& container)
{
    std::cout << "Type the list of scores followed by a non-numeric character and press enter when finished. " 
              << "For instance (22 25 26 f <enter> " << std::endl;
    int temp(0);
    while(std::cin >> temp)
    {
        container.push_back(temp);
    }
    // clear and discard any leftover data from the input stream.
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

int main()
{
    std::vector<int> scores; // uninitialized.  Let readScores fill it.
    readScores(scores);
    printArray(scores.begin(), scores.end());
    return 0;
}


这次我选择不使 readScores 成为模板函数。它不必如此,而且我想让示例保持相当简单。它可以被修改得更通用。如果您敢于尝试,并在运行时观察会发生什么。关键是函数实际上不需要构建数组。在函数内构建数组并返回它很棘手。您将不得不处理垃圾回收,或者按值返回 std 容器,这可能导致不必要的复制构造。

不幸的是,按值返回意味着至少您很可能需要一个赋值,该赋值将导致调用者的 vector 分配内存来保存复制的数据。最好的方法确实是像我在前面的示例中那样,通过引用传递并具有 void 返回类型。该示例也更灵活,因为调用者可以决定是向现有数组添加元素还是填充新数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::vector<int> container readScores()
{
    std::vector<int> container;
    std::cout << "Type the list of scores followed by a non-numeric character and press enter when finished. " 
              << "For instance (22 25 26 f <enter> " << std::endl;
    int temp(0);
    while(std::cin >> temp)
    {
        container.push_back(temp);
    }
    // clear and discard any leftover data from the input stream.
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    
    // return by value. Container will be destroyed but data will be copied into callers vector instance which could result
    // in additional memory allocation.  
    return container;
}


我最后说,还有其他方法可以完成这些类型的编程任务,我想鼓励任何人发布一些使用模板函数或 boost 库的示例。