上一篇写了类的定义方式及类与对象的关系,本篇主要讲解类与对象的6个默认成员函数。本篇中出现的没有提及的概念(重载。缺省参数等)改天再单独写一篇。
首先,先来了解一下,什么叫做默认成员函数呢?所谓的默认成员函数就是指在我们没有写的情况下,编译器会自动帮助我们生成的函数,拿上一篇提到过的空类来说,上一篇中提到空类是没有成员函数也没有成员变量的类。但是空类真的什么都没有吗?并不是,任何一个类在什么都不写的情况下,编译器会自动帮我们生成6个默认成员函数。那么6个默认成员函数都有哪些:1.构造函数;2.析构函数;3.拷贝构造函数;4.赋值重载;5.普通对象取地址重载;6.const对象取地址重载。(重点说前四个),
一、构造函数
感念:
创建类类型对象时由编译器自动调用,以保证每一个成员变量都有一个合适的初始值,他在整个对象的生命周期只会出现一次,它完成的是类的初始化工作。
特征:
1.函数名与类名相同;
2.无返回值;
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
例如:定义了如下一个日期类,并创建了Date类的实例化对象D1;
对比写了构造函数与没有写构造函数的区别,当写了构造函数时,在实例化对象时,编译器会去调用相应的构造函数进行初始化。 当没有写构造函数时,实例化出来的对象会调用编译器自己生成的构造函数进行初始化。
而对与编译器自己生成的构造函数进行初始化时,对于内置类型(int、char、double等)编译器不做处理,仍为随机值。对于自定义类型会去调用自定义类型的构造函数进行初始化,如下所示
对于构造函数,可以对其进行简化,将有参和无参构造函数进行合并,构成全缺省的构造函数:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
所谓全缺省构造函数,就是在形参后给缺省值,在创建对象时,如果传参数,则进行有参构造初始化,如果没有传参数,则会将缺省值作为该对象的初始值。
需要注意的是,默认构造函数只能存在一个,即全缺省构造函数、编译器自动生成的构造函数与有参构造函数和无参构造函数这三种方式只能存在一种。
二、析构函数
概念:
析构函数的功能与公仔函数相反,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。一般是在堆上new开辟出来的空间。
特征:
1.析构函数是在类名前加字符‘~’;
2.无参数、无返回值;
3.一个类有且只有一个析构函数,若没有定义,编译器会自动默认生成析构函数;
4.对象声明周期结束时,编译器自动调用析构函数。
为了方便了解析构函数,这里定义一个简单stack的类,如下:
对比两张图,第一张可以看到创建的S1与S2对象的确创建成功,上面说过析构函数在程序结束时自动调用,第二张图可以看出析构函数的确也成功调用。
那么,对于以上两个对象S1与S2构造与析构的顺序是怎样的呢?
我们知道,局部变量存放在内存中的栈区,而栈的特点则是先进后出,因此在构造时,先构造S1,再构造S2,析构时,先析构S2,再析构S1.
三、拷贝构造函数
概念:
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用。在创建对象时,可以创建一个与已存在对象的一个一模一样的新对象。简单说就是用已经初始化过的对象进行初始化工作。
特征:
1.拷贝构造函数是构造函数的一个重载形式;
2.拷贝构造函数有且只有一个参数,且必须使用引用传参,使用传值会引发无穷递归调用。
如下拷贝构造函数:
由上图可以看到,对象S2是由对象S1拷贝构造进行初始化的。在理解拷贝构造函数时,可以这样进行理解。Date D2(D1);可以理解成 D2.Date(D1);D2传给了this指针,D1传给了s,D1的类型是Date类型,因此传参也用Date类型接收。
需要注意的一点是,如果我们没有定义拷贝构造函数,编译器会自动帮助生成。编译器自动生成的拷贝构造函数,是一个浅拷贝(也叫做值拷贝),他会造成同一块空间在析构时被释放两次,程序会崩溃。对于深浅拷贝的问题,在后续再进行介绍。
四、运算符重载函数
概念:
运算符重载是C++的重要组成部分,就是给已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时产生不同的行为。
特征:
1.函数名字为:关键字 operator + 需要重载的运算符符号。
2.部分运算符不可被重载,如:成员访问运算符 '.', 成员指针访问运算符' .*', 域云算符' ::', 字 符串长度运算符'sizeof ', 条件运算符 ' ?:', 预处理符号' #'。以上一运算符不可被重载
运算符的重载首先来了解一下赋值运算符的重载,例如:
class Date
{
public:
//重载赋值运算符
//这里需要用引用传返回值,简单理解就是如果不用&,这里就是一份临时拷贝,无法达到
//改变d3的作用
Date& operator(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
//为了简便,构造函数与拷贝函数就不写了
Date d1(2024, 4, 1);
Date d2(d1);
Date d3(2024, 3,31);
d3 = d1; //对于这里的理解:编译器会将这段代码编译成d3.operator=(d1);
return 0;
}
如上,写了一个Date类,d1是调用构造函数进行初始化,d2是通过d1拷贝构造出来的,我们先来理解一下拷贝构造与赋值重载的区别,拷贝构造是通过已经存在的对象初始化出新对象的实例,这里已经存在的对象是d1,而d2则是对象d1进行拷贝构造函数初始化出来的新对象实例,对于赋值重载,两个对象均已存在,如上d3已经存在,通过赋值重载将d1成员变量的值 赋值给d3的成员变量。在理解运算符重载函数时,我们需要将 operator与重载的运算符当成一个整体看做时函数名。
需要注意的是对于赋值运算的重载,如果我们不进行定义,编译器也会自动默认生成,但是与拷贝构造函数一样,同样存在浅拷贝问题,在后面涉及内存相关时在进行解释。
为了方便理解,我们再举例说明运算符的重载。如下:
{
public:
//重载赋值运算符
//这里需要用引用传返回值,简单理解就是如果不用&,这里就是一份临时拷贝,无法达到
//改变d3的作用
Date& operator(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//重载 operator==
//可以把一下函数看成bool operator==(const Date* this,const Date& d)
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
//为了简便,构造函数与拷贝函数就不写了
Date d1(2024, 4, 1);
Date d2(d1);
Date d3(2024, 3,31);
d3 = d1; //对于这里的理解:编译器会将这段代码编译成d3.operator=(d1);
d2==d1; //便于理解:可以看成是operator==(&d2,d1),d2传参给类里的隐含的this指针
//d1传给d。函数名是operator== 经过编译器编译之后d2.operator==(d1);
return 0;
}
五、 普通对象取地址重载与const对象取地址重载
对于剩余的两个默认成员函数,由于这两个默认成员函数几乎不会需要自己写,就算自己不进行定义编译器也会自己生成。并且用到的场景几乎没有,因此这里就不过多赘述了。
总结
在本文中,初略讲解了类的6个默认成员函数,希望可以能够看到懂,里面涉及到的深浅拷贝问题在今后讲模拟实现string类的时候再进行讲解。