C++ 面向对象程序设计 《C++Primer》第15章(上篇)———— 读书笔记

40 篇文章 7 订阅

1、深入虚函数

1.1 所有虚函数都必须提供定义

由于虚函数的动态绑定特性,编译器无法在编译阶段就确定到底会使用哪个版本的虚函数,所以所有的虚函数都必须提供定义。
在这里插入图片描述

1.2 派生类中的虚函数要 严格 模仿基类虚函数

派生类的虚函数返回值,和参数列表都要和基类的一模一样。
只有一个例外:
在这里插入图片描述

1.3 override说明符

override修饰的函数,必然覆写了基类中同名的的虚函数,必然和他有着相同的返回值和参数列表
override存在这种特性的原因之一是:当我们在派生类中定义一个与基类虚函数同名,但是返回值和参数列表不尽相同的函数时,编译器会把它当作一个新的函数,不会报错。然而,这很有可能是程序员出错了。那么,有了override,这种情况就会报错啦,啊哈哈哈哈。

所以,overirde修饰的函数必须满足虚函数的要求

说白了,其实就是override关键字用来说明这是派生类的虚函数。

1.4 final修饰的虚函数无法被覆盖

1.5 虚函数 与 默认实参

虚函数可以有默认实参。
若某次函数调用使用了虚函数的默认实参,那么,使用的默认实参本次调用的静态类型决定
在这里插入图片描述

1.6 回避虚函数的动态绑定机制

当想要调用虚函数的某个特定版本时,我们可以使用作用域运算符限定到底调用哪个版本,如下所示:

Bulk_quote* pbulk
double undiscount_price = pbulk->Quote::net_price(42);

尽管上面的pbulk是指向派生类的指针,但可以使用作用域限定符强制调用基类版本net_price虚函数。
此条调用可以在编译阶段完成解析

什么情况下需要回避动态绑定机制?

通常是,当一个 派生类的虚函数 调用 它覆盖的基类的虚函数版本时。

这其实不难理解,因为,基类版本的虚函数完成的是常规工作。派生类的版本需要执行一些与派生类本身密切相关的别的操作,这时,常规工作就直接调用虚函数版本来完成,减少代码量,提高代码复用性。

有时,成员函数(或友元)中的代码也使用作用域运算符回避动态绑定机制。
当然了,派生类的虚函数是包括在成员函数里的。
在这里插入图片描述

2、抽象基类

在这里插入图片描述
对重构的的深入理解,请移步这篇博客。

2.1 纯虚函数

= 0来替代虚函数的函数体,来表示这是个纯虚函数,纯虚函数无须(也不能)定义
virtualoverride同理, = 0能出现在类内部

含有纯虚函数的类是 抽象基类

不能(直接)创建抽象基类的对象。因为它定义了纯虚函数。但是我们可以通过创建抽象基类的派生类间接创建抽象基类对象。

子类必须定给出纯虚函数的定义,否则子类也还是抽象类。

派生类的构造函数值初始化他的直接基类

创建派生类时,派生类调用其抽象基类的构造函数,创建一个抽象基类的对象,抽象基类又调用其基类的构造函数,创建一个基类对象。

书中给的例子:

Quote.h基类:

#pragma once
#include<string>
#include<iostream>

using namespace std;

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

void Quote::debug(ostream& os) const
{
	os << typeid(bookNo).name() << ": " << "bookNo = " << bookNo << endl;
	os << typeid(price).name() << ": " << "price = " << price << endl;
}

Disc_quote.h抽象基类:

#pragma once
#include"Quote.h"

using namespace std;
// 重构
// 这是个抽象基类,无法声明对象,因为含有 纯虚函数 nte_price
class Disc_quote : public Quote
{
public:
	Disc_quote() = default;
	Disc_quote(const string& book, double price, size_t n, double dis) :
										Quote(book, price), quantity(n), discount(dis) { }
	double net_price(size_t) const = 0;		// 纯虚函数
protected:
	size_t quantity = 0;
	double discount = 0.0;
};

Bulk_quote.h抽象基类的派生类:

#pragma once
#include"Disc_quote.h"

using namespace std;

class Bulk_quote : public Disc_quote
{
public:
	Bulk_quote() = default;
	Bulk_quote(const string& book, double price, size_t n, double dis) : 
														Disc_quote(book, price, n, dis) { }
	virtual double net_price(size_t cnt) const override
	{
		if (cnt >= quantity)
			return cnt * (1 - discount) * price;
		else
			return cnt * price;
	}
	~Bulk_quote() = default;
	virtual void debug(ostream&) const override;
};

void Bulk_quote::debug(ostream& os) const
{
	this->Quote::debug(os);
	os << typeid(quantity).name() << ": " << "quantity = " << quantity << endl;
	os << typeid(discount).name() << ": " << "discount = " << discount << endl;
}

在这里插入图片描述

2、访问控制

2.1 protected成员

private类似:

  • 对于本类,protected成员只能在类内部(及友元)访问
  • 对于其派生类,只能在派生类内部访问,派生类的友元只能通过派生类的对象访问,而不能通过基类对象访问,因为他只是派生类的友元,而不是基类的友元
    在这里插入图片描述

2.2 公有、私有、受保护继承

一篇总结得不错的文章。

为什么这篇文章中说:private继承在软件设计层面上没有意义,其意义只在于软件实现层面。
private继承,父类中所有的方法到了子类中都变成了私有的,不能作为接口提供给用户使用,所以只能用于实现子类的功能,所以他的意义只在于软件实现层面。


派生列表里的访问说明符对子类中的成员子类的友元能否访问基类中成员,没有任何影响。子类原来能访问的还能访问,原来不能访问的还是不能访问。(只与基类中的访问说明符有关)

有影响的 是子类的用户子类的子类(以及子类的子类的友元)对基类成员的访问:

  • private继承,相当于父类中publicprotected的成员定义为子类的private成员
  • protected继承,相当于父类中publicprotected的成员定义为子类的protected成员
  • public继承,父类中protectedpublic的成员在子类中不发生改变,相当于把原有的非private成员直接拿到子类成了子类定义的成员

子类的用户子类的子类(以及子类的子类的友元)根据上述三条原则访问子类继承自父类的成员
注:上面说的都是相当于,并不是子类真的有了这些成员,仍然是从父类继承而来的。
(上面说的子类的用户也可理解为用户代码

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

派生列表的访问说明符除了 子类的用户 和 子类的子类 使用父类成员有影响外,还 子类的用户 和 子类的子类 使用子类向父类的类型转换有影响

  • 子类中的成员子类的友元能否使用子类向父类的转换仍然不受任何影响
  • 只有public继承,子类的用户 才能使用子类向父类转换
  • 只有publicprotected继承,子类的子类子类的子类的友元 才能使用子类向父类转换

下面分别通过代码,依次证明上述三条:

class B{};	父类 B

class D: public B	
{
    void function(D &d) { B b = d; }	子类的 成员函数进行转换,没问题
    friend void friendFunction(D &d) { B b = d; }	子类的 友元进行转换,没问题 
};

class E:protected B
{
    void function(E &e) { B b = e; }	子类的 成员函数进行转换,没问题
    friend void friendFunction(E &e) { B b = e; }	子类的 友元进行转换,没问题 
};

class F:private B
{
    void function(F &f) { B b = f; }	子类的 成员函数进行转换,没问题
    friend void friendFunction(F &f) { B b = f; }	子类的 友元进行转换,没问题 
};
这全是子类,随便转换,不受影响
#include <iostream>

class A		父类 A
{
public:
    virtual void print() { cout << "我是A" << endl; }
};
 
class B : public A		子类 B
{
public:
    void print() { cout << "我是B 继承A" << endl; }
};
 
class C : private A		子类 C
{
public:
    void print() { cout << "我是C 继承A" << endl; }
};
 
int main()
{				用户,也称 用户代码,也就是 类外
    A *p;			
    B b;	子类 B 的用户
    C c;	子类 C 的用户
    
    p = &b;		正确
    p = &c; 	错误,C 是 private 继承,不能转换
    p->print();
}
class D : public B	
{
    void function(D &d) { B b = d; }
    friend void friendFunction(D &d) { B b = d; }
};

class E: protected B
{
    void function(E &e) { B b = e; }
    friend void friendFunction(E &e) { B b = e; }
};

class F: private B
{
    void function(F &f) { B b = f; }
    friend void friendFunction(F &f) { B b = f; }
};															上面是 子类 及其 友元
//     分 割 线     \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
class G : D													下面是 子类的子类 及其 友元
{
    void function(D &d) { B b = d; }
    friend void friendFunction2(F &f) { B b = f; }
};

class H : E
{
    void function(E &e) { B b = e; }
    friend void friendFunction2(F &f) { B b = f; }
};
 
class I : F
{
    void function(F &d) { B b = f; }	报错
    friend void friendFunction2(F &f) { B b = f; }		报错
};
本例中的前三个类(DEF)都是子类,随便转换,不受任何影响
后三个类(GHI)都是子类的子类 及友元,但前两个分别是 publicprotected 的,也能转换
最后一个是 private 的,不能转换

在这里插入图片描述

2.4 友元 与 继承

友元关系不能继承

一个可能搞错的情况:

class father
{
	friend class X;		类 X 是 father 的友元
protected:
	int protected_member;
};

class son : public father {};	son 是 father 的子类,友元关系不会继承,X 不能访问 son 的成员

class X
{
public:
	int func(son s) { return s.protected_member; }		注意这里,这是正确的
};

上面代码中,Xfun成员虽然访问son,但这边并没有违背友元不能继承的特性。因为访问的是father的成员protected_member

一个友元对他朋友的可访问性,包括了朋友对象内嵌在其子类对象中的情况。

2.5 改变单个成员的可访问性

首先声明,我们更改某成员的可访问性后,当前的类 得不到任何好处
也就是说,更改成员的可访问性是用来造福 或 坑害他人的。(他人指的是类的用户,这个类的子类及其友元)
我们使用using关键字来更改,using语句前的访问限定符是啥,就是改成了啥。举个例子:

class father			父类
{
private:
	int private_member;		私有
protected:
	int protect_member;		保护
};

class son				子类
{
public:
	using father::protect_member;	改成共有,这样 son的子类,son的用户就能用了,造福了他们
protected:
	using father::private_member;	错误,son都不能访问,你还想改它的访问类型???痴心妄想!
};

正如上例所示,son得不到任何好处,而且它也没有能力更改自己也访问不了的成员的可访问性。

2.6 默认的继承保护级别

structclass唯二的区别之一:

  • class默认以private来继承父类
  • struct默认以public来继承父类

3、继承中的类作用域

子类的作用域嵌套在父类的作用域里。
如果一个名字在子类中无法解析,编译器会继续在外层父类作用域寻找该名字的定义。

3.1 编译时 进行名字查找

编译时进行名字查找,就是说,静态类型 指定编译器查找某个名字的作用域。
因为,编译时只能通过静态类型来查找了,这个时候还没有动态类型

因此,你就不能做出下面代码所示的这种操作:

class Disc_quote : public Quote
{
public:
	void aaa() { return; }
};

Disc_quote dq;		
Quote* p = &dq;		发生隐式转换
p->aaa();			错误,p 的类型是Quote的指针,对 aaa 的搜索在Quote的作用域内,显然不包含aaa

提防这种错误

3.2 名字隐藏

和其他的作用域一样,子父类的作用域之间也存在名字隐藏
子类定义了和父类同名的成员时,就会隐藏掉父类的成员。

  • 子父类都有同名成员函数 不构成重载 ,仍然是隐藏。

    这里涉及到重载与作用域的关系:
    若在内层作用域中声明名字,他将隐藏外层作用域中声明的同名实体在不同的作用域中复发重载函数名
    父类和子类是两个作用域,子类成员根本不可能重载任何父类的成员函数。

  • 名字查找优先于类型检查。

    当调用一个成员时,编译器不管三七二十一先查找成员的名字,找到名字之后再判断这个调用传入的实参类型是否和形参类型相同

    也就是说,当你调用了一个成员函数,类里确实定义了同名的函数,但是参数类型对不上,这时,不存在说,哎?参数类型对不上啊!那咱去它父类里找找有没有参数能对上的吧!
    这是不对的。因为是先查找名字,找到一样的名字就定死了,不去找别的了,你参数类型对不上就报错啦!!!就是下图所示的情况:
    在这里插入图片描述

要是想使用父类的同名成员就只能加上作用域运算符了。

class father { public: int mem; };
class son : public father 
{ 
public: 
	int mem; 
	int get_father_mem() { return father::mem; }
};

作用域运算符直接覆盖原有查找规则,让编译器从指定作用域开始查找名字。
在这里插入图片描述

小节一波:

类成员的查找步骤:( 假设调用p->mem()

  • ① 先确定p(对象)的静态类型
  • ② 在p(对象)的静态类型的作用域中查找mem。若找不到,再到父类里面找直到找到继承链顶端,还找不到,报错。
  • ③ 若找到了,检查此调用是否合法(参数对不对的上啊等等)
  • ④ 若调用合法。编译器根据调用的是否是虚函数产生不同的代码
    • mem是虚函数 此次是通过引用或指针进行的调用,则编译器产生的代码在运行时确定运行该虚函数的哪个版本,依据就是对象的动态类型。
    • mem不是虚函数 此次调用是通过对象进行的,则直接调用相应函数即可。

3.3 覆盖重载的函数

虚函数可以被重载。
子类可以覆盖0个或多个父类中重载的函数。
但如果子类想使用父类重载函数的所有版本,就必须覆盖所有的版本 或 一个都不覆盖。

举个例子:

class father
{
public:
	virtual void aaa() { cout << "空的版本" << endl; }				这四个
	virtual void aaa(int) { cout << "int的版本" << endl; }			全都是
	virtual void aaa(double) { cout << "double的版本" << endl; }	 aaa
	virtual void aaa(string) { cout << "string的版本" << endl; }	的重载
};

class son : public father
{
public:
	virtual void aaa(string) override { cout << "重载的string的版本" << endl; }	 aaa 的覆盖
};

son s;
s.aaa(string());	正确,你覆盖了 string 的版本,当然可以使用
s.aaa(3.14);		错啦!你没有 全部 覆盖,只能用 string 的版本
					你想用的话,要么把剩下的全tm覆盖了,要么全不覆盖

原因仍然是名字隐藏,当你覆盖了一个重载版本后,他隐藏了外部作用域中的同名函数。所以无法访问那些没被覆盖的重载版本。

那我就想 只覆盖一部分,还想用其他没覆盖的重载版本怎么办?使用using声明。

using声明语句指定一个名字而不指定形参列表,所以一条父类成员函数的using声明语句就可以把该函数的所有重载版本添加到子类作用域中。
using把他们添加进来,重载函数的名字就都可见了,就避免了名字隐藏。

此时就可以只覆盖某一个重载版本,同时使用没有覆盖的重载版本。
对那些 子类没有覆盖的重载版本的访问实际上是对using声明点的访问。

class son : public father
{
	如果写在这种地方,那就相当于是把他们改成了 私有继承 下来的,还是访问不了
public:
	using father::aaa;		using 声明一定要写在 publicvirtual void aaa(string) override { cout << "重载的string的版本" << endl; }
};

也就是说,不能这样:

class son : public father
{
	using father::aaa;		修改了 aaa() 的继承限制,成了私有继承下来的了
public:
	virtual void aaa(string) override { cout << "重载的string的版本" << endl; }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值