探索C++对象模型:(拷贝构造、运算符重载)成员函数的深度解读(中篇)

发布于:2025-05-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

 

 

前引:在C++的面向对象编程中,对象模型是理解语言行为的核心。无论是类的成员函数如何访问数据,还是资源管理如何自动化,其底层机制均围绕两个关键概念展开:拷贝复制、取地址重载成员函数。它们如同对象的“隐形守护者”,默默支撑着代码的健壮性与效率。本文将从技术底层出发,结合内存布局、编译器行为与实际案例,深入探讨 ~

目录

拷贝构造函数

语法讲解

拷贝构造函数写法

特点讲解(2)

特点讲解(3)

注意事项

运算符重载

函数引入

语法讲解

函数名的使用

成员函数

成员函数参数

拷贝构造函数与赋值运算重载函数对比

赋值运算重载多参的赋值顺序

默认调用运算符重载函数


拷贝构造函数

作用:用同类型的另一个对象初始化当前对象

特点(下面会根据编号对每个特点进行详细解释!):

           (1)拷贝构造函数是构造函数的一个重载形式

           (2)函数的参数只有一个,且必须是类类型对象的引用,使用传值将会报错,无限递归

           (3)若未显示定义,编译器会生成默认的拷贝构造函数(下面细说)

语法讲解
拷贝构造函数写法
class Mystruct
{
public:
 
	//构造函数
	Mystruct(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

按照构造函数的特点:

(1)它属于构造函数的重载形式,因此函数名相同,没有返回值(不用写)

(2)它的参数只有一个,类类型对象的引用(即拷贝对象的引用)

int main()
{
	//生成St1对象
	Mystruct St1(1111, 11, 11);
	//拷贝构造生成St2对象
	Mystruct St2(St1);
 
	return 0;
}

这样我们就使用拷贝构造函数生成了与 St1 一模一样的对象:St2

但是这是拷贝,万一在使用拷贝构造函数时对模版误操作了,这样就得不偿失了,所以加上const

下面是完整拷贝构造函数的写法,功能自定义:

特点讲解(2)

我们怎么理解?

函数的参数必须是类类型对象的引用,使用传值将会报错,无限递归

这必须先知道:

拷贝的过程是先传参再对对象实现拷贝,这里就需要对类型拷贝的方式进行区分

(1)如果是内置类型:通过字节方式完成对实参的拷贝(也就是:形参是实参一份拷贝)

(2)如果是自定义类型:编译器会先调用拷贝函数实现拷贝,再去调用函数

例如:如果是内置类型直接拷贝

 例如:如果是自定义类型先调用拷贝构造函数实现拷贝,再去调用函数

因此,会出现一个行为:如果是自定义类型,按照形参是实参的一份拷贝,就会一直调用拷贝函数

 

解决办法是两种:(1)使用指针,直接传地址(2)使用引用,使用别名

所以上面我们使用引用和指针都是可以的额,但是引用更加的方便,这就是使用引用的原因!

特点讲解(3)

(1)当自定义了拷贝构造会调用自己设置的拷贝构造函数

(2)当没有自定义的拷贝构造,编译器会调用自己的拷贝构造

下面我们主要来看看调用编译器自己的拷贝构造会发生什么?

我们看到:当全是自定义类型时,会发生浅拷贝,也就是将内置类型的变量全部拷贝给另一个对象

那么如果既有内置类型又有自定义类型呢? 

我们可以看到:编译器还是会发生浅拷贝,内置类型、自定义类型都发生浅拷贝,且这个浅拷                                 贝连开辟空间的地址也一起拷贝过去两个对象的自定义类型成员共用一个空间 

那么如果后面再调用析构函数,对一个空间释放两次,这是何其危险的事情!所以我们得出结论:

(1)如果只有内置类型:可以借助编译器自己的拷贝构造完成

(2)如果有自定义类型,建议自己实现它的功能

(3)编译器自己的拷贝构造函数对自定义、内置类型都会操作

注意事项

(1)拷贝构造其实传的是两个参数,一个是 this 指针指向,一个是拷贝对象

(2)在类里面不受到访问限定符的限制,类的结构只是声明(相当于地图,对象是建筑的房子)

(3)如何利用拷贝构造函数对不同的对象进行不同的拷贝?

在上面我们已经看见了编译器调用自己拷贝构造的严重隐患!可以参考下面的设计!

例如将原有的 St1 数据拷贝给 St2 ,再在后面给 St2 增加数据(注意隐藏的 this 指针指向):

//生成St1对象
Mystruct St1(1111, 11, 11);
//拷贝构造
Mystruct St2(St1);

(4)注意调用拷贝构造时,不再调用构造函数,有开辟空间的自定义类型需要在拷贝构造完成 

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似

函数名字:关键字 operator 后面接需要重载的运算符符号

函数原型:返回值类型 operator 操作符(参数列表)

函数引入

下面是一个日期类,为了方便,我们先把“私有”保护注释掉!

class Myclass
{
public:
 
	Myclass(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
 
//private:
	int _year;
	int _month;
	int _day;
};

下面我们来写一个函数,用来比较日期:

Myclass St1(2025, 11, 11);
Myclass St2(2024, 11, 11);
 
//比较日期
cout << Compare(St1, St2) << endl;
bool Compare(const Myclass St1, const Myclass St2)
{
	if (St1._year < St2._year)
	{
		return false;
	}
	if (St1._year == St2._year && St1._month < St2._month)
	{
		return false;
	}
	if (St1._year == St2._year && St1._month == St2._month && St1._day < St2._day)
	{
		return false;
	}
	return true;
}

上面是一种普通的写法,今天我们学的运算符重载函数,可以简化函数名的表述!例如: 

语法讲解
函数名的使用

使用此特殊的成员函数,就是将原有的函数名换成关键字:operator + 重载的运算符符号

需要注意有5个运算符不支持重载(谨记):     .*      ::      sizeof      ?:       .

函数名和函数调用其实是一起改变的,只是函数调用可以缩写,只是第一种更直观。例如:

//二者等价
St1 < St2;
operator<(St1, St2);
成员函数

我们上面是在类外调用此函数的,如果我们换到类内会发生什么?

上面显示函数参数太多,我们只传了两个比较对象,为什么会发生报错?

 注意我们成员函数里面有一个隐藏的 this 指针,上面很明显它是没有指向的

this指针类的非静态成员函数中隐藏的指针,用于指向调用该函数的对象实例

                 谁调用成员函数,this 指针就指向谁 

 所以我们要将一个类对象交给 this 指针,那么函数的参数、函数调用都会发生变化,例如:

St1.operator<(St2);

这里是 St1 调用这个成员函数,所以 this 指针指向的 St1 

bool  operator<(const Myclass St2)
{
	if (_year < St2._year)
	{
		return false;
	}
	if (_year == St2._year && _month < St2._month)
	{
		return false;
	}
	if (_year == St2._year && _month == St2._month && _day < St2._day)
	{
		return false;
	}
	return true;
}
成员函数参数

重载操作符必须有一个类类型参数

如果单单靠 this 指针是会报错的,这点大家记住即可!

拷贝构造函数与赋值运算重载函数对比

构造函数的实质:用一个已经存在实例化的对象去初始化+创造另一个实例化对象

运算重载函数实质:已经存在的两个对象之间的运算重载操作

例如:

	St1 < St2;//运算符重载函数
	Myclass St3(St2);//拷贝构造函数
赋值运算重载多参的赋值顺序

我们先来看看什么是多实参的比较:

int a = 10;
int b = 50;
//两个变量
a = b;

这是将 b 赋值给 a,注意先后顺序。如果是多个变量呢?

int c, d, e;
e = d = c = a;

先将 a 赋值给 c,再将 c 赋值给 d,以此类推!

同样在赋值重载也是如此,我们既然可以比较两个参数,也可以比较多个参数,例如:

//比较两个
St1 = St2;
//比较多个
St3 = St2 = St1;

此时我们成员函数功能变成了赋值运算,功能、返回值都需要改变

this 指针是指向调用成员函数的对象,因此我们需要返回 *this 

//运算符重载函数
Myclass  operator=(const Myclass St)
{
	_year = St._year;
	_month = St._month;
	_day = St._day;
	return *this;
}

 这里有一个注意点:我们返回整个类肯定是没有问题的,但是这样效率很低,如果这个类的空间很大呢?返回整个类就不那么好,因此我们可以返回对象的别名,也就是返回引用

//运算符重载函数
Myclass&  operator=(const Myclass St)
{
	_year = St._year;
	_month = St._month;
	_day = St._day;
	return *this;
}

为什么可以返回引用?

首先 this 的作用域是在成员函数里的,但是我们返回的不是 this ,而是 this 的指向对象:类对象

类对象也就是已经实例化的 St1、St2、St3这些,它们的作用域是全局的,因此不担心空间销毁!

默认调用运算符重载函数

如果我们没有写运算符重载函数,那么编译器会自己调用自己默认的成员函数

默认生成的赋值运算符重载 跟 拷贝构造的行为一样:

(1)内置类型成员发生值拷贝/浅拷贝

(2)自定义类型成员会去调用它的赋值重载

例如:对内置类型发生浅拷贝

例如:对自定义类型调用自己的赋值重载(改变自定义的地址)

 此时我们创建了2个实例化对象,它们自定义的类型地址不同:

但是调用编译器默认的赋值运算重载函数之后,改变了自定义的地址:

 

                                                 【雾非雾】期待与你的下次相遇!