C++基础:友元,嵌套类,异常,类型转换

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

最常用的用途是转换“函数指针”类型。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值