- 甲乙丙丁又如何,青春的阳光,照亮的是所有人。
目录
类的6个默认成员函数
- 默认构造函数
- 默认析构函数
- 默认拷贝构造函数
- 默认赋值重载函数
- 默认取地址重载函数
- 默认const取地址重载函数
其中,前四个都是比较重要的,下面一一展开。
一.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的作用是自动完成初始化工作,避免了频繁手动调用初始化函数的问题
注意:并不是开空间创建对象,而是初始化对象
下面就是一个构造函数。
class Stack
{
public:
Stack()
{
cout << "Stack()" << endl;
}
}
构造函数的特征如下:
- 函数名与类名相同
- 无返回值,(void都不需要写)
- 对象实例化时编译器
自动调用
对应的构造函数 - 构造函数可以重载,
即一个类可以有多个构造函数,但默认构造函数只能有一个
构造函数的分类:
构造函数分为无参构造函数和有参构造函数
class Date
{
public:
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
}
他们调用的方式是不同的
Date d1;//调用无参构造函数
Date d2(4);//调用有参构造函数
实际上,推荐使用全缺省的构造函数,就能实现对上述两个构造函数的合并
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
这样既可以无参调用,又可以有参调用,传1个,2个,3个参数都可以。
Date d1;
Date d2(2023, 2, 3);
Date d3(2023);
Date d4(2023, 2);
默认构造函数:
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
也就是说:全缺省的构造函数和无参构造函数同时存在会有歧义。
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户实现了任意一个构造函数,编译器将不再生成。
-
有人可能会觉得:既然编译器会自动生成,那么看起来好像我们根本就不用写了。
然而并不是这样。默认构造函数设计的有一个坑
C++把类型分成内置类型(基本类型)和自定义类型
,内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型
- 默认生成的构造函数对内置类型不处理
- 对自定义类型的成员会调用他们的默认构造函数
调用了默认构造函数后,内置类型成了随机值。
为了解决这个问题,c++11新增了个补丁
新增补丁
在内置类型声明时,可以给他们指定一个缺省值,这样就会以这些缺省值来初始化成员变量。
二. 析构函数
析构函数的作用自动完成对象内资源的销毁,解决手动频繁销毁操作的问题
注意:析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
格式如下:
~Stack()
{
_year = _month = _day = 0;
}
析构函数也存在默认析构函数
和默认构造函数类似。
默认生成的析构函数对内置类型不处理,对自定义类型的成员会调用他们的默认析构函数。
📝注意一点:如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数即可;当一个类涉及到动态内存分配时
,就需要我们自己手写一个析构函数,否则会造成资源泄漏
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
三.拷贝构造函数
作用:在创建对象时,去创建一个与已存在对象一模一样的新对象
写法:
构造函数的实现:
class Date
{
public:
Date(const Date& d)
{
//d 拷贝给 *this
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year = 2022;
int _month = 6;
int _day = 9;
};
规范化:
Date(const Date& d) 这里尽量加上const,防止权限放大。加上引用,防止无限递归循环
调用有两种方式:
Date d1;
Date d2(d1); //第一种
Date d3 = d1; //第二种
特征:
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
为什么会发生无穷递归?
参数没有设为引用,进行传值传参
对于内置类型编译器可以可以直接拷贝
对于自定义类型的拷贝,需要调用拷贝构造
**因为调用拷贝构造需要去传值传参,自定义类型的传值传参就会去调用拷贝构造,调用拷贝构造就需要传值传参,而自定义类型的传值传参就会去调用拷贝构造… … …**如下:
Date(Date d)
{
//d 拷贝给 *this
_year = d._year;
_month = d._month;
_day = d._day;
}
Date d2(d1);
因此应该这样写:
Date(const Date& d)
{
//有效避免无穷递归问题
//……
}
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数是按内存存储
按字节序
完成拷贝,这种拷贝叫做浅拷贝
,或者值拷贝。
默认拷贝构造函数
默认拷贝构造函数,既可以对内置类型进行处理,也可以对自定义类型进行处理。
但需要注意深浅拷贝的问题。
- 什么是浅拷贝什么是深拷贝?
对于浅拷贝,即按字节序完成拷贝
这就意味着,当涉及空间开辟时,若只是简单的进行浅拷贝,就会出现两个指针变量指向同一块空间的情况。因为只是简单的把地址进行了拷贝,并没有开辟新的空间。见下图:
当程序要退出时,s2和s1要销毁,s2先销毁,他指向的空间已经被释放,s1不知道,s1会再将他指向的空间释放一次。造成了一块空间的多次释放,引起程序崩溃。
此外,s2写入的数据会被s1写入覆盖。
因此对于拷贝构造,浅拷贝存在问题(数据被覆盖,free两次)
而深拷贝,会先开辟一块同样大的空间,再将空间中的数据拷贝过来
因此:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝
场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
注意: 默认拷贝构造函数与默认构造函数名相同,当我们只写拷贝而不写构造时,编译器就会报错,因为此时的拷贝会被误以为是默认构造函数
四.赋值运算符重载
运算符重载
我们知道,运算符默认只对内置类型有效,它无法作用于自定义类型。
为了能让自定义类型使用运算符,我们就需要进行运算符重载
运算符重载是具有特殊函数名的函数
,也具有其返回值类型,函数名字以及参数列表
写法:返回值类型 operator操作符(参数列表)
举例:定义两个日期类对象,对两个日期类对象的年月日进行比较
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
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;
}
这样就可以对我们的自定义类型进行比较
Date d1(2022, 9, 26);
Date d2(2023, 9, 27);
//两种调用方式,一般都会去用第二种
operator==(d1,d2);//第1种
d1 == d2;//第2种
注意:
- 1.运算符的优先级问题
cout << d1 == d2 << endl;//<< 优先级大于 ==
cout << (d1 == d2) << endl;//需要括号括起来
-
- 运算符重载写在类外,如果没有自定义get等函数获取成员变量,就需要把成员变量定义为公有,这样不好。最好把运算符重载写在类里面。 但写法有所区别,因为作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
bool operator==(const Date& d)
{
return _year == d._year;
&& _month == d._month
&& _day == d._day;
}
这样写在类中,调用也有所不同
d1.operator==(d2);//第一种
d1 == d2;//第二种
-
- 重载操作符必须有一个类型参数
-
.* :: sizeof ?: .
注意以上5个运算符不能重载.
举例:其他运算符的重载
- 小于 <
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;
else
return false;
}
- 小于等于 <=
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
- 大于 >
bool operator>(const Date& d)
{
return !(*this <= d);
}
- 大于等于 >=
bool operator>=(const Date& d)
{
return !(*this < d);
}
- 不等于 !=
bool operator(const Date& d)
{
return !(*this == d);
}
这里需要注意去复用之前的运算符重载
,减少代码量,更加方便。
赋值运算符重载
赋值重载的目的
:将 d1 对象赋值给 d2,非拷贝构造,d1、d2均已存在
有了上面的铺垫,写赋值运算符就比较容易
void operator=(const Date& d)
//能用引用的地方,就用引用
//避免去走拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
但是这样写并不好。我们需要解决两个问题。
- 赋值运算符支持连续赋值例如,j = k = l;
解决:最后return *this
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
- 自己给自己赋值,k = k;
解决:加判断
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
因此,赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,
有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :
要能够连续赋值
注意:
-
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝即:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
-
赋值重载和拷贝构造都能用 = ,如何区别
赋值重载针对两个已经定义好的对象,拷贝构造是用一个已经定义的去初始化另外一个未定义的。
Date d5 = d4;//是拷贝构造
默认生成的特性总结:
const修饰
const修饰this 指针
见下面代码:
class Date
{
public:
//实现简单的打印函数
void Print()
{
cout << _a << endl;
}
private:
int _a = 200;
};
int main()
{
const Date d;
d.Print();
return 0;
}
上面代码会发生报错。
传入Printh函数的d的类型是const Date*,而Print的this指针类型为Date*。
这就造成了权限的放大
,原本不可更改的变为了可更改的。
可以加一个const来修饰this指针
:
class Date
{
public:
//实现简单的打印函数
void Print() const//修饰this指针
{
cout << _a << endl;
}
private:
int _a = 200;
};
int main()
{
const Date d;
d.Print();
return 0;
}
权限的缩小
下面代码即为权限的缩小,将可修改的类型变为了不可修改的类型
class Date
{
public:
//实现简单的打印函数
void Print() const//修饰this指针
{
cout << _a << endl;
}
private:
int _a = 200;
};
int main()
{
Date d;
d.Print();
return 0;
}
此外,
const 修饰可以提高程序的健壮性
,常被用来修饰引用、指针
当被指向对象为常量或临时变量时,应该去使用 const 修饰,避免出现权限放大问题(常量不可更改,从不能改到能改,这就是权限放大了,此时是不行的)
//int* p = 1; //错误,1 具有常性
const int* p = (const int*)1; //正确
//int& a = 2; //错误,2 具有常性
const int& a = 2; //正确
五. 取地址重载函数
作用就是获取当前对象的地址
class Date
{
public:
Date* operator&()
{
return this;
}
private:
int _a = 200;
};
但我们可以直接使用&符号获取地址,完全没有进行重写的必要。
不过可以进行一些另类的操作
Date* operator&()
{
return nullptr;//不想让别人获取地址,直接返回空
}
Date* operator&()
{
return (Date*)0x01;//返回一个假的地址
}
六. const取地址重载
获取 const 修饰对象的地址
const Date* operator&() const
{
return this;
}
取地址重载函数和const取地址重载函数使用编译器默认生成的就够了,基本不需要重写