C++ 类与对象(二)

前言:C++编译器默认在类中生成六个特殊的成员函数,如果在类中显式定义相对应的成员函数,编译器则不会生成对应成员函数

一、构造函数

1.1 什么是构造函数

 根据经验,不少难以察觉的程序错误都是由于变量没有正确初始化导致的,而初始化工作很容易被人遗忘。C++中引入构造函数的概念,构造函数是一个特殊的成员函数,函数名与类名相同,实例化对象时由编译器自动调用构造函数,保证对象中的成员变量都被正确初始化。构造函数在对象的生命周期内只调用一次。注意:构造函数的作用是初始化对象,而不是为对象分配内存空间。

 构造函数不显式定义时由编译器自动生成,当显式定义构造函数时,编译器不会自动生成。构造函数的函数名必要与类名一致,并且没有返回值(void也不行)。

1.2 构造函数特性

1.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
class Person
{
private:
char _name[10];
int _age;

public:
Person() // 无参的构造函数
{
;
}

Person(const char* name, int age) // 带参数的构造函数
{
strcpy(_name,name);
_age = age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

1.2.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
class Person
{
private:
char _name[10];
int _age;

public:
Person() // 无参的构造函数
{
cout << "调用无参构造函数" << endl;
}

Person(const char* name, int age) // 带参数的构造函数
{
strcpy(_name,name);
_age = age;
cout << "调用带参数的构造函数" << endl;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1; // 调用无参构造函数
Person p2("张三",18); // 调用带参构造函数

return 0;
}

输出

1
2
调用无参构造函数
调用带参数的构造函数

说明:实例化对象调用无参构造函数不能这种方式写Person p1();因为编译器会认为这个是函数声明,Person是函数返回类型,p1为函数名。只能这样方式写Person p1;才调用无参构造函数,虽然看起来语法有些奇怪

1.2.3 默认构造函数

构造函数的形参列表为空称为默认构造函数,默认构造函数有3种,一个类中默认构造函数只能有一个,推荐使用全缺省的默认构造函数

1.类中没有显式定义构造函数,则C++编译器会自动生成一个无参的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Person
{
private:
char _name[10];
int _age;
};

int main()
{
Person p1; // 调用编译器生成的默认构造函数
return 0;
}

2.在类中显式定义一个无参构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
private:
char _name[10];
int _age;

public:
Person() // 无参的构造函数
{
cout << "调用无参构造函数" << endl;
}
};

int main()
{
Person p1; // 调用显式定义的默认构造函数

return 0;
}

3.在类中定义一个全缺省的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
private:
char _name[10];
int _age;

public:
Person(const char* name = "王某某", int age = 18) // 全缺省构造函数
{
strcpy(_name, name);
_age = age;
}
};

int main()
{
Person p1; // 调用显式全缺省的默认构造函数

return 0;
}

1.3 编译器生成的默认构造函数的功能

 在类中不显式定义构造函数,编译器生成默认构造函数。这个构造函数对于对象中内置类型成员变量不做任何初始化,如果成员变量为自定义类型则调用它的默认构造函数

内置类型:C++语法已经定义好的类型,例如 int、char、double等
自定义类型:用class、struct、union关键字自己定义的类型

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
class A
{
private:
int x;

public:
A()
{
cout << "这是类A的构造函数" << endl;
}
};

class Person
{
private:
int _age;
A ob;

public:
void Print()
{
cout << "年龄:" << _age << endl;
}
};

int main()
{
Person p1;
p1.Print();
return 0;
}

输出

1
2
这是类A的构造函数
年龄:-858993460

编译器生成Person类的默认构造函数对于内置类型成员变量没有任何处理,所以_age打印出来是随机值,ob为自定义类型,所以调用了ob对应的默认构造函数

1.4 explicit关键字

使用explicit关键字修饰的构造函数将禁止隐式类型转换,只能以显式的方式进行类型转换explicit只能在类内部使用,即构造函数的声明与定义分离,只能在声明前面加explicit修饰。

1.4.1 隐式类型转换

c++支持隐式类型转换,即根据类型A变量的值创建一个类型B的临时变量,并将该临时变量赋值给某个类型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
#include <iostream>

using namespace std;

class A
{
private:
int _a;
public:
A(int a = 10)
{
_a = a;
cout << "构造函数" << endl;
}

A(const A& obj)
{
_a = obj._a;
cout << "拷贝构造函数" << endl;
}
};


int main()
{
A a = {3}; // 隐式类型转换
A b = 4; // 隐式类型转换

return 0;
}

在这里插入图片描述

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

using namespace std;

class A
{
private:
int _a;
public:
explicit A(int a = 10)
{
_a = a;
cout << "构造函数" << endl;
}

A(const A& obj)
{
_a = obj._a;
cout << "拷贝构造函数" << endl;
}
};


int main()
{
A a = {3}; // 错误,不能隐式类型转换
A b = 4; // 错误,不能隐式类型转换
A c = (A)3; // 正确,强制类型转换
A D = (A){4}; // 正确,强制类型转换

return 0;
}

例2: 构造函数有多个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

class A
{
public:
explicit A(int a, int b) : _a(a), _b(b) {}

private:
int _a;
int _b;
};


int main()
{
A a = {1, 2}; // 错误,不能隐式类型转换
A b = (A){1, 2}; // 正确,强制类型转换
}

二、析构函数

 根据经验,动态申请的内存空间在使用结束后往往忘记释放,造成内存泄漏的问题。C++中引入析构函数的概念,析构函数是一个特殊的成员函数,函数名在类名基础上加前缀~。对象生命周期结束时,C++编译器自动调用析构函数。注意:对象销毁工作是由编译器完成的,析构函数作用是在对象销毁前完成某些资源清理

 析构函数不显式定义时由编译器自动生成,当显式定义构造函数时,编译器不会自动生成。析构函数的函数名在类名前加上字符 ~,并且没有返回值(void也不行),没有形参

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
typedef	int ElemType; //元素类型重定义,以后元素类型发生改变只用改这里
#define initSize 10 // 顺序表初始化大小

class SeqList
{
private:
ElemType* _data; //动态内存分配存放元素的数组
size_t _size; //已存放元素个数
size_t _capacity; //数组容量

public:
SeqList()
{
_data = (ElemType*)malloc(sizeof(ElemType) * initSize);
_size = 0;
_capacity = initSize;
}

~SeqList() // 析构函数
{
free(_data); // 释放动态内存分配的空间
_data = nullptr;
_size = 0;
_capacity = 0;
}
};

2.1 编译器生成的析构函数的功能

在类中不显式定义析构函数,编译器自动生成析构函数。这个构造函数对于对象中内置类型成员变量不做处理,如果成员变量为自定义类型则调用它的析构函数

内置类型:C++语法已经定义好的类型,例如 int、char、double等
自定义类型:用class、struct、union关键字自己定义的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A
{
public:
~A()
{
cout << "这是类A的析构函数" << endl;
}
private:
int x;
};

class Person
{
private:
char _name[10];
int _age;
A ob;
};

int main()
{
Person p1;
return 0;
}

输出

1
这是类A的析构函数

三、拷贝构造函数

 在生活中我们有时候会想,如果能够复制一个一模一样的自己那该多好。可以替我学习、替我工作。当然这只是人的幻想罢了,但在C++中可以梦想成真。C++中引入了拷贝构造函数的概念,用一个已存在的对象创建新对象时,由编译器自动调用拷贝构造函数,创建的新对象与已存在对象一模一样

拷贝构造函数不显式定义时由编译器自动生成,当显式定义拷贝构造函数时,编译器不会自动生成。 拷贝构造函数是构造函数的重载,它与构造函数外表唯一区别是拷贝构造函数只有一个形参,形参必须为引用,并推荐用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
class Person
{
private:
char _name[10];
int _age;

public:
Person(const char* name = "王某某", int age = 18) // 全缺省构造函数
{
strcpy(_name, name);
_age = age;
}

Person(const Person& obj) // 拷贝构造函数
{
cout << "调用拷贝构造函数" << endl;
strcpy(_name, obj._name);
_age = obj._age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1("张三",18);
Person p2(p1); // 调用拷贝构造函数,推荐写法
Person p3 = p1; // 调用拷贝构造函数,不推荐写法
p1.Print();
p2.Print();
p3.Print();

return 0;
}

输出

1
2
3
4
5
调用拷贝构造函数
调用拷贝构造函数
姓名:张三 年龄:18
姓名:张三 年龄:18
姓名:张三 年龄:18

3.1 拷贝构造函数特性

3.1.1 显式定义拷贝构造函数时必须显式定义默认构造函数

拷贝构造函数是构造函数的重载,当显式定义拷贝构造函数时,编译器不会生成默认构造函数,此时需要显式定义默认构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
private:
char _name[10];
int _age;

public:
Person(const Person& obj) // 拷贝构造函数
{
cout << "调用拷贝构造函数" << endl;
strcpy(_name, obj._name);
_age = obj._age;
}
};

int main()
{
Person p1; // 错误,没有默认构造函数可以调用
return 0;
}

3.1.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
class Person
{
private:
char _name[10];
int _age;

public:
Person(const char* name = "王某某", int age = 18) // 全缺省构造函数
{
strcpy(_name, name);
_age = age;
}

Person(const Person obj) // 拷贝构造函数
{
cout << "调用拷贝构造函数" << endl;
strcpy(_name, obj._name);
_age = obj._age;
}
};

int main()
{
Person p1("张三", 18);
Person p2(p1); // 错误,变成死循环
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
class Person
{
private:
char _name[10];
int _age;

public:
Person(const char* name = "王某某", int age = 18) // 全缺省构造函数
{
strcpy(_name, name);
_age = age;
}

Person(const Person* obj) // 拷贝构造函数
{
cout << "调用拷贝构造函数" << endl;
strcpy(_name, obj->_name);
_age = obj->_age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1("张三",18);
Person p2(&p1); // 使用指针方法调用拷贝构造函数
p1.Print();
p2.Print();

return 0;
}

输出

1
2
3
调用拷贝构造函数
姓名:张三 年龄:18
姓名:张三 年龄:18

3.2 编译器生成的拷贝构造函数的功能

 在类中不显式定义拷贝构造函数,编译器自动生成拷贝构造函数。这个拷贝构造函数对于对象中内置类型成员变量按字节拷贝(浅拷贝),如果成员变量为自定义类型则调用它的拷贝构造函数

内置类型:C++语法已经定义好的类型,例如 int、char、double等
自定义类型:用class、struct、union关键字自己定义的类型

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
class A
{
private:
int x;

public:
A()
{
cout << "这是类A的构造函数" << endl;
}
A(const A& obj)
{
cout << "这是类A的拷贝构造函数" << endl;
}

};

class Person
{
private:
char _name[10];
int _age;
A ob;

public:
Person(const char* name = "王某某", int age = 18)
{
strcpy(_name, name);
_age = age;
}

void Print()
{
cout << "姓名:" << _name << " 年龄:" << _age << endl;
}
};

int main()
{
Person p1("张三",18);
Person p2(p1); // 调用拷贝构造函数
p1.Print();
p2.Print();

return 0;
}

输出

1
2
3
4
这是类A的构造函数
这是类A的拷贝构造函数
姓名:张三 年龄:18
姓名:张三 年龄:18

四、运算符重载

 C++为了增强代码的可读性引入了运算符重载概念,运算符重载是一个特殊的函数。通过关键字operator加上运算符来表示函数名,函数的返回值类型及参数列表与普通函数类似。重载的运算符并不会改变它们做为内置运算符的使用方法。

● 不能重载C++语言不支持的运算符,比如 @
● 不要试图改变重载运算符的语义,要与内置语义保存一致。例如将++运算符重载后执行递减操作,损害代码的可读性、正确性

4.1 运算符重载的两种形式

运算符重载有两种形式,一种运算符被重载为类的成员函数,另一种重载为全局函数。推荐被重载为类的成员函数。运算符重载为成员函数比重载为全局函数而言函数的形参看起来少一个参数,对于成员函数编译器会将左操作数的地址隐式传给形参this指针

4.1.1 重载为类的成员函数

运算符的左操作数为类的对象时,建议运算符重载为类的成员函数,因为C++编译器给每个“非静态的成员函数“增加了一个隐藏的this指针形参,并会将左操作数(即类对象)的地址做为实参传给形参this指针

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
class Date
{
private:
int _year;
int _month;
int _day;

public:
bool Date::operator<(Date& obj) // 重载< 运算符,用来比较两个日期类型对象的大小
{
if (_year < obj._year)
{
return true;
}
else if(_year == obj._year && _month < obj._month)
{
return true;
}
else if (_year == obj._year && _month == obj._month && _day < obj._day)
{
return true;
}
else
{
return false;
}
}
};

4.1.2 重载为全局函数

运算符的左操作数不是类的对象时,运算符重载为全局函数。假设重载为成员函数,编译器会将左操作数地址传给隐藏的形参this指针,this指针类型为 类名* ,与&左操作数类型不匹配

1
2
3
4
5
6
ostream& operator<<(ostream& out, const Date& obj)
{
out << obj._year << "-" << obj._month << "-" << obj._day << endl;

return out;
}

4.2 运算符重载调用

运算符重载是一种特殊的函数,在与普通函数相比,函数调用有些不同。不仅支持正常的函数名+()方式调用,又支持通过运算符方式调用。推荐使用运算符方式调用,提高代码可读性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Date
{
private:
int _year;
int _month;
int _day;
};

ostream& operator<<(ostream& out, const Date& obj)
{
out << obj._year << "-" << obj._month << "-" << obj._day << endl;

return out;
}

int main()
{
Date d1;
cout << d1; // 运算符方式调用, << 为运算符 cout为左操作数 d1为右操作数
operator<<(cout,d1); // 通过函数名方式调用,operator<< 为函数名, cout、d1为形参

return 0;
}

4.3 不能重载的运算符

 C++运算符集合中,5种运算符不运行重载,为:.* 、:: 、?: 、sizeof 、.

4.4 重载++和- -

 两个运算符分为前置版本与后置版本,运算符重载是一种特殊的函数,函数名由关键字operator+运算符组成,那问题来了,operator++是重载的前置++还是后置++,operator--是重载的前置- -还是后置- -。为了解决这个问题,C++标准规定:当重载前置版本的++或- -时,函数形参为空,当重载后置版本的++或- -时,函数需要定义一个int类型的形参作为标志进行区分

说明:对于自定义类型,尤其是对象所占内存空间比较大时,前置版本比后置版本效率高很多

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
99
100
101
102
103
104
105
106
class Date
{
private:
int _year;
int _month;
int _day;

public:

Date& operator+=(int day); // 日期+=天数

Date& operator++(); // ++日期

Date operator++(int); // 日期++ ,后置++为了跟前置++区分,增加一下int类型参数占位,构成函数重载

Date& operator-=(int day); // 日期-=天数

Date& operator--(); // --日期

Date operator--(int); // 日期-- ,后置--为了跟前置--区分,增加一下int类型参数占位,构成函数重载

};

Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}

_day += day;
while (_day > GetMonthDay(_year, _month)) // 当day大于该月天数时
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}

return *this;
}


Date& Date::operator++()
{
*this += 1;
return *this;
}

Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}

Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}

_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}

return *this;
}

Date& Date::operator--()
{
*this -= 1;

return *this;
}

Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;

return tmp;
}

int main()
{
Date d1;
++d1; // 前置++
d1++; // 后置++
d1.operator++(); // 通过函数名方式调用,没有传入实参,调用前置++
d1.operator++(1); // 通过函数名方式调用,传入int类型实参,调用后置++

return 0;
}

4.5 赋值运算符重载

对已存在的类对象进行赋值拷贝时,会调用赋值运算符重载。赋值运算符重载不显式定义时由编译器自动生成,当显式定义赋值运算符重载,编译器不生成。

注意:赋值运算符只能重载为类的成员函数,而不能将其重载为类的非成员函数。因为当重载为类的非成员函数时并不属于该类,编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。为了避免这样的二义性,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
class Date
{
private:
int _year;
int _month;
int _day;

public:
Date operator=(const Date& obj);

};

Date Date::operator=(const Date& obj)
{
_year = obj._year;
_month = obj._month;
_day = obj._day;

return *this;
}

int main()
{
Date d1;
Date d2;
d2 = d1; // d2对象已经存在,调用赋值运算符重载将d1的值拷贝给d2

return 0;
}

4.5.1 编译器生成的赋值运算符重载功能

 在类中不显式定义赋值运算符重载,编译器自动生成赋值运算符重载。编译器生成的赋值运算符重载对于对象中内置类型成员变量按字节拷贝(浅拷贝),如果成员变量为自定义类型则调用它的赋值运算符重载

内置类型:C++语法已经定义好的类型,例如 int、char、double等
自定义类型:用class、struct、union关键字自己定义的类型

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
class B
{
private:
int _x;
public:
B operator=(B& obj)
{
_x = obj._x;
cout << "类B的赋值运算符重载" << endl;

return *this;
}
};

class A
{
private:
int _y;
B b;
public:
A(int y = 10)
{
_y = y;
}
};

int main()
{
A a(10);
A b;
b = a;
}

输出

1
类B的赋值运算符重载

4.6 取地址运算符重载与const 取地址运算符重载

取地址运算符重载与const 取地址运算符重载不显式定义时由编译器自动生成,当显式定义时,编译器不会自动生成。只有需要对取地址做特殊处理时才显式定义,一般用编译器自动生成的即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Date
{
private:
int _year;
int _month;
int _day;

public:
Date* operator&();

const Date* operator&()const;

};

Date* Date::operator&()
{
return this;
}

const Date* Date::operator&() const
{
return this;
}