C++ 中的类和对象 (I)
类
在C++
中可以使用struct
和class
来定义一个类
struct
和 class
的区别
-
struct
的默认成员权限是public
-
class
的默认成员权限是private
struct 创建类对象
C++中创建对象并不像OC中需要Person *person = [Person new]
,而是只需要Person person
就可以创建完成,同时person
对象只占有4个字节的内存空间,因为Person
类中只有一个int
类型的成员变量
可以看到在汇编代码mov dword ptr [ebp-0Ch],14h
执行后其对应的内存已经把20
这个值存放在了对应的4
个字节中
class 创建类对象
调用方法都是一样的,只不过不一样的在于如果使用class
来定义类,其中成员权限都默认是private
如果外部有调用的话需要手动修改权限为public
struct 和 class 的不同
通过查看汇编代码来观察这两者有何不同
使用class
使用struct
汇编代码是一样的,说明两者只是在成员权限上有所区别,但是实际开发中还是使用class
居多
内存分配
刚才的例子中Person
类只有一个成员变量,现在可以多放几个成员变量来观察
此处Perso
n类中有三个成员变量,其占有内存12
个连续的字节
this指针
对象访问和指针访问
这里分别用对象和指针去访问成员变量,看看有何不同
利用对象访问时很容易看出汇编代码拿到person
的首地址开始每次取4
个字节来进行赋值
利用指针访问成员变量时,ebp-14h
是person
对象的地址,先将person
的地址放入eax
寄存器,再将eax
寄存器的内容放入ebp-20h
中,ebp-20h
就是指针p
的地址,然后每次从指针p
中取出person
的地址,同样每次偏移4
个字节来访问成员变量
如何利用指针间接访问所指向对象的成员变量?
-
从指针中取出对象的地址
-
利用对象的地址 + 成员变量的偏移量计算出成员变量的地址
-
根据成员变量的地址访问成员变量的存储空间
指针访问成员变量的本质
接下来来看一个例子
这样会如何打印呢?答案是:10,30,40
首先可以明确使用指针其实并非指向person
的首地址,而是指向了m_age
所在的位置也就是从person
的地址偏移了4
个字节,那么通过指针访问m_id
和m_age
时是从m_age
的位置开始赋值和偏移4
个字节再次赋值,也就是说30
赋值给了m_age
,40
赋值给了m_height
,而m_id
并未得到修改所以依然是10
。
17: Person person;
18: person.m_id = 10;
005A269F C7 45 EC 0A 00 00 00 mov dword ptr [ebp-14h],0Ah
19: person.m_age = 20;
005A26A6 C7 45 F0 14 00 00 00 mov dword ptr [ebp-10h],14h
20: person.m_height = 180;
005A26AD C7 45 F4 B4 00 00 00 mov dword ptr [ebp-0Ch],0B4h
24: Person* p = (Person *) & person.m_age;
005A26BC 8D 45 F0 lea eax,[ebp-10h]
005A26BF 89 45 E0 mov dword ptr [ebp-20h],eax
25: p->m_id = 30;
005A26C2 8B 45 E0 mov eax,dword ptr [ebp-20h]
005A26C5 C7 00 1E 00 00 00 mov dword ptr [eax],1Eh
26: p->m_age = 40;
005A26CB 8B 45 E0 mov eax,dword ptr [ebp-20h]
005A26CE C7 40 04 28 00 00 00 mov dword ptr [eax+4],28h
27: p->m_height = 50;
005A26D5 8B 45 E0 mov eax,dword ptr [ebp-20h]
005A26D8 C7 40 08 32 00 00 00 mov dword ptr [eax+8],32h
通过汇编代码也可以轻松看出其问题所在,那么如果最后一句打印使用指针来调用会如何呢?
也即是p->display()
通过对象调用和使用指针调用两者的区别在于会影响隐藏参数this
而导致不同,内部访问成员变量时其实是会使用this->m_id
这样来使用,那么使用对象调用会传入person
的地址,而指针调用会传入m_age
的地址,那么结果就会打印30,40,50
在调用函数前,一个将地址ebp-14h(person)
传给this
指针,而另一个将ebp-10h(m_age)
传给this
指针,此后在访问成员变量时已经会发生不同,因为访问的地址并不一样后者比前者多偏移4个字节
注意
如果上面不对m_height
进行赋值,则打印出来m_height
会是一个非常小的负数
通过内存来看就是0xcccccccc
,这里0xcccccccc
其实是机器码int3
中断的意思,主要目的是为了在误跳转到此处时防止执行危险的指令,所以一旦跳转到这里直接发生中断,也就是我们看到的断点,一般分配到空间的时候此处的数据是脏数据,需要将其抹掉,所以采用了这种方法。
同时在栈空间分配的时候也一样会使用0xcccccccc
在调用函数时,函数存放在代码区,或者说函数的机器码存放于代码区,但是执行函数要开辟栈空间,因为代码区是只读的,而且只存放函数的机器码,而函数中的变量需要存储空间,所以是需要在开辟栈空间的。
内存空间
每个应用都有自己的独立内存空间,其内存空间大致分为以下几大区域:
-
代码段
:用于存放代码 -
数据段
:用于存放全局变量等 -
栈空间
:每调用一个函数就会分配一段连续的栈空间,等函数调用完成后系统自动回收这段空间 -
堆空间
:需要主动申请和释放
堆空间
malloc
通过malloc申请了16个字节的堆空间,并用指针偏移来赋值
再将上述例子中char *
换成 int *
因为指针类型的不同,每次偏移的量也不同,char *
类型 每次只取1个字节来写入而 int *
每次取4个字节
而且这段代码是在函数中的,所以每次函数调用结束时指针p
将被销毁而堆空间分配的内存则不会,只是没有指针再指向它,可能会造成内存泄露,所以使用malloc
时需要搭配使用free
来及时释放堆空间
new/delete
堆空间的申请和释放总是成对出现的,上面演示了使用malloc
,除了它还有另外的方法就是new
申请一个int
类型的堆空间
int* p = new int;
*p = 10;
delete p;
接下来演示申请一段连续的空间,如下面代码所示即为申请了一段int
型的数组空间
int* p = new int[4];
*p = 10;
*(p + 1) = 20;
*(p + 2) = 30;
*(p + 3) = 40;
delete[] p;
总结
-
malloc
和free
配对使用 -
new
和delete
配合使用 - 如果是申请一段内存空间则是
new[]
和delete[]
配合使用
堆空间的初始化
int size = sizeof(int) * 10;
int* p = (int *)malloc(size);
直接使用malloc来申请堆内存是并未对其空间进行初始化的,可以通过汇编和查看内存看到并未对空间进行初始化
如果是我们需要对堆空间进行初始化的话建议使用memset
函数
int size = sizeof(int) * 10;
int* p = (int *)malloc(size);
memset(p, 1, size);
使用memset
的效果是从指针p
指向的内存空间开始的40
个字节中每一个字节都初始化为1
,如图所示
int* p0 = new int;//未初始化
int* p1 = new int();//初始化为0
int* p2 = new int(5);//初始化为5
int* p4 = new int[3];//数组未被初始化
int* p5 = new int[3]();//数组元素被初始化为0
int* p6 = new int[3]{};//数组元素被初始化为0
int* p7 = new int[3]{ 10 };//数组首元素初始化为10,其他被初始化为0
对象的内存存放位置
对象的内存可以存放于3个地方:
-
全局区(数据段)
:全局变量 -
栈空间
:函数内的局部变量 -
堆空间
:动态申请内存(malloc、new)
构造函数
构造函数(constructor)
:
- 在对象创建的时候自动调用,一般用于完成对象的初始化操作
- 函数名与类名同名,没有返回值,可以有参数,可以重载,可以有多个构造函数
- 一旦定义了构造函数,必须要使用其中一个自定义的构造函数来初始化对象
- 通过
malloc
分配的对象不会使用构造函数 - 在某些特定的情况下,编译器才会为类生成空的无参的构造函数
struct Person {
int m_age;
Person() {
m_age = 0;
cout << "Person()" << endl;
}
Person(int age) {
m_age = age;
cout << "Person(int age)" << endl;
}
};
int main() {
Person person1;
Person person2(10);
return 0;
}
在Person
类中创建两个构造函数,在调用的时候一个不写参数,另一个带参数,那么创建对象的时候就会分别调用两个不同的构造函数,具体在汇编代码中可以看到是调用了两个不同的构造函数
另外如果使用 Person* person = (Person*)malloc(4);
来创建对象的话并不会调用任何构造函数
如果这里将类中的构造方法都删除,这里发现并不会调用汇编默认生成的构造函数
但是如果给类中成员变量一个默认值,情况就会不一样了,编译器此时会添加一个默认的无参构造函数,同时在这个构造函数其中可以看到默认值20
的存在
构造函数的调用
上图中的代码一共创建了7
个Person
对象,其中调用了4
个无参构造函数,3
个有参构造函数,有2
个仅为函数声明
编译器自动生成的构造函数
C++
编译器在某些特定的情况下会给类自动生成无参的构造函数,比如:
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承了其他类
- 包含了对象类型成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)
推荐阅读
-
C++ 中的类和对象 (I)
-
35 岁实现财务*,腾讯程序员手握2300万提前退休?-1000万房产、1000万腾讯股票、加上300万的现金,一共2300万的财产。有网友算了一笔账,假设1000万的房产用于自住,剩下1300万资产按照平均税后20-50万不等进行计算,大约花上26-60年左右的时间才能赚到这笔钱。也就是说,普通人可能奋斗一辈子,才能赚到这笔钱。在很多人还在为中年危机而惶惶不可终日的时候,有的人的35岁,就已经安全着陆,试问哪个打工人不羡慕?但问题是有这样财富积累必然有像样的实力做靠山。没有人可以不劳而获。 看到这里,肯定有人说,那么对于普通人来说,卷可能真就成了唯一的出路。但是卷也有轻松的卷,“偷懒”的卷法,对于程序员而言,刨除掉一时无法改掉的开会传统占用的大部分时间,如何把有限的时间和精力放在真正重要的架构设计、需求设计上,而不是重复的造*,编码、改bug、手动测试。因此在科技改变生活的今天,学会使用AI工具成为程序员们的必备技能。 以全栈式全自动的软件开发工具飞算SoFlu软件机器人为例,作为全球首款面向微服务架构设计和最佳实践的软件机器人,SoFlu软件机器人改变了原来手工编码的作业模式,通过可视化拖拽方式以及参数配置就能实现等同于编写复杂代码的业务逻辑,在设计业务逻辑时就完成了微服务应用开发,做到“业务即图,图即代码”,实现“软件开发,十倍提效”。 而作为飞算SoFlu软件机器人的一个重要组成部分,FuncGPT(慧函数)支持所有类型函数创建。通过自然语言描述Java函数需求,实时生成高质量、高可读性的Java函数代码。生成代码可直接复制到IDEA,或一键导入Java全自动开发工具函数库。 FuncGPT(慧函数)具有五大核心能力: ● 自然语言:通过自然语言即可生成函数,降低软件开发门槛。 ● 秒级函数开发:全自动秒级开发函数,秒级完成,效率千倍提升。 ● 一站式函数开发应用:函数生成即可直接应用,效率、安全有保障。 ● 质量可靠:生成的函数代码符合业界规范、具备良好的可读性,符合最优解。 ● 灵活生成:可以根据开发人员的具体需求生成并支持调整和修改。 以“将文件流转换为File对象”这个需求为例,以下是FuncGPT(慧函数)秒级生成的代码: / 类名:FileConverter // 函数名:convertStreamToFile // 函数功能:将文件流转换为File对象 // POM依赖包:无 import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; public class FileConverter { /** * 将文件流转换为File对象 * @param inputStream 文件流 * @param filePath 文件路径 * @param fileName 文件名 * @return 转换后的File对象 * @throws IOException 如果发生I/O异常 */ public static File convertStreamToFile(InputStream inputStream, String filePath, String fileName) throws IOException { File file = new File(filePath + File.separator + fileName); // 创建File对象 try (FileOutputStream outputStream = new FileOutputStream(file)) { // 创建文件输出流 byte buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { // 从文件流读取数据并写入文件 outputStream.write(buffer, 0, bytesRead); } } return file; // 返回转换后的File对象 } } // 函数示例 // 将文件流转换为File对象示例 // 入参:inputStream,文件流 // 入参:filePath,文件路径 // 入参:fileName,文件名 // 出参:file,转换后的File对象 // 调用示例: // InputStream inputStream = new FileInputStream("example.txt"); // String filePath = "C:\\Users\\User\\Documents"; // String fileName = "example.txt"; // File file = FileConverter.convertStreamToFile(inputStream, filePath, fileName); // System.out.println(file.getAbsolutePath); // 输出结果:例如,将文件流转换为File对象后,文件的绝对路径为:C:\Users\User\Documents\example.txt // 则输出结果为:C:\Users\User\Documents\example.txt 通过分析,不难发现以上代码:
-
详细解释 Python 中的 __getitem__ 方法和切片对象
-
iOS 中字母 I(大写 i)和 l(小写 L)无法区分的问题
-
贪婪算法在 Python、JavaScript、Java、C++ 和 C# 中的多种实现及其在硬币变化、分数骑士、活动选择和使用哈夫曼编码的最小生成树问题中的应用实例
-
微信 "扫一扫 "物联网,全面揭秘 "扫一扫 "背后的扫盲技术!-1.1 扫一扫感知物体是做什么的? 1.1 微信扫一扫是做什么的? 扫一扫识物是指以图片或视频(商品图片:鞋/包/美妆/服饰/家电/玩具/图书/食品/珠宝/家具/其他商品)为输入媒介,挖掘微信内容生态中的有价值信息(电商+百科+资讯,如图1所示),并展示给用户。这里的电商基本涵盖了微信小程序覆盖上亿SKU的全量优质电商,可以支持用户货比N家并直接下单购买,百科和资讯则聚合了微信内的头部自媒体如搜狗、搜搜、百度等,向用户展示和分享拍摄商品相关的内容资讯。 图 1 扫一扫识别功能示意图 欢迎大家更新iOS新版微信→扫一扫→识货,亲自体验,也欢迎大家通过识货界面的反馈按钮向我们提交反馈意见。 扫一扫识物实景图展示 1.2 扫一扫识物有哪些使用场景? 扫一扫识物的目的是为用户访问微信内部生态内容开辟一个新窗口,以用户扫图片为输入形式,为用户提供微信生态内容中的百科、资讯、电商等作为展示页面。除了用户熟悉的扫一扫操作外,我们还将进一步拓展长按操作,让用户更方便地进行扫一扫操作。"扫一扫知事 "的落地场景主要涵盖三大部分: a. 科普知识: a.科普知识。用户通过扫一扫,可以在微信生态圈中获取该对象的百科、资讯等常识或趣闻,帮助用户更好地了解该对象; b.购物场景。同样的搜索功能支持用户看到喜欢的商品立即检索到微信小程序电商中的同款商品,支持用户即扫即购; c.广告场景。扫一扫识别物体可以辅助公众号文章、视频更好地理解其中蕴含的图片信息,从而更好地投放匹配广告,提高点击率。 1.3 Sweep Sense 为 Sweep 家族带来了哪些新技术? 对于扫一扫来说,大家耳熟能详的应该就是扫一扫二维码、扫一扫小程序码、扫一扫条形码、扫一扫翻译了。无论是各种形式的编码还是文字字符,都可以看作是图片的一种特定编码形式,而物的识别则是对自然场景图片的识别,这对于扫一扫家族来说是一个质的飞跃,我们希望从物的识别入手,进一步拓展扫一扫对自然场景图片的理解能力,比如扫酒、扫车、扫植物、扫人脸等服务,如下图3所示。 图 3 Sweep 家族
-
一种结构设计模式,允许在对象中动态添加新行为。它通过创建一个封装器来实现这一目的,即把对象放入一个装饰器类中,然后把这个装饰器类放入另一个装饰器类中,以此类推,形成一个封装器链。这样,我们就可以在不改变原始对象的情况下动态添加新行为或修改原始行为。 在 Java 中,实现装饰器设计模式的步骤如下: 定义一个接口或抽象类作为被装饰对象的基类。 公共接口 Component { void operation; } } 在本例中,我们定义了一个名为 Component 的接口,该接口包含一个名为 operation 的抽象方法,该方法定义了被装饰对象的基本行为。 定义一个实现基类方法的具体装饰对象。 公共类 ConcreteComponent 实现 Component { public class ConcreteComponent implements Component { @Override public void operation { System.out.println("ConcreteComponent is doing something...") ; } } 定义一个抽象装饰器类,该类继承于基类,并将装饰对象作为一个属性。 公共抽象类装饰器实现组件 { protected Component 组件 public Decorator(Component component) { this.component = component; } } @Override public void operation { component.operation; } } } 在这个示例中,我们定义了一个名为 Decorator 的抽象类,它继承了 Component 接口,并将被装饰对象作为一个属性。在操作方法中,我们调用了被装饰对象上的同名方法。 定义一个具体的装饰器类,继承自抽象装饰器类并实现增强逻辑。 公共类 ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component 组件) { super(component); } } public void operation { super.operation System.out.println("ConcreteDecoratorA 正在添加新行为......") ; } } 在本例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,它继承自装饰器抽象类,并实现了操作方法的增强逻辑。在操作方法中,我们首先调用被装饰对象上的同名方法,然后添加新行为。 使用装饰器增强被装饰对象。 公共类 Main { public static void main(String args) { Component 组件 = new ConcreteComponent; component = new ConcreteDecoratorA(component); 组件操作 } } 在这个示例中,我们首先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传递。最后,调用装饰器的操作方法,实现对被装饰对象的增强。 使用场景 在 Java 中,装饰器模式被广泛使用,尤其是在 I/O 中。Java 中的 I/O 库使用装饰器模式实现了不同数据流之间的转换和增强。 让我们打开文件 a.txt,从中读取数据。InputStream 是一个抽象类,FileInputStream 是专门用于读取文件流的子类。BufferedInputStream 是一个支持缓存的数据读取类,可以提高数据读取的效率,具体代码如下: @Test public void testIO throws Exception { InputStream inputStream = new FileInputStream("C:/bbb/a.txt"); // 实现包装 inputStream = new BufferedInputStream(inputStream); byte bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); } } } } 其中 BufferedInputStream 对读取数据进行了增强。 这样看来,装饰器设计模式和代理模式似乎有点相似,接下来让我们讨论一下它们之间的区别。 第三,与代理模式的区别: 代理模式的目的是控制对对象的访问,它在对象外部提供一个代理对象来控制对原对象的访问。代理对象和原始对象通常实现相同的接口或继承相同的类,以确保两者可以相互替换。 装饰器模式的目的是动态增强对象的功能,而这是通过对象内部的包装器来实现的。在装饰器模式中,装饰器类和被装饰对象通常实现相同的接口或继承自相同的类,以确保两者可以相互替代。装饰器模式也被称为封装器模式。 在代理模式中,代理类附加了与原类无关的功能。
-
正负偏差变量 即 d2+、d2- 分别表示决策值中超出和未达到目标值的部分。而 di+、di- 均大于 0 刚性约束和目标约束(柔性目标约束有偏差) 在多目标规划中,>=/<= 在刚性约束中保持不变。当需要将约束条件转换为柔性约束条件时,需要将 >=/<= 更改为 =(因为已经有 d2+、d2- 用来表示正负偏差),并附加上 (+dii-di+) 注意这里是 +di、-di+!之所以是 +di,-di+,是因为需要将目标还原为最接近的原始刚性约束条件 优先级因素和权重因素 对多个目标进行优先排序和优先排序 目标规划的目标函数 是所有偏差变量的加权和。值得注意的是,这个加权和都取最小值。而 di+ 和 dii- 并不一定要出现在每个不同的需求层次中。具体分析需要具体问题具体分析 下面是一个例子: 题目中说设备 B 既要求充分利用,又要求尽可能不加班,那么列出的时间计量表达式即为:min z = P3 (d3- + d3 +) 使用 + 而不是 -d3 + 的原因是:正负偏差不可能同时存在,必须有 di+di=0 (因为判定值不可能同时大于目标值和小于目标值),而前面是 min,所以只要取 + 并让 di+ 和 dii- 都为正值即可。因此,得出以下规则: 最后,给出示例和相应的解法: 问题:某企业生产 A 和 B 两种产品,需要使用 A、B、C 三种设备。下表显示了与工时和设备使用限制有关的产品利润率。问该企业应如何组织生产以实现下列目标? (1) 力争利润目标不低于 1 500 美元; (2) 考虑到市场需求,A、B 两种产品的生产比例应尽量保持在 1:2; (3)设备 A 是贵重设备,严禁超时使用; (4)设备 C 可以适当加班,但要控制;设备 B 要求充分利用,但尽量不加班。 从重要性来看,设备 B 的重要性是设备 C 的三倍。 建立相应的目标规划模型并求解。 解:设企业生产 A、B 两种产品的件数分别为 x1、x2,并建立相应的目标计划模型: 以下为顺序求解法,利用 LINGO 求解: 1 级目标: 模型。 设置。 variable/1..2/:x;! s_con_num/1...4/:g,dplus,dminus;!所需软约束数量(g=dplus=dminus 数量)及相关参数; s_con(s_con_num);! s_con(s_con_num,variable):c;!软约束系数; 结束集 数据。 g=1500 0 16 15. c=200 300 2 -1 4 0 0 5; 结束数据 min=dminus(1);!第一个目标函数;!对应于 min=z 的第一小部分;! 2*x(1)+2*x(2)<12;!硬约束 @for(s_con_num(i):@sum(variable(j):c(i,j)*x(j))+dminus(i)-dplus(i)=g(i)); !使用设置完成的数据构建软约束表达式; ! !软约束表达式 @for(variable:@gin(x)); !将变量约束为整数; ! 结束 此时,第一级目标的最优值为 0,第一级偏差为 0: 第二级目标: !求 dminus(1)=0,然后求解第二级目标。 模型。 设置。 变量/1..2/:x;!设置:变量/1..2/:x; ! s_con_num/1...4/:g,dplus,dminus;!软约束数量及相关参数; s_con(s_con_num(s_con_num));! s_con(s_con_num,variable):c;! 软约束系数; s_con(s_con_num,variable):c;! 结束集 数据。 g=1500 0 16 15; c=200 300 2 -1 4 0 0 5; 结束数据 min=dminus(2)+dplus(2);!第二个目标函数 2*x(1)+2*x(2)<12;!硬约束 @for(s_con_num(i):@sum(variable(j):c(i,j)*x(j))+dminus(i)-dplus(i)=g(i)); ! 软约束表达式;! dminus(1)=0; !第一个目标结果 @for(variable:@gin(x)); ! 结束 此时,第二个目标的最优值为 0,偏差为 0: 第三目标 !求 dminus(2)=0,然后求解第三个目标。 模型。 设置。 变量/1..2/:x;!设置:变量/1..2/:x; ! s_con_num/1...4/:g,dplus,dminus;!软约束数量及相关参数; s_con(s_con_num(s_con_num));! s_con(s_con_num,variable):c;! 软约束系数; s_con(s_con_num,variable):c;! 结束集 数据。 g=1500 0 16 15; c=200 300 2 -1 4 0 0 5; 结束数据 min=3*dminus(3)+3*dplus(3)+dminus(4);!第三个目标函数。 2*x(1)+2*x(2)<12;!硬约束 @for(s_con_num(i):@sum(variable(j):c(i,j)*x(j))+dminus(i)-dplus(i)=g(i)); ! 软约束表达式;! dminus(1)=0; !第一个目标约束条件; ! dminus(2)+dplus(2)=0; !第二个目标约束条件 @for(variable:@gin(x));! 结束 最终结果为 x1=2,x2=4,dplus(1)=100,最优利润为
-
全面了解 python 中的类、对象、方法和属性。
-
[C++ Primer] 解释 C++ 和 C++ 中浮点数的有效数字