友元类:既不是is-a关系,也不是has-a关系,比如电视和遥控。遥控可以改变电视的状态,因此应该把遥控作为电视的友元。友元类声明可以在public、protected、private部分,位置没有影响。若B是A的友元类,那么编译器需要先了解A类然后才能处理B类,方法是先定义A类,或者使用前向声明。一个例子如下:
class Tv
{
friend class Remote;
};
class Remote
{
};
Remote类可以访问Tv类的私有成员。有时候Remote只有部分成员需要访问Tv类的私有成员,这时候可以如下编码:
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
};
这样声明Remote类的set_chan函数是Tv类的友元,但是有一个问题是,编译器需要知道Remote的定义才能在Tv类中处理这个友元函数,因此需要把Remote类定义放在Tv类的前面,然而Remote类的方法提到了Tv类对象,这就意味着需要把Tv类放到前面,解决这个问题的方案是使用前向声明。排列顺序如下:
class Tv;
class Remote{};
class Tv{};
不能按照如下顺序:
class Remote;
class Tv{};
class Remote{};
因为编译器处理到Tv类的友元的时候,除了需要看到Remote类的声明,还要看到该函数的声明,而这个声明是在Remote类定义中提供的。还有一个问题,若Remote类中有如下这样的内联函数:
void func(Tv &t){t.lala();}
此时编译器需要看到Tv类中lala()的声明,但是此声明是在Tv类定义中提供的,这个问题只能通过编码解决,因为即使类定义中只有声明,也可以在定义的时候加inline使其成为内联函数。
还要注意,Remote类中的函数定义必须在Tv类定义后面给出,因为此时需要Tv类中定义的方法。
让Remote类成为Tv类的友元的时候,不需要将Remote类前向声明的原因是,友元语句已经说明了Remote是一个类。
两个类相互成为对方的友元,应该如下编码:
class Tv
{
friend class Remote;
public:
void buzz(Remote & r);
};
class Remote
{
friend class Tv;
public:
void Bool volup(Tv & t){t.volup();}
};
inline void Tv::buzz(Remote & r)
{
}
其中,Tv类的buzz方法使用了Remote对象,可以在Remote类定义之前声明,因为通过友元已经知道Remote是一个类,但是定义必须在Remote的方法定义之后。
共同的友元:这种友元需要使用的场景是,一个函数需要访问两个类的私有数据,这个函数显然不能作为两个类的成员函数。可以作为一个类的成员函数,另一个类的友元,但是一般将其作为两个类的友元。此时按照如下顺序书写:
class B;
class A
{
friend void func(A & a, B & b);
};
class B
{
friend void func(A & a, B & b);
};
inline void func(A & a, B & b)
{
}
嵌套类:和包含的不同是,包含是将另一个类的对象作为自己的数据成员,而嵌套是在本类内部定义另一个类,如下:
class Queue
{
private:
class Node
{
public:
Item item;
Node * next;
Node(const Item & i) : item(i), next(0){}
};
};
若在方法文件中定义构造函数,需要两次作用域解析运算符,如下:
Queue::Node::Node(const Item &i) : item(i), next(0){}
嵌套类作用域:若嵌套类是在private部分定义的,那么只有后者知道它;若在protected部分定义,那么外部不可见,但是派生类可以直接创建这种类型的对象;若在public部分定义,那么外部可见,但要使用类限定符。
嵌套类访问规则:对嵌套类的访问规则和常规类一样,即使Queue类,也只能显式访问Node类的public成员,因此将Node类的数据成员定义成public的,由于Node在Queue内部,因此对外部世界来说即使是public也是不可见的。
abort():在头文件cstdlib中提供,功能是终止程序,发送标准错误流,是否刷新文件缓冲区因实现而异
exit():和abort()类似,刷新文件缓冲区,但是不显示消息。
异常机制:c++异常机制有3个组成部分:
1.引发异常
2.使用处理程序捕获异常
3.使用try块
一个标准的模板如下:
try {
throw "error";
}
catch (const char * s)
{
cout << s << endl;
}
try语句中throw用于抛出异常,catch格式类似函数,用括号接收throw抛出的类型。和label不同,若try中没有引发异常,那么catch语句块中的内容不会被执行。通过throw引发异常之后沿着调用栈后退,找到第一个try块,然后向下找catch语句。可以有catch语句,捕获不同类型的异常,一般参数使用对象实现的,即不同的错误类。
一个使用对象作为异常类型的例子:
class Test1
{
private:
int i;
public:
Test1(int _i = 0) : i(_i){}
void func(){cout << i << endl;}
};
class Test2
{
private:
string s;
public:
Test2(string _s = "none") : s(_s){}
void func(){cout << s << endl;}
};
try {
if (...)
throw Test1(10);
if (...)
throw Test2("error");
}
catch (Test1 & t)
{
t.func();
}
catch (Test2 & t)
{
t.func();
}
栈解退:try中涉及多层函数调用的时候,引发异常,发生栈解退,找到第一个try块,在这个过程中,所有被解退的函数中的类对象都会被自动析构。
关于抛出对象:throw一个对象的时候,编译器总是创造这个对象的副本,即使catch要捕获的是引用,因为若try中的函数被解退了,那么这个对象就不复存在了。catch语句中捕获引用的好处是,可以使用基类对象的引用,当定义一个派生链的异常类的时候,可以在最后的catch语句中使用基类的引用,捕获所有异常,即按照派生链相反的顺序排列catch语句。
exception类:在头文件<exception>中提供,是所有异常类的基类,有一个what()的虚方法,返回一个字符串,这个方法可以在派生类中重写。c++库提供了一些从exception类中派生而来的常用异常类:
1.stdexcept类,在头文件stdexcept中提供,定义了logic_error和runtime_error类,这两个类直接从exception类public继承,是一系列派生类的基类,logic_error描述了典型逻辑错误,其子类包括:
domain_error:数学函数定义域异常,传入了非法值
invalid_argument:给函数传递了意料之外的值
length_error:没有足够的空间完成操作,比如string.append有长度限制,若超长抛出此异常
out_of_bounds:数组索引无效
runtime_error描述了可能在运行期间发生但是难以预计和防范的错误,其子类有:
range_error:计算结果不在允许的范围内,但是不是上溢或者下溢
overflow_error:上溢错误
underflow_error:下溢错误,浮点数计算时会发生,存在一个浮点数能表示的非零最小值,比这个值还小抛出此异常
2.bad_alloc异常:使用new分配内存出现问题抛出此异常,老的实现是返回一个NULL指针。
3.空指针和new:c++允许调用者显式控制是否开启bad_alloc异常,如下:
int * p = new (nothrow) int;
这样若内存分配失败,那么返回空指针,而不是抛出异常。
异常和动态申请内存:在栈解退的时候,对象会被自动调用析构函数,但是用new动态分配的内存不会被释放,导致内存泄漏,这个问题只能靠编程解决,在catch块中增加清理机制。
RTTI:运行阶段类型识别,主要用来处理派生链中不知道具体对象类型的问题,c++中3个为RTTI使用的元素:
1.dynamic_cast,使用一个指向基类的指针生成一个指向派生类的指针,否则返回空指针
2.typeid:返回一个指出对象的类型的值
3.type_info:存储有关特定类型的信息
RTTI只适用于包含虚函数的类,否则不会有同名函数,没必要使用。
1.dynamic_cast
一个dynamic_cast使用的例子如下:
class Test1
{
public:
virtual ~Test1(){}
};
class Test2 : public Test1
{
};
Test1 * p1 = new Test1;
Test2 * p2 = dynamic_cast<Test2 *>(p1);
这里p2会是一个空指针,因为p1指向的是Test1的内容,而Test2中可能新增内容,导致不安全,此运算符用法如下:
dynamic_cast<Type *>(p)
若p指向的类型是Type,或者使用Type派生而来的类型,那么将p转换成Type *类型,否则返回空指针,这个运算符就是用来检查指针转换安全性的,也可以用于引用类型转换,但是没有空指针对应的引用值,因此无法使用类似空指针这样的返回值表示失败。当请求不正确时,dynamic_case引发类型为bad_cast的异常,使用exception派生出来的,在头文件typeinfo中提供,使用方法如下:
#include <typeinfo>
try {
Superb & rs = dynamic_cast<Superb &>(rg);
}
catch (bad_cast &)
{
}
2.typeid运算符和type_info类
typeid运算符用来确定两个对象是否是相同的类型。可以接受两种参数:
类名
结果为对象的表达式
返回值是对一个type_info对象的引用,type_info是在头文件typeinfo中提供的一个类,重载了==和!=运算符,可以对类型进行比较。使用方法如下:
typeid(Magnificent) == typeid(*pg)
若pg是Magnificent类型的指针,那么表达式的值是true,否则是false。若pg是空指针,引发bad_typeid异常
type_info类提供了一个name(),用来返回类型名,这个实现随厂商而异,比如int类型可能是i或者int,使用方法如下:
cout << typeid(*pg).name() << endl;
类型转换运算符:除了dynamic_cast,还有几种,所有类型转换运算符如下:
dynamic_cast,const_cast,static_cast,reinterpret_cast
const_cast:只允许类型改变值为const或者volatile,语法和dynamic_case相同,如下:
High bar;
const High * pber = &bar;
High * pb = const_cast<High *>pber; //valid
const Low * pl = const_cast<Low *>pber; //invalid
static_cast:语法和前面两个相同,只有两边有一边可以进行隐式转换的时候,才可以进行,如下,其中Pond是无关类,所以最后一个语句是非法的:
High bar;
Low blow;
High * pb = static_cast<High *>(&blow); //valid
Low * pl = static_cast<Low *>(&bar); //valid
Pond * pmer = static_cast<Pond *>(&blow); //invalid
reinterpret_cast:语法一致,不允许删除const,会执行依赖于实现的操作,如下:
struct dat
{
short a;
short b;
};
long value = 0xA224B118;
dat * pd = reinterpret_cast<dat *>(&value);
cout << hex << pd->a << endl;
这种操作和硬件强相关,不可移植。reinterpret_cast有限制,比如不能将指针转换成更小的整型或者浮点型,不能将函数指针转换成数据指针。