本周内容算是Part1的收尾了,通过这三周的课程会对面向对象编程这个概念有了更为深刻的理解,信息量有些大,可能还需要之后再复习。
- 首先是OOP里面对于class这个概念的延伸,主要讲了三种class之间的关系,有的教材上将类与类之间的关系讲的像人生哲学一样,但是只要理解这三种关系就能继续之后的系列课程。
对象之间的关系可以概括为继承(Inheritance)、组合(Composition)和代理(Delegation)3种。
- Composition组合, 表示has-a, 一个class里面完整包含另一个class
Composition概念早就存在,在c语言的struct中,使用了别的struct或者string等,都是组合(c语言实现面向对象就是用了这个特性)。
举例来说,queue中含有deque,即queue和deque是复合关系。(即只要A含有B,则就是一种复合关系)。但看代码,queue中的功能,都由dequeue完成。Dequeu的功能比较强大,就比较特殊了,成为一种adapter模式。
composition的构造是由内而外的:B的构造函数会首先调用A的默认构造函数(编译器自己调用,如果需要传递参数,需要在初始化列表显示调用),然后在调用自己的构造函数
Conainter::Container():: Component() {…} //编译器会自己去构造Component对象
container的构造函数首先调用component的default构造函数
composition的析构是由外而内的:B的析构函数首先执行自己的,然后才调用A的析构函数。Container的析构先调用自己,然后再调用Component的析构函数(编译器帮助完成,我们只要管理好Container的构造和析构就可以)。
如果Component的构造函数有多个,则需要显式的调用构造函数。
- Delegation委托,Composition by reference
Tips:复合是两者生命一起出现,委托(又称Composition by reference)Container先创建起来,等到需要Component的时候,才创建Component,即不同步。
a.设计模式:Handle/Body(pointer to implementation:我有一个指针,去指向为我实现所有功能的那一个类。——指针可以指向不同的实现类,所以又称编译防火墙:(下图)左边的永远不用再编译,要编译的只是(下图)右边的。
b.共享
如上图红圈所示,a,b,c三个指针指向同一个字符串,即共享一个字符串,这样的好处是节省内存空间(当然,共享的内容必须完全一样),但是需要注意,当想改变一个指针指向的内容时,其余两个指针指向的值也就随之改变,也就是牵一发而动全身。
- Inheritance,类的继承,是新的类从已有类那里得到已有的特性。或从已有类产生新类的过程就是类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。
Inheritance的声明:
class 派生类名:继承方式 基类名1, 继承方式 基类名2,...,继承方式 基类名n
{
subclass成员声明;
};
- 一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类,称为单继承。直接派生,间接派生。
- 继承方式规定了如何访问基类继承的成员。继承方式有public, private, protected。如果不显示给出继承方式,默认为private继承。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限。
- 派生类继承基类中除构造和析构函数以外的所有成员。
- 派生类生成:
吸收基类成员(除构造析构函数以外的所有成员);
改造基类成员(根据继承方式调整基类成员的访问,函数在子类中的覆盖,以及虚函数在子类中的覆盖)
Tips:C++类的3种继承方式,分别是public继承,protected继承,private继承。
最常用的还是public继承。class默认的是private继承,它的member如果没写权限也是默认.
3个继承权限的区别:
class ex0
{
private:
void showPrivate(){cout<<"this is private function!";}
public:
void showPublic(){cout<<"this is public function!";}
protected:
void showProtected(){cout<<"this is protected function!";}
};
class ex1:public ex0
{
public:
void func()
{
showPrivate();
//错误因为此函数访问权限只有基类 ex0 自己有
showPublic();
showProtected();
}
};
class ex2:protected ex0
{
public:
void func()
{
showPrivate();
//错误因为此函数访问权限只有基类 ex0 自己有
showPublic();
//正确, 但是此函数由于 ex2 的保护继承这个函数的访问权限已经变成了 protected,
//也就是说对于外部类来说已经不具备访问这个函数的权限了
showProtected();
}
};
class ex3:private ex0
{
public:
void func()
{
showPrivate();
//错误因为此函数访问权限只有基类 ex0 自己有
showPublic();
//正确, 但是此函数由于 ex3 的私有继承这个函数的访问权限已经变成了 private,
//也就是说对于外部类和派生类来说已经不具备访问这个函数的权限了
showProtected();
//正确, 但是此函数由于 ex3 的私有继承这个函数的访问权限已经变成了 private,
//也就是说对于外部类和派生类来说已经不具备访问这个函数的权限了
}
};
- Inheritance(继承)with virtual functions
继承搭配着虚函数。
子类可以继承父类的所有内容,包括数据和函数。从内存角度,可以继承数据;对于函数,不能从内存角度理解,而是子类继承了父类的函数调用权。
函数分为三种情况:
- non virtual function:你不希望derived class重新定义override
- virtual function:希望derived class的重新定义override。已有默认定义。
- pure virtual:你希望derived class一定要重新定义。没有默认定义。
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。
在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了 这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
假设有下面这个类:
class Base {
public:
virtual void f( ) { cout << "Base::f" << endl; }
virtual void g( ) { cout << "Base::g" << endl; }
virtual void h( ) { cout << "Base::h" << endl; }
};
我们可以通过Base的实例来得到虚函数表。
Base b;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
可得到:
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
-
设计模式:Prototype(原型模式)
用原型实例制定创建对象的种类,并且通过拷贝创建新的对象;
类图:
核心是克隆函数的运用
组成元素:
Prototype:声明克隆自身的接口;
ConcretePrototype:实现克隆自身的操作;