OOP2(虚函数/抽象基类/访问控制与继承)

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

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

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

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

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

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

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

派生类中的虚函数:

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

final 和 override 说明符:

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

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

 1 #include <iostream>
 2 using namespace std;
 3
 4 struct B{
 5     virtual void f1(int) const;
 6     virtual void f2();
 7     void f3();
 8 };
 9
10 struct D : B{
11     void f1(int) const override;//正确,f1与基类中的f1匹配
12     // void f1(int) override;//错误,this参数应该是const的
13     // void f2(int) override;//错误,f2没有形如f2(int)的函数
14     // void f3() override;//错误,f3不是虚函数
15     // void f4() override;//错误,B没有名为f4的函数
16 };
17
18 int main(void){
19
20 }

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

 1 #include <iostream>
 2 using namespace std;
 3
 4 struct B{
 5     virtual void f1(int) const;
 6     virtual void f2();
 7     void f3();
 8 };
 9
10 struct D1 : B{
11     //从B继承f2(),f3(),覆盖f1(int)
12     void f1(int) const final;//不允许后继的其它类覆盖f1(int)
13 };
14
15 struct D2 : D1{
16     void f2();//正确,覆盖从间接基类B继承而来的f2
17     // void f1(int) const;//错误,D1已经将f1声明成final的
18 };
19
20 int main(void){
21
22 }

虚函数与默认实参:

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

回避虚函数的机制:

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

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

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

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

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

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

 1 #include <iostream>
 2 using namespace std;
 3
 4 class A{
 5 protected:
 6     int x;
 7
 8 public:
 9     // A();
10     // ~A();
11     virtual ostream& f(ostream &os) const {
12         os << x;
13         return os;
14     }
15 };
16
17 class B : public A{
18 private:
19     int y;
20
21 public:
22     // B();
23     // ~B();
24     ostream& f(ostream &os) const {
25         // return f(os) << " " << y;//错误,调用该函数时会无限递归
26         return A::f(os) << " " << y;
27     }
28 };
29
30 int main(void) {
31     B *b = new B;
32     A *a = b;
33     a->f(cout);//动态类型为B,调用ostream& B::f(ostream&) const
34     delete b;
35
36     return 0;
37 }

抽象基类:

纯虚函数:

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

 1 class Disc_quote : public Quote {
 2 public:
 3     Disc_quote() = default;
 4     Disc_quote(const std::string &book, double price, std::size_t qty, double disc) :
 5         Quote(book, price), quantity(qty), discount(disc) {}
 6
 7     double net_price(std::size_t) const = 0;//纯虚函数
 8
 9     protected:
10         std::size_t quantity = 0;
11         double discount = 0.0;
12 };
13
14 double Disc_quote::net_price(std::size_t sz) const {
15     //纯虚函数可以提供定义,但函数体必须定义在类的外部
16 }

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

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

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

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

 1 #include <iostream>
 2 using namespace std;
 3
 4 class Quote {
 5 public:
 6     Quote() = default;
 7     Quote(const std::string &book, double sales_price) :
 8         bookNo(book), price(sales_price) {}
 9
10     std::string isbn() const {
11         return bookNo;
12     }
13
14     virtual double net_price(std::size_t n) const {//定义成虚函数,运行2时进行动态绑定
15         return n * price;
16     }
17
18     virtual ~Quote() = default;//基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作
19
20 private:
21     std::string bookNo;//书籍的isbn编号
22
23 protected://可被派生类访问
24     double price = 0.0;//代表普通状态下不打折的价格
25 };
26
27 class Disc_quote : public Quote {
28 public:
29     Disc_quote() = default;
30     Disc_quote(const std::string &book, double price, std::size_t qty, double disc) :
31         Quote(book, price), quantity(qty), discount(disc) {}
32
33     double net_price(std::size_t) const = 0;//纯虚函数
34
35     protected:
36         std::size_t quantity = 0;
37         double discount = 0.0;
38 };
39
40 double Disc_quote::net_price(std::size_t sz) const {
41     //纯虚函数可以提供定义,但函数体必须定义在类的外部
42 }
43
44 class Bulk_quote : public Disc_quote {
45 public:
46     Bulk_quote() = default;
47     Bulk_quote(const std::string&, double, std::size_t, double);
48
49     double net_price(std::size_t) const override;//override显式注明该成员函数覆盖它继承的虚函数
50
51     // ~Bulk_quote();
52
53 private:
54     std::size_t min_qty = 0;//适用折扣政策的最低购买量
55     double discount = 0.0;//以小数表示的折扣额
56
57 };
58
59 Bulk_quote::Bulk_quote(const std::string &book, double price, std::size_t qty, double disc) :
60     //调用抽象基类的构造函数来构造派生类中的抽象基类部分数据成员
61     Disc_quote(book, price, qty, disc) {}
62
63 double Bulk_quote::net_price(size_t cnt) const {
64     if(cnt >= min_qty) return cnt * (1 - discount) * price;
65     return cnt * price;
66 }
67
68
69 int main(void){
70     // Disc_quote discount;//错误,不能创建抽象类基类的对象
71     Bulk_quote bqt;//正确,该派生类中覆盖了抽象基类中的纯虚函数,可以创建对象
72     return 0;
73 }

访问控制与继承:

受保护的成员:

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

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

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

 1 #include <iostream>
 2 using namespace std;
 3
 4 class Base{
 5 public:
 6     Base(int a = 0) : prot_mem(a) {}
 7 //     ~Base();
 8
 9 protected:
10     int prot_mem;
11
12 };
13
14 class Sneaky : public Base{
15 friend void clobber(Sneaky&);
16 friend void clobber(Base&);
17 int j;
18
19 public:
20     //如果使用了默认实参,则派生类和基类的默认实参应该保持一致
21     Sneaky(int a = 0, int b = 0) : Base(a), j(b) {}//调用基类的构造函数来构造派生类对象中的基类部分
22     ostream& print(ostream&) const;
23
24 };
25
26 ostream& Sneaky::print(ostream &os) const {
27     os << prot_mem << j;//派生类成员中可以直接使用基类中的受保护数据成员
28     return os;
29 }
30
31 // 注意:clobber(Sneaky&)和clobber(Base&)是派生类的友元,但不是基类的友元,因此我们可以通过派生类对象来访问基类中的受保护数据成员,
32 // 但不能直接通过基类对象来访问基类中的受保护成员。该函数相对于基类仅仅是一个用户,只能直接访问基类的公共成员
33 void clobber(Sneaky &s) {
34     s.j = s.prot_mem = 0;
35 }
36
37 void clobber(Base &b) {
38     // b.prot_mem = 0;//错误,该函数不是Base类的友元,只能访问Base类中的公共成员
39 }
40
41 int main(void){
42     Sneaky s(0);
43     s.print(cout) << endl;
44
45     return 0;
46 }

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

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

 1 #include <iostream>
 2 using namespace std;
 3
 4 class Base {
 5 public:
 6     void pub_mem();
 7
 8 protected:
 9     int prot_mem;
10
11 private:
12     char priv_mem;
13 };
14
15 void Base::pub_mem() {
16
17 }
18
19 struct Pub_Derv : public Base {//公有继承,用户代码(包括Pub_Derv类的派生类)可以访问基类(如果基类的成员本身可以被访问的话)
20     int f() {
21         return prot_mem;//正确,派生类能访问protected成员
22     }
23
24     char g() {
25         // return priv_mem;//错误,派生类不能访问private成员
26     }
27 };
28
29 struct Priv_Derv : private Base{//私有继承,用户代码(包括Priv_Derv类的派生类)可以访问基类
30     int f1() const {
31         return prot_mem;//正确,private不影响派生类的访问权限
32     }
33 };
34
35 //派生类访问说明符还可以控制继承自派生类的新类的访问权限
36 struct Derived_from_public : public Pub_Derv {
37     int use_base() {
38         return prot_mem;
39     }
40 };
41
42 struct Derived_from_private : public Priv_Derv {
43     int use_base() {
44         // return prot_mem;//Priv_Derv中继承自Base的成员都变成private了,不能被派生类调用
45     }
46 };
47
48 int main(void) {
49     Pub_Derv d1;//继承自Base的成员是public的
50     Priv_Derv d2;//继承自Base的成员是private的
51     d1.pub_mem();//正确,pub_mem在派生类中是public的
52     // d2.pub_mem();//错误,pub_mem在派生类中是private的
53
54     // Base *b = &d2;//只有公有继承才能在用户代码中使用派生类像基类转换
55     // b->pub_mem();
56
57     return 0;
58 }

注意:

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 声明改变派生类继承的某个名字的访问级别:

 1 class Base{
 2 public:
 3     // Base();
 4     // ~Base();
 5     std::size_t size() const {
 6         return n;
 7     }
 8
 9 protected:
10     std::size_t n;
11 };
12
13 class Derived : private Base{
14 public:
15     // Derived();
16     // ~Derived();
17     using Base::size;//该成员被标记为public的
18
19 protected:
20     using Base::n;//该成员被标记为protected的
21
22 };

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

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

默认的继承保护级别:

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

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

原文地址:https://www.cnblogs.com/geloutingyu/p/8452330.html

时间: 2024-08-06 22:58:44

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

c++之虚函数和基类指针

1. 基类指针虽然获取派生类对象地址,却只能访问派生类从基类继承的成员 1 #include <iostream> 2 using namespace std; 3 4 //通过基类指针只能访问从基类继承的成员 5 class A 6 { 7 public: 8 A(char x) 9 { 10 this->x = x; 11 } 12 //void virtual who() //基类定义虚函数,则派生类的重定义版本默认为虚函数 13 void who() //除非定义虚函数,否则基类

【C/C++学院】0825-类模板/final_override/类模板与普通类的派生类模板虚函数抽象模板类/类模板友元/位运算算法以及类声明/Rtti 实时类型检测/高级new创建/类以及函数包装器

类模板 类模板多个类型默认类型简单数组模板 #pragma once template <class T=int>//类模板可以有一个默认的值 class myArray { public: myArray(); ~myArray(); }; #include "myArray.h" template <class T=int>//每一个函数都需要加上一个默认的值 myArray<T>::myArray() //类模板成员函数在外部,需要加载类型初始

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

一.构造函数为什么不能为虚函数 1. 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的.问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数. 2. 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用.构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀.所以构造函数没有必要是虚

C++编程实践: 抽象基类

本实例来自<C++ Primer Plus>(第六版) 第十三章 题目要求: 假设你正在开发一个图形程序,该程序会显示圆和椭圆等,需要考虑: 椭圆要包含椭圆中心坐标,半长轴,半短轴以及方向角的数据.圆要实现圆心坐标,半径等数据. 椭圆要包含移动,旋转一定角度,计算面积和缩放等方法,但是圆不需要旋转. 设计思路: 虽然圆也是一种椭圆的特殊形势,但是设计成由圆类继承椭圆类显然是十分笨拙的.比较好的办法是涉及一个基类BaseEllipse,圆和椭圆都继承此基类.这样便可以使用指针数组同时管理Circ

PoEduo - C++阶段班【Po学校】-继承&amp;虚函数&amp;抽象&amp;接口- 课堂笔记

附录(一) 扩展知识:  1--> 面向对象的三个基本特征   1.0 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private,protected,public) 1.1 继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力).可视继承(子窗体使用父窗体的外观和实现代码).接口继承(仅使用属性和方法,实现滞后到子类实现).前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式. 1.3 多态:是将

面向对象程序设计——抽象基类,访问控制与继承,继承中的类作用域,拷贝函数与拷贝控制

一.抽象基类 1)纯虚函数 和普通的虚函数不同,一个纯虚函数无须定义.我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数.其中,=0只能出现在类内部的虚函数声明语句处. 值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部.也就是说,我们不能在类的内部为一个=0的函数提供函数体. 2)含有纯虚函数的类是抽象基类 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类.抽象基类负责定义接口,而后续的其他类可以覆盖该接口.我们不能直接创建一个抽象

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

转载地址:http://blog.csdn.net/acs713/article/details/7352440 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层. ⑴抽象类的定义: 称带有纯虚函数的类为抽象类. ⑵抽象类的作用: 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作.所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些

C++ 纯虚函数与抽象基类

在C++ 中,虚函数可以很好的实现多态,然而也有一定的局限,这个局限在于,虚函数必须在基类中定义,哪怕是空的实现. 例如下面这段代码编译时就会出错: class Base { virtual void print(); }; class Derived:Base { void print() { cout << "Derived" <<endl; } }; 因为Base类中的虚函数print()没有定义. 而在实际编程时我们往往会遇到无法给基类的函数定义的情况,

纯虚函数与抽象基类

这一节我们主要介绍一下抽象基类与纯虚函数的关系 首先,我们来看看纯虚函数 1.纯虚函数的定义   纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法.在基类中实现纯虚函数的方法是在函数原型后加"=0".如下     virtual void funtion()=0 2.定义纯虚函数的目的和意义 定义纯虚函数是为了实现一个接口,起到一个规范的作用.纯虚函数的意义在于:让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供