【C++ Primer】用于大型程序的工具

1. 异常处理

异常以类似于将实参传递给函数的方式抛出和捕获。异常可以是可传给非引用实参的任意实参的类型,这意味着必须能够复制该类型的对象。

当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。

抛出指针通常是个坏主意。


栈展开:沿着嵌套函数调用链继续向上,直到为异常找到一个catch子句。

栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数


一个块可以通过new动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内存将不会释放。


析构函数应该从不抛出异常,如果真的抛出自己的未经处理的另一个异常,将会导致调用terminate函数,然后terminate函数将调用abort函数,使得程序非正常退出。


构造函数发生异常,要保证会适当地撤销已构造的成员。


对于未被匹配的异常,程序会调用库函数terminate。


catch子句中的异常说明符(exception specifier)看起来像只包含一个形参的形参表,异常说明符是在其后跟一个(可选)形参名的类型名:catch(const runtime_error &e)

catch子句中的异常说明符必须是完全类型,不可以为前置声明,因为你的异常处理中常常要访问异常类的成员。例外:只有你的catch子句使用指针或者引用接收参数,并且在catch子句内你不访问异常类的成员,那么你的catch子句的异常说明符才可以是前置声明的类型。

catch的匹配过程是找最先匹配的,不是最佳匹配

catch的匹配过程中,对类型的要求比较严格允许标准算术转换类类型的转换。(类类型的转化包括种:通过构造函数的隐式类型转化和通过转化操作符的类型转化)



重新抛出(rethrow):将异常传递给函数调用链中更上层的函数(而不是同一个函数的下一个catch)。  只用个空throw语句: throw;

只能出现在catch或者从catch调用的函数中。如果在处理代码不活动时碰到空throw,就调用terminate函数。

被抛出的异常是原来的异常对象,而不是catch形参

catch可以改变它的形参,在改变形参之后,如果catch重新抛出异常,只有当异常说明符是引用的时候,才会传播那些改变。



可以使用catch(...){}来捕获所有异常,如果与其他catch子句结合使用,它必须是最后一个,否则,任何它后面的catch子句都不能被匹配。


要想处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块:

A : : A(int a ) 

try : mem(a), use(new size_t(1))

{

    // 函数体

}catch(const std::bad_alloc &e)

{handle_out_of_memory(e);}


exception类型定义的唯一操作是一个名为what的虚成员,该函数返回const char* 对象,一般返回用来在抛出位置构造异常对象的信息。

【C++ Primer】用于大型程序的工具【C++ Primer】用于大型程序的工具



1、auto_ptr为标准库提供的“资源分配即初始化”类,是接受一个类型形参的模板,它为动态分配的对象提供异常安全特性。在memory头文件中定义。

2、auto_ptr操作

auto_ptr<T> ap;

创建名为 ap 的未绑定的 auto_ptr 对象

auto_ptr<T>

ap(p);

创建名为 ap 的 auto_ptr 对象,ap 拥有指针 指向的对象。该构造函数为explicit

auto_ptr<T> ap1(ap2);

创建名为 ap1 的 auto_ptr 对象,ap1 保存原来存储在ap2 中的指针。将所有权转给 ap1ap2 成为未绑定的auto_ptr 对象

ap1 = ap2

将所有权 ap2 转给 ap1。删除 ap1 指向的对象并且使 ap1指向 ap2 指向的对象,使 ap2 成为未绑定的

~ap

析构函数。删除 ap 指向的对象

*ap

返回对 ap 所绑定的对象的引用

ap->

返回 ap 保存的指针

ap.reset(p)

如果 与 ap 的值不同,则删除 ap 指向的对象并且将 ap绑定到 p

ap.release()

返回 ap 所保存的指针并且使 ap 成为未绑定的

ap.get()

返回 ap 保存的指针

    auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组。当auto_ptr被复制或赋值的时候,有不寻找的行为,因此不能将auto_ptr存储在标准库容器类中。

3、每个auto_ptr对象绑定到一个对象或者指向一个对象。当auto_ptr对象指向一个对象的时候,可以说它“拥有”该对象。当auto_ptr对象超出作用域或者另外撤销的时候,就自动回收auto_ptr所指向的动态分配对象。

    auto_ptr是可以保存任何类型指针的模板。

示例

void f()
{
    auto_ptr<int> ap(new int(42)); // allocate a new object
    // code that throws an exception that is not caught inside f
}   // auto_ptr freed automatically when function ends

4、注意到,接受指针的构造函数为explicit构造函数,所以必须用初始化的直接形式来创建auto_ptr对象。

示例

// error: constructor that takes a pointer is explicit and can‘t be used implicitly
auto_ptr<int> pi = new int(1024);
auto_ptr<int> pi(new int(1024)); // ok: uses direct initialization

pi所指的由new表达式创建的对象在超出作用域时自动删除。

5、auto_ptr的主要目的是在保证自动删除auto_ptr对象引用的对象的同时,支持普通指针式行为。

示例

auto_ptr<string> ap1(new string("Hellobaby!"));
*ap1 = "TRex"// assigns a new value to the object to which ap1 points
string s = *ap1; // initializes s as a copy of the object to which ap1 points
if (ap1->empty()) // runs empty on the string to which ap1 points

6、auto_ptr对象的赋值和复制是破坏性操作行为,与普通指针的复制和赋值有区别。普通指针赋值或复制后两个指针指向同一对象,而auto_ptr对象复制或赋值后,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置成为未绑定状态。

    因此,auto_ptr赋值的左右操作数必须是可修改的左值。因为标准库容器要求在复制或赋值后两对象相等,所以auto_ptr不能存储在标准容器中。

7、默认情况下,auto_ptr的内部指针值置为0

8、测试auto_ptr对象

    auto_ptr 类型没有定义到可用作条件的类型的转换,相反,要测试auto_ptr 对象,必须使用它的 get 成员,该成员返回包含在 auto_ptr 对象中的基础指针。

示例

// error: cannot use an auto_ptr as a condition
if (p_auto)
   *p_auto = 1024;
// revised test to guarantee p_auto refers to an object
if (p_auto.get())
   *p_auto = 1024;

    应该只用 get 询问 auto_ptr 对象或者使用返回的指针值,不能用 get 作为创建其他 auto_ptr 对象的实参。

9、reset操作

不能直接将一个地址(或其它指针)赋给auto_ptr对象。

示例

#include <iostream>
#include "memory"
using namespace std;
 
int main()
{
    auto_ptr<int> p_auto(new int(123));
    //p_auto = new int(1024); // error: cannot assign a pointer to an auto_ptr
    if (p_auto.get())
        *p_auto = 1024;
    else
        p_auto.reset(new int(1042));
    return 1;
}

    正如自身赋值是没有效果的一样,如果调用该 auto_ptr 对象已经保存的同一指针的 reset 函数,也没有效果,不会删除对象。

10、正确使用auto_ptr类,有如下限制:

1)不要使用auto_ptr对象保存指向静态分配对象的指针,否则,当auto_ptr对象本身被撤销的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为。

2)永远不要使用两个 auto_ptrs 对象指向同一对象,导致这个错误的一种明显方式是,使用同一指针来初始化或者 reset 两个不同的 auto_ptr 对象。另一种导致这个错误的微妙方式可能是,使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset另一个 auto_ptr 对象。

3)不要使用 auto_ptr 对象保存指向动态分配数组的指针。当auto_ptr 对象被删除的时候,它只释放一个对象—它使用普通delete 操作符,而不用数组的delete [] 操作符。

4)不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,使它们表现得类似于内置类型的操作符:在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr 类不满足这个要求。



异常说明

1)定义

示例

void recoup(intthrow(runtime_error);
void no_problem() throw(); //空说明列表指出函数不抛出任何异常

异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。如果函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。

编译器在编译时不能也不会试图验证异常说明。 如果函数抛出了没有在其异常说明中列出的异常,就直接调用unexpected函数终止程序。

 当某个类的析构函数声明了不抛出异常时,另外一个类继承该类时,并且有成员时,也必须将析构函数声明为不抛出异常。

基类的虚函数的异常说明可以与派生类中对应虚函数的异常说明不同,但派生类虚函数的异常说明应当更严格。基类中虚函数异常列表是派生类异常列表的超集。

可以在函数指针的定义中提供异常说明。在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格


2. 命名空间

给定作用域中出现的名字冲突问题叫做命名空间污染问题(namespace pollution)。

命名空间能够划分全局命名空间。

形如:

namespace cplusplus_primer{

...

}

命名空间可以在全局作用域或者其他作用域内部定义,但不能在函数或类内部定义。命名空间不能以分号结束。

命名空间外部的代码必须指出名字定义在哪个命名空间:   xxspace::Query q; 也可以使用using namespace **直接说明。


命名空间可以是不连续的,多个命名空间可以累积定义。但是,如果命名空间的一个部分需要定义在另一个文件的名字,仍然必须声明该名字。利用这个性质可以实现接口和实现的分离。


可以在命名空间外部定义命名空间成员,返回类型以及函数名必须指出名字限定,类似于在类外部定义类成员函数:

cplus:: Sales_item cpuls:: operate + ( const  Sales_item  & l, const  Sales_item  & r),但是不能定义在不相关的命名空间中。


全局命名空间   ::member_name


未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。其中定义的名字可以直接使用。

如果在文件最外层定义域中定义未命名的命名空间,则其中名字必须与全局作用域中定义的名字不同

int i;

namespace {

    int i;  //  error, 重定义

}

在引入命名空间之前,程序必须将名字声明为static,使得他们局部于一个文件,但是c++推荐使用未命名的命名空间来代替


命名空间别名:  namespace a = cplus;   namespace b = cplus::m;

using声明一次只引入一个命名空间成员: using std::vector;

using指示使得特定命名空间的所有名字可见: using namespace std;

相对于依赖于using指示,对程序使用using声明更好,可以避免名字冲突,除了在函数或其他作用域内部,头文件不应该包含using指示或using声明



实参相关的查找与类类型形参:

考虑下面的简单程序:

std::string s;

getlint(std::cin, s);

为什么可以不加std::限定符或者using声明就使用getline呢?

编译器会在当前作用域、包含调用的作用域以及定义cin类型和string类型的命名空间查找匹配的函数。

接受类类型形参(或类类型指针及引用形参)的且与类本身定义在同一个命名空间中的函数(包括重载操作符),在用类类型对象(或类类型指针及引用)作为实参的时候是可见的

所以std::string s; std::cin >> s; 中的操作符不用再加作用域限定符了。



隐式友元声明与命名空间:

当一个类声明友元函数(非成员函数)的时候,函数的声明不必是可见的。如果不存在可见的声明,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一个命名空间中声明。

namespace A

{

    class C{

        friend void f(const C&); // 相当于在A中进行了声明,

    }

}

void f2()

{

    A::C cobj;

    f(cobj);  // 这里可以不加命名空间限定符就是因为实参相关查找与类类型形参的原则;

}



可能存在异常的程序以及分配资源的程序应该使用类来管理这些资源。使用类管理分配和回收可以保证如果发生异常就释放资源。这一技术常称为“资源分配即初始化”RAII




3. 多重继承(multiple inheritance)与虚继承


class Bear: public ZooAnimal{};

class Panda: public Bear, pulic Endangered{};


多重继承构造的次序:

基类构造函数按照基类构造函数在类派生列表中的出现次序调用:在上例中,即是ZooAnimal, Bear, Endangered


多重继承析构的次序:与构造的次序相反


多个基类之间的转换:

对于派生类来说,基类之间都是等价的,如果存在两个函数

void print(const Bear&);

void print(const Endangered&);

调用print(Pandar()) 是会出现二义性的,会产生编译错误


假定所有基类都将他们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的:

delete pbear;

delete pzooAnimal;


赋值操作符与复制构造函数的行为与构造函数类似,构造的顺序也一样



多个基类容易导致二义性:

假定Bear与Endangered中都定义了print成员,但是Panda中没有定义,则panda.print()在编译时会出错,因为不知道调用哪个版本

如果两个继承的print函数有不同的形参表,同样会发生错误。

如果再ZooAnimal中定义了print而Bear类中没有定义,也会出错。


避免二义性的方法有:自己明确指定使用哪个,如 panda.Bear::print();或者直接在Panda中定义一个print版本。

虚继承:
在虚继承中,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类(virtual base class):
class B: class virtual A{};
class C: virtual class A{};
class D: public B, public C{};

使用虚函数的多重继承层次比没有虚继承的引起更少的二义性。
 a:如果在每个路径中x表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例;
b:如果在某个路径中x是虚基类的成员,而在另一路径中x是后代派生类的成员,也没有二义性,因为特定派生类实例的优先级高于共享虚基类的实例
 c:如果沿每个继承路径,x表示后代派生类的不同成员,则该成员的直接访问是二义性的,就像非虚多重继承层次一样,这时可以在派生类中定义一个同名成员函数,在该函数中指定限定名访问成员x;


初始化:

为了解决重复初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,有最底层派生类的构造函数初始化虚基类。也就是由D来初始化A。如果D没有显示初始化A基类,就使用A的默认构造函数,如果A没有默认构造函数,则代码出错。


构造与析构:

无论虚基类出现在继承层次的任何地方,总是在构造非虚基类之前构造虚基类。


【C++ Primer】用于大型程序的工具,布布扣,bubuko.com

【C++ Primer】用于大型程序的工具

上一篇:Cross Entropy Loss 交叉熵损失函数公式推导


下一篇:Gradient Descend 梯度下降法公式推导