类和对象(三)

我们紧接上回,继续深入讲解类的默认成员函数。在 C++ 中,编译器会在特定条件下自动生成这些成员函数,它们虽不显式出现在代码里,却在对象的构造、拷贝、赋值与析构等关键生命周期环节中扮演着不可或缺的角色。理解它们的生成规则与行为,是编写高效、安全 C++ 代码的重要基础。

拷贝构造

拷贝构造函数是类的默认成员函数之一,用于以已存在的同类对象为蓝本,创建并初始化一个新对象。

例如 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;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们用 Date 类实例化出对象 d1d2,然后用 d1 去初始化 d2,是这样写的:

cpp 复制代码
int main()
{
	Date d1(2026, 9, 13);//创建一个实例化对象
	Date d2(d1);//用已经初始化的对象d1去初始化d2
	return 0;
}

为了增强代码的可读性,将 d1 拷贝给 d2 的操作也可以这样写:Date d2 = d1,和 Date d2(d1) 是一个意思。

拷贝构造函数如何写呢?它是构造函数的一个重载形式,第一个参数必须是类类型的引用,后面也可以跟多个参数,但这些参数必须设置缺省值。

我们 Date 类的拷贝构造函数就可以这么写:

cpp 复制代码
//拷贝构造函数
Date(Date& d)//第一个参数必须是类类型引用
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

至于为什么第一个参数必须是类类型引用而非单纯的类类型呢?这个问题就要涉及到函数的传值传参了。

1. 为什么第一个参数必须是类类型引用

例如,我们定义一个 Func 函数,它的函数参数为 Date 类型。

cpp 复制代码
void Func(Date d)
{
	;
}

我们调用该函数时,形参 d 会拷贝传过来的 Date 类对象的内容,这个过程中就会调用拷贝构造函数。因此我们调用 Func 函数,首先进入的其实是拷贝构造函数,拷贝完成后才会进入 Func 内部的函数体。

所以如果我们的拷贝构造函数第一个参数不是引用的话就会出现大问题,造成无穷递归

2. 浅拷贝与深拷贝

浅拷贝可以简单理解为值的拷贝,上面我们写的 Date 类的拷贝构造函数就是一个浅拷贝。

当我们没有显式编写拷贝构造函数时,编译器会自动生成一个拷贝构造函数。对于内置类型,它会完成值拷贝(浅拷贝);对于自定义类型,则会调用对应的拷贝构造函数完成拷贝。

我们来看一个典型场景:

cpp 复制代码
class Stack
{
public:
	Stack(int n = 4)
	{
		_array = (int*)malloc(sizeof(int) * n);
		_Top = 0;
		_capacity = n;
	}
	
	~Stack()
	{
		if (_array!=NULL)
			free(_array);
		_array==NULL;
		_Top = 0;
		_capacity = 0;
	}
private:
	int* _array;
	int _Top;
	int _capacity;
};
};

因为我们手动申请了空间,所以要显式编写析构函数去释放。这里我们没有编写拷贝构造函数,就会出问题。

cpp 复制代码
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

也就是在析构函数的时候出现了问题。

因为编译器自动生成的拷贝构造函数只进行浅拷贝,指针 _array 也是,这导致了一个问题,st1st2 中的指针 _array 指向了同一块内存空间。

每个对象在销毁时都要调用析构函数,这导致这块内存空间被重复释放了两次,程序直接崩溃了。

编译器自动生成的拷贝构造函数不能满足我们的需求,所以我们需要显式地定义拷贝构造函数来解决浅拷贝问题。

cpp 复制代码
Stack(Stack& st)
{
	if (st._array != NULL)
	{
		//申请和st一样大的空间
		_array = (int*)malloc(sizeof(int) * st._capacity);
		if (_array == NULL)
		{
			perror("malloc");
			exit(-1);
		}
		//将内容按字节拷贝
		memcpy(_array, st._array, sizeof(int) * _capacity);

		_Top = st._Top;
		_capacity = st._capacity;
	}
}

这样 st1 内容拷贝给 st2 的时候,两个对象的成员变量 _array 指向的就是两块一样大小且不同的空间了,在析构时不会产生冲突。

一般来说,需要显式定义析构函数的类也需要显式定义拷贝构造函数。

运算符重载

运算符重载允许我们为自定义类型重新定义运算符的行为,使对象能够像内置类型一样使用 +-==<< 等运算符进行运算和操作。它本质上是编写一个名为 operator 后跟运算符符号的特殊成员函数。合理使用运算符重载可以让代码更直观、更符合直觉,但也要遵循运算符原有的语义和优先级,避免滥用导致可读性下降。

运算符重载函数模板如下:

cpp 复制代码
//函数名为关键字operator + 要重载的运算符
Date operator+(Date& d, int day)
{
	;
}

可以看到,运算符重载是具有返回值类型的,但在使用它时还有一些注意事项:

  • 1.运算符重载的参数列表中必须有一个类类型的参数。
  • 2.C++规定有五个运算符不能重载((.* )( : ? )(sizeof( ))(.)(::) )
  • 运算符重载的参数个数与运算符的操作数个数相同,例如二元运算符有两个参数,一元运算符就有一个。
  • 默认第一个参数作为左操作数,第二个参数作为右操作数。
  • 运算符重载为成员函数的时候,因为成员函数隐含地有 this 指针指向一个类对象且是参数列表第一个参数,所以参数个数要减一。

比如说 Date 类,重载 += 运算符为它的成员函数和全局函数是这样的:

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month)
	{
		int MonthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0 && year % 100) || year % 400 == 0))
			return 29;
		return MonthDay[month];
	}
	//函数声明
	Date& operator+=(int day);
	
private:
	int _year;
	int _month;
	int _day;
};

//+=运算符重载
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

这里将 += 运算符重载写成全局函数 Date& operator+=(Date& d, int day) 就有问题了,因为 _year_month_day 都是类的私有成员,外部是无法直接访问到的,对此我们有三种解决办法:

  1. 使用 Get 函数
  2. 声明为类的友元函数
  3. 定义成类的成员函数

这里我们先声明为类的友元函数,友元函数就是在函数前加关键字 friend,在类里声明,该函数就可以访问类里的私有成员了。

friend Date& operator+=(Date& d, int day)

cpp 复制代码
Date& operator+=(Date& d, int day)
{
	d._day += day;
	while (d._day > d.GetMonthDay(d._year, d._month))
	{
		d._day -= d.GetMonthDay(d._year, d._month);
		d._month++;
		if (d._month == 13)
		{
			d._year++;
			d._month = 1;
		}
	}
	return d;
}

调用该函数方法如下:

cpp 复制代码
int main()
{
	Date d1(2026, 9, 13);
	d1 += 30;//调用运算符重载函数
	return 0;
}

也可以显式地调用运算符重载函数。

cpp 复制代码
d1.operator(d1,30);//显示调用运算符重载

区分前置、后置 ++/--

对于 ++-- 运算符来说,有前置和后置之分,那么编译器是如何区分的呢?

C++ 规定,后置版本比前置版本多一个 int 类型的参数。

cpp 复制代码
//前置++
Date& operator++(Date& d);
//后置++
Date operator++(Date& d, int);
//前置--
Date& operator--(Date& d);
//后置--
Date operator--(Date& d, int);

<</>>运算符重载

对于 <<>> 运算符重载,<< 是流插入符,cout 作为固定的左操作数配合使用,coutostream 类的对象,所以参数中有一个是 ostream 类型,然后返回 ostream 类型的引用,重载为 Date 类的成员函数是这样的:

cpp 复制代码
ostream& operator<<(ostream& out)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
	return out;
}

相应地,>> 运算符重载也是如此:

cpp 复制代码
istream& operator>>(istream& in)
{
	cout << "请输入年月日:";
	in >> _year >> _month >> _day;
	return in;
}

但是我们之前提到过,运算符重载函数默认第一个参数为左操作数,第二个参数为右操作数,成员函数隐含的 this 指针是放在第一个参数位置的,这就导致我们调用这两个运算符重载函数时要这样写:

cpp 复制代码
int main()
{
Date d2;
//调用运算符重载函数
d2>>cin;
//d2.operator>>(cin);
d2<<cout;
//d2.operator<<(cout);
return 0;
}

这跟我们平时写的完全颠倒了,看起来很不习惯,为了统一代码规范,我们通常将这两个运算符重载为全局函数,将 ostream/istream 类型的参数放在第一个位置。

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

istream& operator>>(istream& in,Date& d)
{
	cout << "请输入年月日:";
	in >> d._year >> d._month >> d._day;
	return in;
}
cpp 复制代码
int main()
{
	Date d1(2026, 9, 13);
	cin >> d1;
	cout << d1;
	return 0;
}

赋值运算符重载

赋值运算符是类的六大默认成员函数之一,当我们没有显式定义时,编译器生成的函数会完成浅拷贝操作,也会出现与拷贝构造函数相同的空间问题。

通常,赋值运算符是支持连续赋值的,所以我们需要把表达式的值返回,Date 类的赋值运算符重载是这样写的:

cpp 复制代码
//赋值运算符重载
Date operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}
cpp 复制代码
int main()
{
	Date d1(2026, 9, 13);
	Date d2(d1);
	Date d3;
	//连续赋值
	d3 = d2 = d1;
	//d3.operator=(d2.operator=(d1));
	return 0;
}

一般情况下,需要显式编写析构函数的类,也需要显式编写赋值运算符重载函数。

取地址符重载

取地址符重载函数和const取地址符重载函数都是类的默认成员函数,我们不显式编写时,编译器也会自动生成,完成返回对象地址的功能。

一般不需要我们去显式编写,除非不想让别人获取这个对象的地址,或者恶意返回一个伪造的地址。

cpp 复制代码
//取地址符重载
Date* operator&()
{
	return this;
}
//const取地址符重载
const Date* operator&() const
{
	return this;
}

这个后面跟的 const 是什么意思,我们下面会讲到。而且这两个函数隐含的 this 指针类型不同,所以构成函数重载。

const 成员函数

const 成员函数的意思就是将成员函数的 this 指针类型由 类类型 * const 变为 const 类类型 * const

例如,Date 类的 const 成员函数就是把 this 指针类型由 Date * const 变为 const Date * const

当我们创建一个 Date 类对象,然后被 const 修饰时,这时我们调用 Print 函数就会出现错误。

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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	const Date d1(2026, 9, 13);
	d1.Print();
	return 0;
}

这个问题本质上就是权限的放大,我们用 const 修饰这个对象,是不希望内容被修改,而 Print 函数内部隐含的 this 指针类型为 Date * const,是允许修改对象内容的。

所以我们需要把this指针的类型改为const Date * const,这样才不会产生权限放大的问题。

将其变为const成员函数的操作是,在函数参数列表后加const,声明和定义都要加。

cpp 复制代码
void Print()const
{
	cout << _year << '/' << _month << '/' << _day;
}

因此,为了支持更多操作,对于不修改对象内容的成员函数,我们都可以将其写成const函数。

总结

到这里,类的六大默认成员函数就全部讲完了,它们分别是:

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值运算符重载
  • 取地址符重载(含 const 版本)

其中,构造函数、拷贝构造函数、析构函数、赋值运算符重载这四个,编译器会在特定条件下自动生成,但自动生成的版本往往只进行浅拷贝,对于需要动态申请资源的类,我们必须手动实现深拷贝,否则就会出现内存重复释放、程序崩溃等问题。

而取地址符重载和 const 成员函数,一般不需要我们手动编写,编译器自动生成的版本已经能够满足绝大多数需求,除非有特殊的安全需求。

一句话总结:默认成员函数是 C++ 类的基础设施,理解它们的生成规则与行为,是编写高效、安全 C++ 代码的关键。

相关推荐
用户8181870627461 小时前
第25章 Redis集群脑裂实录:一次真实故障复盘
java·后端
运行时异常1 小时前
【WMS 仓储系统集成 AI Agent 实战】第 6 讲:RAG 知识库——“输出简单流程“为什么检索不到任何东西
java
夜不会漫长1 小时前
C++:类和对象(1)
java·开发语言·c++
czt_java1 小时前
Java文件IO核心知识点总结
java·开发语言
EatFan1 小时前
从单体到模块化:我的 Spring Boot 项目为什么拆成 framework、module、server?
java·spring boot·后端
szarron1 小时前
HTOOL‑SA12 + HT06 近场探头组合实操|手持设备工位 EMI 预兼容排查完整流程
运维·服务器·开发语言·网络·射频工程·频谱仪
菜鸟~noob2331 小时前
【电子战】第07篇:多普勒测向【含matlab代码】
android·开发语言·matlab
Java内核笔记1 小时前
Spring Boot 4 可观测性源码剖析:OpenTelemetry 全链路打通日志、指标、追踪
java·后端
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之跨域处理
java·开发语言·spring