[C++笔记]默认成员函数:构造,析构,拷贝构造,赋值运算符重载(含运算符重载介绍)

1.说明

类的6个默认成员函数:
如果一个类中什么成员都没有,则称为空类。
而任何一个类,在编写者自己不去实现的情况下,都会自动生成以下6个默认成员函数(前四个是重点):
1.构造函数,主要完成初始化工作(不是开空间创建对象),类似数据结构中常写的init函数。
2.析构函数,主要完成清理工作,类似数据结构中常写的destroy函数。
3.拷贝构造函数,使用同类对象初始化创建对象。
4.赋值重载,主要是把一个对象的值赋给另一个对象。
5&6.普通对象和const对象取地址,这两个很少会自己实现。

1.1构造函数:

是特殊成员函数,虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数不是常规的成员函数,调用方式有差别。
构造函数的存在意义是保证对象定义后就被初始化,防止忘记初始化等。

  1. 函数名与类名相同,参数列表没有规定,按需使用。

  2. 无返回值,也不是void,不用写任何返回类型。

  3. 对象实例化时编译器自动调用对应的构造函数,且在对象的生命周期内只调用一次。

  4. 构造函数可以重载,也就是说可以有多种初始化方式。

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

  6. 无参构造函数、全缺省构造函数、用户未写而由编译器默认生成的构造函数,这类不传参就可以调用的构造函数,都可以认为是默认成员函数。

  7. 默认构造函数只能有一个。

    一般建议使用全缺省的默认构造函数,且每个类都提供一个。虽然在语法上可以同时存在全缺省和无参的,但若有对象定义或调用就会报错。
    (另,C中没有拷贝构造,所以C可以安全地直接传结构体,而C++中,可以认为自定义类型传值传参等同于拷贝构造:都是用同类型对象来初始化)

    注意:默认构造函数只会为class,struct等自定义类型成员调用,而对int,char等内置类型成员不做处理。
    c++11中针对内置类型成员不默认初始化这一缺陷打了个补丁:
    内置类型的成员变量在类中声明时可以给缺省值。
    当然,实际操作中大部分时候是需要自己去写构造函数,而不是只靠默认的。

1.1.1构造体函数赋值

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

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

虽然前述构造函数调用后,对象中已有初始值,但不能称之为对于对象中成员变量的初始化。
构造函数体中的语句只能将其称为赋初值,而不能称作初始化。
因为初始化只能进行一次,而构造函数体内可以多次赋值

1.1.2初始化列表

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

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

初始化列表特性:

  1. 初始化列表能只能初始化一次,多次初始化会报错
    -编译器也允许构造函数赋初值和初始化列表初始化混用。混用时初始化列表初始化和构造函数赋初值不冲突,但混用时初始化列表初始化还是要遵循只能初始化一次成员变量的原则。
  2. const成员变量、引用成员变量、没有默认构造函数的自定义类型成员,必须使用初始化列表来进行初始化。
    原因:
    -初始化列表是对象的成员变量定义的地方。
    -对象的内置类型成员变量在初始化列表定义时并不要求必须初始化,因此既可以在初始化列表进行初始化,也可以在构造函数体内初始化。
    -const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量不能先定义再初始化——它们在初始化列表内定义,且必须在定义时就初始化。
class A{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		,_ref(ref)
		,_n(10)
	{}
private:
	A _aobj; //无默认构造函数
	int& _ref; //引用
	const int _n; //const
};
  1. 尽量使用初始化列表来进行初始化。因为不论是否使用初始化列表,自定义类型成员变量一定会先使用初始化列表来进行初始化。
  2. 成员变量在初始化列表中的初始化顺序是其在类中的声明次序,与其在初始化列表中的先后次序无关。

1.1.3 explicit关键字(修饰构造函数)

-构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
-C++中的explicit关键字只能用于修饰只有一个参数的类构造函数。它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)
-用explicit修饰构造函数,将会禁止构造函数的隐式转换。
-explicit关键字只能用于类内部的构造函数声明上,在类定义外部不能重复它。

注:c++98只支持单参构造函数的隐式类型转换。而c++11支持多参构造函数的隐式类型转换,如此使用时原本在其中放置参数的小括号()需改成大括号{}。

class Date{
public:

//1. 单参构造函数,没有使用explicit修饰,具有类型转换作用。
//explicit修饰构造函数,禁止类型转换。explicit去掉之后,代码可以通过编译。
explicit Date(int year)
	:_year(year)
{}

/*
//2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用。
//explicit修饰构造函数,禁止类型转换。
explicit Date(int year, int month = 1, int day = 1)
	: _year(year)
	, _month(month)
	, _day(day)
{}
*/

Date& operator=(const Date& d){
	if (this != &d){
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
return *this;
}
private:
int _year;
int _month;
int _day;
};

void Test(){
	Date d1(2023);
	//用一个整形变量给日期类型对象赋值。
	//实际编译器背后会用2024构造一个无名对象,最后用无名对象给d1对象进行赋值。
	d1 = 2024;
	//将1.注释掉,2.解除注释则编译失败,因为explicit修饰的构造函数,禁止了单参构造函数类型转换的作用。
}

1.2析构函数:

是特殊成员函数,与构造函数功能相反,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数不负责对对象本身的销毁,局部对象销毁工作是由编译器完成的。

  1. 析构函数名通过在类名前加上字符 ~ 得到,其含义是"构造函数取反"。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,由C++编译器自动调用析构函数。对于相同生命周期的多个类,先定义的后析构,后定义的先析构.

注意:同默认构造函数,默认析构函数只会为class,struct等自定义类型成员调用,而对int,char等内置类型成员不做处理。

1.3拷贝构造函数:

是特殊成员函数,只有单个形参,该形参是对本类类型对象的引用(通常会用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,若使用传值方式会引发无穷递归调用:
    -调用拷贝构造需要先传参数,传值传参本身又会是一个拷贝构造,如此便会引发无穷递归,卡在传参。
  3. 若未显式定义,系统生成默认的拷贝构造函数,编译器生成的默认拷贝构造函数可以完成字节序的值拷贝,而有些类还是需要自己实现:
    -默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
    -日期类这种的拷贝用默认生成的就好,但对于栈类等,若直接使用浅拷贝会有"出作用域时某一被指向的空间被析构两次,进而程序崩溃"等各种问题,因此还需要了解深拷贝,自己实现拷贝构造函数来解决此类问题。

拷贝构造总结:

  1. 拷贝构造函数是构造函数的重载,参数只有一个,且必须使用引用传参。
  2. 默认的拷贝构造函数对于内置和自定义类型都会处理:
    -对于内置类型会按字节序进行值拷贝,即浅拷贝。
    -对于自定义的类型,会调用该类型自身的拷贝构造函数。
  3. 若析构函数需要自己实现以释放空间(这意味着涉及了资源管理),拷贝构造便也会需要自己实现。

为提高程序效率,一般对象传参及返回时,应尽量使用引用类型以减少不必要的拷贝。参数基本都可以传引用,返回值需根据具体情况判断是否需要拷贝构造来选择传值还是传引用。

1.4赋值运算符重载

1.4.1运算符重载

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

函数名为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)。
参数与运算符的操作数对应。对于有两个操作数的运算符的重载,第一个参数是左操作数,第二个参数是右操作数。

注意:

  1. 不能通过连接其他符号(非运算符号)来创建新的操作符:比如operator@
  2. 重载操作符必须有至少一个类类型(自定义类型)或者枚举类型的操作数。
  3. 用于内置类型的操作符,其含义不能改变。
    例如:内置的整型 + ,不能改变其含义。
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参。
  5. 重载操作符大部分情况下是在类里定义成员函数,是为了方便解决访问私有成员变量的问题。
    因此若在全局与类内都定义了同样的重载,编译器一般会优先调用类里的。
注意:
.*   ::   sizeof   ?:   .
以上5个运算符不能重载。这个知识点经常在笔试选择题中出现。

再次注意:
* 可以重载
.* 不可重载

1.4.2赋值运算符重载

是特殊成员函数。
就像其他运算符一样,可以重载赋值运算符 = ,用于为已初始化的对象赋值。
-当以拷贝的方式初始化一个对象时,会调用的是拷贝构造函数。而当给一个已实例化的对象赋值时,调用的会是赋值运算符重载函数。

赋值运算符重载格式:

  1. 返回值类型:T&,返回引用可以提高返回的效率。
    -存在返回值是为了支持连续赋值,使之符合 = 的使用方式。
  2. 参数类型:const T&,传引用可以提高传参效率。
  3. 检测是否在自己给自己赋值,并进行相应处理(比如跳过)。
  4. 返回*this :用于支持连续赋值,使之符合 = 的使用方式。

一个类若未显式定义赋值运算符重载,编译器会为其生成一个。
-编译器默认生成的赋值重载功能与拷贝构造类似:
1.对内置类型成员回完成字节序值拷贝,即浅拷贝(比如日期类就可以直接用默认生成的)
2.对自定义类型成员,会调用其自身的operator=
对于栈等浅拷贝不能满足需求的类型,需要自己实现赋值重载
动态开辟的"基本"都需要深拷贝(智能指针和迭代器不需要)

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

机制类似:
构造和析构的处理机制基本类似。
拷贝构造和赋值重载的处理机制基本类似。

都叫重载,但并不相关:
-运算符重载让自定义对象能直接使用运算符来进行相应的处理(比如日期减日期求得天数)。
-函数重载让同一个函数名能够针对不同参数有不同的操作。

1.5补充

C++把类型分为两类:内置类型(基本类型),自定义类型。
-内置类型:int/char/double/指针/内置类型的数组 等。
-自定义类型:struct/class定义的类型编译器生成的默认构造函数,对于内置类型成员变量不做初始化处理,而对自定义类型成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化,若无默认构造函数就会报错。
↑这个机制是考虑到不想释放指针、指针为文件指针等场景,为防误杀而设的。
↑这个不对内置/自定义类型成员变量统一处理的麻烦机制在C++11有些改良。
另,对于默认析构函数也是同理。

虽然C++标准并没有规定,但目前大部分编译器(比如VS)会优化传参和返回过程中连续的构造、拷贝构造,减少其次数。

考虑到优化:
对象返回时:

  1. 接收返回值对象,尽量用拷贝构造方式接收,而不是调用已经存在的对象进行赋值接收。
  2. 函数中返回对象时,尽量返回匿名对象。
    函数传参时:
  3. 尽量使用const 引用 传参。

2.代码

#include
using namespace std;

class Date {
public:
	//构造:
		//C++构造函数推荐实现全缺省或半缺省,因为好用
		//但要注意:语法上全缺省的和同名的无参函数可以同时存在,
			//,但同时存在时不能无参调用,因为会有歧义
	Date(int year = 0, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造:
		//Date d4(d1)的传参:d4传给this,d1传给d
	Date(const Date& d) {
		//若不使用引用,Date(Date d)会在传值传参时调用拷贝构造自身,导致无穷递归,编译报错,
		//,而使用引用,Date(Date& d)传的是别名,不会无穷递归
		//加const可以让编译器检查出“不小心写反成 d._year = _year; 变成反向赋值”之类的情况
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//析构:
	~Date() {
		cout << "~Date()" << endl;
	}
	//当前的Date类没有资源需要清理,所以不实现析构函数也可以,但是Stack等会需要

	//运算符重载(在类内实现)
	bool operator==(const Date& d){
		//与定义在全局时不同的是,这里隐含了一个this指针作为参数,因此用法也会有区别
		//原本的第一个参数便由this指针传递,第二个参数成为显式的第一个参数
		//重载后 d1 == d2 等效于:d1.operator==(d2)
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
		//若写明this指针,则:
		//return this->_year == d2._year
		//&& this->_month == d2._month
		//&& this->_day == d2._day;
	}

	//赋值运算符重载
	Date& operator=(const Date& d) {
		//传参可以不使用引用,因拷贝构造已另外存在而不会触发死递归。
			//当然最好还是引用 ,毕竟能省去多余的拷贝构造调用。
		
		if (this != &d) {
			//确保自我赋值时拷贝操作会被跳过。 d1 = d1 这种。
			//不只是确保效率,对于深拷贝的情况,自我赋值若不跳过拷贝操作,会出各种问题。
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		
		return *this;
		//为确保支持 a = b = c 这种连续赋值,需返回*this,即左操作数。
		//出作用域后*this还在,因此可使用引用返回,而不必使用会产生多余拷贝的值返回。
	}

	//打印年月日
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}


private://为演示全局的运算符重载,方便起见可直接将成员变量改为公有。
			//可使用公有函数间接访问私有变量,或使用友元解决此封装性问题(友元能少用就少用)。
			//当然,最直接的方式是将运算符重载实现在类的内部。
	int _year;
	int _month;
	int _day;
};

运算符重载(在全局实现)
//重载后 d2 == d5 等效于:operator==(d2,d5)
//bool operator==(const Date& d1, const Date& d2){
//	return d1._year == d2._year
//		&& d1._month == d2._month
//		&& d1._day == d2._day;
//}

class Stack {
public:
	Stack(int capacity = 4) {
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr) {
			cout << "malloc failed\n" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	void Push(int x) {

	}
	~Stack() {
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}//会自动调析构函数释放堆上的空间所以不用再主动使用destroy函数
private:
	int* _a;
	size_t _top;
	size_t _capacity;
};

int main() {
	Date d1;//构造函数的调用时,无参数就不能带括号写成Date d1() b;,不然就算编译通过实际上也未定义该对象
	Date d2(2023, 3, 4);
	Date d3(2023);
	Date d4(d1);//拷贝
	//Date d4 = d1;//也是拷贝

	Stack s1;//构造的顺序是s1->s2,而析构的顺序是s2->s1,因为在栈上
	Stack s2(20);

	Date d5(2023, 3, 6);
	Date d6(2023, 3, 7);
	Date d7(2023, 3, 8);
	
	//全局的运算符重载
	//cout << "equal:" << operator==(d5,d6) << endl;//可以看作一个名字特殊的函数,可显式调用。
	//cout << "equal:" << (d5 == d6) << endl;//d5 == d6会被编译器直接转换成调用operator==(d5,d6)的代码。

	//类内的运算符重载
	cout << "equal:" << d5.operator==(d6) << endl;//可以看作一个名字特殊的函数,可显式调用,d2对应的日期通过this指针作为参数传递。
	cout << "equal:" << (d5 == d6) << endl;//d5 == d6会被编译器直接转换成调用d5.operator==(d6)的代码。

	//赋值运算符重载
	d5 = d6;//d5.operator=(d6);
	d5.Print();
	d5 = d6 = d7;
	d5.Print();

	//易混点:拷贝构造与赋值重载
	Date d8 = d5;//等效于 Date d8(d5) 。这个是拷贝构造而不是赋值,赋值重载的操作值只能是已经实例化的对象。

	return 0;
}

你可能感兴趣的:(笔记,c++)