类和对象
类的六个默认成员函数
在我们构建一个类之后即使我们在其中不写任何的成员函数,在其中也会有6个默认编译器自动生成的成员函数,这些函数构成了类的基本功能包括初始化,销毁后的清理工作等。当然这些自动生成的成员函数功能有限有时候或许无法达到预期的效果,因此我们可以对其进行重载让其能够达到我们需要的功能。
1、构造函数
2、析构函数
3、拷贝构造函数
4、赋值运算符重载函数
5、对普通对象取地址运算符重载
6、对常对象取地址运算符重载
以上这6个函数中前四个我们都有极大可能性会对其重载因此需要重点学习,而后两个则很少需要自己实现。
构造函数
构造函数时在类刚刚实例化创建出对象时自动进行调用的,主要负责类的初始化。因此我们就要注意一点,我们在调用构造函数的时候只是对对象进行初始化并不为其分配空间,因此构造函数并不是在构造对象,而是在初始化对象。
特性
构造函数具有以下特性
1、与类名相同
2、实例化自动调用
3、无返回值
4、支持重载
默认构造函数
所有的无参或是全缺省的构造函数都可以看作是默认的构造函数,并不一定非要是默认生成的构造函数,因此默认构造函数大致分为三种。
1、自定义无参构造函数
2、自定义全缺省构造函数
3、自动生成无参构造函数
如果类中没有构造函数,则会默认生成一个无参构造函数,但是如果我们定义了构造函数则不会再默认生成。但是默认生成的无参构造函数不会进行成员变量的初始化,如果成员中有其他自定义类型的成员变量会调用它的构造函数。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 std::cout;
using std::endl;
using std::string;
class Student
{
private:
int _num;//学号
string _name;//姓名
int _classId;//班级编号
public:
//一个类中默认的构造函数只能有一个,否则会产生歧义
//默认构造函数(自动生成的构造函数与它相同)
//Student(){}
//构造函数重载
//全缺省构造函数,也可以视为默认构造函数
//如果有默认值的情况下我们最好在类中将默认构造函数定义为全缺省
Student(int num = 1, string name = "李狗蛋", int classId = 1)
{
_num = num;
_name = name;
_classId = classId;
}
void Print()
{
cout << "学号:" << _num << "\t姓名:" << _name << "\t班级:" << _classId << endl;
}
};
int main()
{
//如果使用无参构造则不需要加括号,否则会和函数声明产生歧义
Student student1;//默认构造,全部用默认值
Student student2(2, "张三", 2);
student1.Print();
student2.Print();
}
[misaki@localhost 第二章-类和对象]$ ./DefaultFun
学号:1 姓名:李狗蛋 班级:1
学号:2 姓名:张三 班级:2
这里需要注意的就是为了避免歧义一类中只能出现一个默认构造函数。同时自动生成的构造函数功能十分优先,不会进行成员变量初始化,所以我们往往需要重载。
析构函数
与构造函数向对立的就是析构函数,析构函数是在对象销毁时自动调用的,主要时为了帮助我们清理我们创建对象时分配的空间,清空指针之类的,析构函数在如果成员变量中没有指针或者没有动态分配内存亦或者没有需要关闭的文件的情况下往往时不需要自己进行定义的。
特性
1、定义在类名前加~
2、无参数无返回值,意味着析构函数无法重载,只能存在一个
3、有且只有一个析构函数,若未显式定义则会生成默认析构
4、对象销毁时自动调用析构函数
析构函数特性与构造十分类似,最为主要的就是一个类中只会唯一存在一个析构函数。
默认析构函数
析构函数和构造函数一样如果我们并未定义则会自动生成一个,同样自动生成的析构函数不会做任何处理,析构函数在处理自定义类型时同样也会自动调用其对应的析构函数。1
2//析构函数,默认生成的析构函数与它相同
~Student(){}
拷贝构造
拷贝构造是用来实例化与某一对象相同的对象的,他会将该对象的数据完全复制一份相同的出来,拷贝构造是构造函数的一个重载。
默认拷贝构造
同样如果我们并未显式定义拷贝构造则会自动生成一个,但是要注意默认生成的拷贝构造只能完成简单的浅拷贝,因此如果我们想要对复杂的如链表类进行拷贝需要自行定义。1
2
3
4
5
6
7//默认拷贝构造
Student(const Student& student)
{
_num = student._num;
_name = student._name;
_classId = student._classId;
}
注意
拷贝构造在很多情况下都会调用,比如说我们将对象传给一个函数时,由于副本传参会复制一份副本进行传入,这个时候就已经调用了拷贝构造。那么我们在调用拷贝构造的时候就会将这个对象当作参数传入,如果我们这个时候调用了拷贝构造就是在拷贝构造中调用了拷贝构造会发生无限递归的情况,这该怎么解决呢?——传引用即可。因此拷贝构造的参数必定是本类对象的引用,不然就会报错。1
2
3
4
5
6
7
8
9
10
11
12//拷贝构造
Student(Student student)
{
_num = student._num;
_name = student._name;
_classId = student._classId;
}
报错:
DefaultFun.cpp:25:26: 错误:无效的构造函数:您要的可能是‘Student (const Student&)’
Student(Student student)
^
在编写函数的时候建议多用引用类型,尤其是在面对占用空间大的自定义类型时,使用引用会提高效率,不会发生拷贝构造,如果我们并不希望修改传入的参数,在前面加上const
来避免修改,这是很好的代码风格。这里极度推荐一定要传入常引用,不然可能会引发赋值运算符重载连续赋值时调用拷贝构造传参产生常引用参数类型不服无法调用的情况。
赋值运算符重载
赋值运算符重载是类似于拷贝构造的默认构造函数,不过是将拷贝构造通过=
来使用,不过我们得先了解运算符重载。
运算符重载
在C++中支持对运算符进行重载,使得给我们带来的很大的遍历,由此我们可以通过预算符来指定操作操作我们的自定义类型。
重载规则
1、不能创建新的操作符
2、必须有一个自定义类型或枚举类型操作数。
3、内置类型操作符不能改变其含义
4、在类内定义时会多一个默认形参this
指针
5、.*
、::
、sizeof
、.
、?:
这五个操作符不能重载。
类外重载
在类外进行操作符重载定义的成为类外重载,往往有两个参数,其中要求至少有一个是自定义类型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15bool operator==(const Student& stu1, const Student& stu2)
{
return stu1._num == stu2._num && stu1._name == stu2._name && stu1._classId == stu2._classId;
}
int main()
{
//如果使用无参构造则不需要加括号,否则会和函数声明产生歧义
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2(2, "张三", 2);
cout << (student1 == student2) << endl;
//实际上会转换为operator==(student1, student2);
}
[misaki@localhost 第二章-类和对象]$ ./DefaultFun
1
但是类外构造有一个致命的缺点,就是在类外我们往往无法访问到类内的很多成员变量,为了实现重载将其改为公有有点以小失大,因此就有了类内构造。
类内重载
类内重载则能解决成员在类外无法调用的问题。定义的类则自动被视为左操作数,参数则为右操作数。不过要注意类内重载会默认有一个形参this
。1
2
3
4
5
6
7
8
9
10
11
12
13//类内重载
bool operator==(const Student& stu2)
{
return _num == stu2._num && _name == stu2._name && _classId == stu2._classId;
}
int main()
{
//如果使用无参构造则不需要加括号,否则会和函数声明产生歧义
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2(2, "张三", 2);
cout << (student1 == student2) << endl;
//实际调用:student1.operator==(&student1, student2);
}
赋值运算符重载
赋值运算符重载不过是将拷贝构造的内容通过赋值运算符重载来表示,这样更加直观,同样的如果我们未显式定义则会默认生成一个。同时这个运算符重载是有返回值的,是为了用于实现连续赋值。这里推荐一定传入引用类型的参数,是为了防止连续赋值时调用拷贝构造出现问题,同时返回值也要返回引用类型,防止调用拷贝构造影响效率。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//赋值运算符重载,自动生成的重载与他相同
Student& operator=(const Student& stu)
{
_num = stu._num;
_name = stu._name;
_classId = stu._classId;
return *this;
}
int main()
{
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2 = student1;
student2.Print();
}
[misaki@localhost 第二章-类和对象]$ ./DefaultFun
学号:2 姓名:张三 班级:2
自动生成的运算符重载也只能完成浅拷贝。
const成员
我们有时会定义常对象,即不可改变的对象,这种对象在调用成员函数如果调用的普通成员函数会出现问题。1
2
3
4
5
6
7
8
9
10
11
12
13int main()
{
const Student student1(2, "张三", 2);
student1.Print();
}
[misaki@localhost 第二章-类和对象]$ make
g++ -g DefaultFun.cpp -o DefaultFun
DefaultFun.cpp: 在函数‘int main()’中:
DefaultFun.cpp:61:18: 错误:将‘const Student’作为‘void Student::Print()’的‘this’实参时丢弃了类型限定 [-fpermissive]
student1.Print();
^
make: *** [DefaultFun] 错误 1
这里主要问题所在就在于调用函数由于我们的对象是const
,那么对象本身不可改,在传给隐式this
的时候这里的this
的类型也应该为const *
,否则就会出现这样的问题。那么我们怎么样解决呢?这里就要用到常成员函数。
使用
1 | void Print() const |
在成员函数后加const
将其变为常成员,常成员会将函数的形参this
类型改为const *
,这样在常成员函数中则不可再修改调用对象的数据。
注意
1、普通对象可以调用常成员函数。
2、常对象不可以调用普通成员函数。
3、建议将一切不会修改对象数据的成员函数都写为常成员函数。
4、常对象只能初始化,不能进行修改。
取地址及const取地址操作符取地址
这两个默认成员函数是最后的两个会默认生成的成员函数,一般情况下不需要我们显式定义,除非我们想让别人通过取地址符获得指定的数据。1
2
3
4
5
6
7
8
9
10//默认生成的取地址及const取地址操作符如下
Student* operator&()
{
return this ;
}
const Student* operator&() const
{
return this ;
}