1 友元
友元类的所有方法都可以访问原始类的私有成员和保护成员。
也可以只将特定的成员函数指定为另一个类的友元。
1.1 友元类
class Tv
{
public:
friend class Remote; // Remote can access Tv private parts
private:
int state;// on or off
}
class Remote
{
}
友元声明可以位于公有,私有或保护部分,其所在位置无关紧要。
1.1.2 友元成员函数
要想使类成员成为另一个类的友元,要在类中将其声明为友元,并且在编译Tv时必须知道Remote的定义,而Remote的方法中又用到了Tv,因此要在Remote前提到声明Tv;
class Tv;
class Remote
{
public:
Remote(int m = Tv::TV) : mode(m) {}
};
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
};
2 嵌套类
声明位置 | 包含他的类是否可以使用他 | 从包含他的类派生而来的类是否可以使用它 | 在外部是否可以使用 |
私有部分 | 是 | 否 | 否 |
保护部分 | 是 | 是 | 否 |
共有部分 | 是 | 是 | 是,通过类限定符来使用 |
2.1 模板中的嵌套
template <class Item>
class QueueTP {
private:
enum {Q_SIZE = 10};
class Node {
public:
Item item;
Node * next;
Node(const Item & i) : item(i), next(0) {}
};
}
如上代码的定义:
QueueTp<double> dq;
QueueTp<char> cq;
将不会冲突,这两个Node类将在两个独立的QueueTp类中定义。
3 异常
3.1 栈解退
假设函数出现异常,程序也将释放栈中内存,直到找到一个位于try块中的返回地址。之后,控制权将转到块尾异常处理程序,这个过程被称为栈解退。
3.2 exception类
3.2.1 stdexcept异常类
logic_error:典型的逻辑错误
domain_error:值不在有效范围
invalid_argument:函数传递了一个意料外的值
length_error:没有足够的空间来执行所需的操作
out_of_bounds:索引越界
runtime_error:典型的运行时错误
range_error:
overflow_error:
underflow_error:比如浮点类型运算时,计算结果比最小非零值还小。
3.2.2 bad_alloc异常和new
对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。
Big * pb;
try {
pb = new Big[10000];
}
catch (bad_alloc & ba) {
cout << ba.what() << endl;
exit(EXIT_FAILURE);
}
3.2.3 空指针和new
为了处理new的变化,有些编译器提供了一个标记,在new失败时如何处理
// 在失败时返回空指针
int * pi = new (std::nothrow) int;
int * pa = new (std::nothrow) int[500];
3.3 异常何时会迷失方向
未捕获异常:首先调用terminate()函数,默认情况下,terminate()调用abort()函数,可以指定terminate()调用别的函数(通过set_terminate())。
如果引发了异常规范中没有的异常:调用unexpected(),这个函数将调用terminate(),或者使用set_expected()函数来调用别的函数。
如果这个别的函数又引发了异常,并且与原来的异常规范匹配,则程序将从那里开始正常处理,即对应的catch块。
如果不匹配,且异常规范中没有包括std::bad_exception则调用terminate()。
如果包括了std::bad_exception,则不匹配的异常被std::bad_exception代替。
3.4 有关异常的注意事项
增加程序代码
降低程序的运行速度
异常规范不适用于模板(模板函数引发的异常可能随具体化而定)
异常和动态内存分配并非总能协同工作
3.5 在构造函数内阻止异常导致的资源泄漏
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address), theImage(0), theAudioClip(0)
{
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);
}
}
BookEntry::~BookEntry()
{
delete theImage;
delete theAudioClip;
}
在上述代码,如果因为内存不够导致分配内存失败,从而new AudioClip失败,那么将退出当前构造函数,从而导致Image未被删除,因为析构函数只会对完全构造好的对象进行处理。
解决方法:
1 在构造函数内部加上try catch,catch内delete对应对象(对性能有要求一般不用异常)
2 设置要初始化的对象类型为智能指针,来保证在BookEntry被销毁时,已经创建完的对象也自动销毁,不需要再手动处理。
3.6 禁止异常流出析构函数之外
错误案例如下
class Session {
public:
Session();
~Session();
...
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
}
Session::~Session()
{
logDestruction(this);
}
如果在其他位置的异常,导致调用了Session的析构函数,析构函数这里的logDestruction再抛出异常到析构函数外,就会导致程序终止。并且导致析构函数执行不全。
3.7 异常捕捉和函数调用的一些不同
1 调用一个函数时,控制权会回到调用端。抛出一个异常时,控制权不再会回到抛出端。
2 不论捕捉异常的exception是按值传递还是按引用传递,都会发生复制行为,不过按值传递会复制两次(本身exception复制一次,非右值给外部exception赋值复制一次,这样的操作不知道后续版本编译器有没有优化)。(因为如果是按引用,抛出之后对象容易销毁导致未定义行为,未消失的情况比如static变量,也会复制)
3 被抛出的exception对象,不能进行例如int转为double这样的隐式类型转换匹配catch,可以有基于继承的类型转换的匹配。
4 catch基于顺序匹配,虚函数重载基于参数最吻合进行匹配
5 throw;
catch (Widget& w)
{
...
throw; // 重新抛出这个exception
}
catch (Widget& w)
{
...
throw w; // 抛出被捕捉exception的一个副本
}
3.8 按引用传递异常和按值传递异常的异常对象切割问题
void someFunction()
{
...
if ( ... ) {
throw Valodation_error();
}
...
}
void doSomething()
{
try {
someFunction();
} catch (exception ex) {
cerr << ex.what(); // 调用的是exception.what();
}
}
void doSomething()
{
try {
someFunction();
} catch (exception& ex) {
cerr << ex.what(); // 调用的是Valodation_error.what();
}
}
3.9 exception specifications
它可以让代码更容易被理解,但是如果函数抛出了一个并未列于其exception specification的exception,这个错误会在运行期被检查出来,特殊函数unexpected会被自动调用,导致程序终止。
需要注意的场景:
1 声明了exception specification的函数调用未声明的函数
2 回调函数未声明exception specification时,无法确定异常类型
3 template和exception specification混合使用,被template重载的运算符或其他实现导致未察觉的异常类型
3.10 异常处理的成本
为了能够在运行时期处理异常,程序必须做大量标记工作。
在每一个执行点,必须能够确认,如果发生了异常,哪些对象需要析构,必须在每个try语句的进入点和离开点做记号,针对每个try语句块必须记录相应的catch子句以及能够处理的异常类型。
如果你决定不使用exceptions,并且让编译器知道,编译器可以适度完成某种优化。
据统计,如果使用try语句块,代码大约整体膨胀5%~10%,执行速度也大约下降这个数。
exception specification也会带来和try语句块差不多的成本。
抛出异常的成本:和正常的函数返回相比,抛出异常导致的函数返回,速度可能比正常情况下降三个数量级。
4 RTTI
RTTI:运行阶段类型识别(RunTime Type Identification)
目的是为程序在运行阶段确定对象的类型提供一种标准方式。(比如基类指针指向派生类对象,确定是哪一个派生类)
4.1 RTTI 工作原理
只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。
4.1.1 dynamic_cast运算符
dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0——空指针。
也可以将dynamic_cast用于引用,由于没有与空指针相对应的引用值,因此无法使用特殊的引用值来表示失败。所以当请求不正确时,dynamic_cast将引发类型为bad_cast的异常。
class Grand {
private:
int hold;
public:
Grand(int h = 0) : hold(h) {}
virtual void Speak() const { cout << "I am a grand class!\n";}
virtual int Value() const { return hold; }
};
class Superb : public Grand {
public:
Superb(int h = 0) : Grand(h) {}
void Speak() const { cout << "I am a superb class!\n";}
virtual void Say() const { cout << "I hold the superb value of " << Value() << "!\n";}
};
class Magnificent : public Superb {
private:
char ch;
public:
Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
void Speak() const { cout << "I am a magnificant class !!! \n";}
void Say() const { cout << "I hold the character " << ch << " and the integer " << Value() << "!\n";}
};
int main() {
std::srand(std::time(0));
Grand * pg;
Superb * ps;
for (int i = 0; i < 5; i++) {
pg = GetOne();
pg->Speak();
if ( ps = dynamic_cast<Superb *>(pg))
ps->Say();
}
return 0;
}
4.1.2 typeid运算符和type_info类
typeid运算符可以确定两个对象是否为同一类型,可以接受两种参数:
1 类名。
2 结果为对象的表达式。
typeid返回一个对type_info对象的引用
Grand * GetOne();
int main() {
srand(time(0));
Grand * pg;
Superb * ps;
for (int i = 0; i < 5 ; i++) {
pg = GetOne();
cout << "Now processing type " << typeid(*pg).name() << ".\n";
pg->Speak();
if (ps = dynamic_cast<Superb *>(pg)) {
ps->Say();
}
if (typeid(Magnificent) == typeid(*pg)) {
cout << "Yes, you're really magnificant.\n";
}
}
return 0;
}
Grand *GetOne() {
Grand * p;
switch (rand() % 3) {
case 0 : p = new Grand(rand() % 100);
break;
case 1 : p = new Superb(rand() % 100);
break;
case 2 : p = new Magnificent(rand() % 100, 'A' + rand() % 26);
break;
}
return p;
}
如果发现在扩展的if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast
5 类型转换运算符
dynamic_cast
const_cast:改变值为const,volatile。不能将类型改为基类或派生类。
int main() {
int pop1 = 38383;
const int pop2 = 2000;
cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
change(&pop1, -103);
change(&pop2, -103);
cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
return 0;
}
void change(const int *pt, int n) {
int * pc;
pc = const_cast<int *>(pt);
*pc += n;
}
// pop2 没有变化,因为const_cast只去掉了函数参数的const,没有去掉其值本身(const int pop2)的const,因此第二个change没变化
static_cast:只有当两个类型可以隐式转换时,才可以使用并进行类型转换。
reinterpret_cast:用于天生危险的类型转换,不允许删除const,没看懂。
5.1 const_cast
用来改变常量性。
5.2 dynamic_cast
用来实现基类(指向基类的指针或引用)转变为子类(的指针或引用),不能用于没有虚函数的类型上。
5.3 reinterpret_cast
最常用的用途是转换“函数指针”类型。