通常情况下,如果我们不适用某个函数,则无需为该函数提供定义。但我们必须为每个虚函数都提供定义而不管它是否被用到了,这因为连编译器也无法确定到底会适用哪个虚函数

对虚函数的调用可能在运行时才被解析:

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与之绑定到指针或引用上的对象的动态类型相匹配的那一个

注意:动态绑定只有当我们通过指针或引用调用虚函数时才会发生。当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来:

 Quote base;
Bulk_quote derived;
base = derived;//将derived的Quote部分拷贝给base
base.net_price();//调用Quote::net_prive

注意:对非虚函数的调用在编译时进行绑定。通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同

派生类中的虚函数:

基类中的虚函数在派生类中隐式地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配(包括 this 参数)。同样,派生类中虚函数的返回类型也必须与基类函数匹配。(当类的虚函数返回类型是类本身的指针或引用时可以返回派生类自己的引用,但要求从派生类到基类的类型转换是可访问的)

final 和 override 说明符:

派生类如果定义一个函数与基类中函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数没有覆盖掉基类中的版本。就实际的编程习惯而言,这种声明往往是错误的,因为我们可能原本希望派生类能覆盖掉基类的虚函数,但是一不小心把形参列表写错了

我们可以通过 override 关键字来发现这种错误。如果我们使用 override 标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错:

 #include <iostream>
using namespace std; struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
}; struct D : B{
void f1(int) const override;//正确,f1与基类中的f1匹配
// void f1(int) override;//错误,this参数应该是const的
// void f2(int) override;//错误,f2没有形如f2(int)的函数
// void f3() override;//错误,f3不是虚函数
// void f4() override;//错误,B没有名为f4的函数
}; int main(void){ }

final 关键字作用和 override 恰好相反,如果我们已经把函数定义成 final 了,则之后任何尝试覆盖该函数的操作都将引发错误:

 #include <iostream>
using namespace std; struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
}; struct D1 : B{
//从B继承f2(),f3(),覆盖f1(int)
void f1(int) const final;//不允许后继的其它类覆盖f1(int)
}; struct D2 : D1{
void f2();//正确,覆盖从间接基类B继承而来的f2
// void f1(int) const;//错误,D1已经将f1声明成final的
}; int main(void){ }

虚函数与默认实参:

和其它函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

回避虚函数的机制:

某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的:

强制调用基类中定义的函数版本而不管 baseP 的动态类型到底是什么

double undiscounted = baseP->Quote::net_price(42);//该调用在编译时完成解析

注意:通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制

通常当一个派生类的虚函数调用它覆盖的基类的虚函数版本时才需要回避虚函数的默认机制

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被析构为派生类版本自身的调用,从而导致无限递归:

 #include <iostream>
using namespace std; class A{
protected:
int x; public:
// A();
// ~A();
virtual ostream& f(ostream &os) const {
os << x;
return os;
}
}; class B : public A{
private:
int y; public:
// B();
// ~B();
ostream& f(ostream &os) const {
// return f(os) << " " << y;//错误,调用该函数时会无限递归
return A::f(os) << " " << y;
}
}; int main(void) {
B *b = new B;
A *a = b;
a->f(cout);//动态类型为B,调用ostream& B::f(ostream&) const
delete b; return ;
}

抽象基类:

纯虚函数:

我们通过在声明语句的分号之前加 =0 可以将一个虚函数声明成纯虚函数:

 class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string &book, double price, std::size_t qty, double disc) :
Quote(book, price), quantity(qty), discount(disc) {} double net_price(std::size_t) const = ;//纯虚函数 protected:
std::size_t quantity = ;
double discount = 0.0;
}; double Disc_quote::net_price(std::size_t sz) const {
//纯虚函数可以提供定义,但函数体必须定义在类的外部
}

注意:纯虚函数可以提供定义,但函数体必须定义在类的外部

含有纯虚函数的类是抽象基类。抽象基类负责定义接口,后继的其他类可以覆盖该接口。我们不能直接创建一个抽象基类对象,我们可以定义抽象基类的派生类对象,前提是这些派生类覆盖了抽象基类中的纯虚函数。

抽象基类的派生类必须覆盖抽象基类中的纯虚函数,否则派生类将仍然是抽象基类,不能创建对象

虽然抽象基类不能创建对象,但是我们仍然需要定义抽象基类的构造函数,因为抽象基类的派生类将会使用抽象基类的构造函数来构造派生类中的抽象基类部分数据成员:

 #include <iostream>
using namespace std; class Quote {
public:
Quote() = default;
Quote(const std::string &book, double sales_price) :
bookNo(book), price(sales_price) {} std::string isbn() const {
return bookNo;
} virtual double net_price(std::size_t n) const {//定义成虚函数,运行2时进行动态绑定
return n * price;
} virtual ~Quote() = default;//基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作 private:
std::string bookNo;//书籍的isbn编号 protected://可被派生类访问
double price = 0.0;//代表普通状态下不打折的价格
}; class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string &book, double price, std::size_t qty, double disc) :
Quote(book, price), quantity(qty), discount(disc) {} double net_price(std::size_t) const = ;//纯虚函数 protected:
std::size_t quantity = ;
double discount = 0.0;
}; double Disc_quote::net_price(std::size_t sz) const {
//纯虚函数可以提供定义,但函数体必须定义在类的外部
} class Bulk_quote : public Disc_quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double); double net_price(std::size_t) const override;//override显式注明该成员函数覆盖它继承的虚函数 // ~Bulk_quote(); private:
std::size_t min_qty = ;//适用折扣政策的最低购买量
double discount = 0.0;//以小数表示的折扣额 }; Bulk_quote::Bulk_quote(const std::string &book, double price, std::size_t qty, double disc) :
//调用抽象基类的构造函数来构造派生类中的抽象基类部分数据成员
Disc_quote(book, price, qty, disc) {} double Bulk_quote::net_price(size_t cnt) const {
if(cnt >= min_qty) return cnt * ( - discount) * price;
return cnt * price;
} int main(void){
// Disc_quote discount;//错误,不能创建抽象类基类的对象
Bulk_quote bqt;//正确,该派生类中覆盖了抽象基类中的纯虚函数,可以创建对象
return ;
}

访问控制与继承:

受保护的成员:

和私有成员类似,受保护的成员对于类的用户来说是不可访问的

和公有成员类似,受保护的成员对于派生类成员和友元来说是可访问的

派生类的友元只能通过派生类对象来访问基类的受保护成员:

 #include <iostream>
using namespace std; class Base{
public:
Base(int a = ) : prot_mem(a) {}
// ~Base(); protected:
int prot_mem; }; class Sneaky : public Base{
friend void clobber(Sneaky&);
friend void clobber(Base&);
int j; public:
//如果使用了默认实参,则派生类和基类的默认实参应该保持一致
Sneaky(int a = , int b = ) : Base(a), j(b) {}//调用基类的构造函数来构造派生类对象中的基类部分
ostream& print(ostream&) const; }; ostream& Sneaky::print(ostream &os) const {
os << prot_mem << j;//派生类成员中可以直接使用基类中的受保护数据成员
return os;
} // 注意:clobber(Sneaky&)和clobber(Base&)是派生类的友元,但不是基类的友元,因此我们可以通过派生类对象来访问基类中的受保护数据成员,
// 但不能直接通过基类对象来访问基类中的受保护成员。该函数相对于基类仅仅是一个用户,只能直接访问基类的公共成员
void clobber(Sneaky &s) {
s.j = s.prot_mem = ;
} void clobber(Base &b) {
// b.prot_mem = 0;//错误,该函数不是Base类的友元,只能访问Base类中的公共成员
} int main(void){
Sneaky s();
s.print(cout) << endl; return ;
}

公有、私有和受保护继承:

某个类对其继承而来的成员的访问权限受两个因素影响:一是基类中该成员的访问说明符,二是在派生类的派生类列表中的访问说明符。与其派生访问说明符无关。派生类访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:

 #include <iostream>
using namespace std; class Base {
public:
void pub_mem(); protected:
int prot_mem; private:
char priv_mem;
}; void Base::pub_mem() { } struct Pub_Derv : public Base {//公有继承,用户代码(包括Pub_Derv类的派生类)可以访问基类(如果基类的成员本身可以被访问的话)
int f() {
return prot_mem;//正确,派生类能访问protected成员
} char g() {
// return priv_mem;//错误,派生类不能访问private成员
}
}; struct Priv_Derv : private Base{//私有继承,用户代码(包括Priv_Derv类的派生类)可以访问基类
int f1() const {
return prot_mem;//正确,private不影响派生类的访问权限
}
}; //派生类访问说明符还可以控制继承自派生类的新类的访问权限
struct Derived_from_public : public Pub_Derv {
int use_base() {
return prot_mem;
}
}; struct Derived_from_private : public Priv_Derv {
int use_base() {
// return prot_mem;//Priv_Derv中继承自Base的成员都变成private了,不能被派生类调用
}
}; int main(void) {
Pub_Derv d1;//继承自Base的成员是public的
Priv_Derv d2;//继承自Base的成员是private的
d1.pub_mem();//正确,pub_mem在派生类中是public的
// d2.pub_mem();//错误,pub_mem在派生类中是private的 // Base *b = &d2;//只有公有继承才能在用户代码中使用派生类像基类转换
// b->pub_mem(); return ;
}

注意:

1)    public 继承:基类成员保持自己的访问级别

2)    protected 继承:基类的 public 和 protected 成员在派生类中为 protected 成员

3)    private 继承:基类所有成员在派生类中为 private 成员

派生类向基类转换的可访问性:

派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定 D 继承自 B:

只有当 D 公有地继承 B 时,用户代码才能使用派生类向基类转换;如果 D 继承 B 的方式是受保护的或私有的,则用户代码不能使用该转换

不论 D 以什么方式继承 B,D 的成员函数和友元函数都能使用派生类向基类的转换

如果 D 继承 B 的方式是公有的或者受保护的,则 D 的派生类的成员和友元可以使用 D 向 B 的类型转换,反之则不行

友元与继承:

不能继承友元关系,每个类负责控制各自成员的访问权限

改变个别成员的可访问性:

我们可以通过 using 声明改变派生类继承的某个名字的访问级别:

 class Base{
public:
// Base();
// ~Base();
std::size_t size() const {
return n;
} protected:
std::size_t n;
}; class Derived : private Base{
public:
// Derived();
// ~Derived();
using Base::size;//该成员被标记为public的 protected:
using Base::n;//该成员被标记为protected的 };

注意:using 声明语句中名字的访问权限由该 using 声明语句之前的访问说明符决定

派生类只能为那些它可以访问的名字提供 using 声明。即不能对基类中的 private 成员提供 using 声明

默认的继承保护级别:

默认情况下,使用 class 关键字定义的派生类是私有继承的;而使用 strcut 关键字定义的派生类是公有继承的:

 class Base {};
struct D1 : Base {};//默认public继承
class D2 : Base {};//默认private继承

OOP2(虚函数/抽象基类/访问控制与继承)的更多相关文章

  1. C++ 虚函数在基类与派生类对象间的表现及其分析

    近来看了侯捷的<深入浅出MFC>,读到C++重要性质中的虚函数与多态那部分内容时,顿时有了疑惑.因为书中说了这么一句:使用“基类之指针”指向“派生类之对象”,由该指针只能调用基类所定义的函 ...

  2. 构造函数为什么不能为虚函数 &amp; 基类的析构函数为什么要为虚函数

    一.构造函数为什么不能为虚函数 1. 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的.问题出来了,假设构造函数 ...

  3. Fluent_Python_Part4面向对象,11-iface-abc,协议(接口),抽象基类

    第四部分第11章,接口:从协议到抽象基类(重点讲抽象基类) 接口就是实现特定角色的方法集合. 严格来说,协议是非正式的接口(只由文档约束),正式接口会施加限制(抽象基类对接口一致性的强制). 在Pyt ...

  4. django abstract base class ---- 抽象基类

    抽象蕨类用于定义一些同享的列.类本身并不会在数据库端有表与之对应 一.例子: 1.定义一个叫Person 的抽象基类.Student 继承自Person from django.db import m ...

  5. C++:抽象基类和纯虚函数的理解

    转载地址:http://blog.csdn.net/acs713/article/details/7352440 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层. ...

  6. 4.6 C++抽象基类和纯虚成员函数

    参考:http://www.weixueyuan.net/view/6376.html 总结: 在C++中,可以通过抽象基类来实现公共接口 纯虚成员函数没有函数体,只有函数声明,在纯虚函数声明结尾加上 ...

  7. 基类中定义的虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型及参数的先后顺序,都必须与基类中的原型完全相同 but------> 可以返回派生类对象的引用或指针

      您查询的关键词是:c++primer习题15.25 以下是该网页在北京时间 2016年07月15日 02:57:08 的快照: 如果打开速度慢,可以尝试快速版:如果想更新或删除快照,可以投诉快照. ...

  8. 18 TaskScheduler任务调度器抽象基类——Live555源码阅读(一)任务调度相关类

    这是Live555源码阅读的第二部分,包括了任务调度相关的三个类.任务调度是Live555源码中很重要的部分. 本文由乌合之众 lym瞎编,欢迎转载 http://www.cnblogs.com/ol ...

  9. thrift之TTransport类体系原理及源码详细解析1-类结构和抽象基类

    本章主要介绍Thrift的传输层功能的实现,传输的方式多种多样,可以采用压缩.分帧等,而这些功能的实现都是相互独立,和上一章介绍的协议类实现方式比较雷同,还是先看看这部分的类关系图,如下: 由上面的类 ...

随机推荐

  1. 特别注意: range.Text.ToString(); 和 range.Value2.ToString(); 的区别

    如果Excell的单元格里面是日期,前面显示2015年05月10日:后面的显示42134 也就是说:Text 和Value2的不同. using System; using System.Data; ...

  2. MySQL 示例数据库sakila-db的安装

    最近在看 “高性能MySql”这本神书,发现上面很多例子采用的官方示例数据库sakila. 官方示例数据库 下载地址 http://dev.mysql.com/doc/index-other.html ...

  3. HTML中禁用表中控件的两种方法与区别

    在网页的制作过程中,我们会经常使用到表单.但是有时候我们会希望表单上的控件是不可修改的,比如在修改密码的网页中,显示用户名的文本框就应该是不可修改状态的. 在html中有两种禁用的方法,他们分别是: ...

  4. suse配置dhcp服务器

    Suse  dhcp服务器安装在安装系统时勾选 Suse dhcp 默认配置文件 /etc/dhcpd.conf Suse dhcp 启动程序 /etc/init.d/dhcpd restart 配置 ...

  5. #Pragma Pack与内存分配

    博客转载自:https://blog.csdn.net/mylinx/article/details/7007309 #pragma pack(n) 解释一: 每个特定平台上的编译器都有自己的默认“对 ...

  6. git 的使用方法

    git 的使用有3个主要步骤: 1.1 工作区域操作: 在自己的git账号下构建一个工作目录, 并往工作目录里添加文件内容(cp /root/data/VIP_Amount_prediction/* ...

  7. (转)Asp.Net生命周期系列一

    原文地址:http://www.cnblogs.com/skm-blog/archive/2013/07/07/3176713.html Asp.Net生命周期对于初级甚至中级程序员来说,一直都是一个 ...

  8. 编写高质量代码改善C#程序的157个建议——建议51:具有可释放字段的类型或拥有本机资源的类型应该是可释放的

    建议51:具有可释放字段的类型或拥有本机资源的类型应该是可释放的 在建议50中,我们将C#中的类型分为:普通类型和继承了IDisposable接口的非普通类型.非普通类型除了包含那些托管资源的类型外, ...

  9. LIS问题---HDU1025 Constructing Roads In JGShining's Kingdom

    发现这个说的比较通俗: 假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5.下面一步一步试着找出它.我们定义一个序列B,然后令 i = 1 to 9 ...

  10. HDU - 1251 统计难题(trie树)

    Ignatius最近遇到一个难题,老师交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在老师要他统计出以某个字符串为前缀的单词数量(单词本身也是自己的前缀).  Input输入数据的第一部 ...