类和对象(中)
前言:
在学习这一章之前,先得对类和对象的一些基础知识进行掌握,具体可以看看我主页中的类和对象(上)
。
在引入这一章之前,我们要知道空类,空类是什么呢?
空类是一个类中什么成员都没有。
那空类真的什么都没有吗?
不是的,任何一个类在我们不写的情况,都会生成6个默认成员函数
下面就对6个默认成员函数
一、构造函数
特殊成员函数
构造函数主要用于对象的初始化
自己Init
的缺陷,可能会忘记初始化
C++为了解决这个问题,引入构造函数
其名虽叫构造,但其主要任务并不是开空间创建对象,而是初始化对象
1.特征
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器
自动
调用对应的函数–保证对象一定会初始化 - 构造函数可以重载 --你可以有多种初始化方式
问:无返回值是void类型吗?
void 是有返回值的,是不过为空。
如:
class Date
{
public:
Date(int year = 0,int month = 0,int day = 0)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
上面这个Date
函数,即为类Date
的构造函数,不用写返回类型,写参数即可。
上面这个函数即为全缺省的函数,可以处理没有参数的情况,默认值初始化。也可处理有参数的情况,给定值初始化。
注:
-
Date
和全缺省的Date
基本只能存在一个 -
我们不写,
编译器会生成一个构造函数
,我们写了,编译器将不再生成。所以构造函数是默认成员函数。但不会给你默认初始化,也就是说还是一堆混乱的值,没有初始化
2.为什么不默认初始化
-
内置类型
语言原生定义的类型,如:int,char,double等,还有指针。
编译器对内置类型不进行初始化
-
自定义类型
编译器会去调用它们默认构造函数去初始化。如:class\struct
前提是其中有自己写的构造函数,如:class中含有一个类,含有那个类如果有自写构造函数,就会初始化,没用就不会初始化
问:没用参数的构造函数怎么调用
int main()
{
Date d1;
Date d2();
}
//究竟是以第三行的形式调用还是第四行的形式调用
是用第三行的形式调用
因为如果我们用第四行的形式调用,编译器不好识别,是个函数声明,还是其他什么
总:
构造函数的细节很多,但是实际中我们用的构造函数是这样的。
大多数都要自己写构造函数,完成初始化,一般建议写一个全缺省的构造函数,能适应各种场景
二、析构函数
特殊成员函数
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器
完成的,而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
1.特征
- 析构函数名是在类名前面加上字符“~”
- 无参数无返回值
- 一个类有且只有一个析构函数,不能重载,若未显示定义,系统会自动生成默认的析构函数
- 对象生命周期结束时,C++编译系统会自动调用析构函数
问:析构函数有什么意义呢,它做了什么处理
不是什么事情都不做,自定义类型其要进行处理,调用其自定义类型中的析构函数,
内置类型不进行处理。
问:
class Stack
{
......
};
int main()
{
Stack st1;
Stack st2;
}
st2要先析构,
对象是定义在函数中,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合后进先出,st1先构造,后析构
问:什么时候需要自己写析构函数?
像类里面有自己申请内存空间的指针等一些其他什么东西,需要我们自己写析构函数,然后进行释放内存空间,将指针置为空等操作。
2.面试题
数据结构的栈和堆和我们讲的内存分段区域也有个栈和堆,它们之间有什么区别和联系
- 它们之间没有绝对的联系,因为他们两个属于两个学科的各自的一些命名
- 一个是数据结构,一个是一段内存分段
- 数据结构的栈和系统分段的栈(函数中的栈帧)中的对象都符合先进后出
三、拷贝构造函数 – 拷贝初始化
特殊成员函数
注意深浅拷贝问题
默认成员函数,我们不写编译器会自动生成拷贝构造
1.特征
- 拷贝构造函数是
构造函数的一个重载形式
- 拷贝构造函数的
参数只有一个
且必须使用引用传参
,使用传值方式会引发无穷递归调用
问:为什么传值会引发无穷调用
为什么是上面的样子呢?
当我们要传值给形参时,是需要创建然后拷贝过去的,这个时候需要调用拷贝构造。调用拷贝构造又需要传值给形参,传值给形参又需要拷贝构造,这样依次往下,就会引发无穷递归调用
所以我们运用引用传参,因为引用传参很好的解决了这个问题。因为形参就是实参的别名,也等于说就是实参
问:那么除了拷贝构造,其他函数还需要用引用作为形参吗?(我们依然用前面的日期类解释
void f1(Date d1)
{
}
void f2(Date d2)
{
}
- 我们调用f1,还需要调用拷贝构造,将我们实例化对象拷贝过去,而引用传参调用f2,d2是其实例化别名,直接运用即可。
引用传参大大提升效率
总:
从此以后注意,函数传参自定义类型的对象,一般推荐用引用传参,如果还想用传值传参,也不是不可以,每次都得调用拷贝构造
2.一些问题
如:一个日期拷贝函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
注:加上const的原因是,我们将无法修改const,只能拿它用作拷贝
很多同学肯定又有疑问了,为什么这里可以访问d
的私有变量。
因为访问限定符限定的是类外面的的访问,在同一个类里面,无论是哪个实例对象传进去,都可以访问
默认拷贝问题
默认生成的拷贝构造
对内置类型会完成浅拷贝(值拷贝)
,也就是说,按内存存储按字节序完成拷贝
对自定义类型会调用他的拷贝构造完成拷贝
那么对内置类型实现浅拷贝会出现什么问题?
- 当然会有问题,浅拷贝就相当于让两个东西指向同一块内存空间,如果是指针,两个指针指向同一块空间,调析构函数会把同一块空间free两次,这样就会出现错误,一次malloc只能free一次。
- 其中一个对象插入删除数据,都会导致另一个对象也插入删除数据
四、赋值运算符重载
1.运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
让自定义类型可以像内置类型意义使用运算符
具有返回值类型、函数名字以及参数列表 与普通函数类似
运算符重载跟函数重载没有关系
特性
-
不能和通过连接其他符号来创建新的操作符,如:operator@
-
重载操作符必须有一个类类型或者枚举类型的操作数
-
用于内置类型的操作数,其含义不能改变
如:内置类型整型+,其含义不能改变
-
作为类成员的重载函数时,其形参看起来比操作数目少1成员函数的操作符有一个默认形参this,限定为第一个 形参
-
.*,::,sizeof,?:,.五个操作符不能重载
如:
class Date
{
public:
bool operate==(Date& x)
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
//编译器会将其处理为:
bool operate==(Date* this,Date& x);
private:
int _year;
int _month;
int _day;
};
函数名字:
关键字operator后面接需要重载的运算符号
函数原型:
返回值类型operator操作符(参数列表)
2.赋值操作运算符
默认成员函数
,不写编译器会自动生成一个
如:标准书写
class Date
{
public:
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
为什么上面要加const
因为我们只是判断其是否相等,加上const,防止修改实例化对象
为什么要用引用
因为不加引用会使用拷贝构造,降低效率
为什么要用Date&作为返回值
前面用void会发生错误,当使用d1=d2=d3这种连续赋值时,d1将不会被赋值,加上Date将解决这个问题。
同时运用引用,避免其调用拷贝构造。同时这个时候函数结束,其作用域还在,可用引用做返回。
i = j = k;
//实际上是先把k赋值给j,然后返回j的值,继续把j赋值给i
总:
编译器默认生成赋值运算符跟拷贝构造的特性一致。
- 针对内置类型,会完成浅拷贝,像Date这样的类不需要我们自己写赋值运算符
- 针对自定义类型,我们会调用它的赋值运算符重载
区别拷贝构造和赋值重载
- 拷贝构造-----拿一个已经存在的对象去构造初始化另一个要创建的对象
- 赋值重载----两个已经存在的对象–》拷贝