这篇文章详细介绍了类的 6 个默认成员函数,它们是构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址和 const 取地址操作符重载以及const 成员函数。这些成员函数在 C++ 中是默认生成的,默认成员函数在类的设计和实现中起着非常重要的作用,下面我们来一一介绍。
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,在 C++ 中,即使一个类中看起来什么都没有,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
例如有如下一个类:
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2024, 2, 5);
d2.Init(2024, 2, 6);
d1.Print();
d2.Print();
return 0;
}
对于上面的Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
在C++中为了让对象在实例化的时候能够完成初始化,提供了构造函数。构造函数是一种特殊的成员函数,用于在创建对象时对其进行初始化。在 C++ 中,构造函数的名称与类名相同,不返回任何值,甚至没有 void 类型的返回值。构造函数的主要作用是初始化对象的数据成员,确保对象在创建时有 一个合适的初始值。创建类类型对象时由编译器自动调用,并且在对象整个生命周期内只调用一次。
例如有如下一个类:
#include
using namespace std;
class Stack
{
public:
void Init(int capacity = 4)
{
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
_nums = tmp;
_capacity = capacity;
_top = 0;
}
void Destroy()
{
if (_nums)
{
free(_nums);
_nums = nullptr;
_capacity = _top = 0;
}
}
void push(int x)
{
//检查扩容
//...
_nums[_top++] = x;
}
private:
int* _nums;
int _top;
int _capacity;
};
int main()
{
Stack st;
st.Init();
st.push(1);
st.push(1);
st.push(1);
st.Destroy();
return 0;
}
对于上面的Stack类,可以通过Destroy公有方法清理对象中的资源(动态申请的空间),但如果每次使用完对象时都调用该方法来主动的释放资源,未免有点麻烦,那能否在对象销毁之前,就自动调用相关函数来清理对象中的资源呢?
在C++中,可以通过析构函数来实现在对象销毁之前自动清理对象中的资源。析构函数是特殊的成员函数,该函数与构造函数功能相反,完成的不是对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会编译器会自动调用析构函数,完成对象中资源的清理工作。
析构函数名是在类名前加上字符 ~。
无参数无返回值类型。
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器
生成的默认析构函数,对自定类型成员调用它的析构函数。
#include
using namespace std;
class B
{
public:
B(int b = 0)
{
_b = b;
cout << "B(int b = 0)" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b;
};
class A
{
public:
private:
int _a;
B _bb;
};
int main()
{
A a;
return 0;
}
上面程序运行结束后输出:~B()。
而在main方法中根本没有直接创建B类的对象,为什么最后会调用B类的析构函数?
因为:main函数中创建了A类的对象a,而a中包含2个成员变量,其中_a是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_bb是B类对象,所以在a销毁时,要将其内部包含的B类的_bb对象销毁,所以要调用B类的析构函数。但是:main函数中不能直接调用B类的析构函数,实际要释放的是A类对象,所以编译器会调用A类的析构函
数,而A没有显式提供,则编译器会给A类生成一个默认的析构函数,目的是在其内部调用B类的析构函数,即当A对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用B类析构函数,而是显式调用编译器为A类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数。
拷贝构造函数是C++中的一种特殊成员函数,它通常用于在对象创建时,使用一个现有对象的内容来初始化新对象,从而实现对象的拷贝。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
语法形式如下:
class Date
{
public:
//默认构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数,使用同类类型的对象初始化
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 2, 6);
Date d2(d1);//使用 d1 拷贝构造 d2
return 0;
}
拷贝构造函数是构造函数的一个重载形式。
需要注意的是:显示的写了拷贝构造以后,编译器就不会生成拷贝构造函数了,同时默认构造函数,编译器不会生成了,因为拷贝构造函数也是构造函数。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
当然像上面这种类是没必要的。那么下面的类呢?
#include
using namespace std;
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
int* tmp = (int*)malloc(sizeof(int) * capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
_nums = tmp;
_capacity = capacity;
_top = 0;
}
//析构函数
~Stack()
{
cout << "~Stack()" << endl;
if (_nums)
{
free(_nums);
_nums = nullptr;
_capacity = _top = 0;
}
}
private:
int* _nums;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);//st1拷贝构造st2
return 0;
}
当我们运行上面的程序时,发现代码崩溃,下面我们来分析一下,为什么代码会崩溃呢?
因此我们得出结论:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
现有如下一个Date类:
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);//使用 d1 拷贝构造 d2
Date d3 = d2;//使用 d2 拷贝构造 d3
return 0;
}
void func(Date d)
{
cout << "void func(Date d)" << endl;
}
int main()
{
Date d1;
func(d1);
return 0;
}
运行结果如下:
3. 函数返回值类型为类类型对象
结论:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。有了运算符重载以后,可以根据需求使得自定义类型的对象也可以像内置类型一样使用操作符。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)。
关于运算符重载,有一下几点需要注意:
举个例子:比如想比较两个日期是否相等。
之前我们想比较两个日期的大小需要写一个比较大小的函数,现在有了运算符重载,我们可以重载运算符 ’ ==’ 使得Date类的对象可以像自定义类型一样使用运算符’ ==’ 来判断两个日期是否相等。
#include
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//private:
int _year;
int _month;
int _day;
};
//运算符 == 重载,重载成全局的
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2024, 2, 6);
Date d2(2024, 2, 7);
cout << (d1 == d2) << endl;//这里d1 和 d2可以像内置类型一样使用操作符
return 0;
}
代码运行结果:
上面将运算符重载成全局的有个很大的问题(上面能运行是因为将成员变量的访问限定符改成了public),就是运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
这里其实可以用我们后面学习的友元来解决,或者干脆重载成成员函数。
因此正确的写法为:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 运算符 == 重载,重载成成员函数
// bool operator==(Date* this, const Date& d)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
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, 2, 6);
Date d2(2024, 2, 7);
cout << (d1 == d2) << endl;//这里d1 和 d2可以像内置类型一样使用操作符
//上面那样写是为了提高代码的可读性,我们也可以像下面这样显示的调用
cout << (d1.operator==(d2)) << endl;//bool operator==(Date* this, const Date& d2)
return 0;
}
总结:运算符函数可以定义为类的成员函数或全局函数。如果运算符函数是类的成员函数,它将自动获得一个隐含的 this 指针,用于访问调用对象的成员;如果是全局函数,则需要在参数列表中显式地传递所有操作数。
//T是类型名
T& operator=(const T& 变量名)
{
// 执行赋值操作
// 返回 *this
}
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。像前面我们介绍拷贝构造函数那里的Stack类一样,我们发现程序就会崩溃掉。
原因如下:
总结:如果类中未涉及到资源管理(动态申请空间),赋值运算符是否实现都可以;一旦涉及到资源管理赋值运算符则必须要实现。
使用const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。并且在函数声明和定义中都需要加上 const 关键字。
语法如下:
返回类型 函数名() const
{
// 函数体
}
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!