讨论C++类与对象

news2024/12/24 11:38:57

讨论C++类与对象

  • C语言结构体和C++类的对比
  • 类的实例化
  • 类对象的大小
    • 猜想一
    • 猜想二
    • 针对上述猜想的实践
  • `this`指针
    • 不同对象调用成员函数
  • 类的6个默认成员函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
      • 浅拷贝和深拷贝
    • 赋值运算符重载
  • 初始化列表
      • 初始化顺序

C语言结构体和C++类的对比

在C语言中,要想描述一个复杂结构,需要使用结构体。例如描述一个学生,需要有学号,学生姓名,年龄以及性别。

C语言中结构体

上述代码结果:

结构体输出结果

C语言中的结构体可以描述复杂结构,但想要修改就需要在外部定义函数,传入结构体指针,较为复杂。

void modifyStudent(struct Student* pst)
{
    printf("请输入想要修改的年龄:");
    int age = 0;
    scanf("%d", &age);
    pst->age = age;
}

修改结构体对象中的值

在C++中,引入了类的概念,它类似于C语言的结构体,可以描述一个复杂结构,不同于结构体,类中是可以定义成员函数的,可以极大方便对成员变量进行修改。

为了兼容C语言,C++中使用struct关键字可以定义类,也可以使用class关键字定义类。

struct关键字定义的类没有访问修饰限定符,默认都是public公开访问的。

class关键字定义的类可以自定访问修饰限定符,privateprotectedpublic,默认的访问权限是private私有的。类外不能访问。

class Student {
public:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};

如上代码便定义出一个学生类,由于成员变量都是public公有的,所以在类外也可以访问。

类的实例化

用类类型创建对象的过程,称为类的实例化。

int main() {
    Student st1;
    st1._stuId = 101;
    st1._name = "张三";
    st1._age = 23;
    st1._gender = "男";

    std::cout << "学生> 学号:" << st1._stuId << ", 姓名:" << st1._name <<
    ", 年龄:" << st1._age << ", 性别:" << st1._gender << std::endl;
    return 0;
}

对象成员变量访问

  • 如果成员变量使用private修饰,类外就不能直接进行访问,会报错。

类对象的大小

类中既有成员变量,又有成员函数,那么类对象的存储方式是什么样的呢?

猜想一

成员变量和成员函数都存储在类对象中。

class Test1 {
public:
    void test() {}
private:
    int _a;
};

类对象存储猜想1

猜想二

成员变量存储在类对象中,成员函数存储在公共代码段中,由函数表记录。

类对象存储猜想2

针对上述猜想的实践

  • 使用sizeof计算类对象的大小。如果成员函数存储在类对象中,则对象所占内存大小一定大于int所占内存大小。
int main() {
    Test1 t1;
    std::cout << sizeof(t1) << std::endl;

    return 0;
}

类对象所占空间大小

由此可得出结论:只有成员变量存储在类对象中,成员函数存储在公共代码段中。

类对象所占空间大小也遵循内存对齐规则。

this指针

针对上述内容的讨论,可以得知每个实例化的对象都有一份自己的成员变量,但成员函数存放在公共代码段中,是所有对象共有的,但通过代码可知,通过不同对象调用成员函数,得到的成员变量是不同的。

不同对象调用成员函数

class Student {
public:
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

public:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};

int main() {
    Student st1;
    st1._stuId = 101;
    st1._name = "张三";
    st1._age = 23;
    st1._gender = "男";

    st1.print();

    Student st2;
    st2._stuId = 102;
    st2._name = "李四";
    st2._age = 24;
    st2._gender = "女";

    st2.print();

    return 0;
}

不同对象打印的值不同

  • 由此可见,在成员函数中一定有一个标识,表示不同的对象调用,这个标识就是***this指针*。

在每一个成员函数中,都有一个隐藏的参数,即this指针,该指针表示当前调用的对象。

this指针

类似于如上图所示,不过这个this指针不需要我们写出来,是编译器默认生成的,这就是即使成员函数不存放在对象中,不同的对象调用成员函数所显示的值会不一样了。

类的6个默认成员函数

默认成员函数即我们不写,编译器会自动生成的成员函数。

6个默认成员函数

构造函数

对于学生类,成员变量如果设为私有,类外便不能访问,实例化对象时,就不能直接赋值。若提供对外的getter & setter接口,也只能在实例化之后再一一赋值。如何达到实例化对象中就完成对对象成员变量的初始化,这就需要构造函数来完成。

构造函数是一个特殊的成员函数,函数名和类名相同,没有返回值,实例化对象时由编译器自动调用,且在对象的生命周期中只调用一次。

class Student {
public:
    Student() {
        std::cout << "无参的构造函数调用了" << std::endl;
    } // 无参的构造函数
    Student(int stuId, std::string name, int age, std::string gender) { // 有参的构造函数
        std::cout << "有参的构造函数调用了" << std::endl;
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};


int main() {
    Student st1;
    st1.print();

    Student st2(102, "李四", 24, "女");
    st2.print();

    return 0;
}

构造函数

C++标准规定,若不对成员变量做初始化,编译器默认对内置类型不做处理,自定义类型会调用其默认的构造函数。

当然,如果我们不显示写构造函数,编译器会生成不带参数的构造函数,如果我们显示写了,编译器就不再生成不带参数的构造函数了。

class Student {
public:
//    Student() {
//        std::cout << "无参的构造函数调用了" << std::endl;
//    } // 无参的构造函数
    Student(int stuId, std::string name, int age, std::string gender) { // 有参的构造函数
        std::cout << "有参的构造函数调用了" << std::endl;
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};


int main() {
    Student st1; // 此处会报错,因为显示写了带参数的构造函数,编译器不再生成不带参的构造函数。
    st1.print();

    Student st2(102, "李四", 24, "女");
    st2.print();

    return 0;
}

为了省事,不写那么多构造函数,我们也可以采用参数全缺省的形式来充当默认成员函数。

class Student {
public:
    Student(int stuId = 101, std::string name = "张三", int age = 23, std::string gender = "男") { // 有参的构造函数
        std::cout << "有参全缺省的构造函数调用了" << std::endl;
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};

实例化对象时,若给初始值,则直接采用缺省值。

全缺省构造函数

析构函数

析构函数与构造函数的作用刚好相反,对象在销毁时自动调用析构函数,完成对象中资源的清理工作。

  • 语法:
    • 析构函数名与类名相同,在函数名前加~
    • 无返回值。
    • 一个类只能有一个析构函数,析构函数不能重载。
    • 对象生命周期结束时,编译器自动调用析构函数。
class Student {
public:
    Student(int stuId = 101, std::string name = "张三", int age = 23, std::string gender = "男") { // 有参的构造函数
        _stuId = stuId;
        _name = name;
        _age = age;
        _gender = gender;
    }
    void print() {
        std::cout << "学生> 学号:" << _stuId << ", 姓名:" << _name <<
    ", 年龄:" << _age << ", 性别:" << _gender << std::endl;
    }

    ~Student() {
        std::cout << "Student类的析构函数调用了" << std::endl;
    }

private:
    int _stuId;
    std::string _name;
    int _age;
    std::string _gender;
};


int main() {
    Student st1(102, "李四", 24, "女");
    st1.print();
    return 0;
}

析构函数

析构函数也是特殊的成员函数,因此对内置类型不做处理,自定义类型调用其析构函数。

拷贝构造函数

拷贝构造函数是构造函数的一个重载,参数只能有一个,且是类类型的引用。

拷贝构造函数

int main() {
    Student st1(102, "李四", 24, "女");
    st1.print();

    Student st2(st1); // 使用拷贝构造函数实例化对象
    st2.print();
    return 0;
}

拷贝构造函数打印结果

若未显示定义拷贝构造函数,编译器会自动生成默认的拷贝构造函数。对象会按照内存存储字节序完成拷贝,即浅拷贝。

浅拷贝和深拷贝

  • 如上述所说,浅拷贝只是对内存的直接复制,如果只是栈上开辟空间的变量,影响还没有那么大,但如果是在堆上开辟的空间,那么只拷贝值会影响非常大。
class Test {
public:
    Test() {
        _a = (int*)malloc(10 * sizeof(int));
        b = 20;
    }
    ~Test() {
        free(_a);
        _a = nullptr;
    }
private:
    int* _a;
    int b;
};

浅拷贝

默认生成的拷贝构造函数

  • 因此,面对这种情形,默认生成的拷贝构造函数就不能满足条件了,因此就需要自己显示定义拷贝构造函数,来达到深拷贝。

深拷贝

赋值运算符重载

C++为了增强代码的可读性,引入了运算符重载。

  • 语法:
    • 返回值类型 operator==(参数列表);
class Test {
public:
    Test() {
        _a = (int*)malloc(10 * sizeof(int));
        b = 20;
    }
    ~Test() {
        free(_a);
        _a = nullptr;
    }
    Test(const Test& t) {
        _a = (int*)malloc(10 * sizeof(int));
        for (int i = 0; i < 10; ++i) {
            _a[i] = t._a[i];
        }
    }
  	// 赋值运算符重载
    Test& operator=(const Test& t) {
        for (int i = 0; i < 10; ++i) {
            _a[i] = t._a[i]; // 深拷贝
        }
        return *this;
    }
private:
    int* _a;
    int b;
};

值得注意的是,我们常常看到这样的代码Test t2 = t1。这里虽然使用了=,但并不是赋值运算符重载,赋值运算符重载的定义是已实例化的对象被赋值,这上面代码是还没有实例化对象,所以是拷贝构造。

拷贝构造调试验证

初始化列表

实例化对象时,编译器会通过构造函数来给成员变量一个初始值,这种行为只能称为赋值,并不能称为初始化,因为初始化只有一次,而构造函数中可以多次赋值。

  • 语法:
    • :开始,用,分割数据成员列表,每个成员变量后面跟初始值(初始值)

初始化列表

初始化顺序

class Test {
public:
    Test()
            : _b(6), _a(_b) {}

    void print() {
        std::cout << _a << " " << _b << std::endl;
    }

private:
    int _a;
    int _b;
};


int main() {
    Test t;
    t.print();
    return 0;
}

如上述代码,按照初始化列表顺序初始化,则应是a = b = 6这个结果。

初始化列表初始化顺序

而真实的结果是_a是随机值,_b是预期值6

可得出结论: 成员变量的初始化顺序和初始化列表顺序无关,和声明顺序有关。

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

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

相关文章

require.context()函数介绍

业务需求&#xff1a; 前端Vue项目怎样读取src/assets目录下所有jpg文件 require.context()方法来读取src/assets目录下的所有.jpg文件 <template><div><img v-for"image in images" :src"image" :key"image" /></div> …

Vision-LSTM: xLSTM 作为通用视觉主干

摘要 尽管Transformer最初是为自然语言处理引入的&#xff0c;但它现在已经被广泛用作计算机视觉中的通用主干结构。最近&#xff0c;长短期记忆&#xff08;LSTM&#xff09;已被扩展为一种可扩展且性能优越的架构——xLSTM&#xff0c;它通过指数门控和可并行化的矩阵内存结…

用函数指针求a和b中的大者

指针变量也可以指向一个函数。一个函数在编译时被分配给一个入口地址。这个函数入口地址就称为函数的指针。可以用一个指针变量指向函数&#xff0c;然后通过该指针变量调用此函数。 先按一般方法编写程序&#xff1a; 可以用一个指针变量指向max函数&#xff0c;然后通过该指…

不能访问huggingface、与GPU配置

不能访问huggingface解决方法 如果是从 huggingface.co 下载模型&#xff0c;由于国内不能访问&#xff0c;所以建议先配置一下环境变量&#xff0c; 通过访问国内镜像站点 https://hf-mirror.com来下载模型。 &#xff08;1&#xff09;Linux系统设置环境变量&#xff1a; e…

STM32引脚外部中断和外部事件模式的区别

STM32引脚外部中断和外部事件模式的区别 STM32引脚模式外部中断和外部事件模式的区别&#xff1a; (以 GPIO_MODE_IT_FALLING 和 GPIO_MODE_EVT_FALLING 为例) GPIO_MODE_IT_FALLING 能够触发中断&#xff0c;用在中断方式编程。GPIO_MODE_EVT_FALLING 只设置中断标志位&…

vue3 基于el-tree增加、删除节点(非TypeScript 写法)

话不多说&#xff0c;直接贴代码 <template><div class"custom-tree-container"><!-- <p>Using render-content</p><el-tree style"max-width: 600px" :data"dataSource" show-checkbox node-key"id" …

【C语言初阶】分支语句

&#x1f31f;博主主页&#xff1a;我是一只海绵派大星 &#x1f4da;专栏分类&#xff1a;C语言 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、什么是语句 二、if语句 悬空else 三、switch语句 default 四、switch语句与if-else语句性能对比如何&#xff1f…

boot整合solr

换了新项目组&#xff0c;技术相对老些&#xff0c;于是用boot框架简单记录下&#xff01; 安装 下载路径&#xff1a;https://solr.apache.org/downloads.html Windows环境 下载solr-8.2.0.zip包并解压缩&#xff0c;以管理员身份打开cmd&#xff0c;执行 solr cmd 命令启…

WordPress 高级缓存插件 W3 Total Cache Pro 详细配置教程

说起来有关 WordPress 缓存插件明月已经发表过不少文章了,但有关 W3 Total Cache Pro 这个 WordPress 高级缓存插件除了早期【网站缓存插件 W3 Total Cache,适合自己的才是最好的!】一文后就很少再提及了,最近因为明月另一个网站【玉满斋】因为某些性能上的需要准备更换缓存…

微服务中调用common模块中的工具类

首先查看common类中的pom文件中的信息 然后再在所需要使用的微服务模块中进行注入 就可以使用其中的工具类了

pyqt opengl 小黑块

目录 OpenGLWidget_g初始化函数&#xff1a; 解决方法&#xff1a;把初始化函数的parent去掉 pyqt opengl 小黑块 原因&#xff1a; 创建OpenGLWidget_g的时候把main_window作为父类&#xff08;self&#xff09;传进去了&#xff0c; self.opengl_widget OpenGLWidget_g(…

前端nvm的安装和使用nodejs多版本管理2024

nvm的安装和使用 1、简介 nvm是一个管理nodejs版本的工具。在实际的开发中&#xff0c;项目的开发依赖需要的nodejs版本运行环境不同&#xff0c;此时我们就需要使用nvm来进行不同nodejs版本的切换。其实就是一个方便的node版本管理工具。 注意&#xff1a;如果有安装过node&a…

PySpark教程(001):基础准备与数据输入

PySpark 学习目标 了解什么是Spark、PySpark了解为什么学习PySpark了解如何和大数据开发方向进行衔接 Spark是什么&#xff1f; Apache Spark是用于大规模数据处理的统一分析引擎。 简单来说&#xff0c;Spark是一款分布式的计算框架&#xff0c;用于调度成百上千的服务器…

算法设计与分析(5题Python版)

1、阿里巴巴走进了装满宝藏的藏宝洞。藏宝洞里面有N堆金币&#xff0c;第i堆金币的总重量和总价值分别是m,v。阿里巴巴有一个承重量为T的背包&#xff0c;但并不一定有办法将全部的金币都装进去。 他想装走尽可能多价值的金币&#xff0c;所有金币都可以随意分割&#xff0c;分…

攻防世界---misc---Excaliflag

1、题目描述&#xff0c;下载附件是一张图片 2、用winhex分析&#xff0c;没有发现奇怪的地方 3、在kali中使用binwalk -e 命令&#xff0c;虽然分离出来了一些东西&#xff0c;但是不是有用的 4、最后用stegsolve分析&#xff0c;切换图片&#xff0c;发现有字符串&#xff0c…

tcp协议中机制的总结

目录 总结 分析 三次握手 总结 分析 其中,序列号不止用来排序,还可以用在重传时去重 确认应答是机制中的核心 因为都需要依靠应答来拿到协议字段,从而判断是否触发机制 保证可靠性的策略也可以提高效率,比如: 流量控制,可以根据多个因素来动态调整数据发送量拥塞控制也是,让…

XSS(跨站脚本攻击)

1.什么是xss XSS全称&#xff08;Cross Site Scripting&#xff09;跨站脚本攻击&#xff0c;为了避免和CSS层叠样式表名称冲突&#xff0c;所以改为了 XSS&#xff0c;是最常见的Web应用程序安全漏洞之一,XSS是指攻击者在网页中嵌入客户端脚本&#xff0c;通常是JavaScript编写…

两句话让LLM逻辑推理瞬间崩溃!!

一道简单的逻辑问题&#xff0c;竟让几乎所有的LLM全军覆没&#xff1f; 对于人类来说&#xff0c;这个名为「爱丽丝梦游仙境」&#xff08;AIW&#xff09;的测试并不算很难—— 「爱丽丝有N个兄弟&#xff0c;她还有M个姐妹。爱丽丝的兄弟有多少个姐妹&#xff1f;」 稍加思考…

Git发布正式

一般我们开发都是在测试环境开发&#xff0c;开发完成后再发布到正式环境。 一.分支代码合并到主分支1.首先切换到自己的分支(比如分支叫&#xff1a;dev)git checkout dev2.把本地分支拉取下来git pull 或者 git pull origin dev3.切换到主分支mastergit checkout master4.更新…

Unity | Shader基础知识(番外:了解内置Shader-Standard<二>)

目录 前言 一、Standard参数详解 1.NormalMap法线贴图 2.HeightMap高度贴图 3.Occlusion遮挡贴图 4.DetailMask细节遮挡 5.Emission自发光 6.Tiling铺地砖和Offset偏移度 二、作者的碎碎念 前言 Unity | Shader基础知识(番外&#xff1a;了解内置Shader-Standard&#x…