C++:类与对象(下)- this指针、(拷贝)构造函数、析构函数、复制运算符重载

news2024/11/24 2:20:06

目录

一、 this指针

1.1 引入

1.2 问题

1.3 特性

二、 构造函数

2.1 概念

2.2 特性

2.3 语法

2.4 注意点

三、 析构函数

3.1 概念

3.2 特性

3.3 示例

四、拷贝构造函数

4.1 概念

4.2 特性

4.3 示例

4.4 深浅拷贝

五、 赋值运算符重载

5.1 概念

5.2 语法

5.3 示例


一、 this指针

1.1 引入

首先看一段代码

#include <iostream>

class Person
{
public:
    void Init(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    void Print()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

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

int main()
{
    Person p1, p2;
    p1.Init("John", 30);
    p2.Init("Alice", 25);
    p1.Print(); // Output: Name: John, Age: 30
    p2.Print(); // Output: Name: Alice, Age: 25
    return 0;
}

1.2 问题

Person类中有 InitPrint 两个成员函数,函数体中没有关于不同对象的区分,那么是如何区分是哪一个对象进行的调用呢?

C++设计者们提出使用this指针解决该问题,当我们调用成员函数时,C++编译器会在内部为每个非静态成员函数增加一个隐藏的指针参数,即this指针。这个this指针是一个指向当前对象的地址的常量指针,它指向调用该成员函数的对象。在函数体中,所有对成员变量的操作都是通过this指针来访问的。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

 如图所示,我们定义类成员函数时,应当像上面的第一种格式定义,第二种是编译器对自动在参数列表传递指向调用该非静态成员函数的成员的常量指针,上图只是为了让读者体会到该过程,在函数内部,可以直接使用成员属性,当函数形参名称与成员属性发生冲突时,使用this->成员属性的方式可进行区分!!!

在成员函数中,我们可以使用this指针来访问当前对象的成员变量和成员函数。例如,如果类中存在一个成员变量和函数都叫做value,我们可以使用this->value来明确表示访问的是成员变量而不是函数。此外,通过在成员函数中返回*this,我们可以实现链式调用,提高代码的可读性和简洁性。

1.3 特性

  • this指针的类型为类类型* const,即成员函数中不能给this指针赋值,因为它指向当前对象的地址,不允许指向其他对象。
  • this指针只能在成员函数的内部使用,不能在类的非成员函数或全局函数中使用。
  • this指针本质上是成员函数的一个隐含形参,在对象调用成员函数时,编译器会将对象的地址作为实参传递给this指针。因此,对象本身不存储this指针。

二、 构造函数

空类:类中无任何成员属性和成员函数。

在C++中,如果你未在类中定义,类会自动为你生成一些默认的成员函数,如果你没有显式地定义它们。这些默认成员函数包括:

  • 默认构造函数 (Default Constructor)
  • 默认析构函数 (Default Destructor)
  • 默认拷贝构造函数(Default Copy Constructor)
  • 默认赋值运算符 (Default Copy Assignment Operator)
  • 默认移动构造函数 (Default Move Constructor)
  • 默认移动赋值运算符 (Default Move Assignment Operator)

本文我们主要介绍前面四种函数,后续介绍其他函数。

2.1 概念

我们可以引用上面的代码来展开介绍。

class Person
{
public:
    void Init(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    void Print()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

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

在我们创建对象时,每次都要显示调用Init函数去给对象属性赋值,但如果每次创建对象时都调用该方法设赋值,显得略为繁琐,那能否在对象创建时,就将信息设置进去呢?便产生了我们的构造函数

  • 作用:用于创建对象时初始化成员变量的默认值。
  • 使用情况:当你创建一个类对象时,如果没有显式地提供构造函数,编译器会为你自动生成一个默认构造函数。默认构造函数没有参数,它将成员变量初始化为其对应类型的默认值(例如,数值类型为0,指针类型为nullptr,类对象的成员会再调用它们自己的构造函数来初始化)。在不同的编译器中可能实现不同,有些编译器对内置类型并不处理,是随机值,类对象的成员会调用他们的构造函数。
  • 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2 特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象,对对象内的成员属性进行初始化。

特征:

  • 1. 函数名与类名相同。
  • 2. 无返回值。
  • 3. 对象实例化时编译器自动调用对应的构造函数。
  • 4. 构造函数可以重载

2.3 语法

#include <iostream>
#include <string>

class Person
{
public:
    // 1.无参构造函数
    Person()
    {}

    // 2.带参构造函数
    Person(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    // 3.打印个人信息
    void PrintInfo()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

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

由上述代码可知,我们暂时可以将构造函数可以分为无参构造函数有参构造函数,我们来看一看如何使用这两个参数对对象进行初始化。

int main()
{
    // 调用无参构造函数创建对象
    Person p1;
    p1.PrintInfo();

    // 调用带参构造函数创建对象
    Person p2("John", 30);
    p2.PrintInfo(); // Output: Name: John, Age: 30

    return 0;
}

使用无参构造初始化对象时,直接使用类名+对象名即可,使用带参构造时要传入对应的参数用来初始化成员属性。

提醒

Person person();

使用无参构造实例化对象是,不要在对象后加上(),这样会导致编译器无法认定这是使用无参构造实例化对象还是声明返回值是Person类型的函数,最好不要这样使用!!!

如果我们在类定义时不主动写构造函数,无论无参还是有参,系统会自动生成默认构造函数,即无参构造函数。一旦用户显式定义编译器将不再生成,即用户自己实现有参或无参,编译器不再实现默认(无参)构造函数。

如果将上述代码的无参构造函数注释,只留下有参构造函数,那么只能通过调用有参构造函数实例化对象,不能够使用无参构造函数。

2.4 注意点

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数

class Date {
public:
    Date() {
        _year = 1900;
        _month = 1;
        _day = 1;
    }

    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

// 以下测试函数能通过编译吗?
void Test() {
    Date d1;
}

答案是不行的。

因为此时无参构造函数和全缺省构造函数均可为此行代码实例化对象,造成二义性,无法编译通过!!!

三、 析构函数

3.1 概念

析构函数是C++中一个特殊的成员函数,用于在对象销毁时进行清理工作和释放资源。它的名称是在类名前加上波浪线(~),例如,如果类名是ClassName,那么析构函数的名称就是~ClassName

析构函数的作用是进行对象的善后处理工作,当对象的生命周期结束时(比如对象超出作用域、被显式删除或者程序退出),析构函数会自动被调用。

3.2 特性

析构函数有以下特点:

  1. 析构函数没有返回值,包括void,也没有参数。
  2. 一个类可以有且只有一个析构函数,而且不能被重载。
  3. 如果你没有显式地定义析构函数,编译器会为你自动生成一个默认析构函数。
  4. 如果类中有动态分配的资源(如堆上的内存、文件句柄等),在析构函数中应该释放这些资源,避免内存泄漏和资源泄漏。

3.3 示例

#include <iostream>

class MyClass {
public:
    // 构造函数
    MyClass() {
        std::cout << "Constructor called." << std::endl;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    std::cout << "Creating object..." << std::endl;
    MyClass obj; // 创建对象,调用构造函数

    std::cout << "Object will be destroyed..." << std::endl;
    // 在这里,obj超出了作用域,对象的生命周期结束,析构函数被自动调用

    return 0;
}
Creating object...
Constructor called.
Object will be destroyed...
Destructor called.

这证明了对象的构造函数和析构函数分别在对象的创建和销毁时被调用。析构函数的调用可以确保对象在销毁时完成必要的清理工作,释放资源,避免资源泄漏。

提醒:如果不手动编写析构函数,系统也会自动生成析构函数,但是系统自带的析构函数时空实现,不做任何事。当然,如果类对象内部不存在堆区开辟的空间,使用系统生成的即可。但如果有堆区开辟的空间,需要在析构函数内部手动释放,否则容易造成内存泄漏!!!其次,类对象中如果有其他类成员属性,会在该对象销毁时,自动调用类成员属性的析构函数,不需要在该类对象的析构函数中管理!!!

四、拷贝构造函数

4.1 概念

拷贝构造函数是C++中的一种特殊构造函数,用于在对象进行复制时创建一个新对象,并将原对象的值拷贝给新对象。它的作用是生成一个新的对象,这个新对象与原对象的内容相同,但是它们是独立的,修改一个对象的内容不会影响另一个对象。

如果你没有显式地定义拷贝构造函数,编译器会为你生成一个默认的拷贝构造函数。默认拷贝构造函数会逐个拷贝成员变量的值,为类中的指针成员进行浅拷贝(即复制指针的值而不是复制指针指向的对象)。如果类中有资源需要深拷贝(如动态分配的内存),则需要自己定义拷贝构造函数来完成深拷贝,否则在析构函数中会对一块空间重复释放导致错误。

4.2 特性

  • 拷贝构造函数是构造函数的一个重载形式。
  • 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
ClassName(const ClassName& other);

4.3 示例

假设有一个类MyClass,并且我们试图定义一个错误的拷贝构造函数,使用传值方式来接收参数:

在上面的例子中,我们定义了一个名为MyClass的类,并试图使用传值方式来定义拷贝构造函数。当我们尝试使用拷贝构造函数创建obj2对象时,会导致无限递归调用,从而导致栈溢出。

这种情况发生的原因是:传值方式会调用拷贝构造函数本身,以创建传递的参数的副本,然后在调用拷贝构造函数的过程中又会再次创建参数的副本,导致无限递归。

为了避免无限递归调用,拷贝构造函数的参数必须使用引用方式接收,这样在拷贝构造函数调用时只会传递对象的引用,而不会创建新的副本。

以下是修正后的示例代码,使用引用方式定义正确的拷贝构造函数:

#include <iostream>

class MyClass {
public:
    // 正确的拷贝构造函数
    MyClass(const MyClass& obj) {
        std::cout << "Copy constructor called." << std::endl;
    }
};

int main() {
    MyClass obj1;
    MyClass obj2 = obj1; // 正确,使用引用方式传递参数
    MyClass obj3(obj2);    //此种方式也可以

    return 0;
}

4.4 深浅拷贝

浅拷贝(Shallow Copy): 浅拷贝是指在拷贝对象时,仅仅是复制对象中的成员变量的值,包括指针成员变量的值。这意味着新对象和原对象会共享相同的资源,而不是为新对象创建独立的资源副本。如果原对象中包含指向堆内存的指针成员,浅拷贝后新对象和原对象的指针成员指向同一块堆内存,造成了两个对象对同一资源的管理,可能会导致资源释放问题和潜在的错误。

深拷贝(Deep Copy): 深拷贝是指在拷贝对象时,会为新对象创建一个独立的资源副本,而不是共享资源。如果原对象中有指向堆内存的指针成员,深拷贝会为新对象的指针成员单独分配内存,将原对象指针所指向的内容复制到新的内存中。这样两个对象就拥有各自独立的资源,修改一个对象的资源不会影响另一个对象。

#include <iostream>
#include <cstring>
#include <cstdlib>

class Person {
public:
    // 构造函数
    Person(const char* name, int age) {
        _name = (char*)malloc(strlen(name) + 1);
        strcpy(_name, name);
        _age = age;
    }

    // 拷贝构造函数(浅拷贝)
    Person(const Person& other) {
        _name = other._name; // 浅拷贝,共享资源
        _age = other._age;
    }

    // 深拷贝构造函数(深拷贝)
    Person(const Person& other) {
        _name = (char*)malloc(strlen(other._name) + 1); // 深拷贝,为新对象分配独立资源
        strcpy(_name, other._name);
        _age = other._age;
    }

    // 析构函数
    ~Person() {
        free(_name);
    }

    // 打印个人信息
    void PrintInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    char* _name;
    int _age;
};

int main() {
    // 创建一个Person对象
    Person p1("John", 30);

    // 浅拷贝
    Person p2(p1);
    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: John, Age: 30

    // 修改p1的值
    p1 = Person("Alice", 25);
    p1.PrintInfo(); // Output: Name: Alice, Age: 25
    p2.PrintInfo(); // Output: Name: Alice, Age: 30(由于浅拷贝,p2共享p1的资源,也被修改为Alice)

    // 深拷贝
    Person p3(p1);
    p1.PrintInfo(); // Output: Name: Alice, Age: 25
    p3.PrintInfo(); // Output: Name: Alice, Age: 25(由于深拷贝,p3拥有独立的资源,不受p1的修改影响)

    return 0;
}

五、 赋值运算符重载

5.1 概念

赋值运算符重载是在C++中允许自定义类的成员之间赋值操作的一种特殊函数。通过重载赋值运算符,我们可以实现类对象之间的自定义赋值行为,确保对象的正确复制和资源管理。

5.2 语法

返回类型 operator=(const 类名& 另一个对象) {
    // 赋值操作的实现
    // 返回对象本身的引用
}

其中,返回类型通常是一个引用类型,这样可以支持连续赋值操作。参数是一个const引用,表示传入的赋值运算符右侧的对象。

5.3 示例

#include <iostream>
#include <cstring>

class Person {
public:
    Person(const char* name, int age) {
        _name = new char[strlen(name) + 1];
        strcpy(_name, name);
        _age = age;
    }

    // 拷贝构造函数
    Person(const Person& other) {
        _name = new char[strlen(other._name) + 1];
        strcpy(_name, other._name);
        _age = other._age;
    }

    // 赋值运算符重载
    Person& operator=(const Person& other) {
        if (this == &other) { // 自我赋值检测
            return *this;
        }
        delete[] _name; // 释放旧资源

        _name = new char[strlen(other._name) + 1];
        strcpy(_name, other._name);
        _age = other._age;

        return *this; // 返回对象本身的引用
    }

    ~Person() {
        delete[] _name;
    }

    void PrintInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    char* _name;
    int _age;
};

int main() {
    Person p1("John", 30);
    Person p2("Alice", 25);

    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: Alice, Age: 25

    p2 = p1; // 赋值操作

    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: John, Age: 30(p2被赋值为p1的内容)

    return 0;
}

在这个示例中,我们在Person类中重载了赋值运算符。在重载函数中,我们首先检查是否发生了自我赋值(对象本身赋值给自己),如果是,则直接返回对象的引用。然后释放旧资源(删除旧的_name内存),然后重新分配内存并复制新的内容。

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

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

相关文章

10-数据结构-队列(C语言)

队列 目录 目录 队列 一、队列基础知识 二、队列的基本操作 1.顺序存储 ​编辑 &#xff08;1&#xff09;顺序存储 &#xff08;2&#xff09;初始化及队空队满 &#xff08;3&#xff09;入队 &#xff08;4&#xff09;出队 &#xff08;5&#xff09;打印队列 &…

编写一个指令(v-focus2end)使输入框文本在聚焦时焦点在文本最后一个位置

项目反馈输入框内容比较多时候&#xff0c;让鼠标光标在最后一个位置&#xff0c;心想什么奇葩需求&#xff0c;后面试了一下&#xff0c;是有点影响体验&#xff0c;于是就有了下面的效果&#xff0c;我目前的项目都是若依的架子&#xff0c;用的是vue2版本。vue3的朋友想要使…

什么是POP3协议?

POP3&#xff08;Post Office Protocol Version 3&#xff09;是一个用于从电子邮件服务器获取邮件的应用层协议。以下是关于POP3的详细解释&#xff1a; 基本操作&#xff1a;使用POP3&#xff0c;电子邮件客户端可以从邮件服务器上下载电子邮件&#xff0c;并将其保存在本地。…

Unity制作护盾——3、蜂窝晶体护盾

Unity制作晶格护盾 大家好&#xff0c;我是阿赵。 继续来做护盾&#xff0c;这一期做一个蜂窝晶体护盾的效果。 一、效果展示 这个晶体护盾的特点是&#xff0c;整个护盾是由很多五边形和六边形的晶体构成&#xff0c;每一块晶体的颜色都在不停的变化&#xff0c;然后每一块晶…

使用 Spring Boot 发送电子邮件(SMTP 集成)

本文探讨了 Spring Boot 与 SMTP 的集成以及如何从您自己的 Spring Boot 应用程序发送电子邮件。 本文探讨如何从您自己的Spring Boot应用程序发送电子邮件。 是的&#xff0c;您可以拥有专用的 REST API&#xff0c;它接受电子邮件发送者和接收者的电子邮件地址、主题…

【雕爷学编程】Arduino动手做(05)---热敏电阻模块之的基本参数、模块特色、电原理与使用说明

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…

SpringBoot 整合Druid

集成Druid Druid简介 Java程序很大一部分要操作数据库&#xff0c;为了提高性能操作数据库的时候&#xff0c;又不得不使用数据库连接池。 Druid 是阿里巴巴开源平台上一个数据库连接池实现&#xff0c;结合了 C3P0、DBCP 等 DB 池的优点&#xff0c;同时加入了日志监控。 D…

Chrome DevTools 与 WebSocket 数据查看失焦的问题

Chrome DevTools 在与 WebSocket 连接交互时可能会出现失焦的问题&#xff0c;这似乎是一个已知的 bug。当 DevTools 选中 WebSocket 消息时&#xff0c;如果有新的消息到达&#xff0c;DevTools 将会自动失焦&#xff0c;导致无法查看完整的消息内容。 虽然这个问题很令人困扰…

C++友元函数和友元类的使用

1.友元介绍 在C中&#xff0c;友元&#xff08;friend&#xff09;是一种机制&#xff0c;允许某个类或函数访问其他类的私有成员。通过友元&#xff0c;可以授予其他类或函数对该类的私有成员的访问权限。友元关系在一些特定的情况下很有用&#xff0c;例如在类之间共享数据或…

高斯模糊与图像处理(Gaussian Blur)

高斯模糊在图像处理中的用途及其广泛&#xff0c;除了常规的模糊效果外&#xff0c;还可用于图像金字塔分解、反走样、高低频分解、噪声压制、发光效果等等等等。正因为高斯模糊太基础&#xff0c;应用太广泛&#xff0c;所以需要尽可能深入认识这个能力&#xff0c;避免在实际…

【css】css中使用变量var

CSS 变量可以有全局或局部作用域。 全局变量可以在整个文档中进行访问/使用&#xff0c;而局部变量只能在声明它的选择器内部使用。 如需创建具有全局作用域的变量&#xff0c;请在 :root 选择器中声明它。 :root 选择器匹配文档的根元素。 如需创建具有局部作用域的变量&am…

无脑——010 复现yolov8 训练自己的数据集 基于yolov8框架 使用rt detr

背景&#xff1a; 2023.08.09导师让调研transformer的相关论文&#xff0c;做CV的都知道transformer多么难跑&#xff0c;需要用8张GPU跑100多个小时&#xff0c;我这个小小实验室放不下这尊大佛&#xff0c;所以就找点小模型跑一跑&#xff0c;调研论文发现最新的是CO-DETR&am…

【BMC】OpenBMC开发基础3:引入新的开源配方

引入新的开源配方 前面介绍了如何在OpenBMC中通过新建配方引入自己的程序&#xff0c;也介绍了如何修改原有的程序&#xff0c;下面要介绍的是如何引入开源的新程序&#xff0c;这在OE系统上是很方便的&#xff0c;重点就在于引入新的配方。 OE为了方便开发者使用&#xff0c…

到 2030 年API 攻击预计将激增近 1000%

导读云原生应用程序编程接口管理公司 Kong 联合外部经济学家的最新研究预计&#xff0c;截至 2030 年 API 攻击将激增 996%&#xff0c;意味着与 API 相关的网络威胁的频率和强度都显着升级。 这项研究由 Kong 分析师和布朗大学副教授 Christopher Whaley 博士合作进行&#x…

Maven进阶2 -- 私服(Nexus)、私服仓库分类、资源上传和下载

目录 私服是一台独立的服务器&#xff0c;用于解决团队内部的资源共享与资源同步问题。 1.Nexus Nexus是sonatype公司的一款maven私服产品。 下载地址 启动 nexus.exe /run nexus 访问 & 登录 2.私服仓库分类 3.资源上传和下载 本地仓库上传和访问资源需要进行配置。…

stm32项目(8)——基于stm32的智能家居设计

目录 一.功能设计 二.演示视频 三.硬件选择 1.单片机 2.红外遥控 3.红外探测模块 4.光敏电阻模块 5.温湿度检测模块 6.风扇模块 7.舵机 8.WIFI模块 9.LED和蜂鸣器 10.火焰传感器 11.气体传感器 四.程序设计 1.连线方式 2.注意事项 3.主程序代码 五.课题意义…

学习pytorch 3 tensorboard的使用

tensorboard的使用 1. 安装2. add_scalar 查看函数图形3. 查看结果4. add_image() 查看训练步骤中间结果的图片 1. 安装 pytorch conda环境 pip install tensorboard pip install opencv-python2. add_scalar 查看函数图形 常用来查看 train val loss等函数图形 from torch…

Mr. Cappuccino的第60杯咖啡——Spring之BeanFactory和ApplicationContext

Spring之BeanFactory和ApplicationContext 类图BeanFactory概述功能项目结构项目代码运行结果总结 ApplicationContext概述功能MessageSource&#xff08;国际化的支持&#xff09;概述项目结构项目代码运行结果 ResourcePatternResolver&#xff08;匹配资源路径&#xff09;概…

基础实验篇 | QGC实时调整控制器参数实验

PART 1 实验名称及目的 QGC实时调整控制器参数实验&#xff1a;在进行硬件在环仿真和真机实验时&#xff0c;常常需要在QGC地面站中观察飞行状态&#xff0c;并对控制器参数进行实时调整&#xff0c;以使得飞机达到最佳的控制效果&#xff0c;但是&#xff0c;在Simulink中设…

网络编程的一些基础知识

什么是协议 协议就是一种网络交互中数据格式和交互流程的约定,通过协议,我们可以与远程的设备进行数据交互,请求或者完成对方的服务(可以认为协议就是一种语言) 什么是端口和端口监听 在Internet上,各主机间通过TCP/IP协议发送和接收数据包,各个数据包根据其目的主机…