侯捷C++视频笔记——C++面向对象高级编程(上)

C++面向对象高级编程(上)

01、C++编程简介

学习目标:

1.学习良好的编写C++类的形式,包括两种,分别是无指针成员类(如Complex)和有指针成员类(如String)
2.学习类之间的关系,即继承,复合和委托。

02、头文件和类的声明

1.C和C++在数据和函数的区别:

C中存在数据以及函数,函数用来处理数据。缺点是缺少关键字管理数据,数据均为全局,难以作限制
C++中通过类将数据和函数包在一起,以类为个体来创建对象。是面向对象的语言。

2.防卫式声明:

#ifndef _name_
#define _name_
//本体
#endif

优点:防止多次引入同一个文件

3.初识模板

侯捷C++视频笔记——C++面向对象高级编程(上)_第1张图片

因为可能存在不同的参数类型,所以通过template可以简化很多编写重载操作

03、构造函数

1.inline内联函数

在class内部定义的函数,自动声明为inline,在外部生命的函数在前面加上inline也可以手动声明为inline。不过具体能不能成为inline函数要看编译器。
inline的优点是相对于普通函数更快,一般来说能声明为inline都声明为inline。

2.访问级别

函数一般放在public,数据一般放在private,如果要访问数据,可以在public写单独的获取数据的函数以访问数据。
侯捷C++视频笔记——C++面向对象高级编程(上)_第2张图片

3.构造函数

学会使用初始化队列来初始化一个对象的数据
侯捷C++视频笔记——C++面向对象高级编程(上)_第3张图片

使用初始化队列初始化和在{}使用赋值语句(如下图)的区别:
在这里插入图片描述
不一样,前者是在初始化,第二个是在赋值,前者更加规范,而且效率更高,也会避免一些错误。比如说有些必须初始化的元素(比如引用),使用赋值可能留下隐患(引用必须在声明时初始化)。

4.函数重载

函数重载仅与以下参数有关:

1.函数名称。函数重载需要函数名称相同
2.函数参数。函数重载需要不同的函数参数列表,列表可以是类型不同,数量不同等

同时也需要注意这些问题

1.如果只有返回值类型不同则不会构成重载
2.对于每种参数,必须要有优先级区分,不然会造成二义性调用。
侯捷C++视频笔记——C++面向对象高级编程(上)_第4张图片

3.函数重载的参数顺序也会构成重载
侯捷C++视频笔记——C++面向对象高级编程(上)_第5张图片

04、参数传递与返回值

1.放在private区的构造函数

说明外界不能调用来创建这个类的对象。
如果是这样的形式,那么这个类可能会是一个单例,即这个类仅允许存在一个它的对象。
侯捷C++视频笔记——C++面向对象高级编程(上)_第6张图片

2.常量成员函数const

如果这个函数内部不会改变数据,那么这个函数应当声明为const。比如下面两个函数仅仅是读取放在private区域的值,就声明为const
在这里插入图片描述

如果不声明为const,那么遇到下面这种情况,即声明了这个类的一个const对象时,如果调用了非const的函数,那么就会报错,即留下了隐患。
侯捷C++视频笔记——C++面向对象高级编程(上)_第7张图片

3.参数传递:值传递和引用传递

1.为什么我们尽量使用引用传递:

如果进行值传递,那么函数会将这个需要传的值本身全部压栈,如果这个值本身很大,那么必然会花费不少时间和空间。
然而,如果进行引用传递,那么不论这个传递的值本身多大,函数也只会传递一个指针大小的值过去(引用实际上就是使用指针实现的),即使这个值本身很大,它的时间和空间复杂度也没有变化。
所以我们需要养成习惯,如果可以的话,尽量使用引用传递

2.如何判断我们能否使用引用传递

因为对引用进行修改会对初始值也造成修改,如果我们不需要对传入值进行修改的话,应该添加const。
如果确定要对引用传递进行修改,切记它也会对原本的对象进行修改。

4.返回传递:值返回和引用返回

为什么我们尽量使用引用返回:基本同参数传递。剩下原因的将在下一章操作符重载中详细介绍

5.友元Friend

1.用法及定义:在类中声明为friend的函数,可以直接访问这个类的私有成员。

侯捷C++视频笔记——C++面向对象高级编程(上)_第8张图片

2.通过相同class生成的对象,这些对象之间默认互为友元

如图,c2可以直接访问c1(即直接通过点运算符)的成员,而不是通过函数返回值获取成员
侯捷C++视频笔记——C++面向对象高级编程(上)_第9张图片

6.传值和传引用的易错点

1.应当注意返回值是不是一个临时声明在函数内部的变量

如果返回值是临时创建在函数内部的话,因为它的生命会在函数运行结束之后终止并死亡,所以如果对一个被销毁的对象传引用的话,那么必然会发生错误。
侯捷C++视频笔记——C++面向对象高级编程(上)_第10张图片

05、操作符重载和临时对象

1.成员函数this

定义:所有的成员函数一定带着一个隐藏的函数参数,叫做this。这个this永远指向函数调用者,并且无法被更改,同时也不能显式的写在参数列表当中,仅能直接使用。

侯捷C++视频笔记——C++面向对象高级编程(上)_第11张图片

上图函数实际隐藏了参数this,完整函数应该如下
侯捷C++视频笔记——C++面向对象高级编程(上)_第12张图片

如果调用这个函数重载,那么c2将作为参数this,c1将作为参数r
侯捷C++视频笔记——C++面向对象高级编程(上)_第13张图片

2.返回传递:引用传递详解

1.接收端无需知道返回的是一个引用还是值,不需要做额外修改

2.返回引用可以实现连续调用的情况。

对于函数+=,举个例子:
在这里插入图片描述

我们一般不会使用这个函数(+=)的返回值,那么我们为什么要设置为返回引用,而不是返回void呢?

如果不进行连续赋值,那么确实可以不用返回引用,返回void即可。但是比如说连续赋值如下:
在这里插入图片描述

我们希望它应该计算c2+=c1,然后将之后得到的c2进行c3+=c2的操作。即c3 = c3 + c2 + c1。
如果我们没有使用引用传递,那么最后的一开始计算c2+=c1返回的就是一个void,c3将与一个void相加,必然会报错。
这就是我们返回引用的理由

3.关于“<<”操作符的重载的补充

<<运算符会将右边的对象作用于左边的对象身上。最常见的就是

cout<<123;//在屏幕中打印123
但是这里就引出了一个问题,cout必然是标准库已经写好了的函数,如果我们制定了一个新的类,并且实例出了它的对象,那么cout无法直接输出这个函数的对象,如果我们需要打印这个对象,那么我们必须要自己为它指定一个输出函数

侯捷C++视频笔记——C++面向对象高级编程(上)_第14张图片

这里ostream不能设置为const,因为os一直在接受输入,即一直在改变,所以不能设置为const。
然后,由于我们经常这样使用

cout<< 1 << 2 << 3; //连续使用<<输出打印

我们需要设定它的输出为ostream&,以便于连续使用。

06、复习与总结:复习Complex类的实现过程(即02~05节总结)

1.使用防卫式声明
2.将函数放在public,数据放在private
3.在构造函数中,使用初始化列表来对数据进行初始化
4.尽量使用引用传递来传递参数和返回值
5.如果函数内不会对数据进行修改,那么应该声明为const
6.如果有需要,记得自己再重载<<操作符实现输出

07、三大函数:拷贝构造,拷贝赋值,析构

1.由String引入带指针类的设计

因为在设计类的时候,我们无法确定使用者需要多大的内存空间来存储这个字符。所以在设计这个类的时候,设计者选择在类中存放一个指针,然后创建对象时再动态的在内存空间当中分配足够的空间来容纳这个字符串,并且让类中存放的这个指针指向刚刚分配的内存空间的地址。
假设我们需要实现以下的功能

int main(){
    String s1();
    String s2("Hello!");
    String s3(s1);  //拷贝构造
    cout << s3 << endl; //重载<<运算符
    s3 = s2;//拷贝赋值
    cout << s3 << endl;
}

Q:之前的Complex类中是否也存在拷贝构造和拷贝赋值?与String中的有何不同?

A:Complex类中存在拷贝构造和拷贝赋值。在编译器中存在默认的拷贝构造和拷贝赋值,默认会将需要拷贝的右边逐位拷贝到左边。对于Complex类来说,这个无所谓。但是对于指针类String来说,进行逐位拷贝会将指针指向同一个对象的地址,但是我们并不想让所有的对象都这么做。所以我们需要自己编写拷贝构造和拷贝赋值函数。

由此我们设计出了String类的基本结构。

public区,从上到下依次是:
1.构造函数
2.拷贝构造函数
3.拷贝赋值函数
4.析构函数

5.普通函数,用来读取数据
private区,存放一个指针,用来动态指向分配的内存地址。

侯捷C++视频笔记——C++面向对象高级编程(上)_第15张图片

2.String类的构造函数和析构函数

侯捷C++视频笔记——C++面向对象高级编程(上)_第16张图片

构造函数解释:

已知c风格字符串需要以’\0’结尾
strlen()函数不会获取后面的’\0’,所以需要额外+1
strcpy()函数是将我们接收到的cstr放到我们刚刚动态分配的内存空间中去,这样,我们获得了独立的一份字符串拷贝。

析构函数解释:

因为类创造的对象死亡时,会自动清理它的内部所有的成员。对于类似Complex的类来说,不需要额外编写析构函数。但是对于我们在类内部动态创建内存空间的String类来说,它只会清理它内部的指针,而不会清理指针所指向的那一份内存空间
所以我们需要额外编写析构函数,目的是是用来释放我们刚刚自己动态分配的内存空间,避免内存泄露。

析构函数的形式如下:

~ 类名(){
    //删除语句
}

3.String类的拷贝构造(copy ctor)和拷贝赋值(op=)

浅拷贝,深拷贝和拷贝构造

假设我们都已经初始化了a和b如下:
侯捷C++视频笔记——C++面向对象高级编程(上)_第17张图片

如果使用默认的拷贝构造(copy ctor)和拷贝赋值(op=)函数,那么不仅a和b会指向同一个对象,对a和b其中的任意一个进行修改都会影响到第二个;同时也发生了内存泄露,因为它原本指向的内存空间也没有得到释放。
这样的拷贝也叫做浅拷贝
侯捷C++视频笔记——C++面向对象高级编程(上)_第18张图片

↑↑↑↑↑↑↑↑↑↑↑↑默认的赋值函数↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
这里可以知道,如果使用默认的拷贝构造函数,不仅会是
那么我们如果不想让a和b指向同一个对象,二是互相独立,我们就需要自己额外编写拷贝构造函数(copy ctor)。
这也叫深拷贝,因为它自己创建了一个新的对象
侯捷C++视频笔记——C++面向对象高级编程(上)_第19张图片

(这里也应用到了之前提到的性质,就是同一个类初始化出的对象互为友元)

拷贝赋值

如果修改一个原本已经存在的值,我们需要做的有三点,
一是删除原来的值,清除其原本所指的内存空间
二是创建一个新的大小合适的内存空间来容纳新的数值
三是将新的数值拷贝到这个新的内存空间中去
即下图中1,2,3所示操作
侯捷C++视频笔记——C++面向对象高级编程(上)_第20张图片

Q:这个if的存在意义是什么?(重点,if不可省略)
A:用来判断这个拷贝赋值的表达式左右两边是不是原本就是同一个值。这个地方不能忽略,不仅仅是效率问题,而是因为如果他们本身就是同一个值的话:假设为s1和s2,目前我们需要执行s1 = s2的操作,如果我们此时我们执行第一步,即

一是删除原来的值,清除其原本所指的内存空间

我们本意是删掉s1,但会同时也把s2也删掉,因为他们本身是同一个值。所以在执行第二步时,由于s2已被删除,我们就无法通过strlen()函数再次获取到s2的长度,将造成错误

同时这里注意一下,返回的是引用,允许了我们进行连续=操作(s1 = s2 = s3)

4.操作符<<重载

ostream& operator<<(ostream& os, const String& str){
    os << str.get_c_str();
    return os;
}

这个重载应当设置为全局函数,不然<<将出现在右边,不符合我们的习惯

08、堆,栈和内存管理

1.栈和堆

侯捷C++视频笔记——C++面向对象高级编程(上)_第21张图片

栈由编译器自动分配释放,用来存放函数的参数值,局部变量的值等。
堆用来存放动态分配的空间,需要由程序员手动分配和释放

2.栈中对象的生命周期

侯捷C++视频笔记——C++面向对象高级编程(上)_第22张图片

对于这种存在于栈中的对象,在作用于结束之后会被自动清理

侯捷C++视频笔记——C++面向对象高级编程(上)_第23张图片

但是,如果在这种对象前面加上static,那么它就不会被清理,而是在整个程序运行结束之后才清理

侯捷C++视频笔记——C++面向对象高级编程(上)_第24张图片

最后,如果在这种对象声明在外部,那么也相当于一种static对象,作用域是整个程序

3.堆中对象的生命周期

侯捷C++视频笔记——C++面向对象高级编程(上)_第25张图片

堆中的对象需要手动进行释放,否则就会造成内存泄露

4.分析new函数(先分配内存,再调用构造函数)

假设我们new了一个Complex对象pc,他将被分解为三个操作

侯捷C++视频笔记——C++面向对象高级编程(上)_第26张图片

第一步实际上就是分配了一块内存,这里实际上使用的是C的malloc函数来动态分配一个堆空间,并且用void* 指针指向这个内存空间
第二步就是将这个指针执行类型转换,变成我们需要的之前指定的类型
第三步就是调用这个函数的构造函数,进行初始化

5.分析delete函数(先调用析构函数,再释放内存)

在这里插入图片描述

第一步是调用它的析构函数,清除它之前请求动态分配的内存空间。但是此时这个指针本身依然存在
第二步是释放它自己所占的内存,实际上是使用的C的free函数删除自己(指针)

6.数组形式的new和delete,以及他们可能的内存泄露

首先了解一下new在内存中的动态分配

对于非数组的new和delete,它的实际大小 = debug大小 + 类型大小,然后向上补到16的倍数(图中pad)

侯捷C++视频笔记——C++面向对象高级编程(上)_第27张图片

对于数组的new和delete,它的实际大小 = debug大小 +(类型大小 X 数组元素个数) ,然后向上补到16的倍数。同时它还存放了一个cookie用来记录当前数组元素的个数

侯捷C++视频笔记——C++面向对象高级编程(上)_第28张图片

array new必须搭配array delete

侯捷C++视频笔记——C++面向对象高级编程(上)_第29张图片

首先,不论我们有没有array new搭配array delete,这个对象的本体都会被完整删除。
如果array new没有搭配array delete,那么析构函数将仅被调用一次,数组中其它的动态分配的内存将不会被回收,造成内存泄露。

09、复习与总结:复习String类的实现过程(即07~08节总结)

1.类似于String这种带有指针的类,需要额外编写拷贝构造函数,拷贝复制函数,析构函数
2.拷贝赋值必须进行判断是否相同
3.使用数组new时需要搭配数组delete

//IDE:VS2019 
#pragma warning( disable : 4996)	//防止编译器强制要求使用strcpy_s
#include 
using namespace std;
class String {
public:
	String(const char* str = "") {	//拷贝构造
		if (str) {
			ptr = new char[strlen(str) + 1];
			strcpy(ptr, str);
		}
		else {
			ptr = new char['\0'];
		}
	}

	String(const String& str) {	//拷贝构造
		ptr = new char[strlen(str.ptr) + 1];
		strcpy(ptr, str.ptr);
	}

	String& operator=(const String& str) {
		if (&str == this) {
			return *this;
		}
		delete[] ptr;
		ptr = new char[strlen(str.ptr) + 1];
		strcpy(ptr, str.ptr);
		return *this;
	}

	char* getStr() const {
		return ptr;
	}

	~String() {	//析构函数
		delete[] ptr;
	}
private:
	char* ptr;
};

ostream& operator << (ostream& os, const String& str)
{
	os << str.getStr();
	return os;
}
int main(void) {
	String s1;	//测试默认构造函数
	String s2 = "shkfjkabc";	//测试拷贝构造
	cout << s1 << endl;
	cout << s2 << endl;
	String s3 = s2;	//测试拷贝赋值
	String s4(s3);	//测试拷贝构造
	String s5("123asdas");	//测试拷贝构造
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
	String s6;	//测试连续赋值
	String s7 = s6 = "12312asdvxcv";
	cout << s6 << endl << s7;
}

10、扩展补充:类模板,函数模板及其他

1.static(静态)介绍

在类中的数据或者函数前面加上static,就变成了一个静态变量/函数。
侯捷C++视频笔记——C++面向对象高级编程(上)_第30张图片

那些没有static的成员

对于数据:每创建一个类的对象,它就会在内存中额外开辟一份空间
对于函数:存在一个隐藏的this指针,用来访问这个函数的调用者

声明为static的成员

对于数据:无论创建多少个类的对象,它始终只在内存中存在一份,所有对象共享
对于函数:将不再存在this指针,通常用来处理静态的数据
侯捷C++视频笔记——C++面向对象高级编程(上)_第31张图片

2.单例模式

侯捷C++视频笔记——C++面向对象高级编程(上)_第32张图片
把构造函数放在private当中,通过静态成员访问

3.cout

为什么cout可以接受这么多种参数

在STL中,cout继承自ostream,在ostream中对<<操作符进行了很多的重载
侯捷C++视频笔记——C++面向对象高级编程(上)_第33张图片

4.模板

1.类模板class template

为了不让我们因为仅仅因为类中元素的数据类型不同而不得不重复声明类,C++提供了类模板,在声明时可以根据传入参数类型自动改变
侯捷C++视频笔记——C++面向对象高级编程(上)_第34张图片

2.函数模板function template

和类模板的作用基本相同,都是为了我们不必再去编写更多地重复代码
侯捷C++视频笔记——C++面向对象高级编程(上)_第35张图片

当我们使用自定义的类(这里是Stone)时,由于模板在调用时是根据传入参数类型而改变的,所以会自动在Stone类中寻找有关操作符<的重载,这个过程也叫做引数推导。如果我们在Stone类中没有重载<操作符,那么会报错

5.namespace

namespace是一块有名字的逻辑区域,其内部有变量和函数等,形式如下。
namespce可以有以下三种用法
侯捷C++视频笔记——C++面向对象高级编程(上)_第36张图片

附namespace和class的区别:

  1. 在C++中,structure和class几乎是完全相同的,区别在于structure默认是public,而class默认是private。namespace则与上面两种完全不同,namespace仅仅是一块有名字的逻辑区域,其内部有变量和函数等
  2. 我们可以定义一个class变量,但不可以定义一个namespace变量。若要使用class内的一个成员函数,则需要首先定义一个class变量,然后通过这个变量来使用其成员函数;而namespace则可以直接使用其内部函数,不需事先声明。
  3. 因为namespace中定义的变量就是一个实体,在任何情况下(只要在可见域内),对namespace A,A::M的写法都是允许的。而对于class来说, A::M的操作仅仅有A的成员函数或者M是一个静态成员的情况下才能使用。
  4. 命名空间name space可以被再次打开,并添加新成员。但是类class不允许。
    侯捷C++视频笔记——C++面向对象高级编程(上)_第37张图片

11、组合和继承

1.Composition,复合

在这里表现为类queue中,有其他类(这里是deque)的对象,使得类queue可以使用deque已经完成的函数,避免重复造轮子。
这里的adapter是指一种设计模式,即适配器模式
侯捷C++视频笔记——C++面向对象高级编程(上)_第38张图片

复合操作可以嵌套

从内存角度来看,在类queue中有一个deque对象,deque中有若干Itr对象。层层分解之后可以知道内存中所占用的大小
侯捷C++视频笔记——C++面向对象高级编程(上)_第39张图片

复合操作的构造和析构

构造函数由内向外
析构函数由外向内
类似于装礼盒,构造函数先把里面物品放好,再进行包装。拆礼盒需要先拆外盒,再拿出内容物
侯捷C++视频笔记——C++面向对象高级编程(上)_第40张图片

2.Delegation,委托,也叫Composition by reference

类似于上面的复合,但是从类A中含有类B的对象变成了类A中含有指向类B对象的一个指针
侯捷C++视频笔记——C++面向对象高级编程(上)_第41张图片

Delegation和Composition的区别

1.由于是指针,他们的生命周期不再同步。一个在栈内存,使用完就自动销毁,一个在堆内存,需要程序员手动分配和释放
2.Delegation优点是可以实现如图所示pimpl(pointer to implementation)分离,它的对外接口(这里是类String)和内部实现类(这里是StringRep),也就是以后我们可以在保持不修改对外接口的同时对内部实现进行修改,不会影响客户,也称为编译防火墙

3.Inheritance,继承

如图所示,_List_node继承了_List_node_base
侯捷C++视频笔记——C++面向对象高级编程(上)_第42张图片

子类继承了父类的哪些成员

子类包含了父类中的数据,比如_List_node_base中的两个指针,体现在子类不仅需要为自己的成员创建内存空间,也要为从父类继承来的成员创建空间
对于父类中的函数,实际上继承的是函数的调用权

继承中子类和父类的构造和析构

构造函数由内向外
析构函数由外向内
侯捷C++视频笔记——C++面向对象高级编程(上)_第43张图片

12、虚函数和多态

1.函数的三种成员函数(从虚函数的角度出发)

名称 描述
非虚函数 不希望子类对其进行覆写
虚函数 自己有一个已经存在的定义作为默认定义,但是允许子类对其进行覆写(override)
纯虚函数 不存在默认定义,继承它的子类必须对其进行覆写(override)

比如对一个形状类进行划分
1.它有一个固定的ID,这个应该不变,所以为非虚函数
2.它有一个默认的打印报错信息的函数,但是对于不同的形状而言,可以有多种不同的报错形式,所以为虚函数
3.它有一个绘画函数,对于一个叫做“形状”的形状,我们不知道怎么画它,当且仅当它的子类比如正方形,长方形对其进行实例化之后,我们才知道自己需要画一个什么形状的图形。所以它应该是一个纯虚函数
在这里插入图片描述

2.继承 + 虚函数 = 模板方法模式(Template Method)

如果我们需要实现一个如下场景:
1.点击文件夹,在文件夹中找到你想到打开的文件,点击这个文件(OnFileOpen)
2.读取文件内容
对于动作1,无论你打开什么文件,操作都是一样的,都需要找到它的位置然后点开,区别在于怎么读取这个文件的内容,不同格式的文件,它的读取方式是不一样的。(Serialize)然后这两个动作应该是连贯的,所以我们把Serialize函数也放到了OnFileOpen函数里面去
在这里我们使用子类CMyDoc继承含有一个非虚方法(OnFileOpen)和一个虚方法(Serialize)的CDocument父类,其中子类需要对父类的Serialize函数进行覆写。
然后实例化一个子类的对象,它的执行过程就是下图。当它遇到OnFileOpen中的虚函数Serialize时,会自己去查找虚函数的实现方法。即提前写好已经固定了的函数,然后让子类去定义哪些暂时无法确定的函数。
侯捷C++视频笔记——C++面向对象高级编程(上)_第44张图片

3.课后小作业:(继承 + 复合)的构造和析构执行顺序

侯捷C++视频笔记——C++面向对象高级编程(上)_第45张图片

先说结论
对于上面的构造顺序,是父类->复合类->子类,析构函数是子类->构造类->复合类
对于下面的构造顺序,是复合类->父类->子类,析构函数是子类->父类->复合类

测试代码如下:

#include 
#include 
int main()
{
//这一段是第一种情况的测试代码
//其中A是父类,B是复合类,然后C继承A并且复合B
        using namespace std;
        class A {      //A是父类
        public:
               A() {
                       cout << "父类的构造函数被调用了" << endl;
               }
               ~A() {
                       cout << "父类的析构函数被调用了" << endl;
               }
        private:
               int a;
        };
        
        class B {      //B是复合类
        public:
               B() {
                       cout << "复合类的构造函数被调用了" << endl;
               }
               ~B() {
                       cout << "复合类的析构函数被调用了" << endl;
               }
        private:
               int b;
        };
        
        class C:A {    //C继承A,并且复合B
        public:
               C() {
                       cout << "子类的构造函数被调用了" << endl;
               }
               ~C() {
                       cout << "子类的析构函数被调用了" << endl;
               }
        private:
               int c;
               B b;
        };
        C c;
}

运行结果:
侯捷C++视频笔记——C++面向对象高级编程(上)_第46张图片

#include 
#include 
//这一段是第二种情况的测试代码
//其中A是父类,与B复合,然后C继承A
int main()
{
        using namespace std;
        class B {      //B是复合类
        public:
               B() {
                       cout << "复合类的构造函数被调用了" << endl;
               }
               ~B() {
                       cout << "复合类的析构函数被调用了" << endl;
               }
        private:
               int b;
        };

        class A {      //A是父类,与B复合
        public:
               A() {
                       cout << "父类的构造函数被调用了" << endl;
               }
               ~A() {
                       cout << "父类的析构函数被调用了" << endl;
               }
        private:
               B b;    
        };
        
        class C:A {    //C是子类,继承A
        public:
               C() {
                       cout << "子类的构造函数被调用了" << endl;
               }
               ~C() {
                       cout << "子类的析构函数被调用了" << endl;
               }
        private:
               int c;
        };
        C c;
}

运行结果:
侯捷C++视频笔记——C++面向对象高级编程(上)_第47张图片

4.观察者模式:委托 + 继承

在编写UI时,可能会出现这种情况:用户对一个UI界面开了多个窗口,这些窗口可能显示同一种内容,但是它的表现形式不同,但同时程序员必须让这些内容中的数据保持一致。这就用到了观察者模式
在观察者模式当中,Observer就是去观察这个Subject的窗口,Subject可以拥有很多个Observer,表现在Subject中含有一个vector
同时Observer含有一个虚方法供子类进行派生,它的子类同样是Observer,可以放到vector中。这样使得Observer可以有多种形式来观察同一个Subject。

在Subject中应当有这些部分:

1.注册函数,用来添加观察者
2.通知函数,用于当更新了Subject时,对它所有的观察者中进行更新
3.删除函数,用于删除观察者(本例中省略了)
4.其它函数,用于处理Subject中的私有数据等等
侯捷C++视频笔记——C++面向对象高级编程(上)_第48张图片

13、委托相关设计

1.组合模式:委托 + 继承

在日常使用过程中,我们会使用到“目录”这种东西,它的特点就是目录中可以创建目录,目录也可以进行合并等等。总结一下就是用户可以使用多个简单的组件以形成较大的组件,而这些组件还可能进一步组合成更大的。它重要的特性是能够让用户一致地对待单个对象和组合对象。

在这张图中,可以大致分为一下三种

1.Primitive,代表为这个目录下的文件
1.Composite,是文件容器,可以容纳很多文件
1.Component,是这个目录本身,容纳文件以及文件容器

侯捷C++视频笔记——C++面向对象高级编程(上)_第49张图片

2.原型模式:委托 + 继承

意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
主要解决:在运行期建立和删除原型。
优点: 1、性能提高。 2、逃避构造函数的约束。
侯捷C++视频笔记——C++面向对象高级编程(上)_第50张图片

比如说子类LastSatIamge,我们从上往下对齐做一个解读(SpotImage同理)
数据: LSAT:LastSatIamge
它本身有一个静态对象LSAT(注:在下面添加一条实线的意思是这个成员为静态成员)
函数:

  1. -LastSatIamge()
    一个私有的构造函数,在创建时会将自己添加到原形中去
  2. #LastSatIamge()
    一个被子类继承的构造函数,直接在内存中分配一个空间用来创建新的副本。注意这里的参数其实是没用的,只是为了构成一个函数重载方便调用而已。
  3. clone():Image*
    克隆函数,调用上面的构造函数,同时返回构造函数生成的新对象

对于父类Image,同样做解读
数据:
prototypes[10]:Image*
用来保存原型,注意每种原形不论生成了几次,只会在这里面保存一个原型
函数:
1.clone():Image*
是一个纯虚方法,必须要子类对其进行实现,clone在子类中的作用是用来分配一个内存空间容纳子类的对象
2.findAndClone(i):Image*
在Image已经保存的原型里面找到我们需要创建的原型,调用这个原型的构造函数,返回生成的对象
3.addPrototype(p:Image*)
在子类中,要求返回他们的原型用来保存到Image的原型库当中去

你可能感兴趣的:(C++,c++,音视频,开发语言)