目录
4、拷贝构造函数?
4.1、概念
4.2、特征
5、赋值运算符重载
5.1、运算符重载
5.2、赋值运算符重载
4、拷贝构造函数?
4.1、概念
在创建对象时,可否创建一个与某一个对象一模一样的新对象呢?
比如:
int main()
{
//在C++中,某一个自定义类型的对象在定义时使用同类型(自定义类型)的已存在对象进行初始化时,并且还满足调用的拷贝构造函数的形参有且只有一个,比如: Date d2(d1); 此时符合拷贝构造函数的调用时机的第一种情况, 则会自动调用拷贝构造函数,而当执行代码:Date d1(2022,7,13);时,发现不满足拷贝构造函数的调用时机的三种情况,故不自动调用拷贝构造函数,而是自动调用普通的构造函数、
Date d1(2022,7,13); //调用普通的构造函数、
Date d2(d1); //调用拷贝构造函数、
//相当于使用对象d1去初始化d2,即使得对象d2中的数据与对象d1中的数据保持一致,此时调用的就是拷贝构造函数、
return 0;
}
//拷贝构造函数的调用时机:
//C++中拷贝构造函数的调用时机通常有3种情况:
//1、某一个自定义类型的对象在定义时使用同类型(自定义类型)的已存在对象进行初始化时,并且还满足调用的拷贝构造函数的形参有且只有一个,比如: Date d2(d1); 则会自动调用拷贝构造函数、
//2、自定义类型的对象以传值的形式进行传参时,则会自动调用拷贝构造函数、
//3、自定义类型的对象以传值的形式进行返回时,则会自动调用拷贝构造函数、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
cout << "Date默认构造函数调用" << endl;
}
Date(int age)
{
cout << "Date带参构造函数调用" << endl;
_age = age;
}
~Date()
{
cout << "Date析构函数调用" << endl;
}
Date(const Date& d)
{
cout << "Date拷贝函数调用" << endl;
_age = d._age;
}
private:
int _age;
};
Date Func() //传值返回、
{
Date d1; //自定义类型的对象以传值的形式进行返回时,则会自动调用拷贝构造函数、
cout << &d1 << endl; //006FF64C
return d1; //自定义类型的对象d1出了其生命周期就销毁了,此时返回的并不是此处的自定义类型的对象d1本身,在该返回过程中会根据自定义类型的对象d1自动调用拷贝构造函数来创建一个新的对象、
}
void test()
{
Date d2 = Func(); //使用对象d2来接收由Func函数中在返回过程中根据自定义类型的对象d1自动调用拷贝构造函数来创建的一个新的对象中的内容、
cout << &d2 << endl; //006FF744
//由两者的地址不同可知,Func函数中的自定义类型的对象d1和test函数中的自定义类型的对象d2并不是同一个对象、
}
int main()
{
test(); //先析构自定义类型的对象d1,再析构自定义类型的对象d2,而先构造自定义类型的对象d2,再构造自定义类型的对象d1,两者顺序相反、
return 0;
}
//注意:
//Date d1;
//Date d2(d1); 等价于 Date d2=d1; 都是通过已经创建的对象来对未创建的对象进行初始化,要保证是同一种类型(自定义类型)、
? ? 拷贝构造函数是构造函数的一种特殊形式,故其函数名和类名相同,即,拷贝构造函数是构造函数的一个重载形式,除此之外,拷贝构造函数有且只有一个参数,并且拷贝构造函数均是非默认拷贝构造函数、
? ? 拷贝构造函数:有且只有单个形参,该形参是对本类类型对象的引用(一般常用const进行修饰,防止在拷贝构造函数的函数体中将被拷贝的对象中的内容修改),在用已存在的类类型对象创建新对象时由编译器自动调用、
4.2、特征
拷贝构造函数也是特殊的构造函数,其特征如下:
1、拷贝构造函数是构造函数的一个重载形式、
2、拷贝构造函数的参数有且只有一个且必须使用传引用传参,当使用传引用传参后,在优先进行的传参过程中,就不会再自动调用拷贝构造函数,则就不会造成无穷递归调用了,使用传值传参的方式会引发无穷递归调用、????????
#define _CRT_SECURE_NO_WARNINGS 1
#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(Date d) //传值传参,对象d的改变不会影响对象d1、
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
? ? ? 若按照上述代码所示的话,即,拷贝构造函数使用传值的形式进行传参,则会引发无穷递归调用,这是因为,当执行代码 Date d2(d1);?时,会自动调用拷贝构造函数,而当调用拷贝构造函数时,要先进行传参,而当传参时,又因为是自定义类型的对象进行传值传参,故该过程中又会自动调用拷贝构造函数,而当调用拷贝构造函数时,又要先进行传参,所以,如此往复下去就会造成无穷递归调用,如下所示:
此时,加不加const都不影响造成无穷递归调用、
为了避免造成无穷递归调用,所以拷贝构造函数必须使用传引用传参,就可以解决问题、
#define _CRT_SECURE_NO_WARNINGS 1
#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(Date& d) //传引用传参,对象d的改变会影响对象d1,因为d是d1的别名、
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//尽量写成如下所示:
Date(const Date& d) //主要是为了保护对象d(对象d1)中的内容不被修改,权限由可读可写改为可读不可写,权限缩小,可以编译成功、
{
_year = d._year;
_month = d._month;
_day = d._day;
//不加const的话,若代码不小心写为如下所示的话,就会造成逻辑错误,不容易找出错误之处,加上const再写成如下所示的话,编译器会自动报错(语法错误)进行提示,方便修改、
d._year=_year;
d._month=_month;
d._day=_day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1); //调用拷贝构造函数、
return 0;
}
除上面的方法外,也可以使用如下方法进行解决问题:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
_month = d->_month;
_day = d->_day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(&d1);
//该方法是可行的,但不再是调用拷贝构造函数了,而是调用普通的构造函数、
return 0;
}
3、
? ? ? 若未在类体中显式实现拷贝构造函数,则系统会自动生成并调用一个拷贝构造函数(非默认拷贝构造函数),?该拷贝构造函数按内存存储按字节序完成拷贝(类似memcpy函数),这种拷贝我们叫做浅拷贝,或者值拷贝、
下面拿日期类代码举例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数、
Date(int year = 1,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//打印函数、
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
d1.Print(); //1-1-1
d2.Print(); //1-1-1
//注意:下面的写法是错误的
//d2(d1);
//同一种类型(自定义类型)的拷贝构造函数只能用来初始化将要创建的对象,而不能用于初始化已经创建完的对象、
return 0;
}
?
? ? 在上面的代码中,我们并没有显式的在类体中实现出拷贝构造函数,使用的是编译器自动生成的拷贝构造函数,但是程序依然输出了正确的结果,这是因为编译器自动生成并自动调用的构造函数对于内置类型的类成员变量进行了浅拷贝(逐字节拷贝,类似memcpy),即,对象d1和对象d2中的两块内存空间中存储的值一模一样,并且此处的内置类型的类成员变量也没有涉及到需要使用深拷贝,所以能够输出正确的结果、
4、
? ? ? 那么由编译器自动生成并调用的拷贝构造函数已经可以完成字节序的值拷贝(浅拷贝)了,我们还需要自己显式的在类体中实现拷贝构造函数吗?当然像日期类这样的类是没必要的,那么下面的类呢?验证一下试试?
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;
class Person
{
public:
//构造函数、
Person()
{
_arr = (int*)malloc(sizeof(int) * 10);
assert(_arr);
}
//析构函数、
~Person()
{
free(_arr);
_arr=nullptr;
}
private:
int* _arr;
};
int main()
{
Person f1;
Person f2(f1); //自动调用拷贝构造函数、
return 0;
}
? ? ?上述代码执行将会报错,为什么?因为我们此时没有在类体中显式的实现拷贝构造函数,所以使用的是编译器自动生成并自动调用的一个拷贝构造函数,而该拷贝构造函数对内置类型的类成员变量执行的是浅拷贝,对于自定义类型的类成员变量则是通过自动调用其对应的拷贝构造函数进行拷贝,在此,由于类成员变量 _arr?为内置类型的类成员变量,故执行的是浅拷贝,而此处的内置类型的类成员变量 _arr?需要执行的是深拷贝,所以会报错,具体原因和下面的代码报错的原因是一样的、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;
class Stack
{
public:
Stack(int capacity = 10)
{
_a = (int*)malloc(sizeof(int)*capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
Stack(const Stack& st)
{
_a = st._a;
_top = st._top;
_capacity = st._capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1(10);
Stack st2(st1); //调用栈的拷贝构造函数、
return 0;
}
? ? ?上述代码是可以调用栈的拷贝构造函数从而通过拷贝构造来完成初始化目的的,但是程序会报错,是在析构过程中发生错误,这是因为:
? ? ?像上面的栈这种类的拷贝构造函数不能随便去写,必须要使用深拷贝才可以达到目的,此时报错是因为,当执行完代码:Stack st2(st1);?后,对象st1和对象st2中的类成员变量的数据是一样的,两个对象中的类成员变量 _a 的值是一样的,故两者同时指向了堆区上的同一块内存空间,我们并不希望是这样,当执行代码:return 0;?时,首先,对象st2先进行析构,两个对象中的类成员变量 _a指向的同一块内存空间被对象st2在析构过程中释放掉了,并把对象st2中的类成员变量 _a?的值置为了空指针,但是要注意,此时,对象st1中的类成员变量 _a?的值并没有被置为空指针,它还是指向了堆区上那一块内存空间,现轮到对象st1进行析构,而对象st1中的类成员变量 _a 指向的仍是那一块内存空间,此时再进行 free(_a);操作的话就会出错,这是因为,同一块内存空间不能被释放多次,只能释放一次,否则就会报错,当前所学的拷贝构造函数进行的都是浅拷贝,目前这个问题只能通过深拷贝进行解决,具体方法在后面再进行阐述,除了会出现上述情况外,在数据结构栈中插入或删除数据等等情况下也会出现互相影响的问题,所以说,浅拷贝具有明显的缺陷,必须使用深拷贝来进行解决问题才可以、
浅拷贝(值拷贝)的问题: 1、指向同一块空间,分别操作数据但是会互相影响、 2、这块内存空间析构时会释放多次,程序会崩溃。
注意:浅拷贝(值拷贝)也会把 内存对齐一起拷贝过去,类似于memcpy函数,按字节进行的? ? ? ? ? ? ? ?拷贝、
? ? 对于上述代码而言,如果不在类体中显式的实现栈的拷贝构造函数的话,则编译器会自动生成并调用一个拷贝构造函数,该拷贝构造函数对内置类型的类成员变量进行浅拷贝,对自定义类型的类成员变量通过自动调用他们对应的拷贝构造函数进行拷贝,由于此时只有内置类型的类成员变量,所以会对他们进行浅拷贝,则两个对象中的内置类型的类成员变量 _a?指向了堆区上的同一块内存空间,这是不对的,他们各自需要有独立的内存空间,这就需要我们显示的在类体中实现拷贝构造函数,通过该拷贝构造函数来完成深拷贝、
注意:
? ? ?当我们没有在类体中显式的实现拷贝构造函数时,则编译器会自动生成并自动调用一个拷贝构造函数(非默认),而该拷贝构造函数对内置类型的类成员变量执行的是浅拷贝(值拷贝),并不代表着浅拷贝一定是错误的,在某些情况下,浅拷贝是可以达到目的的,比如日期类,对于自定义类型的类成员变量则是通过自动调用其对应的拷贝构造函数进行拷贝,若显式的在类体中实现拷贝构造函数的话,那么编译器就不再自动生成了、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
{
_hour = hour;
_minute = minute;
_second = second;
}
//没显式的在类体中实现Time的拷贝构造函数,所以自动调用的就是编译器自动生成的拷贝构造函数、
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << "-" << _time._hour << "-" << _time._minute << "-"<<_time._second << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
Date d2(d1);//对于自定义类型(Time类型)的类成员变量_time,通过自动的调用其对应的构造函数进行拷贝、
d1.Print(); //1 - 1 - 1 - 0 - 0 - 0
d2.Print(); //1 - 1 - 1 - 0 - 0 - 0
return 0;
}
5、
问:对于内置类型的数组类型的类成员变量,浅拷贝是否可以达成我们的目的?
答:可以的,所谓浅拷贝能够达到目的就是保证不涉及到深拷贝即可,常见的涉及到深拷贝的情况有:动态开辟(堆区),new(堆区),fopen(硬盘)出来的资源才会涉及到深拷贝,也可以说是:存在内置类型的类成员变量是指针类型,指向了某一块内存空间,这种情况下一般涉及到了深拷贝,但是会存在一些特殊的情况,即,存在内置类型的类成员变量是指针类型,指向了一块内存空间,但是并没涉及到深拷贝,比如STL中的迭代器中会遇到这样的情况,但比较罕见,后期再说,浅拷贝可以达成目的,即,不需要资源清理的,也即,不需要自己手动释放内存空间的,由于本题中因为并不涉及到深拷贝,所以说,浅拷贝可以达成目的,如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
//构造函数、
Person()
{
for (int i = 0; i < 10; i++)
{
_arr[i] = i;
}
}
//打印、
void Print()
{
for (int i = 0; i < 10; i++)
{
cout << _arr[i]<<" ";
}
cout << endl;
}
private:
int _arr[10];
};
int main()
{
Person f1;
Person f2(f1);
f1.Print(); //0 1 2 3 4 5 6 7 8 9
f2.Print(); //0 1 2 3 4 5 6 7 8 9
return 0;
}
? ? ?由此可以看到编译器自动生成并自动调用的拷贝构造函数能够实现我们的目的,完成数组元素的拷贝,所以,内置类型的数组类型的类成员变量,也算是内置类型的类成员变量、
注意:
? ? ?在C语言中,不管是内置类型的变量还是自定义类型的变量,当进行传值传参或者是传值返回时,都是按照字节进行的拷贝(浅拷贝),在C语言中没有拷贝构造函数的概念,并且在C语言中,不管哪种情况都不会涉及到深拷贝,所以不管哪种情况均按照字节进行拷贝即可,? ? ? 在C++中,对于内置类型的对象而言,若进行传值传参或者是传值返回的话,也都是按照字节进行的拷贝,因为对于内置类型而言,也不存在拷贝构造函数这一概念,并且C++中的内置类型的对象进行传值传参或者是传值返回的时候也都不会涉及到深拷贝,所以不管哪种情况均按照字节进行拷贝即可,拷贝构造函数(浅拷贝和深拷贝)这一概念只是针对于C++中自定义类型而言的,而对于C++中的自定义类型的对象而言的话,若进行传值传参或者是传值返回的话,则是通过自动调用拷贝构造函数来进行的拷贝,首先要知道,拷贝构造函数可以实现浅拷贝,也可以实现深拷贝,而按照字节进行的拷贝只能实现浅拷贝,在C++中,对于一些只涉及到需要浅拷贝的自定义类型的对象而言,可以通过按照字节进行的拷贝,也可以通过自动调用拷贝构造函数进行拷贝,而对于一些涉及到需要深拷贝的自定义类型的对象而言,只能通过自动调用拷贝构造函数来进行拷贝,若按照字节进行拷贝的话,只能进行浅拷贝,所以就会出错,则只能通过自动调用拷贝构造函数来进行拷贝,这是因为,只有拷贝构造函数可以实现深拷贝,但由于不清楚某一个自定义类型的对象是否会涉及到深拷贝,所以为了统一起见,规定C++中自定义类型的对象进行传值传参或者是传值返回时一律通过自动调用拷贝构造函数进行拷贝,不管是按照字节进行的拷贝还是通过自动调用拷贝构造函数进行的拷贝,两者之间的改变互不影响、
构造函数(
包括拷贝构造函数)调用规则如下:
1、如果用户在类体中显式的实现了带参的构造函数,则C++编译器就不再自动生成无参的默认构造函数,但是仍然会自动生成拷贝构造函数(非默认)、
2、如果用户在类体中显式的实现了拷贝构造函数(非默认),那么C++编译器就不再自动生成无参的默认构造函数了、
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Date
{
public:
Date(const Date& d)
{
_age = d._age;
}
void Print()
{
cout << _age << endl;
}
private:
int _age;
};
int main()
{
Date d1; //错误,没有合适的默认构造函数可以使用、
return 0;
}
5、赋值运算符重载
5.1、运算符重载
? ? 运算符重载函数(除赋值运算符之外)和赋值运算符重载函数均是 非默认 的,如果在类体中显式实现了赋值运算符重载函数的话,编译器就不会在类体中自动生成一个赋值运算符重载函数了,否则,编译器会在类体中自动生成一个赋值运算符重载函数、
首先给定一个日期类:
#define _CRT_SECURE_NO_WARNINGS 1
#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(2022,7,13);
Date d2(2022,7,14);
d1.Print();
d2.Print();
return 0;
}
? ? ?那么能不能进行对象d1和对象d2的比较呢,或者是对 对象d1和对象d2分别进行运算呢 ?即,能不能使用运算符对两个对象d1和d2进行操作呢,如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#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(2022,7,13);
Date d2(2022,7,14);
//1、能否通过下面的代码来 比较 两个对象d1和d2呢?、
//答案是不可以的,因为,其中 >,==,< 这三者均属于运算符、
if (d1 > d2)
{
cout << ">" << endl;
}
else if (d1 == d2)
{
cout << "==" << endl;
}
else
{
cout << "<" << endl;
}
//2、能否通过下面的代码对 对象d1或对象d2进行 运算 呢?
//答案是不可以的,其中 ++,+,- 这三者均属于运算符、
d1++;
d1 + 100;
d1 - 100;
d1.Print();
d2.Print();
return 0;
}
注意:
? ? 不管是在C语言中,还是在C++中,对于内置类型都可以直接使用各种运算符,因为内置类型是系统自己定义的,所以系统是知道如何对他们进行各种运算符操作的,比如系统是知道如何去比较这里的两个对象d1和d2中的年,月,日的,按照二进制位去比较即可,这是当操作数均是内置类型的时候,但是对于自定义类型而言的话,是不可以直接使用各种运算符的,这是因为,自定义类型是我们自己定义的,系统是不知道如何对他们进行各种运算符操作的,需要我们自己设定规则,所以,为了让自定义类型也可以使用大部分的运算符,这就引出了运算符重载的概念,从而去间接的使用这大部分的运算符,要知道,此处的运算符重载和之前所谓的函数重载不是一个概念,所谓运算符重载就是重新去定义或者说是控制某些大部分运算符的使用规则,当操作数均是内置类型时,就可以直接使用各种运算符,当操作数只要出现了自定义类型就不可以直接使用各种运算符、
? ??
由此,C++编译器提供了一个新的关键字operator,然后该关键字后面跟上运算符,这样就可以重载这个运算符的使用规则,该运算符重载后的使用规则,可以在该运算符重载函数的函数体内自己去设定即可,运算符重载就是运算符重载函数,如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
};
//1、运算符重载函数,其中 operator== 是该运算符重载函数的函数名,即: operator运算符 、
//2、运算符重载函数的参数的个数是由该要被重载的运算符的操作数的个数决定的,比如:运算符==就有两个操作数,所以该运算符重载函数就有
//两个形参,因为运算符==是双操作数的运算符,常见的单操作数的运算符有:++,-- 等等、
//3、运算符重载函数的返回类型(返回值)是根据该被重载的运算符的运算结果所决定的,比如,运算符==的结果就是:相等或不相等(真和假的布尔值),则该运算符重载函数的
//返回类型就是:bool、
//全局函数、
bool operator==(Date d1, Date d2)
{
//该运算符重载函数的函数体内具体进行的操作就不再是系统语法规定的了,而是由我们自己去定义、
//此处的_year,_month,_day都属于内置类型,是可以直接使用各种运算符进行操作的、
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 13);
//若是调用该全局函数 operator== 的话,应该按照如下方式进行调用:
if (operator==(d1, d2))
{
cout << "==" << endl;
}
以此为例:
//if (d1 == d2)
//{
// cout << "==" << endl;
//}
d1.Print();
d2.Print();
return 0;
}
? ? 上述代码会报错,这是因为,类体中的三个内置类型的类成员变量均是私有的,在类外没办法直接访问到他们,现在有三种方式可以解决上述问题:
1.?可以手动的把类体中的这三个内置类型的类成员变量的属性改为公有,首先要知道,类体中的类成员变量不是必须设置为私有或保护的,但是通常情况下都是设置成私有或保护的,这种方法就违背了当初把他们设置成私有或保护的意愿,所以不采用这种方法、
2.?在类体中实现公有的函数接口,然后在类外访问该公有的函数,通过该公有的函数来访问类体中私有或保护属性的类成员变量,再把这些类成员变量通过该公有的函数返回出来,在类外对这些返回出来的类成员变量进行接收、
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
//全局函数、
bool operator==(Date d1, Date d2)
{
return d1.GetYear() == d2.GetYear() && d1.GetMonth() == d2.GetMonth() && d1.GetDay() == d2.GetDay();
}
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 14);
//若是调用该全局函数 operator== 的话,应该按照如下方式进行调用:
if (operator==(d1, d2))
{
cout << "==" << endl;
}
else
{
cout << "!=" << endl;
}
以此为例:
//if (d1 == d2)
//{
// cout << "==" << endl;
//}
d1.Print();
d2.Print();
return 0;
}
3.?使用友元函数,但是这种方法不太好,会破坏封装、
4.?直接把该全局的运算符重载函数放在类体中去,这样的话,在该运算符重载函数的函数体内对类中的类成员变量进行访问时就不会再受到访问限定符的限制、
? ? 此时,为了方便演示,就先使用方法1,但实际中并不会这样操作,一般都是通过第4种方法进行操作,但由于在使用方法4时,还会涉及到其他内容,具体在后面进行阐述,当前不方便使用方法4,还需要做一些其他的改动,所以目前就先使用方法1,从而方便更好的演示,代码如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
//设为公有public:、
int _year;
int _month;
int _day;
};
//全局函数、
方法一、
//bool operator==(Date d1, Date d2)
//{
// return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
//}
//方法二、
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 13);
//若是调用该全局函数 operator== 的话,应该按照如下方式进行调用:
if (operator==(d1, d2))
{
cout << "==" << endl;
}
else
{
cout << "!=" << endl;
}
//以此为例:
if (d1 == d2)
{
cout << "==" << endl;
}
d1.Print();
d2.Print();
return 0;
}
? ? 通过上面可知,如果按照上述写法的话,其实感觉不出来在使用运算符重载函数,而是和调用普通的全局函数没什么区别,相比之下这样的运算符重载函数反而更加麻烦了,那么我们为什么不自己写一个普通的全局调用函数来实现呢,但其实上,如下所示:
//一、
//if (operator==(d1, d2))
//{
// cout << "==" << endl;
//}
//二、
if (d1 == d2)
{
cout << "==" << endl;
}
? ? 在使用上述方法二时,也能正常打印出 == ,这是普通的全局调用函数所不能实现的,其实,在本质上,当使用这里的方法二时,就等价于在使用这里的方法一:
//二、
if (d1 == d2) //if (operator==(d1, d2))
{
cout << "==" << endl;
}
? ? 此时,假设d1和d2都是内置类型,当编译器看到d1和d2时,由于这两者都是内置类型,则编译器就直接转换成指令对他们进行运算符的操作,但当编译器看到对象d1和d2时,由于这是自定义类型,所以编译器会先去类体中看一下是否定义了运算符重载函数,如果发现已经定义了运算符重载函数的话,那么就不会再去类体外的全局区域内查找是否定义了运算符重载函数,那么此时编译器就把?if (d1 == d2)? 自动进行转换为:if (d1.operator==(d2)) ,根据该代码再去自动调用对应的运算符重载函数,如果编译器在类体中没有发现运算符重载函数的定义,那么编译器才会再去类体外的全局区域查找运算符重载函数的定义,如果发现了,则编译器会自动的把?if (d1 == d2)? 转化为? if (operator==(d1, d2)),根据该代码再去自动调用对应的运算符重载函数,若在类体外的全局区域也没有定义运算符重载函数的话,那么编译器就会报错、
? ? 而此时,当编译器看到对象d1和d2时,由于这是自定义类型,?所以编译器会先去类体中看一下是否定义了运算符重载函数,发现没有定义运算符重载函数,那么此时编译器就会再去类体外的全局区域查找运算符重载函数的定义,发现已经定义了,所以就会自动的把? if (d1 == d2)? 转换为??if (operator==(d1, d2)),根据该行代码再去自动的调用对应的运算符重载函数、
? ? 当我们在类体外的全局区域中定义了运算符重载函数(全局函数)之后,就不再使用方法一,而是可以直接使用方法二了,此时,方法一只是告诉我们可以这样使用,但是我们一般不使用这种方法,而是直接使用方法二,就像直接对内置类型那样使用各种运算符一样、
? ? 但是在实际中,我们不会写成上述代码这样,因为,我们不会使用方法1,其次,方法2也比较麻烦,使用友元函数还会破坏封装,目前最好的方式就是使用最后一种方法,即方法4,把运算符重载函数(全局函数)放在类体中去变成类成员函数(非静态),但是直接拷贝进行会发现,编译器会报错,说是:二进制"operator=="的参数过多,这是为什么呢 ? 根据上面的总结,现在该类体中的运算符重载函数(非静态的类成员函数)有2个参数,按理说是不应该报错的啊,实际上,此时的运算符重载函数就由普通的全局函数变成了类成员函数,且是非静态的,故该函数的第一个形参位置上会多了一个this指针,要知道:类的6个默认类成员函数中均包含有this指针,析构函数说是没有参数,但其实析构函数的形参中也有一个隐藏的this指针,并且,拷贝构造函数也说是只有一个形参,但实际上他的形参列表中的第一个位置上也有一个隐藏的this指针,由此可知,所以此处会进行报错,实际上,该运算符重载函数(非静态的类成员函数)的形参列表中已经有了3个参数,但是由于运算符 == 的操作数只有两个,所以该运算符重载函数(非静态的类成员函数)就必须只能有两个参数,现在就要少写一个参数(除隐藏的this指针外),就默认把左操作数对应的参数舍弃掉,代码如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
//1、
//bool operator==(Date d2)
//{
// return _year == d2._year && _month == d2._month && _day == d2._day;
//}
//2、
优改为:
//bool operator==(Date d) //bool operator==(Date* const this,Date d)
//{
// return _year == d._year && _month == d._month && _day == d._day;
//}
//3、
//上述代码中,在传参时可以使用传值,传址,还可以使用传引用的方法,并没有限制某一种方法不可使用,但是由于C++中规定自定义类型的对象在传值传参时会调用拷贝构造函数,我们要尽量避免调用拷贝构造函数,
//所以就不使用传值的形式进行传参,而使用传址传参和传引用传参均不会调用拷贝构造函数,而传址传参又不如传引用传参,所以应使用传引用传参,要记住,自定义类型的对象在 传参和返回 时尽量避免调用拷贝构造函数
//虽然上述代码可以完成任务,但是不如使用传引用传参好,当三种方法都可以使用时,优先选择传引用传参,故再次优化为:
bool operator==(const Date& d) //bool operator==(Date* const this,const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 13);
//一、
if (d1.operator==(d2)) //把自定义类型对象d1的地址传给了隐藏的this指针、
{
cout << "==" << endl;
}
//二、
if (d1 == d2) //if (d1.operator==(d2)) //if (d1.operator==(&d1,d2))
{
cout << "==" << endl;
}
d1.Print();
d2.Print();
return 0;
}
? ? 此时,当编译器看到对象d1和d2时,由于这是自定义类型,?所以编译器会先去类体中看一下是否定义了运算符重载函数,发现已经定义运算符重载函数,此时就不会再去类体外的全局区域查找是否定义了运算符重载函数,故编译器自动的把? if (d1 == d2)?进行了转换为:if (d1.operator==(d2)) ,然后再去调用对应的运算符重载函数、
? ? 如果在类体中和在类体外的全局域中都定义了运算符重载函数的话,编译时可以通过的,是因为两者在不同的作用域中,所以函数名一样也是可以的,当编译器看到对象d1和d2时,由于这是自定义类型的对象,所以编译器会先去类体中看一下是否定义了运算符重载函数,发现已经定义运算符重载函数,此时不会再去类体外的全局区域查找是否定义了运算符重载函数,编译器现在就会自动的把? if (d1 == d2)?进行了转换为:if (d1.operator==(d2)) ,然后再去调用对应的运算符重载函数、
拓展:
?请写出运算符重载函数(非静态类成员函数)来判断日期类对象d1是否小于(<)日期类对象d2:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
//运算符重载函数(非静态类成员函数)、
bool operator<(const Date& d) //bool operator<(Date* const this,const Date& d)
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day))
{
//对象d1小于对象d2(即,对象d1中的日期小于对象d2中的日期)、
return true;
}
else
{
//对象d1大于等于对象d2(即,对象d1中的日期大于等于对象d2中的日期)、
return false;
}
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 14);
//一、
if (d1.operator<(d2)) //把自定义类型对象d1的地址传给了隐藏的this指针、
{
cout << "<" << endl;
}
//二、
if (d1 < d2) //if (d1.operator<(d2)) //if (d1.operator<(&d1,d2))
{
cout << "<" << endl;
}
d1.Print();
d2.Print();
return 0;
}
? ? C++
为了增强代码的可读性引入了运算符重载
,
运算符重载是具有特殊函数名的函数
,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的全局函数类似、
函数名字为:关键字
operator
后面接需要被重载的运算符符号、
函数原型:
返回值类型
?operator
操作符
(
参数列表
) 、
注意:
? ? 1、不能通过连接其他符号(非运算符的符号)来创建新的操作符:比如
operator@,在C语言和C++中不存在运算符@、
? ? 2、重载运算符时要保证该被重载的运算符的操作数,也即重载运算符函数(常使用非静态的类成员函数)的形参中必须至少存在一个(类类型或者枚举类型)(自定义类型)的操作数、
? ? 3、
用于内置类型的各种操作符,在运算符重载时,其含义不能改变,例如:内置的整型?
+
,在运算符重载时不能改变其含义、
????4、运算符重载函数作为类成员函数(非静态的类成员函数)时,其形参列表中看起来比实际操作数的数目少1个,这是因为该运算符重载函数是非静态的类成员函数,在其参数列表中的第一个位置上隐藏了一个默认的this指针、
? ? 5、? ??
.*? ??::? ??sizeof? ???:? ??.? ?
?
注意,以上
5
个运算符不能进行运算符重载,
*
(作为乘法和指针解引用运算符)可以进行运算符重载,但是?
.*?
不能进行运算符重载、
5.2、赋值运算符重载
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
//赋值运算符重载函数(非静态类成员函数)、
void operator=(const Date& d) //void operator=(Date* const this,const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 14);
Date d3(d1); //调用拷贝构造函数,一个已存在的对象去初始化一个同类型(自定义类型)的要创建的对象、
d2 = d1; // d2.operator=(&d2,d1);
//此处的 = 对于内置类型而言的话,代表的是赋值运算符,由于对象d1和对象d2均是自定义类型,所以这里的 = 代表的就是赋值运算符重载,也叫作复制拷贝,
//会自动的去调用类体中的赋值运算符重载函数(非静态类成员函数),即一个已经存在的对象去给另外一个也已经存在的同类型(自定义类型)的对象进行赋值操作、
d1.Print();
d2.Print();
return 0;
}
? ? 但是上面的赋值运算符重载函数(非静态类成员函数)的写法是不对,不够全面,因为,我们知道,不管是在C语言中还是在C++中,对于内置类型的赋值运算符 = 而言,是支持连续赋值的,比如,如下代码所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
int main()
{
int i = 0, j, k;
j = i; //此时该表达式具有返回值,返回的是左操作数,整型变量j的值,即,不管在C还是C++中,内置类型的赋值操作符=构成的赋值表达式是具有返回值的,因为要支持连续赋值、
k = j = i;
printf("%d %d %d\n", i, j, k);
cout << i << " " << j << " " << k << endl;
return 0;
}
? ? ?但是我们上面所写的赋值运算符函数(非静态类成员函数),就目前而言是不支持连续赋值操作的,所以要进行优化,使得其也支持连续赋值操作,如下所示:
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
//赋值运算符重载函数(非静态类成员函数)、
//一个正确的运算符重载函数是具有返回值(返回类型)的,具体由被重载的运算符的运算结果所决定的、
//Date operator=(const Date& d) //Date operator<(Date* const this,const Date& d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
// return *this; //此时要返回的是左操作数,即自定义类型的对象d2,而不是左操作数的地址,其中,隐藏的this指针中存放的是左操作数(自定义类型对象d2)的地址、
//}
//在此处,三种返回方式均可以使用,但是,已知在C++中,自定义类型的对象以 传值的形式进行返回时,也会自动调用拷贝构造函数,尽量避免调用拷贝构造函数,所以不使用这种方法,使用传址返回和传引用返回均不会调用拷贝构造函数
//而传址返回又不如传引用返回,所以应使用传引用返回,虽然上述代码可以完成任务,但是不如使用传引用返回好,当三种方法都可以使用时,就使用 传引用返回,而不要使用 传值返回和传址返回 故再次优化为:
//标准版、
Date& operator=(const Date& d) // Date& operator=(Date* const this,const Date& d) 不要写成 const Date& operator=(Date* const this,const Date& d)
{ //避免在main函数中出现 (d3=d2)=d1, 这样就会报错、
if (this != &d) //此处的&是取地址操作,上行代码中的&是引用、
{
//最好不要写成: if (*this != d) ,此处的*this就是自定义类型的对象d2, 自定义类型的对象d本质上就是自定义类型的对象d1, 那么此处的 != 就属于 不等于运算符重载 ,那如果我们没有在类体中
//实现该 不等于运算符重载函数 的话,系统就会报错,就算在类体中实现了 不等于运算符重载函数 ,那么 if (*this != d) 也是一种函数的调用,效率较低,所以使用第一种方法比较好、
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; //此处的if判断语句主要是为了避免在main函数中出现像 d1=d1; 这种某一个已经存在的自定义类型的对象去给自己进行赋值操作,避免做无用功、
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 7, 13);
Date d2(2022, 7, 14);
Date d3(d1); //调用拷贝构造函数,一个已存在的对象去初始化一个同类型(自定义类型)的要创建的对象,等价于 Date d3 = d1;
d2 = d1; // d2.operator=(&d2,d1); 把左操作数的地址传给了隐藏的this指针、
//此处的 = 对于内置类型而言的话,代表的是赋值运算符,由于对象d1和对象d2均是自定义类型,所以这里的 = 代表的就是赋值运算符重载,也叫作复制拷贝,
//会自动的去调用类体中的赋值运算符重载函数(非静态类成员函数),即一个已经存在的对象去给另外一个也已经存在的同类型(自定义类型)的对象进行赋值操作、
//经过上述操作,此时该 赋值运算符重载 = 就支持了连续赋值、
//在执行代码 d2=d1; 时,会自动调用类体中的赋值运算符重载函数,然后,该函数返回出来的是自定义类型对象d2,当执行完类体中的赋值运算符重载函数时,会再次返回到该行代码处
//由于返回出来的是自定义类型的对象d2,在本行代码处,自定义类型的对象d2的生命周期并没有结束,所以类体中的赋值运算符重载函数是可以使用 传值,传引用返回和传址返回 的,但是最好
//使用 传引用 进行返回、
//当函数返回时,出了函数作用域,如果返回的对象还未还给系统,则可以使用 传值返回,传引用返回和传址返回 ,但是最好使用 传引用返回 ,如果已经还给系统了,则必须使用传值返回 -> C++入门课件、
d3 = d2 = d1;
//d1 = d1;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
注意:
? ? 若不在类体中显式的实现赋值运算符重载函数的话,则编译器会在类体中自动生成并调用一个赋值运算符重载函数,对内置类型进行浅拷贝(值拷贝),对于自定义类型则是通过自动调用其对应的赋值运算符重载函数进行赋值,功能类似于拷贝构造函数、
拓展:
? 给出如下代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Time
{
public:
Time(int x = 1) //带参全缺省的构造函数、
{
_hour = x;
}
int _hour; //公有、
};
class Date
{
public:
Date()
{
_year = 2022;
_month = 7;
_day = 14;
}
void Print()
{
cout << _year << "->" << _month << "->" << _day << endl;
cout << _time._hour <<endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
? ? ?由上述代码可知,在main函数中,当执行代码 Date d1;? 时,会自动调用Date类中的默认构造函数,此时编译器会先对Date类中的自定义类型的类成员变量 _time?进行处理,通过自动调用它所对应的构造函数进行初始化,由于在该过程中并没有进行传参(实参),所以会自动调用它对应的默认构造函数进行初始化,从而将Time类中的内置类型的类成员变量 _hour置为1,然后编译器会进入Date类中的默认构造函数的函数体内对Date类中的内置类型的类成员变量进行初始化处理,现在如果不想让Time类中的内置类型的类成员变量 _hour 置为1,而是置为10,还有什么方法呢,如下代码所示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Time
{
public:
Time(int x = 1) //带参全缺省的构造函数、
{
_hour = x;
}
int _hour; //公有、
};
class Date
{
public:
Date()
{
_year = 2022;
_month = 7;
_day = 14;
Time time(10);
_time = time;
}
void Print()
{
cout << _year << "->" << _month << "->" << _day << endl;
cout << _time._hour <<endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
? ??由上述代码可知,在main函数中,当执行代码 Date d1;? 时,会自动调用Date类中的默认构造函数,此时编译器会先对Date类中的自定义类型的类成员变量 _time?进行处理,通过自动调用它所对应的构造函数进行初始化,由于在该过程中并没有进行传参(实参),所以会自动调用它对应的默认构造函数进行初始化,从而将Time类中的内置类型的类成员变量 _hour置为1,然后编译器会进入Date类中的默认构造函数的函数体内先对Date类中的内置类型的类成员变量进行初始化处理,之后,再执行代码 Time time(10);? 此时定义了一个新的Time类型的对象time,并把该对象中的内置类型的类成员变量 _hour?置为了10,当执行代码:_time = time;?时,由于对象 time 和对象 _time 均是自定义类型,所以,编译器会自动调用赋值运算符重载函数(非静态类成员函数),此时,在 Time类体中并没有显式的实现赋值运算符重载函数,所以编译器会在类体中自动生成并调用一个赋值运算符重载函数,该函数会对内置类型的类成员变量进行浅拷贝,对自定义类型的类成员变量则会通过自动调用它对应的赋值运算符重载函数(非静态类成员函数)进行赋值,那么此时,自定义类型的对象d1中的自定义类型的类成员变量 _time?中的内置类型的类成员变量 _hour 就变成了10,如果把Time类中的Time(int x = 1)?改为Time(int x)的话,在main函数中执行代码:Date?d1;?时就会出错,此时编译器会自动调用Date类中的默认构造函数,编译器会先对Date类中的自定义类型的类成员变量 _time?进行处理,通过自动调用它所对应的构造函数进行初始化,由于在该过程中并没有进行传参(实参),所以会自动调用它对应的默认构造函数进行初始化,而现在,在 Time 类体中找不到默认的构造函数,所以会报错、
注意:赋值运算符重载函数不可以在类体外的全局区域中进行手动实现,即,不能实现成全局函数,而只能在类体中进行实现,即,只能实现成非静态的类成员函数,但是,除了赋值以外的运算符对应的运算符重载函数,既可以实现成全局函数,也可以实现成非静态的类成员函数,并且,对于赋值运算符重载函数而言,编译器会去类体中查找赋值运算符重载函数,若没有显式实现的话,那么编译器会在类体中自动实现并调用一个赋值运算符重载函数,但是对于除了赋值以外的运算符对应的运算符重载函数而言,编译器先去类体中查找,若没有定义,则会再去类体外的全局区域进行查找,若还没有定义,则编译器就会报错,而不会自动生成一个、
|