C++大杂烩


来源:《C++ Primer Plus(第6版)中文版 [美]Stephen Prata著》

1、命名空间的指导原则

  • 使用在已命名的命名空间中声明的变量,而不是使用外部全局变量;
  • 使用在已命名的命名空间中声明的变量,而不是使用静态全局变量;
  • 如果开发了一个函数库或者类库,将其放在一个命名空间中;
  • 仅将编译指令using作为一种将旧代码转换为使用命名空间的权宜之计;
  • 不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理编译指令#include之后;
  • 导入名称时,首选使用作用域解析运算符或using声明的方法;
  • 对于using声明,首选将其作用域设置为局部而不是全局。

2、C++为类提供的两种类型转换

(1)只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如:将double值赋给Stoneweight对象时,接受double参数的Stoneweight类构造函数Stoneweight(double pounds) { … }将自动调用。然而,在构造函数中声明使用explicit关键字可防止隐式转换,而只允许显示转换,否则Stoneweight(double pounds)还可以用于下面的隐式转换:

  • 将Stoneweight对象初始化为double值时;
  • 将double值赋给Stoneweight对象时;
  • 将double值传递给接受Stoneweight参数的函数时;
  • 返回值被声明为Stoneweight的函数试图返回double值时;
  • 在上述任意一种情况下,使用可转换为double类型的内置类型时,例如Stoneweight Jumbo(7000); 该语句首先将int转换为double,然后使用Stoneweight(double pounds)构造函数,然而,当且仅当转换不存在二义性时,才会进行这种二步转换,也就是说,如果类还定义了Stoneweight(long pounds)之类的构造函数时,编译器将拒绝该语句。

(2)被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员,没有返回类型、没有参数、名为operator typeName(),其中,typeName是对象将被转换成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自动被调用。例如,在Stoneweight类中添加了声明explicit operator double() const { … },那么Stoneweight stone1; double weight1 = double(stone1); 语句就可将类类型转换为double类型。


3、深拷贝

复制构造函数应当复制字符串并将副本的地址赋给str成员,而不仅仅是复制字符串地址。这样每个对象都有自己的字符串,而不是引用另一个对象的字符串。调用析构函数时都将释放不同的字符串,而不会试图去释放已经被释放的字符串,可以这样编写String类的复制构造函数:

String::String(const String &s)
{
    len = s.len;             // same length
    str = new char[len + 1]; // allocate space
    std::strcpy(str, s.str); // copy string to new location
}

必须显示定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。

赋值运算符的深拷贝定义实现与复制构造函数相似,但也有一些差别:

  • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete []来释放这些数据;
  • 函数应当避免将对象赋给自身,否则,给对象重新赋值前,释放内存操作可能删除对象的内容;
  • 函数返回一个指向调用对象的引用,这是为了能够连续进行赋值,语句S0 = S1 = S2; 等效于语句S0.operator=(S1.operator=(S2));

可以这样编写String类的赋值运算符:

String& String::operator=(const String &s)
{
    if (this == &s)          // onject assigned to itself
        return *this;        // all done
    delete []str;            // free old string
    len = s.len;
    str = new char[len + 1]; // get space for new string
    std::strcpy(str, s.str); // copy the string
    return *this;            // return reference to invoking object
}

代码首先检查自我复制,这是通过查看赋值运算符右边的地址(&s)是否与接收对象(this)的地址相同来完成的。如果相同,程序返回*this,然后结束;如果地址不同,函数将释放str指向的内存,这是因为稍后将把一个新字符串的地址赋给str,如果不首先使用delete运算符,则上述字符串将保留在内存中。由于程序中不再包含指向该字符串的指针,因此这些内存被浪费掉。


4、返回对象

如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公共复制构造函数的类(如ostream类),它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。


5、构造函数使用new的指导原则

  • 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete,该运算符将释放分配的内存;
  • 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针;
  • 构造函数中要么使用new [],要么使用new,而不能混用。如果构造函数使用的是new [],则析构函数应使用delete [];如果构造函数使用的是new,则析构函数应使用delete;
  • 应定义一个分配内存(而不是将指针指向已有内存)的复制构造函数。这样程序将能够将类对象初始化为另一个类对象。这种构造函数的原型通常为classname(const classname &);
  • 应定义一个重载赋值运算符的类成员函数,其函数定义如下(其中c_pointer是c_name的类成员,类型为指向type_name的指针),下面的示例假设使用new []来初始化变量c_pointer:
c_name& c_name::operator=(const c_name &cn)
{
    if (this == &cn)
        return *this; // done if self-assignment
    delete []c_pointer;
    // set size number of type_name units to be copied
    c_pointer = new type_name[size];
    // then copy data pointed to by cn.c_pointer to location pointed to by c_pointer
    ...
    return *this;
}

6、基类的虚成员函数

经常在基类中将派生类会重新定义的方法声明为虚方法,方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。如果方法是通过引用或指针而不是对象调用的,当没有使用关键字virtual时,程序将根据引用类型或指针类型选择方法;当使用了关键字virtual时,程序将根据引用或指针指向的对象的类型来选择方法。总之,编译器对非虚方法使用静态联编,对虚方法使用动态联编。例如,如果ViewAccount()不是虚的,程序的行为如下

// behavior with non-virtual ViewAccount() method chosen according to reference type
Brass base("Dominic Banker", 11224, 4183.45);
BrassPlus derived("Dorothy Banker", 12118, 2592.00);
Brass &b1_ref = base;
Brass &b2_ref = derived;
b1_ref.ViewAccount();      // use Brass::ViewAccount()
b2_ref.ViewAccount();      // use Brass::ViewAccount()

如果ViewAccount()是虚的,程序的行为如下

// behavior with virtual ViewAccount() method chosen according to object type
Brass base("Dominic Banker", 11224, 4183.45);
BrassPlus derived("Dorothy Banker", 12118, 2592.00);
Brass &b1_ref = base;
Brass &b2_ref = derived;
b1_ref.ViewAccount();      // use Brass::ViewAccount()
b2_ref.ViewAccount();      // use BrassPlus::ViewAccount()

如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。

如果在派生类中重定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。这引出了两条经验规则:

  1. 如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化;
  2. 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。

7、虚析构函数

如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。这意味着只有Base的析构函数被调用,即使指针指向的是一个Derived对象。如果析构函数是虚的,将调用相应对象类型的析构函数。因此,如果指针指向的是Derived对象,将调用Derived的析构函数,然后自动调用Base的析构函数。因此,使用虚析构函数可以确保正确的析构函数序列被调用。如果Derived包含一个执行某些操作的析构函数,则Base必须有一个虚析构函数,即使该析构函数不执行任何操作。因此通常应给基类提供一个虚析构函数,即使它并不需要析构函数。

顺便说一句,构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将将类构造函数声明为虚的没什么意义。此外,友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。


8、纯虚函数和抽象类

C++通过使用纯虚函数提供未实现的函数。当类声明中包含纯虚函数时,则不能创建该类的对象。类中至少有一个虚函数是纯虚函数时,类才成为抽象类。纯虚函数用于定义派生类的通用接口。总之,在原型中使用virtual return_type function(args) = 0; 指出类是一个抽象基类,在类中可以不定义该函数。


9、使用const

可以用const确保方法不修改参数:

Star::Star(const char *s) { … } // won’t change the string to which s points

可以使用const来确保方法不修改调用它的对象:

void Star::show() const { … }  // won’t change invoking onject

通常,可以将返回引用的函数放在赋值语句的左侧,这意味着可以将值赋给引用的对象,但可以使用const来确保引用或指针返回的值不能用于修改对象中的数据:

const Stock& Stock::topval(const Stock &s) const {
if (s.total_val > total_val) return s; // argument object
else return *this; // invoking object
}

该方法返回对this或s的引用。因为this和s都被声明为const,所以函数不能对它们进行修改,这意味着返回的应用也必须被声明为const。注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。


10、类的包含或私有继承实现has-a关系

通常,用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他了类对象的类。例如:

class Student {
private:
string name;             // use a string object for name
valarray<double> scores; // use a valarray<double> object for scores
}

私有继承声明如下:

class Student : private std::string, private std::valarray<double> { … }

包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是两种方法的第一个主要区别。

①. 构造函数初始化

包含方式的构造函数将使用成员名来标识构造函数:

Student(const char *str, const double *pd, int n) : name(str), scores(pd, n) { … } // use object names for containtent

私有继承方式的构造函数将使用类名来标识构造函数:

Student(const char *str, const double *pd, int n) : std::string(str), std::valarray<double>(pd, n) { … } // use class names for inheritance

这是两种方法的第二个主要区别。

②. 访问基类的方法

包含使用对象名来调用方法:

double Student::Average() const {
if (scores.size() > 0) return scores.sum() / scores.size();
else return 0;
}

私有继承使用类名和作用域解析运算符来调用方法:

double Student::Average() const {
if (std::valarray<double>::size() > 0)
return std::valarray<double>::sum() / std::valarray<double>::size();
else return 0;
}

③. 访问基类对象

包含方式可以实现Student类的Name()方法用以返回string对象成员name:

const string& Student::Name() const { return name; }

私有继承方式则使用强制类型转换,将Student对象转换成string对象,结果为继承而来的string对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:

const string& Strdent::Name() const { return (const string &)*this; }

上述方法返回一个引用,该引用指向用于调用该方法的Student对象中继承而来的string对象。

④. 访问基类的友元函数

用类名显式限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为类来调用正确的函数。

ostream& operator<<(ostream &os, const Student &stu)
{ os << “Scores for ” << (const string &)stu << “:\n”; … }

显式地将stu转换为string对象的引用,进而调用函数operator<<(ostream &, const string &),引用stu不会自动转换为string引用,这是因为在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。

总结:使用包含还是私有继承?

大多数C++程序员倾向于使用包含。首先,它易于理解,类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象;其次,继承会引起很多问题,尤其从多个基类继承时,如包含同名方法的独立的基类或共享祖先的独立基类;另外,包含能够包括多个同类的子对象,而继承则只能使用一个这样的对象。然而,若类包含保护成员,使用组合将这样的类包含在另一个类中,后者位于继承层次结构之外,因此不能访问保护成员,但通过私有继承得到的将是派生类,能够访问保护成员;另一种需要使用私有继承的情况是需要重新定义虚函数,派生类可以重新定义虚函数,而包含类不能。

通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员或需要重新定义虚函数,则应使用私有继承。


11、多继承中的虚基类

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类

class Singer : virtual public Worker { … };
class Waiter : public virtual Worker { … };
class SingingWaiter : public Singer, public Waiter { … };

现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。如果Worker是虚基类,对于下面的构造函数:

SingingWaiter(const Worker &wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) { … } // flawed

存在的问题是,自动传递信息时,将通过两条不同的途径将wk传递给Worker对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类,因此构造函数应该是这样:

SingingWaiter(const Worker &wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) { … }

如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式调用该虚基类的某个构造函数。

当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。

使用虚基类将改变C++解析二义性的方式。如果某个成员名称优先于其他所有成员名称,则使用它时,即便不使用限定符,也不会导致二义性。派生类中的名称优先于直接或间接祖先类中的相同名称。


12、运行阶段类型识别RTTI

类继承中,基类指针可以指向任何一个派生类的对象。这样就可以调用这样的函数:在处理一些信息后,选择一个类,并创建这种类型的对象,然后返回它的地址,而该地址可以被赋给基类指针,那么如何知道指针指向的是哪种对象呢?可能希望调用类方法的正确版本,在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,则并不真正需要知道对象的类型,但派生类可能包含不是继承而来的方法,在这种情况下,只有某些类型的对象可以使用该方法,也可能是出于调试目的,想跟踪生成的对象的类型。

C++有3个支持RTTI的元素:

  • 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针生成一个指向派生类的指针;否则,该运算符返回0 ———— 空指针。
  • typeid运算符返回一个指出对象的类型的值。
  • type_info结构存储了有关特定类型的信息。

只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。

①. dynamic_cast运算符

dynamic_cast运算符不能回答“指针指向的是哪类对象”这样的问题,但能够回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。假设有下面的类层次结构:

class Grand { // has virtual methods };
class Superb : public Grand { … };
class Magnificent : public Superb { … };

接下来假设有下面的指针:

Grand *pg = new Grand;
Grand *ps = new Superb;
Grand *pm = new Magnificent;

最后,对于下面的类型转换:

Magnificent *p1 = (Magnificent *) pm; // safe
Magnificent *p2 = (Magnificent *) pg; // not safe
Superb *p3 = (Magnificent *) pm;      // safe

只有那些指针类型与对象的类型(或对象的直接或间接基类的类型)相同的类型转换才一定是安全的。

dynamic_cast运算符的用法如下,其中pg指向一个对象:

Superb *pm = dynamic_cast<Superb *>(pg);

如果指针pg的类型可以被安全地转换为Superb *类型,则运算符返回对象地址,否则返回一个空指针。

通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针:

dynamic_cast<Type *>(pt)

否则,结果为0,即空指针。

也可以将dynamic_cast运算符用于引用,其用法稍微有点不同:没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的,它是在头文件typeinfo中定义的。因此,可以像下面这样使用该运算符,其中rg是对Grand对象的引用:

#include <typeinfo> // for bad_cast
try {

Superb &rs = dynamic_cast<Superb &>(rg);

} catch(bad_cast &) { … };

②. typeid运算符和type_info类

typeid运算符使得能够确定两个对象是否为同种类型,它与sizeof有些相像,可以接受两种参数:类名和结果为对象的表达式。typeid运算符返回一个对type_info对象的引用,其中type_info是在头文件typeinfo中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。例如,如果pg指向的是一个Magnificent对象,则下述表达式的结果为bool值true,否则为false:

typeid(Magnificent) == typeid(*pg)

如果pg是一个空指针,程序将引发bad_typeid异常,该异常类型是从exception类派生而来的,也是在头文件typeinfo中声明的。

type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串,通常是类的名称。例如,下面的语句显示指针pg指向的对象所属的类定义的字符串:

cout << “Now processing type ” << typeid(*pg).name() << “.\n”;

③. 误用RTTI的例子

pg = function();
pg->speak();
if (ps = dynamic_cast<Superb *>(pg))

ps->say();

上述代码通过放弃dynamic_cast和虚函数,而使用typeid,可以改写为:

pg = function();
if (typeid(Magnificent) == typeid(*pg)) {

pm = (Magnificent *)pg;

pm->speak();

pm->say();

}
else if (typeid(Superb) == typeid(*pg)) {

ps = (Superb *)pg;

ps->speak();

ps->say();

}
else

pg->speak();

上述代码显式地指定各个类存在严重的缺陷。例如,假设发现必须从Magnificent类派生一个Insufferable类,而后者需要重新定义speak()和say(),使用typeid来显示地测试每个类型时,必须添加一个else if,但无需修改使用dynamic_cast的版本。

因此,如果发现在扩展的if – else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast。


13、类型转换运算符

C++的4个类型转换运算符:dynamic_cast、const_cast、static_cast和reinterpret_cast。

①.假设High和Low是两个类,而ph和pl的类型分别为High *和Low *,则仅当Low和High的可访问基类(直接或间接)时,下面的语句才将一个Low *指针赋给pl:

pl = dynamic_cast<Low *> ph;

否则,该语句将空指针赋给pl。通常,该运算符的语法如下:

dynamic_cast<type-name>(expression)

该运算符的用途是,使得能够在类层次结构中进行向上转换,而不允许其他转换。

②.const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_cast运算符相同:

const_cast<type-name>(expression)

如果类型的其他方面也被修改,则上述类型转换将出错。也就是说,除了const或volatile特征(有或无)可以不同外,type_name和expression的类型必须相同。再次假设High和Low是两个类:

High bar;
const High *pbar = &bar;
High *pb = const_cast<High *>(pbar);           // valid
const Low *pl = const_cast<const Low *>(pbar); // invalid

第一个类型转换使得*pb成为一个可用于修改bar对象值的指针,它删除const标签;第二个类型转换是非法的,因为它同时尝试将类型从const High*改为const Low*。

const_cast不是万能的,它可以修改指向一个值的指针,但修改const值的结果是不确定的。

void change(const int *pt, int n) {

int *pc = const_cast<int *>(pt);

*pc += n;

}

int main() {

int pop1 = 1000;

const int pop2 = 2000;

change(&pop1, 100);

change(&pop2, 200);

cout << “pop1, pop2: ” << pop1 << “, ” << pop2 << endl;

}

在change()中,形参pt指针被声明为const int *,因此不能用来修改指向的int,因此不能用来修改指向的int,指针pc删除了const特征,因此可用来修改指向的值,但仅当指向的值不是const是才可行。因此,pc可修改pop1,但不能修改pop2。

③.static_cast运算符的语法与其他类型转换符相同:

static_cast<type-name>(expression)

仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的。

Low *plow = static_cast<Low *>(&phigh); // class Low is derived by High

从基类指针转换到派生类指针,在不进行显式类型转换时,将无法进行,但由于转换的是指针类型,无需进行类类型转换,便可以进行另一个方向的类型转换,因此使用static_cast来进行向下转换是合法的。

同理,由于无需进行显式转换,枚举值就可以被转换为整型,所以可以用static_cast将整型转换为枚举值,同样,可以使用static_cast将double转换为long、将float转换为int以及其他各种数值转换。

④.reinterpret_cast运算符用于天生危险的类型转换,它不允许删除const,但会执行其他令人生厌的操作。该运算符语法与其他三个相同:

reinterpret_cast<type-name>(expression)

下面是一个使用示例:

struct dat { short a; short b; };
long value = 0x12345678;
dat *pd = reinterpret_cast<dat *>(&value);
cout << hex << pd->a; // display first 2 bytes of value

通常,这样的转换依赖于实现的底层编程技术,是不可移植的。

然而, reinterpret_cast运算符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型;另一个限制是,不能将函数指针转换为数据指针,反之亦然。


14、智能指针

三个智能指针模板(auto_ptr、unique_ptr和shared_ptr)都定义了类似指针的对象,可以将new获得的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存,在智能指针过期时,这些内存将自动被释放。

要创建智能指针对象,必须包含头文件memory,该文件模板定义,然后使用通常的模板语法来实例化所需类型的指针。例如,模板auto_ptr包含以下构造函数:

template <class X> class auto_ptr {

public:

explicit auto_ptr(X *p = 0) throw();

};

求X类型的auto_ptr将获得一个指向X类型的auto_ptr:

auto_ptr<double> pd(new double); // pd an auto_ptr to double

auto_ptr<string> ps(new string); // ps an auto_ptr to string

new double是new返回的指针,指向新分配的内存块,它是构造函数auto_ptr<double>的参数,即对应于原型中形参p的实参。其他两种智能指针使用同样的语法:

unique_ptr<double> pdu(new double); // pdu an unique_ptr to double

shared_ptr<string> pss(new string); // pss an shared_ptr to string

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数,因此不需要自动将指针转换为智能指针对象:

shared_ptr<double> pd;

double *p_reg = new double;

pd = p_reg;                         // not allowed (implicit conversion)

pd = shared_ptr<double>(p_reg);     // allowed (explicit conversion)

shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)

shared_ptr<double> pshared(p_reg);  // allowed (explicit conversion)

由于智能指针模板类的定义方式,智能指针对象的很多方面都类似于常规指针。例如,如果p是一个智能指针对象,则可以对它执行解除引用操作(*p)、用它来访问结构成员(p->data)、将它赋给指向相同类型的常规指针,还可以将智能指针对象赋给另一个同类型的智能指针对象,但这将引发一个问题:

auto_ptr<string> ps(new string(“I reigned lonely as a cloud.”));

auto_ptr<string> vocation;

vovation = ps;

这将导致同一个对象被析构函数调用两次,一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:

  • 定义赋值运算符,使之执行深拷贝,这样两个指针将指向不同的对象;
  • 建立所有权(ownership)概念,对于特定的对象,只有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象,然后,让赋值操作转让所有权。这就是auto_ptr和unique_ptr的策略,但unique_ptr的策略更为严格;
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数,这称为引用计数(reference counting)。例如,赋值时,计数将加1,而指针过期时,计数将减1,仅当最后一个指针过期时,才调用delete。这是shared_ptr的策略。

auto_ptr<string> p1(new string(“auto”)); // 1

auto_ptr<string> p2;                     // 2

p2 = p1;                                 // 3

在语句3中,p2接管string对象的所有权后,p1的所有权将被剥夺,这可防止p1和p2的析构函数试图删除同一个对象,但如果程序随后试图使用p1,这时p1将不再指向有效的数据。若将auto_ptr替换成unique_ptr:

unique_ptr<string> p3(new string(“unique”)); // 4

unique_ptr<string> p4;                       // 5

p4 = p3;                                     // 6

编译器将认为语句6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全(编译阶段错误比潜在的运行阶段错误更安全)。

但有时候,将一个智能指针赋给另一个不会留下危险的悬挂指针。假如有如下函数定义:

unique_ptr<string> demo(const char *s) {

unique_ptr<string> temp(new string(s));

return temp;

}

并假设编写了如下代码:

unique_ptr<string> ps;

ps = demo(“Uniquely special.”);

函数demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回的unique_ptr被销毁。这没有问题,因为ps拥有了string对象的所有权,但这里的另一个好处是,demo()返回的临时unique_ptr很快被销毁,没有几乎使用它来访问无效的数据。神奇的是,编译器确实允许这种赋值。

总之,程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做,如果源unique_ptr将存在一段时间,编译器禁止这样做:

using namespace std;

unique_ptr<string> pu1(new string(“Hi ho!”);

unique_ptr<string> pu2;

pu2 = pu1;                                   // 1 not allowed

unique_ptr<string> pu3;

pu3 = unique_ptr<string>(new string(“Yo!”)); // 2 allowed

语句1将留下悬挂的unique_ptr,这可能导致危害,语句2不会留下悬挂的unique_ptr,因为它调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权转让给pu3后就会被销毁。这种随情况而异的行为表明,unique_ptr优于允许两种赋值的auto_ptr。

除此之外,unique_ptr还有另一个优点,它有一个可用于数组的变体。模板auto_ptr使用delete而不是delete[],因此只能与new一起使用,而不能与new[]一起使用,但unique_ptr有使用new[]和delete[]的版本。

std::unique_ptr<double[]> pda(new double(5)); // will use delete[]

15、类模板

template <class T, int n> class ArrayTP { … };

关键字class指出T为类型参数,int指出n的类型为int。这种参数(指定特定的类型而不是用泛型名)称为非类型(non-type)或表达式(expression)参数。表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。因此,double m是不合法的,但double *rm和double *pm是合法的;另外,模板代码不能修改参数的值,也不能使用参数的地址,所以,在ArrayTP模板中不能使用诸如n++和&n等表达式;还有,实例化模板时,用作表达式参数的值必须是常量表达式。

模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。

template <typename Type>
class GrowArray : public Array<Type> { … }; // inheritance

template <typename Type>
class Stack {
Array<Type> ar; // use an Array<> as a component
};

Array < Stack<int> > asi; // an array of stacks of int

①. 递归使用模板

对于前面的数组模板定义,可以这样使用它:

ArrayTP< ArrayTP<int, 5>, 10> twodee;

这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组,与之等价的常规数组声明是int twodee[10][5];

②. 使用多个类型参数

模板可以包含多个类型参数。例如,可以创建Pair模板来保存两个不同的值:

template <class T1, class T2> class pair { … };

③. 默认类型模板参数

类模板的另一项新特性是,可以为类型参数提供默认值:

template <class T1, class T2 = int> class Topo { … };

这样,如果省略T2的值,编译器将使用默认值int:

Topo<double, double> m1; // T1 is double, T2 is double
Topo<double> m2;         // T1 is double, T2 is int

虽然可以为类模板类型参数提供默认值,但不能为函数模板类型参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。

模板的具体化

①. 隐式实例化(implicit instantiation)

声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:

ArrayTP<int, 100> stuff;      // implicit instantiation

编译器在需要对象之前,不会生成类的隐式实例化:

ArrayTP<double, 30> *pt;      // a pointer, no object needed yet
pt = new ArrayTP<double, 30>; // now an object is needed

第二条语句导致编译器生成类定义,并根据该定义创建一个对象。

②. 显式实例化(explicit instantiation)

当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在命名空间中。例如,下面的声明将ArrayTP<string, 100>声明为一个类:

template class ArrayTP<string, 100>; // generate ArrayTP<string, 100> class

这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义),和隐式实例化一样,也将根据通用模板来生成具体化。

③. 显式具体化(explicit specialization)

显式具体化是特定类型(用于替换模板中的泛型)的定义。当需要为特殊类型实例化时,对模板进行修改,使其行为不同。当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。具体化类模板定义的格式如下:

template <> class Classname<specialized-type-name> { … };

例如,要使用新的表示法提供一个专供const char *类型使用的SortedArray模板,可以使用类似于下面的代码:

template <> class SortedArray<const char *> { … };

④. 部分具体化(partial specialization)

C++还允许部分具体化,即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:

template <class T1, class T2> class Pair { … }; // general template
template <class T1> class Pair<T1, int> { … };  // specialization with T2 to int

关键字template后面的<>声明的是没有被具体化,如果指定所有的类型,则<>内将为空,这将导致显式具体化。

template <> class Pair<int, int> { … }; // specialization with T1 and T2 to int

如果有多个模板可供选择,编译器将使用具体化程度最高的模板:

Pair<double, double> p1; // use general Pair template
Pair<double, int> p2;    // use Pair<T1, int> partial specialization
Pair<int, int> p3;       // use Pair<int, int> explicit specialization

也可通过为指针提供特殊版本来部分具体化现有的模板:

template <class T> class Feeb { … };  // general version
template <class T*> class Feeb { … }; // pointer partial specialization modified code

如果提供的类型不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针具体化版本:

Feeb<char> fb1;   // use general Feeb template, T is char
Feeb<char *> fb2; // use Feeb T* specialization, T is char

如果没有进行部分具体化,则第二个声明将使用通用模板,将T转换为char *类型。

部分具体化特性使得能够设置各种限制。例如,可以这样做:

template <class T1, class T2, class T3> class Trio { … };   // general template
template <class T1, class T2> class Trio<T1, T2, T2> { … }; // specialization with T3 set to T2
template <class T1> class Trio<T1, T1 *, T1 *> { … };       // specialization with T2 and T3 set to T1*

给定上述声明,编译器将作出如下选择:

Trio<int, short, char *> t1;   // use general template
Trio<int, short> t2;           // use Trio<T1, T2, T2>
Trio<char, char *, char *> t3; // use Trio<T1, T1 *, T1 *>

将模板用作参数

template <template <typename T> class Thing> class Crab { … };

模板参数是template <typename T> class Thing,其中template <typename T> class是类型,Thing是参数。假设有声明Crab<King> legs; 则模板参数King必须是一个模板类,其声明与模板参数Thing的声明相匹配:

template <typename T> class King { … };

若Crab的声明声明了两个对象:

Thing<int> s1;
Thing<double> s2;

前面的legs声明将用King<int>替换Thing<int>,用King<double>替换Thing<double>。因此,若有声明Crab<Stack> nebula; 则Thing<int>将被实例化为Stack<int>,而Thing<double>将被实例化为Stack<double>。总之,模板参数Thing将被替换为声明Crab对象时被用作模板参数的模板类型。

可以混合使用模板参数和常规参数。例如,Crab类的声明可以像下面这样打头:

template <template <typename T> class Thing, typename U, typename V> Class Crab {
private:
Thing<U> s1;
Thing<V> s2;
};

现在,成员s1和s2可存储的数据类型为泛型,而不是用硬编码指定的类型,这要求程序中nebula的声明修改成这样:

Crab<Stack, int, double> nebula; // T = Stack, U = int, V = double

模板类和友元

①. 模板类的非模板友元函数

在模板类中将一个常规函数声明为友元:

template <class T>
class HasFriend {
public:
friend void counts(); // friend to all HasFriend instantiations
};

上述声明使counts()函数称为模板所有实例化的友元。要为友元函数提供模板类参数时可以这样进行友元声明:

template <class T>
class HasFriend {
friend void report(HasFriend<T> &); // bound template friend
};

注意:report()本身并不模板函数,而只是使用一个模板作为参数。这意味着必须为要使用的友元定义显式具体化:

void report(HasFriend<int> &) { … }; // explicit specialization for int

②. 模板类的约束模板友元函数

首先,在类定义的前面声明每个模板函数:

template <typename T> void counts();
template <typename T> void report(T &);

然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:

template <typename TT>
class HasFriendT {
friend void counts<TT>();
friend void report<>(HasFriend<TT> &);
};

声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断出这样的模板类型参数:HasFriendT<TT>。然而,也可以使用:report< HasFriendT<TT> >(HasFriendT<TT> &)。但counts()函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。

最后,为友元提供模板定义。

③. 模板类的非约束模板友元函数

对于非约束友元,友元模板类型参数与模板类类型参数是不同的:

template <typename T>
class ManyFriend {
template <typename C, typename D> friend void show(C &, D &);
};

模板别名

template <typename T>
using arrtype = std::array<T, 12>; // template to create multiple aliases

这将arrtype定义为一个模板别名,可使用它来指定类型:

arrtype<double> gallons;     // gallons is type std::array<double, 12>
arrtype<int> days;           // days is type std::array<int, 12>
arrtype<std::string> months; // months is type std::array<std::string, 12>

C++11允许将语法using =用于非模板。

typedef const char * pc1;       // typedef syntax
using pc2 = const char *;       // using = syntax
typedef const int *(*pa1)[10];  // typedef syntax
using pa2 = const int *(*)[10]; // using = syntax

Leave a comment

邮箱地址不会被公开。 必填项已用*标注