C++梁哥笔记day12 | 我的日常分享

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
    #include <iostream>

    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
    #include <iostream>

    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
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
class Data{
private:
int a;
int b;
public:
Data(){
cout<<"默认构造函数"<<endl;
}
Data(int a,int b):a(a),b(b){
cout<<"有参构造函数"<<endl;
}
~Data(){
cout<<"析构函数"<<endl;
}
void showData(){
cout<<"a:"<<a<<",b:"<<b<<endl;
}
};
test01(){
cout<<"-----test01------"<<endl;
Data ob1(10,30);
ob1.showData();
cout<<"-----------------"<<endl;
}
test02(){
cout<<"-----test02-------"<<endl;
Data *ob2 = new Data(10,30);
ob2->showData();
cout<<"-----------------"<<endl;
}

运行结果:
图片

test1调用了析构函数,test2没有调用析构函数,如果要调用析构函数我们要像test03一样手动使用delete

解决办法1:
智能指针设计:自动帮忙释放堆区空间

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
#include <iostream>

using namespace std;
class Data{
private:
int a;
int b;
public:
Data(){
cout<<"默认构造函数"<<endl;
}
Data(int a,int b):a(a),b(b){
cout<<"有参构造函数"<<endl;
}
~Data(){
cout<<"析构函数"<<endl;
}
void showData(){
cout<<"a:"<<a<<",b:"<<b<<endl;
}
};
class smartPointData{
public:
Data *ob;
public:
smartPointData(){
ob = NULL;
cout<<"smartPointData无参构造函数"<<endl;
}
smartPointData(Data *ob){
this->ob = ob;
cout<<"smartPointData有参构造函数"<<endl;
}
~smartPointData(){
cout<<"smartPointData析构函数"<<endl;
if(ob != NULL){
delete ob;
ob = NULL;
}
}
};
test04(){
cout<<"-----test04-------"<<endl;
smartPointData(new Data(10,30)).ob->showData();

smartPointData data = smartPointData(new Data(30,50));
data.ob->showData();

cout<<"-----------------"<<endl;
}

int main(int argc, char *argv[])
{
test04();
return 0;
}

运行结果:
图片

从test4的运行结果可以看到。并没有手动调用delete却实现了Data指针对象堆区空间的释放。
这个是利用系统会自动调用smartPointData的析构函数,在smartPointData的析构函数实现了释放Data 指针对象的堆区空间

  • 问题2:但是现在仍然存在一个问题,使用有点麻烦,还不如手动调用delete呢?

    1
    2
    smartPointData data = smartPointData(new Data(30,50));
    data.ob->showData();
  • 解决办法2:
    通过对.->运算符的重载,实现上述智能指针调用方式的简单化

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
69
70
71
72
73
#include <iostream>

using namespace std;
class Data{
private:
int a;
int b;
public:
Data(){
cout<<"默认构造函数"<<endl;
}
Data(int a,int b):a(a),b(b){
cout<<"有参构造函数"<<endl;
}
~Data(){
cout<<"析构函数"<<endl;
}
void showData(){
cout<<"a:"<<a<<",b:"<<b<<endl;
}
};
class smartPointData{
public:
Data *ob;
public:
Data* operator->(){
return this->ob;
}

smartPointData(){
ob = NULL;
cout<<"smartPointData无参构造函数"<<endl;
}
smartPointData(Data *ob){
this->ob = ob;
cout<<"smartPointData有参构造函数"<<endl;
}
~smartPointData(){
cout<<"smartPointData析构函数"<<endl;
if(ob != NULL){
delete ob;
ob = NULL;
}
}
};

//全局函数重载报错 成员函数重载正常 不知原因error: 'Data* operator->(smartPointData)' must be a nonstatic member function
//Data* operator->(smartPointData pData){
// return pData.ob;
//}

//这个用全局函数重载正常
Data operator*(smartPointData pData){
return *(pData.ob);
}
test05(){
cout<<"-----test05-------"<<endl;
smartPointData data = smartPointData(new Data(60,90));
data->showData();
cout<<"-----------------"<<endl;
}
test06(){
cout<<"-----test06-------"<<endl;
smartPointData data = smartPointData(new Data(90,110));
(*data).showData();
cout<<"------------------"<<endl;
}
int main(int argc, char *argv[])
{
test05();
test06();
return 0;
}

运行结果:
图片

显然调用方式简单了

1
2
3
4
//方式1
data->showData();
//方式2
(*data).showData();

四、赋值运算符=重载(重要)

赋值运算符常常使初学者混淆,这是毫无疑问的,因为=在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。

如果没有指针成员就不需要重载赋值运算符,因为有指针成员,由于浅拷贝的原因会导致指针变量会重复被释放。类中有指针成员必须重载=赋值运算符

旧对象给新对象赋值 调用的是拷贝构造函数(默认拷贝构造函数就是单纯的赋值 浅拷贝)

1
2
Person ob1("lucy",20);
Person ob2 = ob1;//调用拷贝构造函数

旧对象给旧对象赋值,调用的是赋值运算符=(默认的赋值运算符重载函数也是浅拷贝)

1
2
3
Person ob1("lucy",20);
Person ob2;
ob2 = ob1;//调用赋值运算符`=`
  • 对默认拷贝构造函数和默认赋值运算符函数进行重载
    拷贝构造函数
    1
    2
    3
    4
    5
    6
    Person(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
    10
    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;
    }
    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
    #include <iostream>
    #include<cstring>
    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指向的空间,重复释放,导致如下图错误。
图片

注意点:

  1. 赋值运算符重载函数中要释放之前空间,不然会导致内存泄漏
  2. 要考虑到ob3 = ob2 = ob1这种的连续赋值,就需要把返回值设置为Person,赋值运算是从右往左的,所以返回*this对象

如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造函数,也是进行简单值拷贝(浅拷贝)


五、等于==和不等于!=运算符重载

判断对象是否相等

1
2
3
4
5
6
bool operator ==(Person &ob){
if(strcmp(this->name,ob.name)==0 && this->age == ob.age){
return true;
}
return false;
}

判断对象是否不相等

1
2
3
4
5
6
bool operator !=(Person &ob){
if(strcmp(this->name,ob.name)!=0 || this->age != ob.age){
return true;
}
return false;
}

完整代码

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <iostream>
#include<cstring>
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;
}

bool operator ==(Person &ob){
if(strcmp(this->name,ob.name)==0 && this->age == ob.age){
return true;
}
return false;
}
bool operator !=(Person &ob){
if(strcmp(this->name,ob.name)!=0 || this->age != ob.age){
return true;
}
return false;
}
};
ostream& operator <<(ostream &out,Person &ob){
out<<"name:"<<ob.name<<",age:"<<ob.age;
return out;
}
void test02(){
Person ob1("lucy",20);
Person ob2("bob",18);
Person ob3("bob",20);
Person ob4("lucy",20);
cout<<"ob1:"<<ob1<<",\nob2:"<<ob2<<",\nob3:"<<ob3<<",\nob4:"<<ob4<<endl;

cout<<"************1**********"<<endl;
if(ob1 == ob2){
cout<<"ob1==ob2"<<" true"<<endl;
}else{
cout<<"ob1==ob2"<<" false"<<endl;
}
cout<<"************2**********"<<endl;
if(ob1 == ob4){
cout<<"ob1==ob4"<<" true"<<endl;
}else{
cout<<"ob1==ob4"<<" false"<<endl;
}
cout<<"************3**********"<<endl;
if(ob1 != ob2){
cout<<"ob1!=ob2"<<" true"<<endl;
}else{
cout<<"ob1!=ob2"<<" false"<<endl;
}
cout<<"************4**********"<<endl;
if(ob2 != ob3){
cout<<"ob2!=ob3"<<" true"<<endl;
}else{
cout<<"ob2!=ob3"<<" false"<<endl;
}
cout<<"***********end*********"<<endl;
}
int main(int argc, char *argv[])
{
test02();
return 0;
}

运行结果:
图片


六、仿函数

函数调用符号()重载 实现一个加法函数

1
2
3
int operator ()(int a,int b){
return a + b;
}

完整代码

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
#include <iostream>

using namespace std;
class Data{
private:
int num;
public:
Data(){
cout<<"无参构造函数"<<endl;
}
Data(int num){
cout<<"有参构造函数"<<endl;
this->num = num;
}
~Data(){
cout<<"析构函数"<<endl;
}
Data(const Data &ob){
cout<<"拷贝构造函数"<<endl;
}
int my_add(int a,int b){
return a + b;
}

int operator ()(int a,int b){
return a + b;
}
};

int main(int argc, char *argv[])
{
Data ob1;
cout<<"ob1.my_add(100,300)="<<ob1.my_add(100,300)<<endl;
cout<<"ob1.operator()(100,300)="<<ob1.operator()(100,300)<<endl;
cout<<"ob1(100,300)="<<ob1(100,300)<<endl;
//匿名对象 未传入参数会调用无参构造
cout<<"Data()(100,300)"<<Data()(100,300)<<endl;
//匿名对象 传入一个参数会调用有参构造
cout<<"Data(12)(100,300)"<<Data(12)(100,300)<<endl;
return 0;
}

运行结果:
图片

注意点:

  1. 注意使用匿名对象调用()的使用

七、不要重载逻辑与&&和逻辑或||

因为用户无法实现&&和||的短路特性

不能重载operator&& 和operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说的具体一些,内置版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了,而且能够保证能不需要。我们已经习惯这种方便的特性了。我们说操作符重载其实是另一种的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。


八、运算符重载总结

=,[],()->运算符只能通过成员函数重载,<<>>只能通过全局函数配合友元进行重载,不要重载&&||运算符,因为用户无法实现短路特性。
常规建议:

运算符 建议使用
所有一元运算符 成员函数
= () [] -> ->* 必须是成员函数
+= -= /= *= ^= &= != %= >>= <<= 成员函数
其它二元运算符 非成员函数