友元
-
友元类
-
友元声明可以位于公有,私有或保护部分,其所在的位置无关紧要。
-
注意类的声明的先后顺序
class Base { public: friend class Point; void show()const{cout<<x<<' '<<y<<endl;} Base(int tx,int ty):x(tx),y(ty){} Base(){x=y=0;} private: int x,y; }; class Point { public: void showx(Base &a)const{cout<<a.x<<endl;} void showy(Base &a)const{cout<<a.y<<endl;} void setBase(Base &a,int x,int y){a=Base(x,y);} }; int main() { Point tmp; Base tmp2=Base(1,2); tmp.showx(tmp2); tmp.showy(tmp2); tmp.setBase(tmp2,3,4); tmp.showx(tmp2); tmp.showy(tmp2); }
-
-
友元成员函数
- 使用前向声明,注意排列的次序
class Tv;//前向声明 class Remote{...}; class Tv{...};
- 如果Remote类中函数需要使用Tv类中的成员时,我们可以先只在类中声明,再在后面使用内联函数进行定义
- 内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。故内联定义位于头文件中。也可以将定义放在实现文件中,但必须删除关键字
inline
,这样函数的链接性将是外部的 - 将整个Remote类成为友元并不需要前向声明,因为友元语句本身已经指出Remote是一个类:
friend class Remote;
-
其他友元关系
- 互相是友元的类
- 注意声明定义的顺序
class A { friend class B; public: void buzz(B &b); ... }; class B { friend class A; public: bool check(A &a); ... }; inline void A::buzz(B &b){...} inline bool B::check(A &a){...}
- 注意声明定义的顺序
- 共同的友元
-
函数需要访问两个类的私有成员
-
注意声明和定义的顺序
-
需要前向声明
class A; class B { friend void sync(A &a,const B &b); friend void sync(B &b,const A &a); ... }; class A { friend void sync(A &a,const B &b); friend void sync(B &b,const A &a); ... }; //define the friend functions inline void sync(A &a,const B &b){...} inline void sync(B &b,const A &a){...}
-
- 互相是友元的类
嵌套类
- 嵌套类的作用域由其声明位置所决定,实际上和普通的成员没有多大不同。
- 当嵌套类的声明位于公有部分,包含类的外面才能使用嵌套类,而且必须使用作用域解析运算符(旧版本的C++不允许嵌套类或无法完全实现这种概念)
- 嵌套类,结构和枚举的作用域特征
声明位置 | 包含它的类是否可以使用它 | 从包含它的类派生而来的类是否可以使用它 | 在外部是否可以使用 |
---|---|---|---|
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
公有部分 | 是 | 是 | 是,通过类限定符来使用 |
异常
-
使用
abort()
-
其典型实现是向标准错误流(即
cerr
使用的错误流)发送消息 abnormal program termination(程序异常终止),然后终止程序。 -
返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。
-
abort()
是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。#include<bits/stdc++.h> using namespace std; int main() { int x,y; while(cin>>x>>y) { if(x+y==0) { cout<<"error"<<endl; abort(); } else cout<<2.0*x*y/(x+y)<<endl; } return 0; }
-
-
异常机制
-
组成部分
- 引发异常
- 使用处理程序捕获异常
- 使用
try
块
-
一个例子
#include<bits/stdc++.h> using namespace std; double solve(double x,double y) { if(x==-y) { throw "bad"; } return 2.0*x*y/(x+y); } int main() { double x,y; while(cin>>x>>y) { double z; try { z=solve(x,y); } catch (const char *s) { cout<<s<<endl; cout<<"error"<<endl; continue; } cout<<"answer: "<<z<<endl; } return 0; }
-
异常规范
-
告诉使用的人所有可能会返回的异常,这项工作也可使用注释轻松完成。由于这项工作很难维护,所以C++11已经摒弃了,但该用法仍在标准之中。
-
C++11支持一种特殊的异常规范,使用新增的关键字
noexcept
指出函数不会引发异常double marm() noexcept;
-
-
栈解退
- C++通常将信息放在栈中来处理函数调用。
- 程序将调用函数的指令的地址(返回地址)放到栈中,但被调用的函数执行完毕时,程序将使用该地址来确定从哪里开始继续执行。
- 当函数结束时,程序流程跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,以此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数将被调用。
- 如果函数出现异常而终止,则程序也将释放栈中的内存,当不会释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于
try
块的返回地址。随后,控制权也将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。
-
其他异常特性
- 将引用作为返回值通常可以避免创建副本以提高效率
- 基类引用可以执行派生类对象,假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配
catch
块的排列顺序应该与派生顺序相反,即层次结构最下面的异常类的catch
语句放在最前面,将捕获基类异常的catch语句放最后面catch(...){//statements}
代表捕获任何异常- 也可以去掉引用,这时则需要使用虚函数
-
exception
类- C++可以把它当作其他异常类的基类,代码就可以引发
exception
异常了。如它有一个名为what()
的虚函数,我们则可以重新声明一个它的派生类,重新定义what()
函数。 stdexcept
异常类stdexcept
定义了logic_error
和runtime_error
类,它们都是以公有方式从exception
派生而来的logic_error
类- 用于描述典型的逻辑错误
- 几个它的派生类
domain_error
- 当参数不在函数所需参数的定义域中会引起
domain_error
异常
- 当参数不在函数所需参数的定义域中会引起
invalid_argument
- 当给函数传递了一个意料之外的值,如函数希望接受一个只包括0或1的字符串,当传递的字符串包含其他字符,该函数将引发
invalid_argument
异常
- 当给函数传递了一个意料之外的值,如函数希望接受一个只包括0或1的字符串,当传递的字符串包含其他字符,该函数将引发
length_error
- 用于指示没有足够的空间来执行所需操作,如
string
类的append()
方法合并的字符串超过最大允许长度时,将引发length_error
异常
- 用于指示没有足够的空间来执行所需操作,如
out_of_bounds
- 用于索引错误
runtime_error
类- 用于描述可能在运行期间发生但难以预计和防范的错误
- 几个它的派生类
range_error
- 范围出错
overflow_error
- 上溢错误
underflow_error
- 下溢错误
- C++可以把它当作其他异常类的基类,代码就可以引发
-
-
bad_alloc
异常和new
- 对于使用
new
导致的内存分配问题,C++最新处理方式是让new
引发bad_alloc
异常。头文件new
包含bad_alloc
类的声明 - 但无法分配请求的内存量时,
new
返回一个空指针
- 对于使用
-
空指针和
new
-
很多代码都是在
new
失败时返回空指针,为处理new
的变化,有些编译器提供了一个标记开关,让用户选择所需的行为。其用法如下int *pi=new (std::nothrow)int[10000]; if(pi==0) { cout << "error"<< endl; exit(EXIT_FAILURE); }
-
-
异常何时会迷失方向
-
意外异常
-
它在带异常规范的函数中引发的,但规范列表中没有该异常
-
默认情况下,这将导致程序异常终止
-
程序首先调用函数
terminate()
。默认情况下terminate()
调用abort()
函数。可以指定terminate()
应调用的函数(而不是abort()
).为此,可调用set_terminate()
函数。它们的声明都在exception
头文件中:typedef void (*terminate_handler)();//没有参数和返回值的函数指针 terminate_handler set_terminate(terminate_handler f)throw();//C++98 terminate_handler set_terminate(terminate_handler f)noexcept;//C++11 void terminate();//C++98 void terminate()noexcept;//C++11
-
一个例子
#include<bits/stdc++.h> #include<exception> using namespace std; double solve(double x,double y)throw(const char *) { if(x+y==0) { throw out_of_range("error"); } return 2.0*x*y/(x+y); } void myQuit() { cout<<"Terminating due to uncaught exception\n"; exit(5); } int main() { set_terminate(myQuit); double x,y; while(cin>>x>>y) { double z; try { z=solve(x,y); } catch(const char *ch) { cout<<ch<<endl; continue; } cout<<z<<endl; } return 0; }
-
-
未捕获异常
- 如果异常不是在函数中引发的(或者函数没有异常规范),且没被捕获
- 默认情况下,这将导致程序异常终止
- 如果发生意外异常,程序将调用
unexpected()
函数,这个函数将调用terminate()
。我们可以使用set_unexpected()
函数。与上面类似。这些新函数也是在头文件exception
中声明的。
typedef void (*unexpected_handler)(); unexpected_handler set_unexpected(unexpected_handler f)throw();//C++98 unexpected_handler set_unexpected(unexpected_handler f)noexcept;//C++11 void unexpected();//C++98 void unexpected();//C++11
-
-
有关异常的注意事先
异常和动态内存分配并非总能协同工作,当进行栈解退时,动态申请的内存很可能并没有释放掉,所以我们需要在catch
中进行释放内存void test3(int n) { double *ar=new double[n]; try { if(oh_no) throw exception(); } catch(exception &ex) { delete [] ar; throw; } ... delete [] ar; return;
RTTI(运行阶段类型识别)
-
dynamic_cast
运算符-
使用方法
Type *p=dynamic_cast<Type *>(pt);
-
如果指向的对象
(*pt)
的类型为Type
或者是从Type
直接或间接派生而来的类型,则上面的表达式将指针pt
转换为Type
类型的指针。否则结果为0,即空指针。
-
-
typeid
运算符-
能够确定两个对象是否是同种类型。它和
sizeof
有些相像。可以接受两种参数。- 类名
- 结果为对象的表达式
-
使用方法
typeid(Base)==typeid(*p);//p为指针
-
如果p是一个空指针,程序将引发
bad_typeid
异常,该异常类型是从exception
类派生而来的,是在头文件typeinfo
中声明的。 -
type_info
类的实现随厂商而异,当包含了一个name()
成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类的名称。
-
类型转换运算符
dynamic_cast
运算符- 之前已经介绍过,该运算符的用途是,使得能够在类层次结构中进行向上转换。
const_cast
运算符-
该运算符用于执行只有一种用途的类型转换,即改变值为
const
或volatile
-
语法
const_cast< type-name> (expression)
-
type-name
和expression
的类型必须相同 -
一个例子
#include<bits/stdc++.h> using namespace std; int main() { int a=3; const int *p=&a; int *p2=const_cast<int *>(p); *p2+=4; cout<<a<<endl; // const int a2=3; const int *p3=&a2; int *p4=const_cast<int*>(p3); p4+=4; cout<<a2<<endl; } /*output: 7 3 */
当指针被声明为
const int*
时,不能修改指向的int
,指针p2和p4删除了const
特征,因此可用来修改指向的值,但仅当指向的值不是const
才行,因此p2可用于修改a,但p4不能用来修改a2。
-
static_cast
运算符-
使用语法
static_cast<type-name>(expression)
-
仅当
type_name
可被隐式转换为expression
所属的类型或expression
可被隐式转换为type_name
所属的类型时,上述转换才合法。 -
一个例子
#include<bits/stdc++.h> using namespace std; class A{}; class B:public A{}; class C{}; int main() { A a; B b; C c; A *tmp1=static_cast<A*>(&b);//valid A *tmp2=static_cast<C*>(&c);//invalid }
-
reinterpret_cast
运算符-
用于天生危险的类型转换。它不允许删除
const
,但会执行其他令人生厌的操作。 -
使用语法与另外3个相同。
-
一个例子
#include<bits/stdc++.h> using namespace std; struct dat { short a,b;//short 2个字节 }; int main() { long val=0xA224B118;//4个字节 dat *pd=reinterpret_cast<dat *>(&val); cout<<hex; cout<<pd->a<<' '<<pd->b<<endl; }
reinterpret_cast
运算符并不支持所有的类型转换。例如可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是,不能将函数指针转换为数据指针,反之亦然。
-