第十五章 面向对象程序设计

发布于:2024-08-15 ⋅ 阅读:(43) ⋅ 点赞:(0)

15.1 OOP:概述

  1. 面向对象程序设计的核心思想:数据抽象、继承和动态绑定。
    数据抽象:可以将类的接口与实现分离。
    继承:可以定义相似的类型并对其相似关系建模。
    动态绑定(运行时绑定):可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
  2. 通过继承联系在一起的类构成一种层次关系:
    基类(base class):定义共同拥有的成员。
    派生类(derived class):定义特有的成员。
    虚函数(virtual function):基类希望派生类各自定义自己合适的版本。
class Quote{
public:
	std::string isbn() const;
	virtual double net_price(std::size_t n) const;
};

//派生类必须通过使用类派生列表明确指出基类
class Bulk_quote:public Quote {
public:
	double net_price(std::size_t) const override;
};
  1. 在C++中,当使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
//计算并打印销售给定数量的某种书籍所得的费用
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::isbn
	   << "#sold: " << n << " total due: " << ret << endl;
	return ret;
}
//basic的类型是Quote;bulk的类型是Bulk_quote
print_total(cout,basic,20);
print_total(cout,bulk,20);

15.2 定义基类和派生类

15.2.1 定义基类

  1. 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
  2. 任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
  3. protected:基类希望它的派生类有权访问该成员,同时禁止其他用户访问。
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
		{ return n * price; }
	virtual ~Quote() = default;
private:
	std::string bookNo;
protected:
	double price = 0.0; //代表普通状态下不打折的价格
}

15.2.2 定义派生类

  1. 派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
class Bulk_quote : public Quote { //Bulk_quote继承自Quote
public:
	Bulk_quote() = default;
	Bulk_quote(const std::string&, double, std::size_t, double);
	//覆盖基类的函数版本以实现基于大量购买的折扣政策
	double net_price(std::size_t) const override;
private:
	std::size_t min_qty = 0; //适用折扣政策的最低购买量
	double discount = 0.0; //以小数表示的折扣额
};
  1. C++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在const成员函数的const关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字override。
  2. 在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
  3. 每个类控制它自己的成员初始化过程。
  4. 派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。
  5. 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc):
	Quote(book, p), min_qty(qty), discount(disc){ }
  1. 必须明确一点:每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。因此,派生类对象不能直接初始化基类的成员。
  2. 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
class Base {
public:
	static void statmem();
};
class Derived : public Base {
	void f(const Derived&);
};

void Derived::f(const Derived &derived_obj)
{
	Base::statmem();
	Derived::statmem();
	derived_obj.statmem();
	statmem(); //通过this对象访问
}
  1. 如果想将某个类用作基类,则该类必须已经定义而非仅仅声明。
  2. 一个类不能派生它本身。
  3. 一个类的基类,同时也可以是一个派生类。
class Base { /* ... */ };
class D1 : public Base { /* ... */ };
class D2 : public D1 { /* ... */ };
  1. 防止继承发生,在类名后跟一个关键字final。(C++11)
class NoDerived final { /* */}; //不能作为基类
class Base { /* */}; 
//Last是final的;我们不能继承Last
class Last final : Base { /* */}; 
class Bad : NoDerived { /* */};  	//错误
class Bad2 : Last { /* */};  		//错误

15.2.3 类型转换与继承

  1. 理解基类和派生类之间的类型转换是理解C++面向对象编程的关键所在。
  2. 使用基类的引用(或指针)时,实际上编译器并不清楚所绑定对象的真实类型。
    静态类型(static type):编译时已知。
    动态类型(dynamic type):运行时才可知。
  3. 可以将一个派生类对象的指针存储在一个基类的智能指针内。
  4. 基类的指针或引用的静态类型可能与其动态类型不一致。如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
  5. 因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
  6. 当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
  7. 存在继承关系的类型之间的转换规则:
    从派生类向基类的类型转换只对指针或引用类型有效。
    基类向派生类不存在隐式类型转换。
    和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。
Quote base;
Bulk_quote* bulkP = &base; //错误
Bulk_quote& bulkRef = base; //错误

Bulk_quote bulk;
Quote *itemP = &bulk;	//正确
//可以通过dynamic_cast或static_cast进行转换
Bulk_quote *bulkP = itemp;//错误:编译器只能通过检验静态类型来推断
// 在对象间不存在类型转换
Bulk_quote bulk; 		//派生类对象
Quote item(bulk); 	//使用Quote::Quote(const Quote&)构造函数
item = bulk; 		//代用Quote::operator=(const Quote&)

15.3 虚函数

  1. 动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
  2. 引用或指针的静态类型与动态类型不同这一事实正是C++支持多态性的根本所在。
  3. 当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
Quote base("0-201-1", 50);
print_total(cout, base, 10); 
Bulk_quote derived("0-201-1", 50, 5, .19);
print_total(cout, derived, 10); 

base = derived;		//把derived的Quote部分拷贝给base
base.net_price(20); 	//调用Quote::net_price
  1. 一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。
  2. 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
  3. 如果使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。(C++11)
  4. 如果已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误。
struct B {
	virtual void f1(int) const;
	virtual void f2();
	void f3()
};
struct D1 : B {
	void f1(int) const override; 	//正确
	void f2(int) override; 		//错误
	void f3() override; 		//错误
	void f4() override; 		//错误
};

struct D2 : B {
	//从B继承f2()和f3(),覆盖f1(int)
	void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};
struct D3 : D2 {
	void f2(); 		//正确
	void f1(int) const; 	//错误,在D2中已声明为final
};
  1. 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
  2. 通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
//强制调用基类中定义的函数版本而不管baseP的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);
//该调用将在编译时完成解析
  1. 如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

15.4 抽象基类

  1. 在声明语句的分号之前书写=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 = 0;
protected:
	std::size_t quantity = 0; //折扣适用的购买量
	double discount = 0.0; //折扣
};
  1. 可以为纯虚函数提供定义,但必须定义在类的外部。
  2. 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。不能(直接)创建一个抽象基类的对象。
//Disc_quote声明了纯虚函数,而Bulk_quote将覆盖该函数
Disc_quote discounted; //错误
Bulk_quote bulk; //正确
  1. 派生类构造函数只初始化它的直接基类。
class Bulk_quote : public Disc_quote {
public:
	Bulk_quote() = default;
	Bulk_quote(const std::string& book, double price, std::size_t qty, double disc):
		Disc_quote(book, price, qty, disc) { }
	//覆盖基类中的函数版本以实现一种新的折扣策略
	double net_price(std::size_t) const override;
};

15.5 访问控制与继承

  1. 控制其成员对于派生类来说是否可以访问。如 protected:
    和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
    和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
    派生类的成员或友元只能访问派生类对象中的基类部分的受保护成员。对于普通的基类对象中的成员不具有特殊的访问权限。
class Base {
protected:
	int prot_mem; //protected成员
};
class Sneaky : public Base {
	friend void clobber(Sneaky&); 	//能访问Sneaky::prot_mem
	friend void clobber(Base&); 	//不能访问Base::prot_mem
	int j;
};
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } 	//正确
void clobber(Base &b) { b.prot_mem = 0; } 		//错误
  1. 某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。
class Base {
public:
	void pub_mem(); 
protected:
	int prot_mem;
private:
	char priv_mem;
};
struct Pub_Derv : public Base {
	int f() { return prot_mem; } //正确
	int g() { return priv_mem; } //错误
};
struct Priv_Derv : private Base {//private不影响派生类的访问权限
	int f1() const { return prot_mem; }
}
//派生访问说明符的目的是控制派生类用户对于基类成语的访问权限
Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem();
d2.pub_mem();//错误:pub_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; } //错误
}
  1. 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。
  2. 派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限,还可以控制继承自派生类的新类的访问权限。
  3. 对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
  4. 不能继承友元关系;每个类负责控制各自成员的访问权限。
class Base {
	//添加friend声明,其他成员与之前的版本一致
	friend class Pal; //Pal在访问Base的派生类时不具有特殊性
};
class Pal {
public:
	int f(Base b) { return b.prot_mem:} //正确
	int f2(Sneaky S) { return s.j; }; //错误:Paul不是Sneaky的友元
	//对基类的访问权限由基类本身控制
	//即使对于派生类的基类部分也是如此
	int f3(Sneaky s) { return s.prot_mem; } //正确:虽然看上去有些奇怪
	//Pal能够访问Base的成员,这种访问包括了Base对象内嵌在其派生类对象中的情况
};

//D2对Base的protected和private成员不具有特殊的访问能力
class D2 : public Pal {
public:
	int mem(Base b) { return b.prot_mem; } //错误:友元关系不能继承
};
  1. 有时需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。
  2. 派生类只能为那些它可以访问的名字提供using声明。
class Base {
public:
	std::size_t size() const { return n; }
protected:
	std::size_t n;
};
class Derived : private Base { //注意:private继承
public:
	//保持对象尺寸相关的成员的访问级别
	using Base::size;
protected:
	using Base::n;
};
  1. 默认情况下,使用class关键字定义的派生类是私有继承的,而使用struct关键字定义的派生类是公有继承的。
class Base { /* ... */ };
struct D1 : Base { /* ... */ }; 	//默认public继承
class D2 : Base { /* ... */ }; 		//默认private继承
  1. 一个私有派生的类最好显式地将private声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。

15.6 继承中的类作用域

  1. 如果一个名字在派生类的作用域内无法解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
  2. 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
class Disc_quote : public Quote {
public:
	std::pair<size_t, double> discount_policy() const
		{ return { quantity, discount }; }
	//其他成员与之前版本一致
};

Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; 	//静态类型与动态类型一致
Quote *itemp = &bulk; 		//静态类型与动态类型不一致
bulkP->discount_policy(); 		//正确
itemP->discount_policy(); 		//错误:itemP的类型是Quote*
  1. 派生类的成员将隐藏同名的基类成员。
struct Base {
	Base() : mem(0) { }
protected:
	int mem;
};

struct Derived : Base {
	Derived(int i) : mem(i) { } 	//用i初始化Derived::mem
				//Base::mem进行默认初始化
	int get_mem() { return mem; } //返回Derived::mem
protected:
	int mem; //隐藏基类中的mem
};
  1. 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
  2. 如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员,即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。
// 同名函数不会重载,只会隐藏
struct Base {
	int memfcn();
};
struct Derived : Base {
	int memfcn(int); //隐藏基类的memfcn
};
Derived d; 
Base b;
b.memfcn(); 	//调用Base::memfcn
d.memfcn(10); 	//调用Derived::memfcn
d.memfcn(); 	//错误:被隐藏了无法调用
d.Base::memfcn(); //正确
  1. 虚函数的作用域。
class Base {
public: 
	virtual int fcn();
};
class D1 : public Base {
public:
	//隐藏基类的fcn,这个fcn不是虚函数,D1继承了Baes::fcn()的定义
	int fcn(int); 		//形参列表与Base中的fcn不一致
	virtual void f2(); 	//是一个新的虚函数,在Base中不存在
};
class D2 : public D1 {
	int fcn(int); 	//是一个非虚函数,隐藏了D1::fcn(int)
	int fcn(); 	//覆盖了Base的虚函数fcn
	void f2(); 	//覆盖了D1的虚函数f2
};
Base bobj; 
D1 d1obj; 
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //虚调用,将在运行时调用Base::fcn
bp2->fcn(); //虚调用,将在运行时调用Base::fcn
bp3->fcn(); //虚调用,将在运行时调用D2::fcn

D1 *d1p = &d1obj; 
D2 *d2p = &d2obj;
bp2->f2(); //错误:Base没有名为f2的成员
d1p->f2(); //虚调用,将在运行时调用D1::f2()
d2p->f2(); //虚调用,将在运行时调用D2::f2()

Base *p1 = &d2obj; 
D1 *p2 = &d2obj; 
D2 *p3 = &d2obj;
p1->fcn(42); //错误:Base中没有一个接受一个int的fcn
p2->fcn(42); //静态绑定,调用D1::fcn(int)
p3->fcn(42); //静态绑定,调用D2::fcn(int)

15.7 构造函数与拷贝控制

15.7.1 虚析构函数

  1. 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
  2. 通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。
class Quote {
public:
	//删除一个指向派生类对象的基类指针,则需要虚析构函数
	virtual ~Quote() = default; //动态绑定析构函数
	//析构函数的属性会被继承,Quote的派生类的析构函数都将是虚函数
};

//基类的析构函数是虚函数,delete基类指针将运行正确的析构函数
Quote *itemP = new Quote; 	//静态类型与动态类型一致
delete itemP; 			//调用Quote的析构函数
itemP = new Bulk_quote; 		//静态类型与动态类型不一致
delete itemP; 			//调用Bulk_quote的析构函数
  1. 虚析构函数将阻止合成移动操作。

15.7.2 合成拷贝控制与继承

  1. 实际编程中,如果在基类中没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作。
  2. 如果定义了拷贝构造、赋值运算符或析构函数,则编译器不会合成移动构造和移动运算符。
class B {
public:
	B();
	B(const B&) = delete;
	//其他成员,不含有移动构造函数
};
class D : public B {
	//没有声明任何构造函数
};
D d; 		//正确:D的合成默认构造函数使用B的默认构造函数
D d2(d); 		//错误:D的合成拷贝构造函数是被删除的
D d3(std::move(d)); //错误:隐式地使用D的被删除的拷贝构造函数
  1. 如果定义了一个移动构造函数/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符被定义为删除的。
class Quote {
public:
	Quote() = default; 			//对成员依次进行默认初始化
	Quote(const Quote&) = default; 	//对成员依次拷贝
	Quote(Quote&&) = default;		 //对成员依次拷贝
	Quote& operator=(const Quote&) = default; //拷贝赋值
	Quote& operator=(Quote&&) = default; //移动赋值
	virtual ~Quote() = default; 
	//其他成员与之前的版本一致
};

15.7.3 派生类的拷贝控制成员

  1. 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
  2. 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
class Base { /* ... */};
class D : public Base {
public:
	D(const D& d): Base(d) //拷贝基类成员
	/*D的成员的初始值*/{/*...*/}
	D(D&& d):Base(std::move(d))//移动基类成员
	/*D的成员的初始值*/{/*...*/}
};
  1. 无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用它们。
//Base::operator=(const Base&)不会自动被调用
D &D::operator=(const D &rhs){
	Base::operator=(rhs); //为基类部分赋值
	//按照过去的方式为派生类的成员赋值
	//酌情处理自赋值及释放已有资源等情况
	return *this;
}
  1. 派生类析构函数只负责销毁由派生类自己分配的资源。
class D : public Base {
public:
	//Base::~Base被自动调用执行
	~D(){ /*该处由用户定义清除派生类成员的操作*/}
};
  1. 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

15.7.4 继承的构造函数

  1. 派生类能够重用其直接基类定义的构造函数。
  2. 一个类只初始化它的直接基类,也只继承其直接基类的构造函数。
  3. 类不能继承默认、拷贝和移动构造函数。
  4. 派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。
  5. 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。
  6. 当一个基类构造函数含有默认实参时,这些实参并不会被继承。
  7. 如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。
class Bulk_quote : public Disc_quote {
public:
	using Disc_quote::Disc_quote; //继承Disc_quote的构造函数
	double net_price(std::size_t) const;
};
//等价于
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc):
	Disc_quote(book, price, qty, disc) { }

15.8 容器与继承

  1. 当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
  2. 使用容器存放继承体系中的对象时,通常采用间接存储的方式,实际上存放的通常是基类的指针(更好的选择是智能指针)。
vector<Quote> basket;
basket.push_back(Quote("0-201-1",50);
//正确:但是只能把对象的Quote部分拷贝给basket
basket.push_back(Bulk_quote("0-201-8",50,10,.25));
//调用Quote定义的版本,打印750,即15*$50
cout<<basket.back().net_price(15)<<endl;
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-1",50));
basket.push_back(make_shared<Bulk_quote>("0-201-8",50,10,.25));
//调用Bulk_quote定义的版本;打印562.5
cout<<basket.back()->net_price(15)<<endl;

网站公告

今日签到

点亮在社区的每一天
去签到