C++第十五章 友元、異常和其他

第十五章 友元、異常和其他

本章內容包括:

  • 友元類
  • 友元方法
  • 嵌套類
  • 引發異常、try塊和catch塊。
  • 異常類。
  • 運行階段類型識別(RTTI)。
  • dynamic_cast 和 typeid。
  • static_cast、const_cast 和 reiterpret_cast。

友元:

前面將友元函數用於類的擴展接口中,類並非只擁有友元函數,也可以將類作爲友元。這種情況下友元類的所有方法都可以訪問原始類的私有成員和保護成員。另外,也可以做更嚴格的限制,只將特定的成員函數指定爲另一個類的友元。哪些函數、成員函數或類爲友元是由類定義的,而不能從外部強加友情。因此,儘管友元被授予從外部訪問類的私有部分的權限,但它們並不與面向對象的編程思想相悖;相反,它們提高了公有接口的靈活性。

友元類:

電視機與遙控機的關係可以描述爲友元。

tv.h

#ifndef D1_TV_H
#define D1_TV_H

class Tv {
private:
    int state; // on or off
    int volume;
    int maxchannel;
    int channel;
    int mode;
    int input;
public:
    friend class Remote;
    enum {Off, On};
    enum {MinVal,MaxVal=20};
    enum {Antenna,Cable};
    enum {TV,DVD};
    Tv(int s = Off,int mc =125) : state(s),volume(5),maxchannel(mc),channel(2),mode(Cable),input(TV){};
    void onoff(){state = (state == On)?Off:On;}
    bool ison() const { return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {mode = (mode==Cable)?Antenna:Cable;}
    void set_input() {input = (input == TV)?DVD:TV;}
    void settings() const ; // display all settings

};

class Remote
{
private:
    int mode;
public:
    Remote(int m = Tv::TV) : mode(m){}
    bool volup(Tv &t){ return t.volup();}
    bool voldown(Tv &t){ return t.voldown();}
    void onoff(Tv &t){t.onoff();}
    void chanup(Tv &t){t.chanup();}
    void chandown(Tv &t){t.chandown();}
    void set_chan(Tv &t, int c) {t.channel = c;}
    void set_mode(Tv & t){t.set_mode();}
    void set_input(Tv & t){t.set_input();}
};
#endif //D1_TV_H

tv.cpp

#include "Tv.h"
#include <iostream>

bool Tv::volup() {
    if (volume < MaxVal)
    {
        volume++;
        return true;
    }
    return false;
}

bool Tv::voldown() {
    if (volume > MinVal)
    {
        volume--;
        return true;
    }
    return false;
}

void Tv::chanup() {
    if (channel < maxchannel)
        channel++;
    else
        channel = 1;
}

void Tv::chandown() {
    if (channel > 1)
        channel--;
    else
        channel = maxchannel;
}

void Tv::settings() const {
    using std::cout;
    using std::endl;
    cout << "TV is " << (state == On?"On":"Off") << endl;
    if (state == On)
    {
        cout << "Volume setting =" << volume << endl;
        cout << "Channel setting =" << channel << endl;
        cout << "Mode =" << (mode == Antenna?"antenna":"cable") << endl;
        cout << "input =" << (input == TV?"TV":"DVD") << endl;

    }
}

use_tv.cpp

#include <iostream>
#include "Tv.h"


int main(){
    using std::cout;
    Tv s42;
    cout << "Initial settings for 42\"TV:\n";
    s42.settings();
    s42.onoff();
    s42.chanup();
    cout << "\nAdjustedd settings for 42\"TV:\n";
    s42.settings();
    Remote grey;
    grey.set_chan(s42,10);
    grey.volup(s42);
    grey.volup(s42);
    cout << "\n42\" settings after using Remote:\n";
    s42.settings();

    Tv s58(Tv::On);
    s58.set_mode();
    grey.set_chan(s58,28);
    cout << "\n58\" settings :\n";
    s58.settings();
    return 0;
}
結果
Initial settings for 42"TV:
TV is Off

Adjustedd settings for 42"TV:
TV is On
Volume setting =5
Channel setting =3
Mode =cable
input =TV

42" settings after using Remote:
TV is On
Volume setting =7
Channel setting =10
Mode =cable
input =TV

58" settings :
TV is On
Volume setting =5
Channel setting =28
Mode =antenna
input =TV

Process finished with exit code 0

例子表明,類友元是一種自然用語,用於表達一些關係。如果不使用某些形式的友元關係,則必須將Tv類的私有部分設置爲公有的,或建立一個大型類來包含電視機和遙控器。這種解決方法無法反應這樣的事實,一個遙控器可控制多臺電視機。

友元成員函數:

上一個例子中,大多數Remote方法都是用Tv類的公有接口來實現的。這意味着這些方法不是真正需要作爲友元。事實上唯一直接訪問Tv成員的Remote方法是Remote::set_chan(),因此它是唯一需要作爲友元的方法。

讓Remote::set_chan()成爲Tv類的友元方法是,在Tv類中將其聲明爲友元

tvfm.h

#ifndef D1_TV_H
#define D1_TV_H

// 讓Remote 類知道 Tv類
class Tv;

class Remote
{
private:
    int mode;
public:
    enum {Off, On};
    enum {MinVal,MaxVal=20};
    enum {Antenna,Cable};
    enum {TV,DVD};
    Remote(int m = TV) : mode(m){}
    bool volup(Tv &t);
    bool voldown(Tv &t);
    void onoff(Tv &t);
    void chanup(Tv &t);
    void chandown(Tv &t);
    void set_chan(Tv &t, int c);
    void set_mode(Tv & t);
    void set_input(Tv & t);
};

class Tv {
private:
    int state; // on or off
    int volume;
    int maxchannel;
    int channel;
    int mode;
    int input;
public:
    // 要讓編譯器處理這條語句,要先讓編譯器看到Remote的定義
    friend void Remote::set_chan(Tv &t, int c);
    enum {Off, On};
    enum {MinVal,MaxVal=20};
    enum {Antenna,Cable};
    enum {TV,DVD};
    Tv(int s = Off,int mc =125) : state(s),volume(5),maxchannel(mc),channel(2),mode(Cable),input(TV){};
    void onoff(){state = (state == On)?Off:On;}
    bool ison() const { return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {mode = (mode==Cable)?Antenna:Cable;}
    void set_input() {input = (input == TV)?DVD:TV;}
    void settings() const ; // display all settings

};
// 由於調用tv方法前,必須知道Tv方法有哪些函數,因此放到Tv下面
inline bool Remote::volup(Tv &t){ return t.volup();}
inline bool Remote::voldown(Tv &t){ return t.voldown();}
inline  void Remote::onoff(Tv &t){t.onoff();}
inline void Remote::chanup(Tv &t){t.chanup();}
inline void Remote::chandown(Tv &t){t.chandown();}
inline void Remote::set_chan(Tv &t, int c) {t.channel = c;}
inline void Remote::set_mode(Tv & t){t.set_mode();}
inline void Remote::set_input(Tv & t){t.set_input();}

#endif //D1_TV_H
其他友元關係:

假如電視機也能讓遙控器發出聲音,也就是互爲友元類:

class Tv{
public:
	friend class Remote;
	void buzz(Remote & r);
	......
}
class Remote{
private:
	friend class Tv;
	void buzz();
	......
}
inline void Tv::buzz(Remote & r){
	r.buzz();
}
共同的友元:

需要使用友元的另一種情況,函數需要訪問兩個類的私有數據。這時可以將函數作爲兩個類的友元。

class Analyzer;
class Probe
{
	friend void sync(Analyzer & a, const Probe & p); // sync a to p
	friend void sync(Probe & p, const Analyzer & a); // sync p to a
	......
}
class Analyzer
{
	friend void sync(Analyzer & a, const Probe & p); // sync a to p
	friend void sync(Probe & p, const Analyzer & a); // sync p to a
	......
}
inline void sync(Analyzer & a, const Probe & p){......}
inline void sync(Probe & p, const Analyzer & a){......}

嵌套類:

在另一個類中聲明的類被稱爲嵌套類,它通過提供新的類型類作用域來避免名稱混亂。包含類的成員函數可以創建和使用被嵌套類的對象;而僅當聲明位於公有部分,才能在類的外面使用嵌套類

Queue.h

template <class Item>
class Queue {
    enum {Q_SIZE = 10};
    class Node{
    public:
        Item item;
        Node * next;
        Node(const Item & it):item(it),next(nullptr){};
    };
    Node *front;
    Node *end;
    int items;
    const int qsize;
    Queue(const Queue &queue){};
    Queue &operator=(const Queue &queue);
public:

    Queue(int qs= Q_SIZE);
    ~Queue();
    bool isempty() const;
    bool isfull() const;
    int queuecount() const;
    bool add(const Item &item);
    bool get(Item &item);
};

異常:

調用abort()

#include <iostream>
#include <cstdlib>
using std::cout;
using std::cin;
using std::endl;
double hmean(double a, double b);


int main(int argnum, char *args[]) {
    cout << hmean(12,3) << endl;
    hmean(3,-3);
    cout << "Done.\n";
    return 0;
}

double hmean(double a, double b){
    if (a == -b){
        std::cout << "a,b val error. abort()" << endl;
        std::abort();
    }
    return  2.0 * a * b / ( a + b );
}
結果
4.8
a,b val error. abort()

在hmean中調用abort()將直接終止程序。

異常機制:

對異常處理有三個組成部分:

  • 引發異常 throw
  • 使用處理程序捕獲異常
  • 使用 try -catch

error3.cpp

#include <iostream>
#include <cstdlib>
using std::cout;
using std::cin;
using std::endl;
double hmean(double a, double b);


int main(int argnum, char *args[]) {
    cout << hmean(12,3) << endl;
    try {
        hmean(3,-3);
    }catch (const char *s){
        cout << s << endl;
    }catch (const int s){
        cout << s << endl;
    }catch (...){
        cout << "catch all error" << endl;
    }
    cout << "Done.\n";
    return 0;
}

double hmean(double a, double b){
    if (a == -b){
        throw "a,b val error. abort()";
    }
    return  2.0 * a * b / ( a + b );
}
結果
4.8
a,b val error. abort()
Done.

throw語句類似返回語句,因爲它也將終止函數的執行。但throw不是將控制權返回給調用程序,而是導致程序沿函數調用序列後退,直到找到包含try塊的函數。
catch塊表明這是一個處理函數,char *s則表明處理該程序與字符串異常匹配。
執行完try模板時,如果沒有引發任何異常,則程序調到catch塊後繼續執行。
catch (…) 可以捕獲全部異常

exception類:

exception 頭文件定義了exception類,C++可以把它作爲其他異常的基類。代碼可以引發exception異常,也可以將exception作爲基類,有一個名爲what()的虛函數,它返回一個字符串。由於是虛方法,可以在派生中重新定義它:

#include <exception>
class bad_hmean:public std::exception{
public:
    const char * what(){ return "bad argument to heman"}
};

c++ 庫定義了很多基於exception的異常類型。

stdexcept異常類:

頭文件stdexcept定義了其他幾個異常類。首先,該文件定義了logic_error 和 runtime_error類,它們都是以公有方式從exception派生而來的:

class logic_error: public exception{
public:
	explicit logic_error(const string & what_arg);
    ......
};
class runtime_error: public exception{
public:
	explict runtime_error(const string & what_arg);
    ......
};

這些類的構造函數接受一個string對象作爲參數,該參數提供了方法what()以C-風格字符串方式返回的字符數據。

這兩個新類被用作兩個派生類系列的基類。異常類系列logic_error描述了典型的邏輯錯誤。總體而言,通過合理編程可以避免這種錯誤,但實際上還是有可能發生:

  • domain_error:

    數學函數有定義域(domain)。如果編寫一個函數,將參數傳遞給std::sin(),則可以讓函數在參數不在定義域-1到1之間時引發異常

  • invalid_argument:

    指出給函數傳遞了一個意料之外的值。,例如,函數希望接受一個這樣的字符串:其中每個字符要麼是’0’要麼是‘1’,則當傳遞的字符串中包含其他字符時,該函數可以引發異常。

  • length_error:

    用於指出沒有足夠的空間來執行所需要的操作。如string類中的append()方法在合併得到的字符串長度超過最大允許長度時,將引發異常

  • out_of_bounds:

    指名索引錯誤,如定義一個數組,其operator(int n)[]在使用索引無效時引發異常

runtime_error 異常系列描述了可能在運行期間發生,但難以預計和防範的錯誤:

  • range_error
  • overflow_error
  • underflow_error

下溢(underflow)錯誤在浮點數計算中。一般而言,存在浮點類型可以表示的最小非零值,計算結果比這個值還小將導致下溢錯誤。整型和浮點型都可能發生上溢錯誤。計算結果不再函數允許範圍內,但沒有發生上溢或下溢,這時,可以使用range_error

bad_alloc 和 new:

對於使用new導致的內存分配問題,C++最新處理方式是讓new引發bad_alloc異常。頭文件new包含bad_alloc類的聲明,它是從exception類公有派生而來的:

newexcp.cpp

#include <iostream>
#include <cmath>
const int length = 120000;
using std::cout;
using std::endl;

struct Big{
    double stuff[20000];
};
int main(){
    Big * pb;
    try{
        pb = new Big[length];
        cout << "past new,size: ";
        cout << sizeof(*pb)* length /1024/1024/1024<< endl;
    }catch (std::bad_alloc &al){
        cout << al.what() << endl;
    }
    return  0;
}
異常何時會迷失方向:

未捕獲異常不會導致程序立即停止。相反,程序首先調用terminate()函數。默認情況下terminate()函數將調用abort()函數。可以指定terminate()函數應調用的函數來修改terminate()的這種行爲。爲此,可調用set_terminate()函數。兩個函數都是在頭文件exception中聲明的

typedef void (*teminate_handler)();
teminate_handler set_terminate(teminate_handler f) noexcept;
void terminate() noexcept;

其中typedef使得teminate_handler成爲這樣一種類型的名稱:指向沒有參數和返回值的函數的指針。set_terminate()函數將不帶任何參數且返回值爲void的函數的名稱(地址)作爲參數,並返回該函數的地址。如果多次調用set_terminate(),將使用最後一次設置的函數

#include <iostream>
#include <cmath>
#include <exception>

const int length = 1200000;
using std::cout;
using std::endl;

struct Big{
    double stuff[20000];
};
void myQuit();

int main(){
    std::set_terminate(myQuit);
    Big * pb;
    try{
        pb = new Big[length];
        cout << "past new,size: ";
        cout << sizeof(*pb)* length /1024/1024/1024<< endl;
    }catch (const char *s){
        cout << s << endl;
    }
    return  0;
}
void myQuit(){
    cout << "Terminating due to uncacught exception.\n";
    exit(5);
}

RTTI運行階段標識:

RTTI的用途:

假設有一個類層次結構其中的類都是從同一個基類派生而來的,則可以讓基類的指針指向其中任何一個類的對象。這樣便可以調用這樣的函數:在處理一些信息後,選擇一個類,並創建這種類型的對象,然後返回它的地址,而該地址可以被賦給基類指針。如何知道指針指向的是哪種對象?

RTTI的工作原理:

C++有3個支持RTTI的元素:

  • 如果可能的話dynamic_cast運算符將使用一個指向基類的指針來生成一個指向派生類的指針;否則,該運算符返回0-空指針
  • typeid運算符返回一個指出對象的類型的值
  • type_info 結構存儲了有關可定類型的信息

只能將RTTI用於包含虛函數的類層次結構,原因在於只有對於這種類層次結構,才能將派生對象的地址賦給基類指針

dynamic_cast運算符:

dynamic_cast運算符是最常用的RTTI組件,它不能回答“指針指向的是哪類對象”的問題,但能夠回答“是否可以安全地將對象的地址賦值給特類型的指針”這樣的問題

Superb *pm = dynammic_const<Superb *>(pg);

如果pg的類型可以安全的轉換成Superb就返回對象地址,否則返回空指針。

rtti1.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>

using std::cout;

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 magnificent class!\n";}
    void Say() const { cout << "I hold the character " << ch <<
                        " and the integer " << Value() << "!\n";}
};

Grand * GetOne();

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();
        }
        delete pg;
    }
    return  0;
}
Grand * GetOne(){
    Grand * p;
    switch (std::rand() % 3){
        case 0: p = new Grand(std::rand() % 100);
                break;
        case 1: p = new Superb(std::rand() % 100);
            break;
        case 2: p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);
            break;
    }
    return p;
}
結果
I am a superb class!
I hold the superb value of 40!
I am a superb class!
I hold the superb value of 43!
I am a magnificent class!
I hold the character I and the integer 53!
I am a magnificent class!
I hold the character H and the integer 46!
I am a grand class!

程序說明,只爲superb和magnificent類調用了Say()方法。

也可將dynamic_cast用於引用,其用法稍有不同;沒有與空指針對應的引用值,因此無法使用特殊的引用值來表示失敗。當請求不正確時,dynamic_cast將引發bad_cast異常,這種異常是從exception派生出來的,它是在typeinfo中定義的。因此,可以像下面這樣使用該運算符,其中rg是對Grand對象的引用。

#include<typeinfo>
...
try{
	Superb & rs = dynamic_cast<Superb &>rb;
	....
}catch(bad_cast &){
	...
}
typeid運算符和type_info類:

typeid運算符使得能夠確定兩個對象是否爲同種類型。它與sizeof有些相似,可以接受兩種參數:

  • 類名
  • 結果爲對象的表達式

typeid運算符返回一個type_info對象的引用,其中,type_info是在頭文件typeinfo中定義的一個類。type_info類重載了==和!=運算符,以便可以使用這些運算符來對類型進行比較。例如,如果pg指向的是一個Magnificent對象,則下述表達式的結果bool值爲true否則爲false:

typeid(Magnificent) == typeid(*pg)

如果pg是一個空指針,程序將引發bad_typeid異常。該異常是從exception類派生而來的,是在頭文件typeinfo中聲明的。

type_info類的實現隨廠商而異,但包含一個name()成員,該函數返回一個隨實現而異的字符串:通常(但並非一定)是類的名稱。例如,下面的語句顯式指針pg指向對象所定義的類定義的字符串。

cout << "Now processing type " << typeid(*pg).name() << ".\n";

rtti2.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <typeinfo>
using std::cout;

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 magnificent class!\n";}
    void Say() const { cout << "I hold the character " << ch <<
                        " and the integer " << Value() << "!\n";}


};

Grand * GetOne();

int main(){
    std::srand(std::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, your're really magnificent.\n";
        }
    }
    return  0;
}
Grand * GetOne(){
    Grand * p;
    switch (std::rand() % 3){
        case 0: p = new Grand(std::rand() % 100);
                break;
        case 1: p = new Superb(std::rand() % 100);
            break;
        case 2: p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);
            break;
    }
    return p;
}
結果
Now processing type 5Grand.
I am a grand class!
Now processing type 5Grand.
I am a grand class!
Now processing type 11Magnificent.
I am a magnificent class!
I hold the character Q and the integer 60!
Yes, your're really magnificent.
Now processing type 5Grand.
I am a grand class!
Now processing type 6Superb.
I am a superb class!
I hold the superb value of 38!
類型轉換運算符:

由於C中類型轉換限制鬆散,更加嚴格地限制允許轉換的類型:

  • dynamic_cast (dynamic 動態的) 用於將類向上轉換

  • const_cast

    const_cast 運算符用於執行只有一種用途的類型轉換,即改變值爲const 或 volatile,如果類型的其他方面也被修改,這轉換將出錯。

    High bar;
    const High * pbar = &bar;
    High * pb = const_cast<High *>(pbar) // valid
    const Low * pl = const_cast<Low *>(pbar) // invalid
    
  • static_cast

    static_cast<type_name>(expression)
    

    僅當type_name可被隱式轉換爲expression所屬的類型或expression可被隱式轉換爲type_name所屬的類型時,上述轉換纔是合法的,否則將出錯。假設High是Low的基類,而Prod是無關類,則從High到Low、從Low到High的轉換都是合法的,而從Low到Prod的轉換是不允許的

  • reinterpret__cast (reinterpret 重新解讀)

    reinterpret__cast用於天生危險的類型轉換。

    struct dat {short a; short b;};
    long value = 0xA224B118;
    dat * pd = reinterpret__cast<dat *>(&value);
    cout << hex << pd->a;
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章