C++梁哥笔记day12
类与对象
一、可重载运算符
几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制。特别是不能使用C中当前没有意义的运算符(例如用**
求幂)不能改变运算符优先级,不能改变运算符的参数个数。这样的限制有意义,否则,这些行为产生的运算符只会混淆而不是澄清其含义。
可以重载的运算符:
+ |
- |
* |
/ |
% |
^ |
& |
` | ` |
! |
= |
< |
> |
+= |
-+ |
*= |
/= |
^= |
&= |
` | =` | << |
>> |
<<= |
>>= |
== |
!= |
>= |
&& |
` | ` | ++ |
-- |
->* |
' |
|
() |
new |
delete |
new[] |
delete[] |
无 | 无 | 无 | 无 |
不能重载的运算符:
· |
:: |
·* |
?: |
sizeof |
二、重载自增++和自减–运算符
- 重载自增运算符++运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using namespace std;
class Data
{
private:
int a;
int b;
public:
friend ostream& operator <<(ostream &out,Data &ob);
Data() {
cout<<"默认构造函数"<<endl;
}
Data(int a,int b):a(a),b(b){
cout<<"有参构造函数"<<endl;
}
~Data(){
cout<<"析构函数"<<endl;
}
void showData(){
cout<<"a="<<this->a<<",b="<<this->b<<endl;
}
//利用成员函数减少一个参数
//前置++ ++a 先加再使用
Data& operator++(){
this->a++;
this->b++;
return *this;
}
//利用成员函数减少一个参数
//后置++ a++ 先使用后++ (后置++要使用一个占位int)
Data& operator++(int){
//用一个变量保存原来的对象 返回的是原来的对象 但是对象的数据修改了
static Data oldData = *this;
this->a++;
this->b++;
return oldData;
}
};
//重载<<
ostream& operator <<(ostream &out,Data &ob){
out<<"a:"<<ob.a<<",b:"<<ob.b<<endl;
return out;
}
test01(){
Data ob1(10,20);
ob1.showData();
ob1.operator ++();
ob1.showData();
cout<<++ob1;
ob1.showData();
}
test02(){
Data ob2(10,20);
ob2.showData();
ob2.operator ++();
ob2.showData();
cout<<ob2++;
ob2.showData();
}
int main(int argc, char *argv[])
{
test01();
test02();
return 0;
}
- 重载自减运算符–
与重载自增运算符类似运行结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using namespace std;
class Data
{
private:
int a;
int b;
public:
friend ostream& operator <<(ostream &out,Data &ob);
Data() {
cout<<"默认构造函数"<<endl;
}
Data(int a,int b):a(a),b(b){
cout<<"有参构造函数"<<endl;
}
~Data(){
cout<<"析构函数"<<endl;
}
void showData(){
cout<<"a="<<this->a<<",b="<<this->b<<endl;
}
//利用成员函数减少一个参数
//前置-- --a 先加再使用
Data& operator--(){
this->a--;
this->b--;
return *this;
}
//利用成员函数减少一个参数
//后置-- a-- 先使用后-- (后置--要使用一个占位int)
Data& operator--(int){
//用一个变量保存原来的对象 返回的是原来的对象 但是对象的数据修改了
static Data oldData = *this;
this->a--;
this->b--;
return oldData;
}
};
//重载<<
ostream& operator <<(ostream &out,Data &ob){
out<<"a:"<<ob.a<<",b:"<<ob.b<<endl;
return out;
}
test03(){
Data ob1(10,20);
ob1.showData();
ob1.operator --();
ob1.showData();
cout<<--ob1;
ob1.showData();
}
test04(){
Data ob2(10,20);
ob2.showData();
ob2.operator --();
ob2.showData();
cout<<ob2--;
ob2.showData();
}
int main(int argc, char *argv[])
{
test03();
test04();
return 0;
}
优先使用++和–的标展形式,优先调用前置++。如果定义了++a,也要定义a++,递增操作符比较麻烦,因为他们都有前缀和后缀形式,而两种语义略有不同。重载operator++和operator–时应该魔方他们对于的内置操作符。对于++和–而言,后置形式是先返回,然后对象++或者–,返回的是对象的原值;前置形式,对象先++或–,返回当前对象,返回的是新对象。其标准形式为:
三、重载指针运算符 * 和 ->(了解)
问题1:
1 | class Data{ |
运行结果:
test1调用了析构函数,test2没有调用析构函数,如果要调用析构函数我们要像test03一样手动使用delete
解决办法1:
智能指针设计:自动帮忙释放堆区空间
1 |
|
运行结果:
从test4的运行结果可以看到。并没有手动调用delete却实现了Data指针对象堆区空间的释放。
这个是利用系统会自动调用smartPointData的析构函数,在smartPointData的析构函数实现了释放Data 指针对象的堆区空间
问题2:但是现在仍然存在一个问题,使用有点麻烦,还不如手动调用delete呢?
1
2smartPointData data = smartPointData(new Data(30,50));
data.ob->showData();解决办法2:
通过对.
和->
运算符的重载,实现上述智能指针调用方式的简单化
1 |
|
运行结果:
显然调用方式简单了
1 | //方式1 |
四、赋值运算符=重载(重要)
赋值运算符常常使初学者混淆,这是毫无疑问的,因为=
在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
如果没有指针成员就不需要重载赋值运算符,因为有指针成员,由于浅拷贝的原因会导致指针变量会重复被释放。类中有指针成员必须重载=赋值运算符
旧对象给新对象赋值 调用的是拷贝构造函数(默认拷贝构造函数就是单纯的赋值 浅拷贝)
1 | Person ob1("lucy",20); |
旧对象给旧对象赋值,调用的是赋值运算符=
(默认的赋值运算符重载函数也是浅拷贝)
1 | Person ob1("lucy",20); |
- 对默认拷贝构造函数和默认赋值运算符函数进行重载
拷贝构造函数
1
2
3
4
5
6Person(const Person &ob){
this->name = new char[strlen(ob.name)+1];
strcpy(this->name,ob.name);
this->age = ob.age;
}赋值运算符重载函数
1
2
3
4
5
6
7
8
9
10Person& operator =(Person &ob){
if(this->name != NULL){//这步必须要,不然会造成内存泄漏
delete [] this->name;
this->name = NULL;
}
this->name = new char[strlen(ob.name)+1];
strcpy(this->name,ob.name);
this->age = ob.age;
return *this;
}main.cpp完整代码
运行结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using namespace std;
class Person{
private:
char *name;
int age;
public:
Person(){
cout<<"无参构造函数"<<endl;
}
Person(char *name,int age){
cout<<"有参构造函数"<<endl;
this->name = new char[strlen(name)+1];
strcpy(this->name,name);
this->age = age;
}
~Person(){
cout<<"析构函数"<<endl;
if(name != NULL){
delete [] name;
name = NULL;
}
}
Person(const Person &ob){
this->name = new char[strlen(ob.name)+1];
strcpy(this->name,ob.name);
this->age = ob.age;
}
friend ostream& operator <<(ostream &out,Person &ob);
Person& operator =(Person &ob){
if(this->name != NULL){//这步必须要,不然会造成内存泄漏
delete [] this->name;
this->name = NULL;
}
this->name = new char[strlen(ob.name)+1];
strcpy(this->name,ob.name);
this->age = ob.age;
return *this;
}
};
ostream& operator <<(ostream &out,Person &ob){
out<<"name:"<<ob.name<<",age:"<<ob.age;
return out;
}
void test01(){
Person ob1("lucy",20);
Person ob2 = ob1;//调用拷贝构造函数 拷贝构造函数中不用释放之前的空间 因为拷贝构造函数一定发生在初始化中
cout<<"ob2,"<<ob2<<endl;
Person ob3;
ob3 = ob1;//调用赋值运算符重载函数 需要判断之前对象是否初始化过,如果初始化过,需要释放之前的空间,重新申请需要的空间
cout<<"ob3,"<<ob3<<endl;
Person ob4("bob",18);
ob4 = ob1;//这个时候ob4初始化过,如果不释放之前空间,会造成内存泄漏;直接用之前的空间也不行,之前空间可能小了或大了不确定,还不如释放后重新申请
cout<<"ob4,"<<ob4<<endl;
}
int main(int argc, char *argv[])
{
test01();
return 0;
}
一旦指针作为类的成员,拷贝构造函数要自定义,赋值运算符也需要重载,不然会导致堆区空间多次释放导致段错误,程序直接中断报错;析构函数中也需要释放类成员的堆区空间,不然会造成内存泄漏
如果没有重载拷贝构造函数,或者赋值运算符重载函数,会导致不同对象的name指向同一个堆区空间,并且不同的对象都会调用析构函数释放name指向的空间,重复释放,导致如下图错误。
注意点:
- 赋值运算符重载函数中要释放之前空间,不然会导致内存泄漏
- 要考虑到
ob3 = ob2 = ob1
这种的连续赋值,就需要把返回值设置为Person
,赋值运算是从右往左的,所以返回*this
对象
如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造函数,也是进行简单值拷贝(浅拷贝)
五、等于==和不等于!=运算符重载
判断对象是否相等
1 | bool operator ==(Person &ob){ |
判断对象是否不相等
1 | bool operator !=(Person &ob){ |
完整代码
1 |
|
运行结果:
六、仿函数
函数调用符号()重载
实现一个加法函数
1 | int operator ()(int a,int b){ |
完整代码
1 |
|
运行结果:
注意点:
- 注意使用匿名对象调用
()
的使用
七、不要重载逻辑与&&和逻辑或||
因为用户无法实现&&和||的短路特性
不能重载operator&& 和operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说的具体一些,内置版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了,而且能够保证能不需要。我们已经习惯这种方便的特性了。我们说操作符重载其实是另一种的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。
八、运算符重载总结
=
,[]
,()
和->
运算符只能通过成员函数重载,<<
和>>
只能通过全局函数配合友元进行重载,不要重载&&
和||
运算符,因为用户无法实现短路特性。
常规建议:
运算符 | 建议使用 |
---|---|
所有一元运算符 | 成员函数 |
= () [] -> ->* |
必须是成员函数 |
+= -= /= *= ^= &= != %= >>= <<= |
成员函数 |
其它二元运算符 | 非成员函数 |