本篇是类和对象的第二站🚌

主要讲述类的几个默认成员函数,以及操作符重载

本篇博客会涉及到很多之前C++专栏里面提到的知识点,建议连起来观看

感谢你关注慕雪,欢迎来我的寒舍坐坐 ❄慕雪的寒舍



文章目录
  • 默认成员函数
  • 1.构造函数
  • 1.1特性
  • 1.2基本使用
  • 1.3编译器默认生成的构造函数
  • 1.4初始化列表
  • 1.5 explicit关键字
  • 1.6规范命名类的成员变量
  • 2.析构函数
  • 2.1特性
  • 2.2基本使用
  • 3.拷贝构造
  • 3.1特性和使用
  • 3.2外置类型拷贝问题
  • 3.3深拷贝
  • 3.3.1new和delete
  • 3.3.2深拷贝实现
  • 3.3.3深拷贝效果
  • 4.运算符重载
  • 4.1定义
  • 4.2基本使用
  • 4.3赋值运算符重载
  • 4.4拷贝构造和赋值重载的调用问题
  • 5.const成员
  • 5.1用const修饰类的成员函数
  • ①实例-权限问题
  • ②什么时候需要使用?
  • ③出错提醒
  • 5.2取地址及对const取地址重载
  • 日期类的实现
  • 特殊:对<<和>>的重载
  • ①简单了解io
  • ②实现
  • ③疑惑解答
  • 结语


默认成员函数

当我们创建一个类的时候,即便类里面啥都不放,都会自动生成下面6个默认成员函数

【C++】类和对象2:默认成员函数+操作符重载_c++

它们都有啥功能呢?且听我一一道来

1.构造函数

众所周周知,当我们写C语言的顺序表、链表等代码的时候,一般都会写一个Init函数来初始化内容。

void Init()
{
	a=(int*)malloc(sizeof(int)*4);
	size=0;
	capa=4;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

但是这样有一个缺点,就是不够智能,需要我们自己来调用它进行初始化。

于是C++就整出来了一个构造函数来解决这个问题

1.1特性

构造函数:名字和类名相同,创建类对象的时候编译器会自动调用,初始化类中成员变量,使其有一个合适的初始值。构造函数在对象的生命周期中只调用一次

构造函数有下面几个特性:

  1. 函数名和类名相同
  2. 无返回值
  3. 构造函数可以重载
  4. 对象实例化的时候,编译器会自动调用对应的构造函数
  5. 如果你自己不写构造函数,编译器会自己创建一个默认的构造函数

1.2基本使用

下面用一个队列来演示一下构造函数

class Queue{
public:
    Queue()
    {
        cout<<"Queue Init"<<endl;//测试是否调用
        _a=(int*)malloc(sizeof(int)*4);
        _size=0;
        _capa=4;
    }
    void Print()
    {
        cout<<this<<": ";
        cout<<"size: "<<_size<<" ";
        cout<<"capa: "<<_capa<<endl;
    }
private:
    int* _a;
    int _size;
    int _capa;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

可以看到,在创建对象q1的时候,编译器就自动调用了类中的构造函数,帮我们初始化了这个队列

【C++】类和对象2:默认成员函数+操作符重载_拷贝构造_02


除了上面这种最基本的无参构造函数以外,一般写构造函数的时候,我们都会带一个有缺省值的参数,这样可以更好地灵活使用这个队列

Queue(int Capacity=4)
{
    _a=(int*)malloc(sizeof(int)*Capacity);
    _size=0;
    _capa=Capacity;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

调用这种构造函数也更加灵活,我们可以根据数据类型的长度,来创建不同容量的队列,避免多次realloc造成的内存碎片

Queue q1;//调用无参的构造函数
Queue q2(100);//调用带参的构造函数
  • 1.
  • 2.

多种构造函数是可以同时存在的,不过!它们需要满足函数重载的基本要求

当你调用一个无参的函数,和一个全缺省的函数的时候,编译器会懵逼!

Queue();
Queue(int Capacity=4);
//这两个函数不构成重载,会报错
  • 1.
  • 2.
  • 3.

正确的重载应该是下面的情况

Queue();
Queue(int Capacity);
  • 1.
  • 2.

编译器在创建对象的时候,就会智能选择这两个构造函数其中之一进行调用。但是同一个对象只会调用一个构造函数

1.3编译器默认生成的构造函数

上面提到过,如果我们不写构造函数,编译器会自己生成一个。

但测试过以后,你会发现,这个默认生成的构造函数,好像啥事都没有干——或者说,它把_a _b _c 都初始化成了随机值!

【C++】类和对象2:默认成员函数+操作符重载_开发语言_03

实际上,编译器默认生成的构造函数是不会处理内置类型的

  • 内置类型:int、char、float、double……
  • 外置类型:自定义类型(其他的类)

在处理的时候,编译器忽略内置类型;外置类型会调用它的构造函数

class Date{
public:
    //默认构造函数:不传参就能调用的
    //1.全缺省 2.无参 3.编译器自动生成
    //可以是半缺省的,但是不实用
    Date(int year=2022,int month=2,int day=30)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    void Print()
    {
        cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
        _A.Print();
    }
private:
    //编译器会自动生成构造函数(如果你没有自己写的话)
    //自动生成的构造函数是不会初始化内置类型的
    //内置类型:int,char,double等等
    int _year;
    int _month;
    int _day;
    //外置类型:自定义类型
    //外置类型会调用它自己的默认构造函数
    Queue _A;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

可以看到,编译器调用了自己的构造函数的同时,还调用了外置类型Queue的构造函数,搞定了它的初始化

【C++】类和对象2:默认成员函数+操作符重载_开发语言_04

如果我们去掉Date的构造函数,就能看到下面的情况。Queue成功初始化,但是内置类型的年月日都是随机值

【C++】类和对象2:默认成员函数+操作符重载_c++_05

一般情况下一个C++类都需要自己写构造函数,下面这两个情况除外

  1. 类里面的成员都是自定义类型成员(且有自己的构造函数)
  2. 如果还有内置类型成员,声明时给了缺省值

注:只有类在声明变量的时候才可以给缺省值

//下面的情况就不需要写
class MyS{
    
private:
      Queue q1;//自定义类型
      Queue q2;
      int a=1;//内置类型声明的时候给了缺省值
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

【C++】类和对象2:默认成员函数+操作符重载_编译器_06


1.4初始化列表

除了上面的方式之外,还有一种构造函数的使用方式为初始化列表

Date(int year=2022,int month=2,int day=30)
    :_year(year),
	_month(month),
	_day(day)
{}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 每个成员变量只能在初始化列表中出现一次
  • 类中包含以下成员必须在初始化列表中进行初始化
  • 引用
  • const成员
  • 自定义类型成员

一般情况下,建议使用初始化列表进行初始化。因为对于自定义类型的成员变量,初始化列表的优先级是高于{ }里面的内容的。

这里还有非常重要的一点!

成员变量在类中声明的顺序就是初始化列表的顺序,而并非初始化列表自己的顺序!

  • 怎么理解呢?看下面这个代码
class Date{
public:
Date(int year=2022,int month=2,int day=30)
    :_day(day),
    _year(year),
	_month(month)
	
{}
private:
    int _year;
    int _month;
    int _day;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

即便我们把_day放在了初始化列表的首位,但由于它是在最后声明的。所以构造函数走初始化列表的时候,会依据声明顺序,依次初始化年、月、日。

  • 这会引起什么问题?再来看看一个错误示例
class Date{
public:
Date(int year=2022,int month=2,int day=30)
    :_day(day),
    _year(year),
	_month(_day)
{}
private:
    int _year;
    int _month;
    int _day;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

当我们用上面这个初始化列表的时候,我们本意是想在初始化完_day以后,将_day的值赋给_month。但由于_month的声明顺序在_day之前,所以_month(_day)会先执行,此时的_day尚为随机值,这就导致月份变成随机值了!

这只是一个示例,实际上肯定不会用天数初始化月数,范围不一样

最好的办法,就是声明顺序和初始化列表的顺序保持一致!


1.5 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有隐式类型转换的作用。

class Date
{
public:
    //正常的构造函数
    //Date(int year)
    //    :_year(year)
    //    {}
	
    explicit Date(int year)
        :_year(year)
        {}
private:
    int _year;
    int _month:
    int _day;
};
void TestDate()
{
    Date d1(2018);
    // 用一个整形变量给日期类型对象赋值
    // 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
    d1 = 2019;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

当我们调用赋值的时候,实际上编译器会先用2019构造出一个date类型对象,再调用赋值重载(这里还没有写)赋值给d1。这就是一个隐式类型转换

如果我们用explicit修饰了这个构造函数,那么编译器将不会进行此类隐式类型转换!

1.6规范命名类的成员变量

为了更好的使用构造函数,以及区分类内外的函数类型

一般我们定义类中的成员变量的时候,都会使用一个下划线进行标明_YEAR

在一些地方,你会看到函数名前面也带了一个_,这一般表明该函数是另外一个函数的子函数,同样是用于区分的。

不同人的代码风格不同,你可以选择你自己喜欢的风格,但不能影响我们程序的正常使用

比如下面这种情况,就会影响类的构造了

class Date{
public:
    Date(int year=2022)
    {
        year=year;
    }
private:
    int year;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

请问year=year里面的这个year,到底是成员变量,还是构造函数的传参呢?编译器又双懵逼了

实际上,编译器在找year的时候,会先在当前{ }中找,找到了传参的year,就不会去找其他地方的year了。所以这个语句实际上是传参过来的year自己给自己赋值,编译器会报错。


2.析构函数

和构造函数相对应,析构函数是对象在出了生命周期后自动调用的函数,用来爆破对象里的成员(如进行free操作)

生命周期是离这个对象最近的{ }括号

2.1特性

  • 析构函数名是在类名前加~
  • 无参数,无返回值
  • 一个类只能有一个析构函数
  • 如果你没有自己写,编译器会自动生成一个析构函数

和构造函数一样,编译器自己生成的析构函数不会处理内置类型;会调用外置类型的析构函数

2.2基本使用

析构函数的定义和我们在外部写的Destroy函数一样,主要执行free操作

#include<iostream>
#include<stdlib.h>
using namespace std;

class Queue{
public:
    Queue()
    {
        cout<<"Queue"<<endl;//测试是否调用
        _a=(int*)malloc(sizeof(int)*4);
        _size=0;
        _capa=4;
    }
    void Print()
    {
        cout<<this<<": ";
        cout<<"size: "<<_size<<" ";
        cout<<"capa: "<<_capa<<endl;
    }
    ~Queue()
    {
        //析构函数
        free(_a);
        _a=nullptr;
        _size=_capa=0;
        cout<<"distory:"<<this<<endl;//测试调用
    }
private:
    int* _a;
    int _size;
    int _capa;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

假设我们在main函数里面定义了两个对象,你能说出q1和q2谁先进行析构函数的调用吗?

【C++】类和对象2:默认成员函数+操作符重载_拷贝构造_07

可以看到,先调用的是q2的析构函数

【C++】类和对象2:默认成员函数+操作符重载_编译器_08

因为在底层操作中,编译器会给main函数开辟栈帧

栈遵从后进先出的原则,q2是后创建的,所以在析构的时候会先析构


3.拷贝构造

3.1特性和使用

拷贝构造是一个特殊的构造函数,它的参数是另外一个Date类型。在用已有的类类型对象来创建新对象的时候,由编译器自动调用

因为拷贝的时候我们不会修改d的内容,所以传的是const

另外,我们必须进行传引用调用!

这里补充说明一下,下面的这个函数,在传参的时候,编译器会去调用Date的拷贝构造

void func(Date d);

如果你没有写拷贝构造,或者拷贝构造里面不是传引用,编译器会就递归不断创建新的对象进行值拷贝构造,程序就死循环辣

//拷贝构造,如果不写的时候,编译器会默认生成一个
    //对内置类型进行值拷贝(浅拷贝)
    Date(const Date& d)
    {
        _year=d._year;
        _month=d._month;
        _day=d._day;
        //外置类型会调用外置类型的拷贝构造
        Queue b(_A);
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

和构造、析构不同的是,编译器自己生成的拷贝构造终于有点用了

  • 它会对内置类型进行按内存存储的字节序完成拷贝,这种称为值拷贝(又称浅拷贝
  • 对外置类型会调用它的构造函数

3.2外置类型拷贝问题

但是!如果你使用了外置类型,该类型中包含malloc的时候,编译器默认生成的构造函数就不能用辣!

因为这时候,编译器默认生成的拷贝构造会进行值拷贝,拷贝完了之后,就会出现q1和q2指向同一个空间的情况。修改q2会影响q1,free的时候多次释放同一个空间会报错,不符合我们的拷贝构造的要求

【C++】类和对象2:默认成员函数+操作符重载_开发语言_09

注意注意,malloc不行的原因是,数据是存在堆区里面,拷贝的时候,q2的_a得到的是一个地址,而不是拷贝了新的数据内容。

如果你在类里面定义了一个int arr[10]数组,这时候拷贝构造就相当于memcpy,是可以完成拷贝的工作的。

如何解决这个问题呢?我们需要使用深拷贝

这里我还没有学到那个地方,后续写深浅拷贝的博客的时候,再来填上这个坑

黑马16分钟视频速成完毕,前来填坑

3.3深拷贝

3.3.1new和delete

这里先给大家从C语言转到C++,讲解一下new和delete关键字,它们分别对应malloc和free

非常简单!比malloc的使用简单多了!

int main()
{
    int*p1=new int;//开辟一个int类型的空间
    int*p2=new int(10);//开辟一个int类型的空间,并初始化为10
    int*p3=new int[10];//开辟10个int类型的空间
    //注意后两个的括号区别!
    
    delete p1;//销毁p1指向的单个空间
    delete p2;//同上
    
    //delete p3;//销毁p3指向的第一个空间,不能用于数组
    delete[] p3;//销毁p3指向的数组
    
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

怎么样?是不是超级简单!

【C++】类和对象2:默认成员函数+操作符重载_c++_10


3.3.2深拷贝实现

在上面写道过,编译器会自动生成拷贝构造函数,完成值拷贝工作。但是队列的代码里面包含堆区的空间,需要我们正确释放。这时候就需要自己写一个拷贝构造完成深拷贝👇

//拷贝构造
Queue(const Queue& q)
{
    _a=new int[q._capa];//注意解引用
    memcpy(_a, q._a, q._capa*sizeof(int));//拷贝内容
    _size=q._size;
    _capa=q._capa;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

用下面这个队列的代码来测试深拷贝

#include<iostream>
#include<stdlib.h>
#include<string.h>
using namespace std;

class Queue{
public:
    Queue()
    {
        cout<<"Queue Init"<<endl;//测试是否调用
        //_a=(int*)malloc(sizeof(int)*4);
        _size=0;
        _capa=4;
        _a=new int[_capa];
    }
    //拷贝构造
    Queue(const Queue& q)
    {
        cout<<"Queue Copy"<<endl;
        _a=new int[q._capa];
        memcpy(_a, q._a, q._capa*sizeof(int));
        _size=q._size;
        _capa=q._capa;

    }
    void Set()
    {
        for (int i = 0; i < _capa; i++)
        {
            _a[i] = i + 1;
        }
    }
    void Print()
    {
        cout<<"this:"<<this<<" ";
        cout<<"_a:"<<_a<<" ";
        cout<<"size: "<<_size<<" ";
        cout<<"capa: "<<_capa<<endl;
        for(int i=0;i < _capa;i++)
        {
            cout<<_a[i]<<" ";
        }
        cout<<endl;
    }
    ~Queue()
    {
        //析构函数
        //free(_a);
        delete[] _a;
        _a=nullptr;
        _size=_capa=0;
        cout<<"distory:"<<this<<endl;
    }
private:
    int* _a;
    int _size;
    int _capa;
};

int main()
{ 
    Queue q1;
    q1.Set();
    q1.Print();
    cout<<endl;
    Queue q2=q1; 
    q2.Print();

    cout<<endl;
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
3.3.3深拷贝效果

先注释掉Queue的拷贝构造函数析构函数(不然会报错)

看一看,发现在不写拷贝构造函数的时候,q2和q1的_a指向了同一个地址

【C++】类和对象2:默认成员函数+操作符重载_c++_11

取消析构函数的注释,可以看到两次释放同一片空间,发生了报错

【C++】类和对象2:默认成员函数+操作符重载_编译器_12

如果我们把写好的深拷贝构造加上,就不会出现这个问题

【C++】类和对象2:默认成员函数+操作符重载_c++_13

当你加上给_a里面初始化一些数据,以及打印_a数据的函数后,就可以看到,不仅q2的_a有了自己全新的地址,其内部的值也和q1一样了

【C++】类和对象2:默认成员函数+操作符重载_开发语言_14

这样写出来的拷贝构造,即便把队列中的int* _a修改为char*或者其他类型,都能正确完成拷贝工作

【C++】类和对象2:默认成员函数+操作符重载_开发语言_15

这里有一个小点哈,就是打印char* _a的地址的时候,咱需要用printf而不是cout,因为cout会把_a直接当作字符串打印了,效果就变成了下面这样

【C++】类和对象2:默认成员函数+操作符重载_c++_16

用printf来控制输出格式为%x即可

printf("_a:%x ",_a);
  • 1.

4.运算符重载

4.1定义

在讲解赋值运算符重载之前,我们可以来认识一下完整的运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名为:关键字 operator运算符,如operator=

函数原型:返回值类型 operator操作符(参数列表),如Date operator=();

下面有几点注意:

  • 重载操作符必须有一个自定义类型的操作数(即操作符重载对内置类型无效)
  • 不能通过其他符号来创建新的操作符
  • 对于类类型的操作符重载,形参比操作数少一个传参(因为有一个默认的形参this指针)
  • 这5个操作符是不能重载的:.*::sizeof? :.

4.2基本使用

以下是在全局定义的操作符重载,用于判断日期是否相等

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year;
    && d1._month == d2._month
    && d1._day == d2._day;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

当我们在main函数中使用d1==d2的时候,编译器就会自动调用该操作符重载

当然,你也可以自己来传参使用,如if(operator==(d1,d2))

但是这样非常不方便,和调用一个而普通函数没啥区别,压根算不上操作符重载。所以我们一般是在类里面定义操作符重载的

【C++】类和对象2:默认成员函数+操作符重载_c++_17


当我们把它放入类Date中间,就需要修改成下面这样

bool operator==(const Date& d2)
{
    return _year == d2._year;
    && _month == d2._month
    && _day == d2._day;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

编译器在调用的时候,会优化成下面这样

bool operator==(Date* this, const Date& d2)
//显示调用为 d1.operator==(d2);
  • 1.
  • 2.

而在main里面使用的时候,这个重载后的操作符和原本的使用方法完全相同

Date d1(2022,6,1)
Date d2(2022,5,1)
d1==d2;//自动调用操作符重载d1.operator==(d2);
  • 1.
  • 2.
  • 3.

后续会以日期类为样板,实现更多的操作符重载


4.3赋值运算符重载

因为每一个类都有不同的成员,编译器不可能智能的进行赋值操作。这时候就需要我们自己写一个赋值运算符重载来进行赋值操作了

以日期类为例,赋值操作其实就是把内置类型成员一一赋值即可

Date& operator=(const Date& d){
    if(this != &d)//避免自己给自己赋值
    {
        _year=d._year;
        _month=d._month;
        _day=d._day;
    }
    return *this;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

编写赋值重载代码的时候,需要注意下面己点:

  • 返回值和参数类型(注意要引用传参,不然会调用拷贝构造)
  • 检测是否自己给自己赋值(避免浪费时间)
  • 因为返回的是*this,出了函数后没有销毁,所以可以用传引用返回
  • 一个类如果没有显式定义赋值运算符重载,编译器也会自己生成一个,完成对象按字节序的值拷贝。

如果类中有自定义类型,编译器会默认调用它的赋值运算符重载(这里也会涉及到深浅拷贝的问题,后面会在深浅拷贝的博客里详解)

4.4拷贝构造和赋值重载的调用问题

当赋值操作符和拷贝构造同时存在的时候,什么时候会调用赋值,什么时候会调用拷贝构造呢?

【C++】类和对象2:默认成员函数+操作符重载_c++_18

在这两个函数中添加cout进行打印提示,可以看到:

  • 如果对象在之前已经存在,就会调用赋值重载
  • 如果是一个全新的变量在定义的时候初始化,就调用的是拷贝构造

【C++】类和对象2:默认成员函数+操作符重载_开发语言_19

5.const成员

5.1用const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

基本的修饰方法如下,在函数的括号后加const即可

void Print()const
{
	cout<<_year<<endl;
}
  • 1.
  • 2.
  • 3.
  • 4.

实际修饰的是该函数隐含的this指针

this指针本身是Date*const类型的,修饰后变为const Date* const类型

void Print(const Date* const this)
{
	cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
  • 1.
  • 2.
  • 3.
  • 4.

①实例-权限问题

这么说好像有点迷糊,我们用实例来演示一下为什么需要const修饰成员函数

class Date{
public:
    Date(int year=2022,int month=2,int day=30)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    void Print()
    {
        cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

假设我们需要在函数中调用Print函数,在main中是可以正常调用的

int main()
{
	Date d1(2022,5,10);
    d1.Print();
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

但当你用一个函数来进行这个操作的时候,事情就不一样了

void TEST(const Date& d)
{
	d.Print();//d.Print(&d) -->const Date*
}
int main()
{
	Date d1(2022,5,10);
    d1.Print();//d1.Print(&d1) -->Date*
    TEST(d1);
    
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

这时候我们进行了引用调用,因为在TEST中我们不会修改d1的内容,所以用const进行了修饰

  • 这时候TEST中的d.Print()函数调用,传入的是const Date*指针,指针指向的内容不能被修改
  • main中的d1.Print();函数调用,传入的是Date*指针

于是就会发生权限冲突问题👇

【C++】类和对象2:默认成员函数+操作符重载_构造函数_20

这时候如果我们在函数后面加了const,就可以避免此种权限放大问题。这样不管是main函数还是TEST函数中对Print()函数的调用,就都可以正常打印了!

总结一下:

  • const对象不可以调用非const成员函数(权限放大)
  • 非const对象可以调用const成员函数(权限缩小)
  • const成员函数内不可以调用其他非const成员函数(权限放大)
  • 非const成员函数可以独调用其他const成员函数(权限缩小)

【C++】类和对象2:默认成员函数+操作符重载_拷贝构造_21


②什么时候需要使用?

众所周周知,const修饰指针有下面两种形式

  • *之前修饰,代表该指针指向对象的内容不能被修改(地址里的内容不能改)
  • *之后修饰,代表该指针指向的对象不能被修改(指向的地址不能改)

this指针本身就是类型名* const类型的,它本身不能被修改。加上const之后,this指向的内容,既类里面的成员变量也不能被修改了。

知道了这一点后,我们可以合理的判断出:只要是需要修改类中成员变量的函数,就不需要在()后面加const修饰

如果一个函数中不需要修改成员变量,就可以加const进行修饰

注意:如果你用了声明和定义分离的写法,那么声明和定义的函数都需要加上const修饰


③出错提醒

这里有一点需要提醒的是,如果你对某一个函数进行了const修饰,那么这个函数里面包含的其他类里面的函数,都需要进行const修饰。不然就会报错

【C++】类和对象2:默认成员函数+操作符重载_c++_22

出现该报错的情况如下

【C++】类和对象2:默认成员函数+操作符重载_构造函数_23

这个情况也提醒我们,不能在const修饰的函数中,调用非const修饰的成员函数


5.2取地址及对const取地址重载

最后两个默认成员函数,编译器会自动生成。这两个函数一般都不需要重载,毕竟返回的本身就是一个this指针,没有什么奇怪的地方

class Date{ 
public :
     Date* operator&()
     {
         return this ;
     }
    
     const Date* operator&()const
     {
     	return this ;
     }
private :
     int _year ; 
     int _month ;
     int _day ;
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

只有特殊情况,我们需要让&只获取特定内容的时候,才需要手动重载这两个函数


日期类的实现

类和对象第一站🚌中提到过,在项目协作的时候,我们一半要用定义和声明分离的形式来些一个项目。

下面就让我们用日期类来演示这样的操作

在类中定义的函数会被默认设置为内联,我们的目标就是:短小函数在.h中定义,长函数在.h中声明,在.cpp中定义

至于源码和解析嘛……大家直接来我的gitee仓库看吧! 【传送门】

注释写的很详细了⏲有啥问题可以在下面留言哦

【C++】类和对象2:默认成员函数+操作符重载_构造函数_24

特殊:对<<和>>的重载

这里的<<和>>主要是在使用cin和cout的时候需要使用

①简单了解io

 cplusplus网站上,你可以看到下面这一副图。在使用cin和cout的时候,我们其实分别调用了不同头文件的内容。

  • cin:istream
  • cout、cerr、clog:ostream

【C++】类和对象2:默认成员函数+操作符重载_c++_25

实际上,流是一个类型的对象,这个对象完成了输入和输出的操作

流操作是系统GUI支持的(了解一下就行,我也不懂)

在cout的定义中,你可以看到,实际上cout为了完成自动识别类型进行输出操作的工作,对各种类型进行了操作符重载 operator<<

【C++】类和对象2:默认成员函数+操作符重载_c++_26

显然,这部分重载中不包含自定义类型,所以我们需要来仿照这里的函数,进行重载操作

这里涉及到了友元函数,在类和对象的下一篇博客中我会写道。不过现在你只需要知道,友元函数是某一个类的朋友,目的是在类外访问类里面的成员变量

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

②实现

最后实现的效果如下,头文件中在最前面进行声明

【C++】类和对象2:默认成员函数+操作符重载_开发语言_27

//这两个是友元函数(因为需要在类外面访问类里面的成员变量)
//注意cin和cout的不同实现
//因为我们没有完全展开std namepace,所以写这个函数的时候需要自己指定std::

//返回值为ostream是为了保证多次cout
std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}

//cin使用的是istream
std::istream& operator>>(std::istream& in, Date& d)
{
	int year,month,day;
    in >> year >> month >> day;
    //这里应该添加一个对日期的正确性判断
     if(year>=0
        &&(month>=1&&month<=12)
        &&(day>=1&&day<=d.GetMonthDay(year,month)) )
    {//判断日期正确性
        d._year=year;
        d._month=month;
        d._day=day;
    }
    else
    {
        cout<<"Date err!"<<endl;
        exit(0);//日期错误直接终止程序
    }

	return in;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

程序运行的效果如下,和我们直接使用cout、cin是一样的!

【C++】类和对象2:默认成员函数+操作符重载_构造函数_28

当你写了一个离谱日期后,程序也会进行正确的报错

【C++】类和对象2:默认成员函数+操作符重载_拷贝构造_29

③疑惑解答

你可能会想,干嘛用友元啊,直接在类里面定义这个函数重载不就可以了?

之所以在外头定义该函数,是因为类里面定义的函数,默认会带有一个隐含的this指针传参,作为操作符的左操作数

然后你的函数使用就得变成下面这样😱

d1<<cout;
  • 1.

虽然也能跑起来并完成工作,但这样写也太怪了!


结语

最后的最后,今天是5月20日,用下图给大家送上祝福😂

【C++】类和对象2:默认成员函数+操作符重载_构造函数_30