前言
空类真的是空的吗,并不是!
本期概览:
类的五个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- 取地址运算符重载
五个默认成员函数
空类中真的如我们看到的一样,真的是空的吗?
其实不然,里边还有五个默认生成的成员函数(如果自己写了对应功能的函数,编译器就不会自动生成;没写就会自动生成)
【为什么要有默认成员函数?】
-
初始化、销毁等等,总是容易忘记,干脆搞个默认的!
-
对于编译器来说,内置类型是很熟悉,能轻松初始化、赋值、拷贝,但是复杂的自定义类型它可搞不定,需要一个函数来操作。
一、构造函数
1. 构造函数是什么?
是用来初始化对象的成员函数。
2. 特性
- 函数名是 className(类的名字)
- 无返回值
- 可重载
3. 调用
- 实例化新对象的时候自动调用。
- 调用顺序符合顺序规则。(谁先实例谁先构造)
4. 编译器自动生成的
- 对内置类型,不处理
- 对自定义类型,调用其构造函数
最终也会走到内置类型这一步啊,那么…
有没有办法弄一下内置类型?
C++打了个补丁:内置类型成员在声明的时候可以给缺省值。
注意,这不是初始化,只是赋初值——定义后才给的缺省值,初始化是定义时赋值。
class Date
{
public:
//...
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};
默认构造函数
需要特别强调,默认构造函数并不特指编译器自动生成的构造函数,而指不用传参的,以下几种都叫做默认构造函数:
- 编译器自动生成的
- 无参数的
- 全缺省的
注意:默认构造函数只能有一个(避免二义性)
5. 使用
- 编译器自动生成的默认构造函数,无需传参
class Date
{
public:
void ShowDate()
{
cout << _year << '-' << _month << '-' << _day << endl;
}
private:
//在声明的时候给缺省值
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.ShowDate();
return 0;
}
:1970-1-1
- 全缺省的默认构造函数,可传可不传
class Date
{
public:
Date(int year = 2022, int month = 10, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void ShowDate()
{
cout << _year << '-' << _month << '-' << _day << endl;
if (_month == 10 && _day == 1)
cout << "祖国万岁!" << endl;
}
private:
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
//Date d1(2022, 1, 1);
Date d1;
d1.ShowDate();
return 0;
}
:2022-10-1
祖国万岁!
- 需要传参的构造函数
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void ShowDate()
{
cout << _year << '-' << _month << '-' << _day << endl;
if (_month == 10 && _day == 1)
cout << "祖国万岁!\n" << endl;
}
private:
int _year ;
int _month;
int _day;
};
int main()
{
Date d1(2022, 10, 1);
d1.ShowDate();
return 0;
}
:2022-10-1
祖国万岁!
【什么时候自己写?】
- 关乎指针
- 关乎数组
直接对指针/数组赋值达不到效果,指向同一块空间不好玩了。
二、析构函数
1. 析构函数是什么?
是用来清理对象资源的成员函数。(如释放空间)
2. 特性
- 函数名是~className(类名前加个’~')
- 无参数无返回值
- 不可重载
3. 调用
- 出对象的生命周期自动调用。
- 调用顺序符合栈的规则。(后实例的先析构)
4. 编译器自动生成的
- 对内置类型,不处理。
- 对自定义类型,调用其析构函数。
5. 使用
-
编译器自动生成的默认析构函数(无法调试看见或打印信息)
-
自定义的析构函数
class DynamicArr
{
public:
DynamicArr(int capacity = 4)
{
cout << "DynamicArr(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_capacity = capacity;
}
void Push()
{
//...
}
~DynamicArr()
{
cout << "~DynamicArr()" << endl;
free(_a);
_a = NULL;
_capacity = _size = 0;
}
private:
void CheckCapacity()
{
//...
}
int* _a = NULL;
int _size = 0;
int _capacity = 0;
};
int main()
{
DynamicArr a;
return 0;
}
:DynamicArr(int capacity = 4)
~DynamicArr()
三、拷贝构造函数
1. 拷贝构造函数是什么?
拷贝别人用于初始化自己的函数——用一个已存在对象给同类型新对象初始化。
我们知道,新对象实例化的时候会调用构造,但是在此基础上,我们如果想用已存在对象的成员给新对象初始化,就可以用拷贝构造
2. 特性
- 是构造函数的一种重载
- 有且只有一个参数,类型为该类的常引用——const className&
:不需要修改用来拷贝的已存在对象,所以加const;引用减少拷贝,提高效率,且传值传参会无穷调用!
- 用已存在的d1给d2初始化,触发调用拷贝构造
- 调用拷贝构造时,d1拷贝给形参,又触发调用拷贝构造
- 此时的拷贝构造,又是同类型的实参拷贝给形参,又触发拷贝构造
:调用拷贝构造,传参的时候又触发调用拷贝构造,无穷调用
3. 调用
当用已存在对象,给新对象初始化的时候调用
如
- 已存在类型给新对象初始化
- 函数传值传参:形参是实参的临时拷贝,要把已存在实参对象拷贝给新的形参对象
- 函数返回对象:返回的时候会产生临时变量,待返回对象拷贝给tmp,tmp再拷贝到返回的地方
4. 编译器自动生成的
- 对内置类型,按字节拷贝(浅拷贝)
- 对自定义类型,调用其拷贝构造函数
这里涉及到浅拷贝和深拷贝,像之前的复制带随机指针的链表,就是深拷贝,开辟了空间的一类都需要深拷贝。
如果需要深拷贝的用了浅拷贝:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
原因是s2的 _array被按字节拷贝成了s1的_array,两指针指向同一块空间,于是析构s1、s2的时候对同一块空间释放两次,自然崩溃。
5. 使用
对于这样的日期类
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);
return 0;
}
:Date(const Date& d)
- 函数传值传参:形参是实参的临时拷贝,要把已存在实参对象拷贝给新的形参对象
void test(Date d)
{}
int main()
{
Date d;
test(d);
return 0;
}
:Date(const Date& d)
- 函数返回对象:返回的时候会产生临时变量,待返回对象拷贝给tmp,tmp再拷贝到返回的地方
Date test()
{
Date d;
return d;//返回了局部对象,不合理的代码,此处仅示范
}
int main()
{
Date d = test();
return 0;
}
:Date(const Date& d)
诶?奇怪,为什么这里只调用了一次拷贝构造?
6. 编译器对拷贝构造的优化
对于紧贴着连续调用的
- 拷贝 + 拷贝构造
- 拷贝构造 + 拷贝构造
部分编译器会直接合并,用内置类型来理解:
int a = 10, b, c;
//不合并
b = a;//a拷贝给b
c = b;//b拷贝给c
//合并
c = a;//a直接拷贝给c
这样的场景有:
- 传值传参
- 传值返回
- 临时变量
- 类型转换
- 传值返回
一句话总结就是
连续出现的拷贝构造会合并。
*运算符重载
想要学习赋值运算符重载,我们得先了解什么是运算符重载。
1. 运算符重载是什么?
拥有特殊函数名(operator + [运算符])的函数。
Date& operator+=(const Date& d2);//Date类的加号运算符重载
*operator是C++中的关键字,用于运算符重载。
2. 特性
- 函数名是 operator [运算符],operator后接要重载的运算符
- 函数的参数必须和操作符的操作数一致
- 隐式传递的this指针也是一个参数
- 用于内置类型的运算符不能重载(不然基本运算都乱了)
- 不能重载出新的运算符,如 # $
- [ .* ] [ : : ] [ sizeof ] [ ? : ] [ . ] 不能重载
3. 调用
直接对对象使用即可。(可读性高)
4. 使用
- Date的全局==重载
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;
};
//全局的operator==
bool operator==(const Date& d1, const Date& d2)
{
//在类外无法访问私有
//1. 可以直接重载成成员函数(类内可以访问)
//2. 也可以将此函数声明成友元(后面会讲)
//此处为了简单演示,直接把私有的权限放开了
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2022, 1, 1);
Date d2(2022, 1, 2);
//这里要注意,==的优先级低于<<
//cout << d1 == d2 << endl;//wrong
cout << (d1 == d2) << endl;
return 0;
}
:0
- Date的成员运算符重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1);
Date d2(2022, 1, 1);
cout << (d1 == d2) << endl;
return 0;
}
:1
四、赋值运算符重载
1.运算符重载是什么?
用同类的已存在对象,给另一已存在对象赋值的函数(运算符重载)
2. 特性
- 参数类型:const className&
- 返回值类型:className&
- 要返回*this(这样才符合赋值运算符的用法功能)
int a;
int b = a = 10;//没有返回值则无法正常使用
3. 调用
d1 == d2;
d1.operator==(d2);
二者相等,后者可以更好地理解:运算符重载的本质是函数
4. 编译器自动生成的
按字节拷贝
5. 使用
int main()
{
Date d1(2022, 1, 1);
Date d2(2022, 1, 2);
cout << (d1 == d2) << endl;
cout << (d1.operator==(d2)) << endl;
return 0;
}
:0
0
6. 注意事项
- 要检查是不是给自己赋值(是就返回)
- 运算符只能重载成类的成员函数,不能重载成全局函数
五、取地址运算符重载
取地址运算符重载分为 取地址运算符重载 和 const对象取地址运算符重载
1. 取地址运算符重载
是对对象取地址的时候调用的函数。
基本不用自己写,除非有这样的需求:让类的用户取地址时取到你指定的东西
2. const取地址运算符重载
是对const对象取地址的时候调用的函数
基本不用自己写,除非有这样的需求:让类的用户取地址时取到你指定的东西
越学越感觉有很多nb的玩法等待挖掘,C++…
今天的分享就到这里啦,这里是培根的blog,期待与你共同进步!