目录
4、拷贝构造函数
4.1、概念
int main()
{
//在C++中,某一个自定义类型的对象在声明和定义时使用同类型(自定义类型)的已存在的对象进行初始
//化时,并且还满足自动调用的拷贝构造函数的形参除隐藏的this指针外有且只有一个,比如:Date d2(d1)
//;此时符合拷贝构造函数的自动调用时机的第一种情况,则会自动调用拷贝构造函数,而当执行代码:Date
//d1(2022,7,13);时,发现不满足拷贝构造函数的自动调用时机的三种情况,故不自动调用拷贝构造函数,
//而是自动调用普通的构造函数、
Date d1(2022,7,13); //自动调用普通的构造函数、
Date d2(d1); //自动调用拷贝构造函数、
//相当于使用自定义类型且已存在的对象d1去初始化自定义类型的对象d2,此时自动调用的就是拷贝
//构造函数、
return 0;
}
//拷贝构造函数的自动调用时机:
//C++中拷贝构造函数的自动调用时机通常有3种情况:
//1、某一个自定义类型的对象在声明和定义时使用同类型(自定义类型)的已存在对象进行初始化时,并且
//还满足自动调用的拷贝构造函数的形参除隐藏的this指针外有且只有一个,比如: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();
return 0;
}
//注意:
//Date d1;
//Date d2(d1); 等价于Date d2=d1;都是通过已经创建的自定义类型的对象来对正在创建的自定义类型的
//对象进行初始化,要保证是同一种类型(自定义类型)、
拷贝构造函数是构造函数的一种特殊形式,故其函数名也和类名相同,即:拷贝构造函数是构造函数的一个重载形式,除此之外,拷贝构造函数除隐藏的 this 指针外,有且只有一个参数,并且拷贝构造函数均是非默认拷贝构造函数、
拷贝构造函数:除隐藏的 this 指针外,有且只有单个形参,该形参是对本类类型对象的引用(一般常用 const 关键字进行修饰,防止在拷贝构造函数的函数体中将被拷贝的自定义类型的对象中的内容修改)、
4.2、特征
拷贝构造函数也是特殊的构造函数,其特征如下:1、拷贝构造函数是构造函数的一个重载形式、2、拷贝构造函数的参数除隐藏的 this 指针外有且只有一个且必须使用 传引用传参 ,当使用传引用传参时,就不会再自动调用拷贝构造函数,则就不会造成无穷递归调用了,使用传值传参的方式会引发无穷递归调用、
#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;
}
#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 中的两块内存空间中存储的值一模一样,并且此处的内置类型的类成员变量也没有涉及到需要使用深拷贝,所以能够输出正确的结果、
#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; } //不显示的在类体中声明和定义拷贝构造函数的话,编译器则会自动生成一个拷贝构造函数(默认(紫色)但 //非默认(蓝色)),其结构 大概 如下所示: MyClass(const MyClass& other) //默认(紫色)但非默认(蓝色)、 :m_x(other.m_x) //初始化列表、 {}
答:可以的,所谓浅拷贝能够达到目的就是保证不涉及到深拷贝即可,常见的涉及到深拷贝的情况有:动态开辟(堆区),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++ 中的自定义类型的对象而言,若进行 上述总结的 3 种自动调用拷贝构造函数的情况时 ,则是通过自动调用拷贝构造函数来进行的拷贝;首先要知道, 拷贝构造函数中可以实现浅拷贝(值拷贝),也可以实现深拷贝,在拷贝构造函数外,也可以实现浅拷贝(值拷贝)和深拷贝 ;为了使得通过自动调用拷贝构造函数就能完成浅拷贝(值拷贝)和深拷贝任务,所以我们只需要维护好拷贝构造函数即可、
注意:1、如果用户在类体中 只 显式的实现了构造函数( 默认 和非 默认 ),则 C++ 编译器就不再自动生成无参(除了隐藏的 this 指针外)的默认(即是 默认 又是 默认 )构造函数,但是仍然会自动生成一个拷贝构造函数( 默认 但非 默认 )、2、如果用户在类体中显式的实现了拷贝构造函数(非 默认 和非 默认 ),那么 C++ 编译器就不再自动生成无参(除了隐藏的 this 指针外)的默认构造函数( 默认 和 默认 )了,这是因为该拷贝构造函数(非 默认 和非 默认 )是非 默认 非 默认 的构造函数、#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;
}
#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++ 中就引出了运算符重载的概念,从而去间接的使用这大部分的运算符,而在 C 语言中没有引出运算符重载的概念,故没有办法间接的使用这大部分的运算符,要知道,此处的运算符重载和之前所谓的函数重载不是一个概念,所谓运算符重载就是重新去定义或者说是控制某些大部分运算符的使用规则,当操作数均是内置类型时,就可以直接使用各种运算符,当操作数只要出现了自定义类型就不可以直接使用各种运算符、
#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;
}
//1和2、
/*int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}*/
//3、
int GetYear()const
{
return _year;
}
int GetMonth()const
{
return _month;
}
int GetDay()const
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
//1、
//全局函数、
//bool operator==(Date d1, Date d2) //传值传参(可以)、
//{
// return d1.GetYear() == d2.GetYear() && d1.GetMonth() == d2.GetMonth() && d1.GetDay() == d2.GetDay();
//}
//2、
//全局函数、
//传引用传参(此处不加关键字const是可以的,但通常我们都需要加上,效果更好,如3所示)、
//bool operator==(Date& d1, Date& d2)
//{
// return d1.GetYear() == d2.GetYear() && d1.GetMonth() == d2.GetMonth() && d1.GetDay() == d2.GetDay();
//}
//3、
//全局函数、
//传引用传参(此处加上关键字 const 是不可以的,会报错)、
bool operator==(const Date& d1, const Date& d2)
{
return d1.GetYear() == d2.GetYear() && d1.GetMonth() == d2.GetMonth() && d1.GetDay() == d2.GetDay();
}
//以d1.GetYear();为例,会被编译器自动替换为:d1.GetYear(&d1);此时,在上述函数体中,自定义类型的
//对象d1的类型是const Date,则&d1后的类型为:const Date*,而对于非静态类成员函数GetYear而言,
//在其形参列表中的第一个位置上隐藏的this指针的类型为:Date* const,则此时属于权限放大,编译错误
//,所以可在非静态类成员函数GetYear的形参列表后面加上关键字const,则属于权限不变,即可编译成功,
//要注意:此处所谓的权限和*右边的const没有关系,只和*左边的const有关,可以直接忽略掉*右边
//的const、
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; //此时该表达式具有返回值,返回的是左操作数,int整型变量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=(Date* const this,const Date& d) 不要写成 const Date& operator //=(Date* const this,const Date& d),避免在main函数中出现 (d3=d2)=d1 ,这样就会报错、 Date& operator=(const Date& d) { 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 类体中找不到默认的构造函数,所以会报错、
注意:赋值运算符重载函数不可以在类体外的全局区域中进行手动实现,即:不能实现成全局函数,而只能在类体中进行实现,即只能实现成类成员函数,但是,除了赋值以外的运算符对应的运算符重载函数,既可以实现成全局函数,也可以实现成类成员函数,并且,对于赋值运算符重载函数而言,编译器会去类体中查找赋值运算符重载函数,若没有显式实现的话,那么编译器会在类体中自动生成一个赋值运算符重载函数(默认和非默认),但是对于除了赋值以外的运算符对应的运算符重载函数而言,编译器先去类体中查找,若没有声明和定义,则会再去类体外的全局区域进行查找,若还没有声明和定义,则编译器就会报错,而不会自动生成一个、
注意:
如果某一个构造函数的初始化列表为空,且在该构造函数的函数体中对类体中的内置类型的类成员变量进行处理的话,那么一定是先对类体中的自定义类型的类成员变量进行处理,后对类体中的内置类型的类成员变量进行处理,具体见初始化列表的总结、