15.1 OOP:概述
- 面向对象程序设计的核心思想:数据抽象、继承和动态绑定。
数据抽象:可以将类的接口与实现分离。
继承:可以定义相似的类型并对其相似关系建模。
动态绑定(运行时绑定):可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
- 通过继承联系在一起的类构成一种层次关系:
基类(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;
};
- 在C++中,当使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
double print_total(ostream &os,const Quote &item, size_t n)
{
double ret = item.net_price(n);
os << "ISBN:" << item.isbn()
<< "#sold: " << n << " total due: " << ret << endl;
return ret;
}
print_total(cout,basic,20);
print_total(cout,bulk,20);
15.2 定义基类和派生类
15.2.1 定义基类
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
- 任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
- 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 定义派生类
- 派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
class Bulk_quote : 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;
};
- C++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在const成员函数的const关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字override。
- 在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
- 每个类控制它自己的成员初始化过程。
- 派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。
- 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc){ }
- 必须明确一点:每个类负责定义各自的接口,要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。因此,派生类对象不能直接初始化基类的成员。
- 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
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();
}
- 如果想将某个类用作基类,则该类必须已经定义而非仅仅声明。
- 一个类不能派生它本身。
- 一个类的基类,同时也可以是一个派生类。
class Base { };
class D1 : public Base { };
class D2 : public D1 { };
- 防止继承发生,在类名后跟一个关键字final。(C++11)
class NoDerived final { };
class Base { };
class Last final : Base { };
class Bad : NoDerived { };
class Bad2 : Last { };
15.2.3 类型转换与继承
- 理解基类和派生类之间的类型转换是理解C++面向对象编程的关键所在。
- 使用基类的引用(或指针)时,实际上编译器并不清楚所绑定对象的真实类型。
静态类型(static type):编译时已知。
动态类型(dynamic type):运行时才可知。
- 可以将一个派生类对象的指针存储在一个基类的智能指针内。
- 基类的指针或引用的静态类型可能与其动态类型不一致。如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
- 因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。
- 当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
- 存在继承关系的类型之间的转换规则:
从派生类向基类的类型转换只对指针或引用类型有效。
基类向派生类不存在隐式类型转换。
和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。
Quote base;
Bulk_quote* bulkP = &base;
Bulk_quote& bulkRef = base;
Bulk_quote bulk;
Quote *itemP = &bulk;
Bulk_quote *bulkP = itemp;
Bulk_quote bulk;
Quote item(bulk);
item = bulk;
15.3 虚函数
- 动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
- 引用或指针的静态类型与动态类型不同这一事实正是C++支持多态性的根本所在。
- 当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
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;
base.net_price(20);
- 一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。
- 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
- 如果使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。(C++11)
- 如果已经把函数定义成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 {
void f1(int) const final;
};
struct D3 : D2 {
void f2();
void f1(int) const;
};
- 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
- 通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
double undiscounted = baseP->Quote::net_price(42);
- 如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
15.4 抽象基类
- 在声明语句的分号之前书写=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;
};
- 可以为纯虚函数提供定义,但必须定义在类的外部。
- 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。不能(直接)创建一个抽象基类的对象。
Disc_quote discounted;
Bulk_quote bulk;
- 派生类构造函数只初始化它的直接基类。
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 访问控制与继承
- 控制其成员对于派生类来说是否可以访问。如 protected:
和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
派生类的成员或友元只能访问派生类对象中的基类部分的受保护成员。对于普通的基类对象中的成员不具有特殊的访问权限。
class Base {
protected:
int prot_mem;
};
class Sneaky : public Base {
friend void clobber(Sneaky&);
friend void clobber(Base&);
int j;
};
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; }
int g() { return priv_mem; }
};
struct Priv_Derv : private Base {
int f1() const { return prot_mem; }
}
Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem();
d2.pub_mem();
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; }
}
- 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。
- 派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限,还可以控制继承自派生类的新类的访问权限。
- 对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
- 不能继承友元关系;每个类负责控制各自成员的访问权限。
class Base {
friend class Pal;
};
class Pal {
public:
int f(Base b) { return b.prot_mem:}
int f2(Sneaky S) { return s.j; };
int f3(Sneaky s) { return s.prot_mem; }
};
class D2 : public Pal {
public:
int mem(Base b) { return b.prot_mem; }
};
- 有时需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的。
- 派生类只能为那些它可以访问的名字提供using声明。
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;
};
- 默认情况下,使用class关键字定义的派生类是私有继承的,而使用struct关键字定义的派生类是公有继承的。
class Base { };
struct D1 : Base { };
class D2 : Base { };
- 一个私有派生的类最好显式地将private声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。
15.6 继承中的类作用域
- 如果一个名字在派生类的作用域内无法解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
- 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。
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();
- 派生类的成员将隐藏同名的基类成员。
struct Base {
Base() : mem(0) { }
protected:
int mem;
};
struct Derived : Base {
Derived(int i) : mem(i) { }
int get_mem() { return mem; }
protected:
int mem;
};
- 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
- 如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员,即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。
struct Base {
int memfcn();
};
struct Derived : Base {
int memfcn(int);
};
Derived d;
Base b;
b.memfcn();
d.memfcn(10);
d.memfcn();
d.Base::memfcn();
- 虚函数的作用域。
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
int fcn(int);
virtual void f2();
};
class D2 : public D1 {
int fcn(int);
int fcn();
void f2();
};
Base bobj;
D1 d1obj;
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn();
bp2->fcn();
bp3->fcn();
D1 *d1p = &d1obj;
D2 *d2p = &d2obj;
bp2->f2();
d1p->f2();
d2p->f2();
Base *p1 = &d2obj;
D1 *p2 = &d2obj;
D2 *p3 = &d2obj;
p1->fcn(42);
p2->fcn(42);
p3->fcn(42);
15.7 构造函数与拷贝控制
15.7.1 虚析构函数
- 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
- 通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。
class Quote {
public:
virtual ~Quote() = default;
};
Quote *itemP = new Quote;
delete itemP;
itemP = new Bulk_quote;
delete itemP;
- 虚析构函数将阻止合成移动操作。
15.7.2 合成拷贝控制与继承
- 实际编程中,如果在基类中没有默认、拷贝或移动构造函数,则一般情况下派生类也不会定义相应的操作。
- 如果定义了拷贝构造、赋值运算符或析构函数,则编译器不会合成移动构造和移动运算符。
class B {
public:
B();
B(const B&) = delete;
};
class D : public B {
};
D d;
D d2(d);
D d3(std::move(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;
};
15.7.3 派生类的拷贝控制成员
- 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
- 在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
class Base { };
class D : public Base {
public:
D(const D& d): Base(d)
{}
D(D&& d):Base(std::move(d))
{}
};
- 无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用它们。
D &D::operator=(const D &rhs){
Base::operator=(rhs);
return *this;
}
- 派生类析构函数只负责销毁由派生类自己分配的资源。
class D : public Base {
public:
~D(){ }
};
- 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相对应的虚函数版本。
15.7.4 继承的构造函数
- 派生类能够重用其直接基类定义的构造函数。
- 一个类只初始化它的直接基类,也只继承其直接基类的构造函数。
- 类不能继承默认、拷贝和移动构造函数。
- 派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。
- 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。
- 当一个基类构造函数含有默认实参时,这些实参并不会被继承。
- 如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。
class Bulk_quote : public Disc_quote {
public:
using 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 容器与继承
- 当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
- 使用容器存放继承体系中的对象时,通常采用间接存储的方式,实际上存放的通常是基类的指针(更好的选择是智能指针)。
vector<Quote> basket;
basket.push_back(Quote("0-201-1",50);
basket.push_back(Bulk_quote("0-201-8",50,10,.25));
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));
cout<<basket.back()->net_price(15)<<endl;