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

对象 普通po转_谈谈C++对象的构造

70c07c5f1e48987172ea27f666bd91b7.png

造化从来自有神

如何对此亦无尘

平生出处皆非妄

老去功名始见真

b6b0bf36c9ae6c2b3d8a6236ab119cc1.png

这是小编以“构造对象”为主题让九歌同学创作的七言绝句。九歌同学以一种非常玄妙的文风向我们介绍了对象的构造,但这似乎并不是我们想要的答案。那么我们每次码代码都在进行的“对象构造”究竟是怎样一回事呢?就让小编带领大家来了解一下吧!

071e5e7f4f8e98673abfab411c5eb113.png

什么是对象的构造?

对象的构造过程就是对象的创建过程。在对象构造的过程中,我们需要进行对象数据成员的初始化,因此对象构造的关键问题就是要解决对象的成员如何初始化的问题。

a9e3532bd811ae1a08c3ed0cd241a459.png

C++中是如何解决对象成员初始化问题的?

我们可以对照着C语言中结构体数据成员初始化的解决方案来看。

假定我们有如下的结构体:

687fb5dce807d50d85a67b633bd7ab1d.png

C语言的程序员们有两种对其进行初始化的方法。

其一是顺序初始化:

e521dd43529f62cee971ee4a5c22b7eb.png

其二是指定初始化:

ee6799db2de89c3e38ae3f5c368f8192.png

我们可以注意到,使用顺序初始化方法往往不够灵活,需要逐一列出所有成员的初始值;而指定初始化法不被很多C++编译器兼容,例如小编使用的Visual Studio就不兼容这种写法,会报错。

在C++中,由于成员函数的引入,我们可以在类中定义一个成员函数用来进行类的初始化。但这样会带来一个问题,假如在对象被创建之后我们忘记调用这个成员函数,那么对象数据成员的值将是不确定的,对于一个大型的工程来说,这可能造成很大的麻烦。这是由于对象的创建以及成员的初始化分离了。此外,对于一些必须进行初始化的成员,例如const类型和引用类型的成员,它们的初始化需要和对象同步进行,这种先创建对象在初始化成员的做法也会带来问题。

3095c10589d6ec3baab7e513393a872c.png

构造函数——千呼万唤始出来

•  构造函数的一般特征

构造函数是一类特殊的成员函数。相较于一般的成员函数,构造函数有这样两个显著的特征:

1、所有构造函数都没有返回类型的声明;

2、构造函数仅在对象被创建时被调用。

我们来看一个最简单的构造函数的例子:

96b593f196544fa8a04fdb61a61a205c.png

在这个例子中,构造函数被调用时,会首先创建A这个对象(此时a中的数据成员尚未被赋值),然后执行构造函数的函数体,将A的数据成员a赋值为1,并输出相关语句。我们可以观察到构造函数的一些基本特征:没有返回类型的声明,且函数名称与类名相同。

  • 多种类型的构造函数

C++中对象被创建的方式是多种多样的,因此构造函数的形式也是多种多样的。接下来就让我们看一看为了完成对象创建这个任务,都有哪些类型的构造函数吧!

默认构造函数:

当且仅当类中没有定义构造函数时,编译器会提供一个默认的构造函数,这个构造函数没有参数,并且函数体也是空的。在上面的例子中,这个构造函数就类似于:Test(){};

无参构造函数:

上面讲到的默认构造函数就是一种无参构造函数。需要注意的是,如果参数表中的形参均具有默认值,此时我们也可以将它当做无参的构造函数来调用:

a8d9a8314050947f0cb4c0dfb7c3223a.png

有参构造函数:

我们通过一个例子来感受一下有参构造函数的使用以及初始化列表的作用。

24c833db769e8ce7890eb210762c191c.png

在这个例子中,我们声明的类Test有三个数据成员,其中有一个是引用成员,一个是const类型的成员,这两种类型的数据成员在定义时就必须进行初始化,因此我们不能在构造函数的函数体内将它们初始化,而应当在初始化列表中将它们初始化,这是因为在初始化列表中进行的初始化工作是在对象创建完成之前进行的,而在函数体中进行初始化是在对象创建之后进行的。

一般来说声明了带参数的构造函数之后我们最好还要定义一个默认构造函数,以防止系统编译时出错。

952d1038ee04a3171eb733a420474a07.png

拷贝构造函数——我来组成一支大军!

•为什么需要拷贝构造函数?

对于普通类型的变量而言,变量的复制是容易的,利用一个变量来初始化一个新变量也是容易的:

a0376060e3b37b64ed27d10dbd07f4b1.png

但正如对象的创建需要构造函数一样,对象内部因为有各种成员变量,其复制过程也比普通的变量要复杂,因此通过复制的方法使用一个对象去初始化另一个对象就需要用到拷贝构造函数。

我们看一个简单的例子:

d2ddf916e6b0ecf977eceae1a9069053.png

在这个例子中,以“const Test& t”为参数的函数就是一个简单的拷贝构造函数。拷贝构造函数的一般特征是:

1、参数中必须含有本类对象的一个引用,可以含有其他参数,但这些参数必须有默认值;

2、函数名与类名相同,且没有返回类型的声明。

因为使用了引用,且拷贝构造函数一般不希望改变被复制的对象,因此我们往往在这个参数前加上“const”修饰符。

  • 拷贝构造函数的调用时机

拷贝构造函数在下3种情况下会被调用:

1、用类的对象初始化类的另一个对象:在上面举出的例子中我们可以看到,当执行

Test B=A;

时,拷贝构造函数会被调用。此外,如果我们把这个语句改写成:

Test B(A);

拷贝构造函数也会被调用。这两种写法执行的操作完全相同。

2、某个函数的形参是类的对象,当这个函数被调用,进行形实结合时也会调用拷贝构造函数:

0509d726cf212a4ae71f88a99c4735a8.png

当调用函数 f(Test a) 时,会调用Test类的拷贝构造函数。只有参数使用值传递时才会调用拷贝构造函数,使用引用传递则不会,因此在传递较大的对象时,一般采用引用传递的策略。

3、函数的返回值是类的对象,在返回值时会调用拷贝构造函数。例如:

95398da77265b946f1bce57be29a265c.png

在这个例子中,执行语句

b=f(A);

时,当传递参数时拷贝构造函数不会被调用(因为使用的是引用传递),但函数返回对象时拷贝构造函数会被调用。

  • 深拷贝和浅拷贝

当我们定义一个类时,系统一般会给出一个默认的拷贝构造函数,这个构造函数可以实现成员对象简单的一一复制。如果成员中没有动态成员的话,这个一一复制的结果就是我们想要的,但如果成员中有一个指针指向了一片动态开辟的存储空间,使用系统默认的拷贝构造函数就会出现问题:

假设我们定义的类Test中有一个指针变量p,且我们先创建了一个对象A。A中的指针p在对象使用的过程中指向的是一片动态开辟的存储空间,那么在我们执行语句

Test B=A;

时,B中的指针p也会指向同一片存储空间。

假设我们在析构函数的函数体中试图释放这片存储空间,那么由于B和A中的指针p指向了同一片空间,而同一片空间不可以被释放两次,系统就会报错。(关于析构函数的内容,可以参考文章的下一部分)

同样的,假如B和A的指针指向的存储空间是一样的,那么A.p指向的内容改变的话,B.p指向的内容也会被改变,这显然不是我们想要的。

因此我们想要实现的是,拷贝过程中,A和B中的动态变量指向的内容相同,但指针值本身不同。这就是所谓的“深拷贝”。

我们可以考虑以下的方式来定义一个深拷贝函数:

42d5e668ea75418d9d793063144a88b3.png

在这个例子中,拷贝构造函数的函数体使得复制出来的对象的指针p指向的内容与被复制对象的指针p指向的内容一致,但两者本身指向的存储空间是不同的,也就实现了深拷贝。

ea691dc81ed9d7d8e6405492d6dc9982.png

~析构函数——我有神奇小尾巴~

•初识析构函数

类的析构函数,它是类的一个成员函数,名字由波浪号加类名构成,是执行与构造函数相反的操作:释放对象使用的资源,并销毁非static成员。

析构函数有这样几个特征:

1.函数名是在类名前加上~,无参数且无返回值,因此析构函数不能重载。

2.一个类只能有且有一个析构函数,如果没有显式的定义,系统会生成一个缺省的析构函数。

我们看一个简单的析构函数的例子:

d6420503280535eafe154d3a824a2752.png

在讨论构造函数时我们提到过,构造函数的函数体是在对象初始化完成之后执行的,这也是某些数据类型的成员不能通过函数体进行初始化的原因。与构造函数相反,析构函数的函数体是在对象被销毁前执行的,并且成员的销毁顺序也是与初始化的顺序相反的。

  • 释放资源?

我们知道析构函数的作用是销毁不再使用的对象(好残忍),释放系统资源,那么析构函数是怎么做到这一点的呢?

其实小编也不知道 如果是对象中非指针类型的成员,那么在执行析构函数之后,这些成员占用的内存空间会被释放;但有一类情形是,假如对象中有指针类型的成员,并且它指向的是一块动态开辟的存储空间,那么这块空间必须由我们手动释放,也就是说需要在析构函数的函数体中释放这块存储空间。

50a17dfd388f7b9edefa3f7f83fd324a.png

小编弱语

关于对象的构造和析构,其实还有很多可聊的,但由于小编实力有限,暂时只能和大家聊这么多啦!最后,献上九歌同学的一首五言绝律诗《你在哪里啊,我的对象》,向大家告别~

6a0308cce09443c92dd733daa6f9536c.png

你在哪里啊,我的对象?

九歌

一自伤春眼,无人识此心
山林随日转,云雨隔年沉
世路皆新鬼,浮生付乱禽
凭君莫剪伐,吾已负初音

e493ca16-2652-eb11-8da9-e4434bdf6706.svge493ca16-2652-eb11-8da9-e4434bdf6706.svg

文字 | 周航

审核|刘政宁

指导老师|李超

d13b727a7d33ec11f2eec877819a9741.png

d943c10beec3e652be05a2336ea9f11e.png


http://www.taodudu.cc/news/show-647659.html

相关文章:

  • 【physx/wasm】在physx中添加自定义接口并重新编译wasm
  • excel---常用操作
  • Lora训练Windows[笔记]
  • linux基础指令讲解(ls、pwd、cd、touch、mkdir)
  • InnoDB 事务处理机制
  • 启明云端ESP32 C3 模组WT32C3通过 MQTT 连接 AWS
  • 内存条ar开头的如何看大小_软网推荐:明明白白看内存
  • if else 简写_15+ JS简写骚操作,让你的代码“秀”起来??
  • iextensionunit类_Java ICompilationUnit.reconcile方法代码示例
  • 报错 插入更新_自增主键,三类插入测验答案,在这里。
  • 事务连接中断_HTTP长连接和短连接
  • 亚马逊评价抓取插件_亚马逊运营必备插件
  • controller需要捕获异常吗_Spring之Controller异常处理
  • mysql8中文排序_mysql中utf8编码的中文字段按拼音排序
  • html 分级切换菜单_FL studio系列教程(十六):FL Studio查看菜单讲解
  • mysql还原数据库后日期显示3000_mysql 直接从date 文件夹备份表,还原数据库之后提示 table doesn`t exist的原因和解决方法...
  • java 函数名调用_粉丝提问|c语言:如何定义一个和库函数名一样的函数,并在函数中调用该库函数...
  • docker mysql sock_docker mysql安装
  • mysql最大述_mysql最大字段数量及 varchar类型总结
  • php协程实现mysql异步_swoole与php协程实现异步非阻塞IO开发
  • mysql中xml类型_使用 SQLXML 数据类型
  • mysql语法6_全面接触SQL语法(6)_mysql
  • sqlerver mysql_转 MYSQL学习(一)
  • tcpdump 识别成dns_dns tcpdump
  • java linkedhashmap_java学习-hashMap和linkedHashMap
  • 简单java题_java
  • java actionsupport_struts2中的Action接口和Actionsupport接口各有什么作用
  • java jar log4j_使用Log4j
  • java课程设计进程管理_GitHub - Shadow-Java/OS: 操作系统课程设计,关键词:进程同步与互斥、进程死锁、LRU页面替换算法、时间片轮转算法、时钟等...
  • mysql中建立索引的原则_在SQL数据库中设定索引的原则是什么?(注意是设定不是创建)...
  • java宝典_JAVA宝典之_JAVA基础
  • java blockingqueue_Java多线程进阶(三一)—— J.U.C之collections框架:BlockingQueue接口...
  • java 转账 锁_Java多线程 多个人转账发生死锁
  • java 静态变量 存储_Java学习笔记9---类静态成员变量的存储位置及JVM的内存划分...
  • java坐标移动题目case_坐标移动
  • java代码实现购物车小程序_使用Taro实现小程序商城的购物车功能模块的实例代码...