基类和派生类的定义以及虚函数

基类Quote的定义

classs Quote {
public:
Quote() = default;
Quote(cosnt std::string& book, double sales_price) : bookNo(book), price(sales_price) {} std::string isbn() const { return bookNo; } virtual double net_price(size_t n) const { return n * price; }
virtual ~Quote() = default; private:
std::string bookNo;
protected:
double price = 0.0;
};

PS: 基类通常都应该定义一个虚析构函数,尽管这个析构函数不执行任何实际的操作
派生类可以直接继承基类的成员。基类应该讲它的成员函数分为两种:一种是基类希望它的派生类重写覆盖的函数,类似net_price()成员函数;另一种是基类希望派生类直接继承而不要改变的成员函数,类似isbn() 。 对于基类的成员函数,派生类如果需要覆盖,则将其声明为虚函数(virtual)的,它只能出现在成员函数的声明的最前面。
基类可以将任意非static成员函数(除了构造函数之外)定义为virtual函数。
如果成员函数被定义为虚函数,则其解析过程发生在运行阶段,如果未被定义为虚函数,解析过程发生在编译阶段。

定义派生类

class Bulk_quote : public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, size_t, double); // override : 覆盖基类的net_price版本
double net_price(size_t) const override; private:
size_t min_qty = 0;
double discount = 0.0;
};

一个派生类不总是需要覆盖它的基类的虚函数,如果派生类没有覆盖掉基类的虚函数,则这个虚函数和普通函数没什么区别,派生类会直接继承基类中的版本;
如果派生类需要覆盖基类的虚函数版本来重写自己的版本,就需要用到C++11关键字override
override允许派生类显示的注明它使用某个成员函数覆盖了它继承的基类的虚函数,具体的做法是:
在虚函数的参数列表后面添加override关键字,如果这个虚函数是一个const成员函数,则override写在const之后,如果这个虚函数还是一个引用成员函数,则override跟在引用限定符&或&&之后

因为派生类对象中含有其基类对应的组成部分,因此一个派生类对象可以被当做一个基类来使用,也能把一个基类指针或引用绑定到派生类对象的基类部分上。

Quote item;
Bulk_quote bulk;
Quote* p = &item;
p = &bulk;
Quote& r = bulk;

这种转换称为“派生类向基类的转换”,在派生类中含有基类的组成部分,这是继承的关键所在。

派生类的构造函数

派生类并不直接构造基类的成员,而是使用基类的构造函数来构造,通过向基类的构造函数传递对应的参数来完成:

Bulk_quote(const std:string& book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {}

改构造函数构造顺序是:先进行基类Quote的构造,待Quote的函数体执行完毕后,按派生类Bulk_quote的成员的声明顺序来初始化它自己的成员,最后执行它的函数体。
首先进行基类Quote的构造,再按照声明的顺序依次初始化派生类的成员

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论派生类有多少个,这个静态成员只有这一个,同时, 静态成员遵循访问控制规则

void Derived::f(const Derived& derived)
{
Base::statmem();
Derived::statmem();
derived.statmem(); // 通过derived对象访问
statmem(); // 通过this对象访问
}

一个类如果被用做基类,则这个类必须是已经定义的类,而非仅仅声明。因此,一个类不能做自己的基类

防止一个类被继承的方法是,在这个类的类名后跟关键字final

class NoDerived final {};
class NoDerived1 : public NoDerived {}; // 错误, NoDerived是final的, 不能被继承

类型转换和继承

静态类型与动态类型
可以将一个基类的指针或引用绑定到派生类的对象上,但是实际上并不知道该指针或引用所绑定对象的真实类型,可能是基类对象,也可能是派生类对象。

PS: 和内置指针一样,智能指针也支持派生类向基类的转换,于是我们就可以将派生类对象存储在一个基类的智能指针内。

在使用继承关系的类型时,需要将静态类型和动态类型区分开。 静态类型在编译时是已知的,它是变量声明时的类型或表达式生成的类型;而动态类型则是变量表示内存中的对象的类型,直到运行时才可知。

如果表达式既不是指针也不是引用,那么它的动态类型永远和静态类型一致。

void print_total(Quote& q){
q.net_price(1);
}

q的静态类型为Quote。
print_total在调用net_price时, 动态类型依赖于q绑定的实参,如果传递的是Quote对象,则它的静态类型和动态类型相同,如果传递的是一个Bulk_quote对象,则它的动态类型和静态类型不一样。

派生类对象可以当做基类使用,但是不存在从基类向派生类的转换,即使一个基类指针或引用绑定在一个派生类对象上,也不能执行基类向派生类的转换。

Bulk_quote bulk;
Quote* itemp = &bulk;
Bulk_quote* bulkp = itemp; // 错误,不能将基类转换成派生类

编译器在编译时无法确定某个转换在运行时是否安全,因为编译器只能通过检查指针或引用的静态类型来推断该转换是否合法
如果我们已知从一个基类转换到派生类是安全的,我们可以使用static_cast来强制覆盖掉编译器的检查工作;如果基类中有虚函数,我们可以使用dynamic_cast请求一个类型转换,该转换的安全检查在运行时执行。

如果只是普通的基类对象和派生类对象,则不存在上述这些转换, 因为把一个派生类赋值给一个基类对象时,实际上执行的是基类的构造函数, 基类构造函数只负责将派生类中基类的部分赋值给自己,剩下的派生类的部分就被切掉了。

虚函数

对虚函数的调用在运行时被解析

void print_total(Quote& q){
q.net_price(1);
}

在print_total的调用中,q的静态类型为Quote,通过q来调用虚函数net_price(), 对虚函数的调用时,在运行时才会决定q的类型到底是Quote还是Bulk_quote。
当且仅当对通过指针或引用调用虚函数时,才会在运行时进行解析。也只有这种情况下,对象的动态类型才可能与其静态类型不同

派生类中的虚函数如果覆盖了基类的虚函数,则它的形参类型必须和它所覆盖的虚函数的形参类型完全一致。 同样,派生类中重写的虚函数的返回类型也必须和覆盖了的函数的返回类型一致,但是有个例外,如果该返回类型是类本身的指针或引用时,该规则无效。
如果D继承自B, 则基类B中的虚函数如果返回B*或B&, 则D中的该虚函数可以返回D*或D&, 前提是从D到B的类型转换时可访问的。

final和override

override说明符可以避免虚函数覆盖中的一些问题:
派生类如果定义了一个函数名和基类中虚函数名相同的函数但是形参列表不同,则派生类定义的这个函数则认为是一个独立的函数,而这时,派生类没有覆盖掉基类的虚函数,对于实际的编程习惯而言,这有可能会引发错误。
可以通过override来帮助我们发现一些错误,如果override标记了一个函数,但该函数并没有覆盖掉已存在的虚函数,此时编译器将报错:

    struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B {
void f1(int) const override; // 正确,覆盖了基类的f1
void f2(int) override; // 错误,基类没有f2(int)的函数
void f3() override; // 错误,f3()不是虚函数
void f4() override; // 错误,基类中没有f4()函数
};

还可以将某个函数指定为final,将基类中一个虚函数指定为final,则它的派生类中试图覆盖该虚函数的操作都将发生错误:

struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
virtual void func_final() final;
};
struct D2 : B {
void func_final() /*override*/; // 错误, func_final函数是final的
};

虚函数与默认实参

虚函数可以拥有默认实参,如果在某次的调用中使用了虚函数的默认实参,则该实参值由本次调用的静态类型决定。
也就是说,如果我们通过基类的指针或引用调用函数, 则使用基类中该虚函数给的默认实参, 即使动态类型时派生类也是如此。

回避虚函数的机制
如果派生类的虚函数需要调用它的基类的该虚函数的版本,需要基类的作用域符来强制要求,否则在运行时调用的是派生类的虚函数版本, 这样会导致无穷递归

class Base {
public:
string name() { return basename; }
virtual void print(ostream& os) { os << basename; }
private:
string basename;
};
class derived : public Base {
public:
void print(ostream& os) { print(os); os << " " << i; } // 错误,会导致无穷递归
private:
int i;
};

在derived类中,print函数的目的是调用基类Base的print函数, 但在实际运行print函数中, 派生类的print函数相当于this->print, this绑定的是derived类,这样就会导致无限递归,修改的方法是使用Base的作用域:

void print(ostream& os) { Base::print(os); os << " " << i; }

C++ Pirmer : 第十五章 : 面向对象程序设计之基类和派生的定义、类型转换与继承与虚函数的更多相关文章

  1. 【C++】《C++ Primer 》第十五章

    第十五章 面向对象程序设计 一.OOP:概述 面向对象程序设计(OOP)的核心思想是数据抽象.继承和动态绑定. 通过使用数据抽象,可以将类的接口和实现分离. 使用继承,可以定义相似的类型并对其相似关系 ...

  2. “全栈2019”Java第三十五章:面向对象

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  3. 20190901 On Java8 第十五章 异常

    第十五章 异常 要想创建健壮的系统,它的每一个构件都必须是健壮的. 异常概念 C++的异常处理机制基于 Ada,Java 中的异常处理则建立在 C++的基础之上(尽管看上去更像 Object Pasc ...

  4. 15第十五章UDF用户自定义函数(转载)

    15第十五章UDF用户自定义函数 待补上 原文链接 本文由豆约翰博客备份专家远程一键发布

  5. C++ Primer 5th 第15章 面向对象程序设计

    面向对象程序设计的核心思想是:数据抽象.继承和动态绑定. 数据抽象:将类的接口与实现分离: 继承:定义相似类型并对相似关系建模: 动态绑定:一定程度上上忽略相似类型间的区别,用同一方式使用它们. 1. ...

  6. 《Linux命令行与shell脚本编程大全》 第十五章 学习笔记

    第十五章:控制脚本 处理信号 重温Linux信号 信号 名称 描述 1 HUP 挂起 2 INT 中断 3 QUIT 结束运行 9 KILL 无条件终止 11 SEGV 段错误 15 TERM 尽可能 ...

  7. CSS3秘笈复习:十三章&十四章&十五章&十六章&十七章

    第十三章 1.在使用浮动时,源代码的顺序非常重要.浮动元素的HTML必须处在要包围它的元素的HTML之前. 2.清楚浮动: (1).在外围div的底部添加一个清除元素:clear属性可以防止元素包围浮 ...

  8. Gradle 1.12用户指南翻译——第四十五章. 应用程序插件

    本文由CSDN博客貌似掉线翻译,其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Githu ...

  9. Gradle 1.12 翻译——第十五章. 任务详述

    有关其他已翻译的章节请关注Github上的项目:https://github.com/msdx/gradledoc/tree/1.12,或访问:http://gradledoc.qiniudn.com ...

随机推荐

  1. Java设置环境变量的含义(JAVA_HOME,PATH,CLASSPATH)

    开发Java程序之前,需要在计算机行安装并配置Java开发环境.一种是直接安装Myeclipse,利用其自带的JDK编译运行:另一种是在我们的Windows或者Linux平台下安装JDK,配置环境变量 ...

  2. (九)errno和perror、标准IO

    3.1.6.文件读写的一些细节3.1.6.1.errno和perror(1)errno就是error number,意思就是错误号码.linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会 ...

  3. springMVC,mybatis配置事务

    首先应该对context:include-filter和context:exclude-filter有一个了解,include是包含这个,而不是只包含,下面会有例子看看: 这是我最开始配置的MVC的配 ...

  4. WPF:换肤

    看了一篇博客,觉得样式很好看,就自己动手做了一下,做个总结. 效果:    选择不同的图片背景就会改变: 直接上代码: 每个Theme对应一张图,除了图的名称不同之外,Theme?.xaml中的内容相 ...

  5. 转载-Web API 入门

    An Introduction to ASP.NET Web API 目前感觉最好的Web API入门教程 HTTP状态码 Web API 强势入门指南 Install Mongodb Getting ...

  6. 禁止Android 横屏竖屏切换

    在Android中要让一个程序的界面始终保持一个方向,不随手机方向转动而变化的办法: 只要在AndroidManifest.xml里面配置一下就可以了. 在AndroidManifest.xml的ac ...

  7. SharePoint 2013 开发——其他社交功能

    博客地址:http://blog.csdn.net/FoxDave 上一篇讲了如何获取用户配置文件的相关属性,它属于SharePoint 2013社交功能的一个小的构成部分.社交功能是SharePoi ...

  8. QT中QWS的含义 (转至 宋金时的专栏

    QT 编程和文档中的术语QWS的全称是Qt windows system,是QT自行开发的窗口系统,体系结构类似X Windows,是一个C/S结构,由QWS Server在物理设备上显示,由QWS ...

  9. iOS学习之GCD

    多线程编程 线程定义:一个CPU执行的CPU命令 列一条无分叉的路径就叫线程. 多线程:执行多个不同的CPU命令 有多条路径. 线程的使用:主线程(又叫作UI线程)主要任务是处理UI事件,显示和刷新U ...

  10. github常见操作和常见错误!错误提示:fatal: remote origin already exists.

    如果输入$ git remote add origin git@github.com:djqiang(github帐号名)/gitdemo(项目名).git 提示出错信息:fatal: remote ...