目录
4. 拷贝构造函数 --- copy constructor
5.3.3. 赋值运算符只能重载成类的成员函数不能重载成全局函数
7.2. const成员函数 && 非const成员函数构成函数重载
7.3. 取地址操作符重载 && const取地址操作符重载
1. 类的6个默认成员函数
在C++语言中,如果一个类中没有任何成员, 我们称之为空类, 可是空类真的什么都没有吗?
答案: 并不是, 对于空类而言, 编译器会自动生成六个默认成员函数,分别是:
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载函数
- 取地址运算符重载函数
- const 取地址运算符重载函数
默认成员函数:是特殊的成员函数!如果用户没有显示实现,编译器会自动生成的成员函数我们称之为默认成员函数。
2. 构造函数 --- constructor
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证类对象的每个成员属性都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.1. 构造函数的特征
- 函数名与类名相同。
- 无返回值。
- 实例化对象时,编译器自动会调用对应的构造函数。
- 构造函数可以重载。
2.2. 构造函数的目的
构造函数的主要目的是在创建对象时对对象进行初始化。当定义一个对象并分配内存空间后,编译器会自动调用与对象类型匹配的构造函数,确保对象被正确初始化。构造函数的作用是给对象的成员变量赋予初始值,以确保对象在被创建后处于一个合法的状态。
换言之, 构造函数是在对象被实例化后的动作, 因此, 构造函数并不是开辟空间创造一个对象,而是初始化已经实例化后的对象。即构造函数不负责在内存中分配对象所需的空间,这是由对象的创建操作来完成的。
2.3. 调用构造的两种方式
类名 对象: 调用默认构造函数;
类名 对象(参数):调用与之匹配的构造函数;
如下:
std::string str1; // 类名 对象
std::string str2("haha"); // 类名 对象(参数)
2.4. 编译器默认生成的默认构造
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成 。
namespace Xq
{
template<typename T>
class volume
{
public:
volume(const T& length,const T& width,const T& height)
:_length(T())
, _width(T())
, _height(T())
{
_length = length;
_width = width;
_height = height;
}
void print_info()
{
std::cout << "length:" << _length << " width:" << _width << " height:" << _height << std::endl;
}
private:
T _length;
T _width;
T _height;
};
}
int main()
{
Xq::volume<int> V(1, 2, 3);
V.print_info();
return 0;
}
现象如下:
正常运行没有问题,但你告诉我编译器会默认生成一份,我们看看编译器默认生成的构造会干什么?
namespace Xq
{
template<typename T>
class volume
{
public:
void print_info()
{
std::cout << "length:" << _length << " width:" << _width << " height:" << _height << std::endl;
}
private:
T _length;
T _width;
T _height;
};
}
int main()
{
Xq::volume<int> V;
V.print_info();
return 0;
}
现象如下:
可以看到的现象:编译器默认生成的无参构造并没有对类的成员属性进行初始化。
那就奇怪了,那它到底做了什么呢?
编译器所生成的默认构造函数,在处理成员属性时,将成员属性的类型分为两种类型:
分别为自定义类型 (struct、class) 和 内置类型 (int、double、(int*)...) (注意任意类型的指针都是内置类型);
默认构造函数对内置类型不做处理,对自定义类型处理。
那么对自定义类型如何处理呢?
调用自定义类型自己的默认构造函数,如果此时自定义类型没有默认构造就会发生编译报错;
C++11 打了一个补丁 ,在调用成员方法时为成员属性给缺省值; 如下:
class volume
{
public:
volume(){}
void print_info()
{
std::cout << "length:" << _length << " width:" << _width << " height:" << _height << std::endl;
}
private:
T _length = 1; //但这里不是初始化,而是声明,给缺省值;
T _width = 1;
T _height = 1;
};
2.5. 默认构造的种类
默认构造函数有三类 (后两个默认构造是用户自己实现的):
- 编译器所生成的默认构造;
- 无参构造;
- 全缺省的构造函数;
2.6. 默认构造的特点
默认构造函数的特点是:不需要我们显式传递参数。
注意:
语法上无参和全缺省可以同时存在(可以构成函数重载),但如果有对象调用就会发生报错(存在二义性);
因此一般一个类里面只有一个默认构造函数,这个默认构造一般是全缺省的构造函数;一般在特殊的情况下会使用编译器生成的默认构造函数, 例如:
我们以前实现过,用两个栈结构实现队列,那么此时, 编译器所生产的默认构造就再合适不过了。
class queue
{
private:
Stack _s1;
Stack _s2;
};
此时这个类里面的所有成员属性都是自定义类型,因此编译器所生产的默认构造就会去调用 Stack 这个类的默认构造函数,即默认生成的默认构造就能符合此时我们的需求;
class queue
{
private:
Stack _s1;
Stack _s2;
size_t capacity = 4; //如果有内置类型,我们可以用C++11的补丁给缺省值;
};
构造函数 done;
3. 析构函数 --- destructor
3.1. 构造函数的特征
- 析构函数名:在类名前加上字符 ' ~ ' ;
- 无参数且无返回值,既然无参数,因此无法构成函数重载;
- 一个类只能有一个析构函数 。若未显式定义,操作系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译器自动调用该对象的析构函数。
3.2. 析构函数的目的
析构函数与构造函数功能相反,析构函数并不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。析构函数的功能:清理资源。当类实例化的对象生命周期结束时,自动调用,完成对象的资源清理工作;
- 再次强调:类实例化的对象在销毁时 (即生命周期结束) 会自动调用析构函数,完成对象的一些资源清理工作;
- 如果类实例化的对象需要资源清理,才需要自己实现析构函数;(例如动态开辟的资源)
- 如果类实例化的对象没有资源需要清理,不用自己实现析构,编译器默认生成的析构函数就可以满足需求。
3.3. 补充知识:
3.3.1. 生命周期 (lifecycle)
生命周期:生命周期指的是一个对象从创建到销毁的整个过程,即对象存在的时期。在 C++ 中,对象的生命周期由其所属的作用域和存储方式 (地址空间的位置) 决定。
- 自动变量(局部变量)的生命周期由其所在的作用域控制,当进程执行离开该作用域时,自动变量会被销毁。
- 静态变量和全局变量的生命周期会持续到进程的结束,在整个进程执行过程中都存在。
- 动态分配的对象(使用 new 运算符 或者 malloc 等函数)的生命周期由程序员显式控制,通过 delete 运算符 (free 函数) 来手动释放所分配的内存空间。
3.3.2. 作用域 (scope)
作用域:作用域指的是标识符(如变量、函数、类等)在程序中可见和有效的范围。在 C++ 中,通常有以下几种作用域:
- 块作用域(局部作用域):在大括号 {} 内声明的变量具有块作用域,只在该块内部有效。
- 文件作用域:在函数外部声明的变量具有文件作用域,在整个源文件内有效。
- 命名空间作用域:命名空间 (namespace) 中的标识符具有命名空间作用域,可以在不同的文件中访问和使用。
- 类作用域:类中声明的成员属性和成员方法具有类作用域,可以通过对象或类名来访问。
总的来说,生命周期描述了对象存在的时期;而作用域描述了标识符在程序中可见的范围;
3.4. 析构函数的特点
编译器默认生成的析构函数:对内置类型不处理,对自定义类型处理 (调用它的析构函数);
- 内置类型成员,销毁时不需要程序员进行显式资源清理,最后操作系统直接将其内存回收即可;
- 一般一个类里面如果有动态开辟的资源,则需要我们自己实现析构函数,例如我们自己实现的栈,队列,链表等等都需要我们自己实现析构,清理类的资源;
- 一般像体积这种类 (成员属性都是内置类型) ,或者类立面的成员属性都是自定义类型的类都不需要我们去实现特定的析构,编译器默认生成的默认析构就符合我们的需求;
像下面这个 Stack 类, 因为成员属性中有动态开辟的资源,因此它就需要程序员自己显式实现析构函数,进行资源清理,避免内存泄漏。
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
:_a(new T[4])
,_top(0)
,_capacity(capacity)
{}
~Stack(){
delete[] _array;
_array = nullptr;
_top = _capacity = 0;
}
private:
T* _array;
int _top;
int _capacity;
}
而像下面 volume 这种类 (成员属性都是内置类型)或者想 Queue 这种类 (成员属性都是自定义类型)并不需要程序员显示实现析构, 编译器默认生成析构的就足以满足需求。
class volume
{
public:
volume(){}
void print_info()
{
std::cout << "length:" << _length << " width:" << _width << " height:" << _height << std::endl;
}
private:
T _length;
T _width;
T _height;
};
//或者
class Queue
{
private:
Stack s1;
Stack s2;
}
//这两个类编译器默认生成的析构函数即可满足需求
3.5. 析构函数的顺序
析构的顺序是相反的,先实例化出来的对象后析构,符合LIFO;
而构造的顺序是 FIFO, 先实例化出来的对象先构造。
如下:
在C++里面,栈帧和里面的对象都要符合LIFO,即先实例化的对象后调用析构 (清理对象的资源),当析构函数调用完后,再销毁 (将空间还给OS) ;
对于全局对象,在符合先实例化后析构的同时并且与局部对象和静态对象相比,是最先构造,且最后析构的;
对于局部静态对象来说,在局部域符合先构造后析构,并且析构的时候与局部对象相比具有滞后性;
根据规则,构造 (FIFO),析构 (LIFO),并要考虑对象的作用域和生命周期;其中静态对象和全局对象都只会构造一次;
总结: 默认生成的析构对内置类型不处理,对自定义类型去调用它的析构函数;且析构顺序是:先实例化的对象,后调用析构函数。
4. 拷贝构造函数 --- copy constructor
拷贝构造函数:只有单个形参 ( this指针也是存在的,但在这里不讨论),该形参是对本类类型对象的引用 (一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.1. 拷贝构造的特点
- 拷贝构造是一个默认成员函数,并且拷贝构造函数是构造函数的一个重载形式;
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
为什么拷贝构造必须使用引用传参呢?
可以这样简单的理解:传值传参就是拷贝构造;调用拷贝构造,需要先传参数,而传值传参又是一个拷贝构造(导致无穷递归);
因此,传值传参会引发无穷递归, 故我们需要传引用。
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
:_a(new T[4])
,_top(0)
,_capacity(capacity)
{}
//Stack(Stack copy) //error,会引发无穷递归
Stack(const Stack& copy) //true
{
//...
}
~Stack(){
delete[] _array;
_array = nullptr;
_top = _capacity = 0;
}
private:
T* _array;
int _top;
int _capacity;
}
int main()}
{
Stack St1;
Stack St2(St1); //第一种调用拷贝构造的方式
Stack St3 = St1; //第二种调用拷贝构造的方式
return 0;
}
若我们没有显式定义一个类的 copy constructor ,编译器会生成默认的拷贝构造函数。
默认生成拷贝构造:对于内置类型和自定义类型都会处理;
- 对于内置类型会按字节序方式进行拷贝 (浅拷贝) 也叫做 (值拷贝) ;
- 对于自定义类型成员也会处理,会去调用它的拷贝构造。
浅拷贝可能会导致问题:
- 其中一个对象修改,会影响到其他的对象,这显然是不合法的 (一般情况下);
- 同一空间被析构两次,非法访问 (第二次析构时),导致进程崩溃, 这是最重要的一个原因;
如下:
可以看到进程直接挂掉了;因此对于这种类来说,编译器默认生成的拷贝构造是不符合要求的,需要我们自己实现对应的 copy constructor (用深拷贝的方式实现);
下面的现象,验证了传值传参会进行拷贝构造。
当我们进行引用传参时,此时就不会调用拷贝构造了,因为引用是对象的别名,即它自身。
我们发现,一个自定义类型,如果是传值传参,那么就会多一次拷贝构造。因此:
- 如果是深拷贝的话,代价是很大的,因此我们以后传递自定义类型对象的时候,建议用传引用,可以减少拷贝,提高效率;
- 对于内置类型,传引用和传值在效率上没有很大区别;
不仅是传值传参会调用拷贝构造, 传值返回也会调用拷贝构造,现象如下:
传值返回生成了一个临时对象,因此会调用copy constructor,作为这个函数的返回值,并且生命周期只有这一行;
那么引用返回会生成临时对象吗? 现象如下:
如果需要返回一个对象,并且这个对象出了函数作用域未被销毁,那么就可以使用引用返回,减少了拷贝,提高效率;
如果出了函数作用域,对象就被销毁了,那么必须用传值返回;
4.2. 拷贝构造的总结
总结:拷贝构造我们不写,编译器会生成默认拷贝构造,其对内置类型成员和自定义类型成员变量都会处理;虽然都会处理,但是有些情况下,是不符合需求的,需要我们自己实现特定的拷贝构造用来满足需求;
5. 赋值运算符重载
5.1. 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表) ;例如: bool operator==()
- 内置类型可以直接使用运算符运算,编译器知道如何运算;
- 自定义类型无法直接使用运算符运算,编译器也不知道如何运算,如果我们想让它支持,则需要实现运算符重载;
- 不能通过连接其他符号来创建新的操作符:比如operator@,必须是运算符。
- 重载操作符必须有一个类类型参数;
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义;
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的 this 指针;
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现;
5.2. 运算符重载 demo
demo如下:
class Date
{
public:
// d1 == d2;
bool operator==(const Date& d) //第一个参数为隐藏的this指针
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// d1 != d2;
bool operator!=(const Date& d) //第一个参数为隐藏的this指针
{
return !operator==(d);
}
private:
int _year;
int _month;
int _day;
};
5.3. 赋值运算符重载 --- 默认成员函数
5.3.1. 赋值运算符重载格式:
- 参数类型:const T&,传递引用可以提高传参效率; 但这里并不是一定的,也有场景适合使用传值传参。
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
5.3.2. 赋值运算符重载的细节
- 检测是否自己给自己赋值, 一般都使用地址检测 (&对象 和 this指针);
- 返回 *this :要复合连续赋值的含义。
实例如下:
//Date d1 = d2;
Date& operator=(const Date& copy)
{
if (this != ©) //判断是否给自己赋值
{
_year = copy._year; //赋值逻辑
_month = copy._month;
_day = copy._day;
}
return *this; //可以连续赋值
}
5.3.3. 赋值运算符只能重载成类的成员函数不能重载成全局函数
- 赋值运算符重载是一个默认成员函数,如果用户没有显示定义,那么编译器就会生成一个默认的赋值运算符重载函数。
- 此时如果我们在类外实现一个全局的赋值运算符重载,那么就会和编译器在类中的默认赋值运算符重载冲突,因此这个赋值运算符重载值必须是类的成员函数;
5.3.4. 赋值运算符重载的行为
测试demo如下:
class my_queue
{
public:
//...
private:
Stack s1;
Stack s2;
};
//或者
class Date
{
public:
//...
private:
int _year;
int _month;
int _day;
}
- 编译器默认生成的赋值运算符重载函数是按照字节序的方式进行拷贝 (浅拷贝) 的(内置类型直接赋值,自定义类型会去调用这个类的赋值运算符重载完成赋值);
- 对于日期类 (成员属性是内置类型) 和 my_queue(成员属性全是自定义类型) 这种类,我们不需要自己实现赋值运算符重载,编译器默认生成的就可以满足需求;
- 但如果对于像 Stack(具体如下) 这种类我们就必须要自己实现赋值运算符重载,因为如果此时我们不实现,那么编译器默认生成的赋值运算符重载就会带来不可忍受的问题:
这两个对象是同一块空间,任何一个对象发生修改,另一个也会随之改变;
在这两个对象生命周期结束时,会调用析构函数,同一空间被析构两次,进程crash;
template<typename T>
class Stack
{
public:
//...
private:
T* _a;
int _top;
int _capacity;
};
5.3.5. 赋值运算符重载的总结
- 如果一个类,用户不显式定义赋值运算符重载,那么编译器就会默认生成。
- 默认生成的赋值运算符重载对内置类型 (以字节序的方式) 和自定义类型 (调用自定义类型的赋值运算符重载) 都会处理;
- 虽然都会处理,但有些情况下 (有动态开辟的资源),用户需要实现深拷贝的赋值运算符重载。
下面是一些需要自定义赋值运算符重载的常见情况:
动态内存分配:如果一个类中有自己动态分配的内存(使用 new 运算符或者 malloc 等函数),则需要自定义赋值运算符重载,以确保正确释放原有的资源并避免内存泄漏。
资源所有权管理:如果类中包含了独占的资源(如文件句柄、网络连接等),则需要自定义赋值运算符重载,以确保资源在被赋值后被正确地管理和释放。
深拷贝需求:如果类中包含指向其他对象或资源的指针,并且需要进行深度拷贝以防止对象间的互相影响,那么则需要我们自己定义赋值运算符重载;
5.4. 其他的一些运算符重载
对于自定义类型 前置++ 和 后置++ 的运算符重载,会用占位参数区分,这个参数类型为 int,增加占位参数,本质上是为了可以构成函数重载。、
前置++:Date& operator++();
后置++:Date operator++(int);// 多了一个占位参数,这个参数是由编译器为我们传递的;
demo 如下:
Date(const Date& copy)
{
std::cout << "Date(const Date& copy)" << std::endl;
}
Date& operator++() //前置++
{
return *this;
}
Date operator++(int) //后置++,多了一个占位参数
{
Date copy(*this);
return copy;
}
int main()
{
{
// 前置++
Date d1;
++d1;
}
std::cout << "---------------" << std::endl;
// 后置++
Date d2;
d2++;
}
现象如下:
可以看到,后置++多了一次拷贝,所以在以后对于自定义类型需要 ++ 操作时,在符合需求的前提下尽量选择前置++,因为这样会减少一次拷贝,当大量使用时也就意味着可以减少拷贝,提高效率。
6. cout && << 的了解
在C++里面,我们打印东西借用的是cout;
那么这个 cout 是什么呢?
在C++中,cout 是一个对象,它是
std::ostream
类的一个实例。cout 是用于标准输出的对象,可以通过它来向控制台写入数据。
既然 cout 是一个对象,这个 cout 就可以实现运算符重载;
<< 在这里叫做流插入操作符, 在这里 cout 通过对 << 实现运算符重载,可以将要输出的数据插入到输出流中;
然后再通过函数重载让cout << 可以处理多种内置类型数据,这也就是为什么cout可以自动识别内置类型, 本质就是运算符重载和函数重载。
文档内容如下:
继续看下面的代码:
int i = 10;
double d = 11.1;
cout << i << endl; //这里相当于 cout.operator<<(i)
cout << d << endl; //这里相当于 cout.operator<<(d)
cout << 可以识别不同的内置类型, 本质上是通过函数重载和运算符重载实现的,那么 cout << 可以识别自定义类型吗?
可以发现此时的 << 是不支持我们将 Date类型的数据插入到输出流当中;
那么如何实现才能支持呢?
因此我们需要实现这个类自己的流插入运算符重载,即 Date::operator<<
实现如下:
void operator<<(std::ostream& out)
{
out << "Date:" << _year << "/" << _month << "/" << _day << std::endl;
}
int main()
{
Xq::Date d(2023, 7, 17);
cout << d;
return 0;
}
但是此时编译器却报错了, 现象如下:
原因如下:
- 因为 operator<< 这个运算符重载函数是一个非静态的成员函数。
- 因此编译器默认会将该对象的地址隐式传递给这个非静态的成员函数,即此时这个运算符重载函数第一个参数是 this 指针,
- 因此此时这个运算符重载函数实际上是这样的:operator<<(Date* const ptr,std::ostream& out)
- 那么既然这样,是不是 d << cout就可以正常运行?
测试如下:
void operator<<(std::ostream& out)
{
out << "Date:" << _year << "/" << _month << "/" << _day << std::endl;
}
int main()
{
Xq::Date d(2023, 7, 17);
d << cout;
return 0;
}
可以发现,的确可以正常运行,但是这是不是有点奇怪啊,因为我们以前使用 cout 的时候,都是按照下面的方式使用的啊;
int i = 0;
float f = 1.1;
char c = 'a';
cout << i ;
cout << f ;
cout << c ;
我们也知道出现这种情况的原因就是因为它是一个非静态的成员函数,其第一个参数都是隐式的 this,且不可改变;
那么我们想让它以 cout << d 这种方式运行的话, 我们其他的不知道,但有一点我们能确定的就是它绝对不可以是一个非静态的成员函数;
因此我们的解决方式是:
将这个非静态的成员函数写在类外,成为一个全局函数;
但这又有问题了,因为是非成员函数,受到访问限定符的限制,不能访问私有成员属性;
那我们还需要写一个获取私有成员属性的成员函数;
或者将这个非成员函数设置为友元函数;
6.1. 获取私有成员属性的方式一
class Date{
public:
//...
int get_year() const
{
return _year;
}
int get_month() const
{
return _month;
}
int get_day() const
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
void operator<<(std::ostream& out, const Xq::Date& d)
{
out << "Date:" << d.get_year() << "/" << d.get_month() << "/" << d.get_day() << std::endl;
}
int main()
{
Xq::Date d(2023, 7, 17);
cout << d;
return 0;
}
现象如下:
6.2. 获取私有成员属性的方式二
将这个非成员函数设置为友元函数,虽然更方便了,但是破坏了类的封装性。
class Date{
public:
//...
friend void operator<<(std::ostream& out, const Xq::Date& d); //友元函数在类里面声明
private:
int _year;
int _month;
int _day;
};
void Xq::operator<<(std::ostream& out, const Xq::Date& d) //在类外定义
{
//友元函数可以访问这个类实例化后的对象的私有属性
out << "Date:" << d._year << "/" << d._month << "/" << d._day << std::endl;
}
int main()
{
Xq::Date d(2023, 7, 17);
cout << d;
return 0;
}
现象如下:
运行成功,没问题;但是以前我们使用cout的时候会这样使用,如下:
int i = 10;
float f = 11.1;
cout << i << f << endl; //从左向右执行
现象如下:
那么我们实现的运算符重载可以向上面那样连续访问吗?现象如下:
可以发现,编译报错了,为什么?
原因就是这个operator<< 是需要有返回值的,这样才可以做到连续赋值;
更改如下:
class Date{
public:
//...
friend std::ostream& operator<<(std::ostream& out, const Xq::Date& d); //友元函数在类里面声明
private:
int _year;
int _month;
int _day;
};
//通过返回值做到向标准输出连续插入数据
std::ostream& Xq::operator<<(std::ostream& out, const Xq::Date& d) //在类外定义
{
//友元函数可以访问这个类实例化后的对象的私有成员
out << "Date:" << d._year << "/" << d._month << "/" << d._day << std::endl;
return out;
}
int main()
{
Xq::Date d1(2023, 7, 17);
Xq::Date d2(2023, 7, 18);
cout << d1 << d2 << endl;
return 0;
}
现象如下:
考虑到这个运算符重载是会被频繁调用的,且这个运算符重载函数是非递归且编译后的指令较少,我们可以考虑使用 inline,减少编译器频繁建立函数栈帧,提高效率;
但是,虽然我们这样做了, 但由于 inline 只是一种建议, 具体实不实施,由编译器决定。
class Date{
public:
//...
friend std::ostream& operator<<(std::ostream& out, const Xq::Date& d); //友元函数在类里面声明
private:
int _year;
int _month;
int _day;
};
//通过返回值做到向标准输出连续插入数据
inline std::ostream& Xq::operator<<(std::ostream& out, const Xq::Date& d) //在类外定义
{
//友元函数可以访问这个类实例化后的对象的私有成员
out << "Date:" << d._year << "/" << d._month << "/" << d._day << std::endl;
return out;
}
int main()
{
Xq::Date d1(2023, 7, 17);
Xq::Date d2(2023, 7, 18);
cout << d1 << d2 << endl;
return 0;
}
现象如下:
既然实现了这个流插入,我们也可以重载一下流提取操作符;
与流插入同理,实现如下:
class Date{
public:
//...
friend std::istream& operator>>(std::istream& in, Xq::Date& d); //友元函数在类里面声明
private:
int _year;
int _month;
int _day;
};
//通过返回值支持连续向标准输入插入数据
inline std::istream& Xq::operator>>(std::istream& in, Xq::Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Xq::Date d;
cin >> d;
cout << d;
return 0;
}
现象如下:
实现方式与标准输出没有太大差别;
最后为了严谨性,在输入数据需要检查一下日期的合法性:
class Date{
public:
void inspect_the_date() //检查日期的正确性
{
static int month_day[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))
{
++month_day[2];
}
else
{
month_day[2] = 28;
}
assert(_year >= 1);
assert(_month >= 1 && _month <= 12);
assert(_day > 0 && _day <= month_day[_month]);
}
//...
friend std::istream& operator>>(std::istream& in, Xq::Date& d); //友元函数在类里面声明
private:
int _year;
int _month;
int _day;
};
//通过返回值支持连续向标准输入插入数据
inline std::istream& Xq::operator>>(std::istream& in, Xq::Date& d)
{
in >> d._year >> d._month >> d._day;
d.inspect_the_date();
return in;
}
int main()
{
Xq::Date d;
cin >> d;
cout << d;
return 0;
}
7. const 成员函数
首先看下面的代码:
这没什么好说的,一个打印函数;
但是,如果是下面这种情况呢?
因为此时这个成员函数的 this 指针默认是 my_class* const this;
这个 const 修饰的是指针本身不可修改,其内容是没有被限制的;
然而当我们传递了一个 const 对象,意味着这个 this 指针是 const my_class* const this; 调用成员函数时,参数不匹配,权限被放大了,故编译报错;
7.1. const (常量) 成员函数
因此我们可以进行如下变更:
- const 位于函数声明的末尾,这表示 print_info() 是一个常量成员函数。
- 当用户将一个成员函数声明为 const 时,它会告诉编译器该函数不会修改对象的任何成员变量。
- 在常量成员函数中,不能修改成员变量的值,也不能调用不是 const 的成员函数。
- 常量成员函数, 可以被const对象和非const对象同时调用。
我们也可以这样理解,这个 const 修饰的是 this 指向的内容,也就是保证了成员函数内部不会修改成员属性,const对象和非const对象都可以调用这个成员函数, 因此我们对于非静态的成员函数,在符合需求的前提下,尽量写成常量成员函数;
7.2. const成员函数 && 非const成员函数构成函数重载
在这里再提一下,有些情况下可能我们要把 非const成员函数 和 const成员函数都要提供;
例如:
namespace Xq
{
template<class T>
class my_class
{
public:
my_class(T a = T())
:_a(a)
{}
//这两个函数是构成函数重载的,参数类型不一样,可以构成函数重载;
//参数为 my_class* const this
void print_info()
{
cout << "_a : " << _a << endl;
}
//参数为 const my_class* const this
void print_info() const
{
cout << "const _a : " << _a << endl;
}
private:
T _a;
};
}
在这里强调一下, const成员函数和非const成员函数是构成函数重载的。为什么呢?
因为这两个成员函数的 this指针类型不一样,即参数类型不一样,构成函数重载。
我们以 class A举例,如下:
- 非const成员函数的 this 指针类型: A* const;
- const 成员函数的 this 指针类型: const A* const。
7.3. 取地址操作符重载 && const取地址操作符重载
这两个函数很好理解, 直接上代码了:
namespace Xq
{
template<class T>
class my_class
{
public:
//这两个函数都是默认的成员函数,我们不写,编译器就会默认生成
//取地址运算符重载
my_class* operator&() //参数为 my_class* const this
{
return this;
}
//const取地址运算符重载
const my_class* operator&() const //参数为 const my_class* const this
{
return this;
}
// 这两个函数构成函数重载;
private:
T _a;
};
}
int main()
{
return 0;
}
上面再强调一点,函数重载的定义: 在同一作用域,函数的参数 (个数、顺序、类型) 不一致 ,才可以构成函数重载, 而函数的返回值无法构成函数重载。
因此,上面这两个函数之所以可以构成重载, 是因为它们的 this指针类型不一致。
同时,这两个函数我们显式定义,编译器会默认生成。
一般情况下,编译器默认生成就足以满足需求,一般没必要显式定义。
除了一些极为特殊的情况,例如:
特殊场景 :不想让别人取到对象的地址,那么可以显示返回 nullptr,具体如下:
namespace Xq
{
template<class T>
class my_class
{
public:
//极为特殊的情况下需要我们自己实现这两个函数
//如果我们不想让用户得到实例化的对象的地址,我们就可以这样操作;
my_class* operator&() //参数为 my_class* const this
{
return nullptr;
}
//const取地址运算符重载
const my_class* operator&() const //参数为 const my_class* const this
{
return nullptr;
}
private:
T _a;
};
}
int main()
{
return 0;
}
总结:编译器默认生成的取地址运算符重载函数 和 const取地址运算符重载函数就可以基本满足我们的需求,因此这两个函数我们很少自己实现;
8. 日期类 demo
最后,为了体现上面我们说的知识,在这里用一个实例说明我们的默认成员函数:
#include <iostream>
#include <assert.h>
using std::cout;
using std::endl;
using std::cin;
namespace Xq
{
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1) //全缺省的默认构造
:_year(year)
, _month(month)
, _day(day)
{
inspect_the_date();
}
//Date d1(d2)
Date(const Date& copy) // copy constructor
:_year(copy._year)
, _month(copy._month)
, _day(copy._day)
{
inspect_the_date();
}
~Date(){} const // destructor
//Date d1 = d2;
Date& operator=(const Date& copy) //赋值运算符重载
{
if (this != ©)
{
_year = copy._year;
_month = copy._month;
_day = copy._day;
}
return *this;
}
void inspect_the_date() const //检查日期的正确性
{
static int month_day[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))
{
++month_day[2];
}
else
{
month_day[2] = 28;
}
assert(_year >= 1);
assert(_month >= 1 && _month <= 12);
assert(_day > 0 && _day <= month_day[_month]);
}
// d1 == d2;
bool operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// d1 != d2;
bool operator!=(const Date& d) const
{
return !operator==(d);
}
static int get_month_day(const Date& d) //获取当前月份的天数
{
static int month_arr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (d._month == 2 && (d._year % 4 == 0 && d._year % 100 != 0 || d._year % 400 == 0))
++month_arr[2];
else
month_arr[2] = 28;
return month_arr[d._month];
}
static const char* get_week_day(const Date& d) //获取当天是星期几
{
static const char* week_day[8] = {
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
"星期日",
nullptr
};
Date sign(1901, 1, 1); // 标志日期 这一天是星期二
if (d == sign)
return week_day[1];
else
{
int count = 0;
if (d != sign)
{
count += d.arrive_special_date_day(sign);
if (count < 0)
count *= -1;
}
count %= 7;
if (count == 6)
return week_day[0];
else
return week_day[count + 1];
}
}
int arrive_special_date_day(const Date& d) const //两个日期相差的天数
{
int count = 0;
//Date copy(*this);
Date copy = *this; //这两种方式都是拷贝构造,因为这是实例化一个未存在的对象
if (copy < d)
{
while (copy < d)
{
++copy;
++count;
}
}
else
{
while (copy > d)
{
--copy;
++count;
}
count *= -1;
}
return count;
}
bool operator>(const Date& d) const
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
return true;
else
return false;
}
bool operator>=(const Date& d) const
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day >= d._day))
return true;
else
return false;
}
bool operator<(const Date& d) const
{
return !(*this >= d);
}
bool operator<=(const Date& d) const
{
return !(*this > d);
}
Date operator+(int day) const
{
if (day < 0)
{
return *this - (-day);
}
Date copy(*this);
while (day + copy._day > get_month_day(copy))
{
day -= (get_month_day(copy) - copy._day + 1);
copy._day = 1;
++copy._month;
if (copy._month > 12)
{
copy._month = 1;
++copy._year;
}
}
if (day > 0)
{
copy._day += day;
}
return copy;
}
Date& operator+=(int day)
{
if (day < 0)
{
return *this -= (-day);
}
while (day + _day > get_month_day(*this))
{
day -= (get_month_day(*this) - _day + 1);
_day = 1;
++(*this)._month;
if (_month > 12)
{
_month = 1;
++_year;
}
}
if (day > 0)
{
_day += day;
}
return *this;
}
Date& operator++() //前置++
{
_day++;
if (_day > get_month_day(*this))
{
_day = 1;
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date operator++(int) //后置++,多了一个占位参数
{
Date copy(*this);
copy._day++;
if (copy._day > get_month_day(copy))
{
copy._day = 1;
++copy._month;
if (copy._month == 13)
copy._month = 1;
}
return copy;
}
Date& operator--() // 前置--
{
--_day;
if (_day == 0)
{
if (_month == 1)
{
--_year;
_month = 13;
}
--_month;
_day = get_month_day(*this);
}
return *this;
}
Date operator--(int) //后置--,多了一个占位参数
{
Date copy(*this);
--copy._day;
if (copy._day == 0)
{
if (copy._month == 1)
{
--copy._year;
copy._month = 13;
}
--copy._month;
copy._day = get_month_day(copy);
}
return copy;
}
Date& operator-=(int day)
{
if (day < 0)
{
return (*this) += (-day);
}
_day -= day;
while (_day <= 0)
{
if (_month == 1)
{
--_year;
_month = 13;
}
--_month;
_day += get_month_day(*this);
}
return *this;
}
Date operator-(int day) const
{
if (day < 0)
{
return (*this) + (-day);
}
Date copy(*this);
copy._day -= day;
while (copy._day <= 0)
{
if (copy._month == 1)
{
--copy._year;
copy._month = 13;
}
--copy._month;
copy._day += get_month_day(copy);
}
return copy;
}
void PrintDate() const
{
std::cout << "Date:" << _year << "/" << _month << "/" << _day << std::endl;
cout << (*this).get_week_day(*this) << endl;;
}
int get_year() const
{
return _year;
}
int get_month() const
{
return _month;
}
int get_day() const
{
return _day;
}
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
//友元函数
friend std::ostream& operator<<(std::ostream& out, const Xq::Date& d);
friend std::istream& operator>>(std::istream& in, Xq::Date& d);
private:
int _year;
int _month;
int _day;
};
}
//支持连续向标准输出插入数据
inline std::ostream& Xq::operator<<(std::ostream& out, const Xq::Date& d)
{
out << "Date:" << d._year << "/" << d._month << "/" << d._day << std::endl;
cout << d.get_week_day(d) << endl;
return out;
}
//支持连续向标准输入插入数据
inline std::istream& Xq::operator>>(std::istream& in, Xq::Date& d)
{
in >> d._year >> d._month >> d._day;
d.inspect_the_date();
return in;
}
int main()
{
return 0;
}
OK,类和对象中到此结束了;