目录
一. 拷贝构造函数
1.1 什么是拷贝构造函数
1.2 编译器默认生成的拷贝构造函数
1.3 拷贝构造函数特性总结
二. 运算符重载
2.1 运算符重载概述
2.2 比较运算符重载(> >= == < <=)
2.2.1 >运算符的重载
2.2.2 ==运算符的重载
2.2.3 >=运算符的重载
2.2.4 <运算符的重载
2.2.5 <=运算符的重载
2.3 加减相关运算符的重载(+= 、++、-=、--、+、-)
2.3.1 +=运算符的重载
2.3.2 +运算符的重载
2.3.3 -=运算符的重载
2.3.4 -运算符重载
2.3.5 ++运算符的重载
2.3.6 --运算符的重载
2.4 赋值运算符重载函数
2.4 取地址及const取地址操作符重载
2.4.1 const成员
2.4.2 &操作符的重载
一. 拷贝构造函数
1.1 什么是拷贝构造函数
拷贝构造函数,是C++类的六个默认构造函数之一,其完成的功能是通过一个已有的类对象,创建并初始化一个新的同类类对象。
拷贝构造函数的函数名与类名相同,有且只有一个类型为本类对象引用的参数,没有返回值。且拷贝构造函数唯一的参数一般使用const修饰。
演示代码1.1以日期类为例,首先创建了一个类对象d1,再通过类对象d1创建了一个新的类对象d2,调用Print成员函数打印类中成员变量的值,可以看到,d1和d2中成员变量值相同。
调用拷贝构造函数创新新对象的语法为:类名 拷贝构造函数名(&对象名)。
演示代码1.1:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day) //构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print() //类成员变量打印函数
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 28); //创建类对象,调用默认构造函数
Date d2(d1); //调用拷贝构造函数创建类对象
d1.Print();
d2.Print();
return 0;
}
警告:拷贝构造函数的参数必须是引用,不可以是值,因为这会引发无穷递归。
从图1.2中可以看出,当采用对象值作为拷贝构造函数的参数时,每次创建形参都会引发一次拷贝构造函数的调用,程序会一直调用调用拷贝构造函数,永远不会执行函数体内部的语句。当然,某些编译器会将值作为拷贝构造函数的参数的错误检查出来。
1.2 编译器默认生成的拷贝构造函数
跟构造函数和析构函数一样,如果用户不显示地定义拷贝构造函数,那么编译器就会自动生成拷贝构造函数。编译器默认生成的拷贝构造函数完成的操作是将一个已存在的类对象的内容逐字节拷贝到新创建的对象中,这种拷贝方式叫浅拷贝(值拷贝)。
演示代码1.3的日期类Date中不显示定义拷贝构造函数,运行程序,用d1拷贝构造d2,可以看到,d1和d2的成员变量值依旧相同,这证实了编译器会生成默认的拷贝构造函数。
演示代码1.2:
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, 2, 28); //创建类对象,调用默认构造函数
Date d2(d1); //调用编译器默认生成的拷贝构造函数创建类对象
d1.Print();
d2.Print();
return 0;
}
问题:根据演示代码1.1和1.2的运行结果,用户显示定义的拷贝构造函数和编译的自动生成的拷贝构造函数完成一样的操作,那是否可以认为,用户显示定义的拷贝构造函数没有什么价值呢?
答案显然是否定的。
对于上面代码中定义的日期类,确实可以使用编译器默认生成的拷贝构造函数。但是,对于那些类对象涉及动态资源申请的类,就不可以使用编译器默认生成的拷贝构造函数。
演示代码1.3中定义了栈类Stack,其中成员变量int* _a用于动态资源申请,在类对象销毁时,编译器会自动调用析构函数~Stack()释放动态开辟的内存空间,如果此时用s1拷贝构造s2,那么s1和s2中的_a就会指向同一块内存空间,对s1和s2两次调用析构函数会对同一块动态开辟的内存空间多次释放,造成程序崩溃。这种情况就必须有用户显示定义拷贝构造函数,实现深拷贝。
演示代码1.3:
class Stack
{
public:
Stack(int capacity = 10) //构造函数
{
_a = (int*)malloc(capacity * sizeof(int));
if (nullptr == _a)
{
printf("malloc fail\n");
exit(-1);
}
_capacity = capacity;
_top = 0;
}
~Stack() //析构函数,释放动态开辟的内存空间
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
void Push(int x) //压栈函数
{
_a[_top] = x;
_top++;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
s1.Push(5); //将1、2、3、4、5压入栈中
Stack s2(s1); //调用编译器默认生成的拷贝构造函数
return 0; //程序结束前两次调用析构函数
}
1.3 拷贝构造函数特性总结
- 拷贝构造函数的函数名与类名相同。
- 拷贝构造函数是构造函数的一种重载形式。
- 拷贝构造函数没有返回值。
- 拷贝构造函数的参数有且只有一个,类型为本类对象的引用。
- 不能将值作为拷贝构造函数形参的类型,否则会引发无穷递归。
- 如果用户不显示定义拷贝构造函数,那么编译器就会默认生成拷贝构造函数。
- 对于存在动态资源申请的类,不可以使用编译器自动生成的拷贝构造函数。
二. 运算符重载
2.1 运算符重载概述
C++不允许直接对类对象使用运算符,但是,有些时候我们需要对类对象的成员进行相关操作,如:比较两个日期的先后(判断d1 > d2是否成立)、计算当天之后的第n天的日期(d2 = d1 + day)。为了增强程序的可读性,C++引入了运算符重载。
运算符重载是具有特殊函数名的函数,其具有一般函数公有的返回值、参数列表和函数名。(注意区分,构造函数、析构函数和拷贝构造函数没有返回值)。
运算符重载函数原型:返回值类型 operator运算符(参数列表)。
关于运算符重载的几点注意事项:
- 不能通过operator后接非运算符来创建新操作符,如operator$、operator@ 。
- 运算符必须有一个一个类类型参数。
- 不能改变运算符原本的含义。
- 参数列表的参数个数必须与运算符的操作数个数相同。
- 有五个操作符(sizeof、::、. 、.* 、?:)不能进行重载。
特别注意:在类对象中定义和声明运算符重载,会存在一个隐含的this指针作为一个参数,this指针为运算符重载函数的第一个参数。
对于有两个操作数的运算符,运算符重载函数的第一个参数为左操作数,第二个参数为右操作数。
本章以日期类为例,对各种运算符的重载进行实现
演示代码2.1:(日期类成员变量及成员函数声明)
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1); //默认构造函数
int GetMonthDay(int year, int month); //月份天数获取函数
void Print(); //日期打印函数
//1.比较类操作
bool operator>(Date& d);
bool operator>=(Date& d);
bool operator==(Date& d);
bool operator<(Date& d);
bool operator<=(Date& d);
//2.加减法类操作
Date& operator+=(int day); //日期加等函数
Date operator+(int day); //日期加法函数
//前置++和后置++重载
//后置++中采用一个int型的占位参数,用于表示后置
Date& operator++();
Date operator++(int);
Date& operator-=(int day); //日期减等函数
Date operator-(int day); //日期减法函数
Date& operator--(); //日期自减函数(前置)
Date operator--(int); //日期自减函数(后置)
//日期间隔计算
int operator-(Date& d);
//星期打印函数
void WeekPrint();
private:
int _year;
int _month;
int _day;
};
2.2 比较运算符重载(> >= == < <=)
我们只需要实现>和==操作符的重载,那么>= <= <就都可以通过复用>和==运算符重载函数来实现重载。
2.2.1 >运算符的重载
依次比较this指针指向的类的年、月、日是否大于被比较对象即可。
bool Date::operator>(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;
}
}
2.2.2 ==运算符的重载
年月日均相等返回值为true,否则为false。
bool Date::operator==(Date& d)
{
return (_year == d._year) && (_month == d._month) && (_day == d._day);
}
2.2.3 >=运算符的重载
d1>d2和d1==d2其中之一成立即d1>=d2成立,*this代表类对象d1。
bool Date::operator>=(Date& d)
{
return (*this > d) || (*this == d);
}
2.2.4 <运算符的重载
d1>d2 和 d1==d2 均不成立即 d1<d2 成立。
bool Date::operator<(Date& d)
{
return !((*this > d) || (*this == d));
}
2.2.5 <=运算符的重载
d1>d2 和 d1==d2 其中之一成立即d1<=d2成立。
bool Date::operator<=(Date& d)
{
return (*this < d) || (*this == d);
}
2.3 加减相关运算符的重载(+= 、++、-=、--、+、-)
2.3.1 +=运算符的重载
对日期类进行+=操作,就是获取当前日期再过day天后的日期。其计算方式为在当前_day上加day,如果大于当前_month天数就向_month进位,如果_month大于12就向年_year进位。
int Date::GetMonthDay(int year, int month)
{
static int MonthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = MonthDayArray[month];
//判断闰年,2月天数+1
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
Date& Date::operator+=(int day) //日期加等函数
{
_day += day;
int MonthDay = 0;
while (_day > (MonthDay = GetMonthDay(_year, _month)))
{
_day -= MonthDay;
_month++; //天数大于当前月份天数就向月进位
if (_month == 13) //向年进位
{
_month = 1;
_year++;
}
}
return *this;
}
2.3.2 +运算符的重载
+运算符不改变被加数的值,+=运算符改变被加数的值,+仅返回做加法后的值,不改变被加数本身。因此我们只需要拷贝构造一份*this对象ret,然后对ret执行+=操作即可。
Date Date::operator+(int day) //日期加法
{
Date ret(*this); //调用拷贝构造函数,创建返回值
ret += day;
return ret;
}
2.3.3 -=运算符的重载
计算当前日期向前day天的日期。首先让_day -= day,如果_day<0,就向月借位,如果_month==0成立,就向年借位,直到_day>0终止。
Date& Date::operator-=(int day) //日期减等函数
{
_day -= day;
while (_day <= 0)
{
_month--; //如果当前天数小于0,向月借位
if (_month == 0) //月为0向年借位
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month); //日期更新
}
return *this;
}
2.3.4 -运算符重载
与+运算符类似,-运算符不改变被减数的值,-=运算符改变被减数的值,-仅返回做减法后的值,不改变被减数本身。因此我们只需要拷贝构造一份*this对象ret,然后对ret执行-=操作即可。
Date Date::operator-(int day) //日期减法函数
{
Date ret(*this); //拷贝构造创建返回值
ret -= day;
return ret;
}
2.3.5 ++运算符的重载
++运算符分为前置++和后置++,C++通过引入一个int型的占位参数来区分后置++和前置++,后置++需要占位参数。
- 前置++函数声明:Date& operator++()
- 后置++函数声明:Date& operator++(int)
Date& Date::operator++() //日期前置++
{
(*this) += 1;
return *this;
}
Date Date::operator++(int) //日期后置++
{
Date ret(*this);
(*this) += 1;
return ret;
}
2.3.6 --运算符的重载
--与++类似,需要一个int型的占位参数区分前置和后置--。
- 前置--函数声明:Date& operator--()
- 后置--函数声明:Date& operator--(int)
Date& Date::operator--() //日期自减函数(前置)
{
(*this) -= 1;
return *this;
}
Date Date::operator--(int) //日期自减函数(后置)
{
Date ret(*this);
(*this) -= 1;
return ret;
}
2.4 赋值运算符重载函数
赋值运算符重载函数为C++类的六个默认成员函数之一,如果用户不显示定义,那么编译器就会自动生成赋值运算符重载函数。赋值运算符重载函数的返回值类型为类对象的引用,参数有且只有一个,类型也为类对象的引用。
- 函数参数类型:const 类名&
- 函数返回值类型:类名&
- 返回值:*this -- 为了满足=运算符可以连续赋值的特性
- 系统自动生成的赋值函数执行的操作为逐字节赋值
演示代码2.2中显示定义了赋值运算符重载函数,并在主函数中执行连续赋值操作,观察程序运行结果,d1、d2、d3都被赋予了d1的值,可见连续赋值成功。
演示代码2.2:
class Date //日期类
{
public:
Date(int year = 0, int month = 1, int day = 1) //默认构造函数
{
_year = year;
_month = month;
_day = day;
}
//赋值运算符重载函数
Date& operator=(const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
return *this;
}
void Print() //日期打印函数
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 25);
Date d2(2023, 2, 15);
Date d3;
d3 = d2 = d1;
cout << "d1: ";
d1.Print();
cout << "d2: ";
d2.Print();
cout << "d3: ";
d3.Print();
return 0;
}
警告:赋值运算符重载函数只能在类的内部定义,不能在类的外部定义为全局函数。
这是因为:如果用户不再类内部显示定义赋值运算符重载函数,那么编译器就会默认生成一个赋值运算符重载函数,这样编译器自动生成的赋值运算符重载函数和用户在类外部自定义的赋值运算符重载函数就会发生冲突。
2.4 取地址及const取地址操作符重载
2.4.1 const成员
假设定义一个const属性的类对象const Date d1,那么如果直接调用d1.Print()会发生报错。这是因为,隐含的this指针参数类型为Date* const this,而&d1的类型为const Date* const,传参时存在权限放大问题。这时,只需要在Print函数的声明后面添加const即可,这里的const的功能是将this指针的参数类型由Date* const转变为const Date* const。
//head.h
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
//如果不用const对成员函数进行修饰,那么就无法打印const date的类对象
//const修饰改变this指针的类型
//Date* const this -> const Date* const this
void Print() const; //const成员函数
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#include "head.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print() const //const成员函数
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//test.cpp
#include "head.h"
int main()
{
const Date d1(2023, 2, 28);
d1.Print(); //调用const成员函数 2023-2-28
return 0;
}
2.4.2 &操作符的重载
&操作符重载函数分为对普通对象取地址和对const对象取地址,一般不需要用户来实现。
演示代码2.3:
//如果用户不自定义,那么编译器会自动生成取地址运算符重载操作符
//取地址重载一般不会由用户自主实现
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&() //普通对象的取地址操作符运算重载
{
return this;
}
const Date* operator&() const //对于const对象的取地址运算符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 2, 27);
cout << &d1 << endl;
const Date d2(2023, 2, 28);
cout << &d2 << endl;
return 0;
}