二十七:
(1)异常使我们能将问题的检测和解决分离开来。
当匹配不到catch时,将调用标准库函数terminate(当异常没有被捕获)
异常对象:编译器使用异常抛出表达式来对异常对象进行拷贝初始化,因此throw表达式必须拥有完全类型(只是声明如class A;不是完全类型),如果是类类型的话,相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。
静态类型决定了异常对象的类型。如果throw表达式解引用一个基类指针,而该指针实际指向派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出。
特殊的catch要出现在前面,如派生类异常的处理要在基类异常的处理之前。应该把继承链最底端的类放在前面。
如果不能完全处理某个异常,需重新抛出,仍是一条throw语句,但不包含任何表达式。
如:throw;
捕获所有异常:catch(...)
(2)要处理构造函数“初始值列表”异常,可以使用“函数try语句块”
try出现在初始值列表的冒号之前,函数体后直接跟catch(..){}
noexcept要么出现在该函数所有声明和定义中,要么一次也不要出现。
函数指针声明定义
可以有noexcept
typedef/类型别名
不能有noexcept
需要在const/引用限定符之后,final,override和虚函数=0之前。
一旦noexcept函数抛出异常,程序就会调用terminate。
鉴于早期C++版本:等价的声明:
1)void fun(int) noexcept;
2)void fun(int) throw();
void fun(int)noexcept(true);//不会抛出异常
void fun(int)noexcept(false);//可能抛出异常。
noexcept()也是一个运算符,返回bool表示是否会抛出异常。
(3)只能严不能松:
函数指针如果声明了不抛出异常,则只可以指向不抛出异常的函数;如果显式或隐式指定可以抛出异常,则可以指向任何函数。
如果虚函数承诺不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺。
如果对所有成员和基类的操作都承诺了不抛出异常,则合成的成员是noexcept的。
(4)异常类层次图:
(5)命名空间
命名空间不可以定义在函数或者类内部。作用域后面无需分号。可以是不连续的。
1)c++11内联命名空间:可以被外层命名空间直接使用,inline namespace name{} 如:
namespacenameTemp{
#include “a.h”//a.h里面有内联的命名空间,那么对于nameTemp::的访问默认就是此命名空间
#include “b.h”
}
inline必须出现在命名空间第一次定义的地方。
2)未命名的命名空间
拥有静态生命周期。第一次使用前创建,程序结束才销毁。
可以在给定的文件内不连续,但是不能跨越多个文件。多个文件的未命名空间没关系。
变量可以直接使用,但是与全局的同名变量会有二义性。
类的作于域中,using声明只能指向基类的成员。(如构造函数)
using指示,可以在全局/局部/命名空间中,但是不能出现在类的作用域中。using指示会将成员提升到包含命名空间本身和using指示最近作用域的能力。(一次注入某个命名空间的所有名字),二义性的名字只有在使用时才会被发现,而using声明引起的二义性在声明处就可以被发现。
命名空间名字的隐藏规则有一个例外:给函数传递类类型对象时,除了常规作用域,还会查找实参类型所属的命名空间。(如果是派生类,包括基类所在命名空间也会查找)
友元只是说明了对类的访问权限,在类外使用需要重新声明。如果包含类对象参数,那么不重新声明也可以直接使用。
继承关系中,对于基类的函数可以using声明引入,再选择性覆盖。
对于命名空间中,using声明会和同名同参的函数冲突。
using指示引入相同函数也不会有冲突,只要我们调用时指定好就行。
(6)
多重继承,如果使用using声明,从多个基类中继承了相同的构造函数(形参完全相同),会有错误。如:
struct D1:publicB1,public B2
{
using B1::B1;
using B2::B2;
//要避免错误,需要为该构造函数定义自己的版本。
D1(const string &s):B1(s),B2(s){}
D1() = default;//一旦定义了自己的构造函数,则必须出现
}
合成版本的拷贝/移动/赋值函数,会自动处理基类的部分。(派生类到派生类赋值,派生类到基类赋值会有截断。)
编译器不会在派生类向基类的几种转化中进行比较和选择,在他看来,转化成任何一种基类都是一样好,没有优先级。
如果名字在多个基类中被找到,会有二义性,使用时要指定它的版本。不使用也会避免二义性。(即使参数列表不同,或者私有公有都会产生二义性)
避免二义性,还可以定义新的版本。
派生类可以直接和间接继承一个类多次。
iostream要对同一个缓冲区进行读写,要用到虚继承。虚继承的基类(共享)叫虚基类。
派生类中只有一份共享的虚基类子对象。virtual可以在继承说明符public等之前或之后。
1:B有x,D1,D2,都没有x 没有二义性
2:B有x,D1或D2有一个有x, 没有二义性,但派生类比B的x优先级高
3:B,D1,D2都有x,会有二义性
解决二义性:D重新定义x
虚基类是由最低层的派生类初始化的。
构造顺序:首先构造虚基类部分,然后按照派生列表构造直接基类
虚基类总是先于非虚基类构造,跟继承体系中的次序和位置无关。
二十八:
(1)
new表达式:
1)申请空间new(malloc/allocator)operator new函数
2)构造函数 /construct (定位new 在指定地点构造对象)
delete表达式:
3)析构函数(destroy执行析构函数)
4)释放空间delete(free/deallocate)operator delete函数
我们可以重载1)4) operator new 和 operatordelete ,全局作用域或类作用域,其中类作用域中是隐式静态的。(默认是static,不用显示指定static),以为是在对象构造之前和销毁之后,所以是静态的。
//返回类型和第一个参数(不能有默认实参)必须是这样的:(可以加参数)
//这些版本可能抛出异常
void *operator new(size_t);
void *operator new[](size_t);
void *operator delete(void *) noexcept;
void *operator delete[](void *) noexcept;
//这些版本承诺不会抛出异常
void *operator new(size_t,nothrow_t&)noexcept;
void *operator new[](size_t,nothrow_t&) noexcept;
void *operator delete(void*,nothrow_t&) noexcept;
void *operator delete[](void *,nothrow_t&)noexcept;
其中,void * operator new(size_t,void*);//这种形式只供标准库使用,不能被重新定义
定位new:在指定地址构造对象:但不分配内存,指针地址不一定是动态内存。如:new (&str) string(“nihao”)
new(place_address) type(args);//红色可省
new(place_address) type[size] {initializer list}//红色可省
调用自己的new,delete程序:
#include<iostream>
using namespacestd;
class MyClass
{
public:
int i;
};
void *operatornew(size_t size)
{
cout << "调用自己的new" << endl;
if (void *mem = malloc(size))
{
return mem;
}
else
{
throw bad_alloc();
}
}
void operatordelete(void *mem) noexcept
{
cout << "调用自己的delete" << endl;
free(mem);
}
int main()
{
cout << "main开始" << endl;
MyClass *pmy = new MyClass;
pmy->i = 20;
cout << "pmy->i = "<< pmy->i << endl;
delete pmy;
cout << "main结束" << endl;
return 0;
}
结果:
(2)运行时类型识别
1)dynamic_cast:可以将基类的指针或引用安全的转成派生类的指针或引用
dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
dynamic_cast<type&&>(e);
对于指针的转换,如果失败,返回所需类型的空指针,而对于引用的转换,如果失败,抛出bad_cast异常(typeinfo头文件中)
2)typeid返回表达式的类型,(只有有虚函数的时候才能到运行时确定实际类型,否则就是静态类型)
数组就是返回数组类型,而不是指针类型。对于指针就是指针的静态编译类型,而不是指向对象的类型。
如果p是空指针,则typeid(*p)抛出bad_typeid异常
typeid操作的结果是一个常量对象的引用,对象类型是type_info(没有默认构造函数,且拷贝/移动构造函数、赋值运算符都是删除的)或它的派生类型。
(3)枚举类型
限定枚举类:enum class/struct开头,成员在作用域外不可访问(c++11),不能进行隐式类型转换成int等
不限定枚举类:省略 class/struct,成员与本身的作用域相同,可以隐式的转换。
可以用作switch
默认的类型可以在enum名字后加:类型。
前置声明:
enum intValues:unsigned long long;//不限定作用域,必须指定成员类型
enum class open_modes;//现代作用域,可以使用默认的int
(4)类成员指针
必须在*之前加上:classname::
1)数据成员指针
声明:
const stringScreen::*pdata;
auto pdata =&Screen::contents;
使用:
auto s =myScreen.*pdata;//用.*或->*运算符,先解引用然后再取值
访问控制规则对成员指针同样有效,对于pdata的使用必须位于成员或友元内部才行。
2)成员函数指针
声明:
auto pmf =&Screen::get_cursor;
这样声明的前提是该函数不接受任何实参,并且返回一个char
要声明具体指向的类型,需要自己先声明好:
char(Screen::*pmf2)(Screen::pos,Screen::pos) const;
pmf2 = &Screen::get;//取地址符不能丢
或者使用类型别名:
using Action =char (Screen::*)(Screen::pos,Screen::pos) const;
Action get =&Screen::get;
使用:
(myScreen.*pmf2)(0,0);
也可以作为函数参数,可以传递默认实参。
3)将成员函数作为可调用对象
因为成员函数指针在调用时,必须绑定到特定的对象上,所以这样的指针不支持函数调用运算符。所以不能将它传递给算法。
如:
auto fp = &string::empty;
find_if(svec.begin(),svec.end(),fp);
实际是:if(fp(*it))//错误,成员函数指针调用必须通过->*
解决办法:
<1>使用function生成可调用对象
我们需要告诉function一个事实:即empty是一个接受string参数并返回bool值的函数,通常,执行成员函数的对象被传给隐式的this形参,所以我们必须翻译该代码,使隐式的形参变成显式的。(第一个形参表示该成员是哪个对象上执行的)
function<bool (const string&)> fcn = &string::empty;
或者:
function<bool (const string*)> fcn = &string::empty;
调用时:
find_if(svec.begin(),svec.end(),fcn);
<2>使用mem_fn生成可调用对象
mem_fn让编译器推断成员的类型。
find_if(svec.begin(),svec.end(),mem_fn(&string::empty));
mem_fn生成的可调用对象,既可通过对象调用又可指针调用。
auto f =mem_fn(&string::empty);
f(*svec.begin());//传入对象,使用.*调用empty
f(&svec[0]);//传入指针,使用->*调用empty
<3>使用bind生成可调用对象
auto it =find_if(svec.begin(),svec.end(),bind(&string::empty,_1));
与function类似的地方:必须将函数中用于表示执行对象的隐式形参转换成显式的。
与mem_fn类似的是:生成的可调用对象的第一个实参即可以是string指针,也可以是string的引用。
(5)嵌套类
外层类与嵌套类的对象互相独立。
嵌套类相当于外层类的一个类型成员。可以直接使用外层类的成员,不必加类名说明符。
嵌套类里面定义的静态成员需要在外层类的作用域之外进行定义。
(6)union(默认公有)
任意时刻只有一个数据成员可以有值。某个成员赋值,其他成员是未定义的状态。不能继承或当基类,不能含有虚函数。
匿名union定义所在作用域内,它的成员可以被直接访问。不能包含受保护和私有成员,也不能定义成员函数。
如果union含有类类型成员,且该类定义了默认构造/拷贝控制成员,编译器将为union合成对应的版本并将其定义为删除的。含有union的类对应的拷贝控制成员也会使删除的。
我们可以定义一个enum或其他东西作为union判别式。(判断union此时表示的是什么类型)
(7)局部类
不能声明静态数据成员。只能访问外层作用域的类型名、静态变量、枚举成员。外面函数的普通变量不能被访问,(包括全局变量必须用::)
(8)extern “C” 连接指示
指出任意非C++函数所用的语言。
连接指示不能出现在类和函数定义的内部。
编写函数所用的语言,是函数类型的一部分,指向C函数的指针,与之相C++函数的指针是不一样的类型。
连接指示对返回类型和参数都有效。因为连接指示作用域声明语句的所有函数,所以如果我们希望给C++传入一个指向C函数的指针,必须使用类型别名。
//FC是指向C函数的指针
extern “C” typedefvoid FC(int);
//f2是一个C++函数,形参是C函数的指针。
void f2(FC *);
对连接到C的预处理器的支持:
#ifdef __cplusplus
extern “C”
#endif
int com(inti,inti);