C++ 类和对象记录(下)

组合与继承

组合-复用类的实现

//patrs.h
class Eye
{
    public:
        void see(void){...}
};

class Nose
{
    public:
        void smell(void){...}
};

class Mouth
{
    public:
        void speak(void){...}
};

class Ear
{
    public:
        void listen(void){...}
};

//head.h
//组合,复用已有类实现的代码
#include"parts.h"
class Head
{
    public:
        Eye leftEye,rightEye;
        Nose nose;
        Mouth mouth;
        Ear leftEar,rightEar;
        void turn(){...}
};

//test.cpp
#include"head.h"
int main()
{
    Head h;
    h.turn();
    h.nose.smell();
}

常用的方式是将嵌入对象作为新类的私有成员, 这时它是新类内部实现的一部分。新类中的方法可以使用成员对象提供的功能, 但新类只向外部展现自己的接口, 隐藏了包含的成员对象。

类中如果包含多个对象成员, 在初始化列表中将它们用逗号隔开。成员初始化的次序和成员声明的次序相同, 并不考虑它们在初始化列表中的排列顺序。建议初始化成员的顺序和声明顺序一致。

当组合对象被撤销时, 会执行其析构函数, 成员对象的析构函数也会被执行。执行次序和构造函数相反, 即先执行组合对象的析构函数, 再执行成员对象的析构函数。

指针成员与聚合关系

聚合关系的特点是成员对象可以独立于聚合对象而存在。当聚合对象被创建或撤销时, 其成员对象可以不受影响, 而只是它们之间的关系受到影响。
聚合关系在C++中使用按指针组合的语法实现: 聚合对象中包含成员类对象的指针。

class MailBody{...};
class Attachment
{
    string filename;
    //其他成员...
};
class Email
{
    string title;
    MailBodey body;
    vector<Attachment*> attch //聚合,指向多个附件对象的指针数组
    public:
        void edit(){...};
        void save(){...};
        void send(string recieverAddr){...};
        void addAttch(Attachment* a){ attch.push_back(a); ... };
};

聚合关系的聚集对象中如果包含多个某类的成员,实现时一般借助数组或vector之类的标准容器。

class Coach {...};
class Player {...};
class Team
{
    private:
        Coach* chiefCoach;
        vector<Player*>players;
    public:
        Team(Coach* pc)
        {
            chiefCoach=pc;
        }
        void changeCoach(Coach* pnc)
        {
            chiefCoach=pnc;
        }
        void employPlayer(Player* player)
        {
            players.push_back(player);
        }
        void firePlayer(Player* player)
        {
            //查找指定球员,返回指定球员的迭代器it
            if(it!=players.end())
                players.erase(it);
        }
};

指针成员与关联关系

组合是一种更强的聚合,聚合是一种特殊的关联。
关联关系再C++中也用按指针组合的语法实现。
组合关系的语义可以用按值组合的对象成员语法实现,聚合或关联都使用按指针组合的语法实现。

class BankAccount
{
    long accountNo;
    double balance;
    string clientName;
    public:
        BankAccount(long aNo,string name,double bal) {...}
};
class Client
{
    string name;
    string address;
    BankAccount* acc;//也可以如下
    //BankAccount* accounts[5]; //一个客户最多5个账户
    //vector<BankAccount*>accounts; //一个客户可以有多个账户
    public:
        Client(string nm,BankAccount* a):name(nm)
        {
            acc=a;
        }
};
BankAccount ba(18024,"Hive",5000); //建立新帐户
Client Hive("Hive",&ba);

继承-复用类的接口

被继承的类称为基类,继承得到的新类称为派生类

class student
{
    string name;
    int stu_id;
    string department;
    public:
        student(string nm,int id,string dp);
        void print()const;
};

class grad_student:public student
{
    string thesis;
    //其他成员继承得到,不用再重复声明
    public:
        grad_student(string nm,int id,string dp,string th);
        void print()const; //重复声明表示要重新实现这个操作
}

派生类成员的访问控制

派生类继承了基类的成员, 但是基类成员在派生类中的可见性由两个因素决定: 成员在基类中的访问限定和继承时使用的访问限定符

一个类的public成员在任何类和函数中都是可以访问的
private成员只有本类或本类的友元可以访问, 在其他类和函数中不可访问, 在自己的派生类中也同样不可访问。
一个类的protected成员的访问权限介于publicprivate之间: 对它的派生类来说,protected 成员和public 成员一样是可访问的, 但对其他类和函数而言, protected 成员就如同private 成员一样不可访问。

在公有派生类中, 基类的public成员和protected成员被继承,分别作为派生类的public成员和protected成员。基类的private成员虽然也被继承了, 但在派生类中是不可见的。

在私有派生类中, 基类的public成员和protected成员被派生类作为自己的private成员继承下来。基类的private成员虽然也被继承了, 但在派生类中是不可见的。

继承时也可以使用protected限定符, 这时基类的public成员和protected成员都被派生类作为自己的protected成员继承下来。基类的private成员在protected派生类中仍是不可见的。

如果担心基类的封装性会因此被破坏, 可以将基类中的所有数据成员都声明为private,而不是protected。如果派生类真的需要访问基类的属性, 就在基类中为其提供相应的protected访问器函数。

//基类
class Base
{
    int attr; //私有
    protected: //设置protected访问器供派生类使用
        int getAttr();
        void setAttr(int);
};

如果要在派生类中对继承的基类成员的可见性进行调整,可以使用using声明,语法形式为:

class Derived:(public|private|protected)Base
{
    public:(|private: |protected:)
        using Base::成员名; //将继承的基类成员声明为public(private或protected)
};

using声明的作用是在派生类中调整个别基类成员的访问限制。派生类只能为它可以访问的名字提供using 声明不能改变基类private 成员的访问限制。

#include<iostream>
using namespace std;
class Base
{
    public:
        void f() {cout<<"public: Base::f()"<<endl;}
        void f(int) {cout<<"public: Base::f(int)"<<endl;}
        void g() {cout<<"public: Base::g()"<<endl;}
    protected:
        void h() {cout<<"protected: Base::h()"<<endl;}
    private:
        void k() { }
};
class Derived1:public Base
{
    protected:
        using Base::g;
    private:
        using Base::f;
    public:
        //using Base::k;
        using Base::h;
};
class Derived2:private Base
{
    public:
        using Base::g;
    protected:
        using Base::f;
};
int main()
{
    Base b; b.f(); b.f(1); b.g(); //b.h();
    Derived1 d1; d1.h(); //d1.g(); d1.f();
    Derived2 d2; d2.g(); //d2.f();
    cin.get();
}
/*Output*/
/*
public: Base::f()
public: Base::f(int)
public: Base::g()
protected: Base::h()
public: Base::g()
*/

派生类对象的创建和撤销

class Point2d
{
    public:
	    Point2d(double x=0.0,double y=0.0):_x(x),_y(y){};
	    double x() {return _x;}
	    double y() {return _y;}
	    void x(double newX) {_x=newX;}
	    void y(double newY) {_y=newY;}
    protected:
        double _x,_y;
};

class Point3d:public Point2d
{
    public:
        Point3d(double x=0.0,double y=0.0,double z=0.0)
        :Point2d(x,y), _z(z) {}
        double z() {return _z;}
        void z(double newZ) {_z=newZ;}
        void print()
        {
            cout<<'('<<x()<<','<<y()<<','<<z()<<')';
        }
    protected:
        double _z;
};

在撤销一个派生类对象时, 基类了对象也被撤销。析构函数的执行次序和构造函数的执行次序相反, 即先执行派生类的析构函数, 再执行基类的析构函数。

继承与特殊成员

1.禁止继承的类
如果不希望一个类被其他类继承,可以在类名后跟一个关键字final

2.不能自动继承的成员
并不是所有的基类成员都能被派生类继承, 下列成员函数是不能继承的:

  • 构造函数
  • 析构函数
  • 赋值运算符函数

如果在派生类中没有定义这些函数, 编译器在必要时会自动生成派生类的默认构造函数、拷贝/ 移动构造函数、拷贝/ 移动赋值运算符和析构函数。在编译器自动生成的构造函数中会调用相应的基类构造函数来完成基类子对象的初始化。编译器自动生成的赋值运算符函数只能用于同类型对象之间的赋值, 其行为是按成员赋值, 如果要对不同类型的对象赋值, 需要自己定义赋值运算符。

3.复用基类的构造函数
派生类复用基类构造函数的方式是在派生类定义中提供一条using声明:

class Derived:public Base
{
    using Base::Base;
}

using声明不改变构造函数的访问级别,无论出现在哪里,基类的构造函数在派生类中仍然是原来的访问权限。

派生类可以复用基类的构造函数, 同时定义自己的。一部分构造函数。如果派生类定义的构造函数与基类的构造承数有相同的参数列表, 则不会继承基类的这个构造函数, 在派生类中定义的构造函数将替换继承到的基类构造函数。

派生类不能复用默认、拷贝和移动构造函数, 如果没有直接定义这些构造函数, 编译器将按照正常规则为派生类自动生成。
继承的基类构造函数不会被作为类中定义的构造函数, 也就是说, 如果一个类只含有继承的基类构造函数, 那么编译器认为它没有定义任何构造函数, 会自动生成默认构造函数。

4.静态成员的继承
如果基类定义了一个static成员, 则在整个继承层次中只存在该成员的唯一定义。不论从基类派生出来多少个派生类, 对于每个静态成员来说都只存在唯一的实例。

class Base
{
    public:
        static void statmem(); //基类static成员
};
void Base::statmem() {}

class Derived:public Base
{
    public:
        void f(const Derived& d);
};
void Derived::f(const Derived& d)
{
    Base::statmem(); //正确,Base类中定义了statmem()
    Derived::statmem(); //正确,Derived类中继承了statmem()
    d.statmem(); //正确,通过派生类对象访问基类static成员
    statmem(); //正确,通过this指向对象访问static成员
}

派生类与基类的不同

在派生类中修改基类的方式有如下两种。
( 1 ) 覆盖或隐藏基类的操作: 重新定义基类接口中已经存在的操作, 从而改变继承到的行为, 使得派生类对象在接收到同样的消息时其行为不同于基类对象。
( 2 ) 扩充接口: 向派生类的接口中添加新操作,使得派生类对象能够接收更多的消息。

覆盖与同名隐藏

class Point2d
{
    public:
        Point2d(double x=0.0,double y=0.0):_x(x),_y(y){};
        double x() {return _x;}
        double y() {return _y;}
        void x(double newX) {_x=newX;}
        void y(double newY) {_y=newY;}
        void moveto(double x,double y) {_x=x;_y=y;}
        void f(int) {}
        void f() {}
        void g(char) {}
    protected:
        double _x,_y;
};

class Point3d:public Point2d
{
    public:
        Point3d(double x=0.0,double y=0.0,double z=0.0)
        :Point2d(x,y), _z(z) {}
        double z() {return _z;}
        void z(double newZ) {_z=newZ;}
        void moveto(double x,double y,double z) //覆盖
        {
            Point2d::moveto(x,y);
            _z=z;
        }
        void f() {} //覆盖了Point2d中的f(),同时隐藏了f(int)
        void g() {} //隐藏了Point2d中的 void g
    protected:
        double _z;
};

覆盖:在派生类中重定义基类接口中的成员函数, 参数表和返回类型保持与基类中一致

隐藏:在派生类中重定义基类接口中的成员函数, 并改变了函数的参数表或返回类型

即使派生类中覆盖了基类的同名承数, 但是如果设置了不同的访问限制, 那么也会引起派生类和基类接口的差异。例如:

class Base
{
    public: //公有接口中有两个操作f和g
        void f() {}
        void g() {}
};
class Derived:public Base
{
    public: //公有接口中只有一个操作f
        void f() {}
    private: //私有的g覆盖了Base中公有的g
        void g() {}
};

派生类向基类类型的转换

继承最重要的特性之一是替代原则: 在任何需要基类对象( 或地址) 的地方, 都可以由其公有派生类的对象( 或地址) 代替。替代原则有时也被称为赋值兼容规则
在C++ 语言中,公有派生类就是基类的子类型, 公有派生类的对象可以自动转换为基类类型, 基类的指针和引用可以指向派生类的对象。例如:

class Base {};
class Derived:public Base {};
int main()
{
    //派生类对象代替基类对象
    Base b;
    Derived d;
    b=d; //正确:对象类型转换,派生类左值代替基类左值
    Base& rb=d; //正确,引用类型转换
    Base* pb;
    pb=&d;
    pb=new Derived;
    delete pb;
}

因为是从更特殊的类型转换到更一般的类型, 所以派生类向基类类型转换总是安全的。

//派生类向基类的隐式类型转换
class Point2d
{
    public:
	    Point2d(double x=0.0,double y=0.0):_x(x),_y(y){};
	    double x() {return _x;}
	    double y() {return _y;}
	    void x(double newX) {_x=newX;}
	    void y(double newY) {_y=newY;}
        void print()
        {
            cout<<'('<<x()<<','<<y()<<')'<<endl;
        }
    protected:
        double _x,_y;
};

class Point3d:public Point2d
{
    public:
        Point3d(double x=0.0,double y=0.0,double z=0.0)
        :Point2d(x,y), _z(z) {}
        double z() {return _z;}
        void z(double newZ) {_z=newZ;}
        void print()
        {
            cout<<'('<<x()<<','<<y()<<','<<z()<<')'<<endl;
        }
    protected:
        double _z;
};

int main()
{
    Point2d p2(1,2), *pt2=&p2;
    Point3d p3(4,5,6), *pt3=&p3;
    pt2->print(); //Point2d::print()
    pt3->print(); //Point3d::print()
    p2=p3; //向上类型转换-对象切片
    p2.print(); //Point2d::print()
    Point2d& r2=p3; //向上类型转换-引用
    r2.print(); //Point2d::print()
    pt2=&p3; //向上类型转换-指针
    pt2->print(); //Point2d::print()
}
/*Output*/
/*
(1,2)
(4,5,6)
(4,5) //对象切片现象
(4,5)
(4,5)
*/

对象切片只是派生类向基类转换过程中改变地址的类型,用不同的方式解读同一段内存空间中的内存,并不会真正切除派生类对象多余的部分

组合与继承的选择

通过组合语法创建新类型时, 通常将己有类型的对象作为私有成员, 这使得被嵌入的成员成为了新类型的内部实现, 新类型可以不受其成员的约束, 向外提供完全不同的接口。即使其内部成员或实现方式发生改变, 也不会影响外部客户代码。组合具有很大的灵活性,是一种简单有效的代码复用方法, 在面向对象的设计模式中得到了大量应用。

继承是面向对象技术中另一种复用代码的重要机制。继承使得派生类与基类之间具有接口的相似性,派生类可以看作是基类的特殊子类型, 派生类对象可以替代基类对象。是否使用继承的一个重要依据便是考察类之间是否存在这种关系, 是否需要由基类提供公共接口, 是否需要派生类向基类的类型转换。

总结:
1.如果多个类共享数据而非行为, 应该创建这些类可以包含的共用对象。
2.如果多个类共享行为而非数据, 应该让它们从共同的基类继承而来, 并在基类里定义共用的操作。
3.如果多个类既共享数据也共享行为, 应该让它们从一个共同的基类继承而来, 并在基类里定义共用的数据和操作。
4.如果想由基类控制接口,使用继承; 如果想自己控制接口, 使用组合。

一个组合的例子:学生成绩单

//score.h
#ifndef SCORE_H
#define SCORE_H

#include<iostream>
#include<string>
using std::string;
using std::istream;
using std::ostream;
class Score
{
    public:
        Score(unsigned long id,string name,int p,int m,int f)
        :sid(id),sname(name),project(p),mid(m),final(f) {}
        Score():Score(0UL,"",0,0,0) {}
        Score(unsigned long id,const string& name=" ")
        :Score(id,name,0,0,0) {}
        ~Score()=default;
    //getter
        unsigned long getSid()const {return sid;}
        const string& getSname()const {return sname;}
        double totalScore()const;
    //便于使用标准算法进行查找
        bool operator==(const Score& s)const;
        bool operator!=(const Score& s)const;
    //重载I/O
        friend ostream& operator<<(ostream& os,const Score& s);
        friend istream& operator>>(istream& is,Score& s);   
    private:
        unsigned long sid;
        string name;
        int project=0;
        int mid=0;
        int final=0;
};

#endif

//function declarations
istream& operator>>(istream is,Score& s);
ostream& operator<<(ostream& os,const Score& s);

//score.cpp
#include"score.h"

double Score::totalScore()const
{
    double total=0.6*final+0.2*project+0.2*mid;
    return total;
}
bool Score::operator==(const Score& s)const
{
    return(s.sid==sid);
}
bool Score::operator!=(const Score& s)const
{
    return !(*this==s);
}
istream& operator>>(istream&is,Score& s)
{
    is>>s.sid>>s.sname>>s.project>>s.mid>>s.final;
    return is;
}
ostream& operator<<(ostream& os,const Score& s)
{
    os<<s.sid<<" "<<s.sname<<" "<<s.project<<" "<<s.mid
    <<" "<<s.final<<" "<<s.totalScore();
    return os;
}

//scoresheet.h
#ifndef SCORESHEET_H
#define SCORESHEET_H

#include<iostream>
#include<string>
#include<vector>
#include"score.h"
using std::string; using std::vector;
using std::istream; using std::ostream;

class ScoreSheet
{
    public:
        ScoreSheet()=default;
        ~ScoreSheet()=default;
        void addItem(const Score& s);
        void print(ostream& os)const;
        string queryScore(unsigned long id)const;
    private:
        vector<Score>sheet;
};

#endif

//scoresheet.cpp
#include<sstream>
#include<algorithm>
#include"scoresheet.h"
using namespace std;

void ScoreSheet::addItem(const Score& s)
{
    sheet.push_back(s);
}
void ScoreSheet::queryScore(unsigned long id)const
{
    ostringstream os;
    Score s(id);
    auto it=find(sheet.begin(),sheet.end(),s);
    if(it!=sheet.end())
    {
        os<<*it;
    }
    else
    {
        os<<"Student "<<id<<" doesn't exist.";
    }
    return os.str();
}
void ScoreSheet::print(ostream& os)const //输出到指定流
{
    for(auto e:sheet)
        os<<e<<endl;
}

//test.cpp
#include<iostream>
#include"score.h"
#include"scoresheet.h"
using namespace std;
int main()
{
    ScoreSheet ss;
    cout<<"Enter Students' Scores: project id final"<<endl;
    Score s;
    while(cin>>s)
    {
        ss.addItem(s);
    }
    cout<<"Score Sheet"<<endl;
    ss.print(cout);
    return 0;
}

补充:多文件命令行编译运行
-g 可执行程序包含调试信息
-o 指定输出文件名
-c 只编译不链接

以下四种命令都可以用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

多重继承

class B1 {};
class B2 {};
class D1:public:B1,B2 {}; //D1公有继承B1,私有继承B2
class D2:public:B1,public:D1 {};

创建派生类的对象时,依据基类的声明顺序执行各个基类的构造函数
如果多重继承中两个基类中有同名成员,会产生二义性,这样的二义性可以通过显式指定类名来消除

obj.B1::f();
obj.b2::f();

C + + 不允许一个基类被同一个派生类多次直接继承, 这会导致编译错误。但是一个类可能间接地被继承多次

class A{};
class B:public A {};
class C:public A {};
class D:public B,public C {};

在这里插入图片描述
类A 被D 间接地继承了两次。在D 对象中会间接包含两个A 类的子对象。在创建D 类对象时, A 的构造数会被执行两次。对A 类成员的引用也会引起二义性。

虚基类

在基类名字前加virtual

class A{};
class B:virtual public A {};
class C:virtual public A {};
class D:public B,public C {};

虚基类的构造函数是由最终产生对象的那个派生类的构造函数调用的
创建D类对象时,构造函数调用顺序:A,B,C,D

总之,慎用继承

虚函数与多态性

C++通过虚函数和动态绑定实现多态性

派生类向基类的类型转换

class employee
{
    public:
        void salary() {}
};
class manager:public employee
{
    public:
        void salary() {}
};
class programmer:public employee
{
    public:
        void salary() {}
};
class parttime:public employee
{
    public:
        void salary() {}
};
void payroll(employee& re)
{
    re.salary();
}

派生类虽然可以隐式转换为基类类型,但类型转换会损失类型信息,不能真正实现替代原则。C++默认的函数调用绑定方式是静态绑定,在程序运行之前,由编译器和链接器实现。
动态绑定将绑定推迟到程序运行时,可以获知实际接收消息的对象类型,根据这时的信息绑定函数调用。

//如果动态绑定,运行下面的代码之前re.salary()没有和任何函数体联系
manager Harry;
programmer Ron;
patrtime Albus;
//执行re.salary()时进行动态绑定
payroll(Harry); //re指向Harry,将re.salary()动态绑定到manager::salary

虚函数

声明虚函数

在C + + 语言中, 基类将两种成员函数区分开来: 一种是基类希望派生类覆盖的函数,另一种是基类希望派生类直接继承而不要改变的函数。
对于前者, 基类将其定义为虚函数。当使用指针或引用调用虚函数时, 该调用将被动态绑定。

动态绑定语法形式
virtual 返回类型 成员函数名(参数表)
动态绑定只对虚函数起作用,并且只有在使用含有虚函数的基类地址(指针或引用)调用时发生。
virtual只能在类内的成员函数声明之前, 不能
用于类外的函数定义。除了构造函数之外, 任何非静态成员函数都可以是虚函数。派生类中可以重定义基类的虚函数, 这称为覆盖( override ) 。例如:

class employee
{
    public:
        virtual void salary() {}
};
class manager:public employee
{
    public:
        void salary() {}
};
class programmer:public employee
{
    public:
        void salary() {}
};
class parttime:public employee
{
    public:
        void salary() {}
};
void payroll(employee& re)
{
    re.salary(); //动态绑定
}

计算一组图形的面积:

class shape
{
    public:
        virtual double area()const { return 0; }
};
class rectangle:public shape
{
    public:
        double area()const {return height*width;}
    private:
        double height,width;
};
class circle:public shape
{
    public:
        double area()const {return PI*radius*radius;}
    private:
        double radius;
};
shape* p[N];
double total_area=0;
for(int i=0;i<N;i++)
    total_area+=p[i]->area();

学生成绩实例:

#include<iostream>
#include<string>
using namespace std;
class Score
{
    public:
        Score(unsigned long id,const string& name,double fnl=0)
        :sid(id),sname(name),final(fnl) { }
        void setFinal(int sc) {final=sc;}
        int getFinal()const {return final;}
        unsigned long getId()const {return sid;}
        const string& getName()const {return sname;}
        //希望派生类覆盖的函数声明为虚函数
        virtual double getTotal()const {return final;}
        virtual void print()const
        {
            cout<<getId()<<" "<<getName()<<" "<<getTotal()<<endl;
            //此处对getTotal的调用实际是this->getTotal():基类指针调用虚函数
            //因此此处getTotal是动态绑定的,根据this指向的对象类型确定
        }
    private:
        unsigned long sid;
        string sname;
        int final=0;
};

class Required:public Score
{
    public:
        Required(unsigned long id,string name,int p,int m,int f)
        :Score(id,name,f),project(p),midterm(m) {}
        Required(unsigned long id,const string& name)
        :Required(id,name,0,0,0) {}
        int getMidterm()const {return midterm;}
        void setMidterm(int mid) {this->midterm=mid;}
        int getProject()const {return project;}
        void setProject(int project) {this->project=project;}
        double getTotal()const override//覆盖基类的虚函数
        {
            double total=0.6*getFinal()+0.2*project+0.2*midterm;
            return total;
        }
    private:
        int project=0;
        int midterm=0;
};

class Optional:public Score
{
    public:
        Optional(unsigned long id,string name,int r,int f)
        :Score(id,name,f),report(r) {}
        Optional(unsigned long id,const string& name)
        :Optional(id,name,0,0) {}
        int getReport()const {return report;}
        void steReport(int report) {this->report=report;}
        double getTotal()const override
        {
            double total=0.5*getFinal()+0.5*report;
            return total;
        }
    private:
        int report=0;
};

void printScore(const Score& sc)
{
    sc.print();
}

int main()
{
    Score sc(20190242,"Hive",99);
    Required reqsc(20190110,"Alveus",83,88,78);
    Optional optsc(20192234,"Reyes",89,92);
    sc.print();
    reqsc.print();
    optsc.print();
    printScore(sc);
    printScore(reqsc);
    printScore(optsc);
    cin.get();
}
/*Output*/
/*
20190242 Hive 99
20190110 Alveus 81
20192234 Reyes 90.5
20190242 Hive 99
20190110 Alveus 81
20192234 Reyes 90.5
*/

override可以提示读者某个函数重写了基类虚函数,表示这个虚函数是从基类继承,不是派生类自己定义的;强制编译器检查某个函数是否重写基类虚函数,如果没有则报错。(程序正确的情况下可有可无)

因为直到运行时才能知道到底调用哪个版本的虚函数, 所以所有虚函数都必须有定义, 无论它是否被调用到, 因为编译器也无法确定到底会使用哪个虚函数。
只有非静态成员函数可以声明为虚函数。静态成员函数没有this 指针, 其调用实际上只与类相关, 而虚函数调用的绑定是根据对象类型确定的。

虚函数的覆盖规则

如果基类声明了一个函数是虚函数, 在所有派生类中, 即使不再重复声明, 该函数也是虚函数。派生类可以根据自己的需要覆盖基类中的虚函数实现。如果没有覆盖, 那么派生类将继承基类的虚函数。

为了保持虚函数的多态性, 在派生类中覆盖基类的虚函数时要用相同的参数表和返回类型, 否则:

  1. 若派生类中重定义的虚函数参数表与基类中不同, 则被视为是定义了另一个独立的同名函数, 在派生类中将会隐藏基类的虚函数, 却没有覆盖掉基类中的版本, 因而不能再进行多态调用。
    C++11中可以使用关键字override来标记派生类中的虚函数, 使程序员的意图更清晰, 并且让编译器可以检查出这种错误。
  2. 若派生类中重定义基类的虚函数时保持函数名和参数表相同, 但是返回类型不相同, 则编译器报告“ 返回类型不一致” 错误。
    当基类的虚函数返回的类型是基类本身的指针或引用时, 派生类中重写的虚函数可以返回派生类的指针或引用, 仍被认为是覆盖的虚函数。
/*虚函数的覆盖和继承*/
#include<iostream>
#include<string>
using namespace std;

class Base
{
    public:
        virtual int f()const
        {
            cout<<"Base::f()"<<endl;
            return 1;
        }
        virtual void f(string)const {}
        virtual void g()const {}
};

class Derived1:public Base
{
    public:
    //覆盖虚函数g,继承了虚函数f()和f(string)
    void g()const override {}
};

class Derived2:public Base
{
    public:
    //覆盖虚函数f(),继承了g(),隐藏了f(string)
    int f()const override
    {
        cout<<"Derived2::f()"<<endl;
        return 2;
    }
};

class Derived3:public Base
{
    public:
    //改变虚函数的参数表,隐藏了f(string)和f(),继承了g
    int f(int)const //此处改变参数表,不能用override
    {
        cout<<"Derived3::f()"<<endl;
        return 3;
    }
};

int main()
{
    string s("hello");
    Derived1 d1;  int x=d1.f();  d1.f(s);
    Derived2 d2;  x=d2.f();
    cin.get();
}
/*
Base::f()
Derived2::f()
*/

如果希望一个虚函数不能被覆盖, 可以将该函数定义为final, 方法是在函数的形参列表之后加关键字。

虚析构函数

基类的析构函数如果声明为虚函数,其派生类的析构函数不加virtual也是虚函数

class Base1
{
    public:
        virtual ~Base1() { cout<<"~Base1()"<<endl; }
};
class Derived1:public Base1
{
    public:
        ~Derived1() { cout<<"~Derived1()"<<endl; }
};

实现多态性的步骤

使用虚函数实现多态性的一般步骤:
(1)在基类中将需要多态调用的成员函数声明为virtual
(2)在派生类中覆盖基类的虚函数,实现各自需要的功能;
(3)用基类的指针引用指向派生类对象,通过基类指针或引用调用虚函数。

注意,只有通过基类的指针或引用才能实现虚函数的多态调用,通过对象调用虚函数不会有多态性。

使用基类指针或引用调用虚函数时的一个常见问题是忘记了指针或引用本身的类型

以指针为例, 声明指针时指针是基类类型,而指针可能指向派生类对象。因此,与指针相关的有两个类型: 一个是声明时的类型,编译时可以知道的,称为静态类型;一个是运行时指针实际指向的对象类型,运行时才能得知,称为动态类型

通过指针调用函数时, 编译时对调用进行静态类型检查, 依据的是指针的静态类型,因此, 指针所属类型的接口决定了函数调用的匹配。

也就是说, 通过基类指针只能调用基类接口中出现的成员函数, 不能调用派生类中新增加的函数, 即使是虚函数。
对非虚函数和对象调用的函数都是在编译时进行绑定, 因为对象的静态类型和动态类型永远都是一致的。通过指针调用虚函数时进行动态绑定, 由指针的动态类型决定运行时执行哪个版本。

/*基类接口和虚函数调用*/
class Base
{
    public:
        virtual void f() {}
};

class Derived:public Base
{
    public:
        void f() {}
        virtual void g() {} //派生类增加的成员函数
};

int main()
{
    Base b;  b.f();
    Derived d;  d.f();  d.g();
    Base* pb=&d;
    pb->f(); //正确,多态调用,绑定到Derived::f();
    pb->g(); //错误,Base中无g,虽然指向d,但pb是Base类型的
}

通过上面的讨论可以看到, 派生类如果扩充了基类接口, 其扩充的部分不能通过基类对象或地址调用。要实现多态性, 保持派生类与基类的接口一致非常重要

动态绑定的实现

如果一个类中包含虚函数,编译器会为该类创建一个虚函数表VTABLE,表中保存该类所有虚函数的地址。同时,编译器还在这个类中放置一个秘密的指针成员VPTR,VPTR指向该类的VTABLE

class shape
{
    public:
        virtual double area()const{return 0;}
        virtual void draw(){}
};

class rectangle:public shape
{
    public:
        double area()const{return height*width;}
        void draw(){...}
    protected:
        double height,width;
};

class squre:public rectangle
{
    public:
        void draw(){...}
};

class circle:public shape
{
    public:
        double area()const{return PI*radius*radius;}
        void draw(){...}
    private:
        double radius;
};

shape* sa[]={new circle,new rectangle,new rectangle,new squre};

编译器创建的VTABLE和VPTR如下:
在这里插入图片描述
当通过基类指针调用一个虚函数时, 例如上面的sa[0] , 它指向circle 对象的起始地址。编译器从sa [ 0 ] 指向的对象中取出VPTR , 根据VPTR 找到相应的虚函数表VTABLE 再根据函数在VTABLE 中的偏移, 找到适当的函数。因此,不是根据指针的类型shape* 决定调用shape::area() , 而是调用“ VPTR +偏移量” 处的函数

因为获取VPTR 和确定实际的函数地址发生在运行时, 所以就实现了动态绑定。

派生类如果覆盖了基类中的虚函数, 就在派生类的VTABLE 中保存新版本虚函数的地址, 没有重定义的仍使用基类虚函数的地址。同一个虚函数在派生类和基类的VTABLE 中处于相同的位置。如果派生类增加了新的虚函数, 则编译器先将基类的虚函数准确地映射到派生类的VTABLE 中, 再加入新增加的虚函数地址。只存在于派生类中的虚函数不能通过基类指针调用

抽象类

C++中用纯虚函数定义抽象操作,纯虚函数没有实现。包含至少一个纯虚函数的类称为抽象类。例如, 可以将交通工具类中的“ 驾驶” 操作定义为纯虚函数, 而交通工具类就成为了抽象类。抽象类一般只用作其他类的基类, 因此也被称为抽象基类

定义纯虚函数的语法如下:
virtual 返回类型函数名(参数表)=0;

class Shape
{
    public:
        virtual double area()=0; //纯虚函数
        virtual double perimeter()=0; //纯虚函数
};//包含纯虚函数,是抽象类

使用抽象类要注意以下几点。
( 1 ) 如果一个类中包含至少一个纯虚函数, 这个类就是抽象类。如果一个抽象类中的所有成员数都是纯虚函数, 这个类称为纯抽象类。C++中的纯抽象类类似于Java中的接口
( 2 ) 当继承一个抽象类时, 要在派生类中实现( 覆盖)所有的纯虚函数, 否则派生类也被看作是一个抽象类。
( 3 ) 不能创建抽象类的实例,但可以创建由抽象类派生的具体子类的实例, 也可以定义抽象类的指针或引用,它们指向具体派生类的对象。事实上,程序中往往要通过抽象基
类的指针或引用来实现虚函数的多态调用。
( 4 ) 抽象类中可以包含普通成员函数,在普通成员函数中可以调用纯虚函数,因为纯虚函数被推迟到某个具体派生类中实现,由于虚函数的动态绑定,在派生类对象实施这个调用时会体现为具体操作对具体操作的调用。

//纯虚函数的调用
#include<iostream>
using namespace std;

class abstract
{
    public:
        virtual void pf()=0;
        void f()
        {
            cout<<"In abstract::f()"<<endl;
            pf(); //由调用f()的对象类型决定调用哪个pf()
        }
};

class concrete:public abstract
{
    public:
        void pf()
        {
            cout<<"concrete::pf()"<<endl;
        }
};

int main()
{
    concrete c;
    c.f();
    abstract& ra=c;
    ra.f();
    cin.get();
}
/*
In abstract::f()
concrete::pf()
In abstract::f()
concrete::pf()
*/

派生类concrete中没有覆盖基类的f(), 所以c.f() 会引起对abstract::f()的调用,f() 的this 指针指向c ;
f() 中调用pf() 时, 实际上是this->pf() ,由于pf() 是虚函数,根据this 指向的对象c的类型, 将调用concrete:pf()
这种方法可以用来实现面向对象中的回调一一在基类成员函数中调用派生类的成员函数, 这是框架(framework) 代码中常用的技术。

( 5 ) 抽象基类和具体派生类的关系是一种继承关系。在确定抽象基类的接口时,应该确保接口中的操作是同类对象共同行为的抽象。否则, 会为继承这个抽象基类的整个类层次带来不利影响, 抽象基类中的变化会引起整个类层次的改变, 所以抽象基类中的信息应该尽可能简单, 尽可能和研究对象的本质相关

RTTI

RTTI:Run-Time Type Identification
RTTI允许使用基类指针或引用来操纵对象的程序获得这些指针或引用实际所指对象的类型。
C++为支持RTTI提供了以下两个运算符

  1. dynamic_cast 允许在运行时刻进行类型转换, 从而使程序能够在一个类层次结构中安全地转换类型, 将基类指针转换为派生类指针, 或者将基类的引用转换为派生类的引用。当然, 这种转换是在确保成功的情况下才进行的。
  2. typeid 指出指针或引用指向的对象的实际类型。

dynamic_cast运算符的操作数类型必须是带有一个或多个虚函数的类。对于带有虚函数的类类型操作数,RTTI 是运行时刻的操作,对其他操作数而言,它只是编译时刻的事件。

dynamic_cast与向下类型转换

同样的基类指针到派生类指针的类型转换, 其安全性不同。当基类指针指向派生类对象时,向下类型转换是安全的;如果基类指针指向基类对象或者其他派生类的对象,那么这种向下类型转换就是危险的。指针指向的对象到底是什么类型只有在程序运行期间才可以获知, 因而需要运行时的类型信息才能判断是否可以安全地转换, 并真正施转换。

显式类型转换dynamic_cast可以把一个类类型对象的指针转换成同一类层次结构中的其他类的指针, 也可以把一个类类型对象的左值转换为同一类层次结构中其他类的引用。和其他显式转换不同的是,dynamic_cast是在运行时执行的。如果指针或左值操作数不能被安全地转换为目标类型, 则dynamic_cast 将失败。如果是对指针类型的dynamic_cast 失败, 则dynamic_cast 的结果是空指针,即0 。如果针对引用类型的dynamic_cast 失败, 则dynamic_cast 会抛出一个bad_cast 类型的异常。

dynamic_cast 先检验所请求的转换是否有效, 只有在有效时才会执行转换,而检验过程发生在程序运行时。dynamic_cast 用于基类指针到派生类指针的安全转换, 它被称为安全的向下类型转换。如果必须通过基类指针(或引用) 使用派生类的特性, 而该特性又没有出现在基类接口中时, 可以使用dynamic_cast

需要注意的是:
( 1 ) 使用dynamic_cast 时, 必须对一个含有虚函数的类层次进行操作。
( 2 ) dynamic_cast 的结果必须在检测是否为0之后才能使用。

void company::payroll(employee *pe)
{
    programmer* pm=dynamic_cast<programmer*>(pe);
    //如果pe指向一个programmer类的对象,则转换成功
    if(pm)//用pm调用派生类programmer的成员函数bouns发奖金
    {
        pm->bouns();
    }
    else { }
    //使用employee的成员,正常发放所有员工的工资
    pe->salary;
}

typeid

RTTI 提供的typeid 运算符对类型或表达式进行操作, 返回操作数的确切类型。
typeid运算符返回一个type_info 类型的引用。type_info 在头文件 中定义。
例如:

#include<typeinfo>
programmer pobj;
employee& re=pobj;
//type_info::name()返回C风格字符串表示的类型名
cout<<typeid(re).name()<<endl;
//输出 "programmer"

typeid运算符必须与表达式或类型名一起使用。当typeid运算符的操作数是类类型,但不是带有虚函数的类类型时,typeid运算符会指出操作数的类型, 而不是底层对象的类型。

#include<typeinfo>
class Base {/*无虚函数*/};
class Derived:public Base {/*无虚函数*/};
Derived dobj;
Base* pb=&dobj;
cout<<typeid(*pb).name()<<endl;
//输出"Base"

类层次设计的例子

两种设计方式:自顶向下、自底向上

模仿钓鱼的例子

UML类图中加下划线的操作是类属性和类操作,在C++中用静态成员实现
在这里插入图片描述

//fish.h
#ifndef FISH_H
#define FISH_H
#include<string>
using namespace std;
class Fish
{
    public:
        virtual bool acceptable()const=0;
        virtual void setWeight()=0;
        double getWeight()const {return weight;}
        virtual string getName()const=0;
    protected:
        double weight;
};

class GoldenFish:public Fish
{
    public:
        bool acceptable()const
        {
            if(getWeight()<=2&&getWeight()>=1)
            {
                return true;
            }
            return false;
        }
        void setWeight()
        {
            weight=rand()/(double)(RAND_MAX/5);
        }
        string getName()const {return NAME;}
    private:
        static const string NAME;
        static const double MAX_WEIGHT;
};
const string GoldenFish::NAME="GoldenFish";
const double GoldenFish::MAX_WEIGHT=5.0;

class SilverFish:public Fish
{
    public:
        bool acceptable()const
        {
            if(getWeight()<=1.5&&getWeight()>=0.5)
            {
                return true;
            }
            return false;
        }
        void setWeight()
        {
            weight=rand()/(double)(RAND_MAX/7);
        }
        string getName()const {return NAME;}
    private:
        static const string NAME;
        static const double MAX_WEIGHT;
};
const string SilverFish::NAME="SilverFish";
const double SilverFish::MAX_WEIGHT=7.0;

#endif

//test.cpp
#include<iostream>
#include<cstdlib>
#include<ctime>
#include"fish.h"

//随机产生0~num-1的整数
int random(int num)
{
    return static_cast<int>((num*static_cast<long>(rand()))/(RAND_MAX+1L));
}

double totalweight=0;

void handle(Fish* fp)
{
    fp->setWeight();
    if(fp->acceptable())
    {
        totalweight=fp->getWeight()+totalweight;
        cout<<fp->getName()<<": "<<fp->getWeight()<<"kg"<<endl;
    }
}

//随机生成两种鱼
int main()
{
    srand((unsigned)time(0)); //种子随机数发生器
    Fish* fp;
    while(totalweight<15.00)
    {
        int type=random(2);
        if(type==1)
        {
            fp->new GoldenFish;
            handle(fp);
        }
        else
        {
            fp=new SilverFish;
            handle(fp);
        }
        
    }
}

handle()函数灵活且易于扩展,如果添加其他类型的鱼,handle代码不会受到任何影响

零件库存管理的例子

在这里插入图片描述
( 1 )关联: 每个Part 对象都关联到一个目录, 用Part 类的一个指向Catalog 的指针成员实现。
( 2 )继承:Component是一个抽象类, 提供了两个抽象操作:Part和Assembly是Compoent 的具体派生类,要分别实现自己的getName()和getPrice()操作。
( 3 )聚合:AssembIy是由一组Compoent,即Part或AssembIy对象组成的,addComponent()实现组装操作。Assembly 类中的向量数据成员comp 存储组装件的多个组成部件。

//parts.h
#ifndef PARTS_H
#define PARTS_H

#include<string>
#include<vector>
using namespace std;
//零件类目
class Catalog
{
    public:
        Catalog(long id,string nm,double pr) {name=nm;ID=id;price=pr;}
        string getName() {return name;}
        double getPrice() {return price;}
        long getID() {return ID;}
        void changePrice(double pr) {price=pr;}
    private:
        string name;
        double price;
        long ID;
};

//抽象部件类
class Component
{
    public:
        virtual double getPrice()=0;
        virtual string getName()=0;
};

//零件类
class Part:public Component
{
    public:
        Part(Catalog* c):cat(c) {} //零件对象必须有分类目录
        string getName() {return cat->getName();}
        double getPrice() {return cat->getPrice();}
        double getID() {return cat->getID();}
    private:
        Catalog* cat; //零件所属的类目
};

//组装件类
class Assembly:public Component
{
    public:
        void addComponent(Component* cp)
        {
            comp.push_back(cp);
        }
        string getName()
        {
            string namelist("");
            for(int i=0;i<comp.size();i++)
            {
                namelist+=comp[i]->getName(); //多态调用
                namelist+=" ";
            }
            return namelist;
        }
        double getPrice()
        {
            double price=0;
            for(int i =0;i<comp.size();i++)
            {
                price+=comp[i]->getPrice(); //多态调用
            }
            return price;
        }
    private:
        vector<Component*>comp; //组成部分
};

#endif

//test.cpp
#include<iostream>
#include"parts.h"
using namespace std;
int main()
{
    //创建各类零件的分类目录
    Catalog c1(100691,"struct",0.3);
    Catalog c2(100692,"screw",0.5);
    Catalog c3(100693,"bolt",0.2);
    //创建各种零件对象
    Part struct1(&c1),struct2(&c1);
    Part screw1(&c2);
    Part bolt1(&c3),bolt2(&c3);
    Assembly a1,a2;
    a1.addComponent(&struct1);
    a1.addComponent(&struct2);
    a1.addComponent(&bolt1);
}
/*
struct1: struct 0.3
screw1: screw 0.5
assembly 1: struct struct bolt 0.8
*/

模板与泛型编程

函数模板

函数模板提供了一种用算法模板自动生成各种类型函数实例的机制。程序员将函数接口(参数表和返回类型)中的全部或者部分类型参数化, 而函数体保持不变。如果一个函数的实现对一组实例都是相同的, 区别仅在于每个实例处理不同的数据类型, 那么该函数就可以定义为函数模板。
定义函数模板的语法如下:
template<模板参数表>函数返回类型 函数名 (函数参数表) {函数体}

template<typename T>
int compare(const T& v1,const T& v2)
{
    if(v1<v2) return -1;
    if(v2<v1) return 1;
    return 0;
}

模板类型参数由关键字classtypename后加一个标识符构成。
class 和typename 这两个关键字在模板参数表中的意义是相同的, 表示后面的标识符代表一个潜在的内置类型或用户自定义类型。
一个函数模板可以有多个类型参数,每个类型参数前都要有class 或typename关键字。当模板被实例化时,会由实际的类型替换模板的类型参数。

模板非类型参数由一个普通的参数声明构成。模板非类型参数表示该参数名代表了一个潜在的值,这个值是模板定义中的一个常量。在模板被实例化时, 非类型参数会被一个编译时刻己知的常量值代替。

模板参数之后是函数定义或声明, 除了可以使用模板参数指定的类型或常量值以外,函数模板的定义与普通函数的定义相同。

返回最小元素:

template<class Type> Type min(Type array[],int size)
{
    Type min_val=array[0];
    for(int i=1;i<size;i++)
    {
        if(array[i]<min_val)
        {
            min_val=array[i];
        }
    }
    return min_val;
}

也可以这样写:

template<class Type,int size> Type min(cosnt Type(&array)[size])

使用模板时必须能够通过上下文为每一个模板实参确定一个唯一的类型或值,否则编译错误

compare(2,6.7);//错误,推断const T的类型出现冲突
解决:显式指定模板实参
compare<double>(2,6.7);//正确,对实参2进行类型转换
compare<int>(2,6.7);//正确,对6.7进行类型转换

template<class T1,class T2,class T3>T1 func(T2 arg1,T3 arg2) {}
int main()
{
    int x,y;
    double d;
    x=func(y,d); //错误,调用中没有T1信息,不能推演出T1
    x=func<int,int,double>(y,d); //正确,显式指定
    x=func<int>(y,d); //正确,显式指定T1,T2和T3可靠推演得出
}

显式模板实参应该只用在解决二义性时,或用在模板实参不能被推演出来的情况下。
应尽可能省略显式模板实参
函数模板可以被另一个模板或一个普通非模板函数重载,名字相同的函数必须具有不同数量或类型的参数(一般是改变参数个数)

下面的是同一个函数模板的重复定义而不是重载,会引起编译错误
template<class T1>T1 min(T1 a,T1 b)
template<class T2>T2 min(T2 a,T2 b)

用非模板函数重载同名的函数模板后,原函数模板仍有效

类模板

类模板的定义

类模板用来实现通用的数据类型,如vector就是C++标准模板库中定义的一个类模板

template<class Type>
class Stack
{
    public:
        Stack(int cap);
        ~Stack() {delete []ele;}
        void push(Type e);
        Type pop();
        bool empty() { return top==bottom; }
        bool full() { return top==size-1; }
    private:
        Type* ele;
        int top;
        int size;
        const static int bottom=-1;
};

类模板在实例化时必须显式指定模板实参。声明一个类模板实例的指针和引用不会引起类模板的实例化。

类模板的成员函数

类模板成员函数在类外定义如下:

template<class Type>Stack<Type>::Stack(int cap)
{
    assert(cap>0);
    size=cap;
    ele=new Type[size];
    top=bottom;
}
template<class Type>void Stack<Type>::push(Type e)
{ ele[++top]=e; }
template<class Type>void Stack<Type>::pop()
{ return ele[top--]; }

类模板成员函数本身也是一个模板,模板实例化时成员函数不自动实例化,只有被调用时才实例化

类模板的静态数据成员

类模板中可以声明静态数据成员,静态数据成员的定义必须出现在类模板的定义之外。类模板的每个实例都有自己的一组静态数据成员。每个静态数据成员实例都与一个类模板实例相对应。因此, 一个静态数据成员的实例总是通过一个特定的类模板实例被引用。

类模板的友元

有三种友元声明可以出现在类模板中, 具体如下。
(1) 非模板友元类或友元函数。不使用模板参数的友元类或友元函数是类模板的所有实例的友元。
(2) 绑定的友元类模板或函数模板。使用模板类的模板参数的友元类和友元函数与实例化后的模板类实例之间是一一对应的关系。如下面的代码中Queue就是QueueItem的友元类模板,通过模板类型参数绑定

#include<cassert>
template<class T>class QueueItem
{
    public:
        QueueItem(const T& data):item(data),next(0) {}
        friend class Queue<T>;
    private:
        T item;
        QueueItem* next;
};
template<class Type>class Queue
{
    public:
        Queue():front(0),back(0) {}
        ~Queue();
        Type remove();
        void add(const Type&);
        bool is_empty()const {return front==0;}
    private:
        QueueItem<Type>* front;
        QueueItem<Type>* back;
};
//类模板成员函数的类外定义
template<class Type>Queue<Type>::~Queue() {...}
template<class Type>void Queue<Type>::add(const Type& val) {...}
template<class Type>Type Queue<Type>::remove() {...}

用实际类型type实例化Queue类模板得到的Queue<type>类是相应的QueueItem<type>类的友元。
(3) 非绑定的友元类模板或函数模板。这时友元类模板或函数模板有自己的模板参数,因此和模板类实例之间形成一对多的映射关系, 即对任一个模板类的实例, 友元类模板或函数模板的所有实例都是它的友元。早期的一些编译器不支持这种友元声明
例如:

template<class T>
class MTC
{
    public:
        friend void foo();
        //foo()是所有MTC模板的实例的友元
        friend void goo<T>(vector<T>);
        //每个实例化的MTC类都有一个相应的goo()友元实例
        template<class Type>friend void hoo(MTC<Type>);
        //对MTC的每个实例,hoo()的每个实例都是它的友元
};

模板的编译

模板的代码组织

对于普通函数的调用, 编译器只需要看到函数的声明。当使用类类型的对象时, 类定义必须是可用的, 但不要求成员函数的定义必须出现。
因此,类定义和函数声明放在头文件中

模板和普通函数不同, 在调用函数模板时, 为了生成一个实例化版本, 编译器需要知道函数模板或类模板成员函数的定义,而不是只有声明。因此, 模板的头文件通常既包含模板的声明, 也包含模板的定义

模板被实例化的文件中要包含模板的完整定义。
对函数模板而言, 要在它被实例化的每个文件中都包含其定义;
对类模板而言, 类模板的定义和所有成员函数以及静态数据成员的定义必须完全被包含在每个要将它们实例化的文件中。

所以, 一般将函数模板的定义、类模板的完整定义(包括在类模板外定义的成员函数) 都放在头文件中, 在实例化模板的文件中包含相应的头文件。

注意, 为了避免模板定义被同一文件多次包含而引起编译错误, 在模板定义的头文件中应该使用包含守卫

/*函数模板的代码组织*/
//templatemin.h
#ifndef TEMPLATEMIN_H
#define TEMPLATEMIN_H

template<class Type>Type min(Type array[],int size)
{
    Type min_val=array[0];
    for(int i=1;i<size;i++)
    {
        if(array[i]<min_val)
        {
            min_val=array[i];
        }
    }
    return min_val;
}
#endif

/*类模板的代码组织*/
//myqueue.h
#ifndef QUEUE_H_
#define QUEUE_H_
#include<cassert>
template<class Type>class Queue;
template<class T>class QueueItem
{
    public:
        QueueItem(const T& data):item(data),next(0) {}
        friend class Queue<T>;
    private:
        T item;
        QueueItem* next;
};
template<class Type>class Queue
{
    public:
        Queue():front(0),back(0) {}
        ~Queue();
        Type remove();
        void add(const Type&);
        bool is_empty()const {return front==0;}
    private:
        QueueItem<Type>* front;
        QueueItem<Type>* back;
};
//成员函数的类外定义也在头文件中
template<class Type>Queue<Type>::~Queue()
{
    while(!is_empty())
        remove();
}
template<class Type>void Queue<Type>::add(const Type& val)
{
    QueueItem<Type>* pt=new QueueItem<Type>(val);
    if(is_empty())
        front=back=pt;
    else
    {
        back->next=pt;
        back=pt;
    }
}
template<class Type>Type Queue<Type>::remove()
{
    assert(!is_empty());
    Type val=front->item;
    QueueItem<Type>*pt=front;
    front=front->next;
    delete pt;
    return val;
}
#endif

显式实例化

当模板被使用时才会进行实例化这一特性意味着相同的实例可能出现在多个目标文件中。当两个或多个独立编译的源文件使用了相同的模板, 并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。

在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。C++11 可以通过显式实例化来避免这种开销。
显式实例化包括声明和定义, 形式如下:
extern template 声明; //实例化声明
template 声明; //实例化定义
声明是一个类或函数声明,其中所有模板参数都已经被替换为模板实参,如:

template class Stack<int>;
extern template class Stack<int>;
template int compare(const int&,const int&);

当编译器遇到extern template声明时, 不会在本文件生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个定义。对于一个给定的实例化版本, 可能有多个声明, 但必须只有一个定义。

extern 声明必须出现在任何使用此实例化版本的代码之前, 否则编译器在遇到使用一个模板时自动对其实例化。当编译器遇到一个实例化定义时, 为其生成代码。一个类模板的实例化定义会实例化
该模板的所有成员, 包括inline 成员函数。
当编译器遇到一个实例化定义时, 它不了解程序会用哪些成员承数, 因此, 编译器会实例化该类的所有成员。不像对类模板的普通实例化那样只有调用的成员函数才实例化。

C++11中的 “模板显式实例化定义、extern模板声明和使用” 就像是“ 全局变量的定义、extern 声明和使用” 方式的再次应用。

extern 模板定义应该是一种针对编译器的编译时间及空间的优化手段, 目的是消除因大量模板使用引起的模板实例化展开而产生的代码冗余。只有在项目比较大的情况下, 才建议进行这样的优化。

模板的更多特性

模板的默认实参

早期C++只允许为类模板提供默认实参,C++11允许为函数模板提供默认模板实参。

//使用标准库less函数作为默认模板实参对象,重写compare<T>函数模板
//compare默认实参:less<T>和默认函数实参F
template<typename T,typename F=less<T>>
int compare(const T& v1,const T& v2,F f=F())
{
    if(f(v1,v2)) return -1;
    if(f(v2,v1)) return 1;
    return 0;
}

模板特化

compare函数模板不适合比较C风格字符串(字符指针),可以为通用的compare定义一个模板特化版本。一个特化版本就是模板的一个独立定义, 在其中一个或多个模板参数被指定为特定的类型。
特化一个函数模板时, 必须为原模板中的每个模板参数都提供实参。例如:

//原模板
template<typename T>int compare(const T& v1,const T& v2);
//特化版本
template<> //空尖括号表明所有模板参数都会提供实参
int compare(const char* const& p1,const char* const& p2)
{
    return strcmp(p1,p2);
}

一个特化版本本质上是一个实例,不是重载,因此不影响函数匹配

可变参数模板

可变参数模板就是接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包: 模板参数包, 表示零个或多个模板参数; 函数参数包, 表示零个或多个函数参数。

模板参数包或承数参数包用省略号...表示。在模板参数表中,class...typename...指出接下来的参数表示零个或多个类型的列表; 一个类型名后面跟...表示零个或多个给定类型的非类型参数的列表。在函数参数表中, 如果一个参数的类型是模板参数包, 那么此参数也是一个函数参数包。例如:

//Args 是一个模板参数包, rest 是一个函数参数包
template<typename T,typename...Args>
void foo(const T& t,const Args&...rest);

当需要知道包中有多少元素时, 可以使用sizeof...运算符。sizeof...运算符返回一个常量表达式, 且不会对实参求值。例如:

template<typename...Args>void g(Args...args)
{
	cout<<sizeof...(Args)<<endl; //类型参数的数目
	cout<<sizeof...(args)<<endl; //函数参数的数目
}

标准库容器和算法

标准模板库:STL

容器和算法概览

容器是保存其他对象的对象。C++容器都定义为模板类,所有容器共享公共接口。

C++标准容器分为顺序容器(如vector和list)和关联容器(如set和map)
拟容器:如string类只能存储字符类型,bitset没有相应迭代器,因而不能用标准算法处理
迭代器是对指向容器和其他序列的指针的抽象,迭代器泛化了指针概念,可以像内置数组上的指针一样进行++ -- *(解引用)

算法通过迭代器作用于容器,所有算法都是函数模板,大部分标准算法都在头文件中声明,有些数值算法在中声明
在这里插入图片描述
在这里插入图片描述

顺序容器

在这里插入图片描述
使用这些容器时,必须包含相应的头文件,头文件名与容器类型名相同。
这些容器都提供高效、灵活的内存管理。除了固定大小的array之外, 都可以添加和删除元素、扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率甚至是否支持特定操作有很大的影响。
新标准的容器比旧版本效率更高,性能几乎与最精心优化过的同类数据结构一样好,甚至更好。现代C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。

选择使用容器的种类基本原则

  • 通常,vector 是最好的选择, 除非有很好的理由, 否则应使用vector
  • 如果程序有很多小元素,且空间额外开销是重要考虑,不要选择list 或forward_list
  • 如果要求随机访问元素,使用vector或deque
  • 如果要求在容器中间插入或删除元素,使用list 或forward_list
  • 如果需要在头尾位置插入或删除元素, 但不会在中间位置插入或删除,使用deque

如果不确定使用哪种容器,那么在程序中只使用vector和list公共的操作:使用迭代器、不使用下标操作、避免随机访问。这样,在必要时使用哪个都很方便。

顺序容器和关联容器的不同之处在于两者组织元素的方式。这些差异直接关系到元素如何存储、访问、添加以及删除。

通用操作

在这里插入图片描述

//适用于所有容器的定义方式
vector<string>svec; 
vector<string>svec1(svec);
//下面两种仅适用于顺序容器
vector<string>svec2(10,"hi"); //大小为10的容器,"hi"初始化每个元素
vector<string>svec3(10); //10个空串
//定义array要指定类型和大小
array<int,3>a1; //3个int,默认初始化:全局0,局部随机
array<int,4>a2={1,2}; //1 2 0 0
array<string,3>a3={"a","bb","ccc"};

特有操作

在这里插入图片描述
特殊的forward_list操作

//从flst删除奇数元素
forward_list<int>flst={0,1,2,3,4,5,6,7,8,9};
auto prev=flst.before_begin(); //flst第一个元素之前
auto curr=flst.begin(); //flst第一个元素,curr是prev后继
while(curr!=flst.end())
{
    if(*curr%2)
        curr=flst.erase_after(prev); //删除prev后继,移动curr
    else
    {
        prev=cuur; //移动两个迭代器
        curr++;
    }
}

顺序容器适配器

标准库定义了三种顺序容器适配器: stack 、queue 和pnority_queue 。容器适配器接受一种已有的容器类型, 使其行为看起来像一种不同的类型。
在这里插入图片描述

//use stack
stack<int>stk;
for(int i=0;i<10;i++)
    stk.push(i);
while(!stk.empty())
{
    int val=stk.top();
    stk.pop();
}

string类的额外操作

在这里插入图片描述

//将s1中所有字串s2替换为s3
#include<iostream>
#include<string>
using namespace std;
void string_replace(string& s1,const string& s2,const string& s3)
{
    string::size_type pos=0;
    string::size_type a=s2.size();
    string::size_type b=s3.size();
    while((pos=s1.find(s2,pos))!=string::npos)
}

迭代器

迭代器范围 [begin,end) 遍历从begin开始,end之前结束

标准库使用左闭右合区间是因为它有三种性质:
1.如果begin和end相等,则范围为空。
2.如果begin与end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
3.可以对begin递增若干次,使得begin==end

end()不指向容器中任何元素,指向最后一个元素的下一个位置

//利用迭代器操作list
#include<iostream>
#include<list>
#include<string>
using namespace std;
class Demo
{
    string str;
    public:
        Demo(string _str="hi") { str=_str; }
        string getstring() { return str; }
};
int main()
{
    list<int> ilist(2,2);
    list<int>::iterator iter=ilist.begin();
    while(iter!=ilist.end())
    {
        cout<<*iter<<endl;
        iter++;
    }
    list<Demo>delist(2,Demo());
    for(list<Demo>::iterator de_iter=delist.begin();de_iter!=delist.end();de_iter++)
        cout<<de_iter->getstring()<<endl;
    list<int>empty;
    list<int>::iterator first=empty.begin(),last=empty.end();
    if(first==last)
        cout<<"Countainer is empty!"<<endl;
    cin.get();
}

//用迭代器插入元素
#include<iostream>
#include<list>
using namespace std;
int main()
{
    list<int>srclist;
    srclist.push_front(2);
    srclist.push_front(1);
    list<int>ilist;
    ilist.push_back(5);
    ilist.insert(ilist.begin(),4);
    ilist.insert(ilist.begin(),1,3);
    list<int>::iterator srcfirst=srclist.begin(),
                        srclast=srclist.end();
    ilist.insert(ilist.begin(),srcfirst,srclast);
    for(list<int>::iterator iter=ilist.begin();iter!=ilist.end();iter++)
        cout<<*iter<<"\t";
    cin.get();
}
/*1    2    3    4    5*/

关联容器

种类和操作

在这里插入图片描述
在这里插入图片描述
pair在<utility>中定义
在这里插入图片描述

map

map也被称为字典或关联数组

//定义
map<string,int>dictionary;

//初始化
map<string,int>dictionary={{"a",1},{"an",2},{"and",3}};

//下标
dictionary["an"]=1; //如果没有an键,插入(an,1)

//插入新元素
dictionary.insert(map<string,int>::value_type("aq",2));
dictionary.insert({s,1});
dictionary.insert(make_pair(s,1));
dictionary.insert(pair<string,int>(s,1));

//迭代器
map<string,int>::iterator map_it=dictionary.begin();
//简单写法:auto map_it=dictionary.begin();
cout<<map_it->first;
dictionary.erase("an");
dictionary.erase(map_it);

/****统计并输出words.txt中每个单词的出现次数****/
#include<iostream>
#include<fstream>
#include<string>
#include<map>
using namespace std;
int main()
{
    map<string,int>dic;
    string word;
    ifstream file("words.txt");
    while(file>>word)
    {
        ++dic[word];
    }
    for(const auto&w:dic)
        cout<<w.first<<"\t"<<w.second<<endl;
    file.close();
}

set

//统计输出words.txt中每个单词出现次数,忽略常见词
#include<iostream>
#include<fstream>
#include<string>
#include<set>
#include<map>
using namespace std;
int main()
{
    map<string,int>dic;
    set<string>exclude={"The","the","But","but","And","and","Or","or","An","an","A","a"};
    string word;
    ifstream file("words.txt");
    while(file>>word)
        if(exclude.find(word)==exclude.end())
            ++dic[word];
    for(const auto&w:dic)
        cout<<w.first<<"\t"<<w.second<<endl;
    file.close();
}

泛型算法

概述

大多数泛型算法在<algorithm>中定义,数值泛型算法在<numeric>中定义

泛型算法通常不直接操作容器, 而是遍历由两个迭代器指定的一个元素范围来进行操作, 迭代器使算法可以独立于特定的容器。
虽然迭代器的使用令算法不依赖于容器类型, 但算法依赖于元素类型的操作。大多数算法都使用了一个或多个元素类型上的操作, 例如, 查找算法find 要用比较==运算符,排序算法sort 要用“ < ” 运算符。
不过, 大多数算法提供了一种方法, 允许用自定义的操作来代替默认的运算符。通过向算法传递谓词或lambda 、函数对象等可调用对象, 能够定制算法的操作。

例如:

//查找第一个正数
#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
using namespace std;
struct isPos
{
    bool operator() (int n){ return n>0; }
};
int main()
{
    vector<int>vi={-1,-2,4,9,-3,5};
    //算法find_if(b,e,unaryPred)返回一个迭代器,
    //指向[b,e)迭代器范围中第一个令unaryPred为true的元素,未找到则返回e
    auto firstPos=find_if(begin(vi),end(vi),isPos());
    if(firstPos!=vi.end())
        cout<<*firstPos<<endl;
}

通常不对关联容器使用泛型算法。关联容器的键是const,这意味着不能将关联容器传递给修改或重排容器元素的算法。关联容器可用于只读取元素的算法, 但是很多这类算法都要搜索序列。关联容器中的元素不能通过键进行快速查找,所以,使用泛型算法如find查找元素远不如关联容器专用的find 成员快速。在实际编程中, 如果真要对关联容器使用算法,要么把它作为一个源序列,要么把它作为一个目标位置。例如,可以用泛型算法copy将元素从一个关联容器复制到另一个序列。

查找

可以在任何容器中使用find(b,e,search_value)函数来查找位于迭代器b 和e 间的值search value 。如果查找成功, 就返回指向该元素的迭代器, 否则返回第二个实参(即传递给e的实参,也是一个迭代器)。find还可以用来在内置数组中查找指定元素。

list<int>ilist;
list<int>::iterator result_it=find(ilist.begin(),ilist.end(),78);
if(result_it==ilist.end())
    cout<<"fail"<<endl;
else
    cout<<"succcess"<<endl;

int ia[6]={100,34,78,3};
int *pr=find(ia,ia+6,78);
if(pr==ia+6)
    cout<<"fail"<<endl;
else
    cout<<"success"<<endl;

排序

//对words.txt中单词按字典序排序并输出到output.txt
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
    ifstream in("words.txt");
    vector<string>words;
    string wd;
    while(in>>wd)
        words.push_back(wd);
    sort(words.begin(),words.end());
    ofstream out("output.txt");
    for(const auto&w:words)
        out<<w<<endl;
    in.close();
    out.close();
}

异常处理

异常处理机制

抛出异常

标准库异常类runtime_error, 在头文件 中定义。初始化runtime_error类型的异常对象时要提供一个字符串作参数,其中可以保存关于异常的辅助信息

void Circle::setRadius(double r)
{
    if(r>0)
        radius=r;
    else
    {
        throw runtime_error("Radius must be postive.");
    }
    
}

当执行一个throw 时,跟在throw后面的语句将不再执行。因此, throw 的用法有点类似于return 语句: 通常作为条件语句的一部分,或者作为某个函数的最后一条语句。抛出异常将终止当前的函数, 并把控制权转移给能处理该异常的代码。

try语句块

可能抛出异常的语句被包围在try语句块之中。try 块的语法形式为:

try {语句序列}
catch ( 异常声明1{ 异常处理代码1 }
catch ( 异常声明2{ 异常处理代码2 }
//try块儿代码组织方法一:try块儿只包围可能抛出异常的调用语句
int main()
{
    Circle c;
    double r;
    cin>>r;
    try
    {
        c.setRadius(r);
    }
    catch(runtime_error e)
    {
        cout<<e.what()<<endl;
        return -1;
    }
    return 0;
}
//try块儿代码组织方法二:try块儿包围正常处理的代码序列
int main()
{
    try
    {
        Circle c;
        double r;
        cin>>r;
        c.setRadius(r);
    }
    catch(runtime_error e)
    {
        cout<<e.what()<<endl;
        return -1;
    }
    return 0; //出现异常返回-1
}
//try块儿代码组织方法三:函数try块,try块包围整个函数体
int main()
try
{
    Circle c;
    double r;
    cin>>r;
    c.setRadius(r);
    return 0;
} //try块和main()函数的结尾
catch(runtime_error e) //main()函数的作用域之外
{
    cout<<e.what()<<endl;
}

程序执行的任何时候都可能发生异常,特别是异常可能发生在处理构造函数初始值的过程中。构造函数在进入函数体之前首先执行初始化列表, 在初始化列表抛出异常时, 构造函数体内的叮语句块还未生效, 所以构造函数体内的catch 语句无法处理构造函数初始化列表抛出的异常。

函数try块是处理构造函数初始化列表异常的唯一方法。关键字位于构造函数参数表之后,初始化列表的冒号:之前。与这个try块关联的catch 既能处理构造函数体抛出的异常, 也能处理成员初始化列表中抛出的异常。

例如:

#include<iostream>
#include<stdexcept>
using namespace std;
const double PI=3.14159;
class Circle
{
    public:
        Circle(double r=1.0);
        void setRadius(double r);
        double getRadius()const;
        double area()const;
        double peremeter()const;
    private:
        double radius=1.0;
};
Circle::Circle(double r)
{
    if(r>0)
        radius=r;
    else
    {
        throw runtime_error("Radius must be positive.");
    }
}
void Circle::setRadius(double r)
{
    if(r>0)
        radius=r;
    else
    {
        throw runtime_error("Radius must be positive.");
    }
}
double Circle::getRadius()const {return radius;}
double Circle::area()const {return PI*radius*radius;}
double Circle::peremeter()const {return 2*PI*radius;}

//组合Circle类对象的圆柱类Cylinder
class Cylinder
{
    Circle bottom;
    double height;
    public:
        Cylinder(double r,double h);
};
//构造函数try块
Cylinder::Cylinder(double r,double h)
try:bottom(r),height(h) { }
catch(runtime_error e) { cout<<e.what()<<endl; }

异常处理流程

在这里插入图片描述

异常对象

异常对象是一种特殊的对象,编译器用throw 抛出的表达式对异常对象进行拷贝初始化。因此,throw 语句中的表达式如果是类类型,该类必须有可访问的析构数和可访问的拷贝或移动构造函数。

异常对象位于编译器管理的空间中,编译器确保无论最终调用了哪个catch子句都能访问该空间。当异常处理完毕后, 异常对象被销毁。异常对象总是在抛出点被创建,即使throw语句不像上面程序中的显式构造函数调用,也会创建一个异常对象。

enum EHstate {NoError,zeroOp,negativeOp,severeError};
enum EHstate st=NoError;
int mathFunc(int iv)
{
    if(iv==0)
    {
        st=zeroOp;
        throw st; //创建异常对象,并用st初始化这个对象
    }
}

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

Derived d; //派生类
Base* pb=&d; //基类指针指向派生类对象
throw *pb; //解引用基类指针pb,抛出的只有基类部分,静态类型

如果抛出指针类型的异常,要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在。因为当一个异常被抛出时,沿着调用链的块将依次退出,直至找到与异常匹配的处理代码。如果退出了某个块,则同时释放块中局部对象的内存。因此,不能抛出指向局部对象的指针。

捕获异常

catch子句的异常声明看起来像是只包含单个形参的函数形参列表。异常声明可以是一个类型声明或一个对象声明。如果要获得throw 表达式的值,或者要操纵throw 创建的异常对象,就应该声明一个对象。这样,在抛出异常时可以将信息存储在异常对象中, catch子句中的语句就可以通过声明的对象来使用这些信息。

声明的类型决定了处理代码能捕捉的异常类型。这个类型可以是左值引用,但不能是右值引用。当进入catch子句时, 通过异常对象初始化异常声明中的参数。如果catch子句的异常声明是对象,则用抛出的异常对象初始化异常声明的对象。类似于函数参数按值传递,该参数是异常对象的一个副本, 在catch 语句块内改变该参数实际上改变的是局部副本,而不是异常对象本身。

void calculate(int op)
{
    try
    {
        mathFunc(op);
    }
    catch(EHstate eObj) //eObj是被抛出的异常对象的副本
    {
        ...
    }   
}

catch子句中的异常声明也可以声明引用类型。如果参数是引用类型,此时改变参数也就是改变异常对象。这样,catch子句中就可以直接引用throw 抛出的异常对象,而不是创建一个局部副本。

为了防止大对象的复制, 类类型的异常应该声明为引用。

//异常对象和捕获异常
#include<iostream>
using namespace std;
class Excp //异常类
{
    public:
    //异常类中应该有适当的构造函数和析构函数
        Excp(){cout<<"Excp():default constructor"<<endl;}
        Excp(const Excp&){cout<<"Excp(Excp&):copy constructor"<<endl;}
        ~Excp(){cout<<"~Excp():destructor"<<endl;}
};
void f()
{
    throw Excp(); //临时对象语法,显式创建异常对象并抛出
}
void g()
{
    Excp e;
    throw e; //抛出异常对象
}
int main()
{
    //捕获并处理f中抛出的异常
    try
    {
        cout<<"try f()..."<<endl;
        f();
    }
    catch(Excp& eobj) //引用声明
    {
        cout<<"catch reference in f()"<<endl;
    }
    try
    {
        cout<<"try g()..."<<endl;
        g();
    }
    catch(Excp eobj) //对象声明
    {
        cout<<"catch object in g()"<<endl;
    }
    cin.get();
}
/*
try f()...
Excp():default constructor
catch reference in f()
~Excp():destructor
try g()...
Excp():default constructor
Excp(Excp&):copy constructor
~Excp():destructor
Excp(Excp&):copy constructor
catch object in g()
~Excp():destructor
~Excp():destructor
*/

catch的参数与函数参数类似的有一个特性:如果catch的参数是基类类型, 可以使用派生类类型的异常对象对其进行初始化。此时,如果catch的参数是非引用类型,则异常对象会被切片; 如果参数是基类的引用类型, 则被绑定到异常对象上。因此,如果要正确调用类类型异常对象的虚函数, 应该将异常声明为引用。
例如:

#include<iostream>
using namespace std;
class Excp //自定义异常类
{
    public:
        virtual void print()
        {
            cerr<<"An exception has occured."<<endl;
        }
};
class NegativeArg:public Excp //派生类描述具体异常
{
    void print()
    {
        cerr<<"Arguement is negative."<<endl;
    }
};
class ZeroOperand:public Excp
{
    void print()
    {
        cerr<<"Operand is zero."<<endl;
    }
};

int main()
{
    try
    {
        //...
    }
    catch(Excp& eobj) //声明为引用,多态调用虚函数
    {
        eobj.print();
        return -1;
    }
    return 0;
    
}

重新抛出异常

当单个catch子句不能完全处理异常时,可重新抛出异常,传递给上一级的另一个catch子句继续处理

只能在catch子句的复合语句中重新抛出异常对象。如果异常声明是对象的形式,则重新抛出的仍然是原来的异常对象,catch 子句中的修改不能传递给上一级。如果想将修改后的异常对象抛出,异常声明必须声明为引用

void calculate(int op)
{
    try
    {
        mathFunc(op);
    }
    catch(EHstate eobj)
    {
        eobj=severError;
        throw; //抛出仍为原异常对象
    }
}
catch(EHstate& eobj)
{
    eobj=severError;
    throw;
}

捕获所有异常

如果一个函数不能处理某些异常,这个函数就会带着异常退出。但是如果函数在退出之前需要执行一些动作, 如释放资源或关闭文件,这些动作可能因为抛出异常被跳过。处理这种问题的方法不是为每个异常都写一个catch子句,而是使用捕获所有异常的catch_all语法,形式为:
catch(...) {异常处理}
任何类型的异常都会进入这个catch子句。在catch_all 中,执行退出前的处理动作之后,经常会使用throw重新抛出原来的异常。

catch_all子句可以单独使用,也可以与其他catch子句配合使用。但要注意,catch 子句被检查的顺序与它们在try块后的排列顺序相应,一旦找到了匹配, 后续的catch 子句就不再被检查。所以,如果catch_all 与其他catch 子句一起使用时,应该放在最后。同样的原因,处理基类异常对象的catch子句应该放在处理派生类异常对象的catch子句之后。

程序终止

一个程序可能以多种方式终止:
1.从main() 返回
2.调用exit()
3.调用abort()
4.抛出一个未被捕捉的异常。

如果一个程序利用标准库函数exit()终止,所有已经构造的静态对象的析构函数都将被调用, 但调用exit()的函数及该函数的调用者之中的局部对象的析构函数都不会被执行。如果程序使用标准库函数abort()终止,那么静态对象的析构函数不会被调用。

调用exit()将终止程序,不会给调用者留下处理问题的机会。exit()的参数被作为程序的返回值返回给系统, 0通常用来指明程序成功结束。

抛出一个异常并捕捉它则能保证局部变量被正确地销毁。如果抛出的异常未被捕捉,那么将调用terminate(),它的默认行为是调用abort()终止程序。

函数exit() 和abort() 都在< cstdlib >中声明。

noexcept说明

如果能预先知道某个函数不会抛出异常,有助于简化调用该函数的代码。同时,如果编译器确认函数不会抛出异常,就能执行某些特殊的优化操作,而这些优化操作可能不适合出错的代码。
C++ 11中用noexcept说明指定某个函数不会抛出异常。

noexcept 有两种语法形式:
void func() noexcept;

另一种可以接受一个常量表达式作为参数, 例如:
void func() noexcept(常量表达式);
常量表达式的结果会被转换成一个bool类型的值。如果值为true,则函数不会抛出异常;反之,则有可能抛出异常。不带常量表达式的noexcept相当于声明了noexcept(true)

noexcept要同时出现在函数的定义和所有声明中。noexcept 的位置在函数的尾置返回类型之前; 对成员函数,noexcept跟在const和引用限定符之后, 在final、override 或纯虚数的=0之前。

noexcept的实参常常与noexcept运算符混合使用,noexcept运算符是一个一元运算符,返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。
noexcept不会对操作数求值。noexcept 运算符的一般形式为:
noexcept(e)

当e调用的所有数都说明了noexcept且e本身不含有throw 语句时,表达式为true,否则返回false

标准异常

在这里插入图片描述
C++标准库的异常类分别定义在4个头文件中:

  • <exception>头文件,定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外的信息。
  • <stdexcept>头文件,定义了几种常用的异常类,如下表所示。
  • <new>头文件,定义了bad_alloc异常类型。
  • <type_info> 头文件,定义了bad_cast 异常类型。
    在这里插入图片描述

处理类类型的异常

编译器按照catch子句在try块后出现的顺序检查处理异常的catch子句。一旦编译器为一个异常找到了一个catch子句,就不会再检查后续的catch子句。由于派生类的异常对象可能被基类的catch 子句捕获,所以要注意catch 子句的排列顺序。

在一个块之后, catch 子句的排列从特殊到一般:

try{...}
catch(派生类类型) {...}
catch(基类类型) {...}
catch(...) {...}

应该将派生类类型的异常处理放在内层,基类类型的异常处理放在外层,最外层使用catch_all,捕获遗漏的异常。

/************END************/
/************Edited_By_Alveus 2020/5/2************/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alveus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值