了解类的六大默认成员函数,看这一篇就够了

🎉🎉🎉欢迎莅临我的博客空间,我是池央,一个对C++和数据结构怀有无限热忱的探索者。🙌

🎈🎈🎈期待与你一同在编程的海洋中遨游,探索未知的技术奥秘💞

📝专栏指路:

📘【C++】专栏:深入解析C++的奥秘,分享编程技巧与实践。

📘【数据结构】专栏:探索数据结构的魅力,助你提升编程能力。

bcd6373d6aae4f86ae55b1bd9a7da837.gif

1. 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类大小是1,占一个字节

class Date//空类
{
};

空类中真的什么都没有吗?

并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数(也就是说我们不写,编译器也会自动生成和调用)

163555ffb4e44c509cd910ec96b631e5.png


2. 构造函数

2.1概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

构造函数的实际用处大吗?我们不妨先来看一下这样一段代码

#include<iostream>
using namespace std;
class Date
{
private:
	int year, month, day;
public:
	void SetDate()//SetDate方法用来给日期设置值
	{
		cin >> year >> month >> day;
	}
	void Show()//显示日期
	{
		cout << year << "/" << month << "/" << day << endl;
	}
};

int main()
{
	Date d1, d2,d3;//创建对象
	d1.SetDate();
	d1.Show();
	d2.SetDate();
	d2.Show();
	d3.SetDate();
	d3.Show();
	return 0;
}

8c6ef06a960c4cea8ba0b8123442eb8a.png

上面这段代码中的日期类对象的初始化赋值我们调用了类的成员函数SetDate(),上面只创建了3个对象,而每个对象都需要调用一次SetDate()方法才能完成赋值操作,那如果创建了N个对象呢,岂不是要调用N次的SetDate()方法。

我们再来看一下有显示构造函数的日期类,有什么不同?

#include<iostream>
using namespace std;
class Date
{
private:
	int year, month, day;
public:
	Date(int y = 0, int m = 0, int d = 0)//构造函数
		//完成初始化类对象
	{
		year = y;
		month = m;
		day = d;
	}
	void SetDate()//SetDate方法用来给日期设置值
	{
		cin >> year >> month >> day;
	}
	void Show()//显示日期
	{
		cout << year << "/" << month << "/" << day << endl;
	}
};

int main()
{
	Date d1(2024,5,20), d2(2023, 5, 20), d3(2022, 5, 20);//创建对象
	d1.Show();
	d2.Show();
	d3.Show();
	return 0;
}

c77fa1255afb46a69cec6aa8f0eab229.png

我们可以发现构造函数直接给我们创建的对象进行了赋初值,而且我们并不需要调用构造函数,在我们创建对象时,编译器会自动调用构造函数,我们不再需要每次都调用SetDate()方法才能进行日期的赋值。

2.2特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任
并不是开空间创建对象,而是初始化对象
特征如下

(1)函数名与类名相同

(2)没有返回值

(3)对象实例化时自动调用

(4)可重载(详细了解可阅读文章缺省参数和函数重载

(5)初始化对象,不开空间

(6) 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成(不传参数的构造函数为默认构造函数),默认构造函数只能有一个。

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。

(7) 默认生成的构造函数,内置类型(如int、char、double)没有规定要处理(是否处理取决于编译器),自定义类型调用默认构造函数

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

#include<iostream>
using namespace std;
class Date
{
private:
	int year, month, day;
public:
	// 1.无参构造函数
	Date() { ; }
	// 2.带参构造函数
	Date(int y, int m, int d )//构造函数
		//完成初始化类对象
	{
		year = y;
		month = m;
		day = d;
	}
	void Show()//显示日期
	{
		cout << year << "/" << month << "/" << day << endl;
	}
};

int main()
{
    Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); // 调用带参的构造函数
	d1.Show();
	d2.Show();
	//Date d3();
	//d3.Show();
	return 0;
}
477aa2c86d8b4b1fa52100a6346dd1bc.png

 在上面这段代码中无参构造函数是默认生成的构造函数,编译器没有处理内置类型,所以显示出来的日期是随机值

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

2.3 构造函数的初始化列表

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

例如

class Date
{
public:
 Date(int year, int month, int day)
 {
 year_ = year;
 month_ = month;
 day_ = day;
 }
private:
 int year_;
 int month_;
 int day_;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为 初始化只能初始化一次,而构造函数体 内可以多次赋值

初始化列表:类对象成员定义初始化,可与函数体混合使用(先列表后函数体)。以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

//MyQueue类的构造函数
MyQueue(int n)
:_pushst(n)
,_Popst(n)
{//函数体}

为什么要把这一部分单独拿出来讲?是因为有些成员变量必须要在初始化列表进行初始化。

(1)必须要在初始化列表进行初始化的类成员

1、const成员变量,因为const只有在定义的时候有唯一一次初始化机会

2、&引用,引用必须初始化

3、没有默认构造的自定义类型成员(因为必须显示传参调用构造函数)

(2)初始化列表,不管写没写,每个成员变量都会先走一遍。

自定义类型成员调用默认构造函数

内置类型有缺省值用缺省值,没有缺省值看编译器是否处理

(3)成员变量在类中声明的次序才是初始化列表中成员变量初始化的顺序

注:初始化列表中成员变量初始化的顺序,与成员变量在初始化列表中的先后位置无关

代码演示

#include <iostream>
using namespace std;

// 自定义类型,没有默认构造函数
class CustomType {
public:
    //explicit 关键字作用阻止隐式类型转换
    explicit CustomType(int value) : value_(value) {}
    void print() const { cout << "CustomType value: " << value_ << endl; }
private:
    int value_;
};

class MyClass {
public:
    // 构造函数,使用初始化列表
    MyClass(int a, int b, int c)
        : const_member_(a) // const成员变量
        ,ref_member_(b)  // 引用成员变量
        ,custom_member_(c) // 没有默认构造的自定义类型成员
    {
        // 函数体
    }

    // 成员函数
    void printMembers() const 
    {
    cout << "const_member_: " << const_member_ << endl;
    cout << "ref_member_: " << ref_member_ << endl;
    custom_member_.print();
    }

private:
    const int const_member_; // const成员变量
    int& ref_member_;        // 引用成员变量
    CustomType custom_member_; // 没有默认构造的自定义类型成员
};

int main()
{
    int x = 10;
    MyClass obj(1, x, 20); // 使用初始化列表中的参数
    obj.printMembers();
    return 0;
}

b028bac85eea42fa91a86d91748bd6cc.png

再来看这样一段代码来验证:初始化列表中成员变量初始化的顺序,与成员变量在初始化列表中的先后位置无关

class A {
public:
 A(int a)
 :_a1(a)
 ,_a2(_a1)
 {}
 
 void Print() {
 cout<<_a1<<" "<<_a2<<endl;
 }
private:
 int _a2;
 int _a1;
};
int main() {
 A aa(1);
 aa.Print();
}

运行结果如图:c42804a1e6a24a6eaee2e909b56449cf.png

先用_a1用来给_a2赋值,由于之前我们并没有给_a1赋初值,所以_a2打印出来是随机值;而a也就是传过来的实参1用来给_a1赋值,所以_a1打印出来是1。

2.4构造函数总结

一般构造函数需要我们自己显示实现,只有少数可以让编译器自动。如MyQueue(队列),成员变量全是自定义类型(编译器自动生成)


3. 析构函数

3.1概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

3.2特性

(1) 析构函数名是在类名前加上字符 ~。
(2)无参数、无返回值类型。
(3) 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。
(5)析构函数与构造函数类似:内置类型不一定处理,自定义类型调用它的析构函数。
代码示例:
class Date
{
private:
	int year, month, day;
public:
	// 1.无参构造函数
	Date() { ; }
	// 2.带参构造函数
	Date(int y, int m, int d )//构造函数
		//完成初始化类对象
	{
		year = y;
		month = m;
		day = d;
	}
	void Show()//显示日期
	{
		cout << year << "/" << month << "/" << day << endl;
	}
	~Date(){}//析构函数
};

3.3总结

(1)有资源清理的需要显示实现析构函数,如Stack(栈)、List(链表)

(2)有两种场景不需要显示实现析构函数

a.没有资源需要清理,如Date类

b.内置类型成员没有资源需要清理,剩下的成员都是自定义类型成员。如MyQueue


4.对象构造和析构的顺序

初阶版

代码演示:

#include<iostream>
using namespace std;
class Date
{
private:
	int year, month, day;
public:
	Date(int y, int m, int d )//构造函数
	//完成初始化类对象
	{
		year = y;
		month = m;
		day = d;
		cout << "构造";
		cout << year << "/" << month << "/" << day << endl;
	}
	
	~Date()
	{
		cout << "析构~Date()";
		cout << year << "/" << month << "/" << day << endl;
	}
};

int main()
{
	Date d1(2023, 10, 10); 
	Date d2(2015, 1, 1); 
	return 0;
}

7873532b90aa4011875f1f0c69e6eb7a.png

通过上面例子可知:先构造的对象后析构,遵循栈的先进后出原则

进阶版

静态全局对象、静态局部对象、全局对象、局部对象,我们再来看一下这些对象的构造和析构顺序会是怎么样?

代码演示

#include<iostream>
using namespace std;
class Date
{
private:
	int year, month, day;
public:
	Date(int y, int m, int d )//构造函数
	//完成初始化类对象
	{
		year = y;
		month = m;
		day = d;
		cout << "构造";
		cout << year << "/" << month << "/" << day << endl;
	}
	
	~Date()
	{
		cout << "析构~Date()";
		cout << year << "/" << month << "/" << day << endl;
	}
};
Date d1(2014, 9, 9);//全局对象
static Date d2(2015, 10, 10);//静态全局对象

int main()
{
	Date d3(2016, 11, 11);//局部对象
	Date d4(2017, 12, 12);//局部对象
	static Date d5(2018, 1, 1);//静态局部对象
	return 0;
}

eb45686e00074632a68b079926b71ec5.png

通过上面示例我们发现,这里的对象析构顺序与我们之前所说的先构造的先析构有所出入。原本按照我们之前案例得出的结论最先析构的应该是d5,最后析构的应该是d1才对,是哪里出了问题呢?

其实问题出在这些对象的生命周期上,下面简要介绍一下各种对象的生存期,以及调用构造函数、析构函数的时机。

1、局部对象
其生存期为函数被调用期间。在建立对象时,调用其构造函数,当函数调用结束时调用其析构函数。

2、静态局部对象
其生存期为函数被首次调用至程序结束期间。当函数被首次调用建立对象时,调用其构造函数,当主函数执行完毕前调用其析构函数。

3、全局对象

全局对象在程序一开始时,其构造函数就会被执行(这通常比程序进入点更早)。当程序即将结束前,全局对象的析构函数会被执行。
 

4、静态全局对象

静态全局对象与全局对象类似,其构造函数也是在程序一开始时被执行。当程序结束时,静态全局对象的析构函数也会被执行,但需要注意的是,它可能比全局对象的析构函数早一步执行。

补充知识

全局对象和静态全局对象生命周期的对比:

静态全局对象和全局对象都在程序开始时创建,并在程序结束时销毁。它们的生命周期实际上是一样长的,都是贯穿整个程序的执行过程。
主要的区别在于,静态全局对象的析构函数可能在全局对象的析构函数之前执行,但这并不意味着静态全局对象的生命周期更长。

链接性的区别:

除了生命周期外,静态全局对象和全局对象还有一个重要的区别:链接性。一般的全局对象在程序的其他文件中可以通过关键字extern来访问,而静态全局对象则只能在本文件中使用。

综上所述,静态全局对象和全局对象的生命周期实际上是相同的,都是贯穿整个程序的执行过程。它们的主要区别在于链接性和析构函数的执行顺序。

所以在上面的例子中局部对象应该是最先被析构的,局部对象d4比局部对象d3先析构(因为d3比d4先构造),然后到静态局部对象d5析构,再到静态全局变量,最后是全局变量。


5. 拷贝构造函数

5.1概念

拷贝构造:用同类型的已经存在的对象,拷贝给另一个要创建初始化的对象。

注意:在编译器中生成的默认拷贝构造中,内置类型按字节直接拷贝(浅拷贝),自定义类型调用其的拷贝构造函数完成(深拷贝)

补充:

  • 浅拷贝:按内存存储按字节序完成拷贝,也叫做值拷贝。
  • 深拷贝:按形状拷贝,开一样大的空间。

5.2特征

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

(2)参数有且只有一个,必是类类型对象的引用,使用传值编译器会直接报错(因为会引发无穷递归调用

5.3拷贝构造函数典型调用场景

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

简单代码示例一

#include<iostream>
using namespace std;
class Date
{
public:
    //构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //拷贝构造函数
	Date(const Date& d) //Date(const Date d) //若传值,编译器报错,会引发无穷递归
	{
		_year = d._year;//等价于this->_year=d._year
		_month = d._month;
		_day = d._day;
	}
	void PrintDate()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.PrintDate();
	Date d2(d1);//等价于Date d2=d1;
	d2.PrintDate();
	Date d3 = d1;
	d3.PrintDate();
	return 0;
}

39cec638bcf041ef98d72756c00ebcbc.png

代码示例二

class Date
{
public:
 Date(int year, int minute, int day)
 {
 cout << "Date(int,int,int):" << this << endl;
 }
 Date(const Date& d)
 {
 cout << "Date(const Date& d):" << this << endl;
 }
 ~Date()
 {
 cout << "~Date():" << this << endl;
 }
private:
 int _year;
 int _month;
 int _day;
};
Date Test(Date d) {
 Date temp(d);
 return temp; }
int main()
{
 Date d1(2022,1,13);
 Test(d1);
 return 0; 
}

03178429e1b84928bc3f9f4bb156f9bd.png

5.4总结

(1)无管理资源/类中都是自定义类型成员变量,内置类型成员变量没有指向资源,默认生成拷贝构造即可。如Date类 ,MyQueue类等

(2)一般来说不需要显示写析构函数,就无需显示写拷贝构造函数

(3)内部有指针/一些值指向资源,就需要显示写析构函数释放,通常就需要显示写拷贝构造函数完成深拷贝。如Stack类,Queue类,List类等


6. 赋值运算符重载

在了解赋值重载函数之前,我们需要了解什么是运算符重载,这里做一个简单的介绍。

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

6.0运算符重载函数

(1)函数名:operator+需要重载的运算符符号

(2)函数原型:返回值类型 operator+符号(形参参数列表)

(3)必须有一个类类型的参数

(4)简单代码示例

6.1概念

赋值重载:一个已经存在的对象,拷贝复制给另一个已经存在的对象(要区别拷贝构造)

注意:
1、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的 this。
2、赋值运算符只能重载成类的成员函数不能重载成全局函数。原因如下:

赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3、 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符

重载完成赋值。
注:如果类中未涉及到资源管理,赋值运算符一般不用显示实现;一旦涉及到资源管理则必
须要实现。

6.2赋值运算符重载格式

  • 返回值类型:类名&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 参数类型:const 类名&(传引用提高效率)
  • 返回*this(要符合连续赋值的含义)
  • 检测是否自己给自己赋值

代码示例

#include<iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(const Date& d) 
	{
		_year = d._year;//等价于this->_year=d._year
		_month = d._month;
		_day = d._day;
	}
	//赋值重载函数
	Date& operator=(const Date& d)
	{
		if (this != &d)//检测是否自己给自己赋值
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void PrintDate()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.PrintDate();
	Date d2 = d1;//拷贝构造
	d2.PrintDate();
	Date d3(2024, 5, 20);
	d1 = d3;//赋值重载
	d1.PrintDate();
	return 0;
}

971cea1b44fb47c5b8c31af3a2dcea38.png

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实
现吗?当然像日期类这样的类是没必要的。那么下面的类呢?我们一起验证一下

代码示例二

#include<iostream>
using namespace std;
class Time
{
public:
    Time(int hour = 0, int minute = 0, int second = 0) 
    {
        _hour = hour;
        _minute = minute;
        _second = second;
    }
    void PrintTime()
    {
        cout << _hour << "/" << _minute << "/" << _second << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1, const Time& t = Time()) 
        : _year(year), _month(month), _day(day), _t(t) // 使用初始化列表
    {
    }

    void PrintDate()
    {
        cout << _year << "/" << _month << "/" << _day << "/";
       _t.PrintTime();
    }

private:
//内置类型
    int _year;
    int _month;
    int _day;
//自定义类型
    Time _t;
};

int main()
{
    // 使用默认Time对象初始化Date对象
    Date d1(2024, 11, 11);
    d1.PrintDate();

    // 使用自定义的Time对象初始化Date对象
    Time t(12, 15, 15);
    Date d2(2024, 10, 10, t);
    d2.PrintDate();

    //赋值重载
    d2=d1;
    d2.PrintDate();
    return 0;
} 

9beee9186f4b49d5a2ec889bc407b99e.png

6.3总结

出了作用域,返回对象未析构可用引用返回,减少拷贝

  • 返回对象生命周期到了会析构,传值返回(会生成临时对象)
  • 返回对象生命周期没到不会析构,传引用返回

7. const成员函数

将const修饰的“成员函数”称之为const成员函数。
const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中 不能对类的任何成员变量进行修改。
代码示例
#include<iostream>
using namespace std;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	//构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void PrintDate()
	{
		cout << "PrintDate()" << endl;
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	void PrintDate()const
	{
		cout << "PrintDate()const" << endl;
		cout << _year << "/" << _month << "/" << _day << endl;
	}
};
int main()
{
	Date d1;
	d1.PrintDate();
	const Date d2(2024, 5, 20);
    d2.PrintDate();
	return 0;
}

运行结果:

11a5fbec4d49418ab033dbcc8cb64af3.png
再来看看下图一
91d324a881a3478db050b4869e2aff45.png
图二

73d5e3c3c83549f5acd6a59c219eb4f5.png

图三

af806c49fe954c55beade4aaf6b3b5db.png

图四

121d279abe2a462fa8e4de00138056f3.png

通过调试我们可以知道const对象可以调用const成员函数;非const对象可以调用非const成员函数。这属于权限的平移

  • const对象可以调用非const成员函数吗?可以,权限的缩小
  • 非const对象可以调用const成员函数吗?不可以,权限的放大
  • const成员函数内可以调用其它的非const成员函数吗?可以,权限的缩小
  • 非const成员函数内可以调用其它的const成员函数吗?不可以,权限的放大

一句话总结权限不能被放大!!!

8. 取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
简单示例:
class Date
{ 
public :
 Date* operator&()
 {
 return this ;
}
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ;
 int _month ; 
 int _day ; 
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如 想让别人获取到指定的内容!
持续更新中...
敬请期待

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值