类的默认成员函数
- 1.构造函数
- 特性
- 2.析构函数
- 特性
- 3.拷贝构造函数
- 特性
- 4.赋值重载函数
- 运算符重载
- 赋值运算符重载
- const成员函数
- 取地址运算符重载
1.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个成员函数都初始值,并且在对象的整个生命周期内只调用一次。
特性
构造函数是特殊的成员函数,需要注意的是,构造虽然名称叫做构造,但是构造函数的主要任务并不是开空间创造对象,而是初始化对象。
key:开空间是函数栈帧的事情,调用函数开辟空间。
- 函数名必须与类名想同
- 无返回值(void也不用写)
- 对象实例化时自动调用对应的构造函数
- 构造函数可以重载
class Time
{
public:
//无参的构造函数
Time()
{
cout << "None Pram" << endl;
}
//带参数的构造函数
Time (int hour, int minute, int second)
{
_hour = hour;
_minute = minute;
_second = second;
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1; //调用无参的构造函数
Time t2(9, 19, 30); //调用带参的构造函数
Time t3(); //该方法调用无参构造函数会报错,该种写法是写一个返回值为Time类型函数的写法。
}
- 如果类中没有显示的定义构造函数,C++编译器会默认生成一个无参的默认构造函数,一旦用户显式定义编译器将不再自动生成。
class Time
{
public:
带参数的构造函数
//Time(int hour, int minute, int second)
//{
// _hour = hour;
// _minute = minute;
// _second = second;
// cout << _hour << ":" << _minute << ":" << _second << endl;
//}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
//1.将显示定义的构造函数屏蔽以后,编译器会默认自动生成一个
//无参的默认构造函数,因此不会报错。
//2.如果将显示定义的构造函数展开,编译器不在生成,因为用户定义了一个。
//此时如果还这样声明变量,就会报错,要以自己定义的方式传参。
Time t1;
}
-
对于编译器生成的默认构造函数,实际上并没有对Time类的成员变量初始化,它究竟有什么用呢?
编译器生成的默认构造函数对内置类型(int/char/double/指针)不做处理,对自定义类型(struct,class,union)等,会去调用它们自己的默认构造函数。(对内置类型不处理是C++的缺陷)。c++11又增加了一个特性,弥补了缺陷,即内置类型的成员变量在类中声明时可以给默认值 -
无参的构造函数、全缺省的构造函数和没写编译器生成的构造函数,都可以认为是默认构造函数,默认构造函数有且只能有一个。
2.析构函数
与构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时自动调用析构函数,完成对象中资源清理工作。对象生命周期到了会自动销毁。
特性
- 析构函数名是在类名前加上字符~
- 无参数无返回值类型
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象在生命周期结束时,C++编译器系统自动调用析构函数
class Stack
{
public:
Stack(int capacity=3)
{
_arr = (int*)malloc(sizeof(int) * capacity);
if (_arr == nullptr)
{
printf("malloc failed");
}
_capacity = capacity;
_size = 0;
printf("malloc success\n");
}
~Stack()
{
if (_arr)
{
free(_arr);
_capacity = 0;
_size = 0;
}
cout << "~stcak" << endl;
}
private:
int* _arr;
int _capacity;
int _size;
};
int main()
{
Stack s1;
return 0;
}
- 当没有显示定义析构函数时,编译器会自动默认生成析构函数,它对内置类型不作处理,对自定义类型去调用它自己的析构函数。
- 如果类中没有申请资源,可以不写析构函数,直接使用编译器默认生成的。否则会造成资源泄露。
3.拷贝构造函数
在创建对象时,可以创建一个与已存在对象一摸一样的新对象。该函数只有单个形参,该形参是本类类型对象的引用(常用const修饰)。
特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类型对象的引用,使用传值方法会引起无穷调用,因为每一次传值调用,传过去的值都是一次拷贝构造(传值都会临时拷贝一份给形参传过去),这样会引发无穷递归。
class Time
{
public:
//全缺省的构造函数
Time(int hour=0, int minute=0, int second=0)
{
_hour = hour;
_minute = minute;
_second = second;
cout << _hour << ":" << _minute << ":" << _second << endl;
}
//复制本应该有两个形参,为什么只写了一个?
//因为还隐含了一个this指针。
//使用const是防止传进来的d被修改
Time(const Time& d)
{
this->_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1;
Time t2(t1); //拷贝构造的语法,获得一个以t1完全一样的t2
return 0;
}
问题1:为什么不用=符号完成拷贝构造呢?
虽然d2、d3都能完成拷贝构造的任务,然而d1并不直观、不明确,容易让人误以为是赋值。
问题2:指针也能完成拷贝构造的任务,为什么要用引用呢?
d4传地址有点冗余麻烦,把d1的地址赋值给d5是什么意思?显然对比d2、d4、d5,发现使用引用是最符合正常人的逻辑的。
- 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或值拷贝
class TestCls {
public:
TestCls()
{
cout << "TestCls()" <<endl;
p = new int;
}
TestCls(const TestCls& d)
{
cout << "TestCls(const TestCls& testCls)" << endl;
a = d.a;
//p = testCls.p; //如果是编译器默认生成的,会直接把指针的地址赋给p
p = new int;
*p = *(d.p); //为拷贝类的p指针分配空间,实现深度拷贝
}
~TestCls()
{
delete p;
std::cout << "~TestCls()" << std::endl;
}
private:
int a;
int* p;
};
int main()
{
TestCls t1;
TestCls t2(t1);
return 0;
}
上面的例子也可以看出,像内置函数类型(简单的赋值操作),一般默认是深拷贝,但是涉及到指针的问题,涉及到自定义类的问题,我们在写构造函数时,一定要注意深浅拷贝的问题。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
return 0;
}
注意这段代码,d1的成员变量和_t的成员变量内存中是挨着的。
-
类中如果没有涉及到资源申请时,拷贝构造函数写不写都可以;一旦涉及到资源申请时,必须写拷贝构造函数,否则就是浅拷贝(只赋给指针)
-
拷贝构造典型场景
使用已存在对象创建新对象;函数参数类型为类类型对象;函数返回值为类类型对象。
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0;
}
因此,为了提高效率,一般对象传参时,尽量使用引用类型;返回时,根据实际场景,能用引用尽量使用引用。
4.赋值重载函数
运算符重载
C++为了增强代码的可读性引入运算符重载,运算符重载是具有特殊函数名的函数。
内置类型可以直接使用运算符运算,编译器知道如何运算;自定义类型无法直接使用运算符,编译器不知道如何运算,因此需要自己实现运算符重载。
函数定义语法:
返回值类型 operator操作符(参数列表)
- 不能创建新的操作符,如:operator@
- 重载操作符必须有一个类类型参数
- 用作内置类型的运算符,其含义不能改变,例如:内置类型+,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
- .*(见的很少) ::(域作用限定符) sizeof ?:(三目运算符) .(函数调用符号)注意以上5个符号不能进行函数重载
class Date
{
public:
//构造函数
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 运算符重载
//相当于bool operator==(Date* const this, const Date& d);
//注意:这里左操作数是this,右操作数是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(2023, 8, 3);
Date d2(d1);
Date d3(1, 1, 1);
//相当于
//cout<<d1.operator==(&d1, d2)<<endl;
cout << (d1 == d2) << endl; //结果为1
cout << (d1 == d3) << endl; //结果为0
return 0;
}
赋值运算符重载
即给一个已经存在的对象赋值(已经存在),拷贝构造(给一个新对象赋值)。
- 参数类型:const T&,传递引用可以提高效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this:保证连续赋值不出错。
// 赋值运算符重载
//参数加引用和const,传参更快,防止参数修改
//返回引用,防止拷贝构造。
Date& operator=(const Date& d)
{
//防止 d1 = d1;
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//可以这样调用
d2 = d1; // d2.operator=(&d2, d1) 第一个参数是地址,第二个参数是传引用
d3 = d2 = d1;
-
赋值运算符只能重载成类的成员函数,不能重载成全局函数
因为赋值运算符重载如果用户不显示的定义在类里面,编译器就在类里面自动生成一个。此时如果再在全局声明一个,就和类里面的冲突了。 -
用户没有显示的实现时,编译器会默认生成一个默认赋值运算符重载,以值的方式逐字节拷贝。内置类型成员变量直接赋值,自定义类需调用自己实现的。
const成员函数
使用const修饰的成员函数
参考文章地址
取地址运算符重载
//写在类里面
//取地址运算符重载
Date* operator&()
{
return this;
}
//通过这样设置可以保证,类外面的成员只能访问地址,不能修改地址。
const Date* operator&()const
{
return this;
}
正常情况下,如果我们不写这个成员函数,编译器会自动生成。所以一般不需要我们自己去写。
那我们什么时候应该写这个成员函数呢?
比如当你不想让别人取到这个类型对象的地址的时候,你可以return nullptr ,这样就永远无法获得这个对象的地址了。