下面将主要描述类中较为重要的成员函数,包括它们的书写形式、用法以及特点,并通过创建一个日期类来便于学习理解。
目录
一、构造函数
首先我们先来看下这样一段代码:
class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void ShowDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.SetDate(2022, 3, 22);
d.ShowDate();
return 0;
}
不难发现,上述Date类代码可以通过调用成员函数来设置成员变量的内容,这样不免会有些麻烦,但有了构造函数后我们就可以在对象实例化时就将类的成员变量内容设置好。
★ 特性及作用
构造函数是一个特殊的成员函数,在创建类类型对象时由编译器自动调用,其作用是初始化对象,并且在对象的生命周期内只能调用一次。
特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
如下面代码所示:皆满足上述特征。
class Date
{
public:
// 1.无参构造函数 (与带参构造构成重载)
Date()
{}
// 2.带参构造函数 (可设置缺省值)
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 调用无参
Date d2(2022, 3, 22); // 调用带参
return 0;
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。
C++中将类的类型分为:内置类型和自定义类型两类。
★ 编译器默认生成的构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
1、对于内置类型的成员变量不做初始化处理。
2、对于自定义类型的成员变量会去调用它的默认构造函数初始化,如果没有就会报错。
3、对于任何一个类的默认构造函数---不用参数就可以调用。
4、对于任何一个类的默认构造函数共有三个,分别为全缺省、无参及编译器默认生成的。
上述情况我们来验证:
情况一:
情况二:
可能你会有这样的疑问,编译器默认生成的构造函数对内置成员变量不操作,其仍是随机数。是的,这是编译器对内置类型的定义,确实是这样,但它对于自定义类型的成员变量会去调用其自己的构造函数。
情况三:
了解即可。
情况四:
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
无参构造函数、全缺省构造函数定义上可以同时存在,但当设定初始值实例化对象时会报错,因为无法确定调用两者中的哪一个。
★ 成员变量命名风格
为了对成员变量和形参加以区分,我们可以采用如下的命名风格:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
二、析构函数
★ 概念
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
★ 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。
3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
参考代码如下:
class Stack
{
public:
// 构造函数
Stack(int capacity = 10)
{
cout << "Stack()" << endl;
int* ptr = (int*)malloc(capacity * sizeof(int));
if (ptr == nullptr)
{
exit(-1);
}
_data = ptr;
_size = 0;
_capacity = capacity;
}
// 析构函数
~Stack()
{
cout << "~Stack()" << endl;
if (_data)
{
free(_data);
_data = nullptr;
_size = _capacity = 0;
}
}
private:
int* _data;
int _size;
int _capacity;
};
int main()
{
Stack s1;
return 0;
}
特征三、四:
三、拷贝构造函数
指的是用已有的 一个类,去实例化一个将要创建的类对象,两者具有相同的类模板。
★ 概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
★ 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
参考代码如下:
class Date
{
public:
// 构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
Date(const Date& s)
{
_year = s._year;
_month = s._month;
_day = s._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 3, 23);
Date d2(d1);
return 0;
}
代码调试结果:
如若拷贝构造函数未使用引用传参,而使用传值的形式,则会引发无穷递归
3.若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
上述的Date类代码,其成员变量在拷贝构造时不会出错,因为我们的本意只是取其值的大小,也就是浅拷贝。但试想如果是对动态开辟空间Stack的拷贝构造呢?我们会将两个栈指向同一块儿内存空间,这样肯定是不合理的。下面通过调试来验证下:
参考代码如下:
class Stack
{
public:
// 构造函数
Stack(int capacity = 10)
{
cout << "Stack()" << endl;
int* ptr = (int*)malloc(capacity * sizeof(int));
if (ptr == nullptr)
{
exit(-1);
}
_data = ptr;
_size = 0;
_capacity = capacity;
}
// 析构函数
~Stack()
{
cout << "~Stack()" << endl;
if (_data)
{
free(_data);
_data = nullptr;
_size = _capacity = 0;
}
}
private:
int* _data;
int _size;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);
return 0;
}
上述代码没有显示定义拷贝构造函数,会由编译器默认生成,调试:
不难看出,此时的拷贝构造函数已经完成了对s2的浅拷贝,此时的s1、s2指向了同一片内存空间。紧接着是调用析构函数对资源进行清理:
可以看到析构函数调用的顺序与实例化对象的顺序相反,此时s2先进行资源的清理,并没又报错。接下来是对s1进行资源清理:
此时程序报错,原因是同一片空间被进行的二次释放,s1、s2指向的是同一片空间。如图:
四、赋值运算符重载
★ 产生的由来
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
★ 书写形式
函数名字为:关键字operator后面接需要重载的运算符符号。
函数类型:返回值类型 operator操作符(参数列表)。
注意:
不能通过连接其他符号来创建新的操作符:比如operator@。
重载操作符必须有一个类类型或者枚举类型的操作数。
参考代码如下:
class Date
{
public:
// 构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 运算符重载
Date& operator=(const Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 3, 23);
Date d2 = d1;
return 0;
}
调试结果如下:
★ 赋值运算符要点
1. 参数类型
2. 返回值
3. 检测是否自己给自己赋值
4. 返回*this
5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。(浅拷贝)
由此产生思考:既然我们可以对赋值运算符重载,那么我们对于其他的运算符呢?答案是当然可以的,具体我们会通过一个日期类来实现。Date · 小夏/learn---C++ - 码云 - 开源中国 (gitee.com)https://gitee.com/xiao-xia_c/learn---c/tree/master/Date
五、const修饰类的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。下图为其书写形式以及等同的书写形式。
六、取地址及const取地址操作符重载
★ 说明
这两个运算符一般不需要重载(编译器会默认生成),使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
参考代码:
class Date
{
public :
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private :
int _year;
int _month;
int _day;
};
七、Date类的实现
请参考:Date · 小夏/learn---C++ - 码云 - 开源中国 (gitee.com)https://gitee.com/xiao-xia_c/learn---c/tree/master/Date
★★★★★ 感谢阅读!