C++——类和对象2

目录

类的六个默认成员函数

1. 构造函数

1.1 特性

1.2 实现 

1.3 默认构造函数

1.4 默认构造函数处理行为

1.5 初始化列表

1.6 explicit关键字

2. 析构函数

2.1 特性

2.2 实现 

2.3 默认析构函数 

2.4 对象析构的顺序

3. 拷贝构造函数

3.1 特性

3.2 拷贝构造的底层实现 

3.3 const修饰 

3.4 默认拷贝构造函数

3.5 深浅拷贝

4. 赋值重载函数

2.1 赋值运算符重载格式

4.2 默认赋值运算符重载

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


类的六个默认成员函数

对于一个类,如果里面一个成员都没有,并不意味着这个类为空,对于任何一个类,都会默认生成六个成员函数(如果我们不自己写的话),它们是:

  • 构造函数——完成初始化工作
  • 析构函数——完成清理工作
  • 拷贝构造函数——使用同类对象进行初始化
  • 赋值重载函数——将一个对象赋值给另一个对象
  • 取地址重载——普通对象取地址
  • const修饰的取地址运算符重载——const修饰的对象取地址

1. 构造函数

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

1.1 特性

  1. 函数名与类名相同
  2. 无返回值(也不需要写void)
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载

1.2 实现 

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

	Date()                             // 重载,无参
	{
		_year = 2000;
		_month = 1;
		_day = 1;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 1, 9);  // 调用带参构造函数,d1=(2013,1,9)
	Date d2;              // 调用无参构造函数,d2=(2000,1,1)
	return 0;
}

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

Date d3(); // 这是一个返回值为Date类型,函数名为d3的无参函数声明

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值,上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值,例如

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
        _day = 1;     // 这里显然不是_day的首次赋值
	}

private:
	int _year;
	int _month;
	int _day;
};

 成员变量真正的初始化是在初始化列表中完成的,见1.5小节

1.3 默认构造函数

默认构造函数有三类:

  1. 用户没有显式定义,编译器自动生成的无参的构造函数
  2. 无参的构造函数(自己写的)
  3. 全缺省的构造函数 (自己写的)

所谓的默认构造函数,就是即使不显式地传参也能完成初始化工作的构造函数,这种函数只能同时存在一个,否则编译器无法确定该调用哪个函数,产生歧义

class Date
{
public:
	Date()              // 无参的默认构造函数
	{
		_year = 2000;
		_month = 1;
		_day = 1;
	}

	Date(int year = 2000, int month = 1, int day = 1)  // 全缺省的默认构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;    // VS报错:“Date::Date”: 对重载函数的调用不明确	
	return 0;
}

对于两种用户自己实现的默认构造函数很好理解,我们几乎可以随心所欲地控制对象的初始化,但是对于编译器自动生成地默认构造函数我们同样需要了解,有时候能够帮助我们更好的控制代码

对于下面的类,并没有显式给出默认构造函数,编译器会自动生成

class Date
{
private:
	int _year;
	int _month;
	int _day;
};

正如C语言全局变量的初始化在编译阶段就完成了一样,在C++中,全局对象的构造函数会在main 函数之前执行,

在Linux环境下,通过查看反汇编的方式来观察底层的实现,发现在地址4006a3处,有一个callq指令,调用了<_Z41__static_initialization_and_destruction_0ii>函数,很显然这个函数就是编译器所生成的默认构造函数

0000000000400695 <_GLOBAL__sub_I_main>:
  400695:	55                   	push   %rbp
  400696:	48 89 e5             	mov    %rsp,%rbp
  400699:	be ff ff 00 00       	mov    $0xffff,%esi
  40069e:	bf 01 00 00 00       	mov    $0x1,%edi
  4006a3:	e8 b0 ff ff ff       	callq  400658 <_Z41__static_initialization_and_destruction_0ii>
  4006a8:	5d                   	pop    %rbp
  4006a9:	c3                   	retq   
  4006aa:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

1.4 默认构造函数处理行为

实际上,通过调试会看到,调用编译器的默认构造函数后,成员变量仍为随机值,这是因为 

C++将类型分为两类:

  • 内置类型(基本类型):已经定义好了的类型,如int,char,double,指针等
  • 自定义类型:class/struct/union等自己定义的类型
  • 对内置类型成员:不进行处理
  • 对自定义类型成员:调用这个自定义类型的默认构造函数

因此,默认生成的构造函数对于成员变量是内置类型的类基本没有用处,但是对于一些特殊的类却很有价值,比如成员变量全是自定义类型的类

// 成员变量均为基本类型,默认生成的构造函数初始化成随机值
class A
{
private:
	int _a;
	char _b;
	double* _p;
};
// A中存在默认构造函数
class A
{
public:
	A()
	{
		_aa = 1;
	}

private:
	int _aa;
};

// 成员变量全为自定义类型A,默认构造函数会去调用A的默认构造函数
class B
{
private:
	A _a1;  // 自定义类型
	A _a2;
    // A* _p;  // 内置类型(任何指针都是内置类型)
};

为了弥补默认构造函数无法对内置类型进行初始化这一问题,C++11提出了一种新的办法,即在成员变量的声明中增加缺省值,而这个缺省值是给默认构造函数的(实际上是给初始化列表的),这样默认构造函数就可以完成对内置类型成员变量的初始化

class Date
{
private:
	int _year = 2000;  // 声明时给出缺省值
	int _month = 1;
	int _day = 1;
};

int main()

	Date d;   {_year=2000 _month=1 _day=1 }
	return 0;
}

1.5 初始化列表

1️⃣ 定义 

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式,每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

class Date
{
public:
	Date(int year, int month, int day)
	: _year(year)     // Date类的初始化列表
	, _month(month)
	, _day(day)
{}

private:
	int _year;
	int _month;
	int _day;
};

2️⃣ 功能 

构造函数的执行可以分成两个阶段

  1. 初始化阶段:所有类类型的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中
  2. 计算阶段 :一般用于执行构造函数体内的赋值操作

初始化列表和在函数体内赋值实际上完成的功能类似,都是给成员变量赋初值,但初始化阶段先于计算阶段

初始化列表可以被认为是成员变量定义的地方 

int a;   // 定义
a = 10; // 赋值

3️⃣ 必须使用初始化列表的场景

但在一些特殊的情况下,无法使用函数体内赋值完成对象的初始化工作 

类中如果包含三种成员,必须通过初始化列表初始化

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数)

(引用和const对象必须在定义的时候初始化) 

class Time        // Time类没有默认构造函数
{
	Time(int hour)
	{
		_hour = hour;
	}

private:
	int _hour;
};

class Date
{
public:
	Date(int hour, const int cyear = 1, int ref = 1)
	{
		Time time(hour);    // Time没有合适的默认构造函数
		_cyear = cyear;     // 必须初始化常量限定类型的对象, 表达式必须是可修改的左值		
		_ref = ref;			// 必须初始化引用
	}

private:
	Time _time;             // 自定义类型成员变量(无默认构造)
	const int _cyear;       // const成员变量 
	int& _ref;              // 引用
};

上述类的初始化可以使用初始化列表进行初始化

class Time
{
public:
	Time(int hour)
	{
		_hour = hour;
	}

private:
	int _hour;
};

class Date
{
public:
	Date(int hour = 1, const int cyear = 1, int ref = 1)
		:_time(hour)
		,_cyear(cyear)
		,_ref(ref)
	{}

private:
    // 这里都是声明
	Time _time;             // 自定义类型成员变量(无默认构造)
	const int _cyear;       // const成员变量 
	int& _ref;              // 引用
    int _year = 2000;       // 在声明处给的缺省值,实际上是给初始化列表的
};                             (C++11,如果显示给了,这里的缺省值就不起作用了)

4️⃣ 成员变量顺序

 下面的代码,输出的结果是多少?

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();
}

 1    随机值

成员变量在初始化列表中的初始化顺序就是在类中声明次序,与其在初始化列表中的先后次序无关,在A类中,_a2先于_a1声明,因此无论在初始化列表中二者是什么顺序,都是先初始化_a2,后初始化_a1,而_a2又是由_a1初始化的,造成_a2未定义

因此一个好的习惯是,按照成员声明的顺序进行初始化

5️⃣ 初始化列表和构造函数的区别

考虑下面的代码

class Time
{
public:
	Time(int hour = 1)
		:_hour(hour)
	{
		cout << "Time(int hour = 1) -- construction" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)   // Time(int hour = 1) -- construction
		:_year(year)
		,_month(month)
		,_day(day)
		,_time()
	{}
 
	Date(int year = 2000, int month = 1, int day = 1)  // Time(int hour = 1) -- construction                                            
	{                                                   Time(int hour = 1) -- construction
		_year = year;
		_month = month;
		_day = day;
		_time = Time();
	}

private:
	int _year;
	int _month;
	int _day;
	Time _time;
};

对于一个自定义的Time类,内部存在默认构造函数,可以使用初始化列表初始化,也可以在函数体内自己初始化,对于这两种情况,使用初始化列表调用一次构造函数,使用函数体初始化需要调用两次构造函数(_time在初始化列表初始化构造一次,函数体内Time()构造第二次),因此使用初始化列表初始化自定义类对象可以提高效率 

🔶对于初始化列表

  • 内置类型和自定义类型,都建议使用初始化列表
  • 建议按照成员变量声明的顺序初始化

1.6 explicit关键字

explicit:指定构造函数或转换函数 (C++11起)为显式,,即它不能用于隐式转换和复制初始化 

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用

class Date
{
public:
	explicit Date(int year)
		:_year(year)
	{
		cout << "Date(int year) -- constrution" << endl;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d) -- copy_constrution" << endl;
	}

private:
	int _year;
};

int main()
{
	Date d1(2023);  // 直接调用构造
	Date d2 = 2023; // 隐式类型转换:构造+拷贝构造+编译器优化->直接调用构造

	return 0;
}

Date d2 = 2023 中,首先2023发生隐式类型转换,生成一个Date类型的临时变量,然后用这个临时变量去拷贝构造d2,但经过编译器的优化以后,变成了直接调用构造,在这一过程中,发生构造函数完成了int 到 Date类型的隐式类型转换

用explicit修饰构造函数,将会禁止构造函数的隐式转换,上述代码无法编译成功 

🔶对于构造函数

  • 对于大多数的类而言,不使用编译器默认生成的构造函数,最好自己写一个(全缺省)
  • 对于一些特殊的类而言(比如上面的B类,不包含指针时,成员变量全为自定义类型),可以交给编译器自动生成

2. 析构函数

析构函数:析构函数不是完成对对象本身的销毁(局部对象销毁工作是由编译器完成的),而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

2.1 特性

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
  4. 析构函数不能重载
  5.  对象生命周期结束时,C++编译系统系统自动调用析构函数

2.2 实现 

class Date
{
public:
	Date()            
	{}

	~Date()  // 析构函数
	{
		cout << "析构" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;  
	return 0;
}

对于内置类型,一般我们写不写析构函数都无所谓,编译器会自动帮我们做好回收工作,但是对于一些自定义类型,特别是在堆区申请过空间的,通常要用到析构函数来完成资源的回收。这是因为对于用户使用malloc或new动态申请的堆区资源,编译器不会自动回收,需要我们显式地使用free或delete进行资源回收,否则容易造成内存泄漏问题,将资源清理工作在析构时完成,可以确保每个对象所申请的堆区资源都能够在对象销毁时得到释放

typedef int DataType;
class Stack
{
	Stack(size_t capacity = 3)
	{
		// 在构造时申请了空间
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		// ...
	}

	~Stack()
	{
		free(_array);   // 在析构时释放申请的空间
		_array = NULL;
		_capacity = 0;
		_size = 0;
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

2.3 默认析构函数 

同默认构造函数一样,如果我们不显式定义,系统会自动生成默认的析构函数,对于上面的Date类,默认析构函数并不会起到什么作用,因为类中的成员变量都是内置类型成员,销毁时不需要资源清理,直接由系统直接将其内存回收即可

默认析构函数对内置类型和自定义类型成员的处理行为和默认构造函数相同

  • 对内置类型成员:不进行处理
  • 对自定义类型成员:调用这个自定义类型的默认析构函数
class A
{
public:
	~A()  // 默认析构函数
	{
		cout << "析构" << endl;
	}

private:
	int _aa;
};


// 成员变量全为自定义类型A,默认析构函数会去调用A的默认析构函数
class B
{
private:
	A _a1;  // 自定义类型
	A _a2;
};

int main()
{
	B b;
	return 0;
}

上面这段代码在main函数中只定义了一个B类型的变量b,但程序退出时输出了两个“析构”,这是因为虽然main函数不能调用A的析构函数,但是在B进行析构时,需要对A类对象_a1和_a2完成释放,因此编译器会调用B类的析构函数,而B类没有显示提供,因此编译器会给B类生成一个默认的析构函数,目的是在其内部调用A类的析构函数,即当B对象销毁时,要保证其内部每个自定义对象都可以正确销毁

2.4 对象析构的顺序

下面的代码创建了三个对象(在栈上) ,可以查看构造和析构的顺序

class A
{
public:
	A(int a = 0)
	{
		_a = a;
		std::cout << "constructor: " << _a << std::endl; // 构造
	}

	~A()
	{
		cout << "destructor: " << _a << std::endl;       // 析构
	}

private:
	int _a;
};


int main()
{
	A a1(1);
	A a2(2);
	A a3(3);

	return 0;
}
// 得到的结果为

constructor: 1
constructor: 2
constructor: 3
destructor: 3
destructor: 2
destructor: 1
  • 构造时的顺序是 a1,a2,a3
  • 析构时的顺序是 a3,a2,a1

这是因为这三个对象都是在栈上的临时变量,符合后构造先销毁的性质

对于更复杂的场景

class A
{
public:
	A(int a = 0)
	{
		_a = a;
		std::cout << "constructor: " << _a<< std::endl;
	}

	~A()
	{
		cout << "destructor: " << _a << std::endl;
	}

private:
	int _a;
};

A a5(5);             // 全局变量
static A a6(6);      // 全局静态变量

int main()
{
	static A a0(0);  // 局部静态变量
	A a1(1);         // 局部变量
	A a2(2);
	A a3(3);
	static A a4(4);

	return 0;
}
// 结果为
constructor: 5
constructor: 6
constructor: 0
constructor: 1
constructor: 2
constructor: 3
constructor: 4

destructor: 3
destructor: 2
destructor: 1
destructor: 4
destructor: 0
destructor: 6
destructor: 5

构造时

  • 全局变量 a5 和 a6 在进入main函数之前,调用构造函数完成初始化工作(PS:C和C++中的全局变量(不包括类)是在编译阶段就完成初始化的,类对象是在进入main函数前初始化的)
  • 局部静态变量 a0 和 a4 的初始化与局部变量相同(PS:具有全局生命期的局部静态变量的初始化,与局部变量相同都是在运行时,执行到该初始化语句完成初始化的,只是局部静态变量只初始化一次)

析构时

  • main函数栈帧上的局部变量a1,a2,a3按照后构造先析构的顺序析构
  • 静态区的变量a4,a0按照后构造先析构的顺序析构
  • 全局变量a5,a6按照后构造先析构的顺序析构

🔶对于析构函数

  • 当类中没有申请资源时,可以不写析构函数,直接使用编译器生成的默认析构函数,比如Date类
  • 当类中有资源申请时,一定要显式提供析构函数并释放资源,否则会造成资源泄漏,比如Stack类

3. 拷贝构造函数

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

3.1 特性

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
  3. 若未显式定义,编译器会生成默认的拷贝构造函数

3.2 拷贝构造的底层实现 

对于内置类型的拷贝,考虑下面的代码

int main()
{
    int a = 10;  // 将10赋值给a
    int b = a;   // 将a的值拷贝给b,完成对b的赋值

	return 0;
}

所得到的汇编为

000000000040050d <main>:
  40050d:	55                   	push   %rbp
  40050e:	48 89 e5             	mov    %rsp,%rbp
  400511:	c7 45 fc 0a 00 00 00 	movl   $0xa,-0x4(%rbp)   // a = 10
  400518:	8b 45 fc             	mov    -0x4(%rbp),%eax   // %eax = a
  40051b:	89 45 f8             	mov    %eax,-0x8(%rbp)   // b = %eax
  40051e:	b8 00 00 00 00       	mov    $0x0,%eax
  400523:	5d                   	pop    %rbp
  400524:	c3                   	retq   
  400525:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  40052c:	00 00 00 
  40052f:	90                   	nop

这份代码中,先完成对a的赋值,然后将a拷贝到寄存器%eax中,再使用%eax对b进行赋值,这便是C++中对于内置类型的拷贝改造实现

对于自定义类型,拷贝构造就是利用一个对象去完成另一个对象的初始化

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)  // 默认构造       
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(Date& d)              // 拷贝构造,这里必须是引用,且参数是对象的引用
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2008, 8, 8);  // 构造对象d1
	Date d2(d1);          // 使用d1的拷贝构造对象d2        

	return 0;
}

我们来看看上述代码在底层是怎么实现的:

objdump -d 生成反汇编
000000000040050d <main>:
  40050d:	55                   	push   %rbp
  40050e:	48 89 e5             	mov    %rsp,%rbp   
  400511:	48 83 ec 20          	sub    $0x20,%rsp               // 分配栈帧,32个字节
  400515:	48 8d 45 f0          	lea    -0x10(%rbp),%rax         // 取d1地址(%rbp-16),复制到寄存器%rax
  400519:	b9 08 00 00 00       	mov    $0x8,%ecx                // %exc 保存8
  40051e:	ba 08 00 00 00       	mov    $0x8,%edx                // %edx 保存8
  400523:	be d8 07 00 00       	mov    $0x7d8,%esi              // %esi 保存2008
  400528:	48 89 c7             	mov    %rax,%rdi                // %rdi 保存&d1(也就是this指针)
  40052b:	e8 1a 00 00 00       	callq  40054a <_ZN4DateC1Eiii>  // 调用默认构造
  400530:	48 8d 55 f0          	lea    -0x10(%rbp),%rdx         // 取d1地址(%rbp-16),复制到寄存器%rdx
  400534:	48 8d 45 e0          	lea    -0x20(%rbp),%rax         // 取d2地址(%rbp-32),复制到寄存器%rax
  400538:	48 89 d6             	mov    %rdx,%rsi                // %rsi 保存&d1(d1的this)
  40053b:	48 89 c7             	mov    %rax,%rdi                // %rdi 保存&d2(d2的this)
  40053e:	e8 37 00 00 00       	callq  40057a <_ZN4DateC1ERS_>  // 调用拷贝构造
  400543:	b8 00 00 00 00       	mov    $0x0,%eax
  400548:	c9                   	leaveq 
  400549:	c3                   	retq   

000000000040057a <_ZN4DateC1ERS_>:
  40057a:	55                   	push   %rbp                     // 保存main函数帧指针
  40057b:	48 89 e5             	mov    %rsp,%rbp                // 创建拷贝改造函数帧指针
  40057e:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)          // &d2保存在地址(%rbp-8)处
  400582:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)         // &d1保存在地址(%rbp-16)处
  400586:	48 8b 45 f0          	mov    -0x10(%rbp),%rax         // %rax 保存&d1
  40058a:	8b 10                	mov    (%rax),%edx              // %edx 保存*(&d1),即2008
  40058c:	48 8b 45 f8          	mov    -0x8(%rbp),%rax          // %rax 保存&d2
  400590:	89 10                	mov    %edx,(%rax)              // d2._year = 2008;
  400592:	48 8b 45 f0          	mov    -0x10(%rbp),%rax         // 重复上面四步
  400596:	8b 50 04             	mov    0x4(%rax),%edx
  400599:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40059d:	89 50 04             	mov    %edx,0x4(%rax)
  4005a0:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  4005a4:	8b 50 08             	mov    0x8(%rax),%edx
  4005a7:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4005ab:	89 50 08             	mov    %edx,0x8(%rax)
  4005ae:	5d                   	pop    %rbp
  4005af:	c3                   	retq   

这里对于默认构造函数的实现进行了省略,调用默认构造时的栈帧为

调用拷贝构造时的栈帧为

3.3 const修饰 

因此在拷贝构造的过程中,虽然参数是引用,可底层还是通过两个指针完成的,也正是因为使用传引用传参,在函数中存在修改原对象的风险,因此最好带上const修饰

Date(const Date &d);  // 加上const修饰
{}

Date d2(d1); 

🔶为什么拷贝构造函数的参数只能是引用传参而不能是传值传参呢?

传值传参时,会发生下面的情况

  1. 传值传参时,先要生成一份原对象的临时备份,由这个备份作为参数传给实参
  2. 生成这个临时备份时,需要去调用拷贝改造函数来生成
  3. 调用拷贝改造是传值传参,又需要生成一份临时拷贝,而这个拷贝需要调用拷贝改造
  4. 如此循环引发无穷递归问题...

传引用传参省略了构建临时对象这一步,直接使用原对象作为参数,可以解决传值传参的问题

3.4 默认拷贝构造函数

有别于默认构造函数和默认析构函数,编译器默认生成的拷贝构造函数会对内置类型进行处理也会对自定义类型进行处理,这些实现都通过字节序的拷贝完成

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

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2000, 1, 2);
	Date d2(d1);          // 编译器生成的默认拷贝构造完成值(浅)拷贝
	return 0;
}

在编译器生成的默认拷贝构造函数中

  • 对内置类型成员:是按照字节方式直接拷贝
  • 对自定义类型成员:调用其拷贝构造函数完成拷贝 

3.5 深浅拷贝

默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

具体深浅拷贝问题见

🔶对于拷贝构造

  • 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以
  • 一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

4. 赋值重载函数

基本的赋值运算符 是“=”,对于 = 的重载必须作为成员函数,用于给同类型对象的赋值

2.1 赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要符合连续赋值的含义 
class Date
{
public:
	Date(int year = 2000, 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;                 // 返回 *this
	}

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2000, 1, 2);
	Date d2;

    // d2.operator=(&d2, d1)
	d2 = d1;                       // 使用d1给d2赋值

	return 0;
}

4.2 默认赋值运算符重载

如果我们不显式提供赋值运算符重载,编译器会自动生成一个默认赋值运算符重载,以值的方式逐字节拷贝,因此,赋值运算符只能重载成类的成员函数不能重载成全局函数,因为会和编译器默认生成的产生冲突

赋值运算符重载对两种类型处理

  • 对内置类型成员:直接进行赋值
  • 对自定义类型成员:调用这个自定义类型的赋值运算符重载

赋值运算符重载和拷贝构造类似,也会涉及深浅拷贝的问题

🔶对于赋值运算符重载

  • 如果类中未涉及到资源管理,赋值运算符重载是否实现都可以
  • 如果类中涉及到资源管理,必须要实现赋值运算符重载

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

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

class Date
{
public:
	Date* operator&()
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}

private:
	int _year; 
	int _month; 
	int _day; 
};

取地址&操作符重载 this的类型:

this指针的指向不可修改,但是this指针指向的对象中的内容可以修改
Date* const

const修饰的取地址&操作符重载 this的类型:

前面const修饰返回值,后面的const用来修饰成员函数,将const修饰的成员函数称为const成员函数
const Date* const
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值