类和对象(三)-默认成员函数详解与运算符重载

xml 复制代码
hello,这里是AuroraWanderll。
兴趣方向:C++,算法,Linux系统,游戏客户端开发
欢迎关注,我将更新更多相关内容!

我的个人主页

这是类和对象系列的第三篇文章,上篇指引:
类和对象(二)访问限定符-类的实例化与this指针

类和对象(三)-默认成员函数详解与运算符重载

简易目录

  • 类的6个默认成员函数概述
  • 构造函数详解
  • 析构函数详解

1. 类的6个默认成员函数概述

核心概念:当一个类中什么成员都没有时(称为空类),即用户没有显式实现时,编译器会自动生成6个默认成员函数。

复制代码
class Date {}; // 看似空的类,实际上编译器会生成6个默认成员函数
序号 默认成员函数 基本作用
1 构造函数 对象创建时自动调用,用于初始化对象
2 析构函数 对象销毁时自动调用,用于清理资源
3 拷贝构造函数 用同类型的已有对象初始化新对象,如v1(v2)
4 拷贝赋值运算符 将一个对象的值赋给另一个同类型对象,如v1=v2
5 移动构造函数 (C++11) 通过"移动"资源来初始化新对象,避免不必要的拷贝
6 移动赋值运算符 (C++11) 通过"移动"资源来赋值,避免不必要的拷贝
7 取地址重载运算符

其中5,6相对比较进阶,本篇不会提及.

2. 构造函数

2.1 构造函数的概念

背景:为什么我们要有构造函数?

答:C语言传统初始化方式繁琐

复制代码
class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // ... 其他成员
};

int main()
{
    Date d1;
    d1.Init(2022, 7, 5); // 每次创建对象后都要手动调用初始化函数Init,未免太过麻烦
    return 0;
}

构造函数定义 :特殊的成员函数,在创建对象时自动调用,用于初始化对象数据成员,整个生命周期只调用一次。

2.2 构造函数的特性

基本特征

  1. 函数名与类名相同

  2. 无返回值

  3. 对象实例化时自动调用

  4. 重载(一个对象可以有多个不同的构造函数)

    需要注意的是:虽然叫构造函数,但是它并不负责开空间创建对象,它的主要工作是初始化对象

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

    复制代码
     // 2. 带参构造函数(重载)
     Date(int year, int month, int day)
     {
         _year = year;
         _month = month;
         _day = day;
     }

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

    void TestDate()
    {
    Date d1; // 调用无参构造函数
    Date d2(2015, 1, 1); // 调用带参构造函数
    // 注意:无参构造不能加括号,否则变成函数声明
    Date d3(); // 错误:声明了d3函数,而非创建对象
    }

编译器自动生成规则

  • 如果类中没有显式定义构造函数,编译器自动生成无参默认构造函数

  • 一旦用户显式定义任何构造函数,编译器不再生成默认构造函数

    class Date
    {
    // 如果用户显式定义构造函数,编译器不再生成默认构造函数
    // Date(int year, int month, int day) { ... }

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

    int main()
    {
    Date d1; // 如果屏蔽自定义构造函数,编译通过;如果放开,编译失败
    return 0;
    }

默认构造函数的作用

看起来编译器自动生成的默认构造函数没有作用,例如int类型的参数,默认构造之后依旧是随机值。实际上默认构造函数是会根据类型来进行不同的初始化的

  • 对内置类型(int、char等):不处理(C++11前)或使用默认值(C++11后)

  • 对自定义类型:调用其默认构造函数

    class Time
    {
    public:
    Time() // Time类的构造函数
    {
    cout << "Time()" << endl;
    _hour = 0;
    _minute = 0;
    _second = 0;
    }
    private:
    int _hour;
    int _minute;
    int _second;
    };

    class Date
    {
    private:
    // 内置类型成员,C++11之前不处理
    int _year;
    int _month;
    int _day;

    复制代码
      // 自定义类型成员
      Time _t;  // 编译器生成的默认构造函数会调用Time的构造函数

    };

    int main()
    {
    Date d; // 调用Date的默认构造函数,同时会调用Time的构造函数
    return 0;
    }

C++11改进:内置类型成员可以在声明时给默认值

复制代码
class Date
{
private:
    // 内置类型成员给默认值,C++11之后,直接初始化成我们给的默认值
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    
    Time _t;  // 自定义类型
};

默认构造函数规则

  • 无参构造函数、全缺省构造函数、编译器生成的构造函数都算默认构造函数

  • 默认构造函数只能有一个

    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;
      }

    };

如果我们在类的对象中同时写了超过一个的默认构造,那么它就会报错

编译错误原因:

当执行 Date d1; 时,编译器面临选择困难:

  • 可以调用无参构造函数 Date()
  • 也可以调用全缺省构造函数 Date(1900, 1, 1)(使用默认参数)

两个函数都匹配,编译器无法确定该调用哪一个,因此报编译错误

正确写法:

方案1:只保留一个默认构造函数

复制代码
class Date
{
public:
    // 只保留全缺省构造函数(推荐)
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
};

方案2:使用不同的参数列表

复制代码
class Date
{
public:
    // 无参构造函数
    Date() : _year(1900), _month(1), _day(1) {}
    
    // 带参构造函数(不是全缺省)
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
};

可以简单理解默认构造函数是:调用时不需要传递参数的构造函数


总结要点

  • 空类会自动获得6个默认成员函数
  • 构造函数在对象创建时自动调用,用于初始化
  • 构造函数可以重载,名称与类名相同且无返回值
  • 编译器在特定条件下自动生成默认构造函数
  • 默认构造函数对内置类型和自定义类型的处理方式不同
  • C++11允许内置类型成员在声明时给默认值

3. 析构函数详解

3.1 析构函数的概念

核心问题:对象是如何被销毁的?

析构函数定义 :与构造函数功能相反,但不是完成对象本身的销毁 (局部对象的销毁由编译器完成),而是在对象销毁时自动调用,完成对象中资源的清理工作

3.2 析构函数的特性

基本特征:
  1. 函数名 :类名前加上 ~

  2. 参数和返回值:无参数、无返回值类型

  3. 唯一性:一个类只能有一个析构函数,不能重载

  4. 调用时机:对象生命周期结束时自动调用

    class Stack
    {
    public:
    // 构造函数:申请资源
    Stack(size_t capacity = 3)
    {
    _array = (int*)malloc(sizeof(int) * capacity);
    if (NULL == _array)
    {
    perror("malloc申请空间失败!!!");
    return;
    }
    _capacity = capacity;
    _size = 0;
    }

    复制代码
     // 析构函数:释放资源
     ~Stack()
     {
         if (_array)
         {
             free(_array);    // 释放动态内存
             _array = NULL;   // 防止野指针
             _capacity = 0;   // 重置容量
             _size = 0;       // 重置大小
         }
     }
     
     void Push(int data)
     {
         _array[_size] = data;
         _size++;
     }

    private:
    int* _array;
    int _capacity;
    int _size;
    };

    void TestStack()
    {
    Stack s; // 构造函数自动调用
    s.Push(1);
    s.Push(2);
    // 函数结束时,s的析构函数自动调用,释放内存
    }

编译器生成的析构函数

重要特性 :编译器生成的默认析构函数会对自定义类型成员调用其析构函数。

复制代码
class Time
{
public:
    ~Time()
    {
        cout << "~Time()" << endl;  // 析构时输出信息
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 内置类型成员
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    
    // 自定义类型成员
    Time _t;  // 包含Time类对象
};

int main()
{
    Date d;  // 创建Date对象
    return 0;
} // d销毁时,输出:~Time()

运行结果解释

  • 虽然 main 函数中没有直接创建 Time 对象
  • Date 对象 d 包含 Time 成员 _t
  • d 销毁时,编译器为 Date 生成的默认析构函数会自动调用 Time 类的析构函数
析构函数的调用规则

关键原则

  • 创建哪个类的对象,就调用该类的构造函数
  • 销毁哪个类的对象,就调用该类的析构函数
  • 编译器生成的析构函数会保证所有成员都能正确销毁

3.3 何时需要编写析构函数

不需要编写的情况:
复制代码
class Date
{
private:
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 只有内置类型,无动态资源,使用编译器生成的析构函数即可
};
必须编写的情况:
复制代码
class Stack
{
private:
    int* _array;    // 动态分配的内存
    int _capacity;
    int _size;
    
public:
    // 必须编写析构函数来释放动态内存
    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = NULL;
        }
    }
};

也就是说不是说自定义类型就一定需要写析构函数,关键在于你的类型之中是否动态申请资源。

资源泄漏风险:如果类中申请了资源(动态内存、文件句柄、网络连接等)但没有编写析构函数,会导致资源泄漏。

3.4 实际应用场景

场景1:动态数组管理
复制代码
class DynamicArray
{
private:
    int* _data;
    size_t _size;
    
public:
    DynamicArray(size_t size) : _size(size)
    {
        _data = new int[size];  // 动态分配
    }
    
    ~DynamicArray()
    {
        delete[] _data;  // 必须释放
        _data = nullptr;
    }
};
场景2:文件资源管理
复制代码
class FileHandler
{
private:
    FILE* _file;
    
public:
    FileHandler(const char* filename)
    {
        _file = fopen(filename, "r");
    }
    
    ~FileHandler()
    {
        if (_file)
        {
            fclose(_file);  // 必须关闭文件
            _file = nullptr;
        }
    }
};

总结:

  1. 析构函数作用:对象销毁时自动调用,用于资源清理
  2. 语法特征~类名(),无参无返回值,不能重载
  3. 调用时机:对象生命周期结束时自动调用
  4. 编译器行为:默认生成的析构函数会调用自定义类型成员的析构函数
  5. 编写原则:有资源申请时必须编写,无资源时可依赖编译器生成
  6. 资源管理:防止内存泄漏、文件未关闭等资源管理问题

核心思想:谁申请,谁释放;构造函数申请资源,析构函数释放资源,形成完整的资源管理生命周期。

xml 复制代码
感谢你能够阅读到这里,如果本篇文章对你有帮助,欢迎点赞收藏支持,关注我,
我将更新更多有关C++,Linux系统·网络部分的知识。
相关推荐
IoT智慧学堂2 小时前
C语言循环结构综合应用篇(详细案例讲解)
c语言·开发语言
青云交2 小时前
Java 大视界 -- Java+Spark 构建企业级用户画像平台:从数据采集到标签输出全流程(437)
java·开发语言·spark·hbase 优化·企业级用户画像·标签计算·高并发查询
Minecraft红客2 小时前
C++制作迷宫第一版
c++·游戏·电脑·娱乐
航Hang*2 小时前
第3章:复习篇——第1节:创建和管理数据库
开发语言·数据库·笔记·sql·sqlserver
云栖梦泽2 小时前
鸿蒙原子化服务开发实战:构建免安装的轻量应用
开发语言·鸿蒙系统
恶魔泡泡糖2 小时前
keil4创建工程项目
c语言·单片机
YY&DS2 小时前
《Qt 手写 HTTP 登录服务实战》
开发语言·qt·http
阿华hhh2 小时前
数据结构(树)
linux·c语言·开发语言·数据结构
雪域迷影2 小时前
Windows11中VS2026使用C++ 现代化json库nlohmann的3种方式
开发语言·c++·json