在C++的类中一共有六个默认函数,分别是:
构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载、const修饰的取地址操作符重载。
如果我们创建一个类,类里我们一个成员都不给,那么这里面什么都没有吗?实际上不是这样的,这个时候会自动生成以上所提到的六个默认成员函数,接下来我们将以一个日期类为例,对这六个默认函数依次进行介绍。
class Date{
public:
Date();//构造函数
~Date();//析构函数
Date(const Date& op);//拷贝构造函数
Date& operator=(const Date& op);//赋值运算符重载函数
Date* operator &();//取地址运算符重载
const Date* operator &() const;//const修饰的取地址运算符重载
};
关键字operator后面接需要重载的运算符符号,表示对该操作符进行重载写入,以支持class类的相关操作。
构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。它的主要作用就是初始化成员变量的内容。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象
构造函数的特性
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载,可以定义多个。
- 可以使用初始化列表,推介使用
- 不能显示调用构造函数
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且 默认构造函数只能有一个
- 编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数
- 构造函数的顺序 为全局对象先于局部对象进行构造,静态对象先于普通对象进行构造
例如
class Date {
public:
//方式一、无参构造
Date()
{
cout<<"Date()"<<endl;
}
//方式二、有参构造
Date(int year,int month,int day)//构造函数
{
_year = year;
_month = month;
_day = day;
//调用时可以使用 Date dat(2020,3,12);
}
//方式三、支持重载
Date(int year,int month,int day,int n)
{
_year = year;
_month = month;
_day = day;
cout<< n <<endl;
}
//方式四、全缺省
Date(int year=2000,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
//方式五、初始化列表
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
//方式六,静态成员一定要在类外进行初始化
private:
static int day_count;
private:
int _year;
int _month;
int _day;
};
int Date::day_count=0;
//测试函数
void test()
{
Date dat();//调用方式错误不能正常调用无参构造函数,相当于声明一个函数
Date dat1;//调用无参构造
Date dat2(2020,3,12);//调用有参构造
Date dat3(2020,3,12,1314);//调用重载的构造函数
Date dat4;
dat4.Date(2020,3,12);//不能进行显示调用,会报错
Date dat6();//全缺省构造函数,不能与其他默认成员函数同时出现
}
}
内含相关补充内容
成员类型
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,
默认成员函数
无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
初始化列表的使用
1.初始化列表是以一个冒号开始,接着以逗号分隔的数据成员列表,每个成员的后面使用的是括号括起来的初始化式.
2.以下情况必须使用带初始化列表的构造函数:
(1)自定义类型成员,即没有默认构造成员函数的成员类对象
(2)const成员
(3)引用类型的成员
3.在对初始化列表使用时,初始化的顺序是按照成员变量在类中声明次序,与其在初始化列表中的先后顺序无关
4.可以设置缺省值
explicit关键字
在一般的构造函数中,如果仅仅只有一个成员变量进行了初始化,那么在初始化是会发生类型的隐式转换,什么意思,例如以下示例中,对象的右边是一个整形的值,但是实际上在编译器编译过程中,会用该整形值构建一个匿名对象,然后使用匿名对象对左边的对象进行赋值
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2000);
d1 = 2019;//在此时也是成立的,由于进行了隐式转换,回将2019构建成一个匿名对象,之后对d1进行赋值。
}
使用explicit关键字,将会禁止但参构造函数的隐式转换
explicit Date(int year)//禁止单参转化
:_year(year)
{}
静态成员变量
1.静态成员变量为所有类对象共享,不属于某一个实例;
2.成员变量必须在类外定义,定义时不添加static关键字
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值
6.静态成员函数不可以调用非静态成员函数
7.非静态成员函数可以调用类的静态成员函数
析构函数
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数的特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数
- 默认析构函数只清理成员变量,若产生的对象实例中不仅有成员变量还有成员变量引入的资源,也许成员变量仅仅只是保存了指向该资源的指针(例如动态开辟的空间,不在对象中存放),当需要消除资源时需要重写析构函数,不然会产生内存泄漏
- 类的析构函数调用完全按照构造函数调用的相反顺序进行调用
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));//动态开辟空间,不在对象中
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private ://类的大小为12个字节,不会因为动态开辟而改变
int* _pData ;//开辟堆上的空间,指针指向该空间
size_t _size;
size_t _capacity;
};
拷贝构造函数
其本质上就是一个构造函数,只是其初始化的值不再是类的成员变量,而是将当前的对象完全复制于目标的对象。即只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数的特性
- 可以将其理解为是构造函数的重载形式,只是原来的形参是成员变量,现在的形参是当前类对象的引用。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
- 若有资源的创建则需要进行显示定义拷贝构造函数完成深拷贝。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
Date(const Date& dt)//使用引用传对象的地址,只进行成员变量的访问,不会造成死循环调用
{
_year=dt._year;
_month=dt._month;
_day=dt._day;
}
private:
int _year;
int _month:
int _day;
};
若进行传值方式,将会产生无穷的调用陷入死循环
赋值运算符重载函数
在上文开头部分我们简单提了一嘴运算符重载的关键字 operator,其用处在于重载原有一般操作符的意义,使其成为可以满足不同类对象使用的升级版操作符。这里我们只讲赋值运算符的重载,operator 的具体用法以及实现实例我们将在今后的博客中继续分享。
函数形式:
返回值类型 operator 操作符(参数列表)
运算符重载函数特性
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载
- 参数类型,参数类型因为 const 类类型名 & 加对象名
- 返回值 返回的应该为 类类型名+& ,这样可以支持连续赋值
- 检查是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。这里只能是浅拷贝,即当前类中只有成员变量,没有资源。
- 如果不显示定义,成员属性包含了自定义类型,则编译器会自动调用自定义函数的默认成员函数。
Date& operator=(const Date& dt)//其中隐含了Date *this指针参数,用于接收符号左边的参数
{
if(this != &d)/判断是否是同一个对象,若不是同一个对象则进行赋值
{
_year=dt._year;
_month=dt._month;
_day=dt._day;
}
return *this; //返回对象本身作为引用接收
}
使用时有两种写法:
Date d1(2010,2,3);
Date d2(2020,3,13);
Date d3(2000,2,2);
d1=d2=d3;//简写
d1.operator=(d2.operator=(d3));//完整写法
取地址操作符重载及const修饰的取地址操作符重载
这两个默认成员函数一般并不再进行重定义,编译器默认生成,只有特殊的情况,比如想让别人知道指定的内容的时候才需要进行重载。
Date* operator&()//此时可以修改成员变量的值,但是不能改变指针的方向
{
return this ;
}
const Date* operator&()const
{
return this ;
}
【注意】
- const只修饰成员函数
- operator&() 其中this的格式为:Date* const this 表示这个指针的指向不能更改
- operator&()const , 其中this的格式为:const Date* const this ,此时既不能修改指针的指向也不能修改指针指向的内容
- 读写的权限不能被放大,但是可以缩小
- const对象可以调用非const成员函数吗?答:不可以,因为const对象表示其内容不能改变,而非const成员函数表示对象内容可以修改则会产生冲突。
- 非const对象可以调用const成员函数吗? 答:可以,大的权限可以兼容小的权限
- const成员函数内可以调用其它的非const成员函数吗?答,不可以,const成员函数中的this是不可修改的,而传递到非const中this可以读亦可以写,权限放大不允许。
- 非const成员函数内可以调用其它的const成员函数吗?答:可以,权限缩小
欢迎大家批评指正,共同进步。