类和对象(中):对象生命周期与运算符重载

个人专栏:《数据结构初阶》《经典OJ题目》《C语言》《小白算法成长录》
欢迎大佬交流

本文代码已同步Github

一、默认成员函数

类里面的默认成员函数就是用户没有显示实现,编译器会自动生成的成员函数;

在一个类中,我们不手动实现成员函数的话,编译器会默认生成下面六个默认成员函数;

1、初始化和清理

  • 构造函数:完成初始化工作
  • 析构函数:完成清理工作

2、拷贝复制

  • 拷贝构造是使用同类对象初始化创建对象
  • 赋值重载主要是把一个对象赋值给另一个对象

3、取地址重载

主要是普通对象和const对象取地址

接下来,我们一个一个来看

#二、构造函数

1、构造函数的定义

构造函数是 C++ 类的一种特殊成员函数,在创建对象时自动调用,用于初始化对象的成员变量。

构造函数主要任务并不是开空间创建对象(局部对象通常在栈帧创建时空间就开好了),而是对象实例化时初始化对象

在此之前,我们创建变量都需要手动初始化变量;

如果忘记初始化就可能会导致结果错误;

而现在,构造函数会在创建对象时自动调用,直接避免了这种情况;

同时提高了效率!

2、构造函数的特点

a、基本特点

  • 函数名与类名相同,无返回值(不能写void
  • 对象实例化时会自动调用对应构造函数
  • 可以重载(多个构造函数,参数不同

我们先来看基本特点对应的代码要求

cpp 复制代码
class Date
{
public:
	//构造函数支持重载,通过参数进行匹配

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

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

	//3.全缺省构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}


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

思考一下,这三个构造函数能同时存在吗?

显然不能

当创建对象之后不传参,编译器就不知道该调用哪个函数,导致错误!

再仔细来看,全缺省构造函数已经覆盖所有情况了

  • 不传参 -> 等价于无参构造
  • 传参 -> 等价于有参构造

因此,推荐的默认构造函数就是全缺省构造函数


我们来看

cpp 复制代码
int main()
{
	//只保留全缺省构造函数
	Date d1;
	d1.Print();

	Date d2(2026);
	d2.Print();

	Date d3(2026,6);
	d3.Print();

	Date d4(2026, 6,8);
	d4.Print();

	return 0;
}
  • 一旦声明任何构造函数,编译器就不再生成默认构造函数

我们通过注释所有手动实现的构造函数来看默认构造函数

发现编译器默认生成的构造函数并没有对对象进行初始化


b、核心特点

  • 无参构造函数,全缺省构造函数,编译器自动生成的构造函数,都称为默认构造函数,三个函数只能存在一个

  • 无参构造函数和全缺省构造函数虽然构成函数重载,但调用时会存在歧义

    cpp 复制代码
    	//保留无参构造函数和全缺省构造函数
    	// error C2668: "Date::Date": 对重载函数的调用不明确
    	Date d;
    	d.Print();

  • 编译器默认生成的构造函数不确定是否对内置类型成员变量进行初始化

  • 对于自定义类型,会调用该成员变量的默认构造函数进行初始化,如果该成员函数没有默认构造函数,就会报错

我们以Stack来观察默认构造函数对于内置类型的处理

cpp 复制代码
#include <iostream>

typedef int STDateType;

using namespace std;

class Stack
{
public:
	
	//全缺省构造函数
	Stack(int n = 4)
	{
		_a = (STDateType*)malloc(sizeof(STDateType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed!\n");
			exit(1);
		}

		_top = 0;
		_capacity = n;
	}


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

int main()
{
	Stack st1;

	Stack st2;

	return 0;
}

此时编译器调用默认构造函数(全缺省构造函数)进行初始化

现在来看如果我们不写,默认构造函数是否会对变量进行初始化

发现编译器自动生成的默认构造函数并没有对变量进行初始化!


下面我们以两个栈实现队列来观察默认构造函数对于自定义类型的处理

cpp 复制代码
#include <iostream>

typedef int STDateType;

using namespace std;

class Stack
{
public:
	
	//如果不写Stack的默认构造函数就会导致在MyQueue中创建对象时报错
	//全缺省构造函数
	Stack(int n = 4)
	{
		_a = (STDateType*)malloc(sizeof(STDateType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed!\n");
			exit(1);
		}

		_top = 0;
		_capacity = n;
	}


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


class MyQueue
{
private:
	Stack st1;
	Stack st2;

};


int main()
{
	MyQueue mq;

	return 0;
}

下面我们通过监视来看是否进行了初始化

发现编译器默认生成的MyQueue的构造函数调用了Stack的构造,完成了初始化

总结:大多数情况,构造函数都需要我们自己去实现,少数情况类似MyQueueStack有默认构造时,MyQueue自动生成的默认构造函数即可够用

三、析构函数

1、析构函数的定义

析构函数是 C++ 类的一种特殊成员函数,在对象生命周期结束时自动调用,用于释放资源。

析构函数与构造函数功能相反,析构函数并不是完成对对象本身的销毁(比如局部对象是存在栈帧的,函数结束后栈帧销毁,就直接释放了),而析构函数是完成对象中的资源清理工作

同时,析构函数和构造函数类似,均会自动调用,无需担心忘记调用造成的内存泄漏

2、析构函数的特点

a、基本特点

  • 函数名是在类名前加~,无参无返回值(不需要void
  • 一个类只能有一个析构函数,若未显示定义,编译器会自动生成默认的析构函数
  • 对象生命周期结束时,系统会自动动调用析构函数

同样先来看基本特点对应的代码要求

cpp 复制代码
class Stack
{
public:
	
	//全缺省构造函数
	Stack(int n = 4)
	{
		_a = (STDateType*)malloc(sizeof(STDateType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed!\n");
			exit(1);
		}

		_top = 0;
		_capacity = n;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;
		
		free(_a);
		_a = nullptr;

		_top = 0;
		_capacity = 0;
	}


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

实现之后,我们来看对象生命周期结束时是否会调用析构函数

只需观察运行结果即可,我们在析构函数中增加了打印

确实调用了析构函数;

那如果没有手动实现析构函数,那么是否会自动调用呢?

我们来看下面例子

发现好像也没有调用,但其实这是因为默认生成的构造函数对内置类型不做处理!

和构造函数类似,编译器默认生成的构造和函数和析构函数对自定义类型都不做处理


b、核心特点

  • 编译器自动生成的析构函数对内置类型成员不做处理,自定义类型会调用他的析构函数
  • 对于自定义类型,即使手动实现析构函数,也会调用他的析构函数
  • 如果类中没有申请资源,析构函数就可以不写,直接使用编译器生成的默认析构函数即可
  • 如果编译器生成的默认析构函数可以满足需求,也不需要手动实现
  • 对于有资源申请的类,一定要写析构函数,否则就会造成资源泄露(编译器不会报错)

通过下面例子来了解

首先来看对于自定义类型的处理

此时mq调用了st1st2的构造函数完成初始化

继续向后走

发现在最后程序结束之前调用了编译器默认生成的析构函数,即调用st1 st2的析构函数,完成了资源释放


那么先析构的是 st1 还是 st2 呢?

答案是 st2

因为栈区符合就像数据结构的栈一样,符合后进先出的原则,因此后来的元素会成为栈顶元素,就会先析构


接下来我们试一下显示实现析构函数会怎么样

发现对于自定义类型,即使手动实现析构函数,编译器也会调用内置类型的析构函数,防止内存泄露!

总结:对于内置类型,有资源申请就需要手动实现析构函数,没有资源申请则不需要;

对于自定义类型,即使手动实现析构函数,编译器仍会调用成员变量的析构函数

四、拷贝构造函数

1、拷贝构造函数的定义

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数成为拷贝构造函数 ,即拷贝构造函数是构造函数的重载

2、拷贝构造函数的特点

a、基本特点

  • 拷贝构造函数是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是类类型对象的引用,传值方式调用直接报错,语法逻辑上会引发无穷递归
  • 拷贝构造函数可以有多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须要有缺省值
  • 自定义类型对象进行拷贝就会调用拷贝构造

先来定义一个日期类的拷贝构造函数体会定义

cpp 复制代码
class Date
{
public:
	//全缺省构造函数
	Date(int year = 1, 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;
	}

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

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

日期类是不需要实现析构函数的,因为没有资源申请,无需释放


接着我们采用传值调用的方式看看会发生什么

cpp 复制代码
//传值调用拷贝构造函数
//"Date": 非法的复制构造函数: 第一个参数不应是"Date"
Date(Date d)
{
    _year = d._year;
    _month = d._month;
    _day = _day;
}

为什么一定要传引用呢?

因为在语法逻辑上,传值调用会引发无穷递归

以日期类为例

cpp 复制代码
int main()
{
	Date d1(2026, 6, 9);
	d1.print();

	Date d2(d1);

	return 0;
}

如果传值调用,第一步会向先调用拷贝构造函数,将d1拷贝给d2

第二步,进入拷贝构造函数之后,会把d1拷贝给参数 d ,此时又会调用拷贝构造函数;

第三步,进入拷贝构造函数之后,会把d拷贝给新的d,就这样一直重复下去;

最终导致栈溢出,程序崩溃!


下面我们来看自定义类型进行拷贝是否会调用拷贝构造函数

当程序运行到断点处时,下一步就直接进入了拷贝构造函数;

因此,对于自定义类型对象,进行拷贝就会调用拷贝构造函数

b、核心特点

  • 若未显式实现拷贝构造函数,编译器会自动生成拷贝构造函数;自动生成的拷贝构造函数对内置类型会完成值拷贝(浅拷贝),即一个字节一个字节拷贝;对于自定义类型成员变量则会调用他的拷贝构造函数

对于日期类而言,成员变量均是内置类型且没有申请空间,编译器自动生成的拷贝构造函数就可以完成拷贝**(浅拷贝)** 不需要我们手动实现拷贝构造函数

而对于Stack 的类而言,虽然成员变量是内置类型,但由于有空间申请,编译器自动生成的拷贝构造函数就不满足需求,因此需要实现深拷贝 (对指向的资源也进行拷贝)

到底为什么需要实现深拷贝呢?怎么就不满足需求了?

我们通过代码来分析

首先来看编译器默认生成的拷贝构造函数对成员变量的值拷贝

由于是值拷贝,因此st1 st2_a 指向的是是一块空间!

首先会造成的影响就是无法正常Push Pop

因为st1 中数据的修改影响了 st2 的数据,但其中的 _top _capacity 却无法及时更新,导致数据结构完全被破坏

其次,在最后的资源清理时,st1调用完析构函数之后,st2同样也会调用析构函数;

两次释放的都是一块空间,导致未定义行为


显然,值拷贝无法满足我们的需求,因此需要手动实现深拷贝

深拷贝就是对申请的空间资源进行拷贝

cpp 复制代码
	//深拷贝
	Stack(const Stack& st)
	{
		_a = (STDateType*)malloc(sizeof(int) * st._top);
		if (_a == nullptr)
		{
			perror("malloc failed!\n");
			exit(1);
		}

		memcpy(_a, st._a, sizeof(STDateType) * st._top);

		_top = st._top;
		_capacity = st._capacity;
	}

通过深拷贝,我们再来观察两个成员变量是否指向了不同的空间

此时,st1 st2 中的 _a 已经分别指向两块不同的空间,成功完成了对类对象的拷贝


那么对于像MyQueue这样的类,内部是自定义类型成员,编译器自动生成的拷贝构造函数会调用Stack 的拷贝构造函数,无需再手动实现拷贝构造函数


  • 传值返回会产生一个临时对象调用拷贝构造函数;传引用返回,返回的就是引用,没有产生拷贝

需要注意的是,如果返回对象是一个当前函数局部域的局部对象,函数结束后对象就被销毁了,而此时就不能使用传引用返回,就像返回野指针一样

传引用返回可以减少拷贝,但是要确保返回对象在当前函数结束后仍存在!

我们通过下面例子来体会:

cpp 复制代码
// warning C4172: 返回局部变量的地址或临时 : st
Stack& func()
{
	Stack st;
	return st;
}

因此,一定要确保返回对象在当前函数结束后仍旧存在

五、赋值运算符重载

1、运算符重载

a、基本特点

  • 运算符重载是具有特殊名字的函数,函数名由operator 和 要重载的运算符构成,有返回值和参数列表及函数
    类类型对象使用运算符时,必须转换成调用对应的运算符重载,没有对应的运算符重载就会报错
  • 重载运算符的参数个数和该运算符作用的运算对象一样多
  • 如果一个重载运算符函数是成员函数,那么第一个参数就是隐式的this指针,因此参数会比运算对象少一个
  • 运算符重载以后,其优先级和结合性对应的内置类型运算符保持一致

我们通过下面例子来理解

cpp 复制代码
//全局运算符重载函数
bool operator==(Date d1,Date d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

由于我们日期类中成员变量为私有,因此在全局运算符重载函数中,无法直接访问

该怎么解决呢?

首先考虑的就是直接把类成员变量设为public ,但代价太大

其次就是直接把运算符重载为类的成员函数,同时省掉一个参数;

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;
 	}
	
	//重载为成员函数
	bool operator==(Date d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

这样完美符合了需求;

重载之后该怎么调用呢?

cpp 复制代码
int main()
{
	Date d1(2026,6,10);
	d1.print();

	Date d2(d1);

	//可以显示调用
	d1.operator==(d2);

	d1 == d2;

	return 0;
}

b、核心特点

  • 不能重载语法中没有操作符
  • .*::sizeof?: . 这五个运算符不能进行重载
  • 运算符重载函数至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
  • 重载++运算符时,有前置++和后置++,而函数名都是operator++ ,区分方式是重载后置++时,增加一个形参int

通过下面例子来体会:

cpp 复制代码
	//重载前置++
	Date& operator++()
	{
		cout << "前置++" << endl;
		_day++;
		return *this;
	}

	//重载后置++
	Date operator++(int)
	{
		//返回自增前的值再进行++
		cout << "后置++" << endl;
		Date tmp = (*this);

		//复用前置++
		++(*this);
		
		//函数结束后tmp就被销毁,因此不能返回引用
		return tmp;
	}

下面进行调用

cpp 复制代码
int main()
{
	Date d1(2026, 6, 10);
	Date d2(2026, 6, 11);

	cout << d1.operator==(d2) << endl;
	
	d1 == d2;//转换成 d1.operator==(d2)


	d1++;
	d1.print();

	++d2;
	d2.print();

	return 0;
}

重载的后置++中复用了前置++的代码,因此会打印出前置++;

2、赋值运算符重载

I、赋值运算符重载的定义

赋值运算符重载是一个默认成员函数,用于完成两个已存在对象的直接拷贝赋值;

与构造函数区分:构造函数用于一个对象拷贝初始化给另一个要创建的对象

II、赋值运算符重载的特点

  • 赋值运算符重载是一个运算符重载,必须重载为成员函数;参数建议使用const修饰的引用,减少拷贝;
  • 返回值建议写成当前类类型的引用,引用返回同样减少拷贝;有返回值是为了支持连续赋值场景
  • 没有显示实现时,编译器会自动生成默认赋值运算符重载函数;对内置类型完成值拷贝,对自定义类型调用他的赋值重载函数

我们通过例子来理解

首先来看正确的赋值重载运算符的实现

cpp 复制代码
	//赋值运算符重载
	Date& operator=(const Date& d)
	{
		//d1 = d2
		//返回的是d1即this指针

		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;		
	}

接着进行调用

cpp 复制代码
int main()
{
	Date d1(2026, 6, 10);
	Date d2(2026, 6, 6);

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

	//赋值重载
	d1 = d2;



	return 0;
}

在手动实现赋值重载运算符之后,完成了赋值任务;


下面我们来看编译器默认生产的复制重载运算符函数能否实现值拷贝

可以看出,编译器默认生产的赋值重载运算符完成了值拷贝;


总结一下:对于像Date 这样全是内置类型且没有空间申请的类,编译器自动生成的赋值运算符重载(值拷贝)就可以满足需求;

对于像 Stack这样有资源申请的类,就需要手动实现赋值运算符重载完成深拷贝;

而像MyQueue这样成员变量是自定义类型的类,编译器默认生成的赋值运算符重载会调用自定义类型成员变量的赋值运算符重载;也不需要手动实现

六、取地址运算符重载

1、const成员函数

定义:const修饰的成员函数称为const成员函数,const修饰成员函数放到成员函数参数列表的后面;

const实际上修饰的是隐含的this指针,表明不能对类的任何成员进行修改

我们以DatePrint函数为例进行解析

cpp 复制代码
class Date
{
public:
	//全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;

		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

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

int main()
{
    Date d(2026,6,10);
    d.Print();

	return 0;
}

在没有用const修饰之前,Print传给this的是&d,而this指针本身是 Date* const this,表明this本身不能被修改,但是在Print函数中, 可以修改成员变量;

显然,我们是绝对不希望在Print函数中修改成员变量的;

那么此时就引出了const,只需在Date* const this指针之前再加上一个const,这样就既保证this指针本身不被修改,同时成员变量也无法被修改;

但是,由于this指针无法在函数参数位置出现,因此不能直接在前面加this

最终,引出了在函数名后面加上const这种写法

2、取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载;

一般情况下,编译器自动生成的就满足需求,无需手动实现;


但是想要返回其他地址改怎么办?

那就手动实现

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

	const Date* operator&()const
	{
		//return this;
		return nullptr;
	}
private:
	int _year; 
	int _month;
	int _day;
};

就像上述例子一样,当取地址时,返回的是空指针!

如果觉得有帮助,可以关注Github项目持续更新

相关推荐
凡人叶枫1 小时前
Effective C++ 条款13:以对象管理资源(RAII)
java·linux·开发语言·c++·嵌入式开发
星恒随风1 小时前
C++ 类和对象入门(六):友元、内部类、匿名对象和编译器优化
开发语言·c++·笔记·学习·状态模式
Irissgwe1 小时前
C++ STL 详解:stack 和 queue 的介绍使用与模拟实现
c++·stl·queue·stack
油炸自行车1 小时前
【bug】Qt 6 Q_NAMESPACE 跨 DLL 链接错误:LNK2019 无法解析 staticMetaObject
数据库·c++·qt·bug·link2019·q_namespace_exp·namespaceexport
插件开发1 小时前
英伟达cuda程序通用性关键 geforce 20xx代到最新版 在20xx上编译的c++程序可以通用吗?
java·c++·人工智能
BestOrNothing_20151 小时前
ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信保姆级教程
c++·service通信·ros2·client·server·srv
KuaCpp1 小时前
C++进阶(上)
linux·c++
草莓熊Lotso2 小时前
【Linux网络】深入理解 TCP 协议(一):报头设计与可靠性基石
linux·运维·服务器·c语言·网络·c++·tcp/ip
加油码2 小时前
Linux 信号详解:从 Ctrl+C 到进程异常退出,真正理解信号机制
linux·服务器·c++