目录
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- // 拷贝构造
- Date(const Date& d)
- {
-
- d._year = _year;
- d._month = _month;
- d._day = _day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- date d1(2023, 2, 3);
- date d2(d1);
-
- return 0;
- }
如果不加&引用符号,编译器会报错。
使用拷贝构造也可以用这种方式:
Date d3 = d1;
- 内置类型的拷贝和传参,编译器可以直接拷贝,
- 自定义类型的拷贝和传参,需要调用拷贝构造。
为什么需要拷贝构造呢?
用栈实现队列的时候,可能一会对st1进行析构一会对st2进行析构,成员变量指针_a指向的空间不能析构两次,而且如果分别对st1和st2赋初值,后赋值的会覆盖先赋值的数据。所以就要求它们不能指向同一块空间,各自要有各自的空间,所以C++规定:自定义类型的拷贝和传参,需要调用拷贝构造,对于栈这种需要深拷贝的拷贝构造(后续学习),现阶段只需要知道需要拷贝构造即可。
自定义类型传参:
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- // 拷贝构造
- // Date d2(d1);
- Date(Date& d)
- {
- d._year = _year;
- d._month = _month;
- d._day = _day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- // 传值传参
- void Func1(Date d)
- {
-
- }
- // 传引用传参
- void Func2(Date& d)
- {
-
- }
- // 传指针
- void Func3(Date* d)
- {
-
- }
- int main()
- {
- Date d1(2023, 2, 3);
- Func1(d1);
-
- //Func2(d1);
- //Func3(&d1);
- return 0;
- }
使用Func1传值传参 :
在Func1(d1)处按F11会直接跳转到拷贝构造。
拷贝构造结束会进行Func1函数。
如果用Func2引用传参,就不需要拷贝构造了。
直接跳转Func2函数。
此外还可以使用以前C语言中常用的指针传参,但这种方法有点啰嗦。
Func3(&d1);
拷贝构造一般会加const,如果不加,比如下面的情况,赋值方向反了,会导致原数据变成随机值。
- Date(Date& d)
- {
- d._year = _year;
- d._month = _month;
- d._day = _day;
- }
加上const可以避免拷贝方向错误时,原数据被修改,所以一般习惯参数前加上const。
- Date(const Date& d)
- {
- _year = d->_year;
- _month = d->_month;
- _day = d->_day;
- }
此外,如果被拷贝的变量是被const修饰,如果拷贝构造的参数不被const修饰,会造成引用的权限扩大,所以一定要用const修饰参数。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- class Date
- {
- public:
- Date(int year = 1, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- void Print()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1(2023, 11, 20);
- d1.Print();
- Date d2(d1);
- d2.Print();
- return 0;
- }
通过输出结果我们可以发现,没写拷贝函数,编译器会自动对内置类型拷贝。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
当然像日期类这样的类是没必要的。
如果是自定义类型呢?程序会怎么处理?
- typedef int DataType;
- class Stack
- {
- public:
- Stack(size_t capacity = 10)
- {
- cout << "Stack(size_t capacity = 10)" << endl;
-
- _array = (DataType*)malloc(capacity * sizeof(DataType));
- if (nullptr == _array)
- {
- perror("malloc申请空间失败");
- exit(-1);
- }
-
- _size = 0;
- _capacity = capacity;
- }
- void Push(const DataType& data)
- {
- // CheckCapacity();
- _array[_size] = data;
- _size++;
- }
- ~Stack()
- {
- cout << "~Stack()" << endl;
- if (_array)
- {
- free(_array);
- _array = nullptr;
- _capacity = 0;
- _size = 0;
- }
- }
- private:
- DataType* _array;
- size_t _size;
- size_t _capacity;
- };
-
- int main()
- {
- Stack st1;
- Stack st2(st1);
-
- return 0;
- }
输出后程序会报错:
如果按照内置类型的方式对自定义类型进行拷贝, 两个Stack类的变量st1和st2的成员变量*array都指向同一块空间,这样会导致两个问题:
这时只有深拷贝才能解决自定义类型的拷贝构造。
- Stack(const Stack& st)
- {
- _array = (DataType*)malloc(sizeof(DataType) * st._capacity);
- if (_array == nullptr)
- {
- perror("malloc fail");
- exit(-1);
- }
- memcpy(_array, st._array, sizeof(DataType) * st._size);
- _size = st._size;
- _capacity = st._capacity;
- }
st2成功实现了拷贝构造,st2与st1的地址不同,它们不在指向同一空间了。
什么时候需要自己实现拷贝构造?
- 当自己实现了析构函数释放空间,就需要实现拷贝构造。
- class MyQueue
- {
- public:
- // 默认生成构造
- // 默认生成析构
- // 默认生成拷贝构造
-
- private:
- Stack _pushST;
- Stack _popST;
- int _size = 0;//缺省值处理
- };
拷贝构造函数典型调用场景:
默认生成拷贝构造:
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名字为:关键字operator后面接需要重载的运算符符号。
- 函数原型:返回值类型 operator操作符(参数列表)
注意:
我们通过代码一点一点理解:
- 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;
- };
- //运算符重载
- 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(2023, 1, 1);
- Date d2(2023, 1, 1);
- //下面两种使用方式均可
- operator==(d1, d2);
- d1 == d2;//编译器会转换成去调用operator==(d1,d2);
- return 0;
- }
如果要输出函数返回值,方式如下:
- cout << (d1 == d2) << endl;//必须加括号,保证优先级
- cout << operator==(d1, d2) << endl;
这里会发现,如果运算符重载成全局的就需要成员变量是公有的,否则会报错。
那么问题来了,封装性如何保证?
我们可以选择后面学习的友元处理,现阶段直接把运算符重载函数重载成成员函数即可。
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = 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(2023, 1, 1);
- Date d2(2023, 1, 1);
- //operator内置只能使用这种形式(d1 == d2,
- cout << (d1 == d2) << endl;
- return 0;
- }
- 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;
- }
- }
还可以写成这种简便形式,但上述较长的方式更直观。
- return _year < d._year
- || (_year == d._year && _month < d._month)
- || (_year == d._year && _month == d._month && _day < d._day);
判断小于等于我们可以借助隐藏的this指针,使用运算符重载嵌套的方式,在判断小于等于中分别调用判断小于和判断等于的运算符重载。
- // d1 <= d2
- bool operator<=(const Date& d)
- {
- return *this < d || *this == d;
- }
同理,判断大于、大于等于和不等于,我们也对上面的运算符重载进行复用。
- // d1 > d2
- bool operator>(const Date& d)
- {
- return !(*this <= d);
- }
-
- bool operator>=(const Date& d)
- {
- return !(*this < d);
- }
-
- bool operator!=(const Date& d)
- {
- return !(*this == d);
- }
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- void Print()
- {
- cout << _year << "/" << _month << "/" << _day << endl;
- }
- void operator=(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- Date d1(2023, 6, 6);
- Date d2(1, 1, 1);
-
- d2 = d1;
- d1.Print();
- d2.Print();
-
- return 0;
- }
operator=就是我们的赋值运算符重载,如果我们不使用引用传值,那自定义类型会调用拷贝构造,我们使用引用传值,就避免了这些拷贝操作。
如果三个数赋值呢?
d3 = d2 = d1;
根据赋值操作符的右结合性,d1赋值给d2需要一个返回值才能对d3赋值。
返回值使用引用返回,避免传值返回过程中,创建临时变量和不必要的拷贝。
- Date& operator=(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
-
- return *this;
- }
如果自己给自己赋值呢?那就可以不进行赋值操作了,我们添加一个判断即可。
- Date& operator=(const Date& d)
- {
- if (this != &d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
- return *this;
- }
上面就是赋值运算符的完整版了。
- 注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值,所以日期类就不需要写运算符重载函数,而类似用栈实现队列则需要。
- 注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。