• 文章
  • 偏好標準庫解決方案而非手動編寫
釋出
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.tw/reference/algorithm/count/
https://cplusplus.tw/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.tw
https://cplusplus.tw/reference/std/
https://cplusplus.tw/reference/algorithm/
https://cplusplus.tw/reference/stl/
https://cplusplus.tw/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.tw/reference/std/functional/not1/