C++ 类和对象(二):默认成员函数详解

在 C++ 面向对象编程中,类的默认成员函数是非常重要的概念。当我们没有显式实现某些成员函数时,编译器会自动生成它们,这些函数被称为默认成员函数。本文将详细介绍 C++ 类的 6 个默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载以及取地址运算符重载。

一、默认成员函数概述

默认成员函数是指用户没有显式实现,编译器会自动生成的成员函数。一个类在我们不写任何成员函数的情况下,编译器会默认生成以下 6 个默认成员函数:

  1. 构造函数

  2. 析构函数

  3. 拷贝构造函数

  4. 赋值运算符重载

  5. 普通取地址运算符重载

  6. const 取地址运算符重载

其中前 4 个是我们需要重点掌握的,后两个在大多数情况下使用编译器自动生成的即可。另外,C++11 以后还增加了两个默认成员函数:移动构造和移动赋值,本文暂不讨论。

二、构造函数

构造函数是一种特殊的成员函数,其作用是在对象实例化时初始化对象,替代了我们以前手动调用的Init函数,并且会自动调用。

构造函数的特点:

  1. 函数名与类名相同

  2. 无返回值(不需要写void

  3. 对象实例化时系统会自动调用对应的构造函数

  4. 可以重载

  5. 若未显式定义,编译器会生成无参的默认构造函数;一旦用户显式定义,编译器不再生成

  6. 无参构造函数、全缺省构造函数、编译器默认生成的构造函数都称为默认构造函数(不传实参即可调用),且这三者不能同时存在

  7. 编译器默认生成的构造函数对内置类型成员变量的初始化不确定,对自定义类型成员变量会调用其默认构造函数

构造函数示例

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

class Date
{
public:
    // 无参构造函数
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    // 带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // 全缺省构造函数(不能与无参构造同时存在)
    // Date(int year = 1, int month = 1, int day = 1)
    // {
    //     _year = year;
    //     _month = month;
    //     _day = day;
    // }

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

int main()
{
    Date d1;              // 调用无参构造函数
    Date d2(2025, 1, 1);  // 调用带参构造函数
    
    // 注意:以下写法是函数声明,不是对象实例化
    // Date d3();
    
    return 0;
}

大多数情况下,我们需要自己实现构造函数。只有少数情况,如类的成员都是自定义类型且这些自定义类型有合适的默认构造函数时(如用两个 Stack 实现队列),编译器自动生成的构造函数才够用。

三、析构函数

析构函数的功能并不是销毁对象本身(对象在生命周期结束时会自动销毁),而是完成对象中资源的清理释放工作,类比我们之前实现的Destroy函数。

析构函数的特点

  1. 析构函数名是在类名前加上~

  2. 无参数无返回值(不需要写void

  3. 一个类只能有一个析构函数,若未显式定义,系统会自动生成

  4. 对象生命周期结束时,系统会自动调用析构函数

  5. 编译器自动生成的析构函数对内置类型成员不做处理,对自定义类型成员会调用其析构函数

  6. 即使显式定义了析构函数,自定义类型成员的析构函数也会被自动调用

  7. 没有申请资源的类(如 Date)可以不写析构函数;有资源申请的类(如 Stack)必须自己写析构函数,否则会造成资源泄漏

  8. 局部域的多个对象,后定义的先析构

析构函数示例

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

typedef int STDataType;
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }

    // 析构函数:释放资源
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

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

// 两个Stack实现队列
class MyQueue
{
public:
    // 编译器默认生成的析构函数会调用Stack的析构函数
    ~MyQueue()
    {
        cout << "~MyQueue()" << endl;
    }

private:
    Stack pushst;
    Stack popst;
};

int main()
{
    Stack st;
    MyQueue mq;
    // 析构顺序:~MyQueue() -> ~Stack()(popst) -> ~Stack()(pushst) -> ~Stack()(st)
    return 0;
}

四、拷贝构造函数

拷贝构造函数是一种特殊的构造函数,用于用一个已存在的对象初始化一个新创建的对象。

拷贝构造函数的特点

  1. 是构造函数的一个重载

  2. 第一个参数必须是自身类类型对象的引用,使用传值方式会引发无穷递归调用;可以有多个参数,但后续参数必须有缺省值

  3. C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造(如传值传参、传值返回)

  4. 若未显式定义,编译器会生成默认拷贝构造函数,对内置类型成员进行值拷贝 / 浅拷贝,对自定义类型成员调用其拷贝构造

  5. 成员全是内置类型且无资源的类(如 Date)不需要显式实现;有资源的类(如 Stack)需要显式实现深拷贝,否则会导致双重释放

  6. 传值返回会产生临时对象并调用拷贝构造;传引用返回可减少拷贝,但需确保返回对象在函数结束后仍存在

拷贝构造函数示例

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

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    
    // 拷贝构造函数
    Date(const Date& d)  // 使用const引用避免修改原对象
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

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

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

int main()
{
    Date d1(2025,11,30);
    d1.Print();  // 2025/11/30

    // 拷贝构造
    Date d2(d1);
    d2.Print();  // 2025/11/30
    
    Date d3 = d2;  // 也是拷贝构造
    d3.Print();  // 2025/11/30
    
    return 0;
}

深拷贝实现(以 Stack 为例)

cpp 复制代码
// 栈的深拷贝构造
Stack(const Stack& st)
{
    // 对指针指向的资源重新申请空间并复制数据
    _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
    if (nullptr == _a)
    {
        perror("malloc申请空间失败!!!");
        return;
    }
    memcpy(_a, st._a, sizeof(STDataType) * st._top);
    _top = st._top;
    _capacity = st._capacity;
}

深 / 浅拷贝区别

  • 浅拷贝:按字节拷贝,对于指针成员只拷贝地址,不拷贝资源,可能导致多个对象共享同一份资源

  • 深拷贝 :不仅拷贝指针本身,还对指针指向的资源重新申请空间并复制数据,每个对象拥有独立的资源

五、赋值运算符重载

赋值运算符重载用于完成两个已存在对象之间的拷贝赋值,与拷贝构造的区别是:拷贝构造用于用已有对象初始化新对象,而赋值重载用于两个已存在对象之间的赋值。

运算符重载基础

  • 运算符重载是具有特殊名字的函数,形式为operator运算符

  • 重载运算符的参数个数与运算对象数量一致,一元运算符 1 个参数,二元运算符 2 个参数

  • 若为成员函数,第一个参数为隐式的this指针,因此参数比运算对象少一个

  • 不能重载的运算符:.*::sizeof?:.

  • 重载操作符至少有一个类类型参数

  • 前置 ++ 和后置 ++ 的区分:后置 ++ 重载时增加一个int形参

  • <<>>建议重载为全局函数,以符合使用习惯

赋值运算符重载的特点

  1. 必须重载为成员函数

  2. 参数建议为const当前类类型引用,避免传值拷贝

  3. 返回值建议为当前类类型引用,支持连续赋值

  4. 未显式实现时,编译器会生成默认赋值重载,行为与默认拷贝构造类似(内置类型浅拷贝,自定义类型调用其赋值重载)

  5. 有资源的类需要显式实现深拷贝的赋值重载

赋值运算符重载示例

cpp 复制代码
// 赋值运算符重载
Date& operator=(const Date& d)
{
    // 避免自己给自己赋值
    if (this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;  // 支持连续赋值
}

// 使用示例
int main()
{
    Date d1(2025, 11, 30);
    Date d2(2025, 12, 1);
    
    d1 = d2;  // 赋值重载,两个已存在对象
    
    Date d3(d2);  // 拷贝构造,用d2初始化新对象d3
    Date d4 = d2;  // 拷贝构造,不是赋值
    
    return 0;
}

六、取地址运算符重载

取地址运算符重载分为普通取地址和 const 取地址 两种,一般情况下编译器自动生成的即可满足需求,不需要显式实现

const成员函数

const 成员函数是指用 const 修饰的成员函数,const 放在参数列表后面,实际修饰的是隐含的this指针,表明在该函数中不能修改类的任何成员。

cpp 复制代码
// const成员函数示例
void Print() const  // 实际为void Print(const Date* const this)
{
    cout << _year << "-" << _month << "-" << _day << endl; 
}

取地址运算符重载示例

cpp 复制代码
class Date 
{ 
public : 
    // 普通取地址运算符重载
    Date* operator&() 
    { 
        return this; 
        // return nullptr; // 可以自定义返回值
    } 
    
    // const取地址运算符重载
    const Date* operator&()const 
    { 
        return this; 
        // return nullptr; // 可以自定义返回值
    } 
private : 
    int _year;  // 年
    int _month; // 月 
    int _day;   // 日 
};
相关推荐
爱吃大芒果5 小时前
Flutter 动画实战:隐式动画、显式动画与自定义动画控制器
开发语言·javascript·flutter·ecmascript·gitcode
郝学胜-神的一滴5 小时前
OpenGL中的glDrawArrays函数详解:从基础到实践
开发语言·c++·程序人生·算法·游戏程序·图形渲染
李白你好5 小时前
Bypass_Webshell webshell编码工具 支持 jsp net php asp编码免杀
开发语言·php
feifeigo1235 小时前
C#中实现控件拖动功能
开发语言·c#
曹牧5 小时前
C#:List<string>类型的集合转换成用逗号分隔的字符串
开发语言·c#·list
fengfuyao9855 小时前
基于C# WinForm的收银管理系统实现
开发语言·c#
05大叔5 小时前
苍穹外买Day05
java·开发语言
Trouvaille ~5 小时前
【LInux】进程程序替换与shell实现:从fork到exec的完整闭环
linux·运维·c语言·c++·ssh·进程替换·基础入门
YXWik65 小时前
Linux安装Whisper(C++版)音频解析文本
linux·c++·whisper