《c++ primer笔记》第十五章 OOP

一、概述

​ 面向对象程序设计的核心思想:数据抽象、继承和动态绑定。数据抽象可以将类的接口与实现分离;继承可以定义相似的类型并对其相似关系建模;动态绑定可以在一定程度上忽略相似类型的区别,以统一的方式使用它们的对象。

继承

​ 继承是一种层次关系,处在根部的称为基类,负责定义在层次关系中所有类共同拥有的成员,其它的称为派生类,可以定义之间独特的成员。如果基类希望一些函数必须被派生类重新定义,这类函数就称为虚函数

​ 本章节以Quote类为例,表示书籍原价销售的对象,派生出一个Bulk_quote的类,表示可以打折销售的书籍。所有类包含两个成员函数。Isbn()返回书籍的编号,该操作不涉及派生类的特殊性,因此只定义在Quote类中;net_price(size_t),返回书籍的实际销售价格,每个类都应该有自己定义的版本。

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; // 加上override表示显式的指出该派生类使用这个成员函数改写基类的虚函数
}

动态绑定

​ 使用动态绑定可以用同一段代码分别处理QuoteBulk_quote对象。

// 当购买的数据和购买的数量已知,打印总费用
double print_total(ostream &os,const Quote &item, size_t n) {
	double ret = item.net_price(n); // 根据item所属对象调用对应的函数
    os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl;
    return ret;
}

虽然上面代码传入的对象类型式Quote,但是我们既能使用基类的对象调用该函数,也能使用派生类的对象调用。(原因后面解释)一般在使用基类的引用或指针调用一个虚函数时将发生动态绑定

二、定义基类和派生类

2.1定义基类

​ 完善Quote

class Quote {
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price) : bookNo(book), price(sales_price) {}
	std::string isbn() const { return bookNo; }
	vitrtual double net_price(std::size_t n) const { return n * price; } // 派生类负责改写使用不同的折扣计算方法
	virtual ~Quote() = default; // 对析构函数进行动态绑定

private:
	std::string bookNo; // 书籍的ISBN编号
protected:
	double price = 0.0; // 折扣
}

​ 一般基类需要将两种成员函数区分开来,一种是基类希望其派生类进行覆盖的函数,称为虚函数,当我们使用指针或引用虚函数时该调用将被动态绑定,根据引用或指针所绑定的对象类型不同,执行对应的成员函数。任何构造函数之外的非静态函数都可以是虚函数,关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义,当基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数

​ 当成员函数没有被声明为虚函数时,解析过程就会发生在编译时而非运行时。

访问控制与继承

​ 派生类的成员函数不一定有权访问从基类继承而来的成员,如果基类希望如此,可以使用受保护的访问运算符指明这样的成员。

2.2定义派生类

​ 完善Bulk_quoto

class Bulk_quoto : public 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; // 折扣
}

派生类中的虚函数

​ 如果派生类没有覆盖掉基类中的某个虚函数,那么该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。我们任然可以在派生类中重写虚函数的前面加上一个virtual,也可以像常用那样在最后加上一个override

派生类对象及派生类向基类的类型转换

​ 在一个对象中,派生类继承自基类的部分和派生类自定义的部分不一定时连续存储的。因为派生类对象中含有与基类对应的组成成分,所以可以把派生类的对象当成基类对象来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分上。

Quote item; // 基类对象
Bulk_quote bulk; // 派生类对象
Quote *p = &item; // p 指向Quote对象
p = &bulk; // p 指向bulk的Quote部分
Quote &r = bulk; // r绑定到bulk的Quote部分

上面这种转换称为派生类到基类的类型转换,编译器会隐式地执行派生类到基类的转换。

派生类构造函数

​ 派生类自己不能直接去初始化从基类继承过来的成员,必须使用基类的构造函数来初始化它的基类部分。构造函数的初始化顺序首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

继承与静态成员

​ 如果基类定义了一个静态成员,则在整个继承体系种只存在该成员的唯一定义。不论从基类中派生出多少个派生类,对于每个静态成员来说都存在唯一的实例。静态成员遵循通用的访问控制规则。

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();
}

防止继承发生

​ 如果对于一些类我们不希望有其它类继承它,可以通过关键字final实现

class NoDerived final {}; // 不能作为基类

2.3类型转换与继承

​ 可以将基类的指针或引用绑定在派生类的对象上,意味着当我们使用基类的引用或指针时,并不清楚该引用或指针所绑定对象的真实类型。

静态类型与动态类型

​ 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型,直到运行时才可知。

double ret = item.net_price(n);

调用之前定义的打印函数,item的静态类型是Quote&,它的动态类型依赖于item绑定的实参,所以如果我们传递一个Bulk_quote对象给该函数,就会造成item的讲台类型将与它的动态类型不一致。当然,如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

不存在从基类向派生类的隐式类型转换

​ 因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换

Bulk_quote bulk;
Quote *itemP = &bulk; // 正确,动态类型是Bulk_quote
Bulk_quote *bulkP = itemP; // 错误,不能将基类转换成派生类

对象之间不存在类型转换

派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。

三、虚函数

​ 如果一个函数不被使用可以不用进行定义,但是虚函数必须被定义,因为编译器无法保证用户调用的是哪个虚函数。

派生类中的虚函数

​ 一旦某个函数被声明成虚函数,则在所有派生类中都是虚函数。一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。派生类中虚函数的返回类型也必须与基类函数匹配,不过当类的虚函数返回类型是类本身的指针或引用时,上诉规则无效。

final和override说明符

​ 当我们在重写派生类的虚函数时,如果给出函数名相等但是形参列表不同,编译器会认为该函数与基类的虚函数都是独立的,不会报错,但是实际上违背了虚函数定义的规则。通过使用关键字override显式说明这是派生类中的虚函数,当出现形参列表不同时,编译器会进行提示。override只对虚函数有效,当派生类对基类的普通函数进行覆盖时也会发生错误。

final关键字前面在当我们不想一个类被继承时可以在声明类时加在末尾,同样的,当我们希望一个类的某个函数不能被派生类继承式,也可以使用。

回避虚函数的机制

​ 如果我们希望不对虚函数的调用不进行动态绑定,而是强迫执行虚函数的某个特定版本,可以通过使用作用域运算符实现这一目的。

double undiscounted = baseP->Quote::net_price(42);

回避机制通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时会进行使用。

四、抽象基类

纯虚函数

​ 继续书店整个例子,如果我们希望扩展程序让其支持几种不同的折扣策略,可以定义Disc_quote类来支持,该类负责保存购买量的值和折扣值。显然该类与任何的折扣策略都无关,所以该类继承于基类Quote中的net_price函数没有实际含义。虽然我们可以在Disc_quote不定义新的net_price,但是用户可能会创建一个Disc_quote对象被为其提供购买量和折扣值,如果将该对象传给一个print_total打印结果,则程序将调用Quote版本的net_price,显然就没有使用到Disc_quote提供的折扣值。

​ 针对上诉问题,核心就是我们不希望用户创建一个Disc_quote对象。可以将net_price定义定义成纯虚函数,一个纯虚函数无须定义,只需要在函数体的位置书写=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 quntity = 0; // 折扣适用购买量
	double discount = 0.0; // 折扣
}

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

​ 抽象基类负责定义接口,而后续的其他类可以覆盖该接口,不能创建一个抽象基类的对象。所以在我们创建了类Disc_quote后,如果定义相关对象Disc_quote dis将会发生错误。Disc_quote的派生类必须给出自己对net_price定义,否则它也是抽象基类

派生类构造函数只初始化它的直接基类

​ 之前写的例子中Bulk_quote是直接继承于Disc_quote,现在我们让它继承于上面定义的Disc_quote。

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) { }
	duoble net_price(std::size_t) const override;
}

虽然Bulk_quote没有自己的数据成员,但是它也提供了接受4个参数的构造函数,这是由于前面提到的每个类各自控制其对象的初始化过程。

五、访问控制与继承

​ 每个类除了控制自己的成员初始化过程,还分别控制着其成员对于派生类来说是否可访问。

受保护的成员

​ 一个类使用protected关键字声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员,派生类对于一个基类对象中的受保护成员没有任何访问特权 (看下面例子)
class Base {
protected:
	int prot_mem;
};
class Sneaky : public Base {
	friend void clobber(S neaky&); // 能够访问Sneaky::prot_mem
	friend void clobber(Base&); // 不能访问Base::prot_mem
	int j; // j默认是private
};
void clobber(Sneaky &s) { // 正确
	s.j = s.prot_mem = 0;
}
void clobber(Base &b) { // 错误
	b.prot_mem = 0;
}

公有、私有和受保护继承

class Base {
public:
	void pub_mem();
protected:
	int prot_mem;
private:
	char priv_mem;
};
struct Pub_Derv : public Base {
	int f() { return prot_mem; } // 派生类可以访问proteted成员
	char g() { return priv_mem; } // 错误 私有成员无法访问
}
struct Priv_Derv : private Base {
	int f1() const { return prot_mem; }
    char g() { return priv_mem; } // 错误 私有成员无法访问
}

Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem(); 
d2.pub_mem(); // 错误,pub_mem在派生类中是private的

从上面代码可以看出,派生类访问说明符对于派生类的成员能否访问其直接基类的成员没有影响,对基类成员的访问权限只与基类中的访问说明符有关。

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

​ 转换取决于使用该转换的代码以及派生访问说明符,假定D继承与B:

  • 当D公有继承B,用户代码可以使用派生类可以向基类转换,其它继承都不行
  • 不论D怎么继承于B,D的成员函数友元都能使用派生类向基类的转换
  • 如果D公有或受保护的继承于B,则D的派生类的成员和友元可以使用D向B的类型转换,反之不行

友元与继承

​ 友元关系不能被传递,同样的友元继承也如此,基类的友元在访问派生类成员不具有特殊性,派生类的友元也不能随机访问基类的成员。

class Base {
	friend class Pal;
}
class Pal {
public:
	int f(Base b) { return b.prot_mem; }
	int f2(Sneaky s) { return s.j; } // 错误成员j是Base另一个派生类Sneaky定义的,Pal无法进行访问,但是可以访问s.prot_mem
}

// 友元的派生类也不能随机访问基类的成员
class D2 : public Pal {
public:
    int mem(Base b) { return b.prot_mem; } // 错误
}

改变个别成员的可访问性

class Base {
public:
	std::size_t size() const { return n; }
protected:
	std::size_t n;
}

class Derived : private Base {
public:
	using Base::size;
protected:
	using Base::n;
}

Derived私有继承于Base,所以继承的成员size()和n都是私有成员,通过关键字using声明改变了这些成员的可访问性,之后Derived的用户可以使用size成员,它的派生类则能使用n。

六、继承中类作用域

​ 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,之前谈到静态类型于动态类型可能不一致,但是我们能使用哪些成员仍然是由静态类型决定的。

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*,里面没有discount_policy成员

和其他作用域一样,派生类中如果重定义基类或间接基类中的名字,此时内层的名字会隐藏基类的名字。在这种情况下如果我们想要使用被隐藏基类的名字,可以使用作用域运算符显式使用Base::mem

虚函数与作用域

​ 假如基类与派生类的虚函数接受的实参不同,则无法通过基类的引用或指针调用派生类的虚函数

class Base {
public:
	virtual int fcn();
};
class D1 : public Base {
public:
	int fcn(int); // 隐藏了基类的fcn,但是本身本身虚函数
	virtual void f2(); // D1定义了一个新的虚函数
};
class D2 : public D1 {
public:
	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,因为D1没有fcn()
bp3->fcn(); // 调用D2::fcn

D1 *dlp = &dlobj; D2 *d2p = &d2obj;
bp2->f2(); // 错误,Base没有名为f2的成员
d1p->f2(); // 调用D1::f2()
d2p->f2(); // 调用D2::f2()

七、构造函数与拷贝控制

7.1虚析构函数

​ 当delete一个动态分配的对象的指针将执行析构函数,如果该指针指向继承体系中的某个类型,则可能出现指针的静态类型与被删除对象的动态类型不符的情况。因此在使用虚函数时,也要把相应的析构函数声明为虚析构函数。

class Quote {
public:
	virtual ~Quote() = default;
}

继承也会把虚析构函数传递给派生类。如果基类的析构函数不是虚函数,delete一个指向派生类对象的基类指针将产生未定义的行为。一个类定义了析构函数,即使通过=default的形式使用了合成的版本,编译器也不会为整个类合成移动操作。

7.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的被删除的拷贝构造函数

移动操作与继承

​ 大多数基类都会定义一个虚析构函数,默认情况下基类通常不含有合成的移动操作,因为基类缺少移动操作会阻止派生类拥有之间的合成移动操作,所以当需要执行移动操作时应该首先在基类中进行定义。一旦基类定义了之间的移动操作,相应的也必须显式地定义拷贝操作。

class Quote {
public:
	Quote() = default; // 默认初始化
	Quote(const Quote&) = default; // 拷贝
	Quote(Quote&&) = default; // 移动拷贝
	Quote& operator = (const Quote&) = default; // 拷贝赋值
	Quote& operator = (Quote&&) = default;  // 移动赋值
	virtual ~Quote() = default;
};

7.3派生类的拷贝控制成员

​ 派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源,对象的成员是被隐式销毁的,派生类对象的基类部分也是自动销毁的。

定义派生类的拷贝或移动构造函数

​ 当为派生类定义拷贝或移动构造函数时,使用对应的基类构造函数初始化对象的基类部分

class Base { /*.....*/};
class D : public Base {
public:
	D(const D& d) : Base(d) {}; // 拷贝基类成员,如果没有显式初始化,那么将会使用基类的默认初始化,造成新构建的对象d有着从其它D对象拷贝过来的成员,而基类成员确实默认值
	D(D&& d) : Base(std::move(d)) {}; // 移动基类成员
}

7.4继承的构造函数

​ 一个类只能继承其直接基类的构造函数,不能继承默认、拷贝和移动构造函数,如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

class Bulk_quote : public Disc_quote {
public:
	using Disc_quote::Disc_quote; //继承Disc_quote的构造函数 对于派生类自己的成员将会默认初始化
    // 等同于下列写法
    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; 
};

继承的构造函数的特点

using声明不会改变构造函数的访问级别,并且也无法通过using指定explicit或者constexpr,只能以继承的方式。当一个基类构造函数含有默认实参,这些实参并不会被继承,派生类将获得多个继承的构造函数,每个构造函数分别省略掉一个含有默认实参的形参。

​ 一般派生类会继承所有这些构造函数,有两个例外,第一个是派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本;第二个是默认、拷贝和移动构造函数不会被继承,这些构造函数按照正常规则被合成。

八、容器与继承

​ 使用容器存放继承体系中的对象时,必须采用简介存储的方式,因为容器中不允许保存不同类型的元素。

vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));

上面代码中我们定义了一个存储Quote对象类型的容器,在第三行代码往容器中添加了一个Bulk_quote对象,但是只有Quote部分能被拷贝给basket,派生类部分将被忽略。

在容器中放置智能指针而非对象

​ 所以如果要往容器中存放具有继承关系的对象,实际应该存放基类的指针。

vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848", 50, 10, .25));
cout << basket.back()->net_price(15) << endl;

在实际存储的过程中,派生类的智能指针会被转换成基类的智能指针,上面代码中,make_shared<Bulk_quote>返回一个shared_ptr<Bulk_quote>对象,调用push_back时该对象被转换成shared_ptr<Quote>

8.1Basket类

​ 进行面向对象编程必须使用指针和引用,但是这样会增加程序的复杂性,所以需要定义一些辅助的类来处理这种情况。

class Basket {
public:
	void add_item(const std::shared_ptr<Quote> &sale) 
		{ items.insert(sale);}
	double total_receipt(std::ostream&) const;
private:
	static bool compare(const std::shared_ptr<Quote> &lhs, const std::shared_ptr<Quote> &rhs) { return lhs->isbn() < rhs->isbn() }
    // multiset保存多个报价,同一本书的多条交易纪录
	std::multiset<std::shared_ptr<Quote>, decltype(compare)*> items{compare};
};

定义成员

add_item接受一个指向动态分配的Quoteshared_ptr,然后将这个shared_ptr放置在multiset中。total_receipt负责将购物篮的内容逐项打印成清单,然后返回总价格。

double Basket::total_receipt(ostream &os) const {
	double sum = 0.0; // 总价格
	//upper_bound返回一个迭代器,指向这批元素的尾后位置(跳过相同关键字的元素)
	for(auto iter = items.cbegin(); iter != items.cend(); iter = items.upper_bound(*iter)) {
		sum += print_total(os, **iter, items.count(*iter));
	}
	os << "Total Sale: " << sum << endl;
	return sum;
}

隐藏指针

​ 目前Basket的用户必须处理动态内存,因为add_item需要接受一个shared_ptr参数。

Basket bsk;
bsk.add_item(make_shared<Quote>("123",45));
bsk.add_item(make_shared<Quote>("345",45, 3, .15));

重新定义add_item,使得它接受一个Quote对象而非shared_ptr。这样由add_item负责处理内存分配。

void add_item(const Quote& sale); // 拷贝给定的对象
void add_item(Quote &sale); // 移动给定的对象

上面定义的问题还有就是add_item不知道要分配的类型,当进行内存分配时,它将携带(或移动)它的sale参数,在某处可能存在new Quote(sale),这会造成如果我们传递的是一个Bulk_quote对象,然而这条表达式将分配一个Quote类型的对象并只拷贝saleQuote部分。

模拟虚拷贝

​ 解决上面的问题的方式是给Quote类添加一个虚函数,该函数将申请一份当前对象的拷贝

class Quote {
public:
	virtual Quote *clone() const & { return new Quote(*this); }
	virtual QUote *clone() && { return new Quote(std::move(*this));}
    //...//
}
class BUlk_quote : public Quote {
    Bulk_quote *clone() const & { return new BUlk_quote(*this); }
    Bulk_quote *clone() && {return new Bulk_quote(std::move(*this)); }
}
class Basket {
public:
	void add_item(const Quote &sale) { items.insert(std::sahred_ptr<Quote>(sale.clone())); }
	void add_item(Quote &&sale) { items.insert(std::sahred_ptr<Quote>(std::move(sale).clone())); }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

madkeyboard

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值