accelerated c++第十三章笔记

第十三章 使用继承与动态绑定

class Core{
public:
    Core();
    Core(std::istream&);
    std::string name() const;
    std::istream& read(std::istream&);
    double grade() const;
private:
    std::istream& read_common(std::istream&);
    std::string n;
    double midterm,final;
    std::vector<double> homework;
};
class Grad:public Core{
public:
    Grad();
    Grad(std::istream&);
    double grade() const;
    std::istream& read(std::istream&);
private:
    double thesis;
};

派生类可以增加新的成员数据和新的构造函数,可以重定义基类中的成员,但是在派生类中不能删除任何的基类成员。

Grad第一行的public表示Core类是Grad接口的一部分,而不属于它的实现部分。

class Core{
public:
    Core();
    Core(std::istream&);
    std::string name() const;
    std::istream& read(std::istream&);
    double grade() const;
protected:
    std::istream& read_common(std::istream&);
    double midterm,final;
    std::vector<double> homework;
private:
    std::string n;
};

protected关键字给Grad这样的派生类赋予了访问基类中保护成员的权力,同时又保持使这些成员不能被类的其他使用者访问。

string Core::name() const {return n;}
double Core::grade() const
{
    return::grade(midterm,final,homework);
}
istream& Core::read_common(istream& in)
{
    in>>n>>midterm>>final;
    return in;
}
istream& Core::read(istream& in)
{
    read_common(in);
    read_hw(in,homework);
    return in;
}
istream& Grad::read(istream& in)
{
    read_common(in);
    in>>thesis;
    read_hw(in,homework);
    return in;
}
istream& Grad::read(istream& in)
{
    Core::read_common(in);
    in>>thesis;
    read_hw(in,Core::homework);
    return in;
}
double Grad::grade() const
{
    return min(Core::grade(),thesis);
}

和max一样,min函数的两个参数也必须是同一类型的数据。

派生类型对象在构造的时候经过以下步骤:

  • 为整个对象分配内存空间(包括基类中与派生类中定义的数据)
  • 调用积累的构造函数以初始化对象中的基类部分数据。
  • 用构造初始化器对对象的派生类部分数据进行初始化。
  • 如果有的话,执行派生类构造函数的函数体。
class Core{
public:
    //Core类的默认构造函数
    Core():midterm(0),final(0){}
    //用一个istream类型变量来构造一个Core对象
    Core(std::istream& is){read(is);}
    //...
};
class Grad:public Core{
public:
    //两个构造函数都隐式的调用Core::Core()函数来初始化对象中的基类部分
    Grad():thesis(0){}
    Grad(std::istream& is){read(is);}
    //...
};
bool compare(const Core& c1,const Core& c2)
{
    return c1.name() < c2.name();
}

Grad g(cin);
Grad g2(cin);

Core c(cin);
Core c2(cin);

compare(g,g2);
compare(c,c2);
compare(g,c);

向函数传递一个对象本身作为参数,与传递对象的一个引用作为参数有着很大的差别。

bool compare_grades(const Core& c1,const Core& c2)
{
    return c1.grade() < c2.grade();
}

我们希望系统根据实际传递给函数的参数的类型来运行正确版本的grade函数,而参数的类型只有到运行的时候才是已知的。

为了支持这种运行时选择,C++提供了virtual function

class Core{
public:
    virtual double grade() const;       //增加虚函数
    //...
};

virtual关键字只能在类的定义里被使用。如果函数体在声明之外单独定义,我们不能在定义的时候重复virtual关键字。如果类中的一个函数时虚拟的,那么在派生类中它的虚拟特性也会被继承。

只有在以引用或者指针为参数调用虚函数的时候,它在运行时选择特性才会又意义。

bool compare_grades(Core c1,Core c2)
{
    return c1.grade() < c2.grade();
}

以Grad类型对象的名字调用这个函数,这个Grad类型对象将被删减的只剩下Core类部分,然后将这一部分的一个复件传递给compare_grades函数。因为参数仍然是Core类型的对象,所以这种对grade类型对象的函数调用是静态绑定的——它们在编译的时候就已经被确定了——调用Core::grade()函数。

动态绑定(dynamic binding)就是指在运行的时候才决定调用合适呢么函数,而不是在编译的情况下就决定下来,那种情况属于静态绑定。

Core c;
Grad g;
Core* p;
Core& r = g;

c.grade();      //对Core::grade()函数进行静态绑定
g.grade();      //对Core::grade()函数进行静态绑定
p->grade();     //根据p所指对象的类型进行动态绑定
r.grade();      //根据r所引用对象的类型进行动态绑定

关于虚函数还有一点值得注意:无论是否调用它们,在程序中都要对它们定义。如果在类中只对虚函数进行了声明而没有加定义的话,很多编译器都会生成一些奇怪的错误信息。

class Core{
public:
    Core():midterm(0),final(0){}
    Core(std::istream& is){read(is);}

    std::string name() const;
    
    virtual std::istream& read(std::istream&);
    virtual double grade() const;
protected:
    std::istream& read_common(std::istream&);
    double midterm,final;
    std::vector<double> homework;
private:
    std::string n;
};

class Grad:public Core{
public:
    Grad():thesis(0){}
    Grad(std::istream& is){read(is);}

    double grade() const;
    std::istream& read(std::istream&);
private:
    double thesis;
};

bool compare(const Core&,const Core&);

写两个程序分别读出完全由两种记录中的一种记录组成的文件。

int main()
{
    vector<Core> students;
    Core record;
    string::size_type maxlen = 0;
    //读入并储存数据
    while(record.read(cin)){
        maxlen = max(maxlen,record.name().size());
        students.push_back(record);
    }
    //对学生记录按字母排序
    sort(students.begin(),students.end(),compare);
    //输出学生姓名与成绩
    for(vector<Core>::size_type i = 0;i!=students.size();++i){
        cout << setw(maxlen+1) << students[i].name();
        try{
            double final_grade = students[i].grade();
            streamsize prec = out.precision();
            cout << setprecision(3) << fianl_grade << setprecision(prec) << endl;
        }catch(domain_error e){
            cout << e.what() << endl;
        }
    }
    return 0;
}

我们可以仅仅改变定义的类型,然后写出上面类似的处理Grad类记录的程序

int main()
{
    vector<Grad> students;
    Grad record;
    string::size_type maxlen = 0;
    //读入并储存数据
    while(record.read(cin)){
        maxlen = max(maxlen,record.name().size());
        students.push_back(record);
    }
    //对学生记录按字母排序
    sort(students.begin(),students.end(),compare);
    //输出学生姓名与成绩
    for(vector<Grad>::size_type i = 0;i!=students.size();++i){
        cout << setw(maxlen+1) << students[i].name();
        try{
            double final_grade = students[i].grade();
            streamsize prec = out.precision();
            cout << setprecision(3) << fianl_grade << setprecision(prec) << endl;
        }catch(domain_error e){
            cout << e.what() << endl;
        }
    }
    return 0;
}

我们可以定义vector<Core*>,然后把record也定义成一个指针。这样我们就在程序中实现了动态绑定,同时消除了在向量和局域临时变量的定义中存在的类型依赖性。不幸的是,在下面我们将看到,这种解决方案给用户带来了太多的麻烦。例如,下面这么一段想当然的改进代码根本不能正常运行:

int main()
{
    vector<Core*> students;

    Core* record;
    while(record->read(cin)){       //出错
        //...
    }
}

这段程序将产生可怕的错误,因为还没有让record指向任何一个实际的对象。

我们可以解决这个问题,但只有一种方法,那就是要求用户亲自管理好从文件中读出的数据所占用的内存。我们的用户还不得不检测程序正在读的记录的类型。我们假定每个记录中都包含一个标志,用这个标志可以区分记录中包含的是什么类型的数据:研究生的记录以G字母打头;而本科生的记录以U字母打头。

我们不能把一个重载函数命名为一个模板参数。如果一定要这么做的话,编译器将无法决定调用函数的哪一个版本。

bool compare_core_ptrs(const Core* cp1,const Core* cp2)
{
    return compare(*cp1,*cp2);
}

写完这个比较函数以后,我们就可以重写主函数了

int main()
{
    vector<Core*>students;      //用来保存指针而不是对象
    Core* record;               //临时变量也必须是一个指针
    char ch;
    string::size_type maxlen = 0;
    //读出并储存数据
    while(cin>>ch){
        if(ch=='U')
            record = new Core;
        else
            record = new Grad;
        record->read(cin);      //虚拟调用
        maxlen = max(maxlen,record->name().size());     //间接引用
        students.push_back(record);
    }
    //把以指针为参数的比较函数作为参数传递给排序函数
    sort(students.begin(),students.end(),compare_core_ptrs);
    //输出学生的姓名与成绩
    for(vector<Core*>::size_type i = 0;i!=students.size();++i){
        //students[i]是一个指针,用来解除对函数的调用
        cout << setw(maxlen+1) << students[i]->name();
        try{
            double final_grade = students[i]->grade();
            streamsize prec = cout.precision();
            cout << setprecision(3) << final_grade << setprecision(prec) << endl;
        }catch(domain_error e){
            cout << e.what() << endl;
        }
        delete students[i];     //释放在读文件的时候生成的临时变量
    }
    return 0;
}
class Core{
public:
    virtual ~Core(){}
    //其他部分同前
};

在所有通过一个指向基类的指针来删除一个派生类对象的时候,都要用到虚析构函数。

handle class

可以写一个新的类封装这个指向Core类型对象的指针,这样就把隐患在用户面前隐藏了起来。

class Student_info{
public:
    //构造函数与复制控制
    Student_info():cp(0){}
    Student_info(std::istream& is):cp(0){read(is);}
    Student_info(const Stduent_info&);
    Student_info& operator=(const Student_info&);
    ~Student_info(){delete cp;}

    //操作
    std::istream& read(std::istream&);

    std::string name() const{
        if(cp) return cp->name();
        else throw std::runtime_error("uninitialized Student");
    }
    static bool compare(const Student_info& s1,const Student_info& s2){
        return s1.name() < s2.name();
    }
private:
    Core *cp;
};

静态成员函数与一般的成员函数不同,静态成员函数不能对类的对象进行操作,和其他的成员函数不同的是,静态成员函数与类关联,而不是与一个特定的类型对象关联。因此,静态成员函数不能访问类型对象的非静态数据成员;因为没有与该函数关联的对象,所以它不能访问任何成员。

istream& Student_info::read(istream& is)
{
    delete cp;      //如果有的话,删除以前所指的对象

    char ch;
    is >> ch;       //得到记录的类型

    if(ch=='U'){
        cp = new Core(is);
    }else{
        cp = new Grad(is);
    }
    return is;
}

在C++语言中删除一个零指针是无害的。

class Core{
    firend class Stduent_info;
protected:
    virtual Core* clone() const {return new Core(*this);}
    //其他部分同前
};

class Grad{
protected:
    Grad* clone() const {return new Grad(*this);}
};

一般来说,在派生类中重定义基类的一个函数的时候,参数列表和返回列表都是一样的。但是,如果基类中的函数返回一个指向基类型对象的指针(或者引用),那么派生类中相应的函数将返回一个指向派生类型对象的指针(或引用)。

Student_info::Student_info(const Student_info& s):cp(0)
{
    if(s.cp) cp = s.cp->clone();
}
Student_info& Student_info::operator=(const Student_info& s)
{
    if(&s != this){
        delete cp;
        if(s.cp)
            cp = s.cp->clone();
        else
            cp = 0;
    }
    return *this;
}

如果cp为0,无论是复制构造函数还是赋值运算符函数都不必做任何事。因为对一个空句柄进行复制或者赋值在语法上都是合法的。

int main()
{
    vector<Student_info> students;
    Student_info record;
    string::size_type maxlen = 0;
    //读出并储存数据
    while(record.read(cin)){
        maxlen = max(maxlen,record.name().size());
        students.push_back(record);
    }
    //对学生记录按姓名字母排序
    Sort(students.begin(),students.end(),Stduent_info::compare);
    //输出学生姓名与成绩
    for(vector<Student_info>::size_type i = 0;i!=students.size();++i){
        cout << set<maxlen+1> << students[i].name();
        try{
            double final_grade = students[i].grade();
            streamsize prec = cout.precision();
            cout << setprecision(3) << final_grade << setprecision(prec) << endl;
        }catch(domain_error e){
            cout << e.what()<< endl;
        }
    }
    return 0;
}
void Core::regrade(double d){final= d;}
void Grad::regrade(double d,double d2){final = d1;thesis = d2;}

//r是Core类型对象的引用
r.regrade(100);     //能够正常运行,调用Core::regrade函数
r.regrade(100,100); //产生编译错误,因为Core::regrade函数只带一个参数

//r是Grad类型对象的引用
r.regrade(100);     //产生编译错误,因为Grad::regrade带有两个参数
r.regrade(100,100); //能够正常运行,调用Grad::regrade函数

如果我们想要在一个派生类对象中以一个基类型对象的名义调用这个版本的函数,就要对它进行显式的调用。

r.Core::regrade(100);   //能够正常运行,调用Core::regrade

如果要把regrade函数声明为虚拟函数,那么我们必须把基类与派生类中该函数的接口声明成一样的,我们可以像下面这样在Core版本的regrade函数中另外增加一个具有默认值的不被使用的参数:

virtual void Core::regrade(double d,double = 0){final = d;}

小结

  • 继承关系是可以嵌套的
  • 派生类的对象在调用构造函数的时候要为整个对象分配内存空间,它先对对象中的基类部分进行构造,然后构造派生类的部分。
  • 具体调用哪一个构造函数是在运行时决定的。由实际用来生成派生类型对象的参数来决定调用哪一个构造函数。
  • 动态绑定指的是一种在运行时决定具体调用哪一个函数的能力。
  • 动态绑定只有在通过指针或者引用来调用虚拟函数的时候才有意义。
  • 如果一个函数在基类中被声明为虚拟的,那么这一虚拟特性可以被派生类继承,在派生类中不需要重复这一声明。
  • 在该类中第一次被声明的虚拟函数,一定要在该类中对它进行定义。
  • 继承其最近的基类中该虚拟函数的定义。
  • 基类的函数返回一个指向基类对象的指针或引用,派生类的函数返回一个指向派生类型对象的指针或引用,如果参数列表不匹配,那么基类的这个同名函数与派生类的这个函数就没有任何关系。
  • 如果我们想用一个指向基类型对象的指针来删除一个实际上可能是派生类的对象,那么基类中就需要声明一个虚析构函数。
  • 一个静态成员函数中不能使用this关键字。这种函数只能访问静态成员数据。整个类的每个静态成员数据只有一个实例。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值