当前位置: 首页 > ai >正文

侯捷---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)


一、头文件与类的声明

c和c++的区别
c++Programs代码基本形式

二、构造函数

1.inline函数

inline(内联)函数会比较快,如果函数太复杂,编译器无法将其变成inline函数
inline函数

2.访问级别

①private(数据);
②public(函数);
访问级别

3.构造函数

构造函数写法独特:
①函数名称与类的名称相同;
②没有返回类型(构造函数是用来创建对象的);
③以初值列(初始化)的方式进行 — 更正确,更大气;

注意:
①默认实参不是构造函数的特性,其他的函数也可以这样做。
②使用初值列和赋值都可以,但是使用初值列效率更高。
构造函数

4.重载

构造函数和函数可以有很多个。
例如下面的第二个①②:
我们可以定义这两个方法,那么为什么同名的函数可以定义呢?因为编译器会将其编译成不用的实际名称,如下图;

但是下面的第一个①②,就不能这么写,因为我们如果使用
complex c1; complex c2();
创建对象时,我们没有给任何参数,编译器会分不清调用哪个函数,因此这种情况就就不能写成②。
重载
编译器编译后的方法

5.把构造函数放在private中

把构造函数放在private中看似是不应该的,但是当我们使用单例模式的时候,就会将构造函数放在private中。
外界通过A::getInstance()调用这个类里面的函数。
Singleton单例模式

三、参数传递和返回值

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里面的属性。
友元friend
注意:相同class的各个object互为友元(friends)
下面黄色的部分直接使用了complex的private属性,可以通过上面这句话进行解释。
相同class的各个object互为友元(friends)

什么情况下可以使用pass by reference,使用情况下可以return by reference。
由于下面__doapl的第一个参数非const,那么我们可以将操作后的返回值定为ths。
class body外的各种定义

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

1.操作符重载-1:成员函数

所有的成员函数一定带着一个隐藏的参数,也就是我们下面标出来的this,指向函数的调用者,通过下面的方式,我们可以对+=进行重载,从而实现两个复数的相加操作。
成员函数

2.传递者无需知道接收者是以reference形式接受

在__doapl函数中,ths是一个指针,ths是指针所指的东西,但是这个函数的返回值声明中写的是返回的是reference类型,因为传递者无需知道接收者是以什么形式接受的
那么按照这个意思,我们也可以将重载+=的符号的返回值设置为void,这样貌似是可以的,但是我们如果使用c3 += c2 += c1,时,将返回值设置为void似乎就不行了,因为我们在上面的式子中,先将c1加到c2,再将c2加到c3,那么此时c2就必须是complex&类型的。
return by reference语法分析
重点:
当类名后面出现
时,意味着这是一个指向该类对象的指针。指针的作用是存储对象的内存地址,而非对象本身。要访问对象的成员,需借助->操作符。
若类名后面是&,表示这是该类对象的引用。引用相当于对象的别名,必须在定义时就进行初始化,并且之后无法再引用其他对象。引用访问对象成员使用的是.操作符。

3.操作符重载-2:非成员函数

下面这些函数绝不可以return by reference,因为,他们返回的必定是个local object,因为我们需要在函数中创建返回的对象,离开这个函数之后,如果只是使用其reference,它的值已经不在了,那么返回值也就不存在了。
非成员函数

4.typename():就是临时对象(temp object)的创建。

例如下面标黄色的部分,他们的生命周期进行到下一行就结束了。
temp object

5.class body之外的各种定义

在下面的operator +中,没有创建任何的temp object,所以其可以return by reference.(所以这块可以更改)
在下面的operator -中,创建了temp object,所以其一定不可以return by reference.

class body之外的各种定义
class body之外的各种定义
对于 << 这种特殊的成员操作符,只能将其定义为全局的函数。
cout就是&ostream类型的对象。
在下面我们重载 << 符号的时候,我们不能将os设置为const,因为我们在函数体内,更改其输出的过程中就已经更改了os的对象结构。
同时,和上面的 += 类型,我们本可以将 << 的重载的返回值设置为void,因为我们在这个函数里面输出就可以了,不需要关心他出来还在不在,但是综合考虑,如果我们要多个 << 连用,就必须将其设置为complex&。
class body之外的各种定义
我的总结:我们定义函数的时候,首先考虑返回值是否是reference,通过查看函数体的定义,如果不行的话,在使用value作为返回值。

五、三大函数:拷贝构造,拷贝复制,析构

1.拷贝构造和拷贝复制的差别

s3第一次出现是通过拷贝构造,s3第二次出现是通过拷贝复制(=);
如果class里面带指针,一定不能使用编译器默认的拷贝,而是需要自己写。
拷贝构造和拷贝复制的差别---s3

2.解析三个特殊函数:

下面public中,第一个函数是本身的构造函数,第二个函数就是我们所说的拷贝构造,第三个函数就是进行了操作符重载,也就是我们所说的拷贝复制,(第二个和第三个函数的参数都是string类型的),第四个函数就是析构函数,当这个类生成的对象死亡的时候,析构函数就会被调用。
三个特殊函数

3.构造函数和析构函数

在下面三个String对象中,前两个对象离开作用域之后就会自动delete,而通过new分配的对象需要手动delete。
因此需要调用三次析构函数。构造函数和析构函数 就像下面使用了默认的拷贝之后就会出现浅拷贝的情况,导致两个指针只想同一片区域,从而当我们修改数据的时候,两个指针指向的同一块数据都会修改。
所以编译器给你的拷贝只是将指针赋值过去,也就是所谓的浅拷贝
在这里插入图片描述

4.拷贝赋值函数

首先需要将自己本来的内存清空,然后创建足够的内存空间,最后将赋值的值添加到自己里面去。
拷贝赋值函数
那么上面的自我赋值是什么呢?也就是将自己赋值给自己嘛,如果这样做的话,效率会更高
那如果不写这个,结果是否会出错呢?因为我们会在下面先将自己杀掉,但是如果左右是同一个,那么我删了右边,左边也就是空了呀,那第二步就会发现右边的值就会被删除了

六、堆,栈与内存管理

1.堆和栈

通过new分配的对象存储在堆(heap)中。
下面的c1也就是所谓的stack object,其生命在作用域结束,这个作用域内的object,又称为auto object,因为它会被自动清理。
堆和栈
static local object的构造函数在程序结束的时候会被调用。
static的对象特性
全局变量
new的对象一定要通过delete将其删除(左边为正确做法),否则会造成内存泄漏。
heap objects的生命周期

2.new和delete的用法

new:先分配内存,再调用构造函数
new生成object的过程
delete:先调用析构函数,再释放memory。
delete删除object

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):
动态分配的array
在第一列中 8 * 3表示数组中有三个complex,32 + 4也是之前说过的, 4 *2也是分配的地址空间,那最后的4是什么呢?因为在VC中,会单独定义数组的大小,所以会有4个字节存储数组的大小。(第一列和第二列的区别我们上面已经说过了)
为什么要使用delete【】
如上图所示,如果不使用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

static的内容补充
这块侯捷老师举了个例子,他说假如每个银行会有10w人存钱,那么每一个用户都需要创建一个成员变量,来存储存的钱,但是银行的利率是固定的,不需要为每一个用户添加一个这个变量,那么这个利率我们就存成static members。
同时相比于非静态成员函数(non-static member functions),静态成员函数(static member functions)没有this变量,因此它只能访问静态变量。
静态static的使用
上述static函数的使用方法中,直接通过class name调用也是可以的

2.Singleton(单例模式)

使用单例模式的目的:该Class只希望生成一个对象。
Singleton
因此我们将他的构造函数放在private中,同时创建一个static的对象a,此时没有任何类可以创建A的对象,此时只有private中唯一的类对象,而且外界也不能访问这个类对象。
那我们应该怎么在其他类中取得这个类对象呢,我们在public中放入一个static的getInstance方法,得到这个唯一的子集。
外界通过A::getInstance().setup()方法来调用setup方法。
但是这个方法不是很完美,因为如果没有类调用的话,这个static对象也已经被创建了,于是有了下面的优化方法:
改进后的static方法
如果没有任何人使用的话,这个单例就不会被创建,只有有其他类使用了这个单例的getInstance方法,这个单例才会被创建。

3. cout

为什么cout可以输出那么多东西呢?
cout的标准库
cout继承自ostream
cout就是一种ostream,然后我们去看ostream的定义,会发现它会接受很多类型,如上上图。

4.class template(类模板)

类模板
template是一个关键字,typename也是一个关键字,用来告诉编译器T目前是一个没有确定的类型,同时使用T来定义complex中的数据类型以及函数返回值,从而增加灵活性。
当我们使用complex类的时候,我们可以绑定double或者int之类的,如上图所示。
“模板会造成代码的膨胀”

5.function template (函数模板)

template function
与上面的类模板类似,我们使用了上述的函数模板。
template 也是使用了相同的关键字。
在类模板的使用时会明确指出类中变量的类型,但是在函数模板中并没有明确的指出,因此编译器会对template function进行实参的推导(argument deduction)。
那么又有一个问题:我们进行比较时怎么知道两个stone类型的大小关系呢,这是stone里面的操作符重载就发挥作用了。
操作符重载

在c++标准库中的算法全部都是函数模板的形式

6.namespace

我们可以自己声明一个namespace,然后将里面的内容包围起来,确保不会和别人的namespace发生冲突。
std就是将标准库所有的东西都包含进去了。
那么使用标准库有以下几种方式:

① using directive

等同于将std的内容全部打开,那么在使用cin和cout的时候我们就可以不在写std::,直接写方法名就好。
using directive

②using declaration

使用声明将std一行一行的打开,例如下图中,我们声明了using std::cout,因此下面使用cout的时候就不需要再进行打开,但是由于我们只声明了cout,所以我们使用cin的时候还是需要再次打开std,也就是std::cin。
using declaration

③not using

我们未使用using,因此在使用每个函数的时候记得加上std。

7.特殊的构造函数explicit

九、组合与继承

类和类之间的关系有三种:
①继承(Inheritance);
②组合(Composition);
③委托(Delegation);

1.组合(Composition):表示has a

组合关系
在上述例子中,queue的Class中包含了deque这个类,这两个类就属于组合关系。
在上述例子中queue中所有的方法都是改装自deque的,因为deque是双端队列, 是一个功能强大的类。
但是上述例子只是Composition的一个特例,假设一个类又很多种方法,但是我们只将其几个方法进行封装,开放了几个方法,这就是所说的Adapter。
Composition例子求组合类的大小
Composition关系下的构造和析构函数
在上图中,我们会将组合的两个类成为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函数,这样的调用过程就结束了。
对上述例子的模拟

http://www.xdnf.cn/news/16089.html

相关文章:

  • 制造工厂高效出图新技术——共享云桌面
  • JavaSE:学习输入输出编写简单的程序
  • list 介绍 及 底层
  • ESP32使用 vscode IDF 创建项目到烧录运行全过程
  • Flink-1.19.0源码详解8-ExecutionGraph生成-前篇
  • RabbitMQ简述
  • vue3 el-table 列数据合计
  • 在一个网页浏览器的控制台测试后端java是否支持SSE服务
  • Vulnhub Matrix-Breakout-2-Morpheus靶机攻略
  • 基于规则架构风格对业务的重构
  • 每日算法刷题Day52:7.24:leetcode 栈5道题,用时1h35min
  • RPG64.制作敌人攻击波数四:优化
  • 让复杂 AI 应用构建就像搭积木:Spring AI Alibaba Graph 使用指南与源码解读
  • 企业级数据分析创新实战:基于表格交互与智能分析的双引擎架构
  • es0102---语法格式、数据类型、整合springboot、创建库、创建映射、新增数据、自定义查询
  • 【Qt开发】信号与槽(一)
  • node.js中的fs与path模块
  • 主流摄像头协议及其开源情况,GB/T 28181协议介绍
  • 云原生MySQL Operator开发实战(一):Operator基础与CRD设计
  • [语言模型训练]基于 PyTorch 的双向 LSTM 文本分类器实现:基于旅店的评论分类语言模型
  • Java_多线程_生产者消费者模型_互斥锁,阻塞队列
  • Java与NLP实战:文本处理到情感分析全解析
  • Ethereum: 从 1e+21 到千枚以太币:解密 Geth 控制台的余额查询
  • 适配器模式——以springboot为例
  • 《云计算蓝皮书 2025 》发布:云计算加速成为智能时代核心引擎
  • MySQL--day13--视图存储过程与函数
  • 垃圾回收GC
  • 【AI News | 20250722】每日AI进展
  • Java应用程序内存占用分析
  • 什么是HTTP长连接、短连接?谁更能抗DoS攻击?