15.1 oop:概述
- 面向对象程序设计核心思想
- 数据抽象 ,继承,动态绑定
继承:联系在一起的类构成一种层次关系,通常在层次关系的根部有一个基类
派生类:其他类则直接或间接地从基类继承而来
继承示例代码
class Quote
{
public:
string isbn()const;
virtual double net_price(size_t n)const;
};
class Bulk_quote :public Quote
{
public:
double net_price(size_t)const override;
};
override关键字:新标准 允许派生类显示地注明他将使用那个成员函数改写基类的虚函数
动态绑定:
//计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream& os, const Quote& item, size_t n)
{
//根据传入的item形参的对象类型调用Quote::net_price
//或者Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() //调用 Quote::net_price
<< " # sold: " << n << "total due: " << ret << endl;
return ret;
}
重点是 Quote的调用 ,看 double ret =item.net_price(n);
这一段是用Quote的对象调用函数,他也是可以用派生类Bulk_quote的对象调用它。而且print_total函数调用的是引用类型调用net_price函数的。
下面的代码解释了你用基类和派生类的区别,是不一样的。
//basic的类型是Quote;bulk的类型是Bulk_quote
Quote basic;
Bulk_quote bulk;
print_total(cout, basic, 20);
print_total(cout, bulk, 20);
动态绑定:上述过程中函数的运行版本由实参决定,
就是在运行时,而不是事先知道,是在运行的时候,
就是运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定
在c++语言,当我们使用基类的引用(或者指针)
调用一个虚函数(virtual)时将发生动态绑定
15.2.1 定义基类和派生类
Quote的具体细节
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; //对析构函数进行动态绑定
private:
string bookNo; //书籍的ISBN编号
protected:
double price = 0.0; //代表普通状态下不打折的价格
};
小知识点:基类通常定义一个虚析构函数, 即使该函数不执行任何实际操作。
成员函数与继承:
派生类可以继承其基类的成员,派生类需要提供自己的新定义覆盖从基类继承而来的旧定义。 派生类-》基类的成员。
2.基类希望将它的两种成员函数区分开来:
一种是虚函数(派生类可以进行覆盖的函数),使用指针或者引用调用虚函数时,将进行动态绑定,前面解释过了,根据绑定的对象类型不同,调用的版本也不同,肯能是派生类的,也可能是基类的版本。
动态绑定关键词:关键字virtual,只能出现来类内部的声明语句之前,
不能用于定义类外部的函数定义。
基类的一个函数声明成虚函数,该函数在派生类是隐式地也是虚函数。
3.成员函数没被声明为虚函数,则解析过程发生在编译时,而非运行时,看isbn函数的行为过程就知道了,继承层次关系只有一个isbn函数,不存在调用isbn()时到底执行哪个版本的疑问
访问控制与继承:
protected:基类希望它的派生类有权访问该成员,同时禁止其他用户访问
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
练习15。1:
基类带的virtual函数,希望派生类继承然后自定义其函数。
练习15。2:
protected:允许基类的派生类可以访问其私有成员,不允许其他类访问它
private:禁止所有用户包括派生类访问其私有属性,但是友元可以
练习15.3:
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; //对析构函数进行动态绑定
private:
string bookNo; //书籍的ISBN编号
protected:
double price = 0.0; //代表普通状态下不打折的价格
};
//计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream& os, const Quote& item, size_t n)
{
//根据传入的item形参的对象类型调用Quote::net_price
//或者Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() //调用 Quote::net_price
<< " # sold: " << n << "total due: " << ret << endl;
return ret;
}
15.2.2定义派生类
示例:class Bulk_quote: public Quote
class Bulk_quote :public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string& book, double p, size_t qty, double disc) :
Quote(book,p),min_qty(qty),discount(disc)
{
}
//覆盖基类的函数版本
double net_price(size_t cnt)const override
{
if (cnt >= min_qty)
{
return cnt * (1 - discount) * price;
}
else
{
return cnt * price;
}
}
private:
size_t min_qty = 0; //适用折扣政策的最低购买量
double discount = 0.0;//以小数表示的折扣额
};
Bulk_quote继承了什么?:继承了isbn函数 和booNo ,price等数据成员
派生公有的,基类的公有成员也是派生类接口的组成部分
公有派生类型的对象绑定到基类的引用或指针上:
Bulk_quote p2("cc",5,5,0.3);
Quote* p = &p2;
print_total(cout, *p, 10);
派生类中的虚函数
:经常(但不总是)覆盖它继承的虚函数,
没有覆盖就直接继承基类的版本,当做是普通的成员函数
c++11标准:显示注明它使用某个成员函数覆盖它继承的虚函数
形参列表后面,或引用成员函数的引用限定符后面加override;
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)
成员的字对象,以及一个与该派生类继承的基类对应的子对象,多个基类就多个子对象。(爸爸多,属性也多)
表示工作机理的概念模型,而非物理模型
Bulk_quote对象
从Quote继承而来的成员 bookNo price
Bulk_quote 自定义的成员 min_qty discount
就是说把派生类的对象当成基类的对象来用,就能用基类的指针或引用绑定到派生类对象中的基类部分
Quote item; //基类对象
Bulk_quote BUIK; //派生类对象
Quote* w = &item; // w指向Quote对象
w = &BUIK; //w指向BUIK的Quote部分
Quote& r = BUIK; //r绑定到BUIK的Quote部分
这种叫做派生类到基类的类型转换是隐式的
这句话真是绕口令:
(派生类对象或派生类引用用在需要基类引用的地方,派生类对象的指针用在需要基类指针的地方 )说白了就是上面这段代码的总结
派生类的构造函数
每个类控制它自己的成员初始化过程:就是说派生类继承的基类成员是由基类来初始化的,而它自己的成员是自己初始化的。
Bulk_quote(const string& book, double p, size_t qty, double disc) :
Quote(book,p),min_qty(qty),discount(disc)
{
//book,p就是由基类来初始化,先指向Quote
//在执行Bulk_quote自己的成员初始化
}
派生类使用基类的成员
double net_price(size_t cnt)const override
{
if (cnt >= min_qty) //如果给定的数量超过了最低购买量
{
// 它可以访问基类的公有成员或受保护的成员
return cnt * (1 - discount) * price;
}
关键概念:遵循基类的接口
每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,
即使这个对象是派生类的基类部分也该如此
派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员
继承与静态成员
基类定义了一个静态成员且在整个继承体系只存在该成员的唯一定义,无论从基类派生处多少个派生类,对于每个静态成员来说只存在唯一一个。
class Base {
public:
static void statmem()
{
cout <<"调用"<< endl;
}
};
class Derived :public Base
{
void f(const Derived&);
};
void Derived::f(const Derived&derived_obj)
{
Base::statmem; //Base定义了statmem
Derived::statmem;//Derived继承了statmem
derived_obj.statmem();//通过Derived 对象访问
statmem(); //this对象访问
}
如果Base 的静态成员是private,则派生类无权访问它,如果是public,
则能通过基类使用它也能通过派生类使用它
派生类的声明:class Bulk_quote; //正确的声明方式
被用作基类的类
如果声明 ,必须有定义,要不然一定报错,它是基类就必须这样做
因为派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类当然要知道它们是什么。
一个类是基类又是派生类:它继承别人的基类,但是它又被别人继承
class Base{....};
class D1 :public Base{...}; //Base 是D1的直接基类,同时是D2的间接基类
class D2 :public D1{...}; //最终的派生类将包含它的直接基类的子对象以及每个间接基类的子对象
防止继承的发生
一种类,我们不希望其他类继承它,或者不考虑是否作为一个基类
类名后面加关键字final:
//class NoDerived final {}; //不能作为基类
//class bbaassee{};
//class Last final :bbaassee{ }; //Last 不能作为基类
//class Bad :NoDerived { }; //报错,NoDerived是final的
//class Bad2: Last{ }; //报错,last是final的
15。2.2练习
15。4:
(1)错了,派生类不能派生它自己
(2)对,但是派生类不能访问基类的私有属性
(3)错,声明包含类名但不包含其他的派生列表
明天写
15.2.3类型转换与继承
通常将引用或指针绑定到一个对象,引用或指针的类型应与对象一致。
但基类的指针或者引用可以指向或绑定到派生类对象上。
当使用基类的引用或指针时并不清楚该引用或指针所绑定对象的真实类型,
可以是基类的对象,也可以是派生类的对象,而且还可以用派生类对象的指针存储在一个基类的智能指针内。
静态类型 动态类型
表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型,动态类型则是变量或表达式表示的内存中的对象的类型
double ret =item.netprice(n);
item的静态类型是Quote&,动态类型则依赖于item绑定的实参。
如果传递的是Bulk_quote对象给print_total,则与item的静态类型和它的动态类型不符合。
表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
不存在从基类向派生类的隐式类型转换
//为什么之前的代码可以从派生类到基类的自动类型转换是因为每个派生类对象都包含
//一个基类部分。
//一个基类的对象既可以是独立的存在,也可以是派生类的一部分,
Quote base;
Bulk_quote* bulkp =&base; //报错
Bulk_quote& bulkref =base; //一样报错
//如果基类对象不是派生类对象的一部分,则它只含有基类定义的一部分,而不含有
//派生类的一部分
//一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换
Bulk_quote bulk;
Quote * itemp = &bulk; //没有问题,动态类型是Bulk_quote
Bulk_quote * bulk p = itemp; //报错
在对象中之间不存在类型转换
Bulk_quote bulk;
Quote item(bulk); //调用Quote(const Quote&)构造函数
item =bulk; //调用operator=(const Quote&)
//构造item时只运行Quote的拷贝构造函数,只处理boonNo和price两个成员,忽略bulk中Bulk_quote部分的成员
用一个派生类对象为一个基类对象初始化或赋值是,只有派生类对象中的基类部分会被拷贝,移动和赋值,它的派生类部分将被忽略掉。
15.2.3节练习
练习15.8:
(1)静态类型:编译器已知的类型,变量声明时的类型或表达式生成的类型
(2)动态类型:只有在运行时才能知道的类型,变量或表达式的内存中的对象模型。
练习15.9:
基类的指针或引用绑定派生类的对象,
Bulk_quote bulk;
Quote * itemp = &bulk; //没有问题,动态类型是Bulk_quote
练习15.10:
ifstream传递给read函数,传递了一个派生类对象给基类,静态类型与动态类型不同
15.3 虚函数
复习:当我们使用基类的引用或指针调用一个虚成员函数会执行动态绑定
虚函数的调用可能在运行时才被解析
Quote base("o-3123-12dasd",50); //item绑定到Quote类型的对象,所以调用的是Quote::net_price的版本
print_total(cout,base,10); //调用的是Quote::net_price
Bulk_quote derived("asd-vd-qqwt-xx1",50,5,.19);//item绑定到Bulk_quote类型的对象,所以调用的是Bulk_quote::net_price的版本
print_total(cout,derived); //调用的是Bule_quote::net_price
//动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
//重点代码解析
bsde =derived; //把derived的Quote部分拷贝给base
base.net_price(20);//调用Quote::net_price
//具有普通类型(没有指针没有引用)的表达式调用虚函数时,编译器就会将调用的版本确定下来
final 和override说明符
struct B {
virtual void f1(int)const;
virtual void f2();
void f3();
};
struct D1 :B {
void f1(int)const override; //加了override可以清晰的认识到发现一些错误
void f2(int)override;//报错,B没有型如f2(int)函数
void f3()override; //错了f3不是虚函数
void f4()override; //报错,B没有名为f4的函数
};
//加了final,任何的覆盖该函数都将引发错误
struct D2 :B
{
//继承了f2()和f3(),覆盖f1(int)
void f1(int)const final; //不允许后续其他继承该类的派生类覆盖f1(int)
};
struct D3 :D2
{
void f2(); //正常,隐式的覆盖了间接基类的B继承下来的f2
void f1(int)const;//错误,直接报错
};
虚函数与默认实参:虚函数可以拥有默认实参
如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
如果派生类函数依赖不同的实参,则程序结果将于我们的预期不符
注意:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
回避虚函数的机制
//强行调用基类定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted =baseP->Quote::net_price(42);
一个派生类的虚函数调用它覆盖的基类的虚函数版本
通常基类的版本可以完成继承层次中所有类型都要做的共同任务,那么就使用这种作用域运算符
注意:如果没有使用作用域运算符,将会在运行时被解析为对派生版本自身的调用,导致无限递归
15.3节练习:
15.11:
virtual void debug()const
{
cout << "bookNo" << bookNo << "price" << price << endl;
}
15.12:
不知道,除非是这个函数被继承又不想被派生类使用,就可以这样做,override是必要的,防止自己多打了一些参数,导致运行的时候结果不一样。
别人的说法就是漂亮:有必要,override:在C+=11新标准中我们可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得我们的意图更加清晰明确地告诉编译器我们想要覆盖掉已存在的虚函数。如果定义了一个虚函数与基类中的名字相同但是形参列表不同,在不使用override关键字的时候这种函数定义是合法的,在使用了override关键字之后这种行为是非法的,编译器会提示出错。final:如果我们将某个函数定义成final,则不允许后续的派生类来覆盖这个函数,否则会报错。因此同时将一个成员函数声明成override和final能够使我们的意图更加清晰。
练习 15.13:
print打印自身的属性,派生类覆盖基类的版本,但是在调用基类的版本时没有加作用域,直接无限递归
如果不加作用域的话,会derived函数实现无限递归,程序直接报错。这是因为虚函数的机制,将会在运行时被解析为对派生版本自身的调用,导致无限递归,加了作用域就调用基类的版本
class base {
public:
virtual void print(ostream &os)
{
os << basename << endl;
}
private:
string basename;
};
class derived :public base
{
public:
void print(ostream &os)
{
base::print(os);
os << i << endl;
}
private:
int i=10;
};
int main()
{
base x;
x.print(cout);
derived b;
b.print(cout);
}
练习15.14
(a):调用基类的版本
(b):先调用基类的版本,在调用派生类的版本
©:调用bobj的name();
(d):调用bp2的name();
(e):调用base的print();
(f):动态绑定,先调用br2的print(),然后输出变量
抽象基类
//保存折扣值和购买量的类,派生类使用这些数据可以实现不同的价格策略
class Dise_quote:public Quote
{
Dis_quote()=default;
Dis_quote(const string&book,double price,size_t qty,double disc):Quote(book,price),quantity(qty),discount(disc){}
//纯虚函数
double net_price(size_t)const =0;
protected:
size_t quantity =0;//折扣适用的购买量
double discount =0.0; //表示折扣的小数值
};
//当同一书籍的销售量超过某个值启动折扣
//折扣的值是一个小于1的正的小数值,以此来降低正常销售价格
class Bulk_quote: public Dis_quote
{
Bulk_quote()=default;
Bulk_quote(const string&book,double price,size_t qty,double disc):Dis_quote(book,price,qty,disc){}
//覆盖基类的函数版本以实现一种新的折扣策略
double net_price(size_t)const override;
}
//Disc_quote声明了纯虚函数,而Bulk_quote将覆盖此函数
Disc_quote discounted; //报错,不能定义Disc_quote的对象,它是抽象基类,它有纯虚函数。抽象基类负责定义接口
Bulk_quote bulk; //Bulk中没有纯虚函数
关键概念:重构
Disc_quote类是重构的一个典型示例,重构负责重新设计类的体系以便操作或数据从一个类移动到另一个类
练习15.15
class Quote
{
public:
Quote() = default;
Quote(const string& book, double sales_price) :booNO(book),price(sales_price) {}
const string& isbn()const { return booNO; }
virtual double net_price(size_t n)const { return n * price; }
virtual ~Quote() = default;
private:
string booNO;
protected:
double price = 0.0;
};
class Dis_quote :public Quote
{
public:
Dis_quote() = default;
Dis_quote(const string & book, double price, size_t qty, double disc) :Quote(book, price), quantity(qty), discount(disc) {}
//纯虚函数
double net_price(size_t)const = 0;
protected:
size_t quantity = 0;//折扣适用的购买量
double discount = 0.0; //表示折扣的小数值
};
//当同一书籍的销售量超过某个值启动折扣
//折扣的值是一个小于1的正的小数值,以此来降低正常销售价格
class Bulk_quote : public Dis_quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string& book, double price, size_t qty, double disc) :Dis_quote(book, price, qty, disc) {}
//覆盖基类的函数版本以实现一种新的折扣策略
double net_price(size_t)const override;
};
练习15.16
class lim_discount :public Dis_quote
{
public:
lim_discount() = default;
lim_discount(const string &book,double price,size_t qty,double disc):Dis_quote(book,price,qty,disc){ }
//覆盖基类的函数版本以实现一种新的折扣策略
double net_price(size_t n)const override
{
if (n <= quantity)
{
return n * (1 - discount) * price;
}
else
{
return n * price;
}
}
};
练习15.17
严重性 代码 说明 项目 文件 行 禁止显示状态
错误(活动) E0322 不允许使用抽象类类型 “Dis_quote” 的对象: Quote D:\c++primer\第十五章\面向对象程序设计1\Quote\Quote\重新写Quote.cpp 99