C++ 类和对象(中):默认成员函数的核心原理与实践

        在 C++ 面向对象编程中,类的默认成员函数是构建健壮代码的基石。当我们未显式实现这些函数时,编译器会自动生成,但其行为是否符合需求、何时需要手动重写,是初学者容易混淆的关键点。本文将围绕类的 6 个默认成员函数,结合实例深入解析构造函数、析构函数、拷贝构造函数、赋值运算符重载的核心逻辑,同时简要介绍取地址运算符重载,帮助你彻底掌握这一重要知识点。

一、默认成员函数概述:编译器的 "自动生成" 规则

默认成员函数是指用户未显式实现时,编译器会自动生成的成员函数。一个空类在编译器处理后,会隐式包含6 个默认成员函数,其中前 4 个是核心,后 2 个(取地址重载)极少需要手动实现。

函数类型核心作用是否需要频繁手动实现
构造函数初始化对象是(内置类型需手动初始化)
析构函数清理对象资源是(有动态资源申请时必须实现)
拷贝构造函数用同类对象初始化新对象是(有动态资源时需深拷贝)
赋值运算符重载两个已存在对象的拷贝是(有动态资源时需深拷贝)
普通取地址重载获取对象地址否(编译器生成足够用)
const 取地址重载获取 const 对象地址否(编译器生成足够用)

学习默认成员函数的两个核心问题

  1. 编译器自动生成的函数行为是什么?是否满足需求?
  2. 当自动生成的函数不满足需求时,如何正确手动实现?

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

构造函数的核心作用是初始化对象,而非开辟内存空间(局部对象的内存在栈帧创建时已分配)。它替代了 C 语言中手动调用Init函数的繁琐操作,在对象实例化时自动执行。

2.1 构造函数的 7 个关键特性

  1. 函数名与类名相同:如Date类的构造函数名为Date,无返回值(无需写void)。
  2. 自动调用:对象实例化时编译器自动调用,无需手动触发。
  3. 支持重载:可定义无参、带参、全缺省等多种构造函数,满足不同初始化场景。
  4. 默认构造函数的唯一性:无参构造、全缺省构造、编译器自动生成的构造,统称为 "默认构造函数",但三者不能同时存在(调用时会产生歧义)。
    • 例:若同时定义Date()Date(int year=1, int month=1, int day=1)Date d1;会编译报错。
  5. 编译器自动生成的构造行为
    • 内置类型成员int、指针等):不做初始化,值为随机值(依赖编译器)。
    • 自定义类型成员(如Stack对象):自动调用该成员的默认构造函数。
  6. 无参构造的调用注意:通过无参构造创建对象时,对象后不能加括号,否则编译器会误认为是函数声明。
    • 错误:Date d3();(编译器认为是声明一个返回Date的无参函数)。
    • 正确:Date d3;

2.2 构造函数实例:Date 类与 Stack 类的实现

示例 1:Date 类的构造函数重载
#include<iostream>
using namespace std;

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

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

    // 3. 全缺省构造函数(与无参构造不能同时存在)
    /*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);  // 调用带参构造
    d1.Print();     // 输出:1/1/1
    d2.Print();     // 输出:2025/1/1
    return 0;
}
示例 2:自定义类型成员的构造调用

当类的成员是自定义类型时,编译器生成的构造会自动调用该成员的默认构造:

class Stack {
public:
    // Stack的全缺省构造
    Stack(int n = 4) {
        _a = (int*)malloc(sizeof(int) * n);
        if (_a == nullptr) {
            perror("malloc失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }
private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

class MyQueue {
private:
    Stack pushst;  // 自定义类型成员
    Stack popst;   // 自定义类型成员
};

int main() {
    MyQueue mq;  // 编译器生成MyQueue的构造,自动调用pushst和popst的默认构造
    return 0;
}

三、析构函数:对象的 "资源清理工"

析构函数的核心作用是清理对象的资源(如动态内存、文件句柄),而非销毁对象本身(局部对象的内存会随栈帧销毁释放)。它替代了 C 语言中手动调用Destroy函数的操作,在对象生命周期结束时自动执行。

3.1 析构函数的 8 个关键特性

  1. 函数名格式:类名前加~,如~Date(),无参数、无返回值。
  2. 唯一性:一个类只能有一个析构函数,不支持重载。
  3. 自动调用:对象生命周期结束时(如局部对象出作用域),编译器自动调用。
  4. 编译器自动生成的析构行为
    • 内置类型成员:不做处理(无需清理)。
    • 自定义类型成员:自动调用该成员的析构函数。
  5. 手动实现的场景:当类中存在动态资源申请(如mallocnew)时,必须手动实现析构函数释放资源,否则会导致内存泄漏。
    • 例:Stack类的_a是动态内存,需在析构中free(_a)Date类无动态资源,可直接用编译器生成的析构。
  6. 析构顺序:局部域中多个对象,后定义的先析构(与栈的 "后进先出" 一致)。

3.2 析构函数实例:Stack 类的资源释放

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack {
public:
    // 构造:申请动态内存
    Stack(int n = 4) {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (_a == nullptr) {
            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;
};

int main() {
    Stack st;  // 构造调用:申请内存
    // 函数结束时,st生命周期结束,自动调用析构:释放内存
    return 0;
}

对比 C 与 C++ 的资源管理:C 语言实现Stack时,需手动调用STInitSTDestroy,若遗漏STDestroy会导致内存泄漏;C++ 通过构造和析构的自动调用,彻底避免了这一问题。

四、拷贝构造函数:用已有对象 "复制" 新对象

拷贝构造函数是一种特殊的构造函数,用于用同类已存在的对象初始化新对象(如Date d2(d1);)。它的核心是解决 "如何拷贝对象成员" 的问题,尤其是动态资源的拷贝。

4.1 拷贝构造函数的 6 个关键特性

  1. 特殊的构造重载:第一个参数必须是自身类类型的引用const Date& d),若用传值会引发无穷递归(传值传参需拷贝,拷贝又需传值,循环往复)。
  2. 参数要求:第一个参数为引用,后续参数可带默认值(极少用)。
  3. 自动调用场景
    • 用对象初始化新对象(Date d2(d1);Date d2 = d1;)。
    • 自定义类型对象传值传参(如void Func(Date d))。
    • 自定义类型对象传值返回(如Date Func())。
  4. 编译器自动生成的拷贝行为
    • 内置类型成员:值拷贝(浅拷贝,逐字节复制)。
    • 自定义类型成员:调用该成员的拷贝构造。
  5. 手动实现的场景:当类中存在动态资源时,浅拷贝会导致多个对象指向同一块内存,析构时重复释放(程序崩溃),需手动实现深拷贝。
    • 例:Stack类的_a是动态内存,浅拷贝会让st1._ast2._a指向同一块内存,析构时free两次,导致崩溃。
  6. 传值返回与引用返回
    • 传值返回:生成临时对象,调用拷贝构造(效率低)。
    • 引用返回:返回对象别名,无拷贝(效率高),但需确保返回对象生命周期长于函数(如静态对象、成员对象),否则会产生野引用。

4.2 拷贝构造实例:深拷贝 vs 浅拷贝

问题场景:浅拷贝导致的崩溃

class Stack {
public:
    Stack(int n = 4) { /* 构造:申请内存 */ }
    ~Stack() { free(_a); } // 析构:释放内存
    // 未手动实现拷贝构造,编译器生成浅拷贝
private:
    int* _a;
    size_t _capacity;
    size_t _top;
};

int main() {
    Stack st1;
    Stack st2 = st1; // 浅拷贝:st2._a = st1._a
    // 析构时:先析构st2,free(st2._a);再析构st1,free(st1._a)(重复释放,崩溃)
    return 0;
}
解决:手动实现深拷贝
class Stack {
public:
    // 构造
    Stack(int n = 4) {
        _a = (int*)malloc(sizeof(int) * n);
        if (_a == nullptr) { perror("malloc失败"); return; }
        _capacity = n;
        _top = 0;
    }

    // 深拷贝构造:为新对象申请独立内存
    Stack(const Stack& st) {
        _a = (int*)malloc(sizeof(int) * st._capacity);
        if (_a == nullptr) { perror("malloc失败"); return; }
        memcpy(_a, st._a, sizeof(int) * st._top); // 拷贝数据
        _top = st._top;
        _capacity = st._capacity;
    }

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

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

int main() {
    Stack st1;
    Stack st2 = st1; // 深拷贝:st2._a指向新内存,析构无重复释放
    return 0;
}

五、赋值运算符重载:两个已存在对象的 "拷贝"

赋值运算符重载用于两个已存在对象之间的拷贝(如d1 = d2;),需与拷贝构造区分:拷贝构造是 "用旧对象初始化新对象",赋值重载是 "两个对象都已存在,将一个的值赋给另一个"。

5.1 运算符重载基础

C++ 允许通过operator+运算符的形式,自定义运算符对类对象的行为。需注意:

        运算符重载函数的参数个数 = 运算符的运算对象个数(一元运算符 1 个参数,二元运算符 2 个参数)。

        若为成员函数,第一个参数默认是this指针(隐含),因此参数个数比运算对象少 1。

        不可重载的运算符:.*::sizeof?:.(5 个,选择题高频考点)。

        重载运算符至少有一个类类型参数(不能改变内置类型行为,如int operator+(int a, int b)是非法的)。

5.2 赋值运算符重载的 5 个关键特性

  1. 必须为成员函数:C++ 规定赋值运算符重载只能是类的成员函数,不能是全局函数(否则编译器会生成默认版本,导致冲突)。
  2. 参数与返回值
    • 参数:建议用const 类类型&(避免传值拷贝,const确保不修改源对象)。
    • 返回值:建议用类类型&(引用返回,支持连续赋值如d1 = d2 = d3;,且避免拷贝)。
  3. 自我赋值检查:需判断this != &d(若d1 = d1;,直接赋值会导致资源泄漏,尤其是深拷贝场景)。
  4. 编译器自动生成的赋值行为:与拷贝构造一致,内置类型浅拷贝,自定义类型调用其赋值重载。
  5. 手动实现的场景:与拷贝构造相同,类中有动态资源时,需手动实现深拷贝,避免重复释放。

5.3 赋值运算符重载实例

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

    // 赋值运算符重载:深拷贝(Date无动态资源,此处为演示)
    Date& operator=(const Date& d) {
        // 自我赋值检查
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this; // 返回当前对象,支持连续赋值
    }

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

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

int main() {
    Date d1(2024, 7, 5);
    Date d2(2024, 7, 6);
    d1 = d2; // 调用赋值重载:d1的值变为d2的值
    d1.Print(); // 输出:2024-7-6

    Date d3;
    d3 = d1 = d2; // 连续赋值:d1 = d2,d3 = d1
    d3.Print(); // 输出:2024-7-6
    return 0;
}

六、取地址运算符重载:编译器生成即够用

取地址运算符重载分为两种:

  1. 普通取地址重载Date* operator&(),返回普通对象的地址。
  2. const 取地址重载const Date* operator&() const,返回 const 对象的地址。

核心结论:编译器自动生成的取地址重载已能满足绝大多数需求(返回this指针),无需手动实现。仅在特殊场景(如禁止外部获取对象地址)下,可手动重写并返回nullptr或其他地址。

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

七、总结:默认成员函数的核心决策指南

学习默认成员函数的关键,在于判断 "何时用编译器生成,何时手动实现"。以下是核心决策指南:

函数类型编译器生成行为手动实现场景
构造函数内置类型随机值,自定义类型调用其默认构造需初始化内置类型(如Date的年月日)
析构函数内置类型不处理,自定义类型调用其析构类中有动态资源(malloc/new
拷贝构造内置类型浅拷贝,自定义类型调用其拷贝构造类中有动态资源(需深拷贝)
赋值重载内置类型浅拷贝,自定义类型调用其赋值重载类中有动态资源(需深拷贝)

记住一个小技巧:若类手动实现了析构函数(释放动态资源),则必须手动实现拷贝构造和赋值重载,否则会因浅拷贝导致资源泄漏或程序崩溃。

掌握默认成员函数,是 C++ 面向对象编程的第一步,也是写出高效、健壮代码的基础。建议结合本文实例动手实践,深入理解浅拷贝与深拷贝的差异,以及构造 / 析构的调用时机,为后续学习继承、多态打下坚实基础。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值