前言:
类和对象中篇,这里讲到的前4个默认成员函数,是类和对象中的重难点,许多资料上的讲法都非常抽象,难以理解,所以我作出这篇总结,分享学习经验,以便日后复习。
目录
6个默认成员函数:
构造函数:
1.概念:
2.用法:
3.特性:
析构函数:
1.概念:
2.用法:
3.特性:
4.析构调用顺序练习:
拷贝构造函数:
1.概念:
2.用法:
3.特性:
运算符重载:
用法:
赋值重载:
前置++和后置++重载:
取地址重载(不重要):
const成员函数:
6个默认成员函数:
什么是默认成员函数?
默认成员函数就是你不写编译器自己会自动生成的成员函数
前四个默认成员函数较为重要,后两个很少会自己实现,除非常特殊的情况下,基本上不会自己实现。
构造函数:
1.概念:
想必大家再用c语言实现栈,链表等数据结构的时候,都会先写一个初始化函数,来初始化我们的数据,但在使用中有时往往会忘记初始化,所以C++就产生了构造函数。
构造函数的作用就是进行初始化
2.用法:
那构造函数具体是怎样来使用的呢?举一个简单的案例,一个日期类的构造函数如下:
3.特性:
构造函数的函数名和类名相同。
无返回值。
对象实例化时编译器自动调用对应的构造函数。
构造函数支持重载。
建议写成全缺省。
一个类必须要有默认构造函数,如果你没有写,编译器会自己生成默认构造函数,但编译器默认生成的构造函数对内置类型不作处理,自定义类型去调用它的默认构造函数。但在C++11中委员会可能认为对内置类型不作处理显得很呆,所以对这个语法打补丁,支持在声明处给缺省值:
这样编译器默认生成的构造函数就会对内置类型进行处理。
但大多数情况下,构造函数都是需要我们自己去实现的。
无参构造函数,全缺省构造函数,编译器默认生成的构造函数,都可以称为默认构造函数,但一个类默认构造函数只能有一个,建议写成全缺省,避免歧义。
析构函数:
1.概念:
如果说构造函数的功能时初始化,那么析构函数就像它的死对头:
析构函数负责清理资源的工作,防止内存泄漏。
还是一样,我们在使用栈,链表等数据结构时最容易忘的就是用完后忘记清理空间,这将导致严重的后果,也就是内存泄漏,而C++中的析构函数可以有效解决这个问题。
2.用法:
由于我这个日期类不涉及动态资源,所以析构函数不必自己实现,但仍会调用:
3.特性:
函数名 = ~类名
无返回值
一个类只能有一个析构函数,如果不写编译器自己生成
析构函数不能重载
对象生命周期结束时,编译器自动调用析构函数
对于编译器自己生成的析构函数,同样是内置类型不作处理,自定义类型调用它自己的析构函数。
4.析构调用顺序练习:
对于析构函数,其实头疼的是它的多个函数析构时的调用顺序,这里给大家一个公式,大家以后套公式即可:
局部对象(后定义先析构)->局部静态(后定义先析构)->全局对象(后定义先析构)
下面由浅入深来练习一下:
练习1:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
cout << "调用析构" << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1);
Date d2(2);
Date d3(3);
return 0;
}
因为他们都是局部变量,遵循后定义先析构,所以:
练习2:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
cout << "调用析构" << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
Date d1(1);
Date d2(2);
Date d3(3);
int main()
{
//Date d1(1);
//Date d2(2);
//Date d3(3);
return 0;
}
现在他们都变为全局变量,仍遵循后定义先析构:
练习3:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
cout << "调用析构" << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1);
static Date d2(2);
static Date d3(3);
Date d4(4);
return 0;
}
现在,d2变成局部静态,根据公式,先局部对象再局部静态,内部仍然按照后定义先析构:
练习4:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
cout << "调用析构" << _year << endl;
}
private:
int _year;
int _month;
int _day;
};
Date d5(5);
static Date d6(6);
Date d7(7);
static Date d8(8);
int main()
{
Date d1(1);
static Date d2(2);
static Date d3(3);
Date d4(4);
return 0;
}
先局部对象,再局部静态,最后全局,不管全局对象是否为静态,都遵循后定义先析构:
拷贝构造函数:
1.概念:
在使用C++中,我们往往会需要将一个类拷贝到另一个相同类型的类中,而拷贝构造函数的作用就是:将该类拷贝到同类型的类中。
2.用法:
为了更严谨,也可以在此处加上const:
3.特性:
函数名和类名相同
无返回值
形参部分传引用
拷贝构造函数也是构造函数
如果我们不写,编译器会默认生成,默认生成的拷贝构造函数对内置类型成员按内存存储按字节序拷贝,也就是浅拷贝,对自定义成员调用它的拷贝构造。
为什么形参部分必须传引用呢?
因为不传引用可能会引发无穷递归,看下面这个例子:
此时像上图一样使用拷贝构造函数,如果我们的拷贝构造函数是传值:
那就需要先调用拷贝构造,调用到拷贝构造时,因为是传值,所以需要将d1先拷贝到形参d,而将d1拷贝到形参d,就又需要调用新的拷贝构造,而新的拷贝构造又是传值,所以就这样一直递归下去,无穷无尽。
编译器默认生成的拷贝构造会拷贝内置类型,那么是不是意味着我们不需要自己实现拷贝构造?
不!!!
编译器默认生成的拷贝构造只能进行浅拷贝。
当我们有一个栈,里面有一个指针,指向了一片空间,当我们还是浅拷贝,用编译器默认生成的拷贝构造函数的话,它就会原原本本的将指针的拷贝到新的指针中,这就导致这篇空间有两个指针指向它,而不是像我们预想的一样,拷贝一块新空间,所以当要进行深拷贝时,我们需要自己来写拷贝构造函数
所以写拷贝构造时,浅拷贝不需要写,只有有动态资源开辟的,才需要自己写拷贝构造。
运算符重载:
关键字:operator
用法:
将函数名改成operator加需要重载的运算符
在c语言中,对于内置类型我们可以直接用< > = + - 等符号进行运算,但如果我们要对自定义类型进行运算的话,就需要自己写函数来实现。
如果用运算符重载的话,将大大提高代码的可读性,比如我们实现一个判断两个日期类是否相同,重载==:
重载成成员函数:
重载成全局函数:
用法:
显而易见,第三种方法最实用,大大提高了代码的可读性 。
赋值重载:
这里的内容涉及的运算符重载,建议先跳到运算符重载,再来学习这段。
区分拷贝和赋值:
众所周知自定义类型是不能直接用等号连接进行赋值的,那么就需要进行运算符重载;
赋值重载和拷贝构造类似,但赋值重载是支持连续赋值的:
注意赋值重载只能重载成成员函数。
前置++和后置++重载:
前置++和后置++这两个运算符一模一样,但是作用效果却不同,为了不产生歧义,该如何进行重载呢?
前置++:
用法:
后置++:
用法 :
可见为了和前置++区分开来,后置++强制增加了一个形参int 来区分。
取地址重载(不重要):
主要是对对象进行取地址操作,编译器自己生成的已经完全够用,没有必要再自己写,除非有特殊需求:
const成员函数:
如果我们希望某个成员函数不能对成员变量进行修改时,我们可以进行const修饰:
用法:
我们可以对不用对成员变量进行修改的成员函数进行const修饰,增加代码的严谨性。如果const修饰的成员函数对成员变量进行了修改,编译器会报错:
C++类和对象中篇到此结束,这篇是最难也是最重要的一篇,下篇我会陆续更新进行收尾。