文章目录
类与对象(中篇)
前言
在学习数据结构的时候,如果我们用面向过程的语言去实现也就是C语言,我们经常要对自定义类型的数据进行初始化和释放内存的操作,不论是对自定义类型的数据的初始化还是内存的释放使用起来都很繁琐,我们有时可能忘记对其初始化或内存释放,造成程序错误和内存泄漏等问题,C++就有办法去简化这部分操作,不用我们再手动的进行自定义类型的数据初始化和释放内存操作,实现这个过程靠的是C++类中默认成员函数。
1. 类的6个默认成员函数
默认成员函数就是即使你不去定义也会由编译器生成默认存在的函数,默认成员函数是类的重要组成部分。
即使是一个成员都没有写的空类,编译器也会自动为其生成6个默认成员函数。
默认成员函数,对实例化对象的成员变量进行操作。
2. 构造函数
2.1 概念
在前言中我们提到,C++默认成员函数能够自动对自定义类型的数据进行初始化操作,这个过程靠的就是构造函数。
#include <iostream>
using namespace std;
class Date
{
public:
Date()//构造函数
{
_year = 0;
_month = 0;
_day = 0;
}
//Date(int year = 0, int month = 0, int day = 0)//构造函数的错误重载,调用时与无参构造函数冲突
//{
// _year = year;
// _month = month;
// _day = day;
//}
Date(int year, int month, int day)//构造函数重载
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
//Date d1();//调用无参构造函数不能这么写,因为这样编译器无法识别它是函数的声明还是类对象实例化
Date d2(2023, 3, 3);
return 0;
}
通过上面代码的结果我们可以看出,在类对象实例化时,会根据参数自动调用构造函数,构造函数中实现了类对象的初始化,我们不用手动去调用,编译器自动会调用完成初始化。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参数能够调用的构造函数)。
2.3 默认生成构造函数的特性
前面提到了默认成员函数编译器自动会生成,如果我们写了任意一种构造函数(有参数或无参数),编译器就不会调用默认生成的构造函数,既然编译器会自动生成构造函数,我们还有必要自己去写构造函数吗?
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
通过上面这段代码的结果可以看出,默认生成的构造函数没有对类对象进行合理的初始化,
默认生成构造函数的特性:
- 内置数据类型不做处理(int/double/char…等类型)。 (有的编译器会擅自处理内置数据类型)
- 自定义类型的成员,会去调用它的默认构造函数(无参数/全缺省的/编译器自动生成的构造函数)。
注意:实际上只要我们不对自定义类型成员变量进行处理,编译器就会调用其默认构造函数,比如显式的写一个构造函数不处理自定义类型成员变量。
#include <iostream>
#include <errno.h>
using namespace std;
class Stack
{
public:
Stack(int n = 4)
{
cout << "Stack()" << endl;
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == nullptr)
{
perror("malloc fail");
}
_a = tmp;
_size = 0;
_capacity = n;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class Queue
{
public:
Queue()
{
cout << "Queue()" << endl;//显式写一个构造函数对自定义类型成员变量不做处理
}
private:
Stack popst;
Stack pushst;
};
int main()
{
Queue q;
return 0;
}
由于默认生成的构造函数的特性,需要对内置数据类型初始化时还是需要写构造函数,但是如果自定义类型的成员只是包含其他自定义类型就不需要自己写构造函数(被包含自定义类型的成员的构造函数已写出)。
class Stack
{
public:
Stack(int n = 4)//构造函数
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = n;
}
~Stack()//析构函数
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class Queue
{
//使用默认生成的构造函数
private:
Stack popst;
Stack pushst;
};
int main()
{
Queue q;
return 0;
}
可以看出自定义类型Queue默认生成的构造函数调用了自定义类型Stack的构造函数对其包含其他自定义类型Stack进行了初始化。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
缺省值不是初始化,只是在使用默认生成构造函数会被使用。
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
3. 析构函数
3.1 概念
有了构造函数对自定义类型数据进行初始化,当然就会有自动对自定义类型数据的清理的操作,这个函数不会清理掉实例化出的对象,因为没必要,实例化出的对象会在栈帧结束自动销毁,比如申请的空间需要销毁,这个操作就是又另一个默认成员函数,析构函数完成的。
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
class Stack
{
public:
Stack(int n = 4)//构造函数
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = n;
}
~Stack()//析构函数
{
cout << "~Stack()" << endl;//显示析构函数是否调用
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st;
return 0;
}
我们可以看出,没有显式调用析构函数,但是编译器自动调用了析构函数。
3.2 特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
3.3 默认生成析构函数的特性
前面提到了默认生成的构造函数对自定义类型不做处理,对于自定义类型会调用其默认构造函数,默认生成的析构函数也有类似的特性,
对于内置数据类型不做处理,对于自定义类型的成员自动调用其默认析构函数。
class Stack
{
public:
Stack(int n = 4)//构造函数
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = n;
}
~Stack()//析构函数
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class Queue
{
//使用默认生成的构造函数
//使用默认生成的析构函数
private:
Stack popst;
Stack pushst;
};
int main()
{
Queue q;
return 0;
}
默认生成析构函数的特性:
- 内置数据类型不做处理(int/double/char…等类型)。
- 自定义类型的成员,会去调用它的默认析构函数。
由于默认生成析构函数的特性,对于有资源申请的类,我们需要写出其析构函数,否则默认生成析构函数不能完成资源清理。
注意:实际上无论如何对于自定义类型成员变量,编译器都会调用其默认析构函数,比如显式的写一个析构函数不处理自定义类型成员变量。
#include <iostream>
#include <errno.h>
using namespace std;
class Stack
{
public:
Stack(int n = 4)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == nullptr)
{
perror("malloc fail");
}
_a = tmp;
_size = 0;
_capacity = n;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class Queue
{
public:
Queue()
{
}
~Queue()
{
cout << "~Queue()" << endl;
}
private:
Stack popst;
Stack pushst;
};
int main()
{
Queue q;
return 0;
}
甚至主动调用析构函数,编译器还是会去调用
#include <iostream>
#include <errno.h>
using namespace std;
class Stack
{
public:
Stack(int n = 4)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == nullptr)
{
perror("malloc fail");
}
_a = tmp;
_size = 0;
_capacity = n;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class Queue
{
public:
Queue()
{
}
~Queue()
{
popst.~Stack();
pushst.~Stack();
cout << "~Queue()" << endl;
}
private:
Stack popst;
Stack pushst;
};
int main()
{
Queue q;
return 0;
}
4. 拷贝构造函数
4.1 概念
对于内置数据类型,在创建变量时可以直接拷贝其他变量的数据,编译器可以自动完成,对于自定义类型数据,编译器会调用其拷贝构造函数。
**拷贝构造函数:**只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。是用一个对象来初始化另一个对象的成员函数
注: const 修饰会避免由于权限放大无法调用。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
~Date(){}//析构函数
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 3, 4);
d1.Print();
Date d2(d1);//创建类对象时使用拷贝构造 或者 Date d2 = d1;也是调用拷贝构造函数
d2.Print();
return 0;
}
可以看出,利用拷贝构造函数,也可以实现像内置数据类型一样,在创建变量时直接拷贝其他变量数据。
4.2 特性
- 拷贝构造函数是构造函数的一种重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
关于第二点特性:
由于是传值传参,d2调用拷贝构造函数初始化d1,调用拷贝构造函数需要传值传参,因此拷贝构造函数为了初始化形参又调用了拷贝构造函数,由于拷贝构造函数为了初始化形参又调用了拷贝构造函数,拷贝构造函数调用的拷贝构造函数又调用了拷贝构造函数,无穷递归。因此定义拷贝构造函数,形参必须是引用形式。
4.3 默认生成拷贝构造函数的特性
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
既然编译器能自己实现拷贝构造函数,还需要显式实现拷贝构造函数吗? 对于不同的自定义类型数据答案是不同的。
对下面的类型:
浅拷贝不会造成什么问题。
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
void Print();
~Date() {}//析构函数
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(d1);
d1.Print();
d2.Print();
return 0;
}
但如果下面这个类型:
class Stack
{
public:
Stack(int n = 4)//构造函数
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
_size = 0;
_capacity = n;
}
~Stack()//析构函数
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st1;
Stack st2;
st2 = st1;
return 0;
}
如果使用浅拷贝,就会有两个或两个以上的对象指向同一块空间,这样会导致,一个改变数据其他的也跟着改变,造成数据的混乱,还会造成同一块空间多次释放的问题。
因此,类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
涉及到资源申请,就要进行深拷贝,也就是我们要自己写拷贝构造函数,要使得每个对象都有独立资源空间,避免冲突。
拷贝构造函数典型调用场景:
-
使用已存在对象创建新对象
-
函数参数类型为类类型对象
-
函数返回值类型为类类型对象
默认生成拷贝构造函数的特性:
- 内置数据类型做浅拷贝(值拷贝)处理
- 自定义类型的成员,会去调用它的拷贝构造函数,如果没有拷贝构造函数也会进行浅拷贝。
5. 赋值运算符重载
前面讲了,如何让自定义类型自动初始化和清理资源还有如何拷贝构造,除此之外C++还提供了其他强大的功能,让自定义类型能够利用运算符,让自定义类型能够像内置类型一样进行比较,赋值等操作。该功能就是运算符重载。
5.1 运算符重载
5.1.1 概念
**C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,**也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表) (运算符有几个操作数就有几个参数,对于两个参数的运算符重载,第一个参数是左操作数,第二个参数是右操作数)
注意:
-
不能通过连接其他符号来创建新的操作符:比如operator@ 。
-
重载操作符必须有一个类类型参数 。
-
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义 。
-
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this 。
-
.* :: sizeof ?: . 注意以上5个运算符不能重载。
5.1.2 运算符重载实现
以下实现均已日期类举例
5.1.2.1 ==运算符的重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);
void Print();
~Date() {}//析构函数
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date::Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._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, 3, 4);
Date d2(2023, 3, 5);
//operator==(d1, d2) //由于运算符的重载是函数实现的,因此可以像调用函数一样调用
//d1 == d2; //会自动转化成operator==(d1, d2)
cout << operator==(d1, d2) << endl;
cout << (d1 == d2) << endl;
return 0;
}
上面的代码的运算符重载是在全局域实现的,全局域实现会产生一些问题,比如对象私有的成员变量无法访问,对于这个问题,可以在类内提供访问成员的函数,或者只能将成员变量变成公有,因此运算符的重载更适合在类内进行实现。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);
void Print();
~Date() {}//析构函数
bool operator==(const Date& d);//==运算符重载
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date::Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool Date::operator==(const Date& d)//************************==运算符重载************************
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2023, 3, 5);
cout << d1.operator==(d2) << endl;//类内实现调用的方式不同于在全局域实现
cout << (d1 == d2) << endl;//自动转化为 d1.operator==(d2)
//由于==的优先级比<<低因此要加括号
return 0;
}
前面我们提到了,运算符重载有几个操作数就要有几个参数,但是在类内实现时就不同了,因为有一个隐藏的参数就是this指针,因此==重载的类内实现的显式参数只有一个。
5.1.2.2 < 运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);
void Print();
~Date() {}//析构函数
bool operator==(const Date& d);//==运算符重载
bool operator<(const Date& d);// < 运算符重载
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date::Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool Date::operator==(const Date& d)//==运算符重载
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::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;
}
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2023, 3, 5);
cout << (d1 < d2) << endl;
return 0;
}
5.1.2.3 其他比较运算符实现
前面实现了==和<运算符的重载,其他比较运算符的重载只需要进行代码的复用就能够实现。
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);
void Print();
~Date() {}//析构函数
bool operator==(const Date& d);//==运算符重载
bool operator!=(const Date& d)//!=运算符重载
bool operator<(const Date& d);// < 运算符重载
bool operator<=(const Date& d);// <= 运算符重载
bool operator>(const Date& d);// > 运算符重载
bool operator>=(const Date& d);// >= 运算符重载
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date::Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool Date::operator==(const Date& d)//==运算符重载
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d)//!=运算符重载
{
return !(*this == d);
}
bool Date::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 Date::operator<=(const Date& d)// <= 运算符重载
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d)// > 运算符重载
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d)// >= 运算符重载
{
return !(*this < d);
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2023, 3, 5);
cout << (d1 == d2) << endl;
cout << (d1 < d2) << endl;
cout << (d1 <= d2) << endl;
cout << (d1 > d2) << endl;
cout << (d1 >= d2) << endl;
return 0;
}
5.1.2.4 前置++(–)和后置++(–))运算符实现
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
void Print();
Date operator+(int x);//日期加天数
Date& operator+=(int x);
Date& operator-=(int x);
Date& operator++();//前置++
Date operator++(int);//后置++
Date& operator--();//前置--
Date operator--(int);//后置--
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
assert(month > 0 && month < 13
&& day > 0 && day <= GetMonthDay(year, month));
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
int Date::GetMonthDay(int year, int month)//获得该月份天数
{
assert(month > 0 && month < 13);
int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return monthArray[month];
}
}
Date& Date::operator+=(int x)
{
if (x < 0)
{
*this -= -x;
return *this;
}
_day += x;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date& Date::operator++()//前置++
{
*this += 1;
return *this;
}
Date Date::operator++(int)//后置++
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator-=(int x)
{
if (x < 0)
{
*this += -x;
return *this;
}
_day -= x;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date& Date::operator--()//*****************************前置--
{
*this -= 1;
return *this;
}
Date Date::operator--(int)//**************后置--**************
{
Date tmp(*this);
*this -= 1;
return tmp;
}
C++规定实现后置++时参数里要写一个int,这个参数仅仅是为了和前置++形成重载。
5.1.2.5 输入输出重载
通过查看文档发现,C++中cout能够自动识别数据类型,也是得益于运算符重载。
因此,若想用cout方式输出自定义类型只需要对运算符进行重载。
但是我们不能对库函数里的运算符重载进行修改,因此只能尝试在自定义类型的类内实现。
void Date::operator<<(ostream& cout)
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
int main()
{
Date d1(2023, 3, 4);
d1.operator<<(cout);
d1 << cout;
return 0;
}
前面说过第一个参数是第一个操作数,第二个参数是第二个操作数,在类内实现的方式会导致使用时cout在对象后面。
由于在类内实现的效果不理想,因此只能在全局域实现。
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);//此处为友元函数,友元函数使得该函数能够访问类内数据
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return cout;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2000, 1, 1);
cout << d1 << d2;
return 0;
}
C++利用友元加运算符重载不仅很方便的实现了输出还能保护类内数据。
同样的cin能够自动识别数据类型也是由于运算符重载。
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return cout;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2000, 1, 1);
cin >> d1 >> d2;
cout << d1 << d2;
return 0;
}
输入和输出的重载这种短小的函数,可以考虑改成内联函数
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return cout;
}
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2000, 1, 1);
cin >> d1 >> d2;
cout << d1 << d2;
return 0;
}
需要注意的是内联函数声明和定义不能分离,如果分离是只能找到声明,无法通过符号表找到定义。
另外由于在类内定义的函数默认被当作内联函数,因此短小的函数可以考虑直接定义在类内。
5.2 赋值运算符重载
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);
void Print();
~Date() {}//析构函数
Date& operator=(const Date& d);// = 运算符重载
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date::Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& Date::operator=(const Date& d)//***************** = 运算符重载************************
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2023, 3, 5);
Date d3(2023, 8, 8);
d1 = d2 = d3;
return 0;
}
赋值运算符重载特性:
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
5.3默认生成赋值运算符的特性
赋值运算符也是成员函数之一,因此即使我们不显式的去写,编译器也会默认生成,和拷贝构造函数类似,对于内置类型进行浅拷贝,对于自定义类型,会调用其赋值运算符,如果未定义也会调用默认生成的赋值运算符,默认生成的赋值运算符会对对象进行浅拷贝操作。
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d);
void Print();
~Date() {}//析构函数
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
Date::Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1(2023, 3, 4);
Date d2(2023, 3, 5);
Date d3(2023, 8, 8);
d1 = d2 = d3;//使用默认生成的赋值运算符
d1.Print();
d2.Print();
d3.Print();
return 0;
}
默认生成赋值运算符特性:
- 对于内置类型,进行浅拷贝
- 对于自定义类型,调用该成员的赋值运算符,如果未定义也会调用默认生成的赋值运算符(浅拷贝)。
6. 四大成员函数默认生成特性总结
构造函数/析构函数:
- 对于内置类型,不进行处理
- 对于自定义类型成员,会去调用它的默认构造函数(无参数/全缺省的/编译器自动生成的构造函数)。
拷贝构造函数/赋值运算符:
- 对于内置类型,进行浅拷贝
- 对于自定义类型,调用该成员的拷贝构造函数/赋值运算符,(如果未定义也会调用默认生成也就是进行浅拷贝)。
7. const成员
定义:将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
const A aa;
aa.Print();
return 0;
}
为什么如上代码,编译器报错了呢?是因为this指针是A*const类型(this的指向不能改变),而aa定义的是const类型,传入print函数时权限被放大了。为了调用函数我们需要改变this的类型,但我们不能显式的改变this的类型,但C++规定我们可以使用如下方式this类型:
#include <iostream>
using namespace std;
class A
{
public:
void Print()const//将this指针改为const A* const类型
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
const A aa;
aa.Print();
return 0;
}
既然this可以被const修饰,对于不改变成员变量的函数,最好都要加上const修饰(如果声明和定义分离,声明和定义都要加),可以使得const对象和普通对象都能调用。
8. 取地址及const取地址操作符重载
相较于其他四个成员函数,这个两个成员函数就没有那么重要,这两个默认成员函数一般不用重新定义 ,编译器默认会生成。这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
#include <iostream>
using namespace std;
class A
{
public:
void Print()const
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
A aa;
const A bb;
cout << &aa << endl;
cout << &bb << endl;
return 0;
}
以上是编译器自动生成的取地址及const取地址操作符重载的使用演示。
如果我们要模拟实现也很简单:
#include <iostream>
using namespace std;
class A
{
public:
void Print()const
{
cout << _a << endl;
}
A* operator&()
{
return this;
}
const A* operator&()const
{
return this;
}
private:
int _a = 10;
};
int main()
{
A aa;
const A bb;
cout << &aa << endl;
cout << &bb << endl;
return 0;
}
9. 日期类完整实现
日期类完整的代码实现戳这里:Date/Date · 钱雪明/日常代码 - 码云 - 开源中国 (gitee.com)