本文架构:
类和对象(2)
- 1 this指针
- 2 默认成员函数
- 2.1 构造函数
- 2.2 析构函数
- 2.3 拷贝构造函数
- 2.4 赋值运算符重载
- 2.4.1 运算符重载
- 2.4.2 赋值运算符重载
- 2.5-2.6 取地址及const取地址操作符重载
- 3 const 成员
- 3.1 const修饰类的成员函数
- 3.2关于函数调用的分析
1 this指针
C++编译器给每个“非静态成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中对所有成员变量的访问都是通过该指针访问的。但这个this指针对用户来说是透明的,即用户不需要来传递,编译器自动完成。
简言之,请看下面程序图解:
下面是一个日期类内的成员函数,通过次函数可以发现this指针在成员函数中扮演的角色。下图展示的两段程序表明this指针是成员函数中默认隐藏的,通过该指针去访问类内的成员变量。
关于this指针有几个需要注意的点:
- this指针的类型:类类型*const
- 只能在成员函数的内部使用
- this指针本质其实是一个成员函数的形参,this指针一般存储在栈上,对象调用成员函数时,将对象地址作为实参传递给this形参。对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
- this是C++的一个关键字,不能用来定义参数变量
- 思考:this指针可以为空吗?
如上图,两段代码,代码一可以调用左边类执行结果为Show(),而代码二会出错,问题在下图:
分析:
代码一:调用类内的show函数,show函数内没有使用到成员变量,所以没有使用到this指针,程序可以正常运行。(说明:p->show()这个函数并没有对p指针解引用,show函数没有使用到this指针,show函数地址也没有存到对象里面,所以不会引发空指针访问的问题,所以就不会发生程序奔溃。)
代码二:调用的类内的PrintA函数,而PrintA函数中使用了类内的成员变量,所以此函数就使用了this指针,void PrintA()转化成 void PrintA(A* this),this指针接收到对象p传来的空指针,引发了访问问题。
2 默认成员函数
下面这个类既没有成员函数,也没有成员变量,我们称之为空类。那么空类里面有什么?很容易想到默认成员函数。下面介绍6个默认成员函数,记住默认成员函数就是我们可以不写,会自动生成的。
1
2class Date{};
2.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
36class Date { public: void Display() { cout << _year << "-" << _month << "-" << _day << endl; } //无参数构造函数 Date(){}; //带参数构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { //调用默认参数 Date d2; d2.Display(); //调用带构造参数 Date d3(2020,2,1); d3.Display(); return 0; }
- 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参数的默认构造函数,一旦用户显示定义,编译器将不再生成。
笔记:
对于内置类型(基本类型,语言原生定义的类型,如int,char,指针等),不初始化。
对于自定义类型(用class,struct等定义的类型),如下图定义的类A的“A _aa;”等,编译器会调用默认构造函数初始化。
6. 无参数的构造函数和全缺省的构造函数都称之为默认构造函数,并且默认构造函数只能有一个。三种构造函数
- 自己不写,编译器默认生成的构造函数
- 自己写的无参数的构造函数
1
2
3
4
5
6
7Date() { _year = 2020; _month = 1; _day = 2; }
- 自己写的全缺省的构造函数,(推荐)
1
2
3
4
5
6
7Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; }
编程个人风格:
成员变量的命名方式:
1
2
3
4int year_; int mYear; int m_year;//m 代表 member,成员变量
2.2 析构函数
析构函数是特殊的成员函数,其功能与构造函数相反,析构函数不是完成对象的销毁,局部对象销毁工作是有编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数有如下特征:
- 析构函数名是在类名前加字符 ~
- 无参数、无返回值
- 一个类有且只有一个析构函数(析构函数不能重载)。若未显示定义,系统会调用默认的析构函数。
- 对象声明周期结束时,C++编译系统会自动调用析构函数。
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
56class Date { public: //全缺省构造函数 Date(int year = 2020, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } //析构函数不需要写,没有什么可清理 //默认生成的析构函数,不做什么 private: int _year; int _month; int _day; }; class Stack { public: //全缺省构造函数 Stack(int capacity = 4) { if (capacity <= 0) { _a = nullptr; _size = _capacity = 0; } else { _a = (int*)malloc(sizeof(int)*capacity); _capacity = capacity; _size = 0; } } //析构函数 ~Stack() { free(_a); _a = nullptr; _size = _capacity = 0; } private: int *_a; int _size; int _capacity; }; int main() { Date d; Stack st; return 0; }
析构和构造的顺序:
若上述栈类定义了两个栈对象
1
2
3Stack st1; Stack st2;
则他们的构造析构顺序为:st1先构造,st2后构造,st2先析构,st1后析构。
解释:对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出规则。
- 对于编译器自动生成的默认析构函数,对自定义类型成员调用它的析构函数。(简言之:自定义类型,调用对应的析构函数;内置类型,不处理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class String { public: String(const char* str = "jack") { _str = (char*)malloc(strlen(str) + 1); strcpy(_str, str); } ~String() { cout << "~String()" << endl; free(_str); } private: char* _str; }; class Person { private: String _name;//自定义类型,调用对应析构函数 int _age;//内置类型,不处理 };
2.3 拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个,且必须使用引用传参, 用传值方式会引发无穷递归调用。
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 Date { public: //构造函数 Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //拷贝构造函数 //Date(Date &d) Date(const Date &d)//使用const修饰Date表示不希望修改对象 { _year = d._year; _month = d._month; _day = d._day; //d._day = _day;//左值不可改变,因为使用了const } /*Date(const Date *d)//这样写可通过,不好,调用方式 Date d2(&d1); { _year = d->_year; _month = d->_month; _day = d->_day; }*/ /*Date(Date d) //传值拷贝,无穷递归,只要传值,就会调用拷贝构造函数 { _year = d._year; _month = d._month; _day = d._day; }*/ private: int _year; int _month; int _day; }; int main() { Date d1(2021, 5, 30); d1.Print(); 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#include <iostream> using namespace std; // 日期类 class Date { public: //构造函数 Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //默认拷贝构造函数 private: int _year; int _month; int _day; }; int main() { Date d1(2021, 5, 30); d1.Print(); //d2调用的是默认拷贝构造完成拷贝,d2和d1的值也是一样的 Date d2(d1); d2.Print(); return 0; }
总结:
- 有时候浅拷贝不能达到使用效果,所以有了深拷贝。
像如Stack、链表等这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,浅拷贝会导致析构两次,程序会奔溃,不满足我们的需求,需要自己实现深拷贝。
浅拷贝适合日期类这样的类。
- 调用析构函数时,这块空间被free了两次。
- 其中一个对象插入删除数据,都会导致另一个对象也插入删除数据。
拷贝构造函数对内置类型,完成浅拷贝或者值拷贝。对于自定义类型,成员会调用它的拷贝构造完成拷贝。
2.4 赋值运算符重载
2.4.1 运算符重载
C++为了增强代码可读性引入运算符重载,运算符重载是具有特殊函数名的函数,也有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字ope
注意点;
- 不能通过连接其他符号来创建新的操作符:如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,如内置类型 - ,不能改变为+
- 作为类成员的重载函数时,其形参看起来比操作数数目少一个成员函数(this指针提供)
- 像 .* 、:: 、sizeof、?: 、 . 这五种运算符不能重载。
一、全局operator==
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#include <iostream> using namespace std; // 日期类 class Date { public: //构造函数 Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } int _year; int _month; int _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(2021, 5, 30); Date d2(2021, 5, 30); d1.Print(); d2.Print(); cout << (d1 == d2) << endl; return 0; }
二、类内成员函数operator==
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#include <iostream> using namespace std; // 日期类 class Date { public: //构造函数 Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //类内运算符重载 //bool operator(Date* this,const Date& d) bool operator==(const Date &d) { return _year == d._year //this._year == d._year && _month == d._month //this._month == d._month && _day == d._day; //this._day == d._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2021, 5, 30); Date d2(2021, 5, 30); d1.Print(); d2.Print(); //调用方式等价于 d1.operator == (d2); //即 d1.operator == (&d1, d2); &1传递给this指针 d2传递给形参d cout << (d1 == d2) << endl; return 0; }
2.4.2 赋值运算符重载
赋值运算符重载的几个特点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝。
一、只能 实现 d1 = d2 功能
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#include <iostream> using namespace std; // 日期类 class Date { public: //构造函数 Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //拷贝构造函数 Date(const Date &d) { _year = d._year; _month = d._month; _day = d._day; } //赋值运算符重载 //d1 = d2 //void operator=(Date* this, const Date &d) void operator=(const Date &d) { if (this != &d)//检查不是自己给自己赋值,才需要拷贝。 { _year = d._year;//this._year = d._year; _month = d._month;//同上 _day = d._day;//同上 } } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2021, 5, 20); Date d3; //调用赋值运算法 d1 = d2 //调用方式 对于 d1 == d2 有 d1.operator=(&d1,d2); d1 = d2; d1.Print(); d2.Print(); d3.Print(); 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#include <iostream> using namespace std; // 日期类 class Date { public: //构造函数 Date(int year = 0, int month = 1, int day = 2) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //拷贝构造函数 Date(const Date &d) { _year = d._year; _month = d._month; _day = d._day; } //连续赋值运算符重载 //d1 = d2 = d3 //Date& operator=(Date* this, const Date &d) Date& operator=(const Date &d) { if (this != &d)//检查不是自己给自己赋值,才需要拷贝。 { _year = d._year;//this._year = d._year; _month = d._month;//同上 _day = d._day;//同上 } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(2021, 5, 20); Date d3; //调用赋值运算法 连续赋值 //调用方式 对于 d1 == d2 有 d1.operator=(&d1,d2); d3 = d1 = d2; d1.Print(); d2.Print(); d3.Print(); return 0; }
对比:拷贝构造函数和赋值运算符重载
调用方式:
- 拷贝构造函数:
已知d1,用同类对象d1初始化一个d2。
1
2
3
4Date d1(2021, 5, 30); Date d2(d1);
- 赋值运算符重载
虽然也是拷贝,但是实现已知d1和d2。相当于把d2的值拷贝给d1。
1
2
3
4
5Date d1; Date d2(2021, 5, 20); d1 = d2;
对比:函数重载和运算符重载
都使用了重载这个名词
- 函数重载时,支持定义同名函数
- 运算符重载是为了让自定义类型可以像内置类型一样去使用运算符
有时候编译器生成的默认赋值重载函数可以实现按字节序的值拷贝,但有时候并不能达到我们要求,所以对于下面程序出现的奔溃问题,提出了深拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class String { public: String(const char* str = "") { _str = (char*)malloc(strlen(str) + 1); strcpy(_str, str); } //析构函数 ~String() { cout << "~String()" << endl; free(_str); } private: char* _str; }; int main() { String s1("blogs"); String s2("yumoz"); s1 = s2; }
2.5-2.6 取地址及const取地址操作符重载
附上代码:
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#include <iostream> using namespace std; class Date { public: Date(int year = 0, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date* operator&() { cout << "Date* operator&()" << endl; return this; } const Date* operator&() const { cout << "const Date* operator&()const" << endl; return this; } private: int _year; int _month; int _day; }; int main() { Date d1(2021, 5, 27); const Date d2(2021, 4, 27); Date* ptr1 = &d1; const Date* ptr2 = &d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
3 const 成员
3.1 const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
3.2关于函数调用的分析
对于:
1)可以调用
2)可以调用
3)可以调用 const Date* 到const Date*(权限缩小)
4)不可调用,出错,错误为下面展示。const Date* 到 Date* (权限放大)
错误 1 error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”
几个思考题:
- const对象可以调用非const成员函数吗?
答:不可以,仔细阅读上图,这种情况输入权限放大,将只读类型调用可读可写类型,故会出错。 - 非const对象可以调用const成员函数吗?
答:可以,可读可写调用只读,权限缩小,所以可以。 - const成员函数内可以调用其他的非const成员函数吗?
答:不可以,const Date* 调用Date* ,权限放大,不可以。 - 非const成员函数可以调用其他的const成员函数吗?
答:可以,Date* 调用const Date* 权限缩小,故可以调用。
最后
以上就是眼睛大寒风最近收集整理的关于C++类和对象(2)(this指针、6个默认成员函数、const成员)1 this指针2 默认成员函数3 const 成员的全部内容,更多相关C++类和对象(2)(this指针、6个默认成员函数、const成员)1内容请搜索靠谱客的其他文章。
发表评论 取消回复