面向懒惰程序员的 C++20 教程(七)
二十、模板
我会不会写一个函数或类,一个带int s,另一个带string s,另一个带double s?听起来不像是懒惰!这一章使我们能够使用模板编写一次*。*
*## 功能模板
回忆一下第八章中的这个交换int的函数,为了方便起见,在这里重新命名。
void mySwap
(int& arg1, int& arg2)
{
int temp = arg2; arg2 = arg1; arg1 = temp;
}
对int来说没问题,但是如果我想要double s 呢?长鼻怪?还是混合?下面是修复方法,这样我就可以把int换成int,int换成double,长鼻怪换成 snarks,任何东西,只要 C++ 知道如何使用=:
template <typename T, typename U>
void mySwap (T& arg1, U& arg2)
{
T temp = arg2; arg2 = arg1; arg1 = temp;
}
这是一个函数模板:它本身不是一个函数,而是关于一旦知道我们想要什么类型,如何让成为一个函数的指令。
最上面一行告诉编译器这将是一个模板,我们调用了要交换的类型,T和U。T和U都是一种空白,当我们决定用一个实际的类型来填充它。例 20-1 说明了它的用法。
// Utterly useless program that uses a function template
// -- from _C++20 for Lazy Programmers_
template <typename T, typename U>
void mySwap (T& arg1, U& arg2)
{
T temp = arg2; arg2 = arg1; arg1 = temp;
}
int main ()
{
int i = 10 , j = 20 ;
double m = 0.5, n = 1.5;
mySwap (i, j);
mySwap (m, n);
mySwap (i, n); // You'll get a warning abt loss of data
// from mixing ints and doubles, but it'll work
return 0;
}
Example 20-1Using the mySwap function template
编译器不会创建任何 mySwap函数,直到它到达第mySwap (i, j);行。然后它注意到i和j是int的,所以它用int替换模板中的T和U,并创建一个带两个int的mySwap函数
在下一行,它为double生成一个mySwap,之后,它又生成一个带int和double的。
我把功能模板放在 main 上面。如果编译器在使用它之前看不到它,它就不会知道如何创建函数——它还没有读过指令。所以函数模板放在程序的开头,或者放在一个.h文件中代替声明。
总结如何将函数转换为函数模板:
-
放
template<typename这个,typename那个...>在前面。 -
**把要替换的类型改成 T 或者 const T &(或者 U/const U &之类的)。**如果是在 C++ 会隐式调用某个东西的复制构造器的地方——没有
&–const T&的返回类型或参数会防止不必要的复制。 -
将新的函数模板放在声明它的地方。
防错法
- **链接,编译器说找不到函数,但是你可以在程序后面或者另一个里面看到。cpp 文件。**见步骤 3。
其他可能的问题:
- 在不该的时候把
int转换成T/const T&。假设我有这个代码来搜索一个int数组:
bool contains (int array[], int howMany, int item)
{
for (int i = 0; i < howMany; ++i)
if (array[i] == item)
return true;
return false;
}
and convert it to
template <typename T>
bool contains (T array[], const T& howMany, const T& item)
{
for (int i = 0; i < howMany; ++i)
if (array[i] == item)
return true;
return false;
}
如果是字符串数组,howMany为const string&就没有意义了!它应该仍然是一个int。
- 对类型
T使用不正确的运算符。也许你发送给mySwap的东西没有=定义;或者=没有做你想做的事情(比如,T是一个指针,你想复制内容而不是做指针赋值)。通常的解决办法是只对有意义的东西使用模板。下一节会有所帮助。
函数模板的概念(可选)
我对概念持观望态度。它们确实给了程序员一些帮助,尤其是让错误消息更加清晰。如果您的编译器还不支持它们,或者您还没有准备好投入时间,那么现在跳过它们当然是可以的。
现在我已经说过了…考虑一下这段代码:
class A {};
class B {};
int main ()
{
A a; B b;
mySwap (a, b);
return 0;
}
我们会得到一个错误消息,说我们没有办法将一个B分配给一个A。
从 C++20 开始,我们可以告诉编译器,我们对mySwap的参数有一定的期望(例如 20-2 ,所以它可以在进入函数体之前就知道它是否可以工作。
#include <concepts>
...
template <typename T, typename U>
requires std::swappable_with<T&,U&>
void mySwap (T& t, U& u)
{
T temp = t; t = u; u = temp;
}
Example 20-2Converting mySwap to use concepts
在<concepts>头文件中的概念——类型限制——告诉模板它可以接受什么类型。在这种情况下,mySwap需要一对可以互相交换的类型。(&是因为我们想要交换的是事物本身,而不是副本,如参数表所示。)当我们给它一个A和一个B时,它们不满足swappable_with约束,所以编译器甚至不会尝试构建mySwap。它只是给出一个类似“相关的约束没有得到满足”的错误消息,这是真的。
那好一点了。当我们在本章后面将模板应用于类时,它会变得更加有用。
表 20-1 列出了一些你可能会用到的来自<concepts>的概念。其他包含文件中有更多,特别是<algorithm>和<range>(见第二十三章)。其余的<concepts>的概念,见(撰写时) en.cppreference.com/w/cpp/concepts 。
表 20-1
使用模板最简单和最有用的概念。所有这些都在std::名称空间中
防错法
你可以打印出一个概念是否适用于类型,这在你很难弄清楚为什么有概念的代码不起作用时很方便(例如 20-3 )。
cout << "Is int same as double? " << same_as<int, double>
<< "\nIs B derived from A? " << derived_from<B, A>
<< "\nIs char8_t integral? " << integral<char8_t>
<< "\nIs double floating-pt? " << floating_point<double>
<< '\n';
Example 20-3Printing concepts as they relate to various types
这将为不适用的概念打印 0,为适用的概念打印 1。
潜在的问题包括:
- 当你使用概念时,你会看到红色的曲线,但实际上似乎没什么不对。可能会好;Visual Studio 的编辑,在写作的时候,不能可靠地识别概念。编译一下就知道了,看看是不是真的错了。
Exercises
对于每个练习,如果您选择涵盖概念,请使用它们。
-
编写一个函数模板,它接受任何浮点数并返回其最高有效(最左边)位。例如,给它 678.9,它将返回 6。你可能想要
log10功能。 -
将示例 10-3-查找数组的最小值-转换为函数模板。
Vector 类
数组是个麻烦。你可以给一个数组一个-2000 的索引,它会很乐意给你一些愚蠢的东西。如果你声明一个数组包含 50 个元素,但是你决定要 51 个,那就太糟糕了。
我们可以通过创建一个名为Vector的行为更好的类似数组的类来解决这个问题(例子 20-4 , 20-5 )。
// Vector class: a variable-length array
// -- from _C++ for Lazy Programmers_
#ifndef VECTOR_H
#define VECTOR_H
class Vector
{
public:
class OutOfRange {}; // exception, for [] operators
Vector () { contents_ = new int[0]; howMany_ = 0; }
Vector (const Vector& other) { copy (other); }
~Vector() { if (contents_) delete [] contents_; }
Vector& operator= (const Vector& other);
bool operator== (const Vector& other) const;
unsigned int size () const { return howMany_; }
int operator[] (unsigned int index) const;
int& operator[] (unsigned int index);
void push_back (int newElement); // add newElement at the back
private:
int* contents_;
unsigned int howMany_;
void copy (const Vector& other);
};
#endif //VECTOR_H
Example 20-4vector.h, for a vector of ints
很像String,只是当然我用不了strcpy之类的。特别注意push_back。
// Vector class: a variable-length array of ints
// -- from _C++20 for Lazy Programmers_
#include "vector.h"
Vector& Vector::operator= (const Vector& other)
{
if (this == &other) return *this; // don't assign to self -- you'll // trash contents_
if (contents_) delete[] contents_; copy(other);
return *this;
}
bool Vector::operator== (const Vector& other) const
{
if (size() != other.size()) return false; // diff sizes => not equal
bool noDifferences = true;
// quit if you find a difference or run out of elements
for (unsigned int i = 0; i < size() && noDifferences; ++i)
if ((*this)[i] != other[i]) noDifferences = false;
return noDifferences;
}
int Vector::operator[] (unsigned int index) const
{
if (index >= size()) throw OutOfRange(); // don't allow out-of-range // access
else return contents_[index];
}
int& Vector::operator[] (unsigned int index)
{
if (index >= size()) throw OutOfRange(); // don't allow out-of-range // access
else return contents_[index];
}
// add newElement at the back
void Vector::push_back (int newElement)
{
int* newContents = new int[howMany_ + 1]; // make room for 1 more...
for (unsigned int i = 0; i < size(); ++i) // copy old elements into new // array...
newContents[i] = contents_[i];
newContents[howMany_] = newElement; // add the new element...
++howMany_; // remember we have 1 more...
delete[] contents_; // throw out old contents_
contents_ = newContents; // and keep new version
}
// Sort of like String::copy from Chapter 17, but without strcpy
void Vector::copy (const Vector& other)
{
// set howMany to other's size; allocate that much memory
contents_ = new int[howMany_ = other.size()];
// then copy the elements over
for (unsigned int i = 0; i < size(); ++i)
contents_[i] = other[i];
}
Example 20-5vector.cpp
示例 20-6 向您展示了如何使用它。
// Example with a Vector of int
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "vector.h"
using namespace std;
int main ()
{
Vector V;
for (int i = 1; i < 11; ++i) V.push_back (i);
cout << "Can you count to 10? The Count will be so proud!\n";
for (unsigned int i = 0; i < V.size(); ++i) cout << V[i] << ' ';
cout << '\n';
return 0;
}
Example 20-6Using Vector
所以它是安全的(如果我们给出了一个不好的索引,就会抛出一个异常),我们可以添加任意多的元素。
效率和 O 符号(可选)
在第二十二章,我们将有另一个元素容器,即“链表”这样我们就可以决定这个或那个任务需要哪个容器——为了练习 O 符号——让我们考虑一下Vector的成员函数的效率(时间需求)。(如果您跳过了 O 符号,请跳过这一小节。)
你可能需要时间自己决定这些函数在 O 符号中是什么。
好吧,你回来了。由operator=和复制构造器使用的Vector::copy中有一个循环,它迭代size ()次。push_back也有这样的循环。其他人只是有一些if的说法。表 20-2 显示了一些函数的效率,给定 N 为当前大小。
表 20-2
一些Vector功能所需的时间
功能
|
效率(时间要求)
|
| --- | --- |
| size | O(1) |
| operator[] | O(1) |
| operator= | O(N) |
| 复制构造器 | O(N) |
| push_back | O(N) |
底线是:如果你想对整个向量做些什么,所需的时间是 O(N)——没什么好惊讶的。如果你只是用一个元素做一些事情,所需时间是 O(1)—除了 push_back。这需要 O(N)时间,因为您必须将旧的contents_复制到新的内存块中。
好吧。总比没有灵活性好。也许有办法让它变得更快(见练习 3)。
Exercises
在下面,如果你没有做“效率和 O 符号”小节,就跳过每个小节中的 O 符号问题。
-
写
pop_back。它的时间要求是什么,用 O 表示法?如果不是 O(1),那就是你工作量太大了! -
(更难)重写
push_back,这样就不用每次添加新元素时都重新分配,而是为十个新元素分配足够的空间——并且只需要每十次重复一次。它改变了时间成本吗,用 O 表示?你认为值得做吗? -
(使用 move 语义)编写 move 构造器并为
Vector移动=,如果你愿意,可以对照我在书中的源代码中的解决方案进行检查。他们需要多长时间来记 O 符号? -
写一个类
Queue。它就像一个Stack,除了你从你添加物品的地方把物品从端对面的端拿走。所以它们出来的顺序和进去的一样。按照惯例,在一端“入队”,在另一端“出列”。
在 O 表示法中,入队和出列的时间是多少?
-
(使用 move 语义)编写 move 构造器并为
Queue移动=。他们需要多长时间来记 O 符号?
制作Vector模板
我是不是要写一个全新的类,取决于我是想要一个整数,字符串,还是 1960 年代的摇滚音乐家的向量?我是一个懒惰的程序员。我不可能做那件事。
进入类模板:本质上是一组制作类的指令,就像函数模板是制作函数的指令一样。
将Vector转换为一个并存储不同类型所需的更改有一个简短列表:
-
改变任何一个矢量声明来表示它是一个什么
Vector**的。**在例 20-7 中,Vector变成了Vector<int>。这是我们在该文件中所做的唯一的更改。 -
将
vector.cpp的内容放入vector.h**;**擦掉vector.cpp。
// Example with a Vector of int
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert>
#include "vector.h"
using namespace std;
int main ()
{
Vector<int> V; // Step #1: change declaration to say what it's a // Vector of
for (int i = 1; i < 11; ++i) V.push_back(i);
cout << "Can you count to 10? The Count will be so proud!\n";
for (unsigned int i = 0; i < V.size(); ++i) cout << V[i] << ' ';
cout << '\n';
return 0;
}
Example 20-7Example 20-6, updated to use a class template for Vector
这和“函数模板”部分是一样的:在你调用 push_back之前,和int s 一起工作的push_back版本是不存在的。在那一行,编译器需要知道如何创建函数,这意味着它需要函数模板的主体。所以主体必须在.h文件中。
-
将模板
<typename T>放在前面-
类定义
-
类定义之外的每个函数体
-
-
与函数模板一样,在适当的地方用 T 或 const T &替换 int。
-
用矢量替换矢量
-
当它是矢量的一部分::
-
在返回类型中,如在
Vector<T>& Vector<T>::operator= (const Vector& other); -
任何你不在班的时候,比如
Vector<T> merge (const Vector<T>& a, const Vector<T>& b); // not a member
-
如果你把Vector<T>放在太多的地方,没有人会开枪打你。但是它对构造器名不起作用。
让我们看看这给了我们什么(示例 20-8 )。
// Vector class: a variable-length array
// -- from _C++ for Lazy Programmers_
#ifndef VECTOR_H
#define VECTOR_H
template <typename T> // Step #3 (a): add template <typename T>
class Vector
{
public:
class OutOfRange {}; // exception, for [] operators
Vector () { contents_ = new T[0]; howMany_ = 0; }// #4: int -> T
Vector (const Vector& other) { copy (other); }
~Vector() { if (contents_) delete [] contents_; }
Vector& operator= (const Vector& other);
bool operator== (const Vector& other) const;
unsigned int size () const { return howMany_; }
const T& operator[] (unsigned int index) const;
// #4: int -> const T&
T& operator[] (unsigned int index); // #4: int& -> T&
void push_back (const T& newElement); // #4: int -> const T&
private:
T* contents_; 1800 // #4: int -> T
unsigned int howMany_;
void copy (const Vector& other);
};
// #2: move contents of vector.cpp into vector.h
// (still contained in #ifndef)
template <typename T> // #3b: add template <typename T>
Vector<T>& Vector<T>::operator= (const Vector& other)
// #5a: Vector:: -> Vector<T>::
// #5b: Vector& -> Vector<T>&
{
if (this == &other) return *this; // don't assign to self -- you'll trash contents
if (contents_) delete[] contents_; copy (other);
return *this;
}
...
#endif //VECTOR_H
Example 20-8Changing Vector to a class template. Along with Example 20-7, it’s in source code as 07-08-vectorTemplate
现在您可以将那个Vector用于您选择的基本类型。在示例 20-9 中,我们将它与string s 一起使用。
// Example with a Vector of strings and more
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert>
#include <string>
#include "vector.h"
using namespace std;
int main ()
{
// Setting up the band...
Vector<string> FabFour;
string names[] = { "John","Paul","George","Ringo" };
constexpr int NUM_BEATLES = 4;
for (int i = 0; i < NUM_BEATLES; ++i)
FabFour.push_back(names[i]);
// Printing them out...
cout << "The Fab Four: ";
for (int i = 0; i < NUM_BEATLES; ++i)
cout << FabFour[i] << ' ';
cout << endl;
// Ensuring other base types compile...
Vector<int> V; for (int i = 0; i < 10; ++i) V.push_back(i);
Vector<Vector<double>> G1, G2; assert(G1 == G2);
return 0;
}
Example 20-9Using the new Vector template from Example 20-8 with strings and more. In source code as 09-vectorTemplate
防错法
-
编译器说你没有写你的类模板的一些成员函数,你知道你写了。是不是所有东西都移入了
.h文件? -
编译器在你的变量声明中说,类模板不是类型。也许你停止了
<yourBaseType>。
Exercises
在这一章的结尾有进一步的相关练习;它们可以带概念使用,也可以不带概念使用。请查看该部分来尝试它们,尤其是涉及队列的部分。
-
将
Vector的push_back、copy和operator=(如果你知道的话,也可以移动函数)转换成使用Vector作为模板。我的解决方案在书的源代码中。 -
将第十七章练习中的
Point2D类改编成一个类模板。你现在可以拥有由doubles、ints、floats 或任何其他合理类型制造的Point2Ds。 -
重写上一章的
CardGroup作为Vector<Card>的子类。
类别模板的概念(可选)
让我们将这些代码添加到一个测试程序中,就像示例 20-9 中的那样。(本节没有编号的示例,但是片段收集在ch20的项目/文件夹vectorTemplateWConcepts中;请随意尝试,评论/取消评论,并尝试自己的方法。)
struct B { B() {}; }; // Two simple classes
struct A { A& operator= (const A&) = delete; };
Vector<A> As;
Vector<B> B1, B2;
看起来还好吗?编译器是这么认为的。但是随着我们对代码的深入研究
As.push_back (A ());
assert (B1 == B2);
它会发现问题。它不能添加一个A,因为push_back需要=,而A没有提供;它不能比较B1和B2,因为它需要==而B没有提供。
当我们试图声明没有提供它所需要的类时,它真的应该很快发现这个问题。我们可以用概念来实现。
要在类模板中使用概念,就像在函数模板中一样:在任何一个template <...>后面的行中放一个requires子句,如
template <typename T>
requires...
void Vector<T>::push_back ...
我们应该对Vector的基类型提出什么样的要求?我们需要=和==。这就要求T是assignable_from本身,也是equality_comparable。而且由于copy创建数组,T会需要一个默认的构造器,也就是必须是default_initializable。
我们可以在requires子句中使用&&、||和!以任意组合的方式组合概念。在我们的例子中,我们只需要&&:
template <typename T>
requires std::assignable_from<T&, T> && std::equality_comparable<T>
&& std::default_initializable<T>
void Vector<T>::push_back ...
我们每次写template <typename T>都要打太多了!我们可以用提供的零件制作我们的自己的概念,从而节省工作:
template <typename T>
concept VectorElement = std::assignable_from<T&, T> && std::equality_comparable<T> &&
std::default_initializable<T>;
...
template <typename T>
requires VectorElement<T>
void Vector<T>::push_back (const T& newElement) ...
还有一种减少打字的方法。如果你的概念只有一个类型参数,你可以把它放在typename的位置,作为一种速记。简短是好的。
template <VectorElement T>
void Vector<T>::push_back (const T& newElement) ...
当声明A或B的Vector时,错误信息将类似于“约束不满足”
如果您想要的概念不在包含文件中,您可以创建自己的概念。假设您想确保类型是可打印的。把你想做的事情放在requires之后的{}中——在这个例子中是{ out << t; },其中out是ostream的类型——在一个参数列表之后,该列表定义了你所引用的任何内容:
template <typename T>
concept Printable = requires (std::ostream& out, const T& t)
{
out << t;
};
关于概念还有很多事情要做,但是这应该可以处理你发现的大多数情况。
值得吗?
C++ 社区似乎对此很兴奋。你可以自己决定。
一副
我使用了pair——一个将任意两种类型捆绑到一个struct中的类模板——很多。在示例 20-10 中进行了描述和测试。
template <typename S, typename T>
struct pair
{
pair ();
pair (const S& s, const T& t);
pair (const pair& other);
// operators =, ==, !=, and others
S first;
T second;
};
...
int main ()
{
pair3 p (1, "C++20 for Lazy Programmers");
cout << "The number " << p.first << " C++ text EVER is "
<< p.second << "!\n";
return 0;
};
Example 20-10Using pair, here simplified from C++’s version in #include <utility>
本章末尾的练习就是一个有用的例子。
非类型模板参数
您也可以让模板参数成为一个值(例如 20-11 )。
template <int SIZE>
class Stack // Stack of chars with at most SIZE elements
{
public:
//...
bool full () const { return howMany_ >= SIZE; }
private:
char contents_[SIZE];
int howMany_;
};
int main ()
{
Stack<30> stack;
//...
return 0;
}
Example 20-11A class template that allows you to specify an integer argument
如练习 5 所示,这也很有用。
Exercises
在练习中,如果你已经涵盖了概念,就使用它们——无论如何都是好的。
-
将前面练习中的类
Queue转换为模板。 -
给
Vector增加一个print成员函数。 -
使用上一节中的
Queue类模板,创建一个子类PriorityQueue,其中每个条目都有一个附加的优先级。当您将一个新项目加入队列时,它会排在Queue中优先级较低的所有项目之前。你会想要pair。 -
(对于概念)编写函数模板
sqr对任何数值类型的值求平方。 -
Write a function template that takes an array and gets from the user each element of that array. Here’s the cool part: it asks for exactly the right number of elements. Here’s how:
template <typename T, int SIZE> ... void inputArray(T (&myArray)[SIZE])这样,函数不仅知道数组的类型,还知道它的大小。(在函数模板制作完成之前,&用来保存数组的大小信息)。 4
它只对静态数组有效,对动态数组或作为参数传入的数组无效。
(从 C++20 开始,
istream& operator>>就是这样保证它读取的输入不会超过给定的char数组所能存储的。) -
(硬)做一个类模板
BigInteger,充当任意大小的整数。让模板参数成为您在BigInteger中想要的字节数(unsigned chars)或位数。支持所有合理的算术运算符和流 I/O。
#include <vector>
我又对你隐瞒了一些事情;我不能再那样做了。C++ 在#include <vector>中已经有了一个std::vector类模板。它没有我们的酷,因为它缺少练习 2 中给出的print功能,但我们不能拥有一切。 5
std:: swap和std:: pair也是内置的,在#include <utility>。
pair s 和(从 C++20 开始)vector s 可以声明为constexpr,如果那是你的事情的话。我们将在第二十六章进一步探索constexpr s。
这是我们的赋值类型返回&而不是const &的一个原因:如果你的operator=返回const &,这个概念认为你的类是不可赋值的。我们不去思考为什么。
2
Visual Studio 或 g++ 编写时不支持。
3
编译器应该能够推断出你想要一对 int 和 string。如果没有,可以告诉它:pair p...;。
4
还记得当你把一个数组传递给一个函数时,它并不关注数组的大小吗?即使你这样写——void f (int a[SIZE])——它的意思和void f (int* a)一样。这被称为“数组到指针衰减”
在这里使用&意味着“我没有把它复制到一个指针上;我真的要在函数中引入一个这样大小的数组。”这使得模板机制能够确定如何将该模板与数组参数匹配,从而识别出SIZE,以便我们可以在函数中使用它。
5
实际上,STL 有很好的理由不使用print函数:你希望如何分隔它?逗号?空格?是要[]的围着,< >的,还是()【的?STL 的创建者可以解决这个问题,他们已经解决了;我们会在第二十三章看到。
*
二十一、虚函数和多重继承
虚函数和多重继承在我写的大多数类中都没有出现——但是当我需要它们的时候,我就需要它们。
虚函数,加上:移动具有可移动父类和类成员的函数
多态性本质上是用同一个词来表示不同的东西。我们一直都是这么做的。考虑操作符+。我们用它来添加int s 或double s 或string s。这些在概念上是相似的,但它们由机器来完成是非常不同的。
另一个例子可能是函数start,它可以应用于汽车、飞机或割草机。在每种情况下,功能的主体将是不同的(转动钥匙,通过飞行检查,拉动曲柄绳)。但是名字是一样的。
考虑一下我们想要在计算机屏幕上绘制二维形状的类:圆形、矩形、正方形和大块文本。这些有很多共同点:位置、颜色以及被绘制和移动的能力。我们可以把那些共同的品质放在一个父类Shape(图 21-1 ,例 21-1 )。
图 21-1
一个等级制度
//Shape class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef SHAPE_H
#define SHAPE_H
#include "SSDL.h"
struct Point2D // Life would be easier if this were a full-fledged class
{ // with operators +, =, etc. . . . but that
int x_, y_; // was left as an exercise
};
class Shape
{
public:
Shape (int x = 0, int y = 0, const char* text = "");
Shape (const Shape& other);
~Shape() { if (description_) delete[] description_; }
Shape& operator= (const Shape& s);
// Color
void setColor(const SSDL_Color& c) { color_ = c; }
const SSDL_Color& color () const { return color_; }
// Access functions
const Point2D& location () const { return location_; }
const char* description() const { return description_; }
// Drawing
void drawAux() const;
void draw () const
{
SSDL_SetRenderDrawColor (color ()); drawAux();
}
// Moving
void moveBy (int deltaX, int deltaY)
{
moveTo (location_.x_ + deltaX, location_.y_ + deltaY);
}
void moveTo (int x, int y)
{
location_.x_ = x; // Point2D::operator= would help here!
location_.y_ = y;
}
private:
Point2D location_;
SSDL_Color color_;
char* description_; // Using char* not std::string helps // illustrate how this chapter
// affects dynamic memory
void copy(const char*); // Used for copying descriptions
};
#endif //SHAPE_H
Example 21-1shape.h
注意函数draw:它使用SSDL_SetRenderDrawColor来告诉 SSDL 开始使用Shape的颜色,然后调用drawAux,一个助手函数来完成实际的绘制。对于Circle s 来说drawAux将是不同的(这里它将使用SSDL_RenderDrawCircle),Text s(这里它将调用SSDL_RenderText),等等。Circle的drawAux如示例 21-2 所示。
//Circle class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef CIRCLE_H
#define CIRCLE_H
#include "shape.h"
class Circle: public Shape
{
public:
Circle () : radius_ (0) {}
Circle (const Circle& c) : Shape(c), radius_ (c.radius()) {}
Circle (int x, int y, int theRadius, const char* txt="") :
Shape (x, y, txt), radius_ (theRadius)
{
}
Circle& operator= (const Circle& c)
{
Shape::operator= (c); radius_ = c.radius (); return *this;
}
int radius () const { return radius_; }
void drawAux() const
{
SSDL_RenderDrawCircle (location().x_, location().y_, radius());
}
private:
int radius_;
};
#endif //CIRCLE_H
Example 21-2circle.h
我们现在可以试着画一个Circle:
Circle c (10,10,5); c.draw (); // draw a Circle of radius 5 at (10,10)...但是行不通;编译器抱怨在Shape::draw中调用的Shape::drawAux没有被编写。没错。Circle的drawAux写了,但是Shape::draw对Circle的功能一无所知。
我们需要的是一种方法,让Shape::draw调用drawAux的右版本:Circle s 用Circle::drawAux,Text s 用Text::drawAux等等。
这是修复:虚函数。
class Shape
{
public:
...
virtual void drawAux ();
...
};
Example 21-3The Shape class, with a virtual function
在示例 21-3 中,我们告诉Shape类,“无论何时调用drawAux,使用子类的版本,如果有的话。”
Circle需要被告知它的drawAux正在覆盖一个虚函数,所以我们也这样做(例子 21-4 )。
class Circle: public Shape
{
...
void drawAux () const override;1
...
};
Example 21-4The Circle class, with the override specifier
在幕后
以前,一个给定类的对象只存储数据成员(图 21-2 )。它没有将成员函数和对象一起存储在内存中,因为这样会浪费空间。
图 21-2
一个Circle对象,在虚函数之前
但是现在对象也包含了任何虚函数的地址,所以它记住了要调用哪个版本(图 21-3 )。
图 21-4
例 21-6 中奥林匹克标志程序的输出
图 21-3
一个Circle对象,使用虚函数
我们增加了一些开销:在每个Circle中为drawAux函数增加了一个额外的指针。但是开销很小,没什么好担心的。
纯虚函数和抽象基类
下一个问题:不管怎样,我们怎么写Shape::draw()?
除非我们指定它是Shape的什么种类——除非它是Circle或其他子类——否则这个问题没有答案。所以我们将采取简单的方法:我们不会为Shape编写它,而是告诉编译器,“你不能有一个Shape那只是一个Shape,这个函数就是为什么。”
class Shape
{
public:
...
virtual void drawAux ()=0;
...
};
Example 21-5The Shape class, with a pure virtual function. This makes Shape an “abstract” class
通过添加=0,我们将drawAux变成了一个纯虚拟的函数,将Shape变成了一个抽象类,这意味着你不能用它来声明变量:
Shape myShape; // Nope, can't do this, the compiler will stop you
Circle myCircle; // No problem: it's a shape, but it's also a Circle,
// and we can drawAux Circles
为什么虚函数经常意味着使用指针
我们可能想对Shape s 做些什么:将它们放入一个向量,并对向量中的每个Shape做些什么(比如draw()):
// Program to show, and move, the Olympic symbol
// It uses Circle, and a subclass of Shape called Text
// -- from _C++20 for Lazy Programmers_
#include <vector>
#include "circle.h"
#include "text.h"
int main (int argc, char** argv)
{
SSDL_SetWindowSize (500, 300); // make smaller window
// Create Olympic symbol
std::vector<Shape> olympicSymbol; // No, this isn’t going to work...
constexpr int RADIUS = 50;
// consisting of five circles
olympicSymbol.push_back (Circle ( 50, 50, RADIUS));
olympicSymbol.push_back (Circle (150, 50, RADIUS));
olympicSymbol.push_back (Circle (250, 50, RADIUS));
olympicSymbol.push_back (Circle (100, 100, RADIUS));
olympicSymbol.push_back (Circle (200, 100, RADIUS));
// plus a label
olympicSymbol.push_back (Text (150,150,"Games of the Olympiad"));
// color those circles (and the label)
SSDL_Color olympicColors[] = { BLUE,
SSDL_CreateColor (0, 255, 255), // yellow
BLACK, GREEN, RED, BLACK };
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i].setColor (olympicColors [i]);
// do a game loop
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (WHITE); // clear the screen
// draw all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i].draw ();
// move all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i].moveBy (1, 1);
}
return 0;
}
这很有意义:创建一系列Shape的序列,然后绘制它们。但是行不通。一个原因是Shape现在是一个抽象类;既然你不能创造出仅仅是Shape的东西,你当然也不能创造出它们的vector。
另一个原因是,例如,olympicSymbol[0]有足够的空间来存储单个Shape。这意味着它有空间容纳一个color_、一个location_、一个description_和一个指向drawAux的指针。你会把Circle的radius_存放在哪里?Text对象的contents_?没有地方了!
为了解决这个问题,我们需要动态内存。是的,我知道;懒惰的程序员避免使用动态内存,因为它容易出错,也更难编写。但有时你必须拥有它。在这种情况下,当您使用new创建一个Circle时,它将分配它需要的数量。
和我们之前使用动态内存的方式不一样。然后,我们想要一个数组,所以我们用[] : char* str = new char [someSize],和delete [] str来清理。这一次,当我们分配一个Shape时,我们分配一个??。所以我们省略了[]的:new Circle而不是new Circle [...];delete不是delete []。我们将在下一章中得到更多分配/释放单个元素的练习。
// Program to show, and move, the Olympic symbol
// It uses Circle, and a subclass of Shape called Text
// -- from _C++20 for Lazy Programmers_
#include <vector>
#include "circle.h"
#include "text.h"
int main (int argc, char** argv)
{
SSDL_SetWindowSize (500, 300); // make smaller window
// Create Olympic symbol
std::vector<Shape*> olympicSymbol;
constexpr int RADIUS = 50;
// consisting of five circles
olympicSymbol.push_back (new Circle ( 50, 50, RADIUS));
olympicSymbol.push_back (new Circle (150, 50, RADIUS));
olympicSymbol.push_back (new Circle (250, 50, RADIUS));
olympicSymbol.push_back (new Circle (100, 100, RADIUS));
olympicSymbol.push_back (new Circle (200, 100, RADIUS));
// plus a label
olympicSymbol.push_back (new Text (150,150,"Games of the Olympiad"));
// color those circles (and the label)
SSDL_Color olympicColors[] = { BLUE,
SSDL_CreateColor (0, 255, 255), // yellow
BLACK, GREEN, RED, BLACK };
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
(*olympicSymbol[i]).setColor (olympicColors [i]);
// do a game loop
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (WHITE); //clear the screen
// draw all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
(*olympicSymbol[i]).draw ();
// move all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
(*olympicSymbol[i]).moveBy (1, 1);
}
// done with our dynamic memory -- throw it back!
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
delete olympicSymbol [i];
return 0;
}
Example 21-6A program that successfully uses a vector of Shapes to display and move a complex symbol. Output is in Figure 21-4
在这段代码中,我们使用的vector不是Shape的,而是Shape*的。然后,当我们使用new来创建一个Circle或Text时,它可以为我们获得一个适合该子类大小的内存块。
由于olympicSymbol[i]是一个指针,我们说的不是olympicSymbol[i].draw ()而是(*olympicSymbol[i]).draw ()。 2
最后,和使用动态内存时一样,当我们完成时,我们用delete把内存扔回去。
为了确保所有的数据都被返回,我们需要下一部分。
虚拟析构函数
考虑前面例子中的Text对象,它包含“奥林匹克运动会”
// Text class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef TEXT_H
#define TEXT_H
#include "shape.h"
class Text : public Shape
{
public:
Text(const char* txt = "") { copy(txt); }
Text(const Text& other) : Shape(other) { copy(other.contents_); }
Text(int x, int y, const char* txt = "") : Shape(x, y)
{
copy(txt);
}
~Text() { if (contents_) delete contents_; } // Not quite right...
Text& operator= (const Text& other);
const char* contents () const { return contents_; }
void drawAux () const override
{
SSDL_RenderText(contents_, location().x_, location().y_);
}
private:
char* contents_;
void copy(const char* txt); // used for copying contents
};
#endif //TEXT_H
Example 21-7text.h
它使用动态内存来分配它的字符数组。自然地,当我们完成后,我们需要把它扔回去。但是main中应该这样做的语句——for (int i = 0; i < olympicSymbol.size(); ++i) delete olympicSymbol [i];——却没有这样做。olympicSymbol [i]是一个Shape的指针,不是一个Text;所以只会删除属于Shape的东西。它不知道Text的contents_。
解决方法还是使用知道的函数版本:?? 的版本。我们通过让Shape的析构函数virtual和Text的析构函数override来做到这一点:
virtual Shape::~Shape () { if (description_) delete[] description_; }
Text::~Text () override { if (contents_) delete contents_; }
现在,当调用Shape上的析构函数时,Shape将知道调用哪个版本。如果是一个Text,被调用的析构函数将是Text::~Text ()——当它完成时,调用父类Shape的析构函数,因为析构函数无论是否是虚拟的都会这样做。
当你建立一个继承层次时,很容易忘记你是否在这个类或那个类中使用了虚函数。当你写父类的时候,没有办法知道没有后代会使用动态内存。因此,如果有任何人动态分配某个子类的任何成员的可能性,你怎么知道呢?–将析构函数设为虚拟的更安全。
Golden Rule of Virtual Functions
(普通版本)如果你在一个类层次结构中使用虚函数,让析构函数是虚拟的。
(更强版本)既然你不知道写类的时候写子类的人会做什么……那就把所有析构函数都虚拟化吧。句号。
我在Text中使用了char* contents_,在Shape中使用了char* description_,以清楚地表明我们需要析构函数。但是如果它们是string的,同样的事情也会发生:当你删除一个指向Shape的指针,而这个指针实际上是一个指向Text的指针时,delete不会知道它实际上是一个Text,所以Text的析构函数——不管是我们写的还是编译器的默认——都不会被调用,也没有什么能告诉Text的成员contents_删除它的内存。所以你仍然需要给Shape一个虚拟析构函数:
virtual Shape::~Shape () { }
使用可移动的父类和类成员移动函数(可选)
如果你为Shape及其子类编写 move 构造器,你可能会发现Shape的工作很好,但是当你试图从Text使用它时:
Text::Text (Text&& other) noexcept: Shape (other) // Nope, not working right...
{
...
}
它调用Shape的常规复制构造器,尽管Shape也是可移动的(有移动函数)。
当您使用Text的 move 构造器时,这是因为 C++ 认为被复制的东西是一个“r 值”,一个您可以安全地用完、破坏等等的东西,因为它不会被再次使用,所以我们窃取它的内存是完全安全的。
但是当它在构造器中的时候,我们需要保留它直到我们完成,所以它的 r 值被去掉了。所以当我们调用Shape (other),时,它不知道使用 move 构造器。
C++ 的解决方法是强制它返回到调用的 r 值。std::move(other)将other变回 r 值:
#include <utility> // for std::move
...
Text::Text (Text&& other) noexcept : Shape (std::move
(other))
{
contents_ = other.contents_; other.contents_ = nullptr;
}
移动=也是如此:在将other交给Shape的移动=之前,我们需要将它变回 r 值。
Text& Text::operator= (Text&& other) noexcept
{
Shape::operator= (std::move(other));
//...
return *this;
}
对于使用移动语义的数据成员也应该这样做。例如,如果我们让Text::contents_被声明为String(它也使用移动语义)而不是char*,Text的移动函数在访问它时将需要使用std::move(示例 21-8 ,突出显示)。
Text::Text (Text&& other) noexcept :
Shape (std::move(other)), contents_(std::move (other.contents_))
{
}
Text& Text::operator= (Text&& other) noexcept
{
Shape::operator= (std::move (other));
contents_ = std::move (other.contents_);
return *this;
}
Example 21-8How to call move functions of parents and movable data members: use std::move
防错法
-
编译器说你的子类的 覆盖 **函数与基类中的任何一个都不匹配。**您可能已经将
const从函数头中去掉,或者拼写了不同的函数名,或者给了它一个稍微不同的参数列表。 -
编译器说你的子类是抽象的,但它不包含纯虚函数。比如,
Circle myCircle;
可能会抱怨Circle很抽象——但是你根本没有在里面放任何虚函数!
你可能忘了覆盖父类的纯虚函数。这也使得子类变得抽象。
或者您可能忘记了将相应的子类函数设置为override,并且它的头文件与父类函数的头文件不完全匹配(请参见反屏蔽一节中的前一条)。解决方法:添加override。
Exercises
图 21-6
从Model和Object继承ModelObject
图 21-5
通电练习,展示“虫洞”动画
- 在这个简单的射击游戏中,你可以点击鼠标击中一个目标。启动的类型——闪速启动、巨型启动或虫洞——在你点击其中一个时会给出不同的点数并显示不同的动画(见图 21-5 )。
在本章的示例代码部分,您会发现一个部分编写的程序,用于拍摄加电。它使用了Shape层次结构。为了让它工作,你需要修改main.cpp来使用指针,并在Powerup类层次结构的正确位置添加virtual和override。我建议将Powerup抽象化,原因和Shape一样。
-
(使用移动语义)修改练习 1 中的类以使用移动语义。
-
在第十九章的
CardGroup类层次中,给每个可能需要它们的子类添加函数isLegalToAdd和isLegalToRemove。(例如,只有当单元格为空时,Cell::isLegalToAdd才返回 true,只有当单元格不为空时,Cell::isLegalToRemove才返回 true。)让
CardGroup的addCardLegally调用isLegalToAdd,使用虚函数,因此它调用适当的子类版本。其他职业都不应该有自己的addCardLegally。测试以确保实际调用了正确的函数。你可能需要一些试抓块。
-
(更大的项目)扩展练习 2,写一个非图形化的自由细胞游戏。你应该有一个
CardGroup(实际上是CardGroup*)的数组或向量,包括FreecellPile、Cell和Foundations。让用户指定将卡片从哪个CardGroup移出或移入哪个CardGroup(比如说,“F1”代表基础 1,“P2”代表堆 2)。选择的CardGroup知道,基于它是什么子类,使用isLegalToAdd或isLegalToRemove的哪个版本:CardGroup* from = askUserToPickCardGroup(); if ((*from).isLegalToRemove()) // can we take card from top? ...
多重继承(可选)
考虑三维图形程序的这两个类:
类别Object有位置、速度和加速度。这是为了研究运动定律。
类是具有物理外观的东西。它由三角形组成,这些三角形组合在一起形成一个明显的固体。
你能拥有不是 ?? 的 ?? 吗?当然可以。你可能有一个其他格式的模型,不使用三角形——也许是一个球体。
你能拥有不是 ?? 的 ?? 吗?当然可以。你可以使用 CAD/CAM 来设计制造产品。
所以一个Model不是一个Object,一个Object也不是一个Model……但是两者兼而有之是有道理的。我会把那个东西叫做ModelObject。
由于ModelObject是一个Model和一个Object,它继承了两者的特征:它将拥有类似于Object和矢量Triangle s 的位置、速度和加速度,加上来自Model的load和display函数。
我们可以使用公共或私有继承。public有道理:
class ModelObject: public Model, public Object
{
public:
ModelObject () {}
ModelObject (const ModelObject& other)
: Model (other), Object (other)
{
}
...
};
要调用父构造器,就像处理普通继承一样使用:;但是这一次,调用两个父构造器(或者使用它们的默认值)。
这不是经常需要的,但是当需要时,它很方便。
防错法
假设我们做一个角色扮演游戏。其中我们有类Player,有一个name_和一些hitPoints_。它还有一个成员函数takeAttack (int howMuch),可以减少一定数量的生命值。
我们创建两个子类,Fighter和Magician。一个Fighter有一个成员attack,带走一个Player,降低其生命值。A Magician有一个成员bespell,做魔法攻击。
但是有些游戏让你为你的角色制造混合职业。我们将使FighterMagician成为Fighter和Magician的子类。现在我们有了一个既能attack又能bespell的类。酷。
但是有一个问题。Fighter有成员hitPoints_和name_(继承自Player)。Magician还有hitPoints_和name_(继承自Player)。所以FighterMagician有两份hitPoints_和两份name_!
出于某种原因,这被称为“钻石问题”。也许图 21-7 会给我们一点线索?
图 21-7
多重遗传中的钻石问题
我们无法推理出解决这个问题的方法;C++ 将会帮助我们。它确实做到了:它让我们可以创建Fighter和Magician“虚拟”基类,本质上是说,“不要做那种额外复制公共祖父母成员的事情”:
class Fighter: virtual public Player ...
class Magician: virtual public Player ...
另一个问题:由于Fighter和Magician可能调用不同的Player构造器,这将导致歧义,FighterMagician必须明确声明它想要调用什么Player构造器,就像这样:
FighterMagician (const char* name) : Fighter (...some args...),
Magician (...some args...),
Player (name)
{
}
如果我们不指定,编译器将使用缺省值。
Exercises
-
编写反欺诈部分的类—
Player、Fighter、Magician和FighterMagician。因为你不是在做一个真正的游戏,为了简单起见,
attack和bespell都可以从对手的生命值中随机抽取数字。FighterMagician用attack还是bespell都没关系——从main中选一个作为它的战斗方式。现在让一个
Fighter(比方说)和一个FighterMagician对抗,看谁赢了这场比赛。 -
使用
Shape类,创建一个既有Shape又有location、virtual drawAux等的Composite类。)和一个Shape*的向量(所以可以用Circles、Texts,随便什么)。请确保您的Composite可以被创建、移动、显示并被正确地析构。如果您已经讨论了移动语义,那么编写移动构造器并移动=。一个棘手的问题是,
Composite有两种位置:从Shape继承的位置和它所有子组件的位置。让你的move函数更新所有位置。 -
如果你做了虚函数部分的练习 1,你可以用一个
PowerupSet类来扩展它,它既是一个Shape也是一个Powerup*的向量。确保你的PowerupSet可以被创建、绘制和动画化,并且被正确地析构。main.cpp中采用vector<Powerup>的函数(或类似的函数)应该改为采用PowerupSet。如果您已经讨论了 move 语义,那么编写 move 构造器并移动=。t 有两种位置:继承自 ?? 的位置和其子组件的位置。让你的
move函数更新所有位置。
编译器并不要求这样,但这是一个非常好的主意*。它会让编译器注意到父函数和子函数的拼写是否不同,或者参数列表或const规格是否略有不同。*
2
如果所有这些括号让你的小指感到疲劳,那么,在下一章中,我们有一个更简单的方法来写它。
二十二、链表
要向Vector添加一个元素,我们必须分配新的内存并复制现有的元素。那还不够快!这里有一个更新更快的存储方案。
什么是列表,为什么有列表
整个城市,一群超级英雄在等待。如果需要他们的权力,他们有一个通知对方的计划:每个人都有另一个人的电话号码,谁有另一个人的电话号码,直到名单上的最后一个人没有电话号码(见图 22-1 )。
图 22-1
我们的城市,三个超级英雄在一个链表中。百变少女第一,在 555-0169;有点能力的男生在 555-0145;神童在 555-0126
在计算机中,我们不用电话号码,而是用内存地址。这个数据结构包含每个人拥有的信息:
struct Superhero
{
std::string name_; // The Superhero's name
Superhero* next_; // The address of the next Superhero
};
记住Superhero*的意思是指向Superhero的指针——在计算机的内存中去哪里找一个Superhero,就像在第二十一章中Shape*的意思是去哪里找一个Shape。神童排在最后,她的下一个电话号码是“无”,所以我们将她的next_字段设置为nullptr。
假设我们想在列表中添加另一个英雄。简单快捷:给他列表中当前第一个的号码。他会把它放进他的领域。然后记住他的联系方式作为新的第一个号码(见图 22-2 )。
图 22-2
同样的列表在我们添加了喷虫人之后
更正式地说,算法是
create a new Superhero struct
put the name of the new person into the Superhero
put the pointer to the start of the list into the new Superhero, as "next"
set the start of the list to the address of the new Superhero
这里没有循环,所以比Vector::push_back快多了。
效率和 O 符号(可选)
在 O(1)处,List添加元素的方式比Vector的 O(N)有了很大的进步!
但是假设我们想要查看 indexth 元素(无论是什么索引)——也就是说,使用operator[]。我们如何做到这一点?对于Vector,它只是contents_[index]——没有循环,没有重复,因此是 O(1)。在这里,我们必须按顺序进行:
current position = start;
for j = 0; j < index; ++j
current position = the address of the next Superhero
if we go off the end of the list, throw an exception
return the name in the current position
这个是否有循环——它的时间要求是 O( index)。平均来说,这将是 O(N/2),或 O(N)。
*表 22-1 显示了你如何知道对于一个给定的任务,Vector和List哪个更好。如果你做了大量的查找(operator[]),那么Vector会更快。如果你添加了很多元素,List会更快。
表 22-1
一些Vector和List功能所需的时间
功能
|
效率(向量)
|
效率(列表)
|
| --- | --- | --- |
| operator[] | O(1) | O(N) |
| operator= | O(N) | O(N) |
| 复制构造器 | O(N) | O(N) |
| push_back | O(N) | 未写 |
| push_front | 未写 | O(1) |
我经常使用Vector,因为我发现我查看序列的次数比构建序列的次数多。如果序列很小,也没多大关系。如果它很大,我会更注意选择最快的。
启动链表模板
现在让我们开始写List。我们将放弃超级英雄的类比,将List作为模板(例如 22-1 )。
// class List: a linked list class
// from _C++20 for Lazy Programmers_
#ifndef LIST_H
#define LIST_H
#include <iostream>
template <typename T>
class List
{
public:
class Underflow {}; // Exception
List ();
List (const List <T>& other);
~List();
List& operator= (const List <T>& other);
bool operator== (const List <T>& other) const;
int size () const;
bool empty () const { return size() == 0; }
void push_front (const T& newElement); // add newElement at front
void pop_front (); // take one off the front
const T& front () const;
private:
struct Entry1
{
T data_;
Entry* next_;
};
Entry* start_; // Points to first element
};
#endif //LIST_H
Example 22-1The List class, first version
就像Vector,除了
-
我们用
push_front而不是push_back。 -
数据成员是不同的。
-
我漏掉了
operator[]。它的效率如此之低,以至于在抱怨了几年之后,我不在乎它是否效率低下;就让我用吧!我已经向社区低头,不再理会它。无论如何,我们将在下一章中找到更合适的方法来访问元素。
现在让我们从默认的构造器开始写一些函数。
List<T>::List ()
让默认值List为空是有意义的。我们如何指定一个列表是空的?按照惯例,当指针start_为nullptr时,这是正确的。你可以说它什么也没有指向——因为列表中什么也没有:
template <typename T>
class List
{
public:
List () { start_ = nullptr; }
...
};
void List<T>::push_front (const T& newElement);
当我们开始push_front的时候,我们有了原来的List,包含了start_和一个newElement。我们需要在前面添加newElement,如图 22-3 所示。
图 22-3
在添加新元素到List之前和之后
以下是实现这一点的方法:
create an Entry
put the newElement into its data field
put the old version of start into its next field
put the address of the new Entry into start
似乎很简单。这是代码:
template <typename T>
void List<T>::push_front (const T& newElement)
{
Entry* newEntry = new Entry; // create an Entry
(*newEntry).data_ = newElement;// put newElement in its data_ field
(*newEntry).next_ = start_; // put old version of start_
// in its next_ field
start_ = newEntry; // put address of new Entry
// into start_
}
让我们一行一行地追踪。
第一行Entry* newEntry = new Entry;,使用动态内存创建新的Entry。就像第二十一章中的新Shape一样,我们一次只分配一个Entry,而不是一个数组,所以我们不需要[]
在第二行,newEntry是新Entry的地址,所以*newEntry就是那个Entry本身。(*newEntry).name_故为其name_场。第三行类似。
第四行将newEntry的地址存储在List的start_字段中,这样我们就可以记住在哪里找到它。我们的新Entry现在指引我们去List的其他地方。如果List有元素,好;我们会见到他们的。如果List为空,那么指向列表其余部分的指针就是nullptr。也不错。我们会知道这就是结局。
void List<T>::pop_front ()
…去掉第一个元素的函数。这是我的第一次尝试。(为了简洁,我只展示了代码,但是当然我会先写算法。)
template <typename T>
void List<T>::pop_front ()
{
if (empty()) throw Underflow();
delete start_; // delete the item
start_ = (*start_).next_; // let start_ go on to the next
}
(和第二十一章一样,我们使用delete而不是delete []——因为我们使用了没有[]的new,分配的不是一个数组而是一个Entry。)
假设我们把杀虫剂从名单上去掉了。图 22-4 显示了我们从什么开始。
图 22-4
准备好pop_front
我会追踪这些步骤。是空的吗?没问题。现在我们删除start_所指向的,得到图 22-5 。
图 22-5
我们删除了pop_front中的元素。不,这是不对的…
然后我们访问(*start_).next_。但是start_所指向的已经被删除,不复存在。名单上其他人的地址都没了。撞车了。
也许我们可以用不同的顺序来做这件事——只有在我们确定已经完成之后才删除它们:
template <typename T>
void List<T>::pop_front ()
{
if (empty ()) throw Underflow ();
Entry* temp = start_; // store location of thing to delete
start_ = (*start_).next_; // let start_ = next thing after start_
delete temp; // delete the item
}
现在让我们看看情况如何。
我们检查空的情况。没问题。
我们设定temp等于start_(图 22-6 )…
图 22-6
开始pop_front(再次)
我们移动start_指向列表的其余部分(图 22-7 )…
图 22-7
将start_设置到它应该去的地方…
…我们删除不再需要的Entry(图 22-8 )。
图 22-8
现在可以正常工作了
列清单时,我总是画这些方框和箭头;没有它们,我必然会失去指针,跟随不好的指针,等等。所以我得到了以下黄金法则:
Golden Rule of Pointers
当改变或删除指针时,画出你正在做的事情的图表。
List<T>::~List ()
最终,我们不得不把所有这些都扔回去。
我可以写一个 while 循环来删除它们,制作一个图表来确保我不会丢失任何指针,但是我是一个懒惰的程序员。我已经有东西可以安全地扔回去了吗?确定,pop_front:
template <typename T>
List<T>::~List () { while (!empty()) pop_front(); }
完成了。
一点语法上的好处
因为使用 Shift 键,我的小指都快磨破了。幸运的是,C++ 提供了另一种写完全相同的东西的方法,更容易输入,也更容易阅读:
newEntry->next_; // means (*newEntry).next_;
这是我们新版本的push_front:
template <typename T>
void List<T>::push_front (const T& newElement)
{
Entry* newEntry = new Entry; // create an Entry
newEntry->data_ = newElement;// put newElement in its data_ field
newEntry->next_ = start_; // put old version of start_ in
// its next_ field
start_ = newEntry; // put address of new Entry
// into start_
}
更友好的语法:指针作为条件
我们经常需要这样的代码:if (next_ != nullptr)...或者while (next_ != nullptr) ...。
考虑 if 语句(以及 while 循环和 do-while)的条件是如何工作的。评估()之间的表达式。如果它的值为 0,那就意味着假;其他都是真的。
嗯,nullptr有点像0——至少,它的意思是“没有”什么都没有,假的,0,随便。所以你可以把if (next_ != nullptr)...写成
if (next_)...
“如果next_不是空的,如果有下一件事……”是这个条件在说什么。你觉得方便就用吧。
链表模板
示例 22-2 包含前面函数的完整版本,以及一些其他的。有的留着当练习。
还有一件事值得注意。考虑一下operator=。在复制另一个列表之前,我们必须用delete扔掉旧的内存。我们不是已经在析构函数里这么做了吗?是的,所以我们做了一个函数eraseAllElements,可以被operator= 和析构函数调用,供代码重用。
createEmptyList是代码重用的另一个实用函数。
// class List: a linked list class
// from _C++20 for Lazy Programmers_
#ifndef LIST_H
#define LIST_H
#include <iostream>
template <typename T>
class List
{
public:
class Underflow {}; // exception
List () { createEmptyList(); }
List (const List <T>& other) : List () { copy(other); }
~List() { eraseAllElements(); }
List& operator= (const List <T>& other)
{
eraseAllElements (); createEmptyList(); copy(other);
return *this;
}
bool operator== (const List <T>& other) const; // left as exercise
int size () const; // left as exercise
bool empty () const { return size() == 0; }
void push_front (const T& newElement); // add newElement at front
void pop_front (); // take one off the front
const T& front () const; // left as exercise
void print (std::ostream&) const; // left as exercise
private:
struct Entry
{
T data_;
Entry* next_;
};
Entry* start_; // points to first element
void copy(const List <T>& other); // copies other's entries
// into this List
void eraseAllElements (); // empties the list
void createEmptyList ()
{
start_ = nullptr; // the list is...nothing
}
};
template <typename T>
inline
std::ostream& operator<< (std::ostream& out, const List <T>& foo)
{
foo.print(out); return out;
}
template <typename T>
void List<T>::eraseAllElements () { while (!empty()) pop_front(); }
template <typename T>
void List<T>::push_front (const T& newElement)
{
Entry* newEntry = new Entry; // create an entry
newEntry->data_ = newElement;// set its data_ field to newElement
newEntry->next_ = start_; // set its next_ field to start_
start_ = newEntry; // make start_ point to new entry
}
template <typename T>
void List<T>::pop_front ()
{
if (empty ()) throw Underflow ();
Entry* temp = start_; // store location of thing to delete
start_ = start_->next_; // let start_ = next thing after start_
delete temp; // delete the item
}
template <typename T>
void List<T>::copy (const List <T>& other)
{
Entry* lastEntry = nullptr; // last thing we added to this list,
// as we go thru other list
Entry* otherP = other.start_; // where are we in the other list?
// while not done with other list...
// copy its next item into this list
while (otherP)
{
// make a new entry with current element from other;
// put it at end of our list (so far)
Entry* newEntry = new Entry;
newEntry->data_ = otherP->data_;
newEntry->next_ = nullptr;
// if list is empty, make it start_ with this new entry
// if not, make its previous Entry recognize new entry
// as what comes next
if (empty ()) start_ = newEntry;
else lastEntry->next_ = newEntry;
lastEntry = newEntry; // keep pointer for lastEntry updated
otherP = otherP->next_;// go on to next item in other list
}
}
...
#endif //LIST_H
Example 22-2list.h, containing the List class, some functions omitted. I invite the reader to do the exercises that follow before examining the completed solution in ch23’s project 1-2-lists
防错法
当指针出错时,它们真的会出错。你可能会遇到程序崩溃。
以下是最糟糕和最常见的指针相关错误:
- 崩溃,来自跟一个
nullptr:比如说myPointer是nullptr的时候说*myPointer。最好的预防措施:在你指向指针指向的地方之前(把*放在前面或者->放在后面),一定要检查
if (myPointer !=``nullptr
当涉及指针时,偏执是一件好事。
-
崩溃,因为使用了尚未初始化的指针。我的解决方案是总是初始化每个指针。如果不知道初始化成什么,就用
nullptr。然后if (myPointer != nullptr) ...(见上一段)将防止错误。 -
崩溃,来自于跟随一个指向已经被删除的东西的指针:正如我在前面的章节中所做的,跟踪代码对图做了什么是我所知道的最好的预防措施。一旦你有了几个你信任的函数,你就可以偷懒了,就像我对
eraseAllEntries一样;让像pop_front这样可信的函数来做可怕的工作。 -
程序陷入循环:
Entry* p = start_; while (p) { ... }
这里的问题是我忘记让 while 循环继续到下一个条目:
p = p->next_;
如果我把它写成 for 循环的形式,我就不太可能忘记:
for (Entry* p = start_; p != nullptr; p = p->next)...
Exercises
对于这些练习,从ch22/listExercisesCode中的项目开始。练习 1–6 在章节源代码(ch22/1-2-lists)中有答案。
自始至终,你都可以使用操作符->来保存你的手指输入。
-
写
List的成员函数front ()。 -
…和
size ()。你能让它在没有循环的情况下工作吗? -
…和
operator==。 -
…和
print。 -
通过给
Entry一个将next_字段设置为nullptr的构造器来稍微清理一下代码。这应该有助于防止忘记初始化的错误。 -
(需要移动构造器,移动
=)给List一个移动构造器,移动=。 -
(需要概念)用你的
List使用概念。确保你放在List里的任何东西都是可打印的,为了print。 -
(更难)添加一个数据成员
Entry::prev_,这样就可以向后遍历列表,添加List::rStart_,这样就知道从哪里开始了。还增加了List成员功能push_back、pop_back和back。 -
(更难)重写蒙大拿,增加一个新选项:撤销。为了支持它,你需要保存一个
List的移动,这样你就可以撤销你的上一个移动,上一个移动,等等,直到没有移动可以撤销。一招包含哪些信息?当新回合开始时,可以清空List。(为什么我们不用Vector?)
#include <list>
是的,链表类也是内置的。它比我的好:它有push_back、pop_back(见练习 8),还有很多其他你可以自己查找的函数。
所以这里有一个问题:如果你不能使用[],你如何用和来获取列表的元素呢?如果你不能,那就没用了!有一种方法:它被称为“迭代器”,将在下一章中介绍。
一个结构/类在另一个里面?没问题。但是该结构的数据成员是公共的(默认情况下)。这是安全风险吗?一点也不。都在 List 的私人部分。
*
二十三、标准模板库
是不是每个程序员都应该自己做向量,列表等等?哦,当然不是。所以前段时间,标准模板库(STL)被放到了标准里。在 STL 中,你会发现像list和vector这样的容器;string年代,正如我们已经使用它们一样;以及swap、find、copy等常用功能。
你还会发现对效率的过分强调。我说“烦人”是因为 STL 通过禁用低效的东西来提升效率。如果你想把operator[]和一个list一起用,那就算了;太慢了(O(N)时间,如果你做 O 记号的话)。如果你想要[],STL 的开发者认为你可以使用vector。他们是对的。但我还是会生气。
我们必须以某种方式获取列表的元素。怎么做?让我们现在就解决这个问题。
迭代程序
list的operator[]不存在。Entry年代是私有的。我们能做什么?
STL 的list类提供了迭代器——说明我们在list中的位置并可以遍历它。以下是您可以对它们做的事情:
// doSomethingTo each element of myList, from beginning to end
for (list<T>::iterator i = myList.begin
(); i != myList.end (); ++i)
doSomethingTo (*i);
像我们的Entry结构一样,iterator类型是list<T>的成员,但是是公开可用的。如图所示,它往往从begin()开始,即列表的开始,一直持续到到达end()(图 23-1 )。
图 23-1
一个列表,有它的begin()和end()访问函数,一个迭代器i表示第二个元素。end()是最后一个元素的下一步
end()不是指最后一个元素,而是指最后一个元素之后的一个*。把list想象成一列汽车,这是它的车尾。我们检查它,看看我们是否走得太远,使用!=而不是<。(一个迭代器是否小于另一个迭代器没有定义——但是它们是否相等是有定义的。)*
*++i和i++按预期工作;他们带你到下一个元素。
要得到的不是迭代器而是它所引用的东西,把*放在前面,就像前面的循环一样。
仅此而已!
你的反应类似于“但是什么是迭代器呢?”?
形式上,就像描述的那样:它是一个引用列表中一个元素的东西,当你说++的时候,它就转到下一个元素。
非正式地…它不完全是一个指针,但是它确实指向一些东西。你可以使用*和->,就像你使用指针一样。但是++的意思是转到列表中的下一个元素,不是下一个内存地址,就像用指针一样。把它想象成一个手指,你可以把它放在一个条目上,并在你喜欢的时候移动到下一个条目——但它实际上是一个类,如示例 23-1 所示。
template <typename T>
class List
{
...
private:
...
Entry* start_; // points to first element
Entry* end_; // ...and the caboose
public:
class iterator // an iterator for List
{
public:
iterator (const iterator& other) : where_ (other.where_) {}
iterator (Entry* where = nullptr) : where_ (where) {}
const iterator& operator= (const iterator& other)
{
where_ = other.where_;
}
bool operator== (const iterator& other) const
{
return where_ == other.where_;
}
const iterator& operator++() // pre-increment, as in ++i
{
if (where_->next_ == nullptr) throw BadPointer();
else where_ = where_->next_;
return *this;
}
iterator operator++ (int) // post-increment, as in i++
{
iterator result = *this; ++(*this); return result;
}
T& operator* ()
{
if (where_->next_ == nullptr) throw BadPointer();
return where_->data_;
}
T* operator->() // This really is how you do it. It works!
{
if (where_->next_ == nullptr) throw BadPointer();
return &(where_->data_);
}
private:
Entry* where_;
};
iterator begin() { return iterator(start_); }
iterator end () { return iterator(end_ ); }
};
Example 23-1An iterator class for List
现在我们可以得到List类之外的List的数据*。*
…也和vector一起
list s 需要迭代器。但是 STL 为vector和其他容器提供了它们,STL 行家推荐使用它们。为什么呢?
-
轻松重写代码。考虑带有
vector:for (int i = 0; i < v.size(); ++i) doSomething(v[i]); and for (vector<T>::iterator i = v.begin(); i != v.end(); ++i) doSomething(*i);的这两个版本的代码
我写这一点后来又想,不行,我现在看 vector 不是办法; list 更好。
如果我使用第一个版本,我对两行都做了重大的修改。如果我用第二个,我要做的就是把vector改成list。
-
vector的一些成员函数已经需要迭代器。比如insert。 -
泛型编程。假设您想对不同类型的容器做些什么:
myIterator = find(digits.begin(), digits.end(), 7); // Where's that 7?
- 因为
list的find必须使用迭代器,所以vector的find也使用迭代器。这样你可以学习一种调用函数的方法,不管你选择什么样的容器,它都会工作。本章末尾的“<algorithm>(可选)”一节介绍了 STL 提供的一些功能。
但是如果你像以前一样用int index代表vector s,天就不会塌下来。
const和反向迭代器
在这段代码中,使用迭代器会导致类型冲突:
class EmptyList {}; // Exception
template <typename T>
double findAverageLength (const List<T>& myList)
{
if (myList.empty()) throw EmptyList();
double sum = 0;
for (List<string>::iterator i = myList.begin();
i != myList.end();
++i)
sum += i->size();
return sum / myList.size();
}
编译器给出一个错误信息,归结为:myList是const,但是你对它使用了一个iterator,这是一个类型冲突。
这就是 STL 如何防止你使用一个带有const容器的迭代器并做一些改变内容的事情。解决方案是使用一个不允许修改的迭代器版本:
template <typename T>
double findAverageLength(const List<T>& myList)
{
if (myList.empty()) throw EmptyList();
double sum = 0;
for (List<string>::const_iterator i = myList.begin();
i != myList.end();
++i)
sum += i->size();
return sum / myList.size();
}
这就够了。
如果你愿意,你可以倒着穿过集装箱(注意rbegin和rend——与begin和end相同,只是方向相反):
for (list<T>:: 反向 _ 迭代器 i=myList. rbegin ();
i !=myList.rend();
++i)
doSomethingTo (*i); // myList must be non-const
...or do backwards and const:
for (List<string>::const_reverse_iterator i = myList.begin();
i != myList.end();
++i)
sum += i->size();
要知道使用哪一种:
-
如果不打算换容器,使用
const_iterator。 -
如果有,使用
iterator。 -
如果你要倒退,把
reverse_放在那里的某个地方。
防错法
-
You get an error message too long to read:
conversion from 'std::__cxx11::list<std::pair<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >::const_iterator' {aka 'std::_List_const_iterator<std::pair<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >'} to non-scalar type 'std::__cxx11::list<std::pair<std::__cxx11::basic_string<char>, int> >::const_iterator' {aka 'std::_List_const_iterator<std::pair<std::__cxx11::basic_string<char>, int> >'} requestedRip out things that don’t look important. This gets us
conversion from list<pair<string, string > >::const_iterator to list<pair<string, int> >::const_iterator requested.我的错误现在更清楚了:我忘记了我想要什么。
-
你得到几页错误信息,报告系统库中的错误。在信息中寻找你自己的文件名,并专注于这些文件名。
Exercises
-
在 for 循环中使用迭代器,编写一个函数
reverse,它返回一个你给它的List的反向副本。 -
现在编写它,这样你不是传递一个
List而是传递两个迭代器,可能是它的begin()和它的end()。 -
给
List添加一个const_iterator类。你需要
begin和end的新const版本,来返回const_iterator而不是iterator。 -
第二十二章最后一组练习中的练习 8 是关于装备一个
List向后移动。使用它,将reverse_iterator和const_reverse_iterator合并到List类中。 -
在 for 循环中使用迭代器,编写函数在 STL
list和vector之间来回转换。
变得非常懒惰:基于范围的for和auto
当然,这适用于遍历容器:
for (vector<string>::const_iterator i = myVector.begin();
i != myVector.end();
++i) // A lot of typing...
cout << *i << ' ';
但是这个更短的版本也有效:
for (const string& i : myVector)
cout << i << ' '; // <-- no *: i is an element, not an // iterator
这是一个“基于范围的”for 循环:它使用迭代器——期望有begin()和end(),因此适用于vector、list或其他——但是我们不必把这些都写出来(耶)。(本节代码在源代码项目/文件夹ch23/range-based-for-and-auto中收集和编译。)
它也适用于数组:
int myArray[] = { 0, 1, 2, 3 };
for (int i : myArray) cout << i << ' ';
很好,但是我现在更懒了。让编译器计算出元素类型:
for (auto
i : myArray) cout << i << ' ';
// Overkill? I *did* know it was an int...
for (auto i : myVector) cout << i << ' ';
您可以将auto用于编译器能够识别类型的任何变量声明——也就是变量被初始化的地方。我们确实需要给 ?? 一些帮助;它不会应用&的,除非我们告诉它:
for (auto& i : myArray) i *= 2; // Without & it won't change the array
你也可以做我们熟悉的const &:
for (const auto& i : myVector) cout << i << ' ';
当类型名太长,我觉得我的手指会掉下来的时候,我就用auto(list<vector<int>>::const_reverse_iterator–aigh!)并且在这些基于范围的循环中。我认为对于简单的基本类型声明来说是过度了。<
跨度
Pascal 语言(始于 1970 年)允许将数组作为函数参数传递,大小作为类型的一部分。问题:每个数组大小都需要不同的函数。
如你所知,C 和 C++ 不会保留这个大小,所以你需要额外的工作来传递它作为另一个参数。
直到 C++20。现在你可以传入一个数组,并让它转换成一个跨度为的数组。span 记住了数组的内容和大小,所以我们可以这样做:
void print (const span<Heffalump> &s)
{
for (int i = 0; i < s.size(); ++i) cout << s[i];
}
或者,既然我们现在有了 ranges 和auto,这:
void print (const span<Heffalump> &s)
{
for (const auto &element : s) cout << element;
}
我们能做到这一点吗——不需要指定长度就可以传递一个序列——不需要跨度?当然,我们可以使用vector s。但是将一个数组复制到vector需要时间,尤其是如果有很多元素的话。一个跨度并不真正拥有它的记忆;不复制,只是用大小捆绑;并使两者都可用,正如我们刚才看到的。示例 23-2 显示了如何使用它。
// Program to read in & dissect a phone number
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <span> // for std::span
using namespace std;
// Converts a char to a single-digit number
int char2digit (char c) { return c - '0'; }
// Read/print arrays, I mean, spans
void read (const span< int>& s);
void print (const span<const int>& s);
int main(void)
{
int phoneNumber[10];
cout << "Enter your phone # (10 digits, no punctuation, ";
cout << "e.g 2025550145): "; read (phoneNumber);
cout << "You entered: "; print (phoneNumber);
cout << '\n';
cout << "Area code was: ";
print (span (phoneNumber).subspan
(0, 3)); cout << '\n';
// subspan is better, but this shows how to use
// a span with a pointer
int* remainder = phoneNumber + 3;
cout << "Rest of number is: "; print (span (remainder, 7));2
cout << '\n';
return 0;
}
void print (const span<const int>& s)
{
for (const auto& element : s) cout << element;
}
void read (const span< int>& s)
{
for (auto& element : s)
element = char2digit (cin.get());
// read in a digit, convert to int
// keeping it simple: no check for bad input
}
Example 23-2A program that uses spans to read, print, and dissect a phone number
print和read中跨度前的const表示跨度的结构不会被该功能改变。(所以你可以传入一个 r 值,一个不能赋值的东西,像span (remainder, 7)。)但是它的元素可以改变。
print跨度中的const int意味着其中的int也是const。如果你不想函数改变数组,使用这个。
Exercises
图 23-2
冠状病毒病例的样本数据,使用 7 天平均值进行平滑
-
重写你在第十章中的程序,其中数组被传递到函数中,使用 ranges,auto 和 spans。
-
假设你有数据,每天都有新的病例,从冠状病毒第一次成为大新闻开始(图 23-2 )。为了平滑数据并帮助我们了解它总体上是增加还是减少——如果我们将曲线拉平——计算每天之前三天、之后三天以及自身的平均值。(您可以跳过数据集的前 3 天和后 3 天。)然后打印那些平均值。使用范围、
auto、跨度和subspan。 -
(使用文件 I/O)做前面的练习,从文件中获取数据。你不知道文件中有多少数据,所以使用动态数组。至少应该向一个函数(打印的函数)传递一个动态数组。
initializer_list s(可选)
从数组到更复杂的容器,我很遗憾地放弃了一件事,那就是括号内的初始化列表,就像在int array [] = {0, 1, 2, 3};中一样。逐个元素初始化比较麻烦。
但是我们也可以用自己的类来做这件事,它是为 STL 内置的。示例 23-3 用Vector说明了这一点。
#include <initializer_list
> // for std::initializer_list
template <typename T>
class Vector
{
public:
Vector (const std::initializer_list<T>& other);
// ...
};
template <typename T>
Vector<T>::Vector (const std::initializer_list<T>& other) : Vector ()
{
for (auto i = other.begin(); i != other.end(); ++i)
push_back (*i);
}
Example 23-3Adapting Vector to use an initializer_list
内置了迭代器,所以这可以工作。不过,由于它内置了迭代器,所以这个更简单的版本也会如此:
template <typename T>
Vector<T>::Vector (const std::initializer_list<T>& other) : Vector ()
{
for (auto i : other) push_back (i);
}
然后我们可以像以前一样初始化数组
Vector<int> V = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
或者因为新的构造器将根据需要隐式调用:
Vector<int> U;
// ...
U = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Exercises
-
在类
Point2D中编写并测试一个接受initializer_list的构造器。 -
…在
Fraction班。 -
如果你按顺序通过一个
initializer_list,使用List::push_front将它的元素添加到一个List,顺序将被颠倒。(如有必要,将它写在纸上,以便确认。)所以如果你还没有做上一章的练习 8,它提供了List::push_back,并给List一个带initializer_list的构造器。
<algorithm>(可选)
您可能想对容器做的许多事情都在包含文件<algorithm>中。
下面是如何在名为digits的容器中找到一个元素。可以是list、vector,或者随便什么。(这些代码片段是在源代码项目/文件夹ch23/algorithm中收集和编译的。)
using namespace std; // the ranges namespace is part of std;
// without "using namespace std," say std::ranges::
auto i = ranges::find (digits, 7);
3
if (i != digits.end()) cout << "Found a 7!\n";
find返回引用第一个元素digits等于 7 的迭代器。如果没有这样的元素,它返回digits.end()。
下面是如何将一个容器中的内容复制到另一个容器中。它们可以是不同类型的容器:
ranges::copy
(digits, back_inserter (newContainer));
或者只是复制那些符合某种标准的。我们可以传入一个函数:
bool isEven (int i) { return i % 2 == 0; }
..
ranges::copy_if
(digits, back_inserter (anotherNewContainer), isEven);
这些功能中的大多数应该适用于任何类型的容器。ranges::sort (digits);做你认为它做的事。(运算符<对其元素必须进行定义。)但是如果你想对一个列表进行排序,你就必须使用它的成员函数:myList.sort ();。去想想。
这些函数从你的容器中删除一个值或者删除所有符合某些条件的元素: 4
erase_if (digits, isEven); // returns quantity erased. Not part of ranges:: // (just std::)
erase (digits, 11); // ditto. Erases all instances of 11
STL 容器不会让 I/O 的<<和>>操作符过载。这是可以理解的,因为我们可能都想用不同的方式在容器中打印或读取,但这仍然是一种痛苦。STL 提供了另一种打印方式,名字很奇怪
ranges::copy (digits, ostream_iterator<int>(cout, " ")); // "copy" digits // to cout
int是我们的列表,cout是它的去向," "是每个元素后要打印的内容。您将需要#include <iterator>(尽管它可能由另一个系统提供)。
有用的功能比较多;网上搜索会找到你需要的东西。cplusplus.com 和 cppreference.com 是开始的好地方。
防错法
-
You add or remove elements in or from a loop, and get a crash. When you do anything to change the contents of your container, it may invalidate iterators already in the container. Consider this code (assume
digitsis avector):for (auto i = digits.begin(); i != digits.end(); ++i) if (isEven(*i)) digits.erase (i); // erase element referenced by i // (different "erase" -- a member of // vector)在你删除了
i的内容后,i指向了一个不存在的元素。循环增加了i,并到达了另一个不存在的元素,结果是不可预知的。这种
erase使哪些迭代器失效,如何修复取决于容器类型。您可以研究并为您选择的任何容器编写一个修复程序,或者使用内置的erase (myContainer, predicate)函数。我知道我会推荐什么。
Exercises
-
在字符串“SoxEr776asdCsdfR1234qqE..T12Ci-98jOapqwe0DweE“有一个秘密代码。使用一个
<algorithm>函数只提取大写字母并读取代码。 -
(使用文件 I/O)首先,创建一个字符串文件。然后复印两份。一种方法是,改变一些字符串的顺序和大小写。另一方面,更换一些琴弦。
Now write a program that can read two files into vectors or lists and, using STL functions, tell if the files are different, ignoring order and capitalization. Use an STL function not covered in this section (so you’ll need an Internet search) that changes a string to all caps or all lowercase. To find what’s in one sequence but not the other, consider this algorithm:
for each element in sequence 1 (using iterators) use the "find" function to see if it's also in sequence 2 -
做前面的问题,但是不使用
find和一个循环,而是使用set_difference(之前也没有提到)。
如果你的编译器处理概念,你可以坚持让auto代表的类型有一些特征,以防止错误:
for (std::integral auto& i: myVector) i *= 2; // Won’t compile if myVector’s base type isn’t integral
2
当从 phoneNumber 转换到 span 时,C++ 知道 phoneNumber 的大小,并可以将它放入 span。但是余数只是一个指针。要为它创建一个 span,我们必须指定它的大小。
3
如果你不是 C++20 兼容的,对于这些函数中的每一个,忘记 ranges::并用它的 begin 和 end 替换容器:auto i = find (digits.begin()、digits.end()、7);
4
在 C++20 之前,我们用了一个函数remove_if,它实际上并没有把你告诉它的东西去掉(!).它只是把它们移动到末尾,然后返回一个迭代器,引用第一个你想删除的东西。为了让它们消失,你必须从迭代器返回到最后的erase:auto removables = remove_if (digits.begin(), digits.end(), isEven);digits.erase (removables, digits.end());
总算摆脱那个了。
*