第十七章 Tools for Large Programs 用于大型程序的工具
这一章值得仔细品味。
17.1 异常处理(Exception Handling)
C++异常处理包括:
1. throw 表达式(throw expressions), throw 引发了异常条件。a throw raises an exceptional condition.
基本形式:
// first check that data is for the same item
if (!item1.same_isbn(item2))
throw runtime_error("Data must refer to same ISBN");
|
2. try程序块try blocks,
基本形式:
while (cin >> item1 >> item2) {
try {
// execute code that will add the two Sales_items
// if the addition fails, the code throws a runtime_error exception
} catch (runtime_error err) {
// remind the user that ISBN must match and prompt for another pair
cout << err.what()
<< ""nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if (cin && c == 'n')
break; // break out of the while loop
}
}
|
3. 由标准库定义的异常类集合。这些异常类用于在throw和相关的catch这件传递错误信息。A set of exception classes defined by the librarywhich are used to pass the information about an error between a throw and an associated catch.
17.1.1. 抛出类类型的异常Throwing an Exception of Class
异常是由抛出对象引起的。对象的类型决定哪一个句柄会被调用。An exception is raised by throwing an object. The type of that object determines which handler will be invoked.
抛出异常和捕捉异常和实参如何传递给函数类似。异常是一个对象,这个对象可以传递给非引用的形参,因此异常的了类类型必须是可以拷贝的类类型。Exceptions are thrown and caught in ways that are similar to how arguments are passed to functions. An exception can be an object of any type that can be passed to a nonreference parameter, meaning that it must be possible to copy objects of that type.
当执行throw的时候,throw后面的语句就不会被执行。控制权由throw转移到匹配的catch。When a throw is executed, the statement(s) following the throw are not executed. Instead, control is transferred from the throw to the matching catch.
throw表达式初始化一个特殊对象,这个对象称为异常对象(exception object)。例如:runtime_error就是一种异常对象。Instead, the throw expression is used to initialize a special object referred to as the exception object.
异常与指针Exceptions and Pointers
抛出指针总是坏主意。抛出指针要求指针所指向的对象在对应处理句柄地方一定是存在的。Throwing a pointer requires that the object to which the pointer points exist wherever the corresponding handler resides.
17.1.2. 栈展开Stack Unwinding
为局部对象调用析构函数Destructors Are Called for Local Objects
栈展开期间,局部对象使用的内存被释放,类类型的局部对象的析构函数被调用。During stack unwinding, the memory used by local objects is freed and destructors for local objects of class type are run.
但是在栈展开期间,资源不会被释放。什么是资源?例如通过调用new动态分配内存。如果程序块是因为exception退出,编译器是不会删除指针的。这样分配的内存也就不会被释放。a block might dynamically allocate memory through a call to new. If the block exits due to an exception, the compiler does not delete the pointer. The allocated memory will not be freed.
析构函数不能抛出异常Destructors Should Never throw Exceptions
栈展开期间,执行析构函数,异常已经产生,但是还没有处理。Destructors are often executed during stack unwinding. When destructors are executing, the exception has been raised but not yet handled.
如果在栈展开期间析构函数也抛出自己的异常,这个异常不会被处理,只会导致调用terminate函数。terminate函数会调用abort函数,强制退出。while stack unwinding is in progress for an exception, a destructor that throws another exception of its own that it does not also handle, causes the library terminate function is called. Ordinarily, terminate calls abort, forcing an abnormal exit from the entire program.
结论:让析构函数做任何会引发异常的事情,都是坏主意。it is usually a very bad idea for a destructor to do anything that might cause an exception.
析构函数和构造函数
构造函数是可以引发异常的。构造函数引发异常时,对象可能已经部分被构造。即使对象已经被部分构造了,我们也必须保证已经构造的成员能够被撤销。Even if the object is only partially constructed, we are guaranteed that the constructed members will be properly destroyed.
未捕获的异常终止程序Uncaught Exceptions Terminate the Program
如果没有匹配的catch,程序就会调用terminate函数。If no matching catch is found, then the program calls the library terminate function.
但是这种情况在Java里是不可能发生的,因为编译的时候就已经翘了。
17.1.3. 捕获异常(Catching an Exception)
异常说明符Exception Specifiers在catch语句里就像是包含一个形参的形参列表。
发现匹配的异常处理句柄(Finding a Matching Handler)
抛出异常的类型和catch说明符(exception specifier)必须完全匹配。只允许以下几种转换:
- 从非const到const的转换。抛出的非const对象异常可以匹配接受const引用的catch。Conversions from nonconst to const are allowed. That is, a throw of a nonconst object can match a catch specified to take a const reference.
- 从派生类到基类的转换。Conversions from derived type to base type are allowed.
- 数组转换为指向数组类型的指针;函数转换为指向函数类型的指针。An array is converted to a pointer to the type of the array; a function is converted to the appropriate pointer to function type.
异常说明符Exception Specifiers
异常说明符就是catch语句后面紧跟的形参。它可以是一般的对象,也可以是引用。
catch(exception &e) { //粉色部分就是异常说明符
// do cleanup
// print a message
cerr << "Exiting: " << e.what() << endl;
size_t status_indicator = 42; // set and return an
return(status_indicator); // error indicator
}
|
异常说明符和继承
基类的异常说明符可以用来捕捉其派生类型的对象。an exception specifier for a base class can be used to catch an exception object of a derived type.
异常说明符的静态类型决定了catch语句可以执行的操作。the static type of the exception specifier determines the actions that the catch clause may perform.这实际上意味,如果异常对象是派生类,但是catch语句的形参是基类类型,那么异常对象中的派生类部分就不能使用。
所以,通常,处理继承相关的异常类型的catch语句应该定义引用类型的形参。Usually, a catch clause that handles an exception of a type related by inheritance ought to define its parameter as a reference.
catch语句的顺序必须反映类型的层次关系
多个具有继承关系的catch语句应该是按从继承层级的最高级到最低级的顺序进行排序。也就是越靠后越接近基类。Multiple catch clauses with types related by inheritance must be ordered from most derived type to least derived.总而言之就是要保证派生类的句柄在基类的catch语句的前面。handlers for a derived type occurs before a catch for its base type.
17.1.4. 重新抛出(Rethrow)
rethrow仅仅就是throw;
虽然rethrow没有指定自己的异常,但是一个异常对象依然会沿着调用链向上传递。抛出的异常是最初的异常对象,而不是catch的形参。当catch的形参是基类类型时,我们不可能知道rethrow抛出的异常的实际类型。这个类型依赖于异常对象的动态类型,而不是静态类型。Although the rethrow does not specify its own exception, an exception object is still passed up the chain. The exception that is thrown is the original exception object, not the catch parameter. When a catch parameter is a base type, then we cannot know the actual type thrown by a rethrow expression. That type depends on the dynamic type of the exception object, not the static type of the catch parameter.
一般情况,catch也许会修改它的形参。如果,形参修改后,catch重新抛出异常,那么这些修改只有在异常说明符是引用时,才会继续传播。In general, a catch might change its parameter. If, after changing its parameter, the catch rethrows the exception, then those changes will be propagated only if the exception specifier is a reference:
17.1.5. 捕获所有异常的句柄 The Catch-All Handler
格式:
// matches any exception that might be thrown
catch (...) {
// place our code here
}
|
如果catch(...)和其它的catch语句结合起来使用,它必须在最后,否则跟在catch(...)后面的任何catch语句都不会被匹配执行。If a catch(...) is used in combination with other catch clauses, it must be last; otherwise, any catch clause that followed it could never be matched.
17.1.6. 函数try程序块与构造函数Function Try Blocks and Constructors
这是为了捕捉构造函数初始化式constructor initializer中的异常。因为构造函数的函数体里的catch语句是不能管理发生在构造函数初始化式执行过程中的异常的。A catch clause inside the constructor body cannot handle an exception that might occur while processing a constructor initializer.
function try block:
template <class T> Handle<T>::Handle(T *p)
try : ptr(p), use(new size_t(1)) //要把初始化式包含在try程序块里
{
// empty function body
} catch(const std::bad_alloc &e)
{ handle_out_of_memory(e); }
|
17.1.7. 异常类层次Exception Class Hierarchies
标准库异常类。基类excepion,其只是定义了虚函数what()。
关注由excepion派生出来的两种异常类:runtime_error和logic_error。
定义应用级的异常类Exception Classes:
class isbn_mismatch: public std::logic_error {
public:
explicit isbn_mismatch(const std::string &s): std::logic_error(s) { }
isbn_mismatch(const std::string &s,
const std::string &lhs, const std::string &rhs):
std::logic_error(s), left(lhs), right(rhs) { }
const std::string left, right;
virtual ~isbn_mismatch() throw() { } // 解释见:Section 17.1.10
};
|
使用自定义异常:
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
if (!lhs.same_isbn(rhs))
throw isbn_mismatch("isbn mismatch",
lhs.book(), rhs.book());
Sales_item ret(lhs); // copy lhs into a local object that we'll return
ret += rhs; // add in the contents of rhs
return ret; // return ret by value
}
|
17.1.8. 自动资源释放Automatic Resource Deallocation
问题提出:
void f()
{
vector<string> v; // local vector
string s;
while (cin >> s)
v.push_back(s); // populate the vector
string *p = new string[v.size()]; // dynamic array
// 如果在这个地方抛出异常,那么动态分配的数组所占用的内存就无法释放
delete [] p;
}
|
用类管理资源分配(Using Classes to Manage Resource Allocation)
简单地说就是使用定义类封装资源的获得和释放。(define a class to encapsulate the acquisition and release of a resource.)
原型:
class Resource {
public:
Resource(parms p): r(allocate(p)) { }
~Resource() { release(r); }
// also need to define copy and assignment
private:
resource_type *r; // resource managed by this type
resource_type *allocate(parms p); // allocate this resource
void release(resource_type*); // free this resource
};
|
调用方法:
void fcn()
{
Resource res(args); // allocates resource_type
// code that might throw an exception
...
}
|
结论就是不要让动态分配的资源“裸奔”L
17.1.9. auto_ptr 类The auto_ptr Class
auto_ptr 类的限制:
只能管理一个对象,不能管理数组。An auto_ptr may hold only a pointer to an object and may not be used to point to a dynamically allocated array.
对照来看安全和不安全的写法:
不安全:
void f()
{
int *ip = new int(42); // dynamically allocate a new object
// 如果此时抛出异常,ip指向的内存不会被释放
delete ip; // return the memory before exiting
}
|
安全:
void f()
{
auto_ptr<int> ap(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f
}
|
定义auto_ptr 对象:
auto_ptr<int> pi(new int(1024)); //实参是new表达式返回的对象
|
使用auto_ptr (Using an auto_ptr)
由于auto_ptr重载了解引用和箭头操作符,因此使用auto_ptr和使用一般的指针类似。The auto_ptr class defines overloaded versions of the dereference (*) and arrow (->) operators (Section 14.6, p. 523). Because auto_ptr defines these operators, we can use an auto_ptr in some ways that are similar to using a built-in pointer:
auto_ptr的拷贝和赋值操作都是破坏性的操作
之所以这么说,是因为当拷贝一个auto_ptr或者赋值给令一个auto_ptr 时,潜在的对象的所有者就从最初的传递给copy结果一方。最初的auto_ptr 被重置为非绑定状态。When we copy an auto_ptr or assign its value to another auto_ptr, ownership of the underlying object is transferred from the original to the copy. The original auto_ptr is reset to an unbound state.
测试auto_ptr
get方法返回的auto_ptr包含的潜在的指针。
if (p_auto.get())
*p_auto = 1024;
|
Reset操作
不能对指针进行地址赋值,要通过reset操作来修改指针。从而是auto_ptr绑定到一个新的指针上。
p_auto.reset(new int(1024));
|
调用auto_ptr 的reset,在auto_ptr绑定到另一个对象之前,删除auto_ptr 指向的对象。(如果有的话)Calling reset on an auto_ptr deletes the object (if any) to which the auto_ptr refers before binding the auto_ptr to another object.
17.1.10. 异常说明Exception Specifications
定义异常说明
void recoup(int) throw(runtime_error);
|
在编译的时候,编译器不能也不会试图验证异常说明。The compiler cannot and does not attempt to verify exception specifications at compile time.
异常说明和析构函数
标准的exception类希望析构函数不抛出任何的异常。可以参看17.1.2析构函数不能抛出异常。
class isbn_mismatch: public std::logic_error {
public:
virtual ~isbn_mismatch() throw() { }
};
|
虚析构函数:即使是基类指针,也会调用派生类的析构函数。如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。(唉,复习一下,就是忽然掰不开了)
异常说明和虚函数
基类的虚函数可以有不同于派生类的异常说明。然而派生类的虚函数的异常说明必须等于基类的虚函数的异常说明,或者更加严格。A virtual function in a base class may have an exception specification that differs from the exception specification of the corresponding virtual in a derived class. However, the exception specification of a derived-class virtual function must be either equally or more restrictive than the exception specification of the corresponding base-class virtual function.
KAO,直说啊,就是派生类不能增加新的异常。
基类:
class Base {
public:
virtual double f1(double) throw ();
virtual int f2(int) throw (std::logic_error);
virtual std::string f3() throw (std::logic_error, std::runtime_error);
};
|
派生类:
class Derived : public Base {
public:
//基类的f1函数不抛出异常,派生类抛出underflow_error异常,这就叫做派生类的异常说明没有基类的异常说明限制线强(less restrictive),所以错误!就是说派生类不能增加新的异常。
double f1(double) throw (std::underflow_error);
// ok: same exception specification as Base::f2
int f2(int) throw (std::logic_error);
// ok: Derived f3 is more restrictive
std::string f3() throw ();
};
|
基类的异常列表是派生类的虚函数可能会抛出异常的超集。Our code can rely on the fact that the list of exceptions in the base class is a superset of the list of exceptions that a derived-class version of the virtual might throw.
17.1.11. 函数指针的异常说明Function Pointer Exception Specifications
异常说明可以看做是函数类型的一部分,因此如果定义函数指针,这个函数指针也要提供异常说明的定义:
void (*pf)(int) throw(runtime_error);
|
如果不指定异常说明,这种指针就是指向抛出任何类型异常的匹配函数。If no specification is provided, then the pointer may point at a function with matching type that could throw any kind of exception.
当指向带有异常说明的函数指针由另一个指针初始化时,两个指针的异常说明可以不一致。但是源指针的说明至少和目标指针的显著性相同。When a pointer to function with an exception specification is initialized from (or assigned to) another pointer (or to the address of a function), the exception specifications of both pointers do not have to be identical. However, the specification of the source pointer must be at least as restrictive as the specification of the destination pointer
void recoup(int) throw(runtime_error);
// ok: recoup is as restrictive as pf1
void (*pf1)(int) throw(runtime_error) = recoup;
// ok: recoup is more restrictive than pf2
void (*pf2)(int) throw(runtime_error, logic_error) = recoup;
// error: recoup is less restrictive than pf3
void (*pf3)(int) throw() = recoup;
// ok: recoup is more restrictive than pf4
void (*pf4)(int) = recoup;
|
把函数指针的比较关系列成下表:
logic runtime_error _error -> runtime_error -> throw() 越来越more restrictive。
recoup
|
pf
|
|
runtime_error
|
runtime_error, logic_error
|
recoup is more restrictive than pf2
|
runtime_error
|
throw()
|
recoup is less restrictive than pf3
|
17.2 命名空间Namespaces
命名空间可以看做是Java中的package的概念吗?
一个命名空间就是一个作用域。A namespace is a scope.
17.2.1. 命名空间的定义Namespace Definitions
定义
注意不能以分号结束。
关键字namespace
namespace cplusplus_primer {
class Sales_item { /* ... */};
Sales_item operator+(const Sales_item&,
const Sales_item&);
class Query {
public:
Query(const std::string&);
std::ostream &display(std::ostream&) const;
// ...
};
class Query_base { /* ... */};
}
|
可以出现在命名空间的,包括:
l 类
l 变量
l 函数
l 模板
l 其它命名空间
以上这些都可以叫做的命名空间成员namespace members。
在命名空间外使用命名空间的成员
namespace_name::member_name
或者
using cplusplus_primer::Query;
命名空间可以是不连续的定义
这样可以按管理类和函数定义的相同的方式来组织命名空间。a namespace can be organized in the same way that we manage our own class and function definitions
1. 定义类的命名空间成员,以及作为类接口的一部分的函数声明与对象声明,可以放在头文件中,使用命名空间成员的文件可以包含这些头文件。
2. 命名空间成员的定义可以放在单独的文件里。
// ---- Sales_item.h ----
namespace cplusplus_primer {
class Sales_item { /* . . . */};
Sales_item operator+(const Sales_item&,
const Sales_item&);
// declarations for remaining functions in the Sales_item interface
}
// ---- Sales_item.cc ----
namespace cplusplus_primer {
// definitions for Sales_item members and overloaded operators
}
|
17.2.2. 嵌套命名空间Nested Namespaces
嵌套命名空间也遵守同样的规则,这些规则包括:
1. 在一个封装的命名空间内声明的名字会被同名的名字隐藏,这个同名的名字声明在一个嵌套的命名空间里。Names declared in an enclosing namespace are hidden by declarations of the same name in a nested namespace.
2. 对于命名空间来说,在其嵌套命名空间里定义的名字是局部的。Names defined inside a nested namespace are local to that namespace.
3. 在封装的命名空间之外的代码如果想要引用嵌套命名空间里的名字,则必须通过限定词。Code in the outer parts of the enclosing namespace may refer to a name in a nested namespace only through its qualified name.
17.2.3. 未命名的命名空间Unnamed Namespaces
对于特殊的文件来说,未命名的命名空间是局部的,并且也不能跨多个文本文件。但是在同一个文件中,它可以是不连续的。the definition of an unnamed namespace is local to a particular file and never spans multiple text files.
未命名的命名空间一般用来定义实体,这些实体对于所在的文件来说是局部实体。定义在未命名的命名空间里的名字只对包含这个命名空间的文件可见。Names defined in an unnamed namespace are visible only to the file containing the namespace.
定义在未命名空间里的名字可以在定义这个未命名空间的命名空间中找到。Names defined in an unnamed namespace are found in the same scope as the scope at which the namespace is defined.
以上这个规则就是造成下面的错误原因:
int i; // global declaration for i
namespace {
int i;
}
// error: ambiguous defined globally and in an unnested, unnamed namespace
i = 10;
|
但是如果这样:
Namespace local {
int i; // global declaration for i
namespace {
int i;
}
//error在这里引用i是错误的,引起二义性
i = 10;
}
// 在这里引用I OK
loca:i = 10;
loca::i = 10;
|
17.2.4. 命名空间成员的使用Using Namespace Members
using 声明的作用域Scope of a using Declaration
名字从using声明开始直到包含该声明的作用域结束都是可见的。外部作用域中定义的同名实体被屏蔽。The name is visible from the point of the using declaration to the end of the scope in which the declaration is found. Entities with the same name defined in an outer scope are hidden.
命名空间别名
很简单,就是一个同义词:
namespace primer = cplusplus_primer;
namespace Qlib = cplusplus_primer::QueryLib;
|
using 指示(using Directives)
基本形式:
using声明和using指示区别是啥?
using声明将名字直接放入其出现的作用域中。using声明就好像是命名空间成员的局部别名。A using declaration puts the name directly in the same scope in which the using declaration itself appears. It is as if the using declaration is a local alias for the namespace member.
using指示不是声明一个命名空间成员名称的别名。using指示具有的作用是:提升命名空间成员到最近的作用域。这样这个作用域中既包含它自己的命名空间还包含using指示指向的命名空间。A using directive does not declare local aliases for the namespace member names. Rather, it has the effect of lifting the namespace members into the nearest scope that contains both the namespace itself and the using directive.
这个说法很让人晕。看看后面大师给的example,杠杠的J即使可以看明白,我还是认为这些说法就如同孔乙己关于回字的N种写法一样,没有实用性。
后面一段,大师也是说慎用using指示,还是用using声明好些。
namespace blip {
int bi = 16, bj = 15, bk = 23;
// other declarations
}
int bj = 0; // ok: bj inside blip is hidden inside a namespace
void manip()
{
// using directive - names in blip "added" to global scope
// (提升命名空间成员到最近的作用域)
using namespace blip;
++bi; // sets blip::bi to 17
++bj; // error: ambiguous, global bj or blip::bj?
++::bj; // ok: sets global bj to 1
++blip::bj; // ok: sets blip::bj to 16
int bk = 97; // local bk hides blip::bk
++bk; // sets local bk to 98
}
|
17.2.5. 类、命名空间和作用域Classes, Namespaces, and Scope
寻址方式
名字寻址
当寻址一个名字时,我们向封闭的作用域外寻找。对于一个命名空间里的名字,它封闭的作用域或许是一个或者多个嵌套命名空间,这些命名空间最终结束于一个完全封闭的全局命名空间。只考虑在使用前就已经声明的名字,并且这些名字在块里依然是可见的。When looking for a name, we look outward through the enclosing scopes. An enclosing scope for a name used inside a namespace might be one or more nested namespaces ending finally with the all-encompassing global namespace. Only names that have been declared before the point of use that are in blocks that are still open are considered
类寻址
当一个名字在类作用域里使用时间,我们首先在成员自身里寻找,这包括基类。只有找遍类,我们才在封闭的作用域里继续寻找。当一个类封装在一个作用域里时,还要遵循同样的规则:首先是成员,然后是类,然后是在封闭的作用域,一个或者多个命名空间。When a name is used in a class scope, we look first in the member itself, then in the class, including any base classes. Only after exhausting the class(es) do we examine the enclosing scopes. When a class is wrapped in a namespace, the same lookup happens: Look first in the member, then the class (including base classes), then look in the enclosing scopes, one or more of which might be a namespace:
依赖于实参的查找和类类型形参
函数,包括重载操作符,形参是类类型(或者是类类型的指针或引用),并且函数和形参是定义在同一个命名空间,当类类型的对象当做是实参时,这样函数是可见的。Functions, including overloaded operators, that take parameters of a class type (or pointer or reference to a class type), and that are defined in the same namespace as the class itself, are visible when an object of (or reference or pointer to) the class type is used as an argument.
下面这个例子getline()无需显式调用std::getline()。因为作为实参的s和getline函数是在同一个命名空间里
std::string s;
// ok: calls std::getline(std::istream&, const std::string&)
getline(std::cin, s);
|
隐式友元声明和命名空间
如果没有可见的声明,那么友元声明具有把函数或者类声明放在作用域的效果。如果类是定义在命名空间里的,那么在同一个命名空间里, 就会声明一个不同的未声明的友元函数。If there isn't a declaration already visible, then the friend declaration has the effect of putting a declaration for that function or class into the surrounding scope. If a class is defined inside a namespace, then an otherwise undeclared friend function is declared in the same namespace
声明:
namespace A {
class C {
friend void f(const C&); // makes f a member of namespace A
};
}
|
使用:
在这里f函数调用时无需使用限定词,是因为前面所讲的“依赖实参查找”的规则。
// f2 defined at global scope
void f2()
{
A::C cobj;
f(cobj); // calls A::f
}
|
17.2.6. 重载与命名空间Overloading and Namespaces
备选函数和命名空间
命名空间在确定备选函数上有两方面的影响:
1. using声明和using指示可以增加备选函数。
2. 如果函数的形参是类类型,那么要在每个定义了这个类(或者是这个类的基类)的命名空间寻找备选函数。在这些命名空间里的函数只要名字和调用的函数相同都要作为备选函数。Each namespace that defines a class used as a parameter (and those that define its base class(es)) is searched for candidate functions. Any functions in those namespaces that have the same name as the called function are added to the candidate set.
Example:
namespace NS {
class Item_base { /* ... */ };
void display(const Item_base&) { }
}
class Bulk_item : public NS::Item_base { };
int main() {
Bulk_item book1;
//display的备选函数还应该包括命名空间NS,因为基类Item_base是在NS中定义的。
display(book1);
return 0;
}
|
重载和using声明
如果函数在命名空间内重载,那么using声明声明了这个函数的所有版本。If a function is overloaded within a namespace, then a using declaration for the name of that function declares all the functions with that name.
为了保证命名空间的接口不冲突,Using声明合并重载函数的所有版本。A using declaration incorporates all versions of an overloaded function to ensure that the interface of the namespace is not violated.
在using声明所在的作用域内,using声明引入的函数重载了其它函数声明。The functions introduced by a using declaration overload any other declarations of the functions with the same name already present in the scope where the using declaration appears.
如果using声明所在作用域内已经有同名的函数,并且这个函数的参数列表也是相同的,那么这个using声明就是错误的。If the using declaration introduces a function in a scope that already has a function of the same name with the same parameter list, then the using declaration is in error.
重载与 using 指示Overloading and using Directives
跨越多个 using 指示的重载Overloading across Multiple using Directives
如果出现多个using指示,每个命名空间的名字都成为候选集合的一部分。If many using directives are present, then the names from each namespace become part of the candidate set.
17.2.7. 命名空间与模板Namespaces and Templates
模板的显式特化必须在通用模板定义的命名空间内声明。否则特化的名字要和模板的名字不同。An explicit specialization of a template must be declared in the namespace in which the generic template is defined. Otherwise, the specialization would have a different name than the template it specialized.
17.3 Multiple and Virtual Inheritance
17.3.1. 多重继承Multiple Inheritance
定义:
class Panda : public Bear, public Endangered {
};
|
派生构造函数初始化所有的基类
基类的构造函数的调用顺序是基类出现在类派生列表中的顺序。The base-class constructors are invoked in the order in which they appear in the class derivation list.
The order of constructor invocation is not affected by whether the base class appears in the constructor initializer list or the order in which base classes appear within that list.
17.3.2. 转换与多个基类Conversions and Multiple Base Classes
指向派生类的指针或引用能够转换成任何基类的指针或引用。A pointer or reference to a derived class can be converted to a pointer or reference to any of its base classes.
使用基类指针不允许访问其它基类的成员。Using a pointer to one base does not allow access to members of another base.
确定使用哪个虚析构函数
假设所有的基类都恰当的定义了它们的析构函数是虚函数,那么不管指向的删除的对象的指针是何种类型,虚函数的处理都是一致的。Assuming all the root base classes properly define their destructors as virtual, then the handling of the virtual destructor is consistent regardless of the pointer type through which we delete the object
下面的例子就是对这段话的最好的说明。假设下面的这些指针都是指向Panda对象的,析构函数执行时,具有完全相同的执行顺序。Assuming each of these pointers points to a Panda object, the exact same order of destructor invocations occurs in each case.
delete pz; // pz is a ZooAnimal*
delete pb; // pb is a Bear*
delete pp; // pp is a Panda*
delete pe; // pe is a Endangered*
|
17.3.3. 多重继承派生类的复制控制
此前的Panda的定义:
class Panda : public Bear, public Endangered {
};
|
因此copy的顺序:ZooAnimal, Bear, Endangered.
17.3.4. 多重继承下的类作用域(Class Scope under Multiple Inheritance)
这部分其实要说明的是如何进行名字查找(name lookup)。
在单继承下,如果在一个成员函数中查找一个名字,先在函数自身中查找,然后在函数所在的类中查找,再到基类中查找。
但是在多重继承下,就会存在并发地在多个基类中查找。
//ying_yang is Panda’s object
ying_yang.print(cout); //error
ying_yang.Endangered::print(cout); //OK.
|
当一个类有多个基类时,名字查找同时发生在所有直接基类。因此就有可能一个多继承的派生类从两个或者多个基类中继承了同名的成员。此时,若不使用限定词就会产生二义性。When a class has multiple base classes, name lookup happens simultaneously through all the immediate base classes. It is possible for a multiply derived class to inherit a member with the same name from two or more base classes. Unqualified uses of that name are ambiguous.
并且,即使连个基类中的函数同名但是形参列表不一致时,也会产生二义性。因为名字查找总是在先(Name Lookup Happens First)。就是说编译器找到两个名字相同的就已经翘了。
避免用户级的二义性(Avoiding User-Level Ambiguities)
避免潜在二义性的最好的方法是在派生类里定义函数版本。The best way to avoid potential ambiguities is to define a version of the function in the derived class that resolves the ambiguity.
std::ostream& Panda::print(std::ostream &os) const
{
Bear::print(os); // print the Bear part
Endangered::print(os); // print the Endangered part
return os;
}
|
17.3.5. 虚继承Virtual Inheritance
在多继承关系中,基类有可能在派生类中出现多次。
举个例子:istream,ostream是ios的派生类,iostream继承了istream,ostream,这就意味着iostream包含两个ios的对象。问题来了L这是不可以的。
解决办法就是虚继承(virtual inheritance)。
虚继承是一种机制:类指定共享它的基类的状态。在虚继承下,不管在派生类的继承层次关系中虚基类出现多少次,只共享一个给定的虚基类的子对象。共享的基类子对象称为虚基类。Virtual inheritance is a mechanism whereby a class specifies that it is willing to share the state of its virtual base class. Under virtual inheritance, only one, shared base-class subobject is inherited for a given virtual base regardless of how many times the class occurs as a virtual base within the derivation hierarchy. The shared base-class subobject is called a virtual base class.
定义:
class istream : public virtual ios { ... };
class ostream : virtual public ios { ... };
// iostream inherits only one copy of its ios base class
class iostream: public istream, public ostream { ... };
|
17.3.6. 虚基类的声明Virtual Base Class Declaration
虚基类成员的可见性
共享虚基类的成员可以直接、无二义性的访问。类似地,如果虚基类的一个成员只沿着一个派生路径重新定义,那么重新定义的成员可以直接访问。在非虚派生下,任何一种访问都是具有二义性的。Members in the shared virtual base can be accessed unambiguously and directly. Similarly, if a member from the virtual base is redefined along only one derivation path, then that redefined member can be accessed directly. Under a nonvirtual derivation, both kinds of access would be ambiguous.
17.3.7. 特殊的初始化语义(Special Initialization Semantics)
.因为是多重继承,就存在基类被多次初始化的问题。因此需要特殊处理。
虚继承,最末层的派生类(most derived class)的构造函数初始化虚基类。In a virtual derivation, the virtual base is initialized by the most derived constructor.
虽然虚基类是被最末层的派生类初始化的,任何直接或者简洁继承虚基类的类都必须提供自己的基类初始化式。只要创建虚基类的派生类类型的对象,派生类必须初始化它的虚基类。这些初始化式只有当我们初始化中间类型的对象时才用到。Although the virtual base is initialized by the most derived class, any classes that inherit immediately or indirectly from the virtual base usually also have to provide their own initializers for that base. As long as we can create independent objects of a type derived from a virtual base, that class must initialize its virtual base. These initializers are used only when we create objects of the intermediate type.
很拗口。看例子就easy to easy。
Bear,Raccoon,Panda都定义构造函数初始化虚基类ZooAnimal。Bear,Raccoon的初始化式只有在创建Bear,Raccoon类型的对象时才用到。如果创建Panda对象,只会调用Panda的初始化式。
Bear::Bear(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Bear") { }
Raccoon::Raccoon(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Raccoon") { }
Panda::Panda(std::string name, bool onExhibit)
: ZooAnimal(name, onExhibit, "Panda"),
Bear(name, onExhibit),
Raccoon(name, onExhibit),
Endangered(Endangered::critical),
sleeping_flag(false) { }
|
构造函数与析构函数次序
不管虚基类出现在继承层次关系的哪部分,虚基类总是优先于非虚基类构造。Virtual base classes are always constructed prior to nonvirtual base classes regardless of where they appear in the inheritance hierarchy.
还是看例子,一目了然,TeddyBear由BookCharacter,Bear,ToyAnimal派生而来。Bear是虚基类ZooAnimal的派生类,这样TeddyBear就存在连个虚基类:ToyAnimal和ZooAnimal。
class Character { /* ... */ };
class BookCharacter : public Character { /* ... */ };
class ToyAnimal { /* ... */ };
class TeddyBear : public BookCharacter,
public Bear, public virtual ToyAnimal
{ /* ... */ };
|
TeddyBear构造函数的调用次序就是:先虚基类,后非虚基类。
ZooAnimal(); // Bear's virtual base class
ToyAnimal(); // immediate virtual base class
Character(); // BookCharacter's nonvirtual base class
BookCharacter(); // immediate nonvirtual base class
Bear(); // immediate nonvirtual base class
TeddyBear(); // most derived class
|
析构函数相反。