侯捷---c++面向对象高级编程
c++面向对象高级编程
文章目录
- c++面向对象高级编程
- 一、头文件与类的声明
- 二、构造函数
- 1.inline函数
- 2.访问级别
- 3.构造函数
- 4.重载
- 5.把构造函数放在private中
- 三、参数传递和返回值
- 1.常量成员函数 const
- 2.参数传递:pass by value VS pass by reference (to const)
- 3.友元(friends)--- 打破了封装的大门
- 四、操作符重载和临时对象
- 1.操作符重载-1:成员函数
- 2.传递者无需知道接收者是以reference形式接受
- 3.操作符重载-2:非成员函数
- 4.typename():就是临时对象(temp object)的创建。
- 5.class body之外的各种定义
- 五、三大函数:拷贝构造,拷贝复制,析构
- 1.拷贝构造和拷贝复制的差别
- 2.解析三个特殊函数:
- 3.构造函数和析构函数
- 4.拷贝赋值函数
- 六、堆,栈与内存管理
- 1.堆和栈
- 2.new和delete的用法
- 3.内存管理
- 七、复习String类的实现过程
- 八、类模板,函数模板以及其他模板
- 1.static
- 2.Singleton(单例模式)
- 3. cout
- 4.class template(类模板)
- 5.function template (函数模板)
- 6.namespace
- ① using directive
- ②using declaration
- ③not using
- 7.特殊的构造函数explicit
- 九、组合与继承
- 1.组合(Composition):表示has a
- 2.Delegation(委托)Compisition by reference
- 3.Inheritance(继承),表示is-a
- 十、虚函数与多态
- 1.虚函数(virtual)
一、头文件与类的声明
二、构造函数
1.inline函数
inline(内联)函数会比较快,如果函数太复杂,编译器无法将其变成inline函数
2.访问级别
①private(数据);
②public(函数);
3.构造函数
构造函数写法独特:
①函数名称与类的名称相同;
②没有返回类型(构造函数是用来创建对象的);
③以初值列(初始化)的方式进行 — 更正确,更大气;
注意:
①默认实参不是构造函数的特性,其他的函数也可以这样做。
②使用初值列和赋值都可以,但是使用初值列效率更高。
4.重载
构造函数和函数可以有很多个。
例如下面的第二个①②:
我们可以定义这两个方法,那么为什么同名的函数可以定义呢?因为编译器会将其编译成不用的实际名称,如下图;
但是下面的第一个①②,就不能这么写,因为我们如果使用
complex c1; complex c2();
创建对象时,我们没有给任何参数,编译器会分不清调用哪个函数,因此这种情况就就不能写成②。
5.把构造函数放在private中
把构造函数放在private中看似是不应该的,但是当我们使用单例模式的时候,就会将构造函数放在private中。
外界通过A::getInstance()调用这个类里面的函数。
三、参数传递和返回值
1.常量成员函数 const
在下面定义的real和imag都是会返回实部或虚部的值,但是不会改变其值,因此可以使用const来修饰这个方法。
如果使用const来修饰变量,那么调用的变量方法也必须是const,否则编译器就会编译失败。
2.参数传递:pass by value VS pass by reference (to const)
pass by reference类似于c++的指针,会更改变量,但是速度更快,但是如果不想修改变量,同时要增加速度,就可以使用pass by reference (to const)。
因此,可以得到一个结论:参数传递尽量都传引用。
同样的,返回值的传递尽量也使用引用(reference)。
3.友元(friends)— 打破了封装的大门
在下图中,我们定义友元函数__dopal,然后我们在这个函数定义时就可以直接使用其private里面的属性。
注意:相同class的各个object互为友元(friends)
下面黄色的部分直接使用了complex的private属性,可以通过上面这句话进行解释。
什么情况下可以使用pass by reference,使用情况下可以return by reference。
由于下面__doapl的第一个参数非const,那么我们可以将操作后的返回值定为ths。
四、操作符重载和临时对象
1.操作符重载-1:成员函数
所有的成员函数一定带着一个隐藏的参数,也就是我们下面标出来的this,指向函数的调用者,通过下面的方式,我们可以对+=进行重载,从而实现两个复数的相加操作。
2.传递者无需知道接收者是以reference形式接受
在__doapl函数中,ths是一个指针,ths是指针所指的东西,但是这个函数的返回值声明中写的是返回的是reference类型,因为传递者无需知道接收者是以什么形式接受的。
那么按照这个意思,我们也可以将重载+=的符号的返回值设置为void,这样貌似是可以的,但是我们如果使用c3 += c2 += c1,时,将返回值设置为void似乎就不行了,因为我们在上面的式子中,先将c1加到c2,再将c2加到c3,那么此时c2就必须是complex&类型的。
重点:
当类名后面出现时,意味着这是一个指向该类对象的指针。指针的作用是存储对象的内存地址,而非对象本身。要访问对象的成员,需借助->操作符。
若类名后面是&,表示这是该类对象的引用。引用相当于对象的别名,必须在定义时就进行初始化,并且之后无法再引用其他对象。引用访问对象成员使用的是.操作符。
3.操作符重载-2:非成员函数
下面这些函数绝不可以return by reference,因为,他们返回的必定是个local object,因为我们需要在函数中创建返回的对象,离开这个函数之后,如果只是使用其reference,它的值已经不在了,那么返回值也就不存在了。
4.typename():就是临时对象(temp object)的创建。
例如下面标黄色的部分,他们的生命周期进行到下一行就结束了。
5.class body之外的各种定义
在下面的operator +中,没有创建任何的temp object,所以其可以return by reference.(所以这块可以更改)
在下面的operator -中,创建了temp object,所以其一定不可以return by reference.
对于 << 这种特殊的成员操作符,只能将其定义为全局的函数。
cout就是&ostream类型的对象。
在下面我们重载 << 符号的时候,我们不能将os设置为const,因为我们在函数体内,更改其输出的过程中就已经更改了os的对象结构。
同时,和上面的 += 类型,我们本可以将 << 的重载的返回值设置为void,因为我们在这个函数里面输出就可以了,不需要关心他出来还在不在,但是综合考虑,如果我们要多个 << 连用,就必须将其设置为complex&。
我的总结:我们定义函数的时候,首先考虑返回值是否是reference,通过查看函数体的定义,如果不行的话,在使用value作为返回值。
五、三大函数:拷贝构造,拷贝复制,析构
1.拷贝构造和拷贝复制的差别
s3第一次出现是通过拷贝构造,s3第二次出现是通过拷贝复制(=);
如果class里面带指针,一定不能使用编译器默认的拷贝,而是需要自己写。
2.解析三个特殊函数:
下面public中,第一个函数是本身的构造函数,第二个函数就是我们所说的拷贝构造,第三个函数就是进行了操作符重载,也就是我们所说的拷贝复制,(第二个和第三个函数的参数都是string类型的),第四个函数就是析构函数,当这个类生成的对象死亡的时候,析构函数就会被调用。
3.构造函数和析构函数
在下面三个String对象中,前两个对象离开作用域之后就会自动delete,而通过new分配的对象需要手动delete。
因此需要调用三次析构函数。就像下面使用了默认的拷贝之后就会出现浅拷贝的情况,导致两个指针只想同一片区域,从而当我们修改数据的时候,两个指针指向的同一块数据都会修改。
所以编译器给你的拷贝只是将指针赋值过去,也就是所谓的浅拷贝。
4.拷贝赋值函数
首先需要将自己本来的内存清空,然后创建足够的内存空间,最后将赋值的值添加到自己里面去。
那么上面的自我赋值是什么呢?也就是将自己赋值给自己嘛,如果这样做的话,效率会更高。
那如果不写这个,结果是否会出错呢?因为我们会在下面先将自己杀掉,但是如果左右是同一个,那么我删了右边,左边也就是空了呀,那第二步就会发现右边的值就会被删除了。
六、堆,栈与内存管理
1.堆和栈
通过new分配的对象存储在堆(heap)中。
下面的c1也就是所谓的stack object,其生命在作用域结束,这个作用域内的object,又称为auto object,因为它会被自动清理。
static local object的构造函数在程序结束的时候会被调用。
new的对象一定要通过delete将其删除(左边为正确做法),否则会造成内存泄漏。
2.new和delete的用法
new:先分配内存,再调用构造函数
delete:先调用析构函数,再释放memory。
3.内存管理
在VC下,给定的内存块一定是16的倍数,所以分配的内存大小一定是52 < 16 * 4 = 64;(因此需要加上第一列中深绿色的pad)
再拿第一列来举例吧:为什么上下的块标记的是00000041呢,按照16进制来说,00000040就是64,那么为什么要写成00000041呢?因为借用最后一块bit来表示操作系统给出去(1)还是收回来(0);(有灰色代表是在调试模式下)
那第二列也就可以解释为什么是00000011了。(没有灰色代表实在非调试模式下)
可是为什么可以将最后一位给出去呢?因为我们的内存大小是16的倍数,所以最后四位数都是0,那么我们就可以给出去一位也没有任何影响。
string类型也是4个字节。
下面的00000031和00000011也是类似的。
array new要搭配array delete,不然会出错。
使用array new分配的对象(因此必须使用array delete来删除array):
在第一列中 8 * 3表示数组中有三个complex,32 + 4也是之前说过的, 4 *2也是分配的地址空间,那最后的4是什么呢?因为在VC中,会单独定义数组的大小,所以会有4个字节存储数组的大小。(第一列和第二列的区别我们上面已经说过了)
如上图所示,如果不使用array delete搭配array new使用,就会引起内存泄漏,内存泄露的部分,将会是下面的其他部分,因为我们只是使用delete删除了第一块区域,而后面的区域则会出现内存泄漏。
设计字符串时,data放数组的话由于不知道大小,所以不太行,但是如果放指针的话,可以动态分配大小,因为都存储的是指针。
七、复习String类的实现过程
1.字符串里面不应该使用数组存储,而是通过指针存储,而字符串本身就是4个字节。
2.构造函数(设置参数的初值,同时不可以改变参数,因此参数使用const修饰)
由于构造函数的内容较多,我们在class之外进行编写,我们传入的参数是char*类型的,但是通过strlen只能得到它的字符串的长度,无法得到结束符,所以我们new 的时候要+1,为其加上结束符,如果我们没有指定初值,那就直接将这个字符串设置为只有一个结束符的字符串。
同时我们建议编译器将这个函数设置为内联函数(inline)
3.拷贝构造(拷贝的string也应该使用const修饰)
拷贝构造函数做的事情和构造函数类似,我们也是将其先创建再拷贝。
4.拷贝赋值(被拷贝的String也应该使用const修饰,同时因为我们赋值的位置的返回值本来就存在,也就是local object,所以我们可以将其返回值设置为引用类型)
判断是否return by reference的方法就是判断其返回值是否为local object。
这部分拷贝赋值要说的东西很多了就
①:首先我们希望这个函数被编译器设置为内联函数;
②:其实这个返回值本来可以是void,因为我们在C里面累加的习惯,,当我们使用累计赋值时,我们就应该将这个函数的返回值设置为String&。
③:因为我们没有在Class里面写,我们就必须通过String::来修饰;
④:String& 和&str的&符号虽然是一样的,但是含义不同,前者是引用(Reference),后者是取地址符;
⑤:这一部分是为了防止自我赋值,因为自我赋值会先delete自己,从而导致方法出错,因此需要单独检验。
⑥:这块其实就是先删除,再创建然后拷贝赋值;
⑦:因为return的时候与返回值无关,我们只需要return这个object即可,不用管这个方法是return by reference还是return by object,所以我们直接通过*,取得this(Reference)的物体。
5.析构函数
析构函数要做的就是将我们之前创建的东西delete,因为我们通过array new创建的字符串,现在我们就必须通过array delete删除我们创建的字符串。
6.返回字符串指针:因为这个方法没有改变函数值,因为可以使用const修饰。
八、类模板,函数模板以及其他模板
1.static
这块侯捷老师举了个例子,他说假如每个银行会有10w人存钱,那么每一个用户都需要创建一个成员变量,来存储存的钱,但是银行的利率是固定的,不需要为每一个用户添加一个这个变量,那么这个利率我们就存成static members。
同时相比于非静态成员函数(non-static member functions),静态成员函数(static member functions)没有this变量,因此它只能访问静态变量。
上述static函数的使用方法中,直接通过class name调用也是可以的
2.Singleton(单例模式)
使用单例模式的目的:该Class只希望生成一个对象。
因此我们将他的构造函数放在private中,同时创建一个static的对象a,此时没有任何类可以创建A的对象,此时只有private中唯一的类对象,而且外界也不能访问这个类对象。
那我们应该怎么在其他类中取得这个类对象呢,我们在public中放入一个static的getInstance方法,得到这个唯一的子集。
外界通过A::getInstance().setup()方法来调用setup方法。
但是这个方法不是很完美,因为如果没有类调用的话,这个static对象也已经被创建了,于是有了下面的优化方法:
如果没有任何人使用的话,这个单例就不会被创建,只有有其他类使用了这个单例的getInstance方法,这个单例才会被创建。
3. cout
为什么cout可以输出那么多东西呢?
cout就是一种ostream,然后我们去看ostream的定义,会发现它会接受很多类型,如上上图。
4.class template(类模板)
template是一个关键字,typename也是一个关键字,用来告诉编译器T目前是一个没有确定的类型,同时使用T来定义complex中的数据类型以及函数返回值,从而增加灵活性。
当我们使用complex类的时候,我们可以绑定double或者int之类的,如上图所示。
“模板会造成代码的膨胀”
5.function template (函数模板)
与上面的类模板类似,我们使用了上述的函数模板。
template 也是使用了相同的关键字。
在类模板的使用时会明确指出类中变量的类型,但是在函数模板中并没有明确的指出,因此编译器会对template function进行实参的推导(argument deduction)。
那么又有一个问题:我们进行比较时怎么知道两个stone类型的大小关系呢,这是stone里面的操作符重载就发挥作用了。
在c++标准库中的算法全部都是函数模板的形式
6.namespace
我们可以自己声明一个namespace,然后将里面的内容包围起来,确保不会和别人的namespace发生冲突。
std就是将标准库所有的东西都包含进去了。
那么使用标准库有以下几种方式:
① using directive
等同于将std的内容全部打开,那么在使用cin和cout的时候我们就可以不在写std::,直接写方法名就好。
②using declaration
使用声明将std一行一行的打开,例如下图中,我们声明了using std::cout,因此下面使用cout的时候就不需要再进行打开,但是由于我们只声明了cout,所以我们使用cin的时候还是需要再次打开std,也就是std::cin。
③not using
我们未使用using,因此在使用每个函数的时候记得加上std。
7.特殊的构造函数explicit
九、组合与继承
类和类之间的关系有三种:
①继承(Inheritance);
②组合(Composition);
③委托(Delegation);
1.组合(Composition):表示has a
在上述例子中,queue的Class中包含了deque这个类,这两个类就属于组合关系。
在上述例子中queue中所有的方法都是改装自deque的,因为deque是双端队列, 是一个功能强大的类。
但是上述例子只是Composition的一个特例,假设一个类又很多种方法,但是我们只将其几个方法进行封装,开放了几个方法,这就是所说的Adapter。
在上图中,我们会将组合的两个类成为Container和Component,这个Container可以认为是我们举例中的queue,Component就是deque类,
在构造函数的调用中,首先调用Component的默认构造函数,再调用Container自己的构造函数(由内到外)。
在析构函数的调用中,首先调用Container自己的析构函数,再调用Component的析构函数(由外到内)。
值得注意的就是:这个顺序其实是编译器帮我们安排好的,默认的构造函数和析构函数的调用都是有编译器执行的。
2.Delegation(委托)Compisition by reference
左边依然有一个右边,但是这个不是很真实(相比于COmposition),因为只是一个指针,这就是一个委托(又称为Composition by reference)。
当我们右边的StringRep无论怎么变动,都不会影响左边的类的调用,也就是所谓的客户端。
(这个委托其实我还没怎么搞懂老师想说什么意思)
3.Inheritance(继承),表示is-a
问题1:struct和class的区别和联系
在上面的例子中,只有数据没有方法,我们想要达到的效果就是子类继承父类的数据,但这样并不是继承最有价值的点,而是跟虚函数搭配使用才能更发挥他的价值。
可以看出继承(Inheritance)和组合(Composition)很像,构造函数和析构函数的调用顺序很像。
在上面的图中也说明了,父类的析构函数必须是virtual,否则就会出现undefined behaviour。原因我们之后再说。
十、虚函数与多态
1.虚函数(virtual)
当我们给一个类中的数据和函数前面加了virtual,那么这样他的这些数据和方法都会被子类继承,在内存方面,数据直接占用了内存的部分;
函数被继承可以理解为调用权给了子类。
当虚函数被重新定义的时候,我们才会用到override关键字。
我们上面给出的例子中有三种类型的函数:
①non-virtual 函数:你不希望子类重新定义(override)它。
②virtual 函数:你希望子类重新定义,并且他有默认定义。
③pure virtual 函数:你希望子类一定要重新定义,并且他木有默认定义。
对于objectID,我们希望这个函数就是用其原本的ID,所以不希望对这个函数进行重新定义。
对于error函数,我们可以重新定义,也可以使用其默认的函数。
但是对于draw函数,我们想要绘制不同的形状,就必须有不同的draw函数,因此我们将其定义为pure virtual。
在上述的例子中,我们使用了读文件的类,这个类首先定义了OnFileOpen方法,这个方法除了一些固定的操作外,还需要子类定义自己的初始化方法,那么我们就需要将Serialize方法定义为pure virtual,这样在每一个子类中我们都必须重写这个方法,从而实现我们想要的功能。
接着我们在main函数里面首先定义CMyDoc类,然后经过一系列操作后,调用了myDoc的OnFileOpen方法,这个方法本质上就是CDocument::OnFileOpen(&myDoc),所以调用地址&myDoc的方法,从而调用了CDocument中的OnFileOpen方法,在执行这个方法的过程中我们需要调用子类中的Serialize方法,(通过this来调用Serialize方法,此处的this是myDoc,所以直接会调用子类的Serialize方法),调用结束后返回main函数,这样的调用过程就结束了。