C++ 类和对象入门(二):默认成员函数、构造函数和析构函数详解

C++ 类和对象入门(二):默认成员函数、构造函数和析构函数详解


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 ✨ 数据即知识,压缩即智能


目录


前言

这一章要讲的内容是:

默认成员函数。

所谓默认成员函数,就是即使我们不写,编译器在某些情况下也会自动生成的成员函数。

一个类中,比较重要的默认成员函数主要有:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值运算符重载

另外还有取地址运算符重载,C++11 之后还有移动构造和移动赋值。入门阶段先重点理解前四个。

这一篇先讲前两个:

  • 构造函数:对象创建时负责初始化
  • 析构函数:对象销毁时负责清理资源

它们解决的问题很现实:

对象创建时,怎么保证内部数据是合理的?

对象销毁时,怎么保证申请过的资源被释放?


一、什么是默认成员函数?

1.1 默认成员函数的基本概念

默认成员函数指的是:

用户没有显式实现时,编译器可能会自动生成的成员函数。

例如:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

这个类里我们没有写构造函数、析构函数、拷贝构造和赋值运算符重载。

但这并不代表这些函数完全不存在。

在需要的时候,编译器会尝试自动生成它们。

不过这里有一个关键点:

编译器自动生成,不代表一定符合我们的需求。

比如 Date 这种类,里面只有三个 int,编译器默认生成的函数一般够用。

但如果是 Stack 这种类,内部有动态申请的空间:

cpp 复制代码
class Stack
{
private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

那编译器默认生成的拷贝行为可能就有问题。

所以学习默认成员函数,要抓住两个问题:

第一,如果我们不写,编译器默认生成的行为是什么?

第二,如果默认行为不满足需求,我们应该怎么自己写?


二、构造函数:对象出生时自动初始化

2.1 构造函数解决了什么问题?

在学习构造函数之前,我们经常会写一个 Init 函数。

比如日期类:

cpp 复制代码
class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

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

使用时:

cpp 复制代码
Date d;
d.Init(2024, 4, 14);

这种写法能用,但有一个问题:

如果用户忘记调用 Init(),对象内部数据就是不确定的。

构造函数就是用来解决这个问题的。

构造函数会在对象实例化时自动调用,负责完成对象初始化。


2.2 构造函数的基本写法

构造函数有几个特点:

  • 函数名和类名相同;
  • 没有返回值,连 void 也不写;
  • 对象创建时自动调用;
  • 构造函数可以重载。

例如:

cpp 复制代码
class Date
{
public:
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

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

    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }

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

使用时:

cpp 复制代码
Date d1;
Date d2(2024, 4, 14);

其中:

cpp 复制代码
Date d1;

调用无参构造函数。

cpp 复制代码
Date d2(2024, 4, 14);

调用带参构造函数。


三、默认构造函数是编译器生成的吗?

3.1 什么叫默认构造函数?

很多初学者会误以为:

默认构造函数就是编译器自动生成的构造函数。

这个说法不够准确。

准确地说:

不需要传实参就能调用的构造函数,都叫默认构造函数。

所以以下三种都可以叫默认构造函数:

第一种:无参构造函数。

cpp 复制代码
Date()
{
    _year = 1;
    _month = 1;
    _day = 1;
}

第二种:全缺省构造函数。

cpp 复制代码
Date(int year = 1, int month = 1, int day = 1)
{
    _year = year;
    _month = month;
    _day = day;
}

第三种:用户没有写构造函数时,编译器自动生成的构造函数。


3.2 默认构造函数只能有一个能被无参调用

下面这种写法会有问题:

cpp 复制代码
class Date
{
public:
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

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

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

因为:

cpp 复制代码
Date d;

既可以调用无参构造函数,也可以调用全缺省构造函数。

编译器会不知道你到底想调用哪个。

所以实际写代码时,无参构造和全缺省构造一般不要同时写。

更推荐这种写法:

cpp 复制代码
Date(int year = 1, int month = 1, int day = 1)
{
    _year = year;
    _month = month;
    _day = day;
}

这样既能支持:

cpp 复制代码
Date d1;

也能支持:

cpp 复制代码
Date d2(2024, 4, 14);

四、一个容易踩的坑

4.1 这不是创建对象

看下面代码:

cpp 复制代码
Date d3();

很多初学者会以为这是创建了一个对象 d3,并调用无参构造。

但它不是。

它会被编译器理解成:

声明了一个函数 d3,这个函数无参,返回值类型是 Date。

所以创建无参对象时,不要写括号。


五、编译器默认生成的构造函数会做什么?

5.1 对内置类型成员

如果我们什么构造函数都不写,编译器会自动生成一个默认构造函数。

但它对内置类型成员,比如:

  • int
  • char
  • double
  • 指针

通常不会主动初始化成我们想要的值。

例如:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

写:

cpp 复制代码
Date d;

对象确实被创建了。

_year_month_day 不一定是合理值。

所以对于这种类,如果你希望日期默认是 1900-1-1,就应该自己写构造函数。


5.2 对自定义类型成员

如果类中有自定义类型成员,编译器默认生成的构造函数会尝试调用这个成员自己的默认构造函数。

例如:

cpp 复制代码
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (int*)malloc(sizeof(int) * n);
        _capacity = n;
        _top = 0;
    }

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

class MyQueue
{
private:
    Stack pushst;
    Stack popst;
};

MyQueue 没有写构造函数。

但创建 MyQueue 对象时:

cpp 复制代码
MyQueue mq;

编译器生成的默认构造函数会自动调用 pushstpopst 的构造函数。

这也是 C++ 对象组合中非常重要的一点:

外层对象创建时,内部成员对象也会被构造。


六、析构函数:对象死亡前自动清理资源

6.1 析构函数解决了什么问题?

构造函数负责对象初始化。

析构函数负责对象销毁前的清理工作。

比如 Date 类:

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

它没有动态申请资源,所以通常不需要自己写析构函数。

但是 Stack 类中有动态内存:

cpp 复制代码
class Stack
{
private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

如果构造函数里 malloc 了空间,析构函数里就应该 free 掉。

否则就会造成资源泄漏。


6.2 析构函数的基本写法

析构函数的特点:

  • 函数名是在类名前面加 ~
  • 没有参数;
  • 没有返回值,连 void 也不写;
  • 一个类只能有一个析构函数;
  • 对象生命周期结束时自动调用。

例如:

cpp 复制代码
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (int*)malloc(sizeof(int) * n);
        if (_a == nullptr)
        {
            perror("malloc fail");
            return;
        }

        _capacity = n;
        _top = 0;
    }

    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _capacity = 0;
        _top = 0;
    }

private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

这里:

cpp 复制代码
~Stack()

就是析构函数。

Stack 对象生命周期结束时,系统会自动调用它。


七、析构函数不是销毁对象本身

7.1 对象空间谁来释放?

需要注意:

析构函数不是负责销毁对象本身,而是负责清理对象内部申请的资源。

比如局部对象:

cpp 复制代码
void Func()
{
    Stack st;
}

st 是局部对象,它的对象空间在栈帧里。

函数结束时,栈帧销毁,对象空间自然释放。

析构函数负责的是:

cpp 复制代码
free(_a);

也就是释放对象内部动态申请的资源。

所以可以这样理解:

对象空间由生命周期管理;

对象内部资源由析构函数清理。


八、有资源申请时,一定要自己写析构

8.1 Date 不需要,Stack 需要

对于 Date

cpp 复制代码
class Date
{
private:
    int _year;
    int _month;
    int _day;
};

成员都是内置类型,没有动态资源,默认析构函数就够了。

对于 Stack

cpp 复制代码
class Stack
{
private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

_a 指向动态申请的空间。

如果不写析构函数,这块空间不会自动释放。

所以需要自己写:

cpp 复制代码
~Stack()
{
    free(_a);
    _a = nullptr;
    _capacity = 0;
    _top = 0;
}

可以记住:

类里如果自己申请了资源,通常就要自己释放资源。


九、多个对象的析构顺序

9.1 后创建的先析构

在一个局部作用域中,如果有多个对象:

cpp 复制代码
int main()
{
    Stack st1;
    Stack st2;
    Stack st3;

    return 0;
}

析构顺序通常是:

cpp 复制代码
st3
st2
st1

也就是:

后定义的对象先析构。

这个规则和栈的特点很像:先进后出。


十、构造和析构带来的代码变化

10.1 C 语言版本容易忘记 Init 和 Destroy

以前用 C 写栈时,可能是:

cpp 复制代码
ST st;
STInit(&st);

// 使用栈

STDestroy(&st);

如果中间某个分支提前返回,就可能忘记调用 STDestroy

比如括号匹配问题中,一旦发现不匹配直接返回,如果忘记销毁栈,就可能造成资源泄漏。


10.2 C++ 版本自动调用构造和析构

C++ 中写成:

cpp 复制代码
Stack st;

对象创建时自动调用构造函数。

函数结束或对象离开作用域时,自动调用析构函数。

这样就不需要我们手动写:

cpp 复制代码
st.Init();
st.Destroy();

这就是构造和析构最大的价值:

让对象的初始化和清理自动发生,减少忘记调用的风险。


十一、本文总结

这一篇主要讲了 C++ 默认成员函数中的构造函数和析构函数。

构造函数:

  • 对象创建时自动调用;
  • 主要任务是初始化对象;
  • 函数名和类名相同;
  • 没有返回值;
  • 可以重载;
  • 不传参能调用的构造函数都叫默认构造函数。

析构函数:

  • 对象生命周期结束时自动调用;
  • 主要任务是清理对象内部资源;
  • 函数名是 ~类名
  • 无参数、无返回值;
  • 一个类只能有一个析构函数;
  • 有动态资源申请时,通常必须自己写析构。

构造函数负责对象出生时初始化,析构函数负责对象死亡前清理资源。


相关推荐
一个不知名程序员www2 小时前
算法学习入门---算法题DAY5
c++·算法
摇滚侠2 小时前
JavaWeb 全套教程 乱码问题 85-88
java·开发语言
问心无愧05132 小时前
ctf show web入门102
android·java·前端·笔记
GHL2842710902 小时前
登录、注册页面学习
学习
devilnumber2 小时前
Java Lambda方法引用的三类核心类型、转化逻辑与深度对比
java·开发语言
MartinYeung52 小时前
[论文学习]利用索引梯度优化基于优化的 LLM 越狱攻击:MAGIC 方法的深度分析与实现
人工智能·学习·算法
じ☆冷颜〃2 小时前
Picard-Lindelöf 定理的多视角证明、推广与加权范数方法
经验分享·笔记·线性代数·数学建模
geminigoth2 小时前
python入门三:字典、输入、while循环
开发语言·python
牛油果子哥q2 小时前
【C++ this指针】C++ this指针深度精讲:this底层本质、存储位置、调用机制、const this指针、空指针调用、面试坑点与工程实战
开发语言·c++·面试