• 文章
  • 偏好标准库解决方案而非手动编写
发布
2009年5月10日

偏好标准库解决方案而非手动编写的模仿者

评分:3.8/5 (36 票)
*****
诚然,市面上有许多面向初学者的教程。本文的目的不是提供又一个教程。 相反,其目的是论证为什么初学者应该偏好标准库容器和算法,而不是手动编写的模仿者。实际上,我们在编写面向对象的 C++ 代码时,都应该偏好标准库解决方案。

目标读者是任何正在学习 C++ 并对语言语法有基本了解的人,特别是关于结构体和类的实例化,以及函数的声明和调用。

首先,让我总结一下在开发早期学习标准库容器和算法的优势。
1) 您将避免在编写 for 循环和 if 语句时出现许多常见错误,程序中出现恼人缺陷的可能性会大大降低。

2) 很有可能标准库算法比您自己编写的循环或函数更快。这些算法是由非常聪明的人编写的,他们对编译器和语言本身的内部工作原理有更多的了解。

3) 使用现成的构建块将使您能够更快地构建程序。不要重新发明轮子,学会使用构建块来实现您的目标。您将能花更多时间研究和实现程序的实际需求。

4) 您将避免棘手的内存管理陷阱。虽然最终每个人都必须学习内存管理,但让标准库容器为您处理这些问题,可以让您在不必担心管理动态内存的情况下启动并运行一些程序。内存管理是一个复杂的主题,可能会让初学者不知所措。一开始,尽量少做这方面的事情。

首先声明,本网站确实有相当不错的 C++ 语言以及标准库的教程。这些示例通常是完整的,因此您可以在自己的编译器中编译和执行它们。找到一个好的编译器并学会使用它的调试器是学习 C++ 的重要部分。通过调试器单步执行程序是分析程序并从中学习的好方法。我正在发布的示例已使用 Microsoft Visual C++ Express 2008 编译。
http://www.microsoft.com/express/vc/
现在,让我们先看一些例子。 在我看来,学习的最佳方式是分析现有代码并编写新的测试代码来测试现有的构建块。

如果您知道如何调用函数,那么您也知道如何使用标准库算法。不要被“算法”这个词吓倒,认为它们对初学者来说太复杂了。考虑 std::count。在这里,我们使用 std::count 来计算数组中 1 的数量。它是一个模板函数,所以 std::count 可以与数组或像 deque 和 vector 这样的标准库容器一起使用。由于它是一个模板函数,它还可以与自定义容器和迭代器一起使用。在这种情况下,指向数组第一个元素的指针是开始迭代器,而指向数组末尾之后一个位置的指针是结束迭代器(请记住数组是零基的)。

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
#include "stdafx.h"
#include <deque>
#include <iostream>
#include <algorithm>

void countExample()
{
    int myArray[10] = { 1, 2, 1, 3, 1, 4, 1, 5, 1, 6 };
    std::cout << "myArray contains " << std::count(myArray, myArray + 10, 1) << " 1s" << std::endl;

    std::deque<int> myDeque(myArray, myArray + 10);
    std::cout << "myDeque contains " << std::count(myDeque.begin(), myDeque.end(), 1) << " 1s" << std::endl;
}
void incorrectCustomCounter()
{
    int myArray[10] = { 1, 2, 1, 3, 1, 4, 1, 5, 1, 6 };

    // Manually search for 1s in the deque.  Can you spot the errors in the loop?
    int count(0);
    for(int i = 1; i <= 10; ++i)
    {
        if(myArray<i> = 1)
        {
            ++count;
        }
    }
    std::cout << "myDeque contains " << count << " 1s" << std::endl;
}
int main()
{
    countExample();
    incorrectCustomCounter();
    return 0;
}


你能发现上面例子中的错误吗?那个程序实际上在我的电脑上运行了,尽管 CustomCounter 产生了一个不正确的结果。程序本可以崩溃的。如果我只是使用了 std::count,不仅代码看起来更漂亮,而且还会正确工作。此外,使用 std::count 和调用函数一样简单。没有理由编写自己的计数函数。std::count 还提供了一个接受谓词的版本。一旦您学会了如何编写谓词,std::count 就会变得更加有用。现在您可以进一步扩展算法的功能!
https://cplusplus.net.cn/reference/algorithm/count/
https://cplusplus.net.cn/reference/algorithm/count_if/
现在让我们看一个更复杂的例子。我注意到许多初学者发帖询问关于简单的数据库任务,例如管理学生或客户记录。这个例子构建了一个学生记录的动态数组,并展示了如何使用标准库算法以很少的努力和很少的用户定义循环来操作、排序和搜索记录。

它还展示了您作为初学者如何编写一个测试应用程序来学习算法和容器的工作原理。

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#include "stdafx.h"
#include <deque>
#include <iostream>
#include <algorithm>
#include <string>

class Student
{
public:
    //constructors
    Student(std::string firstName, std::string lastName, std::string hometown, unsigned int id) 
        : firstName_(firstName), lastName_(lastName), hometown_(hometown), id_(id)  {}
    Student() : firstName_(""), lastName_(""), hometown_(""), id_(0) {}

    //destructor
    ~Student() {}

    // accessor functions
    std::string getHometown() const { return hometown_; }
    std::string getFirstName() const { return firstName_; }
    std::string getLastName() const { return lastName_; }
    unsigned int getId() const { return id_; };

    // mutator functions.
    void addSubject(const std::string& subject) { classes_.push_back(subject); }
    void setFirstName(std::string& name) { firstName_ = name; }
    void setlastName(std::string& name) { lastName_ = name; }
    void setHometown(std::string& name) { hometown_ = name; }
    void setId(std::string& name) { firstName_ = name; }

    // overloaded << so that we can output directly to a stream
    friend std::ostream& operator<<(std::ostream& os, const Student& s)
    {
        return os << s.getFirstName() << "\t\t" << s.getLastName() << "\t\t" 
                  << s.getHometown() << "\t" << s.getId(); 
    }

    // This is an overloaded relational operator.  It is needed to allow arrays of 
    // Student objects to be sorted using std::sort.  This is called during the
    // operation so that each object in the array can be compared against one another.
    bool operator<(const Student& rhs)
    {
        return (id_ < rhs.id_);
    }

    // declare friends
    friend struct PrintSubjects;
    friend struct IsTakingCalculus;

private:
    std::string firstName_;
    std::string lastName_;
    std::string hometown_;
    unsigned int id_;
    std::deque<std::string> classes_;

};


    // printing functor for use with std::foreach.  It works because operator<< is overloaded
    // for the student class.  
    struct SendToStream 
    {
        void operator() (const Student& s) { std::cout << s << std::endl; }
    } StsFtr;

    struct PrintSubjects 
    {
        void operator() (const Student& s) 
        { 
            std::cout << "\n" << s << "\n";
            std::deque<std::string>::const_iterator pos = s.classes_.begin();
            for(; pos < s.classes_.end(); ++pos)
            {
                std::cout << *pos << "   ";  // 3 spaces
            }
            std::cout << "\n";
        }
    } PsFtr;


    // used with count_if to count the number of students from san diego.  
    struct IsFromSanDiego 
    {
        bool operator() (const Student& s) { return (s.getHometown() == "San Diego, CA"); }
    } IfSdFtr;

    // used with count_if to count the number of students taking calculus
    struct IsTakingCalculus 
    {
        bool operator() (const Student& s) 
        { 
            return (std::find(s.classes_.begin(), s.classes_.end(), "calculus") != s.classes_.end()); 
        }
    } ItcFtr;

typedef std::deque<Student> Students;

int main()
{
    //Build an array of students.
    Students theStudents;

    // construct 10 students
    theStudents.push_back(Student("Sandra", "Fox", "San Diego, CA", 12111));
    theStudents.push_back(Student("Warren", "Pierce", "Fairbanks, AK", 12112));
    theStudents.push_back(Student("Dan", "Wright", "St. Louis, MO", 12113));
    theStudents.push_back(Student("Amelia", "Timlin", "Erie, PA", 24312));
    theStudents.push_back(Student("Anne", "Bradley", "Boston, MA", 24315));
    theStudents.push_back(Student("Mike", "Harding", "San Diego, CA", 24316));
    theStudents.push_back(Student("Sandra", "Brown", "Boston, MA", 38125));
    theStudents.push_back(Student("Melissa", "Turner", "Boston, MA", 38126));
    theStudents.push_back(Student("Jack", "Turner", "San Diego, CA", 12114));
    theStudents.push_back(Student("Sandra", "Rice", "St. Louis, MO", 24317));

    // print students in the original order
    std::cout << "\nPrint the students in the original order. " << std::endl;
    std::for_each(theStudents.begin(), theStudents.end(), StsFtr);

    // Use std algorithms to collect and display student metrics.
    Students::iterator pos;

    // print the student with the largest student id
    std::cout << "\nPrint the student with the largest id. " << std::endl;
    if( (pos = std::max_element(theStudents.begin(), theStudents.end())) != theStudents.end())
        StsFtr(*pos);

    // print the student with the smallest student id
    std::cout << "\nPrint the student with the smallest id. " << std::endl;
    if( (pos = std::min_element(theStudents.begin(), theStudents.end())) != theStudents.end())
        StsFtr(*pos);

    // sort the students by student id.  The operator< for the student uses the id so
    // we don't need to use a functor.
    std::cout << "\nSort the student by their student id. " << std::endl;
    std::sort(theStudents.begin(), theStudents.end());
    std::for_each(theStudents.begin(), theStudents.end(), StsFtr);

    // reverse the order
    std::cout << "\nReverse the order of elements within the container" << std::endl;
    std::reverse(theStudents.begin(), theStudents.end());
    std::for_each(theStudents.begin(), theStudents.end(), StsFtr);

    // shuffle the array into a random order
    std::cout << "\nShuffle the container " << std::endl;
    std::random_shuffle(theStudents.begin(), theStudents.end());
    std::for_each(theStudents.begin(), theStudents.end(), StsFtr);

    // add some subjects to the student classes
    std::string subjectsArray[7] = { "calculus", "physics", "philosophy", "history", "biology", "grammar", "spanish" };
    for(pos = theStudents.begin(); pos < theStudents.end(); ++pos)
    {
        // add three random subjects to each student object
        pos->addSubject(subjectsArray[1]);
        pos->addSubject(subjectsArray[3]);
        pos->addSubject(subjectsArray[5]);
        std::random_shuffle(subjectsArray, subjectsArray + 5);
    }
    std::for_each(theStudents.begin(), theStudents.end(), PsFtr);

    // Try using find and count functions using a predicate that searches for subjects contained 
    // in student objects.
    std::cout << std::count_if(theStudents.begin(), theStudents.end(), ItcFtr)
              << " are taking calculus.\n";

    std::cout << std::count_if(theStudents.begin(), theStudents.end(), IfSdFtr)
              << " are from San Diego, CA.\n";
    return 0;
}


您会注意到该示例中有几点。首先,我不需要直接动态分配内存。因此,该程序中没有内存泄漏的风险。我没有错误地删除内存、留下任何悬空的对象引用或使用未初始化的指针。其次,它包含非常少的用户定义的 for 循环来收集指标、排序或打印数据。

虽然本网站为每个算法提供了文档,但将一些内容汇总到一个示例中以了解它们如何协同工作是很有益的。所以试试吧!将其复制并粘贴到一个项目中,然后自己尝试调整它。如果您写了任何有用的改编,请随意发布,直到该帖子被存档。

参考文献
https://cplusplus.net.cn
https://cplusplus.net.cn/reference/std/
https://cplusplus.net.cn/reference/algorithm/
https://cplusplus.net.cn/reference/stl/
https://cplusplus.net.cn/reference/string/
《Effective STL, 50 Specific Ways to Improve Your Use of the Standard Template Library》,Scott Meyers
《The C++ Standard Library, A Tutorial and Reference》,Nicolai M. Josuttis
感谢到目前为止的反馈。我受字符数限制,例子最终有点长。我会考虑大家的意见,并在今晚发布一个更新,其中包含一些额外的技巧。目前,我在 operator< 上方添加了一个注释。我注意到我忘记为它写注释了。
我给例子添加了一个注释,解释了用于排序的 operator< 的作用。Duos 提议添加一些关于仿函数的信息。我决定要真正解释好仿函数,还需要另一篇文章。实际上,关于仿函数可以写很多文章。所以,让我们保持简单,解释与本文相关的基本概念。

首先,仿函数是定义了 operator() 的类,可以作为标准库算法所需的、以及其他东西的回调使用。在我的例子中,我使用仿函数作为回调来定制标准库算法。这是我写过的比较容易的一个。
1
2
3
4
5
6
7
8
9
10
struct IsFromSanDiego 
    {
        bool operator() (const Student& s) { return (s.getHometown() == "San Diego, CA"); }
    } IfSdFtr;

// calls IfSdFtr(*studentIterator) on every student object in the range.  If the
// student is from san diego, true is returned and std::count_if increments a counter.  
// IfSdFtr is an instance of the struct and is passed to count_if as if it were a pointer to a function.
std::cout << std::count_if(theStudents.begin(), theStudents.end(), IfSdFtr)
              << " are from San Diego, CA.\n";


请注意,仿函数是使用 struct 关键字而不是 class 来编写的。这是很常见的。编写仿函数应该很简单,并且由于成员默认是 public 的,所以写成 struct 比写成 class 要容易得多。此外,标准库 `` 头文件中的基类仿函数也是用 struct 编写的。


这里有几篇我在学习仿函数时觉得有用的文章。
http://en.wikipedia.org/wiki/Function_object

这个很有趣,因为它允许您通过简单地否定一个现有的仿函数来实现另一个仿函数。它展示了为什么将回调设计成仿函数比简单地使用全局或静态成员函数更有用。您不能将“!isOdd”作为 count_if 的第三个参数传递,因为这是非法语法。通过继承内置的一元仿函数,您可以利用语言内置的一些其他有用的工具,例如 not1。一开始,很容易养成将谓词或回调写成全局函数的习惯,因为一开始这似乎更容易。如果您养成了编写仿函数的习惯,您会做得更好,尽管回报可能起初并不明显!
https://cplusplus.net.cn/reference/std/functional/not1/