用于大型程序的工具
异常处理
异常使得我们能够将问题的检测与处理过程分离开来
抛出异常
我们通过抛出一条表达式来引发异常,表达式类型和调用链共同决定了哪段处理代码将被用来处理该异常,处理代码是在调用链中与抛出对象类型匹配的最近的处理代码throw range_error("error");
或者
range_error r("error");
throw r;
执行throw后,跟在throw后面的语句不再执行,反而程序的控制权转到与之匹配的catch模块,catch可以是同一个函数中局部catch,也可以是调用该函数的其他函数中。这意味着:沿着调用链的函数可能会提早退出;一旦执行异常处理,沿调用链创建的对象被销毁。并将控制流返回给catch块下面的子句(而非抛出异常的子句)
- 栈展开过程:与try关联的catch子句,找到了匹配的catch就用它处理,若没找到且该try嵌套在其他try中,则检查与外层try匹配的catch子句,如果还没找到,就退出当前函数,在外层函数中寻找,直到退出主函数为止(**如果异常没有被捕获,则中止当前的程序)
- 栈展开过程中,若退出了某个块,则该块中局部变量自动销毁
- 如果一个块分配了资源,并且在释放代码前发生了异常,就有可能跳过这些代码,但是类对象的析构函数总会被执行,所以用类来管理资源,就能确保异常发生时能正确释放资源。此外,因为总会执行析构函数,所以析构函数不能抛出不能被自身处理的异常,否则程序会被terminate,应当将可能抛出异常的操作放在try语句块中
- 异常对象被异常抛出表达式来拷贝初始化,表达式的静态编译时类型决定了异常对象的类型,
void exercise(int *b, int *e)
{
vector<int> v(b,e);
int *p=new int[v.size()];
ifstream in("ints");
//若此处发生异常,会产生内存泄漏。
}
struct intArray
{
intArray() : p(nullptr) { }
explicit intArray(std::size_t s):
p(new int[s]) { }
~intArray()
{
delete[] p;
}
int *p;
};
void exercise(int *b, int *e)
{
vector<int> v(b,e);
intArray p(v.size());
ifstream in("ints");
//若此处发生异常,会产生内存泄漏。
}
捕获异常
catch子句中的异常声明看起来像只包含一个形参的函数形参列表,声明的类型决定了处理代码捕获的异常类型,必须是完全类型,且左值。进入catch语句后,通过异常对象初始化异常声明中的参数,如果catch的参数类型非引用,则参数是异常对象的一个副本,否则是异常对象的别名。
如果catch的参数是基类类型,可以用派生类类型异常对象对其初始化,此时如果参数是非引用类型,则异常对象被切掉一部分,如果参数是基类的引用,则该参数以常规方式绑定到异常对象上。参数的静态编译类型将决定catch语句执行的操作,
- 被挑选的catch语句是第一个与异常匹配的语句,所以对形参匹配规则限制更严,但是以下是允许的:非常量向常量-派生类向基类-数组到指针
- 如果一个catch语句不能完整处理异常,就重新抛出异常将异常传递给另外一个catch语句,仍然通过throw,但是不包含任何表达式,它只能出现在catch语句或者catch语句调用的函数之内,如果在处理代码之外的区域遇到了空throw语句,则terminate。只有当catch异常声明是引用类型时,我们对参数所作的改变才会被保留并且继续传播
catch (error &obj)
{
obj.status = errCodes::badErr;//修改了异常对象的局部副本
throw;
}
- 通过catch(…)可以捕获所有异常
void maip()
{
try{
//抛出异常
}
catch(...)
{
//作局部处理
throw;
}
}
try语句块与构造函数
异常如果发生在处理构造函数初始值的过程中,这时构造函数体内的try还未生效,所以构造函数体内的catch语句无法处理初始值列表抛出的异常。
要想处理这种异常,必须把构造函数写成函数try语句块 这使得catch既能处理函数体,也能处理构造函数初始化过程
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try:data(std::make_shared<std::vector<T>>(il))
{
}
catch(const std::bad_alloc &e){handle_out_of_memory(e);}
try是出现在构造函数初始值列表的冒号和构造函数体的花括号之前的,与这个try关联的catch可以处理构造函数体抛出的异常和成员初始化列表抛出的异常,但是它不能处理初始化构造函数形参时的异常。需要在外部调用时处理。
noexcept标识符
指定noexcept标识符,指定某个函数不会抛出异常,如void recoup(int) noexcept
这有助于简化调用该函数的代码,其次,编译器确认函数不会抛出异常,就能执行某些特殊的优化操作。
noexcept要么出现在函数所有声明和定义语句中,要么一次也不出现,并且在尾置返回类型之前。在成员函数中,noexcept需在const和引用限定符之后,在final override =0之前。typedef或者类型别名中不能出现noexcept
- 编译器不会在编译时检查noexcept,所以有可能还是抛出异常,这时编译器会调用terminate,这一情况未定义是否进行栈展开,所以我们使用noexcept情况有两种:我们确认函数不会抛出异常;我们根本不知道如何处理异常
- 异常说明可以接受一个实参,这时他是一个返回bool类型的一元运算符
noexcept(recoup(i))
如果不抛出异常结果为true否则为false 更广泛的noexcept(e)
当e调用的所有函数都做了不抛出说明并且e本身不含有throw语句时 上述表达式为true,否则为false - 函数指针及其所指的函数必须有一致的异常说明。即为某个指针做了不抛出异常的声明,则该指针只能指向不抛出异常的函数。如果一个虚函数承诺不抛出异常,则派生出来的虚函数也必须做同样的动作。如果基类虚函数允许抛出异常,派生类对应函数可以抛出也可以不允许抛异常
class Base
{
public:
virtual double f1(double) noexcept;//不抛出异常
virtual int f2() noexcept(false);//可能抛出异常
virtual void f3();//可能抛出异常
}
class Derived:public Base
{
public:
double f1(double);//错误 基类承诺了不抛出异常
int f2() noexcept(false);//正确
void f3() noexcept;//正确,基类允许抛出异常
}
当编译器合成拷贝控制成员时,生成异常说明,若所有成员和基类操作承诺不抛出异常,则合成成员是noexcept的,否则是noexcept(false)
异常类处理
标准库中的异常类构成如下
exception类仅仅定义了拷贝控制成员,虚析构函数和what成员(null结尾的字符数组),派生类runtime_error
和logic_error
没有默认构造函数,而是接受一个字符串类型。
- 通常实际应用程序会定义exception的类以拓展其继承体系
class out_of_stock:public std::runtime_error
{
public:
explicit out_of_stock(const std::string &s):std::runtime_error(s){}
};
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 &rhs,const std::string &lhs): std::logic_error(s),left(lhs),right(rhs){}
const std::string left,right;
}
异常的层次越低,表示的异常情况就越特殊,顶层异常是exception 第二层分为运行时错误和逻辑错误,我们的程序进一步细化上述类别:out_of_stock类表示运行时错误,isbn_mismatch表示logic_error的一个特例,程序可以通过比较对象的isbn()结果来处理这一错误。
- 自定义异常类的方式与使用标准异常类的方式完全一样,
Sales_data& Sales_data::operator+(const Sales_data& rhs)
{
if(isbn()!=rhs.isbn())
throw isbn_mismatch("Wrong isbns",isbn(),rhs.isbn());
units_sold+=rhs.units_sold;
revenue+=rhs.revenue;
return *this;
}
Sales_data item1,item2,sum;
while(cin>>item1>>item2)
{
try{
sum=item1+item2;
}
catch(const isbn_mismatch &e)
{
cerr<<e.what()<<":left isbn("<<e.left<<")right isbn("<<e.right<<")"<<endl;
}
}
使用重载加法符号会检测isbn是否相等,不相等抛出异常。
命名空间
多个库将名字放置在全局命名空间中将引发命名空间污染,而命名空间分割了全局命名空间,每个命名空间是一个作用域,
定义
命名空间定义包含两部分,分别是关键字namespace 然后是命名空间名
namespace cpp_primer
{
class A;
class B;
}//无需分号
命名空间的名字必须在定义它的作用域内保持唯一,命名空间可以定义在全局作用域内,也可以是其他命名空间中,但不能在函数或者类的内部。
- 命名空间中的每个名字表示该空间内的唯一实体,在不同命名空间内可以有相同名字的成员,定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问,
- 命名空间可以是不连续的,
namespace nsp {}
可以是打开一个新的命名空间,也可以是为该命名空间添加一些新成员,命名空间一部分成员作用是定义类及其接口,这些应当置于头文件中,但是成员的定义置于另外头文件中 - 通常,我们不把#Include 放在命名空间内部,因为这意味着将头文件中所有的名字定义成该命名空间的成员,例如一个自定义头文件在包含标准库头文件之前就打开了某个命名空间,程序会出错,因为这样意味着我们尝试将命名空间std嵌套在自定义命名空间之中。
- 命名空间中的代码可以使用同一命名空间定义的名字
#include "Sales_data.h"
namespace cplusplus_primer
{
std::istream& operator>>(std::istream& in, Sales_data& s){}
}
也可以在命名空间外部定义成员,但是此时要加上命名空间的作用域指示符
cplusplus_primer::Sales_data cplusplus::primer::operator+(const Sales_data& lhs, const Sales_data& rhs)
{
Sales_data ret(lhs);
}
完整前缀意味着改名字位于命名空间的作用域之内,尽管我们可以在命名空间外部定义成员,但是不能在一个不相关的作用域中定义这个运算符。
- 模板特例化必须在原始模板所属的命名空间中,先声明,再在外部空间定义它
namespace std{
template<> struct hash<Sales_data>;
}
template<> struct std::hash<Sales_data>
{
size_t operator()(const Sales_data &s) const
{
return hash<string>() (s.bookNo)^hash<unsigned>()(s.units_sold)^hash<double>()(s.revenue);}
}
};
- 全局命名空间以隐式方式声明,在所有程序中都存在,但是作用域运算符同样可以适用
::member_name
表示全局命名空间中的一个成员 - 嵌套的命名空间是指定义在其他命名空间中的命名空间中
namespace cplusplus_primer
{
namespace QueryLib
{
class Query {}
Query operator&(const Query&, const Query&);
}
namespace Bookstore
{
class Quote{};
class Disc_Quote:public Quote{}
}
}
嵌套的命名空间是一个嵌套的作用域,且内层空间名字会隐藏外层空间的同名成员
- 内联命名空间中的名字可以被外层命名空间直接调用,无需再添加表示该命名空间的作用域标识符
inline namespace Fifth{}
namespace Fifth
{
class Query_base;
}
namespace std
{
namespace Fifth{}
}
因为Fifth是内联的,所以可以通过std::直接访问Query_base这个类,
- 未命名的命名空间是指关键字namespace 后紧跟花括号连起来的一系列声明语句。未命名空间中定义的变量具有静态声明周期,直到程序结束才销毁,可以直接使用,它可以在某个文件中不连续,但不能跨越多个文件。不同文件的未命名空间,定义的名字哪怕相同,也是不同的实体。如果一个头文件定义了未命名空间,则该命名空间中定义的名字在每个包含了头文件的文件中对应不同实体
namespace local
{
namespace
{
int i;
}
}
local::i =42;//可以访问嵌套的未命名空间
这种做法可以取代静态声明,使某个变量在整个文件范围内可见
使用命名空间成员
-
可以为一个命名空间的名字设定别名
namespace primer=cplusplus_primer
也可以指向嵌套命名空间namespace Qlib=cplusplus_primer::QueryLib;
但是不能在命名空间未定义时声明别名 -
using 声明
using std::string
一次只会引入命名空间的一个成员,有效范围从using声明地方开始,到声明所在的作用域结束为止,而using指示 是引入命名空间的名字,如using namespace std
可以出现在全局作用域、局部作用域和命名空间作用域,但不能出现在类作用域中。 -
using指示将命名空间成员提升到命名空间和using指示的最近作用域(命名空间外层作用域),是令所有命名空间的内容有效,而using声明是令名字在作用域内有效。using指示一般看作出现在最近的外层作用域当中。例如:有一个命名空间A 和函数f 定义在全局作用域中,如果f有一个对A的using指示,则f认为A中名字出现在全局作用域f之前的位置。
namspace A
{
int i,j;
}
void f()
{
using namespace A;
cout<<i*j<<endl;//使用的是A空间中的i和j
}
namespace blip{
int i=16,j=15,k23;
}
int j=0;
void manip()
{
using namespace blip;//引入变量
++i;//blip::i设定为17
++j;//二义性错误
+::j;//全局j为1
++blip::j;//blip::j 加1
int k=97;//局部k隐藏了 blip::k
++k;//对局部k操作
}
3.using指示造成的二义性错误只有在使用了冲突名字的地方才能被发现,意味着可能将特定库引入很久后才爆发冲突,不利于排查。而using声明引发的二义性问题在声明处就可以发现。
4. 头文件如果在顶层作用域中有using指示或者声明,就会将该名字注入到所有包含该头文件的文件中,头文件最多在函数或者命名空间内使用using指示或者声明。
类 命名空间及作用域
由内向外依次查找每个外层作用域,直到最外层的全局命名空间查找过程中止,对于命名空间中的类来说,当成员函数使用某个名字,首先在成员中查找,然后是类,然后是外层作用域
namespace A{
int i;
int k;
class C1{
public:
C1():i(0),j(0){}
int f1() {return k;}
int f2() {return h;}//此时h未定义
int f3();
private:
int i;//隐藏了A::i
int j;
};
int h=i;//用A::i 初始化
}
- 当我们给函数传递一个类类型的对象,除了在常规作用域查找,还会查找实参类所属的命名空间,这一规则允许作为类接口的一部分非成员函数可以不用using声明就可以访问。比如<<运算符,会从cin的命名空间中查找函数名,就会找到operator<< 否则,就只能显式调用该运算符了
std::operator<<(std::cin,s)
- 如果在应用程序定义了标准库已有的函数名,要么按重载规则执行相应版本,要么不执行标准库版本,比如std::move 它是模板函数,又是右值引用形参,能接受任何参数,所以经常冲突。所以推荐使用
std::move
以避免。 - 类声明一个友元时,该声明并没有使得友元可见,但是,一个未声明的类或函数若第一次出现在友元声明中,认为它是最近的外层命名空间的成员
namespace A
{
class C
{ //两个友元,仅在此声明,由于第一次出现在友元声明,隐式地称为命名空间A的成员
friend void f2();
friend void f(const C&);
};
}
int main()
{
A::C obj;
f(cobj);//接受类类型的实参,会在实参类所属命名空间查找,故可以找到
f2();//无参数,不能被找到 必须A::f2()
}
重载与命名空间
using声明或指示能将某些函数添加到候选函数集
- 类类型实参函数,名字查找在实参类所属的命名空间中进行,对于重载函数,会在每个实参类及其基类所属命名空间中搜寻候选函数
namespace NS{
class Quote{};
void display(const Quote&){}
}
class Bulk_item:public NS:Quote {}
int main()
{
Bulk_item book1;
display(book1);
return 0;
}
传递给display的参数为类类型,在Bulk_item
所属的命名空间和基类命名空间查找候选函数
- using声明指定的是名字,而非函数,所以引入了重载函数的所有版本,如果出现在局部作用域中,引入的名字将隐藏外层作用域的相关声明,如果该using声明所在作用域已有一个同名函数且形参列表相同,则该using声明引发错误
- using指示将命名空间的成员提升到外层作用域中,
namespace libs_R_us{
extern void print(int);
extern void print(double);
}
void print(const std::string &);
using namespace libs_R_us;//将名字添加到print调用的候选函数集
void fooBar(int ival)
{
print("Value: ");//调用全局函数
print(ival);//调用libs_R_us::print(int)
}
using指示引入与已有形参相同的函数不会产生错误
- 存在多个using指示,每个命名空间名字都会成为候选函数集合的一部分
多重继承与虚继承
指从多个直接基类产生派生类。
多重继承
class Bear: public ZooAnimal{}
class Panda:public Bear, public Endangerded {}
多重继承的派生列表只能包含已经定义过的类,且不能是final的。
- 多重继承关系中,派生类对象包含每个基类的子对象
- 派生类的构造函数初始化所有基类,构造顺序与派生类列表中基类的出现顺序一致,而非构造函数初始值列表中基类的顺序。
- 派生类可以从基类继承构造函数,但如果从多个基类中继承了相同的构造函数,则程序产生错误,如果一个类从它的多个基类中继承了相同的构造函数,则必须为该构造函数定义自己的版本
struct D2:public Base1, public Base2
{
using Base1::Base1;
using Base2::Base2;
D2(const string &s):Base1(s),Base2(s){}
D2()=default;
}
- 派生类的析构函数只负责清除派生类本身分配的资源。
- 多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整对象上进行该操作;
类型转换与多个基类
多个基类的情况下,派生类的指针或者引用能自动转换成一个可访问基类的指针或引用,编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好,故有可能发生二义性错误
void print(const Bear &);
void print(const Endangered &);
Panda A("yang");
print(A);//会报二义性错误
- 指针或者引用的静态类型决定了我们能使用哪些成员,如Bear类型指针只能访问Bear及其基类ZooAnimal的成员
多重继承下类作用域
只有一个基类,派生类的作用域嵌套在直接基类和间接基类的作用域中,查找过程沿着继承体系自下到上进行,直到找到所需的名字。而在多重继承的情况下,查找将在所有直接基类中进行,如果在多个基类中都被找到,则对该名字的使用具有二义性。
在使用从不同基类继承而来的相同名字的成员时,必须明确指出它的版本,否则引发二义性错误。为了避免,可以在派生类中为该函数定义一个新版本
double Panda::max_weight() const
{
return std::max(ZooAnimal::max_weight(),Endangered::max_weight());
}
虚继承
派生类可以多次继承同一个类,通过两个直接基类分别继承同一个间接基类。如果某个类在派生过程中出现了多次,则派生类中包含该类的多个子对象。这种继承方式对形如iostream
的类是行不通的,因为这种对象希望在同一个缓冲区中进行读写操作,如果真的包含了两份基类的拷贝,则上述的共享行为是无法实现的。
我们通过虚继承的机制解决上述问题,共享的基类子对象为虚基类,这样,无论虚基类在继承体系中出现了多少次,在派生类中只包含唯一一个共享的虚基类子对象。
class Raccon : public virtual ZooAnimal{};//virtual 和 public的顺序随意 指定ZooAnimal为这两个的虚基类
class Bear : virtual public ZooAnimal{};
class Panda:public Bear, public Raccoon, public Endangered{};
定义了两个虚基类,表示在后续的派生类当中共享虚基类的一份实例,同时,也不影响多态性即派生类向基类的转换
- 虚基类中只有唯一共享的子对象,所以基类成员可以直接访问,如果此虚基类的成员只被一条派生路径覆盖,可以直接访问,如果多于一个基类覆盖,则为了防止二义性,派生类需要自定义版本。
例如:B定义了成员x D1 D2 是从B虚继承得到的,D又继承了D1 D2。则D1 D2都有x定义时,才会出现二义性问题,否则可以直接访问x
构造函数与虚继承
虚派生中,虚基类由最终的派生类初始化,如创建一个panda对象,panda位于派生的最底层,并负责初始化共享的ZooAnimal基类部分
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){}
- 含有虚基类的对象构造顺序,先初始化虚基类,接下来按照直接基类在派生列表中出现的次序对其进行初始化,如果最底层派生类没有显式初始化虚基类,则调用虚基类的默认构造函数,若没有,则报错。
- 一个类可以有多个虚基类,按在派生列表中出现的顺序从左向右依次构造,合成的拷贝和移动构造函数也按此顺序执行