【C++面向对象】封装(上):探寻构造函数的幽微之境

 

每文一诗  💪🏼

       我本将心向明月,奈何明月照沟渠  —— 元/高明《琵琶记》

        译文:我本是以真诚的心来对待你,就像明月一样纯洁无瑕;然而,你却像沟渠里的污水一样,对这份心意无动于衷,甚至于不屑一顾。


如果本文对你有所帮助,那能否支持一下老弟呢,嘻嘻🥰

✨✨个人主页 点击✨✨

封装

封装作为C++面向对象的三大特性之一

封装将数据和操作数据的代码组合成一个独立的单元,也就是类。类的成员可以有不同的访问权限,如公有(public)、私有(private)和受保护(protected),以此来控制外部对这些成员的访问。

类的结构

class 类名{ 访问权限: 属性 / 行为 };

  • class类名:指的是类的名称 这个名称可以随意命名例如class hunman
  • 访问权限指的是:如公有(public)、私有(private)和受保护(protected)
  • 属性和行为是指的在这个类当中所定义的变量和方法。

 例如这里定义了一个人类

#include <iostream>
class hunman
{
private:std::string name;
public:hunman();~hunman();
protected:std::string ID_card;
};

struct和class区别

struct默认是公共权限,class默认是私有权限

也就是说在sttruct中定义的变量和方法,可以在外部用该对象直接访问;

而对于Class中定义的变量和方法,如果不指定其权限,默认是无法在外部通过其对象访问的。

构造函数和析构函数

        在C++中,构造函数:是指在这个类当中进行对变量的初始化操作,主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。即在该类的对象被实例化后,构造函数会被立即调用。

        在C++中,析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。例如,当程序中有使用动态内存,即使用new操作符,那么可以在析构函数中进行delete,即内存释放。

构造函数的分类

构造函数的语法是:类名(){}   名称和类名相同,可以重载

  • 无参构造函数:human(){}
  • 有参构造函数:human(std::string name){}
  • 拷贝构造函数:human(const human& h){}

解释:

  • 无参构造函数:是指这个类当中的构造函数不传入任何参数
  • 有参构造函数:是指这个类当中的构造函数传入参数,通常时将传入的参数赋值给类当中的成员变量
  • 拷贝构造函数:传入的参数是和个类的对象,为什么要传入const +引用的形式呢?,是因为我们不想传入的对象被修改,并且以引用的方式传递,这个可以避免被传入的对象占用的内存非常大时,对其的拷贝,使用引用本质上是在使用指针,避免内存的拷贝,提高程序的效率。

构造函数调用规则

  • 括号法
  • 显示法
  • 隐式转换法

 形式1:类名 变量名  hunman p  调用函数:无参构造函数,析构函数

代码

#include <iostream>
class hunman
{
private:std::string name;
public://无参构造函数hunman(){std::cout<<"无参构造函数"<<std::endl;}//有参构造函数hunman(float heightval){height = heightval;std::cout<<"有参构造函数"<<std::endl;}//拷贝构造函数hunman(const hunman& h){height = h.height;std::cout<<"拷贝构造函数"<<std::endl;}//析构函数~hunman(){std::cout<<"析构函数"<<std::endl;       }float height;
protected:std::string ID_card;
};int main(int argc, char const *argv[])
{hunman h;/* code */return 0;
}

 输出

  形式2:类名() 或者 类名 变量名() hunman(1.80) 或者hunman h(1.80);调用函数:有参构造函数,析构函数

代码

#include <iostream>
class hunman
{
private:std::string name;
public://无参构造函数hunman(){std::cout<<"无参构造函数"<<std::endl;}//有参构造函数hunman(float heightval){height = heightval;std::cout<<"有参构造函数"<<std::endl;}//拷贝构造函数hunman(const hunman& h){height = h.height;std::cout<<"拷贝构造函数"<<std::endl;}//析构函数~hunman(){std::cout<<"析构函数"<<std::endl;       }float height;
protected:std::string ID_card;
};int main(int argc, char const *argv[])
{// hunman h;hunman(1.80);/* code */return 0;
}

输出

  形式2:类名 变量名 = 类名(参数)  hunman h1 = hunman(1.80);  调用函数:有参构造函数,析构函数

代码

#include <iostream>
class hunman
{
private:std::string name;
public://无参构造函数hunman(){std::cout<<"无参构造函数"<<std::endl;}//有参构造函数hunman(float heightval){height = heightval;std::cout<<"有参构造函数"<<std::endl;}//拷贝构造函数hunman(const hunman& h){height = h.height;std::cout<<"拷贝构造函数"<<std::endl;}//析构函数~hunman(){std::cout<<"析构函数"<<std::endl;       }float height;
protected:std::string ID_card;
};int main(int argc, char const *argv[])
{// hunman h;// hunman(1.80);hunman h1 = hunman(1.80);// hunman h2 = hunman(h1);/* code */return 0;
}

输出

  形式2:类名 变量名 = 参数 hunman h1 = 1.80;调用函数:有参构造函数,析构函数

代码

#include <iostream>
class hunman
{
private:std::string name;
public://无参构造函数hunman(){std::cout<<"无参构造函数"<<std::endl;}//有参构造函数hunman(float heightval){height = heightval;std::cout<<"有参构造函数"<<std::endl;}//拷贝构造函数hunman(const hunman& h){height = h.height;std::cout<<"拷贝构造函数"<<std::endl;}//析构函数~hunman(){std::cout<<"析构函数"<<std::endl;       }float height;
protected:std::string ID_card;
};int main(int argc, char const *argv[])
{// hunman h;// hunman(1.80);// hunman h1 = hunman(1.80);// hunman h2 = hunman(h1);hunman h1  = 1.80;/* code */return 0;
}

输出:

默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数

拷贝构造函数

拷贝构造函数的调用

当将一个当前类的对象作为参数传入构造函数中后,就会调用拷贝构造函数

    hunman h;hunman h2 = hunman(h);

 

 这里为什么析构函数调用了两次呢?

很好理解,因为你实例化了两个对象,分别是h,h2。

深拷贝与浅拷贝

这个问题时对于拷贝构造函数的经典问题

浅拷贝:

        是指在定义类时,没有在类中显式的构建拷贝构造函数,将一个类的对象作为参数传入类的构造函数中,编译器会把原对象中栈区的变量的值和堆区指针的值复制,而不复制一个指针指向的值

导致的问题:

        在类执行完成,调用析构函数函数时,并且析构函数中有使用delete对指针进行释放时,会给两个对象中的指针分别释放,而两个对象中指针的值时相同的,就比如说第一个对象中指针的值是0x123,另一个对象中指针的值也是0x123,根据栈区先进后出的原则,后被创建的对象会先进行释放对象中的指针0x123,释放之后,另一个对象中的指针0x123也会被释放,但是这时就会报错,因为0x123这个指针已经被释放过了。

代码演示 (先看一下没有构建拷贝构造函数时,栈区和堆区变量是否被复制)

#include <iostream>
class hunman
{
private:std::string name;
public://无参构造函数hunman(){std::cout<<"无参构造函数"<<std::endl;}//有参构造函数hunman(float heightval,int ageval){height = heightval;age = new int(ageval);std::cout<<"有参构造函数"<<std::endl;}//析构函数~hunman(){std::cout<<"析构函数"<<std::endl;       }float height;int *age;
protected:std::string ID_card;
};int main(int argc, char const *argv[])
{hunman h(1.80,23);hunman h2 = hunman(h);std::cout<<"第一个对象中的height的值:"<<h.height<<"  第二个对象中的height的值:"<<h2.height<<std::endl;std::cout<<"第一个对象中的age的值:"<<h.age<<"  第二个对象中的age的值:"<<h2.age<<std::endl;return 0;
}

输出

解析:

        这段代码中比没有构建拷贝构造函数,而这时编译器会默认构建一个,刚才说过栈区和堆区变量会被复制,根据输出可以直观的看到,第一个对象中的栈区变量height和堆区中的age变量所存储的值是相同的。

但是上段代码有些问题,因为我们既然创建了一个指针变量,即动态分配内存,那么就应该手动释放这块内存,即使用delete。

在析构函数中添加

 ~hunman(){if(age != nullptr){delete age;age = nullptr;}std::cout<<"析构函数"<<std::endl;       }

 但是执行后出现问题

        通过图中我们可以看到,第一个析构函数已经成功执行,但是第二个析构函数执行时却发生的报错free(): double free detected in tcache 2

        这个错误提示表明你试图对同一块已经释放的内存进行了多次释放操作,也就是所谓的 “双重释放” 问题

这个双重释放也很好理解,因为在第二个对象对0x123这个地址释放后,原对象再次对这个地址释放是没有用的,因为他已经被释放过了。

注:

 hunman h(1.80,23);原对象h(先进后出,后释放)
 hunman h2 = hunman(h);第二个对象h2(先进后出,先释放)

 解决方法:

使用深拷贝

        深拷贝是指在复制对象时,不仅复制对象的基本数据类型的值,还会为对象中的指针成员分配新的内存空间,并将原指针所指向的内存内容复制到新的内存空间中。这样,原对象和拷贝对象的指针成员会指向不同的内存地址。

代码,显式构建拷贝构造函数

    // 拷贝构造函数hunman(const hunman& h){height = h.height;age = new int(*h.age);std::cout<<"拷贝构造函数"<<"地址:"<<std::endl;}

 在拷贝构造函数中对栈区的变量重新复制,并且对堆区的指针重新分配内存,使其和原对象中的指针的值不是一个地址,这样第二个对象释放一个地址,而另一个对象释放里一个地址,这样互不干涉,程序就不会崩溃了。

由图可知,两个对象当中的age的值即地址是不一样的,完美解决!

注:在显式的构建拷贝构造函数时,在函数中应该对需要复制的值手动赋值,因为在没有构建拷贝苟造函数时,编译器会帮你把所有的变量复制,但是当你显式构建是,就没了,所以需要手动复制。

构造函数初始化列表

使用构造函数初始化列表可以帮助我们快速的初始化成员变量

语法:类名(参数1,参数2,...): 成员变量1(参数1),成员变量2(参数2),...

#include <iostream>
class hunman
{
public://构造函数初始化列表hunman(int a,float b,std::string c): age(a),height(b),name(c){std::cout<<"age:"<<age<<std::endl;std::cout<<"height:"<<height<<std::endl;std::cout<<"name:"<<name<<std::endl;}int age;float height;std::string name;
};int main(int argc, char const *argv[])
{hunman(21,1.80,"rqtz");return 0;
}

类对象作为类成员

当有另一个类a的对象作为类h的成员变量时候,构造函数和析构函数的调用顺序

构造函数顺序 :先a后h

析构函数顺序:先h后a

代码

#include <iostream>
class animal
{public:animal(){std::cout<<"animal无参构造函数"<<std::endl;}~animal(){std::cout<<"animal析构函数"<<std::endl;}};
class hunman
{
public://无参构造函数hunman(){std::cout<<"hunman无参构造函数"<<std::endl;}~hunman(){std::cout<<"hunman析构函数"<<std::endl;}animal a;
};int main(int argc, char const *argv[])
{hunman h;return 0;
}

 

静态成员

在类中使用 static关键字所修饰的变量和函数叫做类的静态成员

  • 静态成员不属于任何对象
  • 该类的任何对象共用一分数据,共享一块内存
  • 该类的任何对象都可以修改静态成员的值,并且该值会被更新
  • 静态成员的初始化需要在类外,使用类名加作用域的方式初始化
#include <iostream>class hunman
{
public://无参构造函数hunman(){// std::cout<<"hunman无参构造函数"<<std::endl;}~hunman(){// std::cout<<"hunman析构函数"<<std::endl;}static void func(){std::cout<<"静态成员函数"<<std::endl;}static int a;
};
//类外初始化
int hunman::a = 1;
int main(int argc, char const *argv[])
{hunman h;std::cout<<h.a<<std::endl;//其他对象改变静态变量的值班hunman h2;h2.a = 10;//在用原对象访问时,值已经更新std::cout<<h.a<<std::endl;//通过类名加作用域访问静态变量和静态成员函数std::cout<<hunman::a<<std::endl;hunman::func();//该类的对象静态变量时统一快内存std::cout<<&h2.a<<std::endl;std::cout<<&hunman::a<<std::endl;return 0;
}

输出:

 

 

  🔥🔥个人主页 🔥🔥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/76259.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript性能优化(下)

1. 使用适当的算法和逻辑 JavaScript性能优化是一个复杂而重要的话题&#xff0c;尤其是在构建大型应用时。通过使用适当的算法和逻辑&#xff0c;可以显著提高代码的效率和响应速度。以下是一些关键策略和实践&#xff0c;用于优化JavaScript性能&#xff1a; 1.1. 采用适当…

蚂蚁 Flink 实时计算编译任务 Koupleless 架构改造

张冯君&#xff08;远远&#xff09; Koupleless PMC 蚂蚁集团技术工程师 就职于蚂蚁集团中间件团队&#xff0c;参与维护与建设蚂蚁 SOFAArk 和 Koupleless 开源项目、内部 SOFAServerless 产品的研发和实践。 本文 3488 字&#xff0c;预计阅读 11 分钟 业务背景 基于开源 A…

使用pycharm社区版调试DIFY后端python代码

目录 背景 前置条件 DIFY使用的框架 API服务调试配置步骤&#xff08;基于tag为0.15.3的版本&#xff09; 1.配置.env文件 2.关闭docker里面的docker-api-1服务 3.使用DOCKER启动本地环境需要用到的中间件&#xff0c;并暴露端口 注意事项一&#xff1a; 注意事项二&#xff1a…

从 macos 切换到 windows 上安装的工具类软件

起因 用了很多年的macos, 已经习惯了macos上的操作, 期望能在windows上获得类似的体验, 于是花了一些时间来找windows上相对应的软件. 截图软件 snipaste​​​​​​ windows和macos都有的软件, 截图非常好用 文件同步软件 oneDrive: 尝试了不同的同步软件, 还是微软在各…

MySQL体系架构(一)

1.1.MySQL的分支与变种 MySQL变种有好几个,主要有三个久经考验的主流变种:Percona Server,MariaDB和 Drizzle。它们都有活跃的用户社区和一些商业支持,均由独立的服务供应商支持。同时还有几个优秀的开源关系数据库,值得我们了解一下。 1.1.1.Drizzle Drizzle是真正的M…

【项目实训项目博客】prompt初版实践

通过对camel技术的理解&#xff0c;我们向其中添加了市场营销角色的prompt 初版设计如下&#xff1a; chatchainconfig.json { "chain": [ { "phase": "DemandAnalysis", "phaseType": "SimplePhase", "max_turn_step…

[Bond的杂货铺] CKS 证书也到货咯

最近比较忙&#xff0c;忘记写Blog了&#xff1a;&#xff09; 一年前黑五去官网蹲了一手Cyber Monday&#xff0c;买了英文考试券bundle&#xff0c;当时只考了cka,后来cks差点都忘记了。将近一年后&#xff0c;无意中收到官方的提醒邮件&#xff0c;说考试券本已过期&#x…

【回眸】Linux 内核 (十五) 之 多线程编程 上

前言 进程和线程 区别 线程API 1.创建线程 2.线程退出 3.线程等待 4.线程脱离 5. 线程ID获取及比较 6.创建及销毁互斥锁 7.创建及销毁条件变量 8. 等待 9.触发 多线程编程 后记 前言 高产的几天。 进程和线程 区别 进程——资源分配的最小单位&#xff0c;线…

127.0.0.1本地环回地址(Loopback Address)

127.0.0.1 是计算机网络中的一个特殊IPv4地址&#xff0c;称为本地环回地址&#xff08;Loopback Address&#xff09;&#xff0c;主要用于以下用途&#xff1a; 1. 基本定义 本地主机&#xff08;Localhost&#xff09;&#xff1a;该地址始终指向当前正在使用的计算机本身&a…

S7-1200 PLC热电偶和热电阻模拟量模块

热电偶和热电阻模拟量模块 S7-1200 PLC有专用用于对温度进行采集的热电偶模块SM1231 TC和SM 1231RTD。热电偶模块有4AI和8AI两种&#xff0c;下面以SM1231 TC 4AI为例看一下接线图。 该模块一共有4个通道&#xff0c;每个通道有两个接线端子&#xff0c;比如0&#xff0c;0-。…

深度了解向量引论

今天去研究了一个基本数学原理 这个其实需要证明 今天推导了一下这个公式&#xff0c;感觉收获挺大 下面是手工推导过程

Feign修仙指南:声明式HTTP请求的优雅之道

各位在微服务世界摸爬滚打的道友们&#xff01;今天要解锁的是Spring Cloud的绝世神通——Feign&#xff01;这货堪称HTTP界的"言出法随"&#xff0c;只需定义接口&#xff0c;就能自动生成HTTP请求代码&#xff01;从此告别手动拼装URL的苦日子&#xff0c;让你的代…

UDP学习笔记(四)UDP 为什么大小不能超过 64KB?

&#x1f310; UDP 为什么大小不能超过 64KB&#xff1f;TCP 有这个限制吗&#xff1f; 在进行网络编程或者调试网络协议时&#xff0c;我们常常会看到一个说法&#xff1a; “UDP 最大只能发送 64KB 数据。” 这到底是怎么回事&#xff1f;这 64KB 是怎么来的&#xff1f;TCP…

LabVIEW 中串口设备与采集卡的同步精度

在 LabVIEW 项目开发中&#xff0c;常涉及多种设备协同工作&#xff0c;如通过串口设备采集温度&#xff0c;利用采集卡&#xff08;如 NI 6251&#xff09;采集压力。此时&#xff0c;设备间的同步精度至关重要&#xff0c;它直接影响系统数据的准确性与可靠性。下面&#xff…

DP_AUX辅助通道介绍

DisplayPort&#xff08;简称DP&#xff09;是一个由PC及芯片制造商联盟开发&#xff0c;视频电子标准协会&#xff08;VESA&#xff09;标准化的数字式视频接口标准。该接口免认证、免授权金&#xff0c;主要用于视频源与显示器等设备的连接&#xff0c;并也支持携带音频、USB…

[GESP202312 五级] 平均分配

文章目录 题目描述输入格式输出格式输入输出样例 #1输入 #1输出 #1 输入输出样例 #2输入 #2输出 #2 提交链接提示解析参考代码 题目描述 小杨认为&#xff0c;所有大于等于 a a a 的完全平方数都是他的超级幸运数。 小杨还认为&#xff0c;所有超级幸运数的倍数都是他的幸运…

[Mysql]buffersize修改

1、找到my.cnf文件位置 ps -ef|grep mysqld 2、编辑my.cnf cd /etc/my.cnf.d vim my.cnf 一般修改为内存的50%~70% 3、重启服务 systemctl restart mysqld

清晰易懂的 Apollo 配置中心安装与使用教程

Apollo 是携程开源的分布式配置管理平台&#xff0c;支持配置实时推送、版本管理、权限控制等功能。本教程将手把手教你完成 Apollo 核心组件安装、基础配置管理及避坑指南&#xff0c;助你快速掌握企业级配置管理能力。 一、环境准备&#xff08;关键依赖&#xff09; 1. 基础…

PyTorch池化层详解:原理、实现与示例

池化层&#xff08;Pooling Layer&#xff09;是卷积神经网络中的重要组成部分&#xff0c;主要用于降低特征图的空间维度、减少计算量并增强模型的平移不变性。本文将通过PyTorch代码演示池化层的实现原理&#xff0c;并详细讲解最大池化、平均池化、填充&#xff08;Padding&…

如何构建并优化提示词?

提示词是一个小白最容易上手大模型的方式&#xff0c;提示词就是你告诉大模型应该如何去完成一项工作的系统性的命令&#xff0c;所以写一个好的提示词是比较关键的&#xff0c;那么如何写好一个提示词呢&#xff1f; 要写好提示词&#xff0c;其实就像我们要把一些命令清晰地传…