【C++】类和对象

摘要 :类和对象把数据与操作数据的函数放进同一个类型,并用访问限定符控制外部能改什么。本文从 Date 结构体加一组全局函数写起,讲类的定义、访问限定符、类域、实例化与对象大小、this 指针;再用 Stack 和 Date 两个类串起构造、析构、拷贝构造、赋值重载,以及 Date 的比较和算术运算符;最后补初始化列表、explicit、static 成员、友元、内部类、匿名对象和拷贝省略。贯穿全篇的线索是对象什么时候构造、什么时候拷贝、什么时候赋值、什么时候析构。

文章目录

  • [1. 从结构体到类](#1. 从结构体到类)
    • [1.1 访问限定符与封装](#1.1 访问限定符与封装)
    • [1.2 类域](#1.2 类域)
    • [1.3 C++ 的 struct 也是类](#1.3 C++ 的 struct 也是类)
  • [2. 实例化与对象大小](#2. 实例化与对象大小)
    • [2.1 类不占空间,对象才占](#2.1 类不占空间,对象才占)
    • [2.2 对象里只存成员变量](#2.2 对象里只存成员变量)
    • [2.3 内存对齐与空类](#2.3 内存对齐与空类)
  • [3. this 指针](#3. this 指针)
    • [3.1 成员函数怎么知道操作哪个对象](#3.1 成员函数怎么知道操作哪个对象)
    • [3.2 空指针调用成员函数的两种结果](#3.2 空指针调用成员函数的两种结果)
  • [4. 封装:同一个 Stack 的两种写法](#4. 封装:同一个 Stack 的两种写法)
    • [4.1 C 的写法](#4.1 C 的写法)
    • [4.2 C++ 的写法](#4.2 C++ 的写法)
    • [4.3 两种写法对照](#4.3 两种写法对照)
  • [5. 构造函数与析构函数](#5. 构造函数与析构函数)
    • [5.1 六个默认成员函数](#5.1 六个默认成员函数)
    • [5.2 构造函数](#5.2 构造函数)
    • [5.3 析构函数](#5.3 析构函数)
  • [6. 拷贝构造与赋值重载](#6. 拷贝构造与赋值重载)
    • [6.1 拷贝构造函数](#6.1 拷贝构造函数)
    • [6.2 什么时候调用拷贝构造](#6.2 什么时候调用拷贝构造)
    • [6.3 赋值运算符重载](#6.3 赋值运算符重载)
  • [7. 用 Date 串起运算符重载](#7. 用 Date 串起运算符重载)
    • [7.1 const 成员函数](#7.1 const 成员函数)
    • [7.2 比较运算符](#7.2 比较运算符)
    • [7.3 日期加减与前后置 ++](#7.3 日期加减与前后置 ++)
    • [7.4 取地址运算符重载](#7.4 取地址运算符重载)
    • [7.5 完整实现与测试](#7.5 完整实现与测试)
  • [8. 初始化列表与 explicit](#8. 初始化列表与 explicit)
    • [8.1 初始化列表](#8.1 初始化列表)
    • [8.2 声明处缺省值与初始化顺序](#8.2 声明处缺省值与初始化顺序)
    • [8.3 explicit 与隐式类型转换](#8.3 explicit 与隐式类型转换)
  • [9. static 成员与友元](#9. static 成员与友元)
    • [9.1 静态成员](#9.1 静态成员)
    • [9.2 友元函数](#9.2 友元函数)
    • [9.3 友元类](#9.3 友元类)
  • [10. 内部类与匿名对象](#10. 内部类与匿名对象)
    • [10.1 内部类](#10.1 内部类)
    • [10.2 匿名对象](#10.2 匿名对象)
  • [11. 拷贝省略](#11. 拷贝省略)
  • [12. 小结](#12. 小结)

1. 从结构体到类

写一个日期结构体,再配几个函数,是 C 里常见的写法:

c 复制代码
#include <stdio.h>

typedef struct Date
{
    int year;
    int month;
    int day;
} Date;

void DateInit(Date* d, int year, int month, int day)
{
    d->year = year;
    d->month = month;
    d->day = day;
}

void DatePrint(const Date* d)
{
    printf("%d-%d-%d\n", d->year, d->month, d->day);
}

int main(void)
{
    Date d;
    DateInit(&d, 2024, 3, 31);
    DatePrint(&d);
    d.month = 13;
    DatePrint(&d);
    return 0;
}

输出:

text 复制代码
2024-3-31
2024-13-31

第二行是 2024-13-31:DateInit 挡不住后面的 d.month = 13。数据放在结构体里,函数放在外面,谁能改、什么时候改,只能靠调用者记住。

C++ 的类把两者放进同一个类型:

cpp 复制代码
#include <iostream>

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

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

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

int main()
{
    Date d;
    d.Init(2024, 3, 31);
    d.Print();
    return 0;
}

class 后面的 Date 是类名,{} 里是类体,类定义结束后的分号不能省。类体里的变量叫成员变量,函数叫成员函数。

成员变量名前的 _ 只是约定,用来和形参区分,不是语法要求;写成 year_ 或 m_year 也可以,跟着项目规范走。

定义在类体内的成员函数默认是内联的。Date 的两个函数也能放到类外定义,1.2 节会看到写法。

1.1 访问限定符与封装

public 成员在类外可以访问,private 和 protected 不行。访问权限从限定符出现的位置开始,到下一个限定符或类结束的 } 为止。

限定符 类内访问 类外访问 用途
public 可以 可以 对外接口
protected 可以 不可以 继承时才有区别
private 可以 不可以 成员变量默认放这里

protected 目前和 private 表现一样,继承时才有区别。class 的成员默认 private,struct 的成员默认 public。

封装(encapsulation)不是把成员藏起来就完事,而是让外部只能通过公开接口修改数据。Date 的 _month 是 private,外部写 d._month = 13 会编译报错,只能调用 Init。

访问控制本身不检查日期是否合法。它的作用是把修改集中到 Init 一个入口;要挡非法日期,检查逻辑写在这里即可。C 版本没有这个入口,任何拿到 Date* 的代码都能直接改字段。

1.2 类域

类定义了一个新的作用域,成员都在类域里。类外定义成员函数时必须写 Date:::

cpp 复制代码
#include <iostream>

class Date
{
public:
    void Init(int year, int month, int day);
    void Print();

private:
    int _year, _month, _day;
};

void Date::Init(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}

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

int main()
{
    Date d;
    d.Init(2024, 3, 31);
    d.Print();
    return 0;
}

如果写成 void Init(...) 而不带 Date::,编译器会把它当成全局函数,函数体里找不到 _year 等成员,直接报错。

指定 Date:: 后,编译器才知道这是成员函数;当前作用域找不到的名字会继续到类域里查找。类域只影响编译期的名字查找,不影响成员变量的生命周期。

1.3 C++ 的 struct 也是类

C++ 保留 C 的 struct,同时把它升级成类:可以在里面定义函数;struct 名本身就能当类型名,不再需要 typedef;C 的用法仍然兼容。

cpp 复制代码
struct ListNode
{
    void Init(int x)
    {
        val = x;
        next = nullptr;
    }

    ListNode* next;
    int val;
};

int main()
{
    ListNode node;
    node.Init(1);
    return 0;
}

C 版本通常写成 typedef struct ListNode { ... } LTNode;,用的时候写 LTNode;C++ 直接写 ListNode。struct 的成员默认 public,class 的成员默认 private,这是两者在成员访问上的默认差别。

定义类一般用 class;只是把几个字段聚合在一起、不需要访问控制时,可以用 struct。

2. 实例化与对象大小

类只是类型,像一张图纸。声明 Date 不会给成员变量分配空间;用类创建对象的过程叫实例化(instantiation)。

类中的成员变量只是声明,对象创建时才分配空间。每个对象有自己独立的一份成员变量;d1.Init(...) 不会改到 d2。

2.1 类不占空间,对象才占

Date d1; 和 Date d2; 各创建一个对象,d1 与 d2 的 _year、_month、_day 分别独立。

一个类可以实例化出很多对象。同一张图纸可以盖很多栋房子,每个对象的数据互不影响。

2.2 对象里只存成员变量

对象只存非静态成员变量。成员函数编译成一段指令,放在代码段,所有对象共用一份。

普通成员函数的地址在编译链接阶段就能确定,不需要每个对象存一份函数指针。sizeof(Date) 是 12(三个 int),成员函数不计入对象大小。

sizeof 是编译期运算,写 sizeof(Date) 或 sizeof d1 都不会在运行时遍历对象。

2.3 内存对齐与空类

对象大小遵循内存对齐(memory alignment)规则:第一个成员放在偏移 0;其他成员放到对齐数的整数倍;对象总大小是最大对齐数的整数倍。

对齐数取 min(编译器默认对齐数, 成员大小)。VS2026 默认对齐数是 8,int 是 4,char 是 1。

cpp 复制代码
#include <iostream>

class A
{
public:
    void Print() { std::cout << _ch << std::endl; }

private:
    char _ch;
    int _i;
};

class B
{
public:
    void Print() { std::cout << "B" << std::endl; }
};

class C {};

int main()
{
    std::cout << sizeof(A) << std::endl;
    std::cout << sizeof(B) << std::endl;
    std::cout << sizeof(C) << std::endl;
    return 0;
}

输出:

text 复制代码
8
1
1

A 的 _ch 在偏移 0,后面补 3 字节,_i 在偏移 4 到 7,总大小 8。B 和 C 没有成员变量,sizeof 仍是 1。

空类占 1 字节是占位:如果一个字节都不给,就无法表示"对象存在过",也没法区分两个不同对象的地址。

成员顺序也会改变大小:

cpp 复制代码
class E1
{
    char _a;
    int _i;
    char _b;
};

class E2
{
    char _a;
    char _b;
    int _i;
};

E1 的排布是 _a 在 0,补 3 字节,_i 在 4 到 7,_b 在 8,再补 3 字节,总共 12。E2 是 _a 在 0、_b 在 1,补 2 字节,_i 在 4 到 7,总共 8。

类 偏移排布 sizeof
E1 _a@0,补 3;_i@4--7;_b@8,补 3 12
E2 _a@0、_b@1,补 2;_i@4--7 8

同样三个成员,换个顺序差 4 字节。把占用小的成员集中放在一起通常更省空间,但不要为了几字节打乱可读性。

3. this 指针

3.1 成员函数怎么知道操作哪个对象

d1.Init(...) 和 d2.Init(...) 调的是同一段代码。函数体里没有写对象名,它怎么知道改的是 d1 还是 d2?

C++ 给每个非静态成员函数加一个隐含参数 this,类型是 Date* const。Init 编译后的真实原型是:

cpp 复制代码
// 编译器内部的等价形式,不能直接写
void Init(Date* const this, int year, int month, int day);

this 指向调用该函数的对象。函数体里的 _year = year 等价于 this->_year = year。

cpp 复制代码
class Date
{
public:
    void Init(int year, int month, int day)
    {
        this->_year = year;
        _month = month;
        this->_day = day;
    }

private:
    int _year, _month, _day;
};

this 不能在形参列表里显式写,编译器已经加了;可以在函数体里用。它的类型是 Date* const,指针本身是常量,this = nullptr; 会编译报错。

this 是成员函数的隐含形参,随函数调用保存在栈上或寄存器里,具体位置由编译器决定,不在对象内部。

写法 实际含义
_year this->_year
d1.Init(2024, 3, 31) Date::Init(&d1, 2024, 3, 31)
d2.Init(2023, 7, 5) Date::Init(&d2, 2023, 7, 5)

3.2 空指针调用成员函数的两种结果

空指针调用成员函数有两种结果,取决于函数有没有访问成员变量。

Print 的实现 结果 原因
只输出字符串,不访问成员 多数实现能运行,但属于未定义行为 调用成员函数不需要解引用对象;this 是空指针但没被使用
访问 _a 运行崩溃 _a 即 this->_a,对空指针解引用

只输出字符串、不访问成员的函数,多数实现能跑通;但 C++ 标准把这种调用定为未定义行为(undefined behavior,UB),不要依赖。

一旦函数里读写 _a,_a 就是 this->_a,对空指针解引用,程序会崩溃。

4. 封装:同一个 Stack 的两种写法

4.1 C 的写法

C 版 Stack 把数据放在结构体里,函数单独定义,每个函数显式接收 ST*:

c 复制代码
typedef struct Stack
{
    int* a;
    int top;
    int capacity;
} ST;

调用时要写 STInit(&s)、STPush(&s, 1)、STTop(&s)、STDestroy(&s),每一步都把 &s 传来传去。

数据、函数、参数三处都要对上;任何拿到 ST* 的代码都能改 s.top 或 s.capacity。初始化、扩容、释放的规则靠调用者记住。

4.2 C++ 的写法

把数据和接口放进类,成员变量设为 private,只暴露 Push、Pop、Top、Empty、Init、Destroy:

cpp 复制代码
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <iostream>

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

    void Push(int x)
    {
        if (_top == _capacity)
        {
            size_t new_capacity = _capacity == 0 ? 4 : _capacity * 2;
            int* tmp = (int*)realloc(_a, sizeof(int) * new_capacity);
            if (tmp == nullptr)
            {
                perror("realloc 失败");
                return;
            }
            _a = tmp;
            _capacity = new_capacity;
        }
        _a[_top++] = x;
    }

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

    int Top()
    {
        assert(_top > 0);
        return _a[_top - 1];
    }

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

    void Destroy()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

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

int main()
{
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    while (!s.Empty())
    {
        std::cout << s.Top() << std::endl;
        s.Pop();
    }
    s.Destroy();
    return 0;
}

输出:

text 复制代码
3
2
1

_a、_capacity、_top 是私有的,外部只能通过接口操作。Init 统一申请空间,Destroy 统一释放;调用时不用再传 ST*,this 隐式传递。

Init 只应调用一次;重复调用会覆盖 _a,旧空间就泄漏了。构造函数版本没有这个问题,因为对象只构造一次。

不过这份代码仍要手动调用 Init 和 Destroy。下一节的构造函数和析构函数会把这两步也自动化。

4.3 两种写法对照

操作 C 版 C++ 版
初始化 STInit(&s); s.Init();
入栈 STPush(&s, 1); s.Push(1);
取栈顶 STTop(&s); s.Top();
出栈 STPop(&s); s.Pop();
判空 STEmpty(&s); s.Empty();
释放 STDestroy(&s); s.Destroy();
对比项 C 版 C++ 版
数据与函数 分开 同一个类
访问控制 无 public / private
参数传递 显式传 ST* this 隐式传递
类型名 需要 typedef 类名直接当类型名
初始化 手动 STInit 仍需手动 Init
清理 手动 STDestroy 仍需手动 Destroy
接口 时间复杂度 说明
Push 均摊 O(1) 扩容时复制 n 个元素,均摊到每次插入
Pop / Top / Empty O(1) 只改下标或读一个元素
Init / Destroy O(1) 一次 malloc 或 free

Push 的最坏情况是扩容,复杂度 O(n);连续插入 n 个元素总代价 O(n),所以均摊 O(1)。

5. 构造函数与析构函数

5.1 六个默认成员函数

不写时由编译器自动生成的成员函数叫默认成员函数(default member function)。一个类默认有 6 个,前 4 个最常用:

函数 作用 调用时机
构造函数 初始化对象 对象创建时
析构函数 清理资源 对象生命周期结束时
拷贝构造函数 用同类型对象初始化新对象 拷贝构造时
赋值运算符重载 operator= 两个已存在对象之间赋值 d1 = d2 时
operator& 普通对象取地址 &d1 时
operator&() const const 对象取地址 &d2 时(d2 为 const)

C++11 还会增加移动构造和移动赋值,本文不展开。学习每个默认成员函数都问两件事:不写时编译器生成的行为是什么,够不够用;不够用时怎么自己实现。

5.2 构造函数

构造函数(constructor)的名字和类名相同,没有返回值,对象创建时自动调用,可以重载。它不负责给对象开空间(局部对象的空间在栈帧创建时就有了),负责初始化成员。

1.1 的 Date d; 要先调用 Init;忘记调用,成员就是不确定值。构造函数把这一步放到对象创建时自动完成。

cpp 复制代码
#include <iostream>

class Date
{
public:
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

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

private:
    int _year, _month, _day;
};

int main()
{
    Date d1;
    Date d2(2024, 3, 31);
    d1.Print();
    d2.Print();
    return 0;
}

输出:

text 复制代码
1-1-1
2024-3-31

不写构造函数时,编译器生成一个无参的默认构造;一旦自己定义了任何一个构造函数,编译器就不再生成。

无参构造、全缺省构造、编译器生成的构造,都叫默认构造函数(default constructor)。三者只能存在一个:Date() 和 Date(int year = 1, int month = 1, int day = 1) 都能被 Date d; 调用,同时写会产生歧义。

Date d3(); 不是创建对象,而是声明一个返回 Date、无参数的函数。创建无参对象写 Date d3;。

如果只保留带参构造,Date d; 会找不到可用的默认构造;VS2026/MSVC 报 C2512:没有合适的默认构造函数可用。

编译器生成的构造函数对内置类型(built-in type)成员不做初始化,对自定义类型(user-defined type)成员调用其默认构造:

cpp 复制代码
#include <iostream>

class Time
{
public:
    Time() { std::cout << "Time()" << std::endl; }
};

class Date
{
private:
    int _year;
    Time _t;
};

int main()
{
    Date d;
    std::cout << "Date 构造完成,_year 的值不确定" << std::endl;
    return 0;
}

输出:

text 复制代码
Time()
Date 构造完成,_year 的值不确定

Date d; 会输出 Time(),但 _year 的值不确定。内置类型成员没有被初始化,不要假设它是 0。

如果自定义类型成员没有默认构造,编译器生成的构造函数无法完成初始化,必须用初始化列表显式调用它的构造函数;第 8 节展开。

5.3 析构函数

析构函数(destructor)的名字是 ~类名,没有参数和返回值,一个类只能有一个,对象生命周期结束时自动调用。它不销毁对象本身,负责清理对象持有的资源。

把 4.2 的 Init 和 Destroy 换成下面两个函数,其余成员不变:

cpp 复制代码
class Stack
{
public:
    Stack(int n = 4)
    {
        assert(n > 0);
        _a = (int*)malloc(sizeof(int) * n);
        if (_a == nullptr)
        {
            perror("malloc 申请空间失败");
            _capacity = _top = 0;
            return;
        }
        _capacity = n;
        _top = 0;
    }

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

    // Push、Pop、Top、Empty 与 4.2 相同

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

现在写 Stack s; 就会自动初始化,Push、Top 等成员照 4.2 的写法调用,离开作用域时自动释放。不需要 Init,也不需要 Destroy。

C 版在每个提前 return 的分支都要补 STDestroy;C++ 版无论从哪条路径返回,析构函数都会执行。

编译器生成的析构函数不处理内置类型成员,对自定义类型成员调用其析构。即使自己写了析构函数,自定义类型成员也一定会析构。

同一局部域的多个对象,后定义的先析构;对象的成员按声明顺序的逆序析构。

cpp 复制代码
#include <iostream>

class A
{
public:
    A(int id = 0) : _id(id) { std::cout << "A(" << _id << ")" << std::endl; }
    ~A() { std::cout << "~A(" << _id << ")" << std::endl; }

private:
    int _id;
};

class MyQueue
{
private:
    A _push;
    A _pop;
};

int main()
{
    A a1(1);
    A a2(2);
    MyQueue q;
    return 0;
}

输出:

text 复制代码
A(1)
A(2)
A(0)
A(0)
~A(0)
~A(0)
~A(2)
~A(1)

q 的两个成员按声明顺序构造,析构时先 _pop 后 _push;q 之后才是 a2、a1。局部对象整体后定义先析构。

6. 拷贝构造与赋值重载

6.1 拷贝构造函数

拷贝构造函数(copy constructor)是构造函数的重载:第一个参数是自身类类型的引用,其余参数都有默认值。

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

    Date(const Date& d)
        : _year(d._year), _month(d._month), _day(d._day) {}

    Date(Date* d)
        : _year(d->_year), _month(d->_month), _day(d->_day) {}

private:
    int _year, _month, _day;
};

int main()
{
    Date d1(2024, 7, 5);
    Date d2(d1);    // 拷贝构造
    Date d3 = d1;   // 也是拷贝构造,不是赋值
    Date d4(&d1);   // 普通构造:形参是 Date*
    return 0;
}

Date d3 = d1; 看起来像赋值,但 d3 是新对象,调用的是拷贝构造。两个已经存在的对象之间用 = 才是赋值。

第一个参数必须是引用。写成 Date(Date d) 会引发无穷递归:传值传参本身又要调用拷贝构造,编译器直接报错。

参数加 const 是为了能拷贝 const 对象,也表明不修改源对象。Date(Date* d) 只是普通构造,不是拷贝构造。

不写拷贝构造时,编译器生成默认版本:内置类型成员逐字节复制,自定义类型成员调用其拷贝构造。这种复制叫浅拷贝(shallow copy)。

对 Date 来说,浅拷贝够用:三个 int 各复制一份。对 Stack 来说,_a 指向堆内存,浅拷贝会让两个对象指向同一块缓冲区。

Stack s2 = s1; 后,s1._a 和 s2._a 指向同一块内存。两个对象析构时各 free 一次,同一块内存被释放两次,程序崩溃。

深拷贝(deep copy)要重新申请一块空间,再把数据复制过去。在 Stack 类中写:

cpp 复制代码
Stack(const Stack& st)
    : _a((int*)malloc(sizeof(int) * st._capacity)),
      _capacity(st._capacity), _top(st._top)
{
    if (_a == nullptr)
    {
        perror("malloc 申请空间失败");
        _capacity = _top = 0;
        return;
    }
    memcpy(_a, st._a, sizeof(int) * st._top);
}

申请失败时把 _capacity 和 _top 置 0,避免后面使用未初始化的计数。memcpy 只复制 _top 个有效元素,不复制容量里的空闲部分;使用它需要包含 <cstring>。

判断规则:析构函数释放了资源,拷贝构造通常也要自己写;否则默认浅拷贝会让两个对象共用同一份资源。

6.2 什么时候调用拷贝构造

拷贝构造在三种场景下会被调用:用已有对象初始化新对象、传值传参、传值返回。

场景 是否调用拷贝构造 说明
Date d2(d1);、Date d2 = d1; 是 用已有对象初始化新对象
void f(Date d); f(d1); 是 形参本身是新对象
Date f() { Date tmp; return tmp; } 是,可能被优化掉 返回值对象或临时对象
Date& f() { ... } 否 返回引用,不拷贝
Date d4(&d1); 否 形参是指针,走普通构造

下面几个函数对应表中的场景,接着 6.1 的 Date 类看:

cpp 复制代码
void Func1(Date d)
{
    // 形参 d 是 d1 的拷贝
}

Date Func2()
{
    Date tmp(2024, 7, 5);
    return tmp;   // 传值返回,通常构造返回值对象
}

Date& Func3()
{
    Date tmp(2024, 7, 5);
    return tmp;   // 悬空引用:tmp 在函数结束时销毁
}

int main()
{
    Date d1(2024, 7, 5);
    Func1(d1);       // 拷贝构造形参
    Date d2(d1);     // 拷贝构造
    Date d3 = d1;    // 拷贝构造,不是赋值
    Date d4(&d1);    // 普通构造:形参是 Date*
    return 0;
}

传值返回会产生临时对象;编译器可能省略这次拷贝,第 11 节再讲。传引用返回不拷贝,但返回局部对象的引用会变成悬空引用(dangling reference):函数结束,tmp 已销毁,引用指向无效内存。

6.3 赋值运算符重载

赋值重载是运算符重载(operator overloading)的一种。运算符重载用 operator 加运算符作为函数名,给类类型对象定义这个运算符的含义。

规则:至少有一个操作数是类类型;不能改变内置类型的运算符含义;不能创造新运算符;不能改变优先级、结合性和操作数个数。

.*、::、sizeof、?:、. 这 5 个运算符不能重载。重载成成员函数时,第一个操作数由 this 承担,参数比操作数少一个。

++ 的前置和后置都叫 operator++,C++ 规定后置版本多一个 int 形参来区分;<<、>> 通常重载成全局函数,9.2 节会看到。

赋值运算符重载用于两个已经存在的对象之间赋值,必须写成成员函数。在 Date 类中写:

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

参数用 const Date&,避免传值多一次拷贝;返回 Date&,少一次拷贝,也支持连续赋值 d1 = d2 = d3。

if (this != &d) 防止自己给自己赋值。返回 *this 是因为 d1 = d2 的结果应该是 d1,不是临时副本。

不写赋值重载时,编译器生成默认版本:内置类型成员浅拷贝,自定义类型成员调用其赋值重载。Stack 这类持有资源的类必须深拷贝,在 Stack 类中写:

cpp 复制代码
Stack& operator=(const Stack& st)
{
    if (this != &st)
    {
        int* tmp = (int*)malloc(sizeof(int) * st._capacity);
        if (tmp == nullptr)
        {
            perror("malloc 申请空间失败");
            return *this;
        }
        memcpy(tmp, st._a, sizeof(int) * st._top);
        free(_a);
        _a = tmp;
        _capacity = st._capacity;
        _top = st._top;
    }
    return *this;
}

先申请新空间,复制数据,成功后再释放旧空间;如果先 free(_a) 再申请失败,原数据就丢了。

对比项 拷贝构造 赋值重载
使用时机 用已有对象初始化新对象 两个已存在对象之间赋值
典型写法 Date d2(d1);、Date d2 = d1; d1 = d2;
是否创建新对象 是 否
返回类型 无 Date&,支持连续赋值

7. 用 Date 串起运算符重载

7.1 const 成员函数

把 const 写在成员函数参数列表后面,就是 const 成员函数。它修饰隐含的 this:Date* const this 变成 const Date* const this,函数内不能修改成员变量。

前面的 Print 没有加 const,因为还没讲到;现在补上,看看它约束了什么。

cpp 复制代码
#include <iostream>

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

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

private:
    int _year, _month, _day;
};

int main()
{
    Date d1;
    d1.Print();
    const Date d2(2024, 2, 29);
    d2.Print();
    return 0;
}

输出:

text 复制代码
1900-1-1
2024-2-29

普通对象可以调用 const 成员函数,权限缩小;const 对象只能调用 const 成员函数,不能调用普通成员函数,否则是权限放大。

对象 \ 成员函数 普通成员函数 const 成员函数
普通对象 可以 可以(权限缩小)
const 对象 不可以(权限放大) 可以

如果某个成员在 const 成员函数里确实要修改,可以用 mutable 修饰,例如缓存或计数器。它打破 const 约束,非必要不用。

operator+ 不修改原对象,只是复制一份再改,所以能声明成 const;operator+=、operator++ 会修改对象,不能加 const。只读接口都加上 const,const 对象才能正常使用。

7.2 比较运算符

六个比较运算符不用各写一套。先在 Date 类中声明,再在 Date.cpp 中定义;完整的类声明在 7.5。先写 < 和 ==,其余四个用它们组合:

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); }
bool Date::operator>=(const Date& d) const { return !(*this < d); }
bool Date::operator!=(const Date& d) const { return !(*this == d); }

< 先比年,再比月,最后比日;== 三个字段都相等才返回 true。<= 是 < 或 ==,> 是 !(<=),>= 是 !(<),!= 是 !(==)。

这样只需要维护两处字段比较,其余四个不会再写错。六个函数都不修改对象,全部声明成 const。

7.3 日期加减与前后置 ++

+= 修改对象并返回引用;+ 复制一份再调用 +=,返回新对象,不改变原对象。在 Date.cpp 中定义:

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

    _day += day;
    while (_day > GetMonthDay(_year, _month))
    {
        _day -= GetMonthDay(_year, _month);
        if (++_month == 13)
        {
            ++_year;
            _month = 1;
        }
    }
    return *this;
}

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

GetMonthDay 处理闰年:能被 4 整除且不能被 100 整除,或者能被 400 整除,二月是 29 天。day 为负数时转给 -= 处理。

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

    _day -= day;
    while (_day <= 0)
    {
        if (--_month == 0)
        {
            _month = 12;
            --_year;
        }
        _day += GetMonthDay(_year, _month);
    }
    return *this;
}

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

int Date::operator-(const Date& d) const
{
    Date max = *this;
    Date min = d;
    int flag = 1;

    if (max < min)
    {
        max = d;
        min = *this;
        flag = -1;
    }

    int n = 0;
    while (min != max)
    {
        ++min;
        ++n;
    }
    return n * flag;
}

- 有两个重载:Date operator-(int) 返回日期,int operator-(const Date&) 返回相差天数。参数类型不同,编译器能区分调用哪个。

日期差先确定较大的日期,再逐日 ++min,直到相等;flag 保留方向,d1 - d2 和 d2 - d1 符号相反。

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

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

前置 ++ 修改后返回引用,后置 ++ 先保存旧值,再修改,返回旧值的副本。后置版本的 int 参数只是占位,调用时不用传。-- 的写法相同,把 += 1 换成 -= 1。

复杂度:比较是 O(1);+=、-= 按跨过的月份数推进,O(k);+、- 先复制再调用,复杂度相同;日期差逐日推进,O(n),n 是相差天数。

构造、GetMonthDay、CheckDate、Print 都是 O(1);++、-- 每次最多跨一个月,也是 O(1)。

7.4 取地址运算符重载

编译器会生成两个取地址运算符:Date* operator&() 和 const Date* operator&() const,分别返回普通对象和 const 对象的地址。一般不用自己写。

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

    Date* operator&()
    {
        return this;
    }

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

private:
    int _year, _month, _day;
};

只有不想让外部拿到真实地址时才自己写,例如返回 nullptr 或代理地址。这种写法会让调用方意外,除非有明确需求,否则保留默认版本。

7.5 完整实现与测试

把声明和定义分开,得到三个文件。Date.h:

cpp 复制代码
#pragma once
#include <iostream>

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1);

    void Print() const;
    bool CheckDate() const;
    int GetMonthDay(int year, int month) 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 _year;
    int _month;
    int _day;
};

Date.cpp:

cpp 复制代码
#include "Date.h"
#include <cassert>
#include <iostream>

Date::Date(int year, int month, int day)
    : _year(year), _month(month), _day(day)
{
    if (!CheckDate())
        std::cout << "日期非法: " << _year << "-" << _month << "-" << _day << std::endl;
}

bool Date::CheckDate() const
{
    return _month >= 1 && _month <= 12
        && _day >= 1 && _day <= GetMonthDay(_year, _month);
}

int Date::GetMonthDay(int year, int month) const
{
    assert(month >= 1 && month <= 12);
    static const int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
        return 29;
    return days[month];
}

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

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); }
bool Date::operator>=(const Date& d) const { return !(*this < d); }
bool Date::operator!=(const Date& d) const { return !(*this == d); }

Date& Date::operator+=(int day)
{
    if (day < 0)
        return *this -= -day;

    _day += day;
    while (_day > GetMonthDay(_year, _month))
    {
        _day -= GetMonthDay(_year, _month);
        if (++_month == 13)
        {
            ++_year;
            _month = 1;
        }
    }
    return *this;
}

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

Date& Date::operator-=(int day)
{
    if (day < 0)
        return *this += -day;

    _day -= day;
    while (_day <= 0)
    {
        if (--_month == 0)
        {
            _month = 12;
            --_year;
        }
        _day += GetMonthDay(_year, _month);
    }
    return *this;
}

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

int Date::operator-(const Date& d) const
{
    Date max = *this;
    Date min = d;
    int flag = 1;

    if (max < min)
    {
        max = d;
        min = *this;
        flag = -1;
    }

    int n = 0;
    while (min != max)
    {
        ++min;
        ++n;
    }
    return n * flag;
}

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

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

Date& Date::operator--()
{
    *this -= 1;
    return *this;
}

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

main.cpp:

cpp 复制代码
#include "Date.h"

int main()
{
    Date d1(2024, 2, 28);
    Date d2 = d1 + 1;
    std::cout << "d1 + 1 = ";
    d2.Print();

    Date d3(2023, 2, 28);
    Date d4 = d3 + 1;
    std::cout << "d3 + 1 = ";
    d4.Print();

    Date d5(2024, 12, 31);
    Date d6 = d5 + 1;
    std::cout << "d5 + 1 = ";
    d6.Print();

    Date d7(2024, 3, 1);
    Date d8 = d7 - 1;
    std::cout << "d7 - 1 = ";
    d8.Print();

    std::cout << "d1 < d7: " << (d1 < d7) << std::endl;
    std::cout << "d1 == d1: " << (d1 == d1) << std::endl;

    Date d9(2024, 1, 1);
    Date d10(2025, 1, 1);
    std::cout << "d10 - d9 = " << (d10 - d9) << std::endl;

    Date d11 = d1++;
    std::cout << "后置++ 返回: ";
    d11.Print();
    std::cout << "d1 变成: ";
    d1.Print();

    Date d12 = ++d1;
    std::cout << "前置++ 返回: ";
    d12.Print();
    std::cout << "d1 变成: ";
    d1.Print();
    return 0;
}

在 VS2026 中把三个文件加入同一个项目,编译运行。输出:

text 复制代码
d1 + 1 = 2024-2-29
d3 + 1 = 2023-3-1
d5 + 1 = 2025-1-1
d7 - 1 = 2024-2-29
d1 < d7: 1
d1 == d1: 1
d10 - d9 = 366
后置++ 返回: 2024-2-28
d1 变成: 2024-2-29
前置++ 返回: 2024-3-1
d1 变成: 2024-3-1

Date.h 只放声明,Date.cpp 放定义,main.cpp 做测试;#pragma once 防止头文件被重复包含。成员函数在类外定义时要带 Date::,声明和定义里的 const 都要写。

8. 初始化列表与 explicit

8.1 初始化列表

在构造函数体内写 _year = year; 是"先构造、再赋值"。初始化列表(member initializer list)在成员定义时直接给初值,以冒号开始,逗号分隔。

cpp 复制代码
#include <iostream>

class Time
{
public:
    Time(int hour = 0) : _hour(hour)
    {
        std::cout << "Time(" << _hour << ")" << std::endl;
    }

private:
    int _hour;
};

class Date
{
public:
    Date(int& x, int year = 1, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day), _t(12), _ref(x), _n(1)
    {}

private:
    int _year, _month, _day;
    Time _t;
    int& _ref;
    const int _n;
};

int main()
{
    int x = 10;
    Date d(x);
    std::cout << "ok" << std::endl;
    return 0;
}

输出:

text 复制代码
Time(12)
ok

_t(12) 调用 Time(int);_ref(x) 把引用绑定到 x;_n(1) 初始化 const 成员。成员在进入函数体之前就已经构造完成。

引用成员、const 成员、没有默认构造的类类型成员,必须写进初始化列表;否则 VS2026/MSVC 分别报 C2530、C2789、C2512。

每个成员在初始化列表里只能出现一次。无论是否显式写,每个构造函数都有初始化列表;无论是否显式列出,每个成员都要走这一步。

没列进初始化列表的成员,按下面的规则处理:

成员情况 结果
声明处有缺省值 用该缺省值
内置类型且无缺省值 是否初始化取决于编译器,值不确定
类类型且能默认构造 调用默认构造
类类型且无默认构造 编译错误

类类型成员如果有默认构造,写在函数体里赋值也能编译,但会先默认构造再赋值,多一次操作;能用初始化列表就用初始化列表。

8.2 声明处缺省值与初始化顺序

C++11 允许在成员声明处写 = 值。这不是在声明处初始化,而是给初始化列表准备缺省值;只有列表没提到该成员时才用。

cpp 复制代码
class Time
{
public:
    Time(int hour = 0) : _hour(hour) {}

private:
    int _hour;
};

class Date
{
public:
    Date() : _month(2) {}

private:
    int _year = 1;
    int _month = 1;
    int _day;
    Time _t = 1;
};

Date d; 中 _year 用缺省值 1,_month 被初始化列表的 2 覆盖,_day 是内置类型且没有缺省值,值不确定;_t = 1 会用 Time(int) 构造。

内置类型不会自动清零。没有在初始化列表出现、声明处也没有缺省值的成员,C++ 不规定是否初始化,不要假设它是 0。

初始化顺序由声明顺序决定,和初始化列表的书写顺序无关:

cpp 复制代码
#include <iostream>

class A
{
public:
    A(int a) : _a1(a), _a2(_a1) {}

    void Print() const
    {
        std::cout << _a1 << " " << _a2 << std::endl;
    }

private:
    int _a2 = 2;
    int _a1 = 2;
};

int main()
{
    A a(1);
    a.Print();
    return 0;
}

_a2 先声明,所以先初始化;它的初值是 _a1,但 _a1 还没初始化,读到的是不确定值。然后 _a1 才初始化为 a。这是未定义行为。

声明处的 = 2 被初始化列表里的 _a2(_a1) 覆盖,不参与这次初始化。

把声明顺序和初始化列表顺序写成一致,能避免这类问题;部分编译器在两者不一致时会给出警告。

8.3 explicit 与隐式类型转换

如果类有一个可以用一个实参调用的构造函数,编译器允许把该实参隐式转换成类类型对象。这个构造函数叫转换构造函数(converting constructor)。

cpp 复制代码
class Point
{
public:
    explicit Point(int x) : _x(x) {}

    int Get() const { return _x; }

private:
    int _x;
};

int main()
{
    Point p1(1);       // 直接初始化,可以
    Point p2{2};       // 直接列表初始化,可以
    // Point p3 = 3;   // 隐式转换,编译错误
    // Point p4 = {4}; // 隐式转换,编译错误
    // const Point& r = 5; // 隐式转换,编译错误

    const Point& r = Point(5);
    return 0;
}

不加 explicit 时,Point p = 1; 会先构造一个临时 Point(1),再拷贝给 p;编译器通常会把这两步合并。加上 explicit 后,这条隐式转换被禁止,所以上面的 Point 把相关写法注释掉了。

explicit 只禁止隐式转换。Point p(1);、Point p{1};、static_cast<Point>(1) 仍然可以编译。

写法 含义 构造函数为 explicit 时
Point p(1); 直接初始化 允许
Point p{1}; 直接列表初始化 允许
Point p = 1; 拷贝初始化,隐式转换 禁止
Point p = {1}; 拷贝列表初始化 禁止
const Point& r = 1; 临时对象绑定常引用 禁止
f(1)(参数为 Point) 实参隐式转换 禁止

单参数构造函数建议默认加 explicit,除非确实需要让内置类型自动转换成类类型。Stack(int n = 4) 这种构造函数也可以加,避免 Stack s = 4; 这种意外写法。

9. static 成员与友元

9.1 静态成员

用 static 修饰的成员变量叫静态成员变量(static member variable):属于类,被所有对象共享,不存放在对象里,存放在静态区。

静态成员变量必须在类外定义并初始化;声明处不能给缺省值(C++17 的 inline static 例外)。访问受访问限定符限制。

cpp 复制代码
#include <iostream>

class A
{
public:
    A() { ++_scount; }
    A(const A&) { ++_scount; }
    ~A() { --_scount; }

    static int GetACount() { return _scount; }

private:
    static int _scount;
};

int A::_scount = 0;

int main()
{
    std::cout << A::GetACount() << std::endl;
    A a1, a2;
    A a3(a1);
    std::cout << A::GetACount() << std::endl;
    std::cout << a1.GetACount() << std::endl;
    std::cout << sizeof(A) << std::endl;
    return 0;
}

输出:

text 复制代码
0
3
3
1

int A::_scount = 0; 是定义并初始化,不是类内的声明;只写类内声明、不写类外定义,链接时会报找不到定义。

构造函数和拷贝构造让计数加一,析构让它减一,GetACount 返回当前存活对象数。sizeof(A) 是 1,因为 _scount 不在对象里。

静态成员函数没有 this,只能访问静态成员,不能直接访问非静态成员;可以通过 A::GetACount() 或对象调用。

对比项 静态成员 非静态成员
存储位置 静态区 对象内部
归属 所有对象共享 每个对象独立
是否有 this 否 是
访问方式 A::member 或 对象.member 对象.member
能否访问非静态成员 否 是
声明处能否给缺省值 不能(C++17 inline static 例外) 能
是否计入 sizeof 否 是

C++17 起可以写 inline static int _scount = 0;,在类内直接定义并初始化;本文按传统的类内声明、类外定义写法。

9.2 友元函数

友元(friend)让一个非成员函数访问类的私有和保护成员。在类内用 friend 声明,函数本身定义在类外。

cpp 复制代码
#include <iostream>

class Date
{
    friend std::ostream& operator<<(std::ostream& out, const Date& d);

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

private:
    int _year, _month, _day;
};

std::ostream& operator<<(std::ostream& out, const Date& d)
{
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
}

int main()
{
    Date d(2024, 2, 29);
    std::cout << d << std::endl;
    return 0;
}

输出:

text 复制代码
2024-2-29

把这条 friend 声明加到 7.5 的 Date 类里,把函数定义加到 Date.cpp,就可以用 std::cout << d 输出日期。

operator<< 必须重载成全局函数。写成成员函数时,this 会抢占第一个参数位置,调用变成 d << std::cout,不符合 std::cout << d 的习惯。

友元声明可以放在类内任何位置,不受 public / private 影响;友元函数不是成员函数,一个函数可以是多个类的友元。

operator>> 的写法相同,只是参数换成 std::istream& 和非 const 的 Date&,因为要往对象里写数据。

友元会增加类之间的耦合,也会破坏封装。能用公开接口解决时不要用;流运算符是常见的合理场景,因为要么开放成员,要么加一组 getter,友元让 operator<< 直接读私有成员。

9.3 友元类

在 Date 中写 friend class DateTool;,DateTool 的所有成员函数都能访问 Date 的私有和保护成员。

cpp 复制代码
#include <iostream>

class Date
{
    friend class DateTool;

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

private:
    int _year, _month, _day;
};

class DateTool
{
public:
    static bool IsLeap(const Date& d)
    {
        return (d._year % 4 == 0 && d._year % 100 != 0) || d._year % 400 == 0;
    }
};

int main()
{
    Date d(2024, 2, 29);
    std::cout << DateTool::IsLeap(d) << std::endl;
    return 0;
}

输出:

text 复制代码
1

友元关系有三个性质:单向、不传递、不继承。

性质 含义
单向 A 是 B 的友元,不代表 B 是 A 的友元
不传递 A 是 B 的友元、B 是 C 的友元,不代表 A 是 C 的友元
不继承 友元关系不会被派生类继承

友元类同样增加耦合。只把确实需要访问内部表示的辅助类设为友元,不要为了方便把所有类都开放。

10. 内部类与匿名对象

10.1 内部类

定义在另一个类内部的类叫内部类(nested class)。它是独立的类,只是名字受外部类类域和访问限定符限制。

cpp 复制代码
#include <iostream>

class A
{
private:
    static int _k;
    int _h = 1;

public:
    class B
    {
    public:
        void foo(const A& a) const
        {
            std::cout << _k << " " << a._h << std::endl;
        }
    };
};

int A::_k = 1;

int main()
{
    std::cout << sizeof(A) << std::endl;
    A::B b;
    A a;
    b.foo(a);
    return 0;
}

输出:

text 复制代码
4
1 1

创建内部类对象要写 A::B b;。A 的对象里不包含 B 对象,sizeof(A) 只计算非静态成员 _h,所以是 4。

内部类默认是外部类的友元,可以访问外部类的私有成员,包括静态成员 _k 和对象成员 a._h;反过来,外部类没有访问内部类私有成员的特权。

把 B 放在 private 里,B 就成了 A 的专属实现细节,外部不能直接使用。两个类紧密关联、且一个主要给另一个使用时,可以这样设计。

对比项 内部类 普通成员对象
占用外部对象空间 否 是
生命周期 独立,可单独创建 随外部对象
访问外部类私有成员 默认可 不可以
影响外部类 sizeof 不影响 影响

10.2 匿名对象

用 类型(实参) 直接创建的对象叫匿名对象(anonymous object);用 类型 对象名(实参) 创建的是有名对象。

cpp 复制代码
#include <iostream>

class A
{
public:
    A(int a = 0) : _a(a) { std::cout << "A(" << _a << ")" << std::endl; }
    ~A() { std::cout << "~A(" << _a << ")" << std::endl; }

    void Print() const { std::cout << _a << std::endl; }

private:
    int _a;
};

int main()
{
    A a1;
    A();
    A(1);
    A a2(2);
    A().Print();
    const A& r = A(3);
    r.Print();
    std::cout << "end" << std::endl;
    return 0;
}

输出:

text 复制代码
A(0)
A(0)
~A(0)
A(1)
~A(1)
A(2)
A(0)
0
~A(0)
A(3)
3
end
~A(3)
~A(2)
~A(0)

A a1; 是有名对象,生命周期到作用域结束;A();、A(1); 是匿名对象,生命周期到当前完整表达式结束,通常就是这一行。

A a1(); 不是创建对象,而是函数声明。创建无参对象写 A a1;。

匿名对象适合只调用一次成员函数的场景,例如 A().Print();;调用完立刻析构,不需要为一次调用起名字。

const A& r = A(3); 会把临时对象的生命周期延长到 r 结束,这是常引用绑定临时对象的规则。

对比项 有名对象 匿名对象
定义形式 A a(1); A(1);
生命周期 作用域结束 当前完整表达式结束
典型用途 反复使用 只调用一次成员函数、临时传参

11. 拷贝省略

传值传参和传值返回会产生拷贝。编译器在保证语义不变的前提下,可以把连续几次拷贝合并成一次构造,这叫拷贝省略(copy elision)。

cpp 复制代码
#include <iostream>

class A
{
public:
    A(int a = 0) : _a(a) { std::cout << "A(int)" << std::endl; }
    A(const A& aa) : _a(aa._a) { std::cout << "A(const A&)" << std::endl; }

    A& operator=(const A& aa)
    {
        std::cout << "operator=" << std::endl;
        if (this != &aa)
            _a = aa._a;
        return *this;
    }

    ~A() { std::cout << "~A()" << std::endl; }

private:
    int _a;
};

void f1(A aa) {}

A f2()
{
    A aa;
    return aa;
}

int main()
{
    A aa1;
    f1(aa1);
    f1(1);
    f1(A(2));
    f2();
    A aa2 = f2();
    aa1 = f2();
    return 0;
}

f1(aa1) 的 aa1 已经存在,形参必须是新对象,这次拷贝省不掉。f1(1) 先构造临时 A(1) 再拷贝给形参,编译器可以直接用 1 构造形参。

f1(A(2)) 是匿名对象加拷贝,通常合并成一次构造;f2() 是局部对象加返回值临时对象,通常也合并;A aa2 = f2() 连续两次拷贝,通常合并成一次构造。

aa1 = f2() 的赋值不能省,因为 aa1 已经存在;临时对象的构造可能被合并。

调用场景 未优化时的步骤 常见优化结果
f1(aa1) 拷贝 1 次 不变,拷贝省不掉
f1(1) 构造 1 次 + 拷贝 1 次 构造 1 次
f1(A(2)) 构造 1 次 + 拷贝 1 次 构造 1 次
f2() 构造 1 次 + 拷贝 1 次 构造 1 次
A aa2 = f2() 构造 1 次 + 拷贝 2 次 构造 1 次
aa1 = f2() 构造 1 次 + 拷贝 1 次 + 赋值 1 次 构造 1 次 + 赋值 1 次

表中只统计该行表达式本身引起的构造、拷贝和赋值;A aa1; 那次构造不计入。

C++17 起,用同类型的纯右值(prvalue)初始化对象时,拷贝省略是语言规则,不再是可选优化,典型例子是 A a = A(1);。命名返回值(如 f2() 的 aa)是否省略仍由编译器决定。

拷贝是否被省略取决于编译器、版本、优化选项和具体表达式。不要把拷贝构造的调用次数当作程序逻辑,也不要在拷贝构造里写必须执行的业务代码。析构次数同理,少一次拷贝就少一次析构。

12. 小结

  • 类把数据和操作数据的函数放进同一个类型;public / private 控制外部能访问什么,类外定义成员要用 类名:: 指定类域。
  • 类不占对象空间;对象只存非静态成员变量,成员函数在代码段共用;对象大小按内存对齐计算,空类占 1 字节。
  • 非静态成员函数隐含 this 参数;const 成员函数把 this 变成 const 类名* const,const 对象只能调用 const 成员函数。
  • 构造函数在对象创建时初始化,析构函数在生命周期结束时清理;局部对象后定义先析构,有资源申请时要自己写析构。
  • 拷贝构造的第一个参数必须是类类型引用;默认浅拷贝,持有资源的类要深拷贝;赋值重载必须写成成员函数,检查自赋值并返回 *this。
  • 运算符重载不能改变优先级、结合性和操作数个数;比较运算符只写 < 和 ==,其余复用;初始化列表负责成员定义时的初始化,顺序由声明顺序决定。
  • static 成员属于类、存放在静态区;友元单向、不传递、不继承;内部类不占外部对象空间;匿名对象生命周期到当前表达式结束;拷贝或析构次数不能当作程序逻辑。
相关推荐
D e re k ZH2 小时前
C++ 并发、未定义行为与工程化难点(No 3)
数据结构·c++·算法·面试
释厄6233 小时前
同性线套娃律——圆面积必然=1+2+...+n 的求和·圆心不与 π 相关
java·人工智能·算法·1024程序员节
暴力求解3 小时前
C++ ---继承(二)
开发语言·c++·算法
码林拾遗WJ3 小时前
告警分级的自动化实测:模型判断 vs 人工拍板
人工智能·算法·自动化
Tisfy3 小时前
LeetCode 0453.最小操作次数使数组元素相等:两次遍历(反向思维)
算法·leetcode·题解·遍历·反向思维
君万4 小时前
【LeetCode每日一题】回溯:46. 全排列 78. 子集 39. 组合总和
算法·leetcode·go
Keep_Trying_Go4 小时前
Consistent-Point半监督人群计数算法详解
人工智能·深度学习·算法·计算机视觉·人群计数
在所不辞兄5 小时前
【零基础学智能仿真-69】综合项目——材料参数驱动的仿真研究
人工智能·深度学习·神经网络·算法·机器学习·1024程序员节
凉茶钱5 小时前
红黑树:用“颜色”换“平衡”,set 与 map 的底层发动机
开发语言·c++·红黑树