作者介绍:
关于作者:东条希尔薇,一名喜欢编程的在校大学生
主攻方向:c++和linux
码云主页点我
本系列仓库直通车
作者CSDN主页地址
类的默认成员函数之构造函数
构造函数定义及其特性
如果我们这儿有一个日期类
class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1,d2;
d1.SetDate(2018,5,1);
d1.Display();
Date d2;
d2.SetDate(2018,7,1);
d2.Display();
return 0;
}
我们可以观察到,我们专门设置了一个初始化函数来对我们实例化出的对象进行赋值,但这种初始化函数不仅设置麻烦,而且我们在每次可能会忘记调用初始化函数,所以c++类中,为了我们的使用方便,提供了一个默认成员函数:构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数的表示方式如下:
Date();//没有返回值,函数名与类名必须相同
我们当然可以设计程序来测试一下构造函数的调用
class Date
{
public:
Date()
{
cout << "构造函数调用" << endl;//看这句话打印的时机
_year = 2022;
_month = 1;
_day = 15;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Display();
return 0;
}
输出结果如下:
可以看出,构造函数在对象实例化时就会被自动调用,然后将成员变量根据指定值来设置我们实例化出的对象
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数一般分为默认构造和含参构造
而默认构造又可分为无参构造和全缺省构造,含参构造分为半缺省构造和含参构造
使用示例
class Date
{
public:
Date()//无参构造
{
cout << "构造函数调用" << endl;
_year = 2022;
_month = 1;
_day = 15;
}
Date(int year,int month,int day)//含参构造
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
注意:无参和含参函数构成函数重载
使用
Date d1(2022,1,15);//调用含参构造
Date d2;//调用默认构造
Date d3();//注意,这个是一个返回Date类型,名为d3的函数声明,并不是函数的无参构造
构造函数的其它特性
在我们没有定义构造函数时,编译器会默认传一个默认构造函数,在用户定义了析构函数时,将不再生成默认构造函数
默认函数对内置类型(int,char)等变量不做处理
对自定义类型(class)会调用此自定义类型的构造函数
我们设计一个程序来验证一下
class A
{
public:
A()
{
cout << "A的析构函数" << endl;
}
};
class Date
{
public:
//不给构造函数
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
A a;
};
int main()
{
Date d1;
d1.Display();
return 0;
}
程序输出如下
第二行的随机值,表示了默认析构不对其做出处理
但如果A里面没有默认构造函数,而是含参构造函数,程序会报错
析构函数
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
而什么是需要资源呢?
其实就是我们在堆上开辟的空间
析构函数同样无返回值,且无参数,定义方式是~加上函数名
例如:~Date();
我们一般在什么场景使用析构函数呢?
当我们需要动态释放堆上空间的时候
class SeqList//顺序表类
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));//开辟在堆上的空间
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
同样,析构函数有和构造函数类似的特性
- 系统会默认生成,默认的析构函数对内置类型不处理,对自定义类型调用其析构函数
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
注意
因为变量进入栈区是按照先进先出的特性,所以后定义的变量会先会被析构
当某个对象的生命周期结束,将会自动调用其析构函数
拷贝构造入门
我们知道,我们在写作业的时候,如果写不出来的时候,我们就可以抄借鉴别人的作业,从而就有了两份一模一样的作业
那么在程序中我们能不能创建与某个对象一模一样的对象呢
就可以使用拷贝构造函数了
拷贝构造函数定义如下:
Date(const& date);
使用如下
Stu s1(...);
Stu s2(s1);
我们根据函数特征可以推断出,拷贝构造其实是构造函数的重载形式,参数类型必须是本类型的引用
那么,为什么要传引用呢
因为函数的传值拷贝要调用类的拷贝构造函数,如果我们传值的话,函数就会无限递归下去
而且,为了防止被复制的对象被意外修改,我们都要加一个const修饰我们的参数
与前面两个函数一样,若我们没有定义拷贝构造,系统将会自动生成一个拷贝构造函数
默认拷贝构造函数对内置类型做按字节拷贝(类似memcpy),对自定义类型调用其拷贝构造函数
注意,默认拷贝构造函数是执行浅拷贝
**需要深拷贝的例子:**动态开辟的资源
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
深浅拷贝问题我们在后面的章节中介绍
运算符重载
为了我们人的使用和理解方便,c++允许我们将内置的运算符号重载到我们的自定义类型中,对某个自定义类型,赋予符号计算方法方便我们对自定义类型进行运算,本质是一种特殊的函数
内置符号不会直接计算我们的自定义类型
不能重载非内置符号,比如@,$等
对于二目运算符,如果我们少传了一个参数,将会自动传入this指针
定义方式:返回类型+operator+重载符号+参数列表
例如:
bool operator>(const Date& d);
例如我们有一个日期类,我们要进行比较大小的操作
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
我们可以在类中进行这么定义
bool operator>(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}
在主函数中这两种使用方法等价
d1>d2;
d1.operator>(d2);
赋值运算符
我们怎么完成赋值的操作?
首先,如果我们要实现连续赋值,必须要返回Date&,&可减少拷贝
我们可以在函数体内返回*this
Date& operator=(const Date& d)
{
this->day=d.day;
this->month=d.month;
this->yeat=d.year;
return *this;
}
如果我们有时候写成了d=d(本身赋值怎么办?)可以不用进行操作
Date& operator=(const Date& d)
{
if(this!=&d)
{
this->day=d.day;
this->month=d.month;
this->yeat=d.year;
}
return *this;
}
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。