C++类和对象:六个默认成员函数详解

一、引言

在上一篇中,我们学习了如何定义一个类、如何创建对象、理解this指针,也简单了解了面向对象的三大特性。

今天我们学习C++类和对象中另外一个重要的知识点:默认成员函数

在学习C语言以及数据结构时,我们完成一个数据结构通常需要写初始化函数(Initial)、销毁函数(Destroy)等各种函数来实现我们的数据结构,不仅在实现时麻烦,而且还需要我们在每次使用时手动调用

C++的创始人显然考虑到了这些问题,于是便在发明C++时创造出了默认成员函数 这个概念。今天我们就来学习一下六个默认成员函数,它们分别是:

  1. 构造函数(Constructor)
  2. 析构函数(Destructor)
  3. 拷贝构造函数(Copy Constructor)
  4. 运算符重载(Assignment Operator)
  5. 取地址运算符重载(Address-of Operator)
  6. const取地址运算符重载(const Address-of Operator)

默认成员函数也属于成员函数 ,它之所以叫默认成员函数,是因为如果我们不显示地定义它们,编译器也会在需要的时候自动生成。但是编译器生成地不一定符合我们的预期,所以我们要理解它们,在必要的时候自己实现。

二、构造函数

在创建对象时自动调用,来初始化对象,代替我们之前写的 Init() 函数

构造函数的特点:

  1. 函数名与类名相同
  2. 无返回值(不是void,而是什么都不写)
  3. 实例化对象时自动调用
  4. 可以进行重载

观察以上特点,我们会发现它很好地解决了我们之前提到的问题:不用再自己去实现简单的初始化函数、不用再自己调用函数来初始化对象。

为了更好地理解,我们本篇文章去实现一个简单的日期类,在实现的过程中来感受各个默认成员函数的用法和作用。

cpp 复制代码
Class Date {
private:
    int _year;
    int _month;
    int _day;

public:

    //构造函数
    Date(int year = 2000, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
};

int main()
{
    Date d1(2026, 8, 29);
    Date d2;

    return 0;
}

这样,一个简单的构造函数就完成了,对象d1 被初始化为2026年8月29日,对象d2被初始化为2000年1月1日。

下面这种情况是错误的!!!

cpp 复制代码
Class Date {
private:
    int _year;
    int _month;
    int _day;

public:

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

int main()
{
    Date d2;

    return 0;
}

由于我们显示地写了构造函数,编译器便不会再生成,但是我们在实例化对象时又没有传递参数,因此我们在创建d2时它就找不到可以调用的构造函数,发生错误。

所以我们一定要注意:不能让对象在实例化出来时找不到可以调用的构造函数!

如果我们没有显示地定义构造函数,C++编译器会自动帮我们生成一个无参的构造函数。

三、析构函数

在对象生命周期结束时自动调用,来销毁对象,释放资源,相当于 Destroy() 函数

析构函数特点:

  1. 函数名为 ~类名
  2. 没有返回值
  3. 没有参数,不能重载(一个类只能有一个析构函数)
  4. 对象销毁时由编译器自动调用
cpp 复制代码
Class Date {
private:
    int _year;
    int _month;
    int _day;

public:

    //构造函数
    Date(int year = 2000, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //析构函数
    ~Date()
    {
        cout << "析构函数" << endl;
    }
};

int main()
{
    Date d1(2026, 8, 29);
    Date d2;

    return 0;
}

由于我们的日期类目前没有需要动态分配的资源,因此并不需要析构函数,此处写一个只有输出的析构函数向大家展示一下。在程序运行结束后会答应两个"析构函数",分别为d1和d2销毁时调用的析构函数所打印的。

注意:如果没有显示地定义析构函数,编译器会自动生成一个空的析构函数。因此如果类中有动态分配的资源,必须显示定义析构函数来释放资源,否则会造成内存泄露。

四、拷贝构造函数

拷贝构造函数是用同类型的对象来初始化新对象的构造函数

函数特点:

  1. 参数必须是本类类型的引用
  2. 在对象发生拷贝行为时自动调用
  3. 本质是构造函数的重载
cpp 复制代码
Class Date {
private:
    int _year;
    int _month;
    int _day;

public:

    //构造函数
    Date(int year = 2000, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造函数
    Date(const Date& other)
    {
        _year = other._year;
        _month = other._month;
        _day = other._day;
    }
};

int main()
{
    Date d1(2026, 8, 29);
    Date d2(d1);
    Date d3 = d1;

    return 0;
}

首先我们解释一下在什么情况下会自动调用

最简单的情况就是示例中这样,d2和d3这两种方式都可以,只是写法不一样,d3这种写法可读性更高一点。

除了这种明显的拷贝,还有其他只要发生拷贝行为的情况,都会调用拷贝构造函数。比如传参时,如果是传值传参,我们说形参是实参的拷贝,这时就会调用拷贝构造函数来对实参进行拷贝。再比如函数返回时,返回的也是值的拷贝。

这也就解释了为什么参数必须是本类类型的引用

如果不是引用,那么在调用拷贝构造函数时要将本类类型的对象先拷贝一份形参,而拷贝形参时又要调用拷贝构造函数,拷贝构造函数的形参又是本类类型的对象,这样就陷入了无穷调用递归。与我们当时学习函数递归时的死递归有点像。

深拷贝 vs 浅拷贝

编译器自动生成的拷贝构造函数只能进行浅拷贝 ,如果需要深拷贝,则需要我们自己来实现

下面我们来聊聊深浅拷贝的区别

浅拷贝(Shallow Copy): 按字节复制,只复制指针本身,不复制指针指向的内容。
深拷贝(Deep Copy): 不仅复制指针,还复制指针指向的内容(重新分配内存)。

举个例子大家就明白了:

比如我们想拷贝一份顺序表 ,那么只需要进行浅拷贝 即可,因为浅拷贝只复制它的内容;但如果我们想复制一份链表 ,就需要深拷贝 ,如果进行浅拷贝只会复制结点本身,而无法复制结点指向的内容,因此两份链表会指向同一块空间 ,这时就需要深拷贝来重新开辟空间来存放链表结点指向的内容。

如图所示,浅拷贝就是只拷贝表面上的值,也就是地址,但拷贝完指向的还是同一块空间;深拷贝则是重新开辟出相同的空间,将原空间的内容拷贝过来再重新指向。

实在不明白也没关系,只需要记住,涉及到动态资源的管理,那么就需要显示定义深拷贝。

内容补充:

  1. 拷贝构造函数的形参最好使用 const 修饰,否则会让人误以为会修改源对象。
  2. 拷贝构造函数的访问权限
    拷贝构造函数可以使 public 或 private,如果是 private 就表明禁止对象被拷贝

五、运算符重载

我们常用的运算符(+、-、=、*等)只能对内置类型进行运算 ,如果想对自定义类型也实现运算符的运算,比如我们的日期类,两个日期之间进行相减算出相差的天数,或者日期加一个天数算出另一个日期,那么就需要运算符重载。

首先我们来看一下运算符重载的基本语法

  1. 有返回值
  2. 函数名为 operator + 要重载的运算符
  3. this 指针默认作为第一个参数,也就是左操作数
cpp 复制代码
Class Date {
private:
    int _year;
    int _month;
    int _day;

public:

    //构造函数
    Date(int year = 2000, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造函数
    Date(const Date& other)
    {
        _year = other._year;
        _month = other._month;
        _day = other._day;
    }

    //赋值运算符重载
    Date& operator=(const Date& other)
    {
        _year = other._year;
        _month = other._month;
        _day = other._day;
    }

    //+、-运算符重载

    //日期+天数得到新日期
    Date operator+(int days)
    {
        Date d(*this);
        d._day += day;
        while (d._day > GetMonthDay(d._year, d._month))
        {
	        d._day -= GetMonthDay(d._year, d._month);
	        if (d._month < 12)
	        {
	        	d._month++;
	        }
	        else
	        {
	        	d._month = 1;
	        	d._year++;
	        }
        }
        return d;
    }

    //日期-日期得到天数
    int operator-(const Date& d)
	{
		int DaySum1 = 0;
		if (_month != 1)
		{
			for (int i = 1;i < _month;i++)
			{
				DaySum1 += GetMonthDay(_year, i);
			}
		}
		DaySum1 += _day;

		int DaySum2 = 0;
		if (d._month != 1)
		{
			for (int i = 1;i < d._month;i++)
			{
				DaySum2 += GetMonthDay(d._year, i);
			}
		}
		DaySum2 += d._day;

		int YearSum = 0;
		if (_year != d._year)
		{
			for (int i = d._year;i < _year;i++)
			{
				if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0)
				{
					YearSum += 366;
				}
				else
				{
					YearSum += 365;
				}
			}
		}

		return YearSum + DaySum1 - DaySum2;
	}

};

int main()
{
    Date d1(2026, 8, 29);
    Date d2(d1);
    Date d3 = d1;
    Date d4 = d1 - d2;
    d3 = d1 + 100;

    return 0;
}

这里简单为大家实现了+和-的重载,如此一来就能够对我们的自定义类型也进行示例中加或减的运算了。

要注意的是

  1. this 指针已经作为了第一个参数,因此对于二元操作符我们只需要传递一个参数。

  2. 重载后的运算符与原运算符具有相同的优先级

  3. 重载时要注意运算符对于此类型是否有意义。比如对于我们的日期类来说,* 和 / 这样的运算符重载出来没有实际意义。

  4. .*、::、sizeof、?:、.,这五个运算符不支持重载

  5. 为了区分前置++/--与后置++/--,我们在后置++/--重载时会添加一个int参数构成重载

    cpp 复制代码
    // 前置++
    Date& operator++()
    {
    	*this += 1;
    	return *this;
    }
    
    // 后置++
    Date operator++(int)
    {
    	Date d(*this);
    	_day += 1;
    	if (_day > GetMonthDay(_year, _month))
    	{
    		_day -= GetMonthDay(_year, _month);
    		if (_month < 12)
    		{
    			_month++;
    		}
    		else
    		{
    			_month = 1;
    			_year++;
    		}
    	}
    	return d;
    }

流插入/流提取运算符

如果我们想输出自定义类型的对象,在之前我们可以写一个 print 函数来实现;学习了运算符重载后我们就可以直接使用 << 和 >> 对自定义类型进行输入和输出了。

我们重载 << 和 >> 时,需要重载为全局函数,如果重载为成员函数,this 指针会抢占第一个参数位置 ,也就是操作符的左操作数 ,调用时就变成了d << cout 来输出d。语法上没有错误,但是不符合我们的书写习惯,可读性较差,因此我们最好将 << 和 >> 重载为全局函数

但是重载为全局函数又存在一个问题:我们在外部无法访问 _year 等内部成员变量

所以在这里,我们简单介绍一下什么是友元函数,这时下一期要讲的内容。

简单来说,友元函数就是在类中声明的一个函数 ,使它能够访问类中的成员变量

只要在类中添加一句声明:

friend ostream& operator<<(ostream& out, const Date& d);

这个外部函数就能够访问类中的成员变量了

cpp 复制代码
Class Date {
private:
    int _year;
    int _month;
    int _day;

public:
    //友元函数
    friend ostream& operator<<(ostream& out, const Date& d);

    //构造函数
    Date(int year = 2000, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造函数
    Date(const Date& other)
    {
        _year = other._year;
        _month = other._month;
        _day = other._day;
    }

    //赋值运算符重载
    Date& operator=(const Date& other)
    {
        _year = other._year;
        _month = other._month;
        _day = other._day;
    }

    //+、-运算符重载

    //日期+天数得到新日期
    Date operator+(int days)
    {
        Date d(*this);
        d._day += day;
        while (d._day > GetMonthDay(d._year, d._month))
        {
	        d._day -= GetMonthDay(d._year, d._month);
	        if (d._month < 12)
	        {
	        	d._month++;
	        }
	        else
	        {
	        	d._month = 1;
	        	d._year++;
	        }
        }
        return d;
    }

    //日期-日期得到天数
    int operator-(const Date& d)
	{
		int DaySum1 = 0;
		if (_month != 1)
		{
			for (int i = 1;i < _month;i++)
			{
				DaySum1 += GetMonthDay(_year, i);
			}
		}
		DaySum1 += _day;

		int DaySum2 = 0;
		if (d._month != 1)
		{
			for (int i = 1;i < d._month;i++)
			{
				DaySum2 += GetMonthDay(d._year, i);
			}
		}
		DaySum2 += d._day;

		int YearSum = 0;
		if (_year != d._year)
		{
			for (int i = d._year;i < _year;i++)
			{
				if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0)
				{
					YearSum += 366;
				}
				else
				{
					YearSum += 365;
				}
			}
		}

		return YearSum + DaySum1 - DaySum2;
	}

};

ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << '/' << d._month << '/' << d._day << endl;
    return out;
}

int main()
{
    Date d1(2026, 8, 29);
    Date d2(d1);
    Date d3 = d1;
    Date d4 = d1 - d2;
    d3 = d1 + 100;

    cout << d1 << d2 << d3 << d4 << endl;

    return 0;
}

这样,我们就能够直接使用 << 来输出我们的自定义类型了。输入也是同理。

六、取地址运算符重载

1.const 修饰成员函数

我们如果定义了一个 const 修饰的成员变量:

const Date d1;

如果调用了成员函数,就会出现我们之前提到过的权限放大 ,因为这个对象本身被 const 修饰 ,而我们调用成员函数时的this 指针并没有被 const 所修饰 ,所以我们在成员函数中依然可以用 *this 来修改我们的 d1

因此我们在定义成员函数时为了让 const 修饰的对象也能调用成员函数,我们可以将不修改对象的函数也用 const 修饰起来。例如:

void Print() const;

编译后变为

void Print ( const Date* const this );

2.取地址运算符重载

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

这里目前也没什么可讲的,需要注意的就是我们取地址的对象有没有被 const 修饰 ;或者如果我们不希望const修饰的对象被取地址 ,可以在重载时直接返回 nullptr

绝大多数情况下无需我们显式定义,默认生成的就足够,除非有一些特殊的需求。

十、总结

以上就是我们本篇的全部内容了。

这一篇我们学到了:

  1. 构造函数:对象创建时自动调用,用于初始化成员变量。推荐使用初始化列表。

  2. 析构函数:对象销毁时自动调用,用于释放资源。

  3. 拷贝构造函数:用已有对象创建新对象时调用,有动态资源时需要深拷贝。

  4. 拷贝赋值运算符:已有对象被赋值时调用,同样需要注意深拷贝。

  5. 取地址运算符:通常不需要重载,使用编译器默认生成的即可。

下篇预告: 类和对象(下)------ static 成员、友元、内部类、匿名对象......

相关推荐
吴声子夜歌17 分钟前
Java——类、对象及方法(二)
java·开发语言
2401_8906034019 分钟前
Python入门语法(一)
java·开发语言·python
库玛西2 小时前
深入浅出传输层:UDP 与 TCP 协议全景指南
linux·服务器·网络·c++·笔记·tcp/ip·udp
Chester_19998 小时前
CSP202203C.计算资源调度器
开发语言·数据结构·c++·蓝桥杯
EXI-小洲9 小时前
Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载
java·开发语言·spring boot·word
会周易的程序员10 小时前
给 PLC 写一个字节码虚拟机:STVM 虚拟机架构设计
开发语言·c++·虚拟机·软plc·iec61131·stvm
机器视觉知识推荐、就业指导10 小时前
为什么老说“纯 Qt 没啥就业市场”?
开发语言·qt
telepan10 小时前
Qt 开发避坑与性能指南:如何优雅、安全地定义全局常量字符串?
开发语言·qt