1、像所有的标准库长度类型一样,vector<double>::size_type是无符号整数类型。这样的类型是根本不能用来存储负数值的;相反,它们所存储的值以2的n次方为模(n的大小取决于不同的系统环境)。例如,在程序中我们永远不会检查homework,size() < 0是否成立,因为这个不等式总是产生false值。
此外,每当普通整数和无符号整数在表达式中结合在一起时,普通整数就要被转换成无符号整数。因此,诸如homework.size() - 100这样的表达式将会产生无符号的结果,这也意味着结果不能小于0------即使是homework.size() < 100。
2、const vector<double>&,这个类型经常被称为“对参数类型为double的向量常量的引用”,或者通俗点,我们把它称为“双精度向量常量引用”。我们说名称是一个引用是指,这个名称是一个特定对象的另一个名称。例如,如果我们编写了以下的语句:
vector<double> homework;
vector<double>& hw = homework;// hw 是homework的一个替代名
我们说,hw 是homework的另一个名称。从现在起,我们对hw所做的任何动作等等价于对homework做同样的动作,反过来也是一样。在下面的语句中再增加一个const:
// chw 是homework的一个只读的替代名
const vector<double>& chw = homework;
这条语句仍然是表示chw是homework的另一个替代名,但是const确保了我们将不会对chw做任何可能改变它的值得动作。
如果我们定义一个非常量引用(也就是一个允许访问的引用)我们就不能让它指向一个常量对象或者常量引用,因为这样做表示了,我们将要做const所不允许做的动作。因此,我们不能编写:
vector<double>& hw2 = chw;// 错误;请求了对chw的写访问
3、我们为什么要明确地提到std::vector而不是编写一个using声明呢?有一个微妙的原因。
一般而言,头文件应该只声明必要的名称。限定了包含在头文件中的名称之后,我们就可以为我们的用户保留最大限度的灵活性了。例如,我们之所以对std:vector使用限定名是因为,我们无法得知median函数的用户希望以何种方式引用std:vector。我们的代码的用户可能不想使用一个using声明。因此,如果我们在头文件中编写了一个的话,那么所有包含了我们的头的程序就会获得一个using std:vector,而不管它们是否需要它。因此,头文件应该使用完整的限定名而不是使用using声明。
4、每一个标准容器,例如向量,都定义了两种相关的迭代器类型:
container-type::const_iterator
container-type::iterator
在这里,container-type是诸如vector<Student_info>这样的容器的类型,它包括了容器元素的类型。如果我们想用一个迭代器来修改存储在容器中的值,使用iterator类型;如果我们仅仅需要读操作,那么使用const_iterator类型。注意,我们能把一个iterator类型的对象转换为const_iterator类型的,但反过来则不行。
5、vector<Student_info> extract_fails(vector<Student_info>& students)
{
vector<Student_info> fail;
vector<Student_info>::iterator iter = students.begin();
while (iter != students.end()) {
if (grade(*iter)) {
iter = students.erase(iter);
} else
++iter;
}
return fail;
}
注意:我们把erase的返回值赋给iter。为什么要这样呢?
稍微思考一下我们会知道,这个删除iter所指示的元素的动作一定会使迭代器失效。在我们调用了students.erase(iter)之后,我们知道,iter已经不能再指向同一个元素了---因为这个元素已经不存在。实际上,如果我们调用erase来删除向量中的一个元素,那么所有指向位于被删除元素后面的元素的迭代器都会失效。erase返回了一个迭代器,这个迭代器指向紧跟在我们刚刚除掉的元素后面的那个元素。因此,执行
iter = students.erase(iter);
会让iter指向被删除元素后面的那个元素--这个结果正是我们所期望的。
6、如果容器只是(或者主要是)从尾部增长和缩小的话,那么vector的性能就会比list好。不过,如果程序要从容器中删除很多元素--正如我们的程序所做的那样--那么对于大规模的输入,list的速度就会更快,而且当输入增加时它的速度优势就会更加明显。
7、typedef typename vector<T>::size_type vec_sz; 在vec_sz的定义中,typename的使用是另一个我们不熟悉的概念。它会告知系统环境,vector<T>::size_type是一个类型名--即使系统环境还不知道类型T具体是代表什么的。无论何时,只要我们有一个诸如vector<T>这样的依赖于一个模板参数的类型,并且我们希望使用这个类型的一个诸如size_type,这样的成员--这个成员本身也是一个类型,我们都必须在整个名称的前面加上typename,从而让系统环境知道应该把这个名称当做作一个类型来处理。
8、如果声明一个构造函数是explicit的,那么编译器只有在用户显示地使用构造函数时才会调用它,否则就不调用。
Vec<int> vi(100); // 正确,显示地调用Vec的构造函数,以一个int类型数据作参数
Vec<int> vi = 100; // 错误,隐式地调用Vec的构造函数,并把它复制到vi
9、template <class T> class Vec {
public:
Vec& operator=(const Vec&);
};
template <class T>
Vec<T>& Vec<T>::operator=(const Vec& rhs)
{
// 判断是否进行自我复制
if (&rhs != this)
{
// 删除运算符左侧的数组
uncreate();
// 从右侧复制元素到左侧
create(rhs.begin(), rhs.end());
}
return *this;
}
第一个概念是在除了类的头文件以外的地方定义一个模板成员函数的语法。对于任何一个模板,我们都在一开始就告诉编译器我们要定义一个模板,并且马上给出模板的参数。注意到在这里我们定义返回类型时,用的是Vec<T>&。对比在头文件中定义返回类型的语法,在头文件中定义时用的是Vec&类型,不需要显示地声明返回类型名称。因为在模板文件的范围内,C++允许我们忽略其具体类型名称。在头文件里,因为模板参数是隐式的,所以不需要重复写<T>。而在头文件外面,我们必须声明返回类型,所以要在必要的地方显示地写出模板参数。类似地,函数名是Vec<T>::operator=,而不能简写成Vec::operator=。不过,一旦在前面指定我们的定义是一个Vec<T>类型的成员函数,后面就不需要重复使用这个定语了。因此,在参数列表中我们把参数写成const Vec&的形式,而不必写成const Vec<T>&这样的复杂形式。