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


🌟 个人主页噜噜大王

❄️ 个人专栏《C语言》 《C++》

🏆 道阻且长,行则将至


🎨噜噜大王的简介:


文章目录

  • 前言
  • 一、默认成员函数总览
  • [二、构造函数:对象的 "初始化器"](#二、构造函数:对象的 “初始化器”)
    • [1. 核心作用](#1. 核心作用)
    • [2. 五大核心特点](#2. 五大核心特点)
    • [3. 默认构造函数(3 种形式)](#3. 默认构造函数(3 种形式))
    • [4. 代码实例(Date 日期类)](#4. 代码实例(Date 日期类))
    • [5. 编译器默认构造的规则](#5. 编译器默认构造的规则)
  • [三、析构函数:对象的 "清洁工"](#三、析构函数:对象的 “清洁工”)
    • [1. 核心作用](#1. 核心作用)
    • [2. 六大核心特点](#2. 六大核心特点)
    • [3. 什么时候需要手动写析构?](#3. 什么时候需要手动写析构?)
    • [4. 代码实例(Stack 栈类)](#4. 代码实例(Stack 栈类))
  • [四、拷贝构造函数:对象的 "复制器"](#四、拷贝构造函数:对象的 “复制器”)
    • [1. 核心定义](#1. 核心定义)
    • 2.拷贝构造传值传参引发死循环
    • [3. 三大核心特点](#3. 三大核心特点)
    • [4. 浅拷贝 vs 深拷贝](#4. 浅拷贝 vs 深拷贝)
    • [5. 代码实例](#5. 代码实例)
  • 五、运算符重载
    • [1. 特点](#1. 特点)
    • [2. 类内重载函数顺序问题](#2. 类内重载函数顺序问题)
    • [3. 重载为全局的面临对象访问私有成员变量的问题](#3. 重载为全局的面临对象访问私有成员变量的问题)
  • [六、赋值运算符重载:对象的 "赋值器"](#六、赋值运算符重载:对象的 “赋值器”)
    • [1. 核心区别](#1. 核心区别)
    • [2. 四大核心特点](#2. 四大核心特点)
    • [3. 代码实例(Date 类赋值重载)](#3. 代码实例(Date 类赋值重载))
  • [七 、const成员函数](#七 、const成员函数)
  • [八、 默认成员函数核心规则](#八、 默认成员函数核心规则)
  • 总结

前言

在 C++ 面向对象编程中,类的默认成员函数 是核心基础,也是面试高频考点。编译器会为每个类默认生成 6 个成员函数,其中构造函数、析构函数、拷贝构造函数、赋值运算符重载最为关键。本文结合代码实例,系统拆解这四大核心默认成员函数,帮你彻底掌握 C++ 对象的初始化、销毁与拷贝逻辑。


一、默认成员函数总览

用户未显式实现时,编译器自动生成 6 个默认成员函数:

  1. 构造函数:初始化对象(可以理解为以前的初始化函数Init)
  2. 析构函数:清理对象资源(可以理解为以前的销毁函数destroy)
  3. 拷贝构造函数:用已有对象初始化新对象
  4. 赋值运算符重载:已有对象间赋值
  5. 普通取地址重载:获取对象地址
  6. const 取地址重载:获取 const 对象地址

重点学习前 4 个,后 2 个默认生成即可,无需手动实现。

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

1. 核心作用

构造函数不是创建对象,而是初始化对象 。对象的内存空间在栈帧 / 堆上分配时已开辟,构造函数负责给成员变量赋初始值,替代 C 语言中的Init初始化函数

2. 五大核心特点

  • 函数名与类名完全相同
  • 无返回值(不写void,C++ 语法规定)
  • 对象实例化时自动调用对应的构造函数。
  • 支持函数重载(无参、带参、全缺省)
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。

3. 默认构造函数(3 种形式)

不传参数即可调用的构造函数统称默认构造,3 种形式仅能存在一个:

  1. 无参构造函数
  2. 全缺省构造函数
  3. 编译器自动生成的无参构造

注意:调用默认构造函数仅需 类名 变量名 例如:Date a 而不是 Date a()

4. 代码实例(Date 日期类)

cpp 复制代码
#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); // 调用带参构造

    // 易错点:无参构造创建对象,不要加括号(会被识别为函数声明)
    // Date d3(); // 警告:未调用原型函数

    d1.Print(); // 输出:1/1/1
    d2.Print(); // 输出:2025/1/1
    return 0;
}

5. 编译器默认构造的规则

  • 内置类型(int/char/ 指针等):不初始化,值随机
  • 自定义类型(自己定义的类):调用其默认构造函数初始化

三、析构函数:对象的 "清洁工"

1. 核心作用

析构函数与构造函数功能相反,不是销毁对象本身,而是清理对象占用的资源(如malloc申请的堆内存、文件句柄等)。对象生命周期结束时(局部对象出作用域、堆对象delete),自动调用析构函数。

2. 六大核心特点

  • 函数名:~ 类名(加波浪线)
  • 无参数、无返回值
  • 不支持重载
  • ⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  • 对象销毁时自动调用
  • 默认析构:内置类型不处理,自定义类型调用其析构
  • 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
  • 析构顺序:后定义的对象先析构(在同一作用域中,不在的时候先析构局部,在析构全局)

3. 什么时候需要手动写析构?

✅ 需要手动写:类中申请了堆内存(malloc/new)、打开文件等,必须手动释放资源,否则内存泄漏

❌ 不用写:仅内置类型成员(如 Date 类)、无资源申请,默认析构足够

4. 代码实例(Stack 栈类)

cpp 复制代码
#include<iostream>
#include<cstdlib>
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 << "调用析构函数" << endl;
        free(_a);       // 释放资源
        _a = nullptr;
        _top = _capacity = 0;
    }

private:
    STDataType* _a; // 指向堆内存
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack st; // 构造:申请内存
    return 0; // 析构:释放内存,无泄漏
}

四、拷贝构造函数:对象的 "复制器"

1. 核心定义

拷贝构造是特殊的构造函数 ,用已存在的同类型对象初始化新对象。

  • 格式:类名(const 类名& 对象名)(最好带const)
  • 第一个参数必须是类类型引用(传值会无限递归)
  • 拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值。

2.拷贝构造传值传参引发死循环

C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成 。所以当你拷贝构造是传值的时候,会陷入一个死循环:拷贝构造--->传值--->拷贝构造--->...这种情况。

注:所以拷贝拷贝构造必须传引用

3. 三大核心特点

  • 是构造函数的重载
  • 参数必须是引用(最好加上const)(传值报错:无穷递归)
  • 默认拷贝:内置类型浅拷贝(字节复制),自定义类型调用其拷贝构造

注:看需不需要写拷贝构造就看这个类有没有实现析构函数

4. 浅拷贝 vs 深拷贝

浅拷贝:默认拷贝,仅复制成员值。若含指针成员,两个对象指向同一块堆内存,析构时重复释放导致程序崩溃

深拷贝:手动实现,重新申请堆内存,复制数据,两个对象独立,互不影响 (要自己手动实现)

5. 代码实例

(1)Date 类:默认拷贝足够(无指针)

cpp 复制代码
class Date
{
public:
    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, _month, _day;
};

int main()
{
    Date d1(2025, 1, 1);
    Date d2(d1); // 调用默认拷贝构造
    d2.Print();   // 输出:2025-1-1-1
    return 0;
}

(2)Stack 类:必须手动深拷贝(含指针)

cpp 复制代码
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        _capacity = n;
        _top = 0;
    }

    // 手动深拷贝构造
    Stack(const Stack& st)
    {
        // 重新申请内存
        _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
        memcpy(_a, st._a, sizeof(STDataType) * st._top); // 复制数据
        _top = st._top;
        _capacity = st._capacity;
    }

    ~Stack() { free(_a); }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack st1;
    Stack st2= st1; // 深拷贝,独立内存,析构不崩溃
    return 0;
}

注: stack st2(st1)和stack st2 = st1都是拷贝构造,不一样的写法

五、运算符重载

1. 特点

  • 重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数。
  • 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。
  • 重载时候函数参数必须有一个类型是自定义类型
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
  • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
  • 该五个运算符不能重载
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分
cpp 复制代码
Date& operator++()
{
cout << "前置++" << endl;
//...
return *this;
}

Date& operator++(int)
{
cout << "后置++" << endl;
//...
return *this;
}

2. 类内重载函数顺序问题

有些重载函数必须写成全局,不然不符合使用习惯

例如:重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

cpp 复制代码
class Date
{
	friend  ostream& operator<<( ostream& out, const Date& D);
public:
	Date(int a = 1, int b = 1, int c = 1)
	{
		_year = a;
		_month = b;
		_day = c;
	}
private:
	int _year;
	int _month;
	int _day;
};
 ostream& operator<<( ostream& out,const Date& D)
{
	out << D._year << "/" << D._month << "/" << D._day;
	return out;
}
int main()
{
	Date A;
	Date B;
	cout << A << endl << B;
	return 0;
}

3. 重载为全局的面临对象访问私有成员变量的问题

解决方案:

  1. 成员放公有
  2. Date提供getxxx函数
  3. 友元函数
  4. 重载为成员函数

六、赋值运算符重载:对象的 "赋值器"

1. 核心区别

  • 拷贝构造:创建新对象,用旧对象初始化(Date d2 = d1)
  • 赋值重载:两个已存在对象间赋值(d1 = d2)

2. 四大核心特点

  • 必须重载为成员函数 (重载为全局时候,类内会默认构造一个形成冲突)
  • 返回值:类名 &(d1=d2返回d1)
  • 参数建议:const 类名 &(避免传值拷贝)
  • 默认赋值:同默认拷贝,内置类型浅拷贝,自定义类型调用赋值重载
    注:和拷贝构造一样看需不需要写赋值重载就看这个类有没有实现析构函数

3. 代码实例(Date 类赋值重载)

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

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

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

int main()
{
    Date d1(2025, 1, 1);
    Date d2;
    d2 = d1; // 调用赋值重载
    d2.Print(); // 输出:2025-1-1
    return 0;
}

七 、const成员函数

  • 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
  • const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
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 Date* const this) const
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	// 这⾥非const对象也可以调用const成员函数是⼀种权限的缩小
	// Date * -> const Date *
	Date d1(2024, 7, 5);
	d1.Print();
	const Date d2(2024, 8, 5);
	d2.Print();
	return 0;
}

注:加上const 你this指针指向的对象就不可以修改了

八、 默认成员函数核心规则

函数 核心作用 手动实现场景 关键注意点
构造函数 初始化对象 需自定义初始值 默认构造 3 种形式,仅存一个
析构函数 清理资源 含堆内存 / 文件等资源 后定义先析构,无重载
拷贝构造 初始化新对象 含指针成员(需深拷贝) 参数必须是 const 引用
赋值重载 已存在对象赋值 含指针成员(需深拷贝) 避免自赋值,返回引用

总结

相关推荐
草莓啵啵~3 小时前
pywinauto-打开程序+连接已打开的程序
开发语言·python
Non-existent9874 小时前
海拔批量查询 + 批量 KML 生成工具-WPS 插件 TableGIS 新功能
javascript·c++·excel·wps
Ws_10 小时前
C#学习 Day2
开发语言·学习·c#
杰克尼10 小时前
天机学堂复习总结(day03-day04)
java·开发语言·redis·elasticsearch·spring cloud
咩咦11 小时前
C++学习笔记28:静态成员应用:不用循环求1到n的和
c++·学习笔记·类和对象·static·构造函数·oj·静态成员
x***r15111 小时前
jdk-11.0.16.1_windows使用步骤详解(附JDK 11环境变量配置与验证教程)
java·开发语言·windows
EllinY11 小时前
CF2217E Definitely Larger 题解
c++·笔记·算法·构造
筠筠喵呜喵12 小时前
Linux软件开发性能优化
linux·c++·性能优化
luck_bor12 小时前
File类&递归作业
java·开发语言