Snowming04's Blog
一颗红❤
Toggle navigation
Snowming04's Blog
主页
Cobalt Strike
Accelerated C++
区块链安全
友链
关于我
常用工具
代码积累
归档
标签
【Accelerated C++】课时13:使用继承与动态绑定
? C++ ?
2020-04-15 17:38:53
559
0
0
snowming
? C++ ?
本章重点: - 继承 - 动态绑定 Core 类: ``` class Core { public: //默认构造函数,按照各个属性的构造函数递归默认生成 Core(); //另一个构造函数 Core(std::istream&); //存取器函数,获取名字属性 std::string name() const; std::istream& read(std::istream&); //不改变类属性的值,所以是const。实质上等于const Core,计算成绩的函数 double grade() const; private: //一个新的私有类成员函数,用来读取学生记录中所有学生公共部分的数据 std::istream& read_common(std::istream&); std::string n; //其实是name,为了避免跟name()重名 double midterm, final; std::vector<double> homework; }; ``` Grad 类(满足获得研究生学位的一些额外的要求): ``` class Grad:public Core { public: Grad(); //重新定义,因为比起Core的构造函数多了thesis属性 Grad(std::istream&); //重新定义 double grade() const; //重新定义 std::istream& read(std::istream&); //重新定义 private: double thesis; } ``` 一个问题:Core 类中的 private 的属性 Grad 有没有继承? 答:继承!所以 Grad 类型对象有五个成员数据,其中四个是从 Core 类中继承而来,第五个成员数据则是 double 类型的 thesis 变量。 - 注意:在`派生类`中可以重定义`基类`中的函数,但是在派生类中不能删除任何基类成员。 ## 回顾保护类型 保护类型即使用 `protect` 关键字限定的成员数据。 使用场景:  使用 protect 关键词重写 Core 基类: ``` class Core { public: Core(); //构造函数1 Core(std::istream&); //构造函数2 std::string name() const; //存取器函数,跟成员数据n配合使用 double grade() const; //计算成绩的函数 std::istream& read(std::istream&); protect: std::istream& read_common(std::istream&); double midterm, final; std::vector<double> homework; private: std::string n; //不必设为protect,配合存取器读取即可 } ``` 访问权限:  |限定符|访问权限| |:----:|:-:| |public|所有使用者| |protect|派生类| |private|类自身和友员函数/类| ## 操作函数 学生成绩的数据结构: - 本科生:姓名、期中成绩、期末成绩、家庭作业成绩 - 研究生:姓名、期中成绩、期末成绩、论文成绩、家庭作业成绩 据此写出来 Core 类中的操作函数: ``` 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; } ``` Grad 类中的操作函数: ``` istream& Grad::read(istream& in) { read_common(in); in >> thesis; read_hw(in, homework); return in; } //重定义grade函数,用来计算论文成绩(thesis)的影响。该函数比较论文成绩与考试和家庭作业计算得到的成绩,返回低一点的那个成绩。 double Grad::grade() const { return min(Core::grade(), thesis); } ```   ## 继承与构造函数  ``` 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);} //... }; ``` >注:`:`和`{`之间的一系列是`构造函数初始化程序`。  问:`Grad():thesis(0){}`的函数体为什么是空的? 答:因为这个构造函数做的事情是: 1. 通过构造函数初始化程序给 `thesis` 初始化值; 2. 通过隐式的调用基类 Core 的默认构造函数(根据参数个数类型指定具体是 Core 类的哪个构造函数)来对 `midterm`、`final`、`homework`、`name` 成员数据进行初始化。可以看到,Core 中被调用的默认构造函数又通过构造函数初始化程序对 `midterm`、`final` 进行了初始化,而通过 `homework`、`name` 所属类的默认构造函数(vector 类、string 类)隐式的对这两个成员数据进行初始化。而 Grad 的构造函数并没有其他工作要做,所以它的函数体是空的。  ## 多态和虚拟函数  每个 Grad 都一定是一个 Core,但是不是每个 Core 都是 Grad(缺成员属性)。所以我们可以向要求 Core& 类型参数的函数传递 Grad 类的对象作为参数,但是不能向要求 Grad& 类型参数的函数传递 Core 类的对象作为参数。 ## 在不知道对象类型的情况下获得对象的值 适用场景:   解决办法: 通过 `virtual` 关键字定义虚拟函数,这样可以支持运行时选择:   也就是说: 1. `virtual` 这个关键字只需要在类中的成员函数声明时候使用一次。 2. 如果此虚拟函数的定义在类外,无需在定义时再次使用 `virtual` 关键字。 3. 基类中的成员函数使用了 `virtual` 关键字的情况下,派生类直接继承此函数的虚拟特性,所以派生类中此函数哪怕被重定义(也正适用于重定义的场景),也无需再重复 `virtual` 关键字。 ## 动态绑定  - `对象的引用`或者`指向对象的指针`,使用的函数在运行时确定(动态绑定),可能调用基类的函数,也可能基类的派生类的函数。 - 对象,使用的函数在编译时确定,根据函数的形参类型确定(静态绑定),哪怕是派生类,也仅仅传递给函数对象的基类部分的数据,删减派生类直至剩下基类部分。 **<u>一个出错的例子:</u>**  **<u>动态绑定和静态绑定</u>** - 动态绑定和静态绑定的概念和虚拟函数是分不开的。 - 动态绑定和静态绑定是针对调用`函数`的概念!用于决定调用的虚拟函数的具体版本。   实例:  **<u>多态性</u>**  ## 把 read 函数也声明为一个虚拟函数 ``` 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; protect: //可以被所有派生类的成员访问 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);} //注意:grade函数与read函数也被继承为虚拟函数!!! std::istream& read(std::istream&); //到时候传入cin这种参数 double grade() const; private: double thesis; }; //注意在compare函数中会调用grade函数,就涉及到虚拟函数的动态绑定特性:) //所以嘛,看清楚是Core类的&引用,直接用对象类型作实参不行!!!只能是引用或指针!!! bool compare(const Core&, const Core&); //一个全局的非成员函数 ``` ## 用继承来解决我们的问题 现在来写两个程序分别处理 Core 类记录和 Grad 类记录。 处理 Core 类记录: ``` int main() { vector<Core> students; //读取并处理文件中的 Core 记录 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); //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(); //Core::grade() streamsize prec = cout.precision(); cout << setprecision(3) << final_grade << setprecision(prec) << endl; } catch(domain_error e) { cout << e.what() << endl; } } return 0; } ``` 处理 Grad 类记录: ``` int main() { vector<Grad> students; //读取并处理文件中的 Core 记录 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); //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(); //Grad::grade() streamsize prec = cout.precision(); cout << setprecision(3) << final_grade << setprecision(prec) << endl; } catch(domain_error e) { cout << e.what() << endl; } } return 0; } ``` 怎样去合成一个版本的程序?使其既可以处理 Core 类的对象,又可以处理 Grad 类的对象。  ## 容纳(实际上)未知类型的容器 为了得到我们想要的动态绑定特性,我们通过一个指向 Core 的指针或引用来调用 read 函数和 grade 函数,这样,绑定的对象类型随着指针或引用的类型而变。所以解决四个类型依赖性问题的办法就是: 写一个控制指针而不是控制对象的程序,这样可以消除在向量和局域临时变量的定义中存在的类型依赖性。 我们需要: 1. 亲自管理好从文件中读出来的数据所占的内存 2. 检测程序正在读的记录的类型。我们假定每个记录中都包含一个标志,用这个标志可以区分记录中包含的是什么类型的数据:研究生的记录以`G`字母打头,而本科生的记录以`U`字母打头。 3. 对一个包含指针的容器进行排序  写完专用的比较函数,然后我们开始重写主函数: ``` int main() { //用来保存指针而不是对象 vector<Core*> students; //临时变量也必须是一个指针 Core* record; char ch; //每个记录中都包含的一个标志 string::size_type maxlen = 0; //读出并储存数据 while(cin>>ch) { if(ch == 'u'){ //为一个 Core 类型对象分配内存 record = new Core; } else{ //为一个 Grad 类型对象分配内存 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; } ```  ## 虚拟析构函数  ## 一个简单的句柄类 **<u>句柄类使用场景:</u>**  **<u>什么是句柄类:</u>** 句柄类是一个类,用来封装指针,此指针可以指向多种类(要在程序中混用的基类和派生类)。  ``` class Student_info { public: //构造函数与复制控制 Student_info():cp(0){} //初始化为零指针 Student_info(std::istream& is):cp(0){ read(is); } Student_info(const Student_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"); } double grade() const { if(cp) return cp -> grade(); 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; }; ``` 一个问题:在下面的代码中怎么决定具体是调用 Core 的 `name` 和 `grade` 函数还是 Grad 的 `name` 和 `grade` 函数呢? ``` std::string name() const { if(cp) return cp -> name(); else throw std::runtime_error("uninitialized Student"); } double grade() const { if(cp) return cp -> grade(); else throw std::runtime_error("uninitialized Student"); } ``` 答:因为 cp 是 `Core* cp`,`Core::grade` 函数是个虚拟函数。所以当我们通过 cp 指针调用 grade 函数的时候,系统根据 cp 实际所指向的对象类型来判断实际调用的是哪一个类的 grade 函数。例如在 cp 指向一个 Grad 类型对象的时候,在运行时将调用 Grad::grade 函数。 这里这个句柄类的主要思想是:定义一个 `Student_info` 类,该类的对象既可以表示一个 Core 类型对象,又可以表示一个 Grade 类型对象。从这个意义上来看,它就像一个指针一样。不过,Student_info 的用户不用担心为 Student_info 对应的对象进行内存分配,因为这个类本身就能够处理程序中的这些繁琐而又充满着错误隐患的工作。 初始化:  一个问题:为什么在这个 Student_info 类中,也定义了 name 和 grade 函数? 答:  **<u>静态成员函数</u>** 也就是说:静态成员函数不能访问类中的成员数据,对类中成员数据、函数的调用也要通过类的公共接口。在这里,通过此特性就实现了 compare 函数作为全局非成员函数的需求。另一方面,既然还是类的成员函数,就可以用类的限定词,通过 `Student_info::compare` 这样的限定词,传给 sort 函数作为第三个谓词参数时,编译器就会知道应该调用哪个函数。   ## 读取句柄 ``` class Student_info { public: Student_info(std::istream& is):cp(0){ read(is); } //操作 std::istream& read(std::istream&); ``` 可以看到,`Student_info` 类的第二个构造函数,通过 read 函数来进行初始化。read 函数需要为适当的类型分配内存空间、对对象进行初始化。 read 函数有三个任务: 1. 首先其必须释放该句柄指向对象(如果句柄指针不为0)占用的空间; 2. 其次,其必须判断将要读的对象是什么类型; 3. 最后,其还要为正确类型的对象分配合适大小的内存空间,然后从 read 函数参数提供的输入流中读取数据对对象进行初始化。 下面是 read 函数的实现: ``` istream& Student_info::read(istream& is) { delete cp; //如果有的话,删除以前所指的对象 char ch; is>>ch; //得到记录的种类 if(ch == 'U') { //在对象被初始化后,把指向对象的指针保存在cp中 //new 函数返回一个指针 //用is的内容存入新生成的对象中 cp = new Core(is); }else { cp = new Grad(id); } return is; } ```  ## 复制句柄对象 定义一个复制构造函数。 问题场景:  解决办法:  然后写出复制构造函数和赋值运算符函数: ``` 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; } ```  ## 使用句柄类 ``` 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(),Student_info::compare); //输出学生姓名与成绩 for(vector<Student_info>::size_type i=0; i!=students.size();++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; } } return 0; } ``` ## 继承与容器  ## 需要哪一个函数?   ## 本章小结 **<u>继承</u>**   **<u>动态绑定</u>**  **<u>覆盖</u>**  **<u>虚拟析构函数</u>**  **<u>构造函数和虚拟函数</u>**  **<u>静态成员</u>** 
上一篇:
【Accelerated C++】课时14:近乎自动地管理内存
下一篇:
【Accelerated C++】课时12:使类对象像一个数值一样工作
0
赞
559 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
提交评论
立即登录
, 发表评论.
没有帐号?
立即注册
0
条评论
More...
文档导航
没有帐号? 立即注册