C++ 类和对象中篇:默认成员函数、拷贝控制与运算符重载

C++ 类和对象中篇:默认成员函数、拷贝控制与运算符重载

写 C++ 类时,最容易被忽略的一类函数,往往不是普通成员函数,而是编译器会在背后帮我们生成的那些函数。

一个 Date 类只有 int 成员,看起来怎么拷贝都没问题;一个 Stack 类也只是几个成员变量,但只要里面有一块动态申请的空间,默认拷贝就可能引发二次释放。类和对象真正开始变复杂,通常就是从这些"默认成员函数"开始的。

这篇文章围绕 C++ 类的默认成员函数展开,重点讲清楚构造函数、析构函数、拷贝构造函数、赋值运算符重载、普通运算符重载、const 成员函数以及取地址运算符重载。

一、什么是类的默认成员函数

默认成员函数指的是:当我们没有显式实现时,编译器可能自动生成的特殊成员函数。

一个类通常会涉及 6 个默认成员函数:
#mermaid-svg-hqIuKj0w8nEgaD0Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hqIuKj0w8nEgaD0Y .error-icon{fill:#552222;}#mermaid-svg-hqIuKj0w8nEgaD0Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hqIuKj0w8nEgaD0Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .marker.cross{stroke:#333333;}#mermaid-svg-hqIuKj0w8nEgaD0Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hqIuKj0w8nEgaD0Y p{margin:0;}#mermaid-svg-hqIuKj0w8nEgaD0Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .cluster-label text{fill:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .cluster-label span{color:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .cluster-label span p{background-color:transparent;}#mermaid-svg-hqIuKj0w8nEgaD0Y .label text,#mermaid-svg-hqIuKj0w8nEgaD0Y span{fill:#333;color:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .node rect,#mermaid-svg-hqIuKj0w8nEgaD0Y .node circle,#mermaid-svg-hqIuKj0w8nEgaD0Y .node ellipse,#mermaid-svg-hqIuKj0w8nEgaD0Y .node polygon,#mermaid-svg-hqIuKj0w8nEgaD0Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .rough-node .label text,#mermaid-svg-hqIuKj0w8nEgaD0Y .node .label text,#mermaid-svg-hqIuKj0w8nEgaD0Y .image-shape .label,#mermaid-svg-hqIuKj0w8nEgaD0Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-hqIuKj0w8nEgaD0Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .rough-node .label,#mermaid-svg-hqIuKj0w8nEgaD0Y .node .label,#mermaid-svg-hqIuKj0w8nEgaD0Y .image-shape .label,#mermaid-svg-hqIuKj0w8nEgaD0Y .icon-shape .label{text-align:center;}#mermaid-svg-hqIuKj0w8nEgaD0Y .node.clickable{cursor:pointer;}#mermaid-svg-hqIuKj0w8nEgaD0Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .arrowheadPath{fill:#333333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hqIuKj0w8nEgaD0Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hqIuKj0w8nEgaD0Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hqIuKj0w8nEgaD0Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hqIuKj0w8nEgaD0Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .cluster text{fill:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y .cluster span{color:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hqIuKj0w8nEgaD0Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hqIuKj0w8nEgaD0Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-hqIuKj0w8nEgaD0Y .icon-shape,#mermaid-svg-hqIuKj0w8nEgaD0Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hqIuKj0w8nEgaD0Y .icon-shape p,#mermaid-svg-hqIuKj0w8nEgaD0Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hqIuKj0w8nEgaD0Y .icon-shape .label rect,#mermaid-svg-hqIuKj0w8nEgaD0Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hqIuKj0w8nEgaD0Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hqIuKj0w8nEgaD0Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hqIuKj0w8nEgaD0Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 默认成员函数
初始化与清理
拷贝控制
取地址重载
构造函数
析构函数
拷贝构造函数
赋值运算符重载
普通对象取地址
const 对象取地址

这 6 个函数并不是都需要我们亲自写。判断的关键不在于"能不能自动生成",而在于自动生成的行为是否符合类的语义

默认成员函数 主要作用 什么时候需要自己写
构造函数 对象创建时初始化成员 需要明确初始化规则时
析构函数 对象销毁时清理资源 类中申请了堆空间、文件句柄等资源时
拷贝构造函数 用已有对象初始化新对象 默认浅拷贝不够用时
赋值运算符重载 两个已存在对象之间赋值 默认浅拷贝不够用时
普通取地址重载 对普通对象取地址 极少需要手写
const 取地址重载 对 const 对象取地址 极少需要手写

从经验上看,如果一个类显式写了析构函数,并且析构函数里释放了资源,那么拷贝构造函数和赋值运算符重载也要重点检查。资源类最怕默认浅拷贝,表面上能编译,运行时才出问题。

二、构造函数:对象创建时自动初始化

构造函数是特殊的成员函数。它的名字和类名相同,没有返回值,对象实例化时会被自动调用。

构造函数容易被误解成"创建对象的函数"。更准确地说,对象空间的开辟通常已经由运行环境完成,构造函数负责的是初始化对象内部状态 。以前写 C 风格结构体时,我们经常手动调用 Init 函数;到了 C++,这件事应该交给构造函数。

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

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

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

int main() {
    Date d1;
    Date d2(2026, 7, 9);

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

    return 0;
}

编译运行:

bash 复制代码
g++ -std=c++11 date_demo.cpp -o date_demo
./date_demo

运行结果:

text 复制代码
1-1-1
2026-7-9

这里的 Date(int year = 1, int month = 1, int day = 1) 是一个全缺省构造函数,不传参也能调用,所以它也是默认构造函数。

默认构造函数并不只指"编译器自动生成的无参构造"。只要创建对象时不需要传实参,就可以称为默认构造函数。常见形式有三种:

形式 示例 特点
无参构造函数 Date() 调用时不传参
全缺省构造函数 Date(int year = 1, int month = 1, int day = 1) 所有参数都有默认值
编译器生成的构造函数 类中没有显式构造函数 对自定义类型成员会调用其默认构造

这里有两个坑很常见。

一个类中不能同时保留无参构造函数和全缺省构造函数,因为 Date d; 到底调哪个会产生歧义。

另一个是 Date d3(); 并不是创建对象,而是声明了一个返回 Date、参数为空的函数。创建无参对象应写成:

cpp 复制代码
Date d3;

编译器自动生成的构造函数还有一个细节:对 intchar、指针这类内置类型成员,不保证完成有意义的初始化;对自定义类型成员,会尝试调用这个成员自己的默认构造函数。如果成员类型没有默认构造函数,就需要使用初始化列表来解决,这属于后面更细的对象初始化问题。

三、析构函数:对象销毁时自动清理

析构函数和构造函数刚好相反。构造函数负责初始化,析构函数负责对象生命周期结束时的资源清理。

析构函数的写法是在类名前加 ~

cpp 复制代码
~Stack()
{
    // 清理资源
}

它没有参数,没有返回值,一个类只能有一个析构函数。对象生命周期结束时,析构函数会自动调用。

对于 Date 这种只包含普通整型成员的类,不写析构函数通常没有问题。对于 Stack 这种内部申请堆空间的类,不写析构函数就会泄漏内存。

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

typedef int STDataType;

class Stack {
public:
    Stack(size_t capacity = 4) {
        _array = static_cast<STDataType*>(malloc(sizeof(STDataType) * capacity));
        if (_array == nullptr) {
            perror("malloc failed");
            _capacity = 0;
            _top = 0;
            return;
        }

        _capacity = capacity;
        _top = 0;
    }

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

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

析构函数不负责销毁对象本身。比如局部对象在栈帧上,函数结束后栈帧释放,对象空间自然消失。析构函数要做的是把对象内部管理的额外资源释放掉。

如果一个类里有自定义类型成员,即使我们显式写了析构函数,这些成员对象的析构函数仍然会自动调用。多个局部对象在同一个作用域内创建时,析构顺序与定义顺序相反,后定义的先析构。

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

构造函数和析构函数的一个直接收益是:少写手动初始化和手动销毁代码。以前写 C 风格栈时,常见写法是:

cpp 复制代码
ST st;
STInit(&st);
// 使用 st
STDestroy(&st);

一旦中途有多个 return 分支,就很容易忘记释放资源。C++ 写法可以把这件事交给对象生命周期管理。

下面用栈判断括号匹配。为了突出构造和析构,这里只看 IsValid 的核心逻辑,假设 Stack 已经提供 PushPopTopEmpty 这几个接口。Stack 会在进入函数时构造,在函数退出时自动析构。

cpp 复制代码
bool IsValid(const char* str) {
    Stack st;

    while (*str != '\0') {
        if (*str == '(' || *str == '[' || *str == '{') {
            st.Push(*str);
        } else {
            if (st.Empty()) {
                return false;
            }

            char top = static_cast<char>(st.Top());
            st.Pop();

            if ((*str == ')' && top != '(')
                || (*str == ']' && top != '[')
                || (*str == '}' && top != '{')) {
                return false;
            }
        }

        ++str;
    }

    return st.Empty();
}

这段代码的关键不是括号匹配算法本身,而是资源管理方式。函数里不再出现 InitDestroy,提前返回也不会跳过析构函数。对象生命周期结束时,清理动作自动发生。

五、拷贝构造函数:用已有对象初始化新对象

拷贝构造函数也是构造函数的一种重载。它的典型写法如下:

cpp 复制代码
Date(const Date& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

参数必须使用当前类类型的引用,通常还会加上 const。如果写成传值:

cpp 复制代码
Date(Date d)

逻辑上会出问题。因为传值本身就要拷贝对象,拷贝对象又要调用拷贝构造函数,于是会形成递归调用,编译器会直接报错。

拷贝构造发生在"用已有对象创建新对象"时:

cpp 复制代码
Date d1(2026, 7, 9);

Date d2(d1);   // 拷贝构造
Date d3 = d1;  // 也是拷贝构造

很多初学者容易把下面两句混在一起:

cpp 复制代码
Date d4 = d1;  // 创建 d4,并用 d1 初始化,调用拷贝构造
d4 = d1;       // d4 已经存在,调用赋值运算符重载

默认拷贝构造对内置类型成员做值拷贝。对于 Date 这种类,默认行为够用。但对于 Stack 这种管理堆空间的类,默认浅拷贝会出大问题。

假设 st1 中的 _array 指向一块堆空间,默认拷贝后:

cpp 复制代码
Stack st2 = st1;

st1._arrayst2._array 会指向同一块空间。两个对象析构时都会 free 这块空间,轻则崩溃,重则出现更隐蔽的内存错误。

资源类应该实现深拷贝:

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

typedef int STDataType;

class Stack {
public:
    Stack(size_t capacity = 4) {
        _array = static_cast<STDataType*>(malloc(sizeof(STDataType) * capacity));
        if (_array == nullptr) {
            perror("malloc failed");
            _capacity = 0;
            _top = 0;
            return;
        }

        _capacity = capacity;
        _top = 0;
    }

    Stack(const Stack& st) {
        _array = static_cast<STDataType*>(malloc(sizeof(STDataType) * st._capacity));
        if (_array == nullptr) {
            perror("malloc failed");
            _capacity = 0;
            _top = 0;
            return;
        }

        memcpy(_array, st._array, sizeof(STDataType) * st._top);
        _capacity = st._capacity;
        _top = st._top;
    }

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

    void Push(STDataType x) {
        if (_top == _capacity) {
            size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
            STDataType* tmp = static_cast<STDataType*>(
                realloc(_array, sizeof(STDataType) * newCapacity)
            );
            if (tmp == nullptr) {
                perror("realloc failed");
                return;
            }

            _array = tmp;
            _capacity = newCapacity;
        }

        _array[_top++] = x;
    }

    void Pop() {
        assert(_top > 0);
        --_top;
    }

    STDataType Top() const {
        assert(_top > 0);
        return _array[_top - 1];
    }

    bool Empty() const {
        return _top == 0;
    }

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

这段代码的核心在 Stack(const Stack& st):新对象重新申请一块空间,再把旧对象中的有效元素拷贝过去。这样两个栈对象各自管理自己的空间,析构时互不影响。

还有一个容易踩坑的点:传值返回会产生拷贝,而引用返回不会产生拷贝。但引用返回有前提,返回的对象在函数结束后必须仍然存在。

cpp 复制代码
Date& BadFunc() {
    Date tmp(2026, 7, 9);
    return tmp; // 错误:tmp 是局部对象,函数结束后就销毁
}

这种代码返回的是一个失效引用,后续使用结果不可预期。

六、运算符重载:让类对象也能使用运算符

当运算符作用于类类型对象时,C++ 允许我们通过 operator 函数重新定义它的含义。

比如日期对象可以比较大小:

cpp 复制代码
bool operator==(const Date& d) const {
    return _year == d._year
        && _month == d._month
        && _day == d._day;
}

使用时可以写成:

cpp 复制代码
if (d1 == d2) {
    cout << "same date" << endl;
}

编译器会把它转换成类似下面的函数调用:

cpp 复制代码
d1.operator==(d2);

运算符重载有几条硬规则:

规则 说明
不能创造新运算符 不能写 operator@
不能改变操作数个数 二元运算符仍然需要两个操作数
不能改变优先级和结合性 重载后的 + 优先级仍然和内置 + 一样
至少有一个类类型参数 不能重载 int + int 的含义
部分运算符不能重载 .*::sizeof?:. 不能重载

++ 比较特殊,因为它有前置和后置两种形式。C++ 用一个额外的 int 形参区分后置版本:

cpp 复制代码
Date& operator++();    // ++d
Date operator++(int);  // d++

前置 ++ 修改对象后返回自身引用;后置 ++ 需要保留修改前的临时对象,所以通常返回值。

<<>> 通常重载为全局函数,而不是成员函数。原因很简单:cout << d 的左操作数是 cout,如果写成 Date 的成员函数,调用形式会变成 d << cout,不符合使用习惯。

cpp 复制代码
ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "年" << d._month << "月" << d._day << "日";
    return out;
}

如果这个函数要访问 Date 的私有成员,可以把它声明为友元函数。

七、赋值运算符重载:两个已有对象之间的拷贝

赋值运算符重载用于处理这种场景:

cpp 复制代码
Date d1(2026, 7, 9);
Date d2(2026, 8, 1);

d1 = d2; // d1 和 d2 都已经存在

它和拷贝构造的区别一定要分清:

写法 含义 调用函数
Date d2(d1); 创建新对象并初始化 拷贝构造函数
Date d3 = d1; 创建新对象并初始化 拷贝构造函数
d1 = d2; 已有对象之间赋值 赋值运算符重载

赋值运算符重载必须写成成员函数,推荐形式如下:

cpp 复制代码
Date& operator=(const Date& d) {
    if (this != &d) {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    return *this;
}

这里有三个关键点。

参数使用 const Date&,避免传值导致不必要的拷贝;返回值使用 Date&,是为了支持连续赋值:

cpp 复制代码
d1 = d2 = d3;

返回 *this,表示赋值表达式的结果是左操作数本身。

对资源类来说,赋值运算符同样要做深拷贝。下面是在 Stack 中补上的版本:

cpp 复制代码
Stack& operator=(const Stack& st) {
    if (this == &st) {
        return *this;
    }

    STDataType* tmp = static_cast<STDataType*>(
        malloc(sizeof(STDataType) * st._capacity)
    );
    if (tmp == nullptr) {
        perror("malloc failed");
        return *this;
    }

    memcpy(tmp, st._array, sizeof(STDataType) * st._top);

    free(_array);
    _array = tmp;
    _capacity = st._capacity;
    _top = st._top;

    return *this;
}

这个实现先申请新空间,成功后再释放旧空间,可以避免申请失败时把原对象破坏掉。this == &st 用来处理自己给自己赋值的情况。

八、日期类中的运算符设计思路

日期类很适合练习运算符重载,因为它天然支持比较、加减天数、前后置自增、自减、日期相减等操作。

一个简化版接口可以这样设计:

cpp 复制代码
class Date {
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);

public:
    Date(int year = 1900, int month = 1, int day = 1);
    void Print() const;

    bool operator<(const Date& d) const;
    bool operator<=(const Date& d) const;
    bool operator>(const Date& d) const;
    bool operator>=(const Date& d) const;
    bool operator==(const Date& d) const;
    bool operator!=(const Date& d) const;

    Date& operator+=(int day);
    Date operator+(int day) const;

    Date& operator-=(int day);
    Date operator-(int day) const;

    int operator-(const Date& d) const;

    Date& operator++();
    Date operator++(int);

    Date& operator--();
    Date operator--(int);

private:
    int GetMonthDay(int year, int month) const;
    bool CheckDate() const;

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

比较运算符可以复用,没必要每个都从头写一遍:

cpp 复制代码
bool Date::operator<(const Date& d) const {
    if (_year != d._year) {
        return _year < d._year;
    }

    if (_month != d._month) {
        return _month < d._month;
    }

    return _day < d._day;
}

bool Date::operator==(const Date& d) const {
    return _year == d._year
        && _month == d._month
        && _day == d._day;
}

bool Date::operator<=(const Date& d) const {
    return *this < d || *this == d;
}

bool Date::operator>(const Date& d) const {
    return !(*this <= d);
}

+= 负责真正修改对象,+ 只需要创建临时对象并复用 +=

cpp 复制代码
Date& Date::operator+=(int day) {
    if (day < 0) {
        return *this -= -day;
    }

    _day += day;
    while (_day > GetMonthDay(_year, _month)) {
        _day -= GetMonthDay(_year, _month);
        ++_month;

        if (_month == 13) {
            ++_year;
            _month = 1;
        }
    }

    return *this;
}

Date Date::operator+(int day) const {
    Date tmp = *this;
    tmp += day;
    return tmp;
}

前置 ++ 和后置 ++ 也可以复用已有逻辑:

cpp 复制代码
Date& Date::operator++() {
    *this += 1;
    return *this;
}

Date Date::operator++(int) {
    Date tmp = *this;
    *this += 1;
    return tmp;
}

这种写法的好处是修改日期的核心逻辑集中在 +=-= 中,其他运算符只做薄封装。后续如果日期进位逻辑要调整,维护成本会低很多。

九、const 成员函数:限制 this 指针的权限

成员函数后面加 const,称为 const 成员函数:

cpp 复制代码
void Print() const {
    cout << _year << "-" << _month << "-" << _day << endl;
}

它修饰的不是返回值,而是隐藏的 this 指针。

普通成员函数中的 this 可以理解为:

cpp 复制代码
Date* const this

const 成员函数中的 this 可以理解为:

cpp 复制代码
const Date* const this

这意味着:在 const 成员函数内部,不能修改成员变量。

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

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

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

int main() {
    Date d1(2026, 7, 9);
    d1.Print();

    const Date d2(2026, 8, 1);
    d2.Print();

    return 0;
}

const 对象可以调用 const 成员函数,因为这是权限缩小;const 对象不能调用普通成员函数,因为那可能修改对象。

所以像 Print、比较运算符、operator+ 这类不修改对象的函数,建议都加上 const

十、取地址运算符重载:了解即可

取地址运算符重载分为普通版本和 const 版本:

cpp 复制代码
class Date {
public:
    Date* operator&() {
        return this;
    }

    const Date* operator&() const {
        return this;
    }

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

大多数情况下,编译器自动生成的版本完全够用,不需要手写。只有在很少见的场景下,比如不想让外部拿到对象真实地址,才可能自己实现。不过日常写业务类、数据结构类时,很少会动这两个函数。

十一、常见问题与易错点

Date d(); 为什么不是对象?

这是函数声明。创建无参对象应写 Date d;

什么时候需要自己写析构函数?

类中管理了需要手动释放的资源时需要写。比如 malloc 申请的空间、打开的文件、系统句柄等。只包含普通内置类型成员时,默认析构通常就够了。

为什么拷贝构造参数要写成引用?

传值会触发拷贝,拷贝又需要调用拷贝构造,逻辑上会递归。写成 const T& 可以避免这个问题。

浅拷贝为什么危险?

浅拷贝只复制指针值,不复制指针指向的资源。两个对象指向同一块空间后,析构时可能二次释放,修改其中一个对象也可能影响另一个对象。

赋值运算符重载为什么返回引用?

为了减少拷贝,并支持连续赋值。d1 = d2 = d3; 中,d2 = d3 的结果应该还能继续赋给 d1

为什么 <<>> 常写成全局函数?

因为左操作数分别是 ostreamistream。写成全局函数后,调用形式才能保持 cout << dcin >> d 这样的习惯写法。

哪些运算符不能重载?

.*::sizeof?:. 不能重载。也不能创造 C++ 中不存在的运算符。

const 对象为什么只能调用 const 成员函数?

const 对象不允许被修改,普通成员函数没有承诺"不修改对象"。只有函数后面带 const,编译器才能确认调用是安全的。

十二、总结

类的默认成员函数是 C++ 对象模型里绕不开的一块。构造函数和析构函数解决对象初始化、清理问题;拷贝构造和赋值运算符重载决定对象如何复制;运算符重载让类对象用起来更接近内置类型;const 成员函数则把"能不能修改对象"这件事交给编译器检查。

写普通数据类时,很多默认行为已经够用。写资源管理类时,默认浅拷贝往往不够安全,需要自己实现深拷贝。判断一个类是否要手写这些函数,可以抓住一个问题:这个对象是否独占或管理某种资源?如果答案是肯定的,构造、析构、拷贝构造和赋值运算符重载就要放在一起考虑。

相关推荐
ctrl_v助手1 小时前
C#表达题笔记
开发语言·c#
不会调制解调的猫2 小时前
笔记 | 什么是 rp_filter(反向路径过滤)?
服务器·网络·笔记
汉克老师2 小时前
GESP2026年6月认证C++八级( 第二部分判断题(1-10))精讲
c++·归并排序·dijkstra·二分·互斥·gesp8级·可重复组合
2zcode2 小时前
基于MATLAB卷积神经网络的口罩佩戴检测系统
开发语言·matlab·cnn
米饭不加菜3 小时前
使用万用表判断三极管(BJT)好坏
java·开发语言
绝世唐门三哥3 小时前
vue3中页面返回时刷新首页的处理方案
开发语言·前端·javascript
nianniannnn3 小时前
c++复习自存--流、异常处理、多线程与C++工程规范
开发语言·c++
geovindu3 小时前
CSharp: Dijkstra Algorithms
开发语言·后端·算法·c#
XR1234567883 小时前
食品饮料与制药行业GMP合规网络建设:无尘车间的网络不能成为污染源
开发语言·网络·php
小猿备忘录3 小时前
JVM 调优完全手册(Java 8 版)
java·开发语言·jvm