C++类和对象
1.拷贝构造函数
1.1 概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
1.2. 特征
拷贝构造函数是一种特殊成员函数,其特征如下:
1.2.1. 拷贝构造函数构造函数的一种重载形式;
1.2.2. 拷贝构造函数的参数只能有一个,是对本类类型对象的引用,不能是传值调用,编译会直接报错,或者是直接进入死循环。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
_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;
}
注意:调用拷贝构造函数是在定义新类的时候调用的,如果两个已经定义了两个类的类型d1、d2,d1=d2是赋值运算符的重载,是简单的拷贝,不会调用拷贝构造函数!
所以,在写拷贝构造函数的时候,如果用传值调用,会出现一直调用拷贝构造函数的情况,则会出现没有尽头的递归调用。
1.2.3. 若未显示定义拷贝构造函数,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
1.2.4. 编译器生成的默认拷贝构造函数是一种浅拷贝
注意:类中如果没有涉及资源申请的时候,可以不用写拷贝构造函数,使用默认的拷贝构造函数即可;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
1.2.5. 拷贝构造函数典型调用场景
- 使用已存在的对象创建新的对象
- 函数参数类型为类类型对象
- 函数返回值类型是类类型
用新定义类类型对象接受函数的返回值的时候,编译会优化,只会调用一次拷贝构造函数,把连续的两次调用合并成一次。
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
2.运算符重载
2.1 问题提出
比较日期的大小
【如果函数这样写】
函数定义的时候没有依据功能命名,也没有添加注释,则函数调用的时候就难以知道函数的功能是什么,再次读函数的时候就很难理解。如果类之间的比较可以用> 或<符号就会显得很简单了,所以这就要用到运算符重载的关键字operator。
这样子就会显得简单易懂
2.2. 运算符重载
运算符对内置类型来说可以使用,通过运算符重载,我们可以灵活的对我们自定义类型进行运算符相应的结果运算,从而达到简化的效果。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符号
函数原型:返回值类型 operator(参数列表)
注意:
- 不能通过链接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整形+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
注意以上五个运算符不能进行重载,特别是第一个记住
【全局的运算符重载】
【运算符重载成员函数】
2.3. 赋值运算符重载
①赋值运算重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- **返回值类型:**T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值(判断地址是否相同)
- 返回 * this:要复合连续赋值的含义
②赋值运算只能重载成类的成员函数,不能做成全局函数
原因:赋值运算如果不显示实现,编译器会生成一个默认的赋值运算重载函数。此时用户在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
③**用户没有显示定义赋值运算符重载,编译器会生成一个默认运算符重载,以值的方式逐字节拷贝。**注意:内置类型成员变量是直接赋值的,而自定类型成员变量需要调用对应类的运算符重载完成赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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;
s2 = s1;
return 0;
}
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
辨析:赋值运算符重载函数和构造函数
赋值运算符重载:两个已经存在的对象之间的赋值
构造函数:在用一个已经存在的对象初始化另一个对象
2.4. 前置++和后置++的区分
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
一份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}