【C++】类与对象——类的六个默认成员函数

文章目录

  • 一、构造函数
    • 1.概念
    • 2.特征
      • 编译器生成的默认成员函数的意义
    • 3.成员变量的命名风格
    • 4.构造函数体赋值
    • 5.初始化列表
      • 5.1 格式
      • 5.2 注意事项
      • 5.3初始化顺序
    • 6.explicit关键字
  • 二、析构函数
    • 1.概念
    • 2.特征
  • 三、拷贝构造函数
    • 1.概念
    • 2.特征
    • 3.为什么必须是引用传参
    • 4.浅拷贝
    • 5.自己实现的意义
      • 浅拷贝后果
  • 四、赋值运算符重载
    • 1.概念
    • 2.特征
    • 3.赋值运算符重载
  • 五、const成员
    • 1.const修饰的类的成员函数
    • 2.小问题
    • 3.总结
  • 六、取地址及const取地址操作符重载

【C++】类与对象——类的六个默认成员函数_第1张图片

类中有六个默认成员函数,即使一个成员都没有,空类中也不是空白的,任何一个类在我们不写的情况下都会默认生成6个成员函数
下面一一介绍

一、构造函数

1.概念

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

2.特征

  • 1.函数名与类名相同

  • 2.无返回值

  • 3.对象实例化时编译器自动调用对应的构造函数

  • 4.构造函数可以重载

举例

class Data
{
public:
	//1.无参构造函数
	Data()
	{}
	//2.带参的构造寒素
	Data (int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Data d1;//调用无参构造函数,不用+”()“
	Data d2(2022, 3, 28);//调用带参的构造函数
	Data d3();
}
  • 5.如果类中没有显示的定义构造函数, 则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成
class Data
{
public:
	//带参的构造函数,如果用户显示定义构造函数,编译器将不再生成
	Data (int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	//没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
	Data d;//因为这里是无参的
}
  • 6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个

无参构造函数,全缺省构造函数,我们没有写编译器默认生成的构造函数,都可以认为是默认成员函数

编译器生成的默认成员函数的意义

在C++中,把类型分为内置类型和自定义类型,内置类型就是语法已经定义好的类型,例如int/char 自定义类型就是我们使用class/struct/union自己定义的类型
举例

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int second;
};	
class Data
{
private:
	//基本类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time _t;
};

void Test()
{
	Data d;
	return 0;
}

例如上面的例子中,编译器就会生成默认的构造函数对自定义类型成员_t调用它的默认成员函数

3.成员变量的命名风格

我们一般建议将类的成员函数定义的名字加一些前缀和后缀
举例

class Data
{
public:
	Data(int year)
	{
		//无法区分这里的year是成员变量,还是函数形参
		year = year;
	}
private:
	int year;
};

所以我们通常建议这样定义

class Data
{
public:
	Data(int year)
	{
		//这样就容易区分了
		_year = year;
	}
private:
	int _year;
}

4.构造函数体赋值

需要注意的是,构造函数名字虽然叫构造,但是构造函数的主要任务不是开空间创建对象,而是初始化对象
举例

class Data
{
public:
	//这里不能倍称为初始化,只能叫赋值
	Data (int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

构造函数体内的语句只能称作为赋初值,不能称作初始化,主要区别为:初始化只能初始化一次,赋值可以多次,所以通常的成员变量初始化,是由初始化列表实现的

5.初始化列表

5.1 格式

初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量“后面跟一个放在括号中的初始值或表达式
举例

class Data
{
public:
	//带参的构造函数,如果用户显示定义构造函数,编译器将不再生成
	Data (int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
		{}
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

5.2 注意事项

  • 1.每个成员变量在初始化列表中只能出现一次,因为初始化只能初始化一次

  • 2.当类中包含以下成员时,必须放在初始化列表位置进行初始化

1.引用成员变量
2.const成员变量
3.自定义类型成员

举例

class A
{
public:
	A(int a)
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
	{
		: _self(a);
		, _ref(ref)
		, _n(10)
		{}
private:
	A _self;//自定义类型成员
	int& _ref;//引用成员变量
	const int _n;//const成员变量
};
  • 3.尽量使用初始化列表初始化,对于自定义类型成员函数,一定会先使用初始化列表初始化,即使我们不写,编译器也会给,只不过时随机值

  • 4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

举例
大家可以思考一下面这段代码的结果

A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值

#include 
using namespace std;

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	//是按照这里的顺序初始化的,这个例子中,先初始化_a2,再初始化_a1,但是_a2是由_a1的值来初始化的
	int _a2;
	int _a1;
};

int main()
{
	A aa(1);
	aa.Print();
	return 0;
}

自测结果如下
【C++】类与对象——类的六个默认成员函数_第2张图片

5.3初始化顺序

  • 数据成员在类中定义的顺序就是参数列表中的初始化顺序;
  • 初始化列表仅用于初始化数据成员,并不指定这些数据成员的初始化顺序;
  • 每个成员在初始化列表中只能出现一次;
  • 尽量避免使用成员初始化成员,成员初始化顺序最好和成员的定义顺序保持一致。

6.explicit关键字

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用,但是通常会造成代码的可读性不好,使用explicit修饰构造函数,将会禁止单参构造函数的隐式转换

class Data
{
public:
	Data (int year)
		: _year(year)
		{}
	explicit Data (int year)
		: _year(year)
		{}
private:
	int _year;
	int _month;
	int _day;
};

void Test
{
	Data d1(2018);
	d1 = 2019;
}

二、析构函数

1.概念

析构函数是特殊的成员函数,与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理工作。

2.特征

  • 1.析构函数名是在类名前加上~
  • 2.析构函数没有参数也没有返回值
  • 3.一个类只能有一个析构函数,且不能重载,如果没有显示定义析构函数,系统会自动生成默认的析构函数
  • 4.对象生命周期结束时,C++编译系统会自动调用析构函数
  • 5.编译器生成的默认析构函数,对自定义类型成员调用他的析构函数
    举例
class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};

class Person
{
private:
	String _name;
	int _age;
};

int main()
{
	Person p;
	return 0;
}

三、拷贝构造函数

1.概念

拷贝构造函数是用已存在的类类型对象来创建新的对象,只有单个形参,且该形参必须是本类类型对象的引用,一般用const修饰

2.特征

  • 1.拷贝构造函数时特殊的成员函数
  • 2.拷贝构造函数是构造函数的一个重载格式
  • 3.拷贝构造函数的参数只有一个且必须是引用传参,用传值的方式会引发无穷递归调用
    举例
class Data
{
public:
	Data (int year = 2022, int month = 03, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//格式要非常注意
	Data(const Data& d)
	{
		_year = d.year;
		_month = d.month;
		_day = d.day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Data d1;
	Data d2(d1);
	return 0;
}

3.为什么必须是引用传参

【C++】类与对象——类的六个默认成员函数_第3张图片

4.浅拷贝

如果没有显示定义拷贝构造函数的话,系统会生成默认的拷贝构造函数,默认的拷贝构造函数按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
举例

class Data
{
public:
	Data (int year = 2022, int month = 03, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Data d1;
	Data d2(d1);//这里就调用了默认拷贝构造函数
	return 0;
}

5.自己实现的意义

❓我们刚刚都提到了,如果一个类没有显示实现拷贝构造函数,则编译器会生成一份默认构造函数,既然编译器会给我们生成默认拷贝构造函数,那我们还有必要自己写嘛?

我们要知道默认拷贝构造函数的拷贝方式:将一个对象原封不动的拷贝到新对象中

例子

class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char *)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
//多次调用析构函数
	~String()
	{
		free(_str);
		_str = nullptr;
	}
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2(s1);
	system("pause");
	return 0;
}

默认拷贝构造函数执行完成之后,s1,s2在底层公用的是同一份堆空间

【C++】类与对象——类的六个默认成员函数_第4张图片

浅拷贝后果

多个对象共同使用同一份资源,在这些对象被销毁时,同一份资源会被释放多次,引起崩溃

  • 编译器生成的默认拷贝构造函数,是按照浅拷贝方式实现的

  • 浅拷贝就是将一个对象中的内容原封不动的拷贝到另一个对象中

  • 后果是多个对象共享同一份资源,最终在对象销毁时该份资源被释放多次而导致程序崩溃

所以如果类中涉及到资源管理时,该类必须显示提供析构函数,在析构函数中将对象的资源释放掉

如何判断需要自己实现拷贝构造函数,什么时候实不实现无所谓?

如果一个类中如果设计到资源管理时,拷贝构造函数是必须要实现的

四、赋值运算符重载

1.概念

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

2.特征

  • 1.函数名字:关键字operator后面接需要重载的运算符符号
  • 2.函数原型:返回值类型 operator 操作符(参数列表)
  • 3.不能通过连接其他符号来创建新的操作符,例如operator@
  • 4.重载操作符必须有一个类类型或者枚举类型的操作数
  • 5.用于内置类型的操作符,其含义不能改变
  • 6.作为类成员的重载函数时,其形参看起来比操作数数目少1, 成员函数的操作符有一个默认的形参this,限定为第一个形参
  • 7..*/::/sizeof/?:/.这五个运算符不能重载
    举例
class Data
{
public:
	Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void PrintfData()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    Data d1(2022, 1, 12);
    Data d2(d1);
    Data d3(2022, 1, 13);
    //调用赋值运算符重载函数
    d1 = d3; 
}

如果类没有显示实现赋值运算符重载函数,则编译器会生成一份默认运算符重载函数,完成对象之间的赋值操作

❓但是观察下面的代码有没有问题

class String
{
public:
    //构造函数
	String(const char* str = "jack")
	{
		_str = (char *)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
    //拷贝构造函数
    String(const string& s)
    {
        cout << "拷贝构造函数" << endl;
    }

	~String()
	{
        if(_str)
        {
			free(_str);
			_str = nullptr;
        }
        }
private:
	char* _str;
};
int main()
{
	String s1("hello world");
	String s2("Hello World");
    s1 = s2;
	system("pause");
	return 0;
}

下面时结果的监视窗口
【C++】类与对象——类的六个默认成员函数_第5张图片
【C++】类与对象——类的六个默认成员函数_第6张图片
【C++】类与对象——类的六个默认成员函数_第7张图片
上面的代码存在两个问题
编译器生成的赋值运算符重载是按照浅拷贝方式实现的,类中涉及到资源管理时,会造成以下两个后果

1.浅拷贝:一份内存资源释放多次,引起代码崩溃
2.s1被赋值后,地址和s2一样,s1的内存丢失了,造成内存泄露

类中涉及资源管理时,赋值运算符重载必须显示写出来

3.赋值运算符重载

赋值运算符重载与函数重载没有任何关系

  • 函数重载:在相同作用域,函数名字相同,参数列表不同(个数,类型,类型次序),与返回值类型没有关系
  • 运算符重载:为了提高代码的可读性

举例赋值运算符重载

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

	Data(const Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Data& operator=(const Data& d)
	{
		//判断有没有自己给自己赋值
		if(this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
private:
	int _year;
	int _month;
	int _day;
};

注意

  • 参数类型
  • 返回值
  • 检测是否自己给自己赋值
  • 返回*this
  • 一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节的值拷贝

五、const成员

1.const修饰的类的成员函数

const 修饰的类成员函数称为const成员函数,实际上const修饰的时成员函数隐藏的this指针,表明该成员函数不能对类中的任何成员进行修改

2.小问题

  • const对象可以调用非const成员函数吗?❌
  • const对象可以调用const成员函数吗?⭕
  • const成员函数内可以调用其他的非const成员函数吗?❌
  • const成员函数内可以调用其他的const成员函数吗?⭕

3.总结

1.如果在成员函数中不需要修改成员变量,最好将该函数修饰成const类型
2.如果需要修改当前对象中的成员变量时,该函数不能用const修饰

六、取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义,编译器会自动生成
举例

class Data
{
public:
	data* operator&()
	{
		return this;
	}
	const Data* operator&()const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

你可能感兴趣的:(c++,c++,开发语言,linux)