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

news2025/7/19 5:23:53

 

每文一诗  💪🏼

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

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


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

✨✨个人主页 点击✨✨

封装

封装作为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.coloradmin.cn/o/2331575.html

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

相关文章

每日算法-250409

这是我今天的算法学习记录。 2187. 完成旅途的最少时间 题目描述 思路 二分查找 解题过程 为什么可以使用二分查找&#xff1f; 问题的关键在于寻找一个最小的时间 t&#xff0c;使得在时间 t 内所有公交车完成的总旅途次数 sum 大于等于 totalTrips。 我们可以观察到时间的单…

如何实现文本回复Ai ChatGPT DeepSeek 式文字渐显效果?前端技术详解(附完整代码)

个人开发的塔罗牌占卜小程序&#xff1a;【问问塔罗牌】 快来瞧瞧吧&#xff01; 一、核心实现原理 我们通过三步实现这个效果&#xff1a; 逐字渲染&#xff1a;通过 JavaScript 定时添加字符 透明度动画&#xff1a;CSS 实现淡入效果 光标动画&#xff1a;伪元素 CSS 动画…

linux下截图工具的选择

方案一 gnome插件Screenshot Tool&#xff08;截屏&#xff09; ksnip&#xff08;图片标注&#xff09; gnome setting设置图片的默认打开方式为ksnip就可以快捷的将Screenshot Tool截屏的图片打开进行标记了。 但是最近我发现Screenshot Tool的延迟截图功能是有问题的&…

rkmpp 解码 精简mpi_dec_test.c例程

rkmpp 解码流程&#xff08;除 MPP_VIDEO_CodingMJPEG 之外&#xff09; 源码 输入h264码流 输出nv12文件 /** Copyright 2015 Rockchip Electronics Co. LTD** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file exce…

怎么构造思维链数据?思维链提示工程的五大原则

我来为您翻译这篇关于思维链提示工程的文章&#xff0c;采用通俗易懂的中文表达&#xff1a; 思维链(CoT)提示工程是生成式AI(GenAI)中一种强大的方法&#xff0c;它能让模型通过逐步推理来解决复杂任务。通过构建引导模型思考过程的提示&#xff0c;思维链能提高输出的准确性…

网络安全之-信息收集

域名收集 域名注册信息 站长之家 https://whois.chinaz.com/ whois 查询的相关网站有:中国万网域名WHOIS信息查询地址: https://whois.aliyun.com/西部数码域名WHOIS信息查询地址: https://whois.west.cn/新网域名WHOIS信息查询地址: http://whois.xinnet.com/domain/whois/in…

JdbcTemplate基本使用

JdbcTemplate概述 它是spring框架中提供的一个对象&#xff0c;是对原始繁琐的JdbcAPI对象的简单封装。spring框架为我们提供了很多的操作模板类。例如:操作关系型数据的JdbcTemplate和MbernateTemplate&#xff0c;操作nosql数据库的RedisTemplate&#xff0c;操作消息队列的…

openEuler24.03 LTS下安装Spark

目录 安装模式介绍 下载Spark 安装Local模式 前提条件 解压安装包 简单使用 安装Standalone模式 前提条件 集群规划 解压安装包 配置Spark 配置Spark-env.sh 配置workers 分发到其他机器 启动集群 简单使用 关闭集群 安装YARN模式 前提条件 解压安装包 配…

使用 DeepSeek API 实现新闻文章地理位置检测与地图可视化

使用 DeepSeek API 实现新闻文章地理位置检测与地图可视化 | Implementing News Article Location Detection and Map Visualization with DeepSeek API 作者&#xff1a;zhutoutoutousan | Author: zhutoutoutousan 发布时间&#xff1a;2025-04-08 | Published: 2025-04-08 标…

如何精准控制大模型的推理深度

论文标题 ThinkEdit: Interpretable Weight Editing to Mitigate Overly Short Thinking in Reasoning Models 论文地址 https://arxiv.org/pdf/2503.22048 代码地址 https://github.com/Trustworthy-ML-Lab/ThinkEdit 作者背景 加州大学圣迭戈分校 动机 链式推理能显…

【力扣hot100题】(078)跳跃游戏Ⅱ

好难啊&#xff0c;我愿称之为跳崖游戏。 依旧用了两种方法&#xff0c;一种是我一开始想到的&#xff0c;一种是看答案学会的。 我自己用的方法是动态规划&#xff0c;维护一个数组记录到该位置的最少步长&#xff0c;每遍历到一个位置就嵌套循环遍历这个位置能到达的位置&a…

WES与WGS数据线粒体DNA数据分析及检测工具

1. 线粒体DNA的异质性 传统的全外显子组测序&#xff08;WES&#xff09;和全基因组测序&#xff08;WGS&#xff09;的二代测序&#xff08;SGS&#xff09; 数据分析流程&#xff0c;能够识别多种类型的基因改变。但大多数用于基因变异分析和注释的工具&#xff0c;在输出文…

word表格间隔设置

1.怎么解决word表格间隔达不到我们想要的要求 其实很简单, 我们直接在word表格里面, 全选表格中里面的内容。接着,我们选择自动调整---->根据内容自动调整表格,即可达到我们想要的要求

SpringBoot 接口限流Lua脚本接合Redis 服务熔断 自定义注解 接口保护

介绍 Spring Boot 接口限流是防止接口被频繁请求而导致服务器负载过重或服务崩溃的一种策略。通过限流&#xff0c;我们可以控制单位时间内允许的请求次数&#xff0c;确保系统的稳定性。限流可以帮助防止恶意请求、保护系统资源&#xff0c;并优化 API 的可用性&#xff0c;避…

I/O进程4

day4 九、信号灯集 1.概念 信号灯(semaphore)&#xff0c;也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制&#xff1b;System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。 通过信号灯集实现共享内存的同步操作。 2.编程…

【语法】C++的list

目录 为什么会有list&#xff1f; 迭代器失效&#xff1a; list和vector的迭代器不同的地方&#xff1a; list的大部分用法和vector都很像&#xff0c;例如push_back&#xff0c;构造&#xff0c;析构&#xff0c;赋值重载这些就不再废话了&#xff0c;本篇主要讲的是和vecto…

【Ai/Agent】Windows11中安装CrewAI过程中的错误解决记录

CrewAi是什么&#xff0c;可以看之下之前写的 《初识CrewAI多智能代理团队协框架》 (注&#xff1a;这篇是基于linux系统下安装实践的) 基于以下记录解决问题后&#xff0c;可以再回到之前的文章继续进行CrewAI的安装 遇到问题 在windows系统中安装 CrewAi 不管是使用 pip 或者…

OSPF的数据报文格式【复习篇】

OSPF协议是跨层封装的协议&#xff08;跨四层封装&#xff09;&#xff0c;直接将应用层的数据封装在网络层协议之后&#xff0c;IP协议包中协议号字段对应的数值为89 OSPF的头部信息&#xff1a; 所有的数据共有的信息字段 字段名描述版本当前OSPF进程使用的版本&#xff08;…

【力扣刷题实战】Z字形变换

大家好&#xff0c;我是小卡皮巴拉 文章目录 目录 力扣题目&#xff1a;Z字形变换 题目描述 解题思路 问题理解 算法选择 具体思路 解题要点 完整代码&#xff08;C&#xff09; 兄弟们共勉 &#xff01;&#xff01;&#xff01; 每篇前言 博客主页&#xff1a;小卡…

力扣题解:142. 环形链表 II

在链表学习中&#xff0c;我们已经了解了单链表和双链表&#xff0c;两者的最后一个结点都会指向NULL&#xff1b;今天我们介绍的循环列表则不同&#xff0c;其末尾结点指向的这是链表中的一个结点。 循环链表是一种特殊类型的链表&#xff0c;其尾节点的指针指向头节点&#…