一、构造函数
#include <iostream>
using namespace std;
class Date
{
public:
void Init()
{
_year = 1970;
_month = 1;
_day = 1;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init();
//输出 1970年1月1日
d1.Print();
Date d2;
//输出 -858993460年 - 858993460月 - 858993460日
d2.Print();
return 0;
}
我们写出的 Date 类,实例化对象后,对象的成员的成员变量默认是随机值,于是每个对象都需要调用初始化函数,难免会有忘记的时候
1. 构造函数的定义
C++ 提供了一个特殊的成员函数 用于初始化对象的成员变量 叫做构造函数,可以很好的解决这个问题
构造函数的特点:
- 实例化对象时,自动调用,在对象的声明周期中只会调用一次
- 函数名和类名相同
- 无返回值,指的是不能写返回值
- 构造函数可以重载,类的对象可以有多种初始化方式
将类中的 Init 函数改为构造函数,并提供多个构造函数
#include <iostream>
using namespace std;
class Date
{
public:
//无参构造函数
Date()
{
_year = 1970;
_month = 1;
_day = 1;
//验证是否调用了构造函数
cout << "Date()" << " ";
}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
//验证是否调用了构造函数
cout << "Date(int year, int month, int day)" << " ";
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
构造函数是在实例化对象的时候自动调用的
int main()
{
//调用无参构造函数
Date d1;
//输出 Date() 1970年1月1日
d1.Print();
//传参调用构造函数
Date d2(2023, 2, 4);
//Date(int year, int month, int day) 输出 2023年2月4日
d2.Print();
return 0;
}
-
为什么不允许 d1.Date() 调用构造函数呢?
如果可以这样调用便和我们写的 Init 函数,然后自己调用没有区别,并且构造函数只能调用一次 -
为什么不传参调用构造函数时不加括号?
如果加上扩号则为 Date d1(); 此时和声明一个函数的返回值是 Date,函数名为 d1,无参的函数一模一样,便会产生歧义
2. 编译器生成的构造函数
如果类中没有构造函数,编译器会自动生成一个无参的构造函数,如果存在构造函数,便不会生成
编译器自动生成的构造函数的行为是什么呢?
- 内置类型的成员变量不做处理
内置类型包括(char short int long float double 型,各种类型的指针,数组元素是内置类型等) - 自定义类型的成员变量调用该类型的默认构造函数(不需要传参的构造函数)
自定义类型(类,结构体,联合体)
#include <iostream>
using namespace std;
//时间类
class Time
{
public:
//时间类的构造函数
Time()
{
_hour = _minute = _second = 0;
}
void Print()
{
cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
//日期类
class Date
{
public:
//无构造函数
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << " ";
_time.Print();
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d;
//默认构造函数的行为:内置类型不处理,自定义类型调用他的默认构造函数
//输出 -858993460年-858993460月-858993460日 0时0分0秒
d.Print();
return 0;
}
3. 默认构造函数
无参构造函数、全缺省构造函数、我们没写编译器自动生成的构造函数,这些都是 不需要传参就可以调用的构造函数,称之为默认构造函数,默认构造函数只能有一个
#include <iostream>
using namespace std;
class Date
{
public:
//只有带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//报错,没有合适的默认构造函数可用
Date d1;
return 0;
}
上述代码中,由于存在带参数的构造函数,编译器便不会自动生成构造函数,于是就没有默认构造函数可用
因此如果是自己写构造函数,需要提供默认构造函数,一般提供全缺省
//全缺省构造函数
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
4. 初始化列表
构造函数体内的赋值语句其实并不是成员变量初始化的地方,因为初始化只能有一次,而构造函数体内可以对成员变量进行多次赋值
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
//构造函数体内可以对成员变量进行多次赋值
//构造函数体内并不是成员变量初始化的地方
_year = year;
_month = month;
_day = day;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
初始化列表才是对成员变量初始化的地方
初始化列表:在构造函数括号后,以冒号开始,逗号分隔的成员列表,每个成员后跟一个放在括号中的初始值或者构造函数中的参数
#include <iostream>
using namespace std;
//时间类
class Time
{
public:
Time()
//初始化列表
: _hour(0)
, _minute(0)
, _second(0)
{
}
Time(int hour, int minute, int second)
//初始化列表
: _hour(hour)
, _minute(minute)
, _second(second)
{
}
void Print()
{
cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
//日期类
class Date
{
public:
Date()
//初始化列表
: _year(1970)
, _month(1)
, _day(1)
{
}
Date(int year, int month, int day, int hour, int minute, int second)
//初始化列表
: _year(year)
, _month(month)
, _day(day)
, _time(hour, minute, second)
{
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << " ";
_time.Print();
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
d1.Print(); //输出 1970年1月1日 0时0分0秒
Date d2(2023, 2, 8, 22, 58, 24);
d2.Print(); //输出 2023年2月8日 22时58分24秒
return 0;
}
注意:
- 每个成员变量在初始化列表中只能出现一次
初始化只能初始化一次 - 内置类型 如果未出现在初始化列表中则不做处理
- 自定义类型 如果未出现在初始化列表中则调用该类型的默认构造函数
- 类中包含以下成员时,必须放在初始化列表位置进行初始化:
引用成员变量,const 成员变量,自定义类型成员(且该类没有默认构造函数时)
建议尽量使用初始化列表对成员变量初始化,因为无论如何自定义类型都会根据初始化列表来进行初始化的方式
注意:成员变量初始化完成之后才会进入构造函数体中,构造函数体中只能用来赋值,当成员变量存在数组时,就可以在构造函数体内赋值
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
//根据成员声明顺序
//先用随机值初始化_a1,在用 a 初始化 _a2
: _a2(a)
, _a1(_a2)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A aa(1);
aa.Print(); //输出 -858993460 1
return 0;
}
5. 内置成员变量指定缺省值(C++11)
由于编译器自动生成的构造函数对内置类型不做处理,不是很好,到了 C++11 给这里打了补丁,内置类型的成员变量在类中声明时可以指定缺省值
我们没写构造函数时,编译器生成的默认构造函数,内置类型使用缺省值初始化成员变量(C++11)
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print(); //输出 1970年1月1日
return 0;
}
我们写了构造函数时,内置类型成员变量在初始化时,先使用初始化列表中指定的值,如果初始化列表中没有指定,才使用成员变量声明时指定的缺省值(C++11),如果都没有,则只定义不初始化(随机值)
#include <iostream>
using namespace std;
class Date
{
public:
Date()
: _year(1970)
{
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 2023;
int _month = 1;
int _day;
};
int main()
{
//_year 即在初识化列表中指定,又在声明时给缺省值
//_month 只在声明时给了缺省值
//_day 什么都没有给
Date d1;
d1.Print(); //输出 1970年1月-858993460日
return 0;
}
二、析构函数
#include <iostream>
using namespace std;
typedef int StackDataType;
class Stack
{
public:
Stack(int capacity = 4)
: _capacity(capacity)
, _top(0)
{
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
}
void Push(StackDataType x)
{
//扩容...
//插入
_a[_top] = x;
++_top;
}
//...
void Destroy()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
}
private:
StackDataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
st.Push(1);
//使用...
return 0;
}
对于像栈这样,需要在堆上开辟内存空间存储数据的类,每个对象使用完时都需要调用销毁函数,否则会导致内存泄漏,和调用初识化函数一样,难免会有忘记的时候
1. 析构函数的定义
C++ 提供了一个特殊的成员函数 用于清理对象资源 叫做析构函数,可以很好的解决这个问题,他的工作和构造函数相反,可以将析构函数的特性和构造函数对比
析构函数的特点:
- 对象生命周期结束时,编译器会自动调用析构函数
- 函数名是在类名前加上字符 ~
- 无参数,无返回值,指的是不能写返回值
- 一个类只能有一个析构函数,析构函数不能重载
将类中的 Destroy 函数改为析构函数
#include <iostream>
using namespace std;
typedef int StackDataType;
class Stack
{
public:
Stack(int capacity = 4)
: _capacity(capacity)
, _top(0)
{
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
}
void Push(StackDataType x)
{
//扩容...
//插入
_a[_top] = x;
++_top;
}
//...
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//验证是否调用了析构函数
cout << "~Stack()" << " ";
}
private:
StackDataType* _a;
int _top;
int _capacity;
};
析构函数是在对象销毁的时候自动调用的
int main()
{
Stack st;
st.Push(1);
//使用...
//可以手动调用析构函数
st.~Stack();
return 0;
}
输出 ~Stack() ~Stack()
析构函数允许 st.~Stack() 调用,并且无论什么时候,只要在对象销毁时,编译器都会自动调用析构函数
2. 编译器生成的析构函数
如果类中没有析构函数,编译器会自动生成析构函数,那编译器自动生成的析构函数的行为是什么呢?
- 内置类型的成员变量不做处理,也不需要处理(因为不用释放资源)
- 自定义类型的成员变量调用该类型的析构函数
#include <iostream>
using namespace std;
typedef int StackDataType;
//栈
class Stack
{
public:
Stack(int capacity = 4)
: _capacity(capacity)
, _top(0)
{
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
}
void Push(StackDataType x)
{
//扩容...
//插入
_a[_top] = x;
++_top;
}
//...
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//验证是否调用了析构函数
cout << "~Stack()" << " ";
}
private:
StackDataType* _a;
int _top;
int _capacity;
};
//两个栈的类
class Queue
{
public:
void Push(StackDataType x)
{
_pushS.Push(x);
}
//...
//没有析构函数
private:
Stack _pushS;
Stack _popS;
};
int main()
{
Queue q;
return 0;
}
输出 ~Stack() ~Stack()
3. 自己写的析构函数的执行方式
我们自己写的析构函数,会先执行析构函数体中的内容,然后再调用自定义类型的析构函数
在上个代码中,在 Queue 类中加上析构函数,输出 ~Queue() ~Stack() ~Stack()
~Queue()
{
cout << "~Queue()" << " ";
}
三、拷贝构造函数
1. C语言值传递和返回值时存在 bug
在用 C语言写栈这些数据结构时,我们在进行参数传递时,都是采用传地址的方式,一直以为传值的方式,只是因为效率不高,其实对于像栈这样,需要在堆上开辟内存空间存储数据的结构,以传值的方式传参会存在 bug
#include <stdio.h>
#include <stdlib.h>
typedef int StackDataType;
//栈结构
typedef struct Stack
{
StackDataType* a;
int top;
int capacity;
}Stack;
//初始化
void Init(Stack* ps)
{
ps->a = (StackDataType*)malloc(sizeof(StackDataType) * 4);
if (ps->a == NULL)
{
exit(-1);
}
ps->capacity = 4;
ps->top = 0;
}
//插入
void Push(Stack* ps, StackDataType x)
{
ps->a[ps->top] = x;
ps->top++;
}
//测试
void test(Stack s)
{
s.a[0] = 0;
}
int main()
{
Stack s;
Init(&s);
Push(&s, 1);
printf("%d\n", s.a[0]); //输出 1
test(s);
printf("%d\n", s.a[0]); //输出 0
return 0;
}
根据输出结果,发现 采用值传递的方式调用 test 函数,尽然可以修改栈的数据,为什么呢?
C语言中,值传递是通过按字节的方式将实参拷贝给形参(按字节拷贝称为浅拷贝)
于是在 C++中,为了避免这种情况,如果函数传递的参数是自定义类型时,需要调用该类型的拷贝构造函数,同样的 返回值是自定义类型时,也需要调用该类型的拷贝构造函数
2. 拷贝构造函数的定义
拷贝构造函数是 C++ 提供的一个特殊的成员函数,用于创建一个与已存在对象一某一样的新对象
拷贝构造函数是构造函数的重载,具有构造函数的特性,但是拷贝构造函数的参数只能有一个,并且必须是类类型对象的引用
拷贝构造的参数如果是类类型,采用值传递的方式可以吗?
自定义类型传参时,需要调用拷贝构造函数,此时就会导致无穷递归,所以不可以进行值传递
于是为了避免无穷递归,拷贝构造函数的参数需要传内置类型,因此必须采用地址传递的方式,因此参数可以是类类型的指针,也可以是类类型的引用,由于引用更方便,因此规定拷贝构造函数的参数必须是类类型的引用
#include <iostream>
#include <string.h>
using namespace std;
typedef int StackDataType;
//栈
class Stack
{
public:
Stack(int capacity = 4)
: _capacity(capacity)
, _top(0)
{
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
}
//拷贝构造函数
//加上 const 防止内部修改源对象
Stack(const Stack& s)
: _capacity(s._capacity)
, _top(s._top)
{
//开新空间
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
//拷贝数据
memcpy(_a, s._a, sizeof(StackDataType) * _capacity);
//验证是否会调用拷贝构造
cout << "Stack(Stack& s)" << " ";
}
//虽然可以完成拷贝构造的工作,但这个不是拷贝构造函数
//只是构造函数的一个重载形式而已
Stack(Stack* s)
: _capacity(s->_capacity)
, _top(s->_top)
{
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
//拷贝数据
memcpy(_a, s->_a, sizeof(StackDataType) * _capacity);
}
void Push(StackDataType x)
{
//扩容...
//插入
_a[_top] = x;
++_top;
}
void Print()
{
for (int i = 0; i < _top; ++i)
{
cout << _a[i] << " ";
}
cout << endl;
}
//...
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
}
private:
StackDataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Print(); //输出 1 2
//两种方式都可以调用拷贝构造函数
//Stack s2(s1);
Stack s2 = s1;
s2.Print(); //输出 Stack(Stack& s) 1 2
//调用指针版的构造函数
Stack s3(&s1);
s3.Print(); //输出 1 2
return 0;
}
3. 编译器自动生成的拷贝构造函数
如果类中没有拷贝构造函数,编译器会自动生成拷贝构造函数,那编译器自动生成的拷贝构造函数的行为是什么呢?
- 内置类型的成员变量进行浅拷贝
- 自定义类型的成员变量调用该类型的拷贝构造函数
#include <iostream>
#include <string.h>
using namespace std;
typedef int StackDataType;
//栈
class Stack
{
public:
Stack(int capacity = 4)
: _capacity(capacity)
, _top(0)
{
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
}
//拷贝构造函数
//加上 const 防止内部修改源对象
Stack(const Stack& s)
: _capacity(s._capacity)
, _top(s._top)
{
//开新空间
_a = (StackDataType*)malloc(sizeof(StackDataType) * _capacity);
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
//拷贝数据
memcpy(_a, s._a, sizeof(StackDataType) * _capacity);
}
void Push(StackDataType x)
{
//扩容...
//插入
_a[_top] = x;
++_top;
}
void Print()
{
for (int i = 0; i < _top; ++i)
{
cout << _a[i] << " ";
}
cout << endl;
}
//...
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
}
private:
StackDataType* _a;
int _top;
int _capacity;
};
class Test
{
public:
//无拷贝构造函数
void Push(StackDataType x)
{
++_size;
_st.Push(x);
}
void Print()
{
cout << "size:" << _size << " ";
_st.Print();
}
private:
int _size = 0;
Stack _st;
};
int main()
{
//Test 类中,可以使用编译器生成的默认构造函数
Test t1;
t1.Push(1);
t1.Push(2);
t1.Print(); //输出 size:2 1 2
//内置类型进行浅拷贝,自定义类型调用该类型的拷贝构造函数
Test t2(t1);
t2.Print(); //输出 size:2 1 2
return 0;
}
4. 自己写的拷贝构造函数的执行方式
拷贝构造函数的执行方式和构造函数的执行方式是一样的,成员变量先使用拷贝构造函数的初始化列表或成员变量声明时指定的缺省值(C++11) 初始化成员变量,然后执行拷贝构造函数中的内容
因此自定义类型成员变量,需要自己在拷贝构造函数的初始化列表显示指定调用该类型的拷贝构造函数,或者自己在拷贝构造函数体中显示指定调用该类型的拷贝构造
注意:拷贝构造函数也是构造函数,当我们写了拷贝构造函数后,编译器便不会生成构造函数
5. 拷贝构造函数的调用场景
有如下三种场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int, int, int)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date():" << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2023, 2, 10);
Test(d1);
return 0;
}
输出结果:
Date(int, int, int)
Date(const Date& d)
Date(const Date& d)
Date(const Date& d)
~Date():
~Date():
~Date():
~Date():
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用
四、赋值运算符重载
1. 运算符重载
(1) 运算符重载的定义和使用
在 C语言中,运算符只支持内置类型,而不支持自定义类型,于是 C++ 为了增强代码的可读性引入了运算符重载,使得自定义类型也可以使用运算符
运算符重载是具有特殊函数名的函数,其返回值类型与参数列表与普通的函数类似
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator运算符(参数列表); 参数列表的个数和运算符的操作数相同,如果有两个操作数,则第一个参数表示左操作数,第二个表示右操作数
==运算符的重载函数 和 使用
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
}
//== 运算符重载
//左操作数是 this,指向调用函数的对象
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 2, 6);
Date d3(d1);
//两种使用运算符重载函数的方式
cout << d1.operator==(d2) << endl; //输出 0
// << 比 == 优先级高,需要加括号
//编译器会解释为 d1.operator==(d3);
cout << (d1 == d3) << endl; //输出 1
return 0;
}
注意:
- operator 关键字后不能连接语言中没有的运算符
比如:operator@ - 运算符重载函数必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型 +,不能改变其含义 - .(成员访问运算符)、.*(点星)、::(域作用限定符)、?:(三目运算符)、sizeof
注意:这 5 个运算符不能重载 - 运算符重载不能改变运算符操作数的个数
比如 + 需要两个操作数,则重载的 + 也必须要有两个操作数 - 运算符重载不能改变运算符的优先级和结合性
(2) 前置++/-- 和 后置++/-- 的运算符重载规定
默认表示前置++/-- 的运算符重载,++d1 解释为 d1.operator++();
Date& operator++();
Date& operator--();
前置++/-- 和后置++/-- 都是一元运算符,为了可以让前置++ 和后置++ 形成函数重载,C++ 规定后置++ 重载时多增加一个 int 类型的参数(可以不用写形参名),调用函数时该参数不用传递,编译器自动传递,d1++ 解释为 d1.operator++(整形),int 参数编译器会自动传
Date operator++(int);
Date operator--(int);
(3) 运算符重载也可以实现在全局域中
cout 和 cin 其实分别是 ostream 和 istream 类的对象,流插入 << 和流提取 >> 运算符都在各自的类中重载了,因此 cout 可以自动识别类型,就是根据 << 运算符重载和函数重载达到的
在类中实现流插入 << 和流提取 >> 运算符重载时,通常我们都是 cout 和 cin 在左,由于左操作数必须为第一参数,而类中的成员函数第一参数是 this,此时 << 和 >> 运算符便不能实现在自己写的类中,需要写到全局域中
但是声明在全局域中时,便不能访问类的私有成员,这里可以在类中进行友元函数声明
//返回 ostream 对象,为了可以连续输出
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//返回 istream 对象,为了可以连续输入
inline istream& operator<<(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
2. 赋值运算符重载的定义
赋值运算符重载也是 C++ 提供的一个特殊的成员函数,用于将一个已存在对象赋值给另一个已存在的对象
//赋值运算符重载
Date& operator=(const Date& d)
{
//自己给自己赋值,不需要处理
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//this 指针指向的对象在上一层栈帧中,可以返回引用,
return *this;
}
- 参数传 const 引用,是为了防止源数据被修改以及避免拷贝构造
- 返回值 *this 是为了支持连续赋值,并且满足连续赋值的含义,d1 = d2 = d3
- 返回值传引用,是为了避免拷贝构造
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, 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;
}
//this 指针指向的对象在上一层栈帧中,可以返回引用,
return *this;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2020, 9, 1);
Date d3(2023, 2, 6);
d1.Print(); //输出 1970年1月1日
d2.Print(); //输出 2020年9月1日
d3.Print(); //输出 2023年2月6日
//赋值运算符重载,支持连续赋值
d1 = d2 = d3;
d1.Print(); //输出 2023年2月6日
d2.Print(); //输出 2023年2月6日
d3.Print(); //输出 2023年2月6日
return 0;
}
3. 编译器自动生成的赋值运算符重载
如果类中没有赋值运算符重载函数,编译器会自动生成赋值运算符重载函数,那编译器自动生成的赋值运算符重载函数的行为是什么呢?
- 内置类型的成员变量进行浅拷贝
- 自定义类型的成员变量调用该类型的赋值运算符重载函数
由于类中没有赋值运算符重载函数时,编译器会自动生成,因此类的赋值运算符重载不可以写在全局中
五、取地址运算符重载
1. const 成员函数
在成员函数括号后用 const 修饰,称为 const 成员函数,实际上是用 const 修饰成员函数参数中的 *this 指针,使得调用成员函数的对象的成员变量在函数中不能被修改,并且 const 对象和非 const 对象均可以调用 const 成员函数
class Date
{
public:
//const 成员函数,使得调用成员函数的对象的成员变量不会被修改
void Print() const
{
}
private:
int _year;
int _month;
int _day;
};
2. 取地址运算符重载
一般不需要自己写,使用编译器生成的取地址运算符重载即可
Date* operator&()
{
return this;
}
六、const 取地址运算符重载
一般不需要自己写,使用编译器生成的 const 取地址运算符重载即可
const Date* operator&() const
{
return this;
}
空类其实并不是什么都没有,编译器会自动生成这 6 个 默认成员函数
默认成员函数:用户没有显式实现时,编译器会自动生成