C++3(类与对象中篇)

作为 C++ 初学者,类与对象是从 "面向过程" 转向 "面向对象" 的第一道坎,而默认成员函数又是类与对象的核心骨架。这篇文章用萌新能看懂的语言,拆解 6 个默认成员函数的底层逻辑、使用坑点和实战技巧,帮你彻底搞懂 "类到底是怎么工作的"!

📚目录
1.【开胃小菜】空类真的 "空" 吗?------6 个默认成员函数揭秘
2.【对象的出生证明】构造函数:给对象 "初始化" 的专属保姆
3.【对象的告别仪式】析构函数:帮对象 "清理垃圾" 的清洁工
4.【对象的克隆术】拷贝构造函数:如何复制一个一模一样的对象?
5.【对象的交换术】赋值运算符重载:给已有对象 "换新装"
6.【++ 的小秘密】前置 ++ vs 后置 ++:为啥参数多一个 int?
7.【对象的只读模式】const 成员函数:不让改的对象该怎么用?
8.【冷门但必考】取地址重载:几乎用不上但要知道的知识点
9.【实战大练兵】完整日期类:把所有知识点串起来10.【萌新避坑清单】核心知识点 + 常见错误总结

1. 【开胃小菜】空类真的 "空" 吗?------6 个默认成员函数揭秘

刚学 C++ 时,我一度以为写个空类class Date {};就真的啥都没有了,直到老师说 "编译器会偷偷给你加 6 个函数",才知道自己太天真了!

**核心结论:**如果一个类中没有显式定义任何成员,编译器会自动生成 6 个默认成员函数,这些函数是类对象能正常使用的基础,就像给空房子配了必备的家具。
初始化 / 清理组:构造函数(给对象 "装修")、析构函数(给对象 "收尾")
拷贝 / 赋值组:拷贝构造函数(复制新对象)、赋值运算符重载(给旧对象换新值)
取地址组:普通对象取地址、const 对象取地址(几乎用不上,但编译器会默认生成)

理解:你可以把类想象成 "汽车模板",对象是具体的 "一辆车"。默认成员函数就是工厂给汽车配的基础功能:造新车(构造)、报废车(析构)、复制新车(拷贝构造)、给车换零件(赋值)、查车牌(取地址)------ 哪怕你没说要,工厂都会默认给。

2. 【对象的出生证明】构造函数:给对象 "初始化" 的专属保姆

构造函数是我初学最容易搞混的函数:明明叫 "构造",却不是 "创建对象",而是 "初始化对象"!

2.1 核心特性(必记)
1.函数名和类名一模一样(比如Date()),没有返回值(连void都不用写,这点我总忘);
2.对象创建时自动调用,一生只调一次(就像人出生只办一次出生证明);
3.支持重载(可以写多个,比如无参、带参);
4.如果你没写,编译器会自动生成一个 "无参默认构造函数"------ 但这个默认的有坑!

2.2 最容易踩的坑
坑 1:默认构造函数对内置类型不初始化

编译器生成的默认构造函数,只会给自定义类型成员初始化(比如类里套了另一个类),但对int/char这些内置类型,直接留随机值

cpp 复制代码
class Date {
private:
    int _year; // 内置类型,默认构造后是随机值
};
int main() {
    Date d;
    // 打印_year会是一串乱码,新手容易以为"初始化了就有值"
    return 0;
}

解决办法:C++11 后,内置类型可以在类里直接给默认值(强烈推荐):

cpp 复制代码
class Date {
private:
    int _year = 1900; // 给默认值,解决随机值问题
    int _month = 1;
    int _day = 1;
};

坑 2:无参构造创建对象不能加括号

cpp 复制代码
Date d1; // ✅ 正确(我一开始总写成下面这样)
Date d1(); // ❌ 错误!编译器会以为你声明了一个返回Date的函数

坑 3:默认构造函数只能有一个

无参构造、全缺省构造、编译器默认构造,都叫 "默认构造函数",写多了会报错!推荐写全缺省构造函数(最实用):

cpp 复制代码
class Date {
public:
    // 全缺省构造:想初始化就传参,不想传就用默认值
    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
// 新手用法示例
int main() {
    Date d1; // 用默认值:1900-1-1
    Date d2(2024); // 只传年:2024-1-1
    Date d3(2024, 3); // 传年和月:2024-3-1
    Date d4(2024, 3, 10); // 传全部:2024-3-10
    return 0;
}

2.3 总结
构造函数的核心是 "初始化",不是 "创建"。新手优先写全缺省构造函数,避免踩内置类型未初始化的坑!

3. 【对象的告别仪式】析构函数:帮对象 "清理垃圾" 的清洁工

析构函数是我初学最容易忽略的函数,直到写栈(Stack)类出现内存泄漏,才知道它的重要性。

3.1 核心特性(白话版)
1.函数名是类名(比如Date()),无参数、无返回值;
2.对象生命周期结束时自动调用(比如函数执行完、main 函数结束);
3.一个类只能有一个析构函数,不能重载;
4.编译器生成的默认析构函数:只清理自定义类型,内置类型不管(和构造函数反过来)

3.2 什么时候需要手写析构函数?(关键)
核心原则:类里有没有申请 "堆内存 / 文件句柄 / 网络连接" 等资源?

✅ 不需要手写:Date 类(只有 int 成员,无资源申请);

❌ 必须手写:Stack 类(用 malloc 申请了堆内存,不释放会内存泄漏)。

cpp 复制代码
typedef int DataType;
class Stack {
public:
    // 构造函数:申请堆内存
    Stack(size_t capacity = 3) {
        _array = (DataType*)malloc(sizeof(DataType) * capacity);
        if (_array == NULL) {
            perror("malloc失败"); // 新手要加错误提示,方便调试
            exit(-1);
        }
        _capacity = capacity;
        _size = 0;
    }

    // 析构函数:释放堆内存(新手一定要写!)
    ~Stack() {
        if (_array) { // 先判断是否为空,避免重复释放
            free(_array); // 释放申请的内存
            _array = NULL; // 置空,避免野指针(新手必做)
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType* _array; // 堆内存指针
    int _capacity;
    int _size;
};

3.3 避坑点

1.别漏写析构函数 :堆内存申请了必须释放,否则程序运行久了会卡顿(内存泄漏 );

2.别重复释放:析构函数里先判断指针是否为空,避免程序崩溃

3.析构函数不销毁对象只是清理资源对象本身的内存由编译器回收

4. 【对象的克隆术】拷贝构造函数:如何复制一个一模一样的对象?

拷贝构造是我初学最难理解的函数,尤其是 "为什么必须传引用",直到老师画了递归图才明白。

4.1 核心特性(拆解)

是构造函数的重载,函数名和类名相同;

1.参数必须是类的 const 引用(const Date& d)------ 新手记死这条!

2.为什么加 const ?防止不小心改了被拷贝的对象;

3.为什么传引用?传值会触发 "无穷递归"(传值参数要拷贝,拷贝又要传值,无限循环);

4.没手写的话,编译器生成默认拷贝构造 ------ 做 "浅拷贝"(按字节复制)。

4.2 浅拷贝 vs 深拷贝(必懂)

浅拷贝(编译器默认):像 "复制文件快捷方式"
原理:按内存字节逐字复制,指针变量只复制地址,不复制指向的内容
适用场景:无资源申请的类(Date 类)

坑点:有资源申请的类(Stack 类)会出大问题!

踩坑示例:Stack 类浅拷贝的灾难

cpp 复制代码
int main() {
    Stack s1;
    Stack s2 = s1; // 浅拷贝:s1和s2的_array指向同一块堆内存
    // 函数结束时,s2先析构,释放了_array;s1再析构,重复释放同一块内存→程序崩溃!
    return 0;
}

深拷贝(必须手写):像 "复制文件本身"

原理:不仅复制指针,还会重新申请一块内存,把指针指向的内容也复制过去;

适用场景:有资源申请的类(Stack 类);

实战:Stack 类深拷贝构造函数

cpp 复制代码
// Stack类中添加拷贝构造函数
Stack(const Stack& s) {
    // 1. 重新申请内存(和s的容量一样)
    _array = (DataType*)malloc(sizeof(DataType) * s._capacity);
    if (_array == NULL) {
        perror("malloc失败");
        exit(-1);
    }
    // 2. 复制内容(不是复制地址!)
    memcpy(_array, s._array, sizeof(DataType) * s._size);
    // 3. 复制其他成员
    _size = s._size;
    _capacity = s._capacity;
}

4.3 拷贝构造的调用场景(易漏)

除了Date d2 = d1,这两种场景也会调用(笔试常考):

1.函数参数是类对象(值传递):void func(Date d) {};

2.函数返回值是类对象(值返回):Date func() { Date d; return d; };
优化建议:函数参数尽量用引用(const Date& d),避免不必要的拷贝!

5. 【对象的交换术】赋值运算符重载:给已有对象 "换新装"

赋值重载和拷贝构造很像,我初学总搞混 ------ 核心区别是:拷贝构造是 "创建新对象",赋值重载是 "给已有对象赋值"。
5.1 运算符重载基础(入门)

C++ 允许我们给运算符(+、=、== 等)重新定义规则,让类对象也能像 int 一样用运算符。

函数名格式:operator+运算符(比如operator=、operator==);

注意:5 个运算符不能重载(.*、::、sizeof、?:、.),记不住就先记 "赋值运算符只能重载为成员函数"。

5.2 赋值运算符重载的 "黄金格式"(必背)

cpp 复制代码
类名& operator=(const 类名& 变量名) {
    // 1. 检测自赋值(避免自己赋值给自己)这里需要重点记忆一下
    if (this != &变量名) {
        // 2. 清理当前对象的旧资源(有资源时写)
        // 3. 复制新值/申请新资源
        // 4. 返回*this(支持连续赋值)
    }
    return *this;
}

5.3 实战:Date 类赋值重载

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

    // 赋值运算符重载(黄金格式)
    Date& operator=(const Date& d) {
        // 1. 检测自赋值(新手容易忘,比如d1 = d1)
        if (this != &d) {
            // 2. 复制值(Date类无资源,直接赋值)
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        // 3. 返回*this,支持d1 = d2 = d3
        return *this;
    }
private:
    int _year, _month, _day;
};

// 新手用法示例
int main() {
    Date d1(2024, 3, 10);
    Date d2;
    d2 = d1; // 调用赋值重载,d2变成2024-3-10
    Date d3;
    d3 = d2 = d1; // 连续赋值,新手要知道为什么能行:返回的是引用
    return 0;
}

5.4 区分:拷贝构造 vs 赋值重载

6. 【++ 的小秘密】前置 ++ vs 后置 ++:为啥参数多一个 int?

初学 ++ 重载时,我最疑惑的是 "后置 ++ 为啥要加个没用的 int ",其实这是 C++ 的特殊规定 ------ 专门用来区分前置和后置

6.1 核心区别

6.2 实战:Date 类 ++ 重载

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

    // 前置++:先加后用,返回引用(新手记:无int参数)
    Date& operator++() {
        _day += 1; // 先自增
        return *this; // 返回自己,支持++(++d)
    }

    // 后置++:先用后加,返回值(新手记:有int参数)
    Date operator++(int) {
        Date temp(*this); // 先保存当前值(关键!)
        _day += 1; // 再自增
        return temp; // 返回旧值,临时对象不能用引用
    }

    // 新手调试用:打印日期
    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year, _month, _day;
};

// 新手测试示例
int main() {
    Date d(2024, 3, 10);
    Date d1 = ++d; // 前置++:d变成3-11,d1也是3-11
    d1.Print(); // 输出:2024-3-11

    Date d2(2024, 3, 10);
    Date d3 = d2++; // 后置++:d2变成3-11,d3是3-10
    d3.Print(); // 输出:2024-3-10
    d2.Print(); // 输出:2024-3-11
    return 0;
}

6.3 理解

后置 ++ 的 int 参数只是 "标记",编译器会自动传一个 0 进去,我们不用管它。新手记住:前置 ++ 无 int、返回引用;后置 ++ 有 int、返回值!

7. 【对象的只读模式】const 成员函数:不让改的对象该怎么用?

const 成员函数是我初学最容易出错的点 ------ 写了 const 对象却调不了成员函数,查了半天才知道是 const 的问题。
7.1 核心原理(白话版)
const 修饰成员函数,其实是给隐藏的this指针加 const:
普通成员函数:this是Date const(指针本身不能
改,指向的内容能改);
*
const 成员函数:this是const Date const(指针和指向的内容都不能改)。 *

理解:const 成员函数里,不能修改类的任何成员变量(相当于 "只读模式")。

7.2 调用规则(笔试高频,记死)

1.const 对象 ❌ 不能调用非 const 成员函数(只读对象不能改);

2.非 const 对象 ✅ 可以调用 const 成员函数(可写对象允许只读);

3.const 成员函数 ❌ 不能调用非 const 成员函数(只读函数里不能改对象);

4.非 const 成员函数 ✅ 可以调用 const 成员函数(可写函数里允许只读)

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

    // 非const成员函数(可以改成员变量)
    void SetDay(int day) {
        _day = day; // 能修改
    }

    // const成员函数(不能改成员变量)
    void Print() const {
        // _day = 10; // ❌ 错误!const函数里不能改成员变量
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year, _month, _day;
};

int main() {
    Date d1(2024, 3, 10); // 非const对象
    d1.SetDay(11); // ✅ 能调非const函数
    d1.Print();    // ✅ 能调const函数

    const Date d2(2024, 3, 10); // const对象
    // d2.SetDay(11); // ❌ 错误!const对象不能调非const函数
    d2.Print(); // ✅ 能调const函数
    return 0;
}

7.4 建议
写成员函数时,只要函数不修改成员变量,就加 const(比如 Print 函数)------ 这样 const 对象和非 const 对象都能调用,更通用!

8. 【冷门但必考】取地址重载:几乎用不上但要知道的知识点

取地址重载是 6 个默认成员函数里最冷门的,新手几乎用不到,但笔试偶尔会问,简单了解即可。

8.1 核心知识点(速记)

1.包括两个函数:普通对象取地址、const 对象取地址;

2.编译器默认生成的版本:直接返回this指针(对象的地址);

3.自定义场景:想隐藏对象的真实地址(比如单例模式),新手暂时不用管。

cpp 复制代码
class Date {
public:
    // 普通对象取地址重载
    Date* operator&() {
        return this; // 默认就是返回this
    }

    // const对象取地址重载
    const Date* operator&() const {
        return this; // 默认就是返回this
    }
private:
    int _year, _month, _day;
};

9. 【实战大练兵】完整日期类:把所有知识点串起来

学完单个知识点,新手一定要整合起来写完整类 ------ 这是检验是否真的掌握的关键!以下是适合新手的完整 Date 类声明(带详细注释):

cpp 复制代码
#include <iostream>
#include <cstdlib> // 用于exit
using namespace std;

class Date {
public:
    // 1. 获取某年某月的天数(新手工具函数)
    int GetMonthDay(int year, int month) {
        // 新手注意:数组下标从0开始,month从1开始,所以加个空的0月
        static int days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
        // 闰年2月特殊处理(新手要记闰年规则:能被4整除且不能被100整除,或能被400整除)
        if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
            return 29;
        }
        return days[month];
    }

    // 2. 全缺省构造函数(新手优先)
    Date(int year = 1900, int month = 1, int day = 1) {
        // 新手优化:加合法性检查,避免无效日期
        if (year < 1900 || month < 1 || month > 12 || day < 1 || day > GetMonthDay(year, month)) {
            cout << "日期非法:" << year << "-" << month << "-" << day << endl;
            exit(-1);
        }
        _year = year;
        _month = month;
        _day = day;
    }

    // 3. 拷贝构造函数(const+引用)
    Date(const Date& d) {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    // 4. 赋值运算符重载(黄金格式)
    Date& operator=(const Date& d) {
        if (this != &d) {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
        return *this;
    }

    // 5. 析构函数(Date类无资源,可省略,新手写出来更规范)
    ~Date() {}

    // 6. 日期+=天数(新手核心:处理跨月/跨年)
    Date& operator+=(int day) {
        if (day < 0) { // 处理负数,复用-=(新手偷懒技巧)
            return *this -= -day;
        }
        _day += day;
        // 新手逻辑:天数超过当月,就减当月天数,月份+1
        while (_day > GetMonthDay(_year, _month)) {
            _day -= GetMonthDay(_year, _month);
            _month++;
            // 月份超过12,年份+1,月份归1
            if (_month > 12) {
                _year++;
                _month = 1;
            }
        }
        return *this;
    }

    // 7. 日期+天数(复用+=,新手注意返回值)
    Date operator+(int day) {
        Date temp(*this); // 先拷贝当前对象
        temp += day;      // 复用+=,避免重复写逻辑
        return temp;      // 值返回,因为temp是局部对象
    }

    // 8. 前置++(无int参数)
    Date& operator++() {
        *this += 1; // 复用+=,新手少写代码
        return *this;
    }

    // 9. 后置++(有int参数)
    Date operator++(int) {
        Date temp(*this);
        *this += 1;
        return temp;
    }

    // 10. 关系运算符重载(==)
    bool operator==(const Date& d) const {
        return _year == d._year && _month == d._month && _day == d._day;
    }

    // 11. 关系运算符重载(>)
    bool operator>(const Date& d) const {
        if (_year > d._year) return true;
        else if (_year == d._year && _month > d._month) return true;
        else if (_year == d._year && _month == d._month && _day > d._day) return true;
        return false;
    }

    // 12. 复用==和>,实现其他关系运算符(新手偷懒技巧)
    bool operator>=(const Date& d) const {
        return *this > d || *this == d;
    }
    bool operator<(const Date& d) const {
        return !(*this >= d);
    }
    bool operator<=(const Date& d) const {
        return !(*this > d);
    }
    bool operator!=(const Date& d) const {
        return !(*this == d);
    }

    // 13. 打印函数(const修饰,新手好习惯)
    void Print() const {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

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

// 新手测试代码
int main() {
    Date d(2024, 2, 28);
    d += 3; // 2024是闰年,2月有29天,结果应该是2024-3-2
    d.Print(); // 输出:2024-3-2

    Date d2 = d + 10; // 2024-3-12
    d2.Print();

    Date d3 = ++d2; // 2024-3-13
    d3.Print();
    return 0;
}

10. 【萌新避坑清单】核心知识点 + 常见错误总结

10.1 核心知识点(必背)

1.空类不空:编译器自动生成 6 个默认成员函数;
2.构造函数:初始化对象,全缺省最实用,内置类型要给默认值;
3.析构函数:清理资源,有堆内存必须手写,避免内存泄漏;
4.拷贝构造:参数必须是 const 引用,有资源要写深拷贝;
5.赋值重载:黄金格式(自赋值检测 + 返回 * this),区分拷贝构造;
6.const 成员函数:不修改成员变量就加 const,const 对象只能调 const 函数

10.2 新手常见错误(我踩过的雷)

1.无参构造创建对象加括号(Date d(););
2.拷贝构造参数没加引用,导致无穷递归;
3.Stack 类漏写析构函数,导致内存泄漏;
4.后置 ++ 忘记保存旧值,或返回引用;
5.const 对象调用非 const 成员函数;
6.赋值重载忘记检测自赋值,导致资源重复释放。

相关推荐
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十三):空气质量传感器实战 ——KQM6600 模块从协议到代码(串口通信 + 数据解析)
c++·stm32·单片机·嵌入式硬件·架构·硬件架构·嵌入式实时数据库
2501_945423542 小时前
C++与Rust交互编程
开发语言·c++·算法
tankeven2 小时前
HJ131 数独数组
c++·算法
liuyao_xianhui2 小时前
优选算法_丢失的数字_位运算_C++
linux·数据结构·c++·算法·动态规划·哈希算法·散列表
code_whiter2 小时前
C++2(类与对象上篇)
开发语言·c++
m0_743297422 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
代码改善世界2 小时前
【C++ 初阶】命名空间 / 输入输出 / 缺省参数 / 函数重载
开发语言·c++
小小怪7502 小时前
高性能密码学库
开发语言·c++·算法
2301_821700532 小时前
模板代码生成工具
开发语言·c++·算法