C++ Primer 第十五章 面向对象程序设计 15.2 定义基类和派生类 练习和总结

a 夏天 提交于 2020-02-28 22:43:38

基类和派生类是相对的概念,如果类A继承自类B,则类B相对于类A是基类,类A相对于类B是派生类。

15.2 定义基类和派生类

在定义基类时,我们需要将析构函数和需要子类重写的函数定义为虚函数因为当我们使用指针或者引用调用虚函数时,会根据实际绑定的动态类型来调用基类或者子类的函数。

为什么要这样做?
在定义派生类的时候解释。

之前在修饰类的成员时,我们只使用了private和public,这里还有一个protected用来修饰成员,表示派生类可以直接访问,但是其他用户(类,函数)不能对修饰为protected的成员访问。

15.2.1 定义基类

练习

15.1

使用virtual关键字修饰的函数时虚函数

15.2

用protected修饰的成员,使用类的用户不能直接访问,但是继承该类的派生类可以访问。

使用private修饰的成员,使用这个类的用户不能直接访问,继承该类的派生类也不可以直接访问。

15.3
class Quote
{
public:
	Quote()=default;
	virtual ~Quote()=default;
	Quote(const string& s, double p) :bookNo(s), price(p) {};
	virtual double net_price(std::size_t n) const {
		return n * price;
	}
private:
	std::string bookNo;
	protected:
		double price = 0.0;
};

15.2.2 定义派生类

使用类派生列表来指明当前类是哪些基类的派生类,C++支持多继承,但是一般只用单继承

class A:public class B{
	
};

如果指定了从哪个基类派生,则必须要定义这个派生类,在声明类A时,不用写继承自哪个类。 所以下面的代码时错误的

class A:public class B;

如果我们需要重写父类的某一个函数,为了支持动态绑定,这个函数在父类中需要被声明为virtual,此后这个函数就被隐式的声明为virtual了,如果想要显式的说明子类写的函数时重写符类的函数,则在函数的参数列表后面使用overrid关键字,此时如果父类对应的函数不是虚函数编译器就会报错。

class A{
	public:
	virtual void func();
	A(int a):value_a(a){};
	private:
	int value_a;
}
class B:public class A{
	public:
	virtual void func() override;
	//virtual void func() const override;
	//virtual void func() & override;
	private:
	int value_b;
}

我们可以使用基类的引用或者指针绑定派生类的对象,这是由于派生类是有多个部分组成的,它由基类的成员和自己的非静态成员构成可以理解为一个派生类的对象包含了自身定义的对象以及基类的对象(不包含静态成员)。

所以我们用一个基类对象的指针或者引用绑定的实际是这个派生对象中基类的子对象。

那么虚函数有什么用?

假设类B是类A的派生类,类B重写了类A的函数func(); 那么一个类B的对象中就包含了两个func(),一个是类A自己的(因为包含了类A的子对象),一个类B重写的。那么如果我们使用类A的引用绑定类B的对象,此时调用func()到底是调用类A的func还是类B重写的func()?

A a;
B& =a;
a.func();

答案是:如果func()是虚函数而且类B重写了func(),那么调用的类B的func(),如果func()不是虚函数,那么类B就算重写了调用的还是类A的func()

虚函数的意义就在于此,它可以让一个变量调用正确的函数。

至于为什么析构也必须是虚函数,将在后面介绍。

派生类实际上是由基类成员和派生类定义的成员两部分组成的,在派生类的构造函数中,我们需要初始化基类的成员,我们通过调用基类的构造函数来完成。如果我们没有指出基类如何初始化,则基类执行默认初始化。

如下所示,使用
//B(int a,int b):value_a(a),value_b(b){};
这种方式是错误的,因为a实际上是private,类B无法直接访问。

class A{
	public:
	virtual void func();
	A(int a):value_a(a){};
	private:
	int value_a;
}
class B:public class A{
	public:
	virtual void func() override;
	//这是错误的
	//B(int a,int b):value_a(a),value_b(b){};
	//这才是正确的
	B(int a,int b):A(a),value_b(b){};
	private:
	int value_b;
}

另外两点,
1.对于基类的静态数据成员 整个继承体系下只存在一份。即基类和派生类公用静态成员。

2.如果我们不像让一个类被继承,则在类后加上final进行修饰。

下面的类B无法被继承

class B final:public class A{
}

练习

15.4

a。错误,不能自己继承自己
b。正确
c。错误,声明的时候不需要写继承关系,写了继承关系就一定要定义。

15.5,15.6 15.7

头文件

#pragma once
#include "Quote.h"
class Bulk_quote :
	public Quote
{
public:
	Bulk_quote() = default;
	~Bulk_quote() = default;
	Bulk_quote(const string& s, double p, size_t q, double dis);
	double net_price(std::size_t n) const override;
private:
	size_t min_qty = 0;
	double discount = 0.0;
};

class Over_part_original_price :public Quote {
public:
	Over_part_original_price(const string&s, double p, size_t q, double dis);
	double net_price(std::size_t n) const override;

private:
	size_t over_threshould = 0.0;
	double discount = 0.0;
};

cpp文件

#include "pch.h"
#include "Bulk_quote.h"
#include <iostream>

Bulk_quote::Bulk_quote(const string & s, double p, size_t q, double dis):Quote(s,p),min_qty(q),discount(dis)
{

}

double Bulk_quote::net_price(std::size_t n) const
{
	std::cout << "Bulk_quote::net_price" <<std::endl;
	double money=0.0;
	if (n>=min_qty)
	{
		money =  price * n*discount;
	}
	else {
		money = n * price;
	}
	return money;
}

Over_part_original_price::Over_part_original_price(const string & s, double p, size_t q, double dis):Quote(s,p), over_threshould(q), discount(dis)
{

}

double Over_part_original_price::net_price(std::size_t n) const
{
	std::cout << "Over_part_original_price::net_price(std::size_t n) " << std::endl;
	double money = 0.0;
	if (n<=over_threshould)
	{
		money = n * price*discount;
	}
	else {
		money = (n - over_threshould)*price + over_threshould * price*discount;
	}
	return money;
}

测试文件:

Quote q("123",20);
Bulk_quote q1("123", 20, 10, 0.8);
Over_part_original_price q2("123", 20, 10, 0.8);
Quote& d = q;
Quote& d1 = q1;
Quote& d2 = q2;

cout<<d.net_price(20)<<endl;
cout<<d1.net_price(20)<<endl;
cout<<q1.Quote::net_price(20)<<endl;
cout << q2.net_price(20) << endl;

15.2.3 类型转换与继承

用指针和引用绑定对象时,往往要求指针或者引用的类型和所绑定的类型需要一致。
**但是一个基类的引用或者指针可以绑定一个派生类的对象,**只要好好理解派生类的对象由派生类定义的非静态成员和基类定义的非静态成员组成,就能明白为什么能将派生类的对象绑定到基类的引用或指针上。

首先需要明白一个静态类型动态类型,静态类型就是变量或者表达式本身的类型。这在程序没有运行时就已经确定了。

A& a,a就是a&类型,是静态类型
动态类型变量或者表达式所涉及的在内存中的对象中的内存。这需要在程序运行时才能确定。

A& a = b; a的动态类型就是B。
如果一个对象不是指针和引用类型,那么它的静态类型和动态类型是一样的。

为什么a可以绑定B的对象因为B中由类A的子对象,所以绑定的实际上是类B的类A子对象。
对于指针同样如此。

但是我们不能使用派生类的引用或者指针去绑定一个基类的对象,因为基类对象中不存在派生类的成员。

下面的方式虽然基类绑定到了一个派生类,但是由于编辑器在检查时只检查静态类型,所以b1无法绑定a的对象,但是我们可以使用static_cast<B*>(a)来转化a。

B b;
A* a = &b;
B* b1 = a;

我们可以使用派生类的对象来初始化一个基类的对象或者为一个基类对象赋值,这是由于基类的拷贝构造、拷贝赋值函数往往是引用类型,而引用类型就可以绑定派生类的对象,从而进行初始化。但是初始化或者赋值的对象和原来的派生类的对象是独立的。

练习

15.8

静态类型就是变量或者表达式的生成类型。
动态类型就是变量或者表达式在内存中的对象的类型。

即静态类型在没运行程序的时候,我就知道这个变量是什么类型,但是这个变量指向的对象是什么类型,我是不知道的。

15.9

基类的引用绑定派生类的对象
基类的指针指向派生类的对象
或者指针指向对一个对象,因为静态类型是类类型的指针,而动态类型是一个类类型

15.10

ifstream是istream的派生类,所以istream的引用可以接收一个ifstream的对象。
使得istream& 实际指向的是一个ifstream对象,而ifstream override了输入运算符,所以使用istream调用>>实际调用的是ifstream实现的版本。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!