c++ -- 面向对象程序设计
第15章 面向对象程序设计
一、OOP概述
1.面向对象程序设计的核心思想:数据抽象(封装)、继承和动态绑定(多态性)。
通过数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类),它们的目的都是为了代码重用。而多态则是为了实现另一个目的,接口重用!
2.虚函数意义:对于某些函数,基类希望它的派生类各自定义适合自己的版本,此时基类就将这些函数声明成虚函数(virtural)。
如果基类将某函数声明为虚函数,则在派生类中,该函数隐式的也为虚函数。
任意非static成员都可以为虚成员。
3.动态绑定:当我们使用基类的引用(或指针)调用一个虚函数时,将发生动态绑定。
定义基类
1.被用作基类的类
如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明:
class Quote;
//错误:Quote必须定义
class Builk_quote:public Quote
{
...
};
2.基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作。这保证了在任何情况下,不会出现由于析构函数未被调用而导致的内存泄漏。
class Quote { public: ... virtual ~Quote()=default; //虚析构函数 }
3.成员函数与继承
基类必须将两种成员函数区分开:一种是基类希望其派生类进行覆盖的函数,此类函数声明为虚函数;另一种是希望其派生类直接继承而不要改变的函数,非虚成员函数。
虚函数的解析过程发生在运行时(直到运行时才会决定到底执行哪个版本);;对于非虚成员函数,其解析过程发生在编译时而非运行时(在编译时进行绑定)。
4.访问控制与继承
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的没有影响。对基类成员的访问权限只与基类中的访问说明符有关。派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
派生类能访问基类的public成员,而不能访问private成员。
而对于基类的protected成员:
1)和private成员一样,protected成员对于类的用户来说是不可访问的。
2)和public成员类似,proected成员对于派生类的成员和友元来说是可访问的。
3)派生类的成员或友元只能通过派生类对象访问(类内访问)基类的protected成员(只能访问派生类对象中的基类部分的受保护成员)。派生类对于一个基类对象中的protected成员没有任何访问特权。
1 #include <iostream> 2 using namespace std; 3 4 class Parent 5 { 6 public: 7 Parent()=default; //默认构造函数 8 virtual ~Parent()=default; //默认析构函数 9 Parent(int x,int y,int z):a(x),b(y),c(z){} //三参数构造函数 10 int a; 11 protected: 12 int b; 13 private: 14 int c; 15 16 }; 17 //public继承 18 class child1:public Parent 19 { 20 public: 21 child1(int x,int y,int z,int m):Parent(x,y,z),mem(m){}; 22 void print() 23 { 24 cout<<"a:"<<a<<endl; 25 cout<<"b:"<<b<<endl; //派生类可以访问基类的protected成员b 26 //cout<<"c:"<<c<<endl; //c在基类中是private成员,派生类不可访问 27 cout<<"mem:"<<mem<<endl; 28 } 29 void print(child1& p); 30 //void print(Parent& p) //不能访问Parent::b 31 32 private: 33 int mem; 34 }; 35 void child1::print(child1& p) 36 { 37 p.b=10; //能访问child1::b 38 cout<<"b:"<<p.b<<endl; 39 } 40 41 int main() 42 { 43 int x=1,y=2,z=3,m=4; 44 child1 child(x,y,z,m); 45 child.print(); //输出1,2,4 46 child.print(child); //输出10 47 return 0; 48 }
5.抽象基类
含有纯虚函数的类是抽象基类,我们不能创建抽象基类的对象。
//抽象基类Disc_quote class Disc_quote:public Quote { public: Disc_quote()=default; Disc_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; }; Disc_quote disc; //错误:不能定义抽象基类的对象
纯虚函数(=0).
定义派生类
1.派生类必须通过类派生列表明确指出它是从哪个基类继承而来的。派生访问说明符:public/protected/private.
//class A:public B; //错误:派生列表不能出现在这里 class A; //正确:声明类A class A:public B { ... }
2.派生类中的虚函数:override
如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。
struct B { virtual void f1(int) const; vitural void f2(); void f3(); }; struct A:B { void f1(int) const override; //正确:f1与基类中的f1匹配 void f2(int) override; //错误:B没有形如f2(int)的函数 void f3() override; //错误:f3不是虚函数 void f4() override; //错误:B没有名为f4的函数 };
3.派生类对象及派生类向基类的类型转换
因为派生类对象中含有与其基类对应的组成部分,所以我们能够把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
4.派生类构造函数
每个类控制它自己的成员初始化过程。
派生类构造函数只初始化它的直接基类。
//调用基类初始化
child1::child1(int x,int y,int z,int m):Parent(x,y,z),mem(m){}
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
5.继承与静态成员(唯一定义,唯一实例)
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。无论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员遵循通用的访问控制规则:private/protected/public
6.防止继承的发生:final
可以把某个函数指定为final,如果已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都会引发错误。
struct D1 { void f1(int) const final; }; struct D2 { void f1(int) const; //错误:D1中f1声明成了final };
7.虚函数与默认实参
静态类型决定默认实参!!!
通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。所以说,若虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
二、类型转换与继承
1.存在继承关系的类型之间的转换规则:
1) 从派生类向基类的类型转换只对指针或引用类型有效;
2) 基类向派生类不存在隐式类型转换;
3) 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。
2.当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
3.派生类向基类转换的可访问性:
派生类向基类的转换是否可访问由使用该转换的代码块决定,同时派生类的派生访问说明符也会有影响,假定D继承自B:
1)只有当D继承B的方式为public时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是pretected/private的,则用户代码不能使用该转换。
2)不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
3)如果D继承B的方式public/protected的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果class D:private B ,则不能使用。
三、构造函数与拷贝控制(NOTE)
在继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为,这些操作包括创建、拷贝、移动、赋值和销毁。
1.虚析构函数
基类需要定义一个虚析构函数,。如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。
经验准则:如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作(P447)。基类的析构函数不遵循上述规则,是个例外。
虚析构函数将阻止合成移动操作:如果一个基类定义了析构函数,即使它通过=defaultde形式使用了合成的版本,编译器也不会为这个类合成移动操作(P475)。基类没有移动操作意味着它的派生类也没有(但在需要是可以自定义的)。
2.合成拷贝控制与继承
大量内容在P(552-555)。
NOTE:每个类控制它自己的成员初始化过程;每个类负责控制各自成员的访问权限。
3.派生类的拷贝控制成员(拷贝、移动构造函数,赋值运算符,析构函数)
定义派生类的拷贝或移动构造函数
派生类的拷贝控制成员:在默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
1 class Base {/*.......*/}; 2 class D:public Bse 3 { 4 public: 5 //必须显式使用基类的拷贝构造函数,拷贝基类成员 6 D(const D &d):Base(d) 7 { ... } 8 9 //必须显式使用基类的移动构造函数,移动基类成员 10 D(D &&d):Base(std::move(d)) 11 { ... } 12 };
派生类赋值运算符
和拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值:
1 //Base::operator=(const Base&) 不会被自动调用 2 D &D::operator=(const D &rhs) 3 { 4 Base::operator=(rhs); //必须显式使用基类拷贝赋值运算符,为基类部分赋值 5 /*自赋值及释放已有资源等情况*/ 6 return *this; 7 };
对Base::operator=的调用语句将执行Base的拷贝赋值运算符,只与该运算符是Base显式定义的还是有编译器合成的无关紧要。
消除代码重复方法:如果发现copy 构造函数和copy assignment 操作符有相近的代码,建立一个新的成员函数给两者调用。这样的函数往往是private 而且常被命名为init .
派生类析构函数
和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源;对象销毁的顺序与其创建的顺序相反。
4.继承的构造函数(NOTE)
类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成他们。
1 class Bulk_quote:public Disc_quote 2 { 3 public: 4 using Disc_quote::Disc_quote; //继承Disc_quote的构造函数 5 double net_price(size_t) const; 6 }; 7 8 //等价于 9 10 Bulk_quote(string& book,double price,size_t qty,double disc): 11 Disc_quote(book,price,qty,disc){ }
和普通的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。例如,不管using出现在哪儿,基类的私有构造函数在派生类中还是一个私有构造函数...
当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数。
5.容器与继承
当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器当中。
当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针)。和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型。
1 vector<shared_ptr<Quote>> basket; //容器中存放基类指针 2 3 //vector<T>::push_back源码:void push_back(const T& x); 4 basket.push_back(make_shared<Quote>("0-201-82",50)); 5 basket.push_back(make_shared<Bulk_quote>("0-201-54",10,.25)); 6 //实际调用的net_price版本依赖于指针所指对象的动态类型 7 cout<<basket.back()->net_price(15)<<endl;
正如可以将一个派生类的普通指针转换成基类指针一样,我们也能把一个派生类的智能指针转换成基类的智能指针。