de风——【从零开始学C++】(三):类和对象(中序):默认成员函数全解析

大家好,我是你们的小小风呀!上一篇我们讲了类和对象的基础,认识了类的定义、实例化和 this 指针。今天我们继续深入,来聊聊类里最核心的默认成员函数

很多新手刚学的时候会发现,我明明什么成员函数都没写,创建对象、销毁对象居然都能正常运行?这就是因为编译器偷偷帮我们做了很多事,默认生成了 6 个成员函数!今天我们就把这 6 个函数一个个拆开来,用大白话 + 简单代码,保证你看完就懂!

目录

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

基础概念

二、构造函数:对象的初始化

1.基础概念

2.构造函数的特点

[示例 1:构造函数的基本使用](#示例 1:构造函数的基本使用)

[示例 2:全缺省的构造函数](#示例 2:全缺省的构造函数)

三、析构函数:对象的销毁

1.基础概念

2.析构函数的特点

[3.示例 3:析构函数的基本使用](#3.示例 3:析构函数的基本使用)

四、拷贝构造函数:对象的克隆

1.基础概念

2.拷贝构造函数的特点

[3.示例 4:默认拷贝构造的使用](#3.示例 4:默认拷贝构造的使用)

[4.示例 5:浅拷贝的坑,重复释放崩溃](#4.示例 5:浅拷贝的坑,重复释放崩溃)

[5.示例 6:深拷贝的拷贝构造,解决浅拷贝问题](#5.示例 6:深拷贝的拷贝构造,解决浅拷贝问题)

五、赋值运算符重载:对象的赋值

1.基础概念

2.注意点:

3.赋值运算符的重载

[4.示例 7:赋值运算符重载的实现](#4.示例 7:赋值运算符重载的实现)

[六、取地址运算符重载和 const 成员函数](#六、取地址运算符重载和 const 成员函数)

1.基础概念

[2.示例 8:取地址重载和 const 成员函数](#2.示例 8:取地址重载和 const 成员函数)

总结:


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

基础概念

默认成员函数就是:你定义一个类的时候,如果你什么成员函数都不写,编译器也会自动给你生成 6 个默认的成员函数,帮你处理对象的创建、销毁、拷贝这些通用的操作,不用你自己写。

这 6 个函数分别是什么呢?我们用一个简单的思维导图来给大家列出来:

是不是很清晰?接下来我们一个个来拆解,每个函数到底是干嘛的,有什么特点!


二、构造函数:对象的初始化

1.基础概念

我们之前写类的时候,创建完对象,还要手动调用一个**Init函数** 来初始化成员变量,比如之前的 Student 类,要手动set_nameset_age,太麻烦了!

而构造函数,就是专门用来初始化对象的!创建对象的时候,编译器会自动调用它,帮你把成员变量初始化好,不用你手动调用了,是不是很方便?

2.构造函数的特点

它有 7 个非常重要的特点,新手一定要记牢:

  1. 函数名必须和类名一模一样 :比如类叫Date,构造函数就必须叫Date,不能改名字。

  2. 没有返回值!连 void 都不能写:因为它是创建对象的时候自动调用的,不需要返回什么东西给你。

  3. 可以重载!:你可以写多个构造函数,只要参数不同就行,就像我们之前学的函数重载一样。

  4. 可以带参数,也支持缺省参数:你可以给参数加默认值,这样不传参也能初始化。

  5. 创建对象的时候,编译器自动调用:你不用手动去调用它,定义对象或者 new 对象的时候,它自己就跑了。

  6. 如果你没写,编译器会自动生成一个空的无参构造:就是你什么都不写,编译器也会给你一个空的构造函数,帮你创建对象。

  7. 默认构造函数指的是 "不用传参就能调用的构造":无参的、全缺省的都算,一个类只能有一个默认构造函数,不然编译器不知道调用哪个。

示例 1:构造函数的基本使用

这个例子会展示怎么写不同的构造函数,看看它们是怎么自动调用的。

cpp 复制代码
#include <iostream>
using namespace std;

class Date 
{
private:
    int _year;
    int _month;
    int _day;
public:
    // 1. 无参构造函数
    Date() 
    {
        _year = 2024;
        _month = 1;
        _day = 1;
        cout << "无参构造函数调用了!" << endl;
    }

    // 2. 带参构造函数
    Date(int year, int month, int day) 
    {
        _year = year;
        _month = month;
        _day = day;
        cout << "带参构造函数调用了!" << endl;
    }

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

int main() 
{
    // 调用无参构造,注意!这里不能写Date d1(); 那会变成函数声明!
    Date d1;
    d1.Print();

    // 调用带参构造
    Date d2(2024, 4, 26);
    d2.Print();

    return 0;
}

运行结果:

cpp 复制代码
无参构造函数调用了!
2024-1-1
带参构造函数调用了!
2024-4-26

新手踩坑提醒:定义无参对象的时候,千万不要写Date d1();!这会被编译器当成一个返回值是 Date、没有参数的函数声明,而不是对象!

示例 2:全缺省的构造函数

这个例子会展示全缺省的构造函数,它既可以不传参,也可以传参,非常灵活。

cpp 复制代码
#include<iostream>
using namespace std;

class Date 
{
private:
    int _year;
    int _month;
    int _day;
public:
    // 全缺省构造,所有参数都有默认值
    Date(int year = 2024, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
        cout << "全缺省构造调用了!" << endl;
    }

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

int main() 
{
    // 不传参,用默认值
    Date d1;
    d1.Print();

    // 传一个参数
    Date d2(2025);
    d2.Print();

    // 传三个参数
    Date d3(2024, 4, 26);
    d3.Print();

    return 0;
}

运行结果:

cpp 复制代码
全缺省构造调用了!
2024-1-1
全缺省构造调用了!
2025-1-1
全缺省构造调用了!
2024-4-26

你看,一个构造函数就搞定了所有情况,是不是很方便?而且它就是我们说的默认构造函数,因为不用传参就能调用。


三、析构函数:对象的销毁

1.基础概念

析构函数和构造函数正好反过来!构造是对象出生的时候,帮你初始化、申请资源;析构就是对象死亡的时候,帮你清理资源!

比如你在对象里申请了堆内存、打开了文件、申请了锁,这些系统不会自动帮你释放,你就要在析构函数里自己写,对象销毁的时候,编译器会自动调用析构,帮你把这些资源释放掉,不然就会造成内存泄漏!

2.析构函数的特点

它有 6 个重要的特点:

  1. 函数名是~加类名 :比如类叫Date,析构就是~Date,前面加个波浪号,很好记。

  2. 没有返回值,也没有任何参数:因为它是自动调用的,不需要你传东西,也不需要返回东西。

  3. 不能重载!:因为没有参数,所以一个类只能有一个析构函数,你没法写多个不同的。

  4. 对象销毁的时候,编译器自动调用:比如局部对象,出了作用域就自动调用析构,不用你手动调用。

  5. 如果你没写,编译器会自动生成一个空的析构:如果你的类里没有什么需要清理的资源,比如只有 int、string 这些,编译器生成的空析构就够了。

  6. 主要用来释放对象自己申请的资源:比如堆内存、文件句柄、锁这些,这些系统不会自动帮你释放,需要你在析构里自己写。

3.示例 3:析构函数的基本使用

这个例子会展示析构函数怎么帮我们释放堆内存,避免内存泄漏。

cpp 复制代码
#include <iostream>
using namespace std;

class Stack 
{
private:
    int* _a;
    int _top;
    int _capacity;
public:
    //构造函数
    Stack(int capacity = 4)
    {
        // 构造里申请堆内存
        _a = (int*)malloc(sizeof(int) * capacity);
        _top = 0;
        _capacity = capacity;
        cout << "构造函数调用了,申请了内存" << endl;
    }

    // 析构函数,释放资源
    ~Stack() {
        free(_a);
        _a = nullptr;
        cout << "析构函数调用了,释放了内存" << endl;
    }

    void Push(int x) 
    { }
};

int main() 
{
    // 我们用一个大括号来做作用域
    {
        Stack s;
        // 在这里s还是活着的
    }
    // 出了这个作用域,s就销毁了,自动调用析构!
    cout << "出了作用域了" << endl;

    return 0;
}

运行结果:

cpp 复制代码
构造函数调用了,申请了内存
析构函数调用了,释放了内存
出了作用域了

你看,是不是很神奇?我们什么都没做,出了作用域,析构就自动跑了,把我们申请的内存释放了,这样就不会有内存泄漏了!


四、拷贝构造函数:对象的克隆

1.基础概念

拷贝构造,顾名思义,就是拷贝一个已有的对象,来创建一个一模一样的新对象,就像克隆一样!你已经有了一个对象 d1,现在要创建一个新对象 d2,和 d1 完全一样,这时候就会调用拷贝构造函数。

2.拷贝构造函数的特点

它有 6 个重要的特点:

  1. 它是构造函数的一种,专门用来初始化新对象:注意哦,它是初始化,不是赋值!是创建新对象的时候才用的。

  2. 只有一个参数,就是当前类的 const 引用:必须是引用,不然会无限递归!因为传值的话,又要调用拷贝构造来拷贝参数,就死循环了。

  3. 如果你没写,编译器会自动生成,默认是浅拷贝:就是把原来对象的成员变量,一个个的值拷贝过来,但是如果有指针的话,就会出问题。

  4. 用一个对象初始化另一个对象的时候,自动调用 :比如Date d2(d1);或者Date d2 = d1;,这时候都会调用拷贝构造。

  5. 传值传参的时候,会自动调用拷贝构造 :比如你函数的参数是Date类型,传值的话,就会把实参拷贝给形参,调用拷贝构造。

  6. 值返回的时候,也会自动调用拷贝构造:函数返回值是值类型的话,会把返回的对象拷贝到外面,调用拷贝构造。

3.示例 4:默认拷贝构造的使用

这个例子会展示默认的拷贝构造,对于普通的成员变量,它是完全没问题的。

cpp 复制代码
#include <iostream>
using namespace std;

class Date 
{
private:
    int _year;
    int _month;
    int _day;
public:
    //构造函数
    Date(int year, int month, int day) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

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

int main() 
{
    //初始化d1
    Date d1(2024, 4, 26);
    // 用d1初始化d2,调用编译器自动生成的默认拷贝构造
    Date d2 = d1;

    d1.Print();
    d2.Print();

    return 0;
}

运行结果:

cpp 复制代码
2024-4-26
2024-4-26

你看,两个对象的内容完全一样,因为都是 int 成员,浅拷贝没问题,两个对象互不影响。

4.示例 5:浅拷贝的坑,重复释放崩溃

但是如果你的类里有指针成员,默认的浅拷贝就会出大问题!这个例子会给你展示这个坑。

cpp 复制代码
#include <iostream>
using namespace std;

class Stack 
{
private:
    int* _a;
    int _top;
    int _capacity;
public:
    //构造函数
    Stack(int capacity = 4) 
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        _top = 0;
        _capacity = capacity;
        cout << "构造调用" << endl;
    }

    //析构函数
    ~Stack() 
    {
        free(_a);
        _a = NULL;
        cout << "析构调用" << endl;
    }

    void Push(int x) 
    { }
};

int main() 
{
    Stack s1;
    // 用s1拷贝构造s2,默认的浅拷贝
    Stack s2 = s1;

    return 0;
}

这个代码运行之后会直接崩溃!为什么?因为默认的浅拷贝,只是把s1_a指针的值拷贝给了s2,也就是说,两个对象的指针指向了同一块堆内存

然后对象销毁的时候,s2 先析构,把这块内存 free 了,然后 s1 析构的时候,又 free 了一次!重复释放同一块内存,程序就直接崩了!这就是浅拷贝的大问题!

5.示例 6:深拷贝的拷贝构造,解决浅拷贝问题

那怎么解决呢?我们自己写拷贝构造,做深拷贝!就是不仅拷贝指针的值,还要把指针指向的内存也拷贝一份,让两个对象的指针指向不同的内存!

cpp 复制代码
#include <iostream>
#include <string.h>
using namespace std;

class Stack 
{
private:
    int* _a;
    int _top;
    int _capacity;
public:

    Stack(int capacity = 4) 
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        _top = 0;
        _capacity = capacity;
        cout << "构造调用" << endl;
    }

    // 自己写的拷贝构造,深拷贝!
    Stack(const Stack& s) 
    {
        // 先拷贝普通的成员
        _top = s._top;
        _capacity = s._capacity;
        // 重点!自己申请新的内存,把数据拷贝过来!
        _a = (int*)malloc(sizeof(int) * _capacity);
        memcpy(_a, s._a, sizeof(int) * _top);
        cout << "拷贝构造(深拷贝)调用" << endl;
    }

    ~Stack() 
    {
        free(_a);
        cout << "析构调用" << endl;
    }

    void Push(int x) 
    { }
};

int main() 
{
    Stack s1;
    // 现在调用我们自己写的深拷贝构造
    Stack s2 = s1;

    return 0;
}

运行结果:

cpp 复制代码
构造调用
拷贝构造(深拷贝)调用
析构调用
析构调用

你看,现在就正常了!两个对象各自有自己的内存,析构的时候各自释放,就不会重复释放了!


五、赋值运算符重载:对象的赋值

1.基础概念

首先我们来聊聊什么是运算符重载,我们之前学了函数重载,运算符重载其实就是,把运算符当成一个特殊的函数,你可以自己定义这个运算符对你的类怎么用!

比如我们之前学的+,只能给 int、double 这些用,现在你可以重载它,让它能给你的 Date 类用,比如d1 + 10,就是日期加 10 天,这就是运算符重载!

2.注意点:

  • 不能改变运算符的优先级,比如*本来比+高,你重载之后还是一样。

  • 不能创造新的运算符,比如你不能创造一个**的运算符,只能重载 C++ 已经有的。

  • 不能改变运算符的操作数个数,比如+本来是两个操作数,你不能改成一个。

而我们今天要讲的赋值运算符重载 ,就是重载=这个运算符,用来把一个对象赋值给另一个已经存在的对象!

注意哦,和拷贝构造不一样!拷贝构造是创建新对象的时候用的,赋值是两个对象都已经创建好了,把一个的内容给另一个!

3.赋值运算符的重载

它有 4 个重要的特点:

  1. 参数是当前类的 const 引用:和拷贝构造一样,不能传值,不然会递归调用。

  2. 返回值是当前类的引用 :这样才能支持连续赋值,比如d1 = d2 = d3;,不然返回值的话就做不到了。

  3. 必须检查自赋值 :就是不能自己给自己赋值,比如d1 = d1;,不然你把自己的资源释放了,就凉了。

  4. 如果你没写,编译器会自动生成,默认也是浅拷贝:和拷贝构造一样,默认的赋值也是值拷贝,有指针的话也会出问题。

4.示例 7:赋值运算符重载的实现

这个例子会展示我们怎么自己实现赋值运算符的深拷贝,解决浅拷贝的问题。

cpp 复制代码
#include <iostream>
#include <string.h>
using namespace std;

class Stack 
{
private:
    int* _a;
    int _top;
    int _capacity;
public:

    Stack(int capacity = 4) 
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        _top = 0;
        _capacity = capacity;
    }

    // 拷贝构造我们之前写过了
    Stack(const Stack& s) 
    {
        _top = s._top;
        _capacity = s._capacity;
        _a = (int*)malloc(sizeof(int) * _capacity);
        memcpy(_a, s._a, sizeof(int) * _top);
    }

    // 赋值运算符重载!
    Stack& operator=(const Stack& s) 
    {
        // 1. 第一步,检查自赋值!
        if (this == &s) 
        {
            // 自己给自己赋值,直接返回,啥也不用干
            return *this;
        }

        // 2. 第二步,释放自己原来的旧资源
        free(_a);

        // 3. 第三步,拷贝新的资源,和拷贝构造差不多
        _top = s._top;
        _capacity = s._capacity;
        _a = (int*)malloc(sizeof(int) * _capacity);
        memcpy(_a, s._a, sizeof(int) * _top);

        // 4. 第四步,返回*this,支持连续赋值
        return *this;
    }

    ~Stack()
    {
        free(_a);
    }

    void Push(int x)
    {
        _a[_top] = x;
        _top++;
    }
};

int main() 
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);

    Stack s2;
    // 赋值,调用我们自己写的赋值重载
    s2 = s1;

    // 连续赋值!因为返回了引用,所以可以这么写
    Stack s3;
    s3 = s2 = s1;

    return 0;
}

这样,我们就搞定了赋值的问题,两个对象的资源是独立的,不会再有浅拷贝的坑了!


六、取地址运算符重载和 const 成员函数

1.基础概念

最后我们来看两个比较简单的默认成员函数,首先是const 成员函数

什么是 const 成员函数?就是成员函数后面加个const,比如void Print() const;,它的意思是:这个函数里,this 指针是 const 的,也就是说,你不能在这个函数里修改成员变量,相当于给这个函数加了个只读的保护!

这样有什么好处呢?const 对象也能调用这个函数!因为 const 对象的 this 是 const 的,普通的成员函数的 this 是非 const 的,const 对象不能调用,但是 const 成员函数就可以!

然后是取地址运算符重载 ,就是重载&这个运算符,默认的&运算符,对一个对象用,就是返回这个对象的地址,但是你也可以自己重载它,比如你想隐藏对象的真实地址,或者返回别的东西,这时候就可以重载。

而且,你还要写一个 const 版本的,因为 const 对象调用&的时候,会调用 const 的重载版本,这就是为什么默认成员函数里有两个取地址的重载!

2.示例 8:取地址重载和 const 成员函数

这个例子会展示这两个特性的用法。

cpp 复制代码
#include <iostream>
using namespace std;

class Date {
private:
    int _year;
    int _month;
    int _day;
public:

    Date(int year, int month, int day)
        //初始化列表:后面会进一步学习,先做了解
        : _year(year), _month(month), _day(day)
    {
    }

    // 普通的取地址重载,给普通对象用
    Date* operator&() 
    {
        cout << "普通的取地址重载调用了" << endl;
        // 这里我们可以返回别的,比如隐藏真实地址,这里就返回this演示
        return this;
    }

    // const的取地址重载,给const对象用的
    const Date* operator&() const 
    {
        cout << "const的取地址重载调用了" << endl;
        return this;
    }

    // const成员函数,不能修改成员变量
    void Print() const 
    {
        cout << _year << "-" << _month << "-" << _day << endl;
        // 这里不能修改_year,不然会报错,因为this是const的
        // _year = 2025; 这行编译会报错!
    }
};

int main() 
{
    Date d1(2024, 4, 26);
    // 普通对象调用,调用普通的取地址
    cout << &d1 << endl;

    const Date d2(2024, 1, 1);
    // const对象调用,调用const的取地址
    cout << &d2 << endl;

    // const对象调用const成员函数
    d2.Print();

    return 0;
}

运行结果:

cpp 复制代码
普通的取地址重载调用了
0x7ffd8b7c5a4c
const的取地址重载调用了
0x7ffd8b7c5a48
2024-1-1

你看,是不是很清晰?普通对象和 const 对象,分别调用了对应的重载版本,const 成员函数也保护了成员变量不会被修改。


总结:

今天我们把 6 个默认成员函数全部讲完了!这几个函数是类和对象的核心,很多新手刚学的时候会搞混拷贝构造和赋值,搞不清浅拷贝和深拷贝,没关系,把今天的例子自己敲一遍,动手实操比看十遍都有用!

如果有任何不懂的地方,欢迎在评论区留言,我会一一回复!下一篇我们会讲类和对象的进阶内容,别忘了点赞收藏关注,我们下期再见~

相关推荐
迷途之人不知返2 小时前
vector的模拟实现
c++
龙俊杰的读书笔记2 小时前
一文读懂python并发&并行编程--以xinference框架应用为例
开发语言·网络·python
liulilittle2 小时前
递归复制搜索所有的lua文件到指定目录
java·开发语言·lua·cmd
Gofarlic_oms12 小时前
Allegro高级功能模块许可证管理注意事项
运维·服务器·开发语言·matlab·负载均衡
启山智软2 小时前
前沿主流技术栈商城系统(Java JDK21 + Vue3 + Uniapp)
java·开发语言·uni-app
浅念-2 小时前
分治算法专题|LeetCode高频经典题目详细题解
数据结构·c++·算法·leetcode·职场和发展·排序·分治
H Journey2 小时前
C++ 性能瓶颈分析与优化
c++·性能优化·gprof·perf·valgrind·瓶颈分析
QH139292318802 小时前
Rohde & Schwarz ZNA43矢量网络分析仪的使用方法
开发语言·php
沐知全栈开发2 小时前
SVG 实例
开发语言