C++之类和对象(二)

x33g5p2x  于2021-10-16 转载在 C/C++  
字(24.9k)|赞(0)|评价(0)|浏览(274)

类和对象(二)

在一个空类中,我们都知道一个空类的大小是一字节,那么空类真的什么都没有吗?答案是并不是,在我们写任何一个空类时,编译器其实都会自动生成6个默认的成员函数。那么这6个默认的成员函数是什么呢?在这篇博客中,我将讲解构造函数、析构函数、拷贝构造函数、运算符重载函数、取地址以及const取地址操作符重载6个函数

构造函数

我们前面在初始化成员变量是怎么初始化的呢?是定义一个Init成员函数来初始化,如下:

class Date
{
public:
    //一般写
    void Init(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    return 0;
}

这种方式不好,我们可能有时候会忘记调用Init函数,而且如果每次创建对象都调用该方法设置信
息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

可以,C++增加了一个构造函数,他是在对象定义的时候就调用,那么我们就来看看构造函数是什么样子的:

构造函数概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

构造函数特征

构造函数的规则:

  • 函数名和类名相同
  • 没有返回值
  • 对象实例化的时候编译器自动调用对应的构造函数

首先我们来定义一个日期类,并且写一个构造函数:

class Date
{
public:
    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    return 0;
}

我们进行调式发现对象在被定义后就自动被初始化了:

  • 构造函数可以重载

构造函数是可以重载的,还记得重载的概念吗?函数名相同,参数的列表不同(参数的顺序、类型、个数)

class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;

    }
    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
    private:
        int _year;
        int _month;
        int _day;
};
int main()
{
    Date d1;//不传参调用没参数的构造函数
    Date d2(2021, 10, 9);//传参调用有参数的构造函数
    return 0;
}

我们进行调试:

我们发现不传参的调用了没参数的构造函数,传参的调用了有参数的构造函数

所以构造函数可以重载这个特性提供了多种的初始化的方式,参数怎么写决定于你想怎么初始化

  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成

当我们显式的定义构造函数时:

class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
    void Print()
    {
        cout << _year <<"/"<< _month <<"/"<< _day << endl;
    }
    private:
        int _year;
        int _month;
        int _day;
};
int main()
{
    Date d1;
    d1.Print();
    Date d2(2021, 10, 9);
    d2.Print();
    return 0;
}

这时我们写了构造函数:

那么当我们不写的时候:

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;
}

我们运行程序:

我们发现初始化成员变量都是随机值了,这时其实是生产了默认的构造函数,很多同学疑惑,这默认的构造函数有什么用呢?下面我们来看这样一个场景:

class A
{
public:
    A()
    {
        _a1=1;
        _a2=2;
    }
private:
    int _a1;
    int _a2; 
};
class Date
{
public:
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
    A aa;
};
int main()
{
    Date d1;
    d1.Print();
    return 0;
}

我们定义了两个类,一个Date类,一个A类,其中A类的一个对象是Date的成员变量。

在C++中,把变量分为内置类型和自定义类型,哪些是内置类型呢?比如说int、char、double,指针类型等等,哪些是自定义类型呢?struct、class;当我们不写构造函数,编译器默认生成构造函数,编译器做了一个偏心的处理,对于内置类型不会初始化,自定义类型会调用它的无参构造函数初始化

我们可以看到aa这个对象的成员变量已经被初始化了,但是内置类型并没有被初始化:

这是早期C语法设计缺陷,这种偏心的处理导致了语法复杂了,但是因为要向前兼容,不能改动,C11,语法委员会为这里打了一个补丁:

class A
{
public:
    A()
    {
        _a1=1;
        _a2=2;
    }
private:
    int _a1;
    int _a2; 
};
class Date
{
public:
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }    
private:
    int _year = 0;
    int _month = 1;
    int _day = 1;
    A aa;
};
int main()
{
    Date d1;
    d1.Print();
    return 0;
}

解决方案是在类里面的成员变量后面加缺省值,当我们不写构造函数,编译器默认生成构造函数,编译器做了一个偏心的处理,对于内置类型不会初始化,自定义类型会调用它的无参构造函数初始化,这里本来不处理内置类型,但是这里会处理了:

这里的成员变量只是定义,这里不是初始化,这里就像函数缺省参数一样,我们给了参数就用给的,不给就用缺省值,这里我们没写构造函数,也就没有初始化成员变量,所以全部用缺省值,故这里内置类型进行处理了,但是当我们写构造函数初始化成员变量时,初始化了的成员变量就用初始化的值,没初始化的就用默认值:

  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class Date
{
public:
    //本来是构成函数重载的,但是他们两不能同时存在
    //Date d;会出现歧义,编译器不知道调用谁了,全缺省还是无参呢?
    Date(int year = 0,int month = 0,int day = 0)
    {
        _year=year;
        _month=month;
        _day=day;

    Date()
    {
        _year = 0;
        _month = 1;
        _day = 1;
    }
    void Print()
    {
        cout<<_year<<_month<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;   
};
int main()
{
   Date d;
   return 0;
}

这里会报错,为什么呢?

本来是构成函数重载的,但是他们两不能同时存在

Date d;会出现歧义,编译器不知道调用谁了,全缺省还是无参呢?

误区:有的人会认为,我们不写构造函数,编译器默认生成的构造函数,叫做默认构造函数。这个是不对的。

总结:不用传参数就能调用的构造函数就是默认构造函数

  • 书写构造函数风格
class Date
{
public:
    Date(int year)
    {
        year=year;
    }
    private:
    int year;
}
int main()
{
    Date d(1);
}

这里的year是什么值呢?

这里是随机值,为什么呢?因为成员变量没有成功被初始化,这里的year是成员变量还是局部变量呢?答案是局部变量,因为编译器坚持局部优先,在看到year时,它会先看它的局部范围内有没有这个变量。

所以我们一般都建议这样写:

class Date
{
public:
    Date(int year)
    {
    	_year = year;
    }
private:
	int _year;
};

或者这样写:

class Date
{
public:
    Date(int year)
    {
        m_year = year;
    }
private:
	int m_year;
};

学习了构造函数之后,我们知道了一个对象被定义的时候会做些什么,那么一个对象的声明周期结束的时候又会做什么呢?析构函数就是做这个事情的,下面我们来看析构函数。

析构函数

析构函数概念

析构函数的功能与析构函数恰好相反,析构函数不是完成对象的销毁,局部对象销毁的工作是由编译器完成的。而在对象在销毁的时候会自动调用析构函数,完成类的一些资源清理工作

对象出了它的声明周期,就调用析构函数:

class Date
{
public:
    Date(int year)
    {
    	_year = year;
    }
    ~Date()
    {
        cout<<"~Date()"<<endl;
    }
private:
	int _year;
};
int main()
{
    Date d1(2021);
    return 0;
}

我们发现析构函数调用了,这里我们只是进行演示一下会自动调用析构函数,实际中像Date这样的类是不需要析构函数的,因为没有需要清理的资源。

析构函数特征

析构函数是特殊的成员函数。其特征如下:

析构函数名是在类名前加上字符 ~。
*
无参数无返回值。
*
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

class Stack
{
public:
Stack(int capacity = 4)
  {
      _a = (int*)malloc(sizeof(int)*capacity);
      if(_a==nullptr)
      {
          cout<<"malloc fail"<<endl;
          exit(-1);
      }
      _top = 0;
      _capacity = capacity;
  }
private:
  int* _a;
  int _top;
  int _capacity;
};
int main()
{
  Stack s1;
  return 0;
}

上面这段代码编译器就会自动生成一个析构函数进行处理

但是跟构造函数类似,我们如果不写,编译器默认生成的析构函数做了偏心处理:

1、内置类型不处理

2、自定义类型成员会去调用它的析构函数

请看下面这个例子:

class A
{
public:
    A()
    {}
    ~A()
    {
        cout << "~A()" << endl;
    }
};
class Stack
{
public:
    Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int) * capacity);
        if(_a==nullptr)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
private:
    int* _a;
    int _top;
    int _capacity;
    A aa;
};
int main()
{
    Stack s1;
    return 0;
}

那么构造函数和析构函数有什么价值呢?

在我们通过两个栈实现队列的时候,会体现它的价值:

class Stack
{
public:
    Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int)*capacity);
        if(_a==nullptr)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
    ~Stack()
    {
        free(_a);
        _top=_capacity=0;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};
class MyQueue
{
private:
    Stack _pushST;
    Stack _popST;
};
int main()
{
    MyQueue mq;
    return 0;
}

在MyQueue没有写构造函数的情况下,我们调用了自定义类型的构造函数,这时相当于完成了两个栈的初始化

当我们对象在被销毁的时候:

在对象被销毁的时候,自动的完成了两个栈的析构函数的调用

这里可以看到编译器默认生成构造函数和析构函数也是有价值的

  • 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Stack
{
public:
	Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int)*capacity);
        if(_a==nullptr)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
    ~Stack()
    {
        //完成对象中的资源清理工作
        free(_a);
        _a=nullptr;
        _top = _capacity = 0;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};
int main()
{
    Stack s1;
    return 0;
}

在前面的日期类中没有需要清理的地方,但是这个类就有需要清理的地方

可以看到它会自动的调用析构函数:

下面我们来看拷贝构造函数:

拷贝构造函数

拷贝构造函数概念

在现实生活中,我们ctrl cv就可以复制粘贴一个东西,那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?答案是可以的。
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象
创建新对象时由编译器自动调用。

拷贝函数特征

拷贝构造函数也是特殊的成员函数,其特征如下:

拷贝构造函数是构造函数的一个重载形式。
*
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

在看来概念后,你肯定一脸懵逼,小白肯定会看不懂,这是啥意思呢?下面我将说一说拷贝构造函数:

我们首先创建一个日期类:

class Date
{
public:
    Date(int year = 0,int month = 0,int day = 0)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    
    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;   
};
int main()
{
   	Date d1(2021,10,9);
    d1.Print();
    
   	return 0;
}

这里我们创建了一个d1对象,并给它初始化,那么现在想拷贝复制一个d1对象出来怎么拷贝呢?

Date d2(d1);
//等价于
Date d2 = d1;

我们可以这样写,这两种都可以,是一样的,但是这样写是调用的是构造函数吗?不是的,这里调用的是拷贝构造函数,并不是我们前面讲的构造函数,对象初始化调用构造函数,但是d1是同类型,调用拷贝构造函数。

那么拷贝构造函数怎么写呢?这样写可以吗?

Date(Date d)
{
    _year=d._year;
    _month=d._month;
    _day=d._day;
}

我们把代码写进去看一看可不可以:

在vs2019中你都不运行它就会报错,那么为什么不可以呢?

这里会无穷递归调用拷贝构造,因为得传参:

那么怎么解决呢?

用引用来接收,这样就不会在传参时拷贝,操作引用就相当于操作它本身:

Date(Date& d)
{
    _year=d._year;
    _month=d._month;
    _day=d._day;
}

引用传参解决了这里传值无穷递归的问题

更加安全的写法,保证被拷贝的类对象不会被改变,在前面加const增加代码的健壮性:

Date(const Date& d)
{
    _year=d._year;
    _month=d._month;
    _day=d._day;
}
  • 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

我们不写,编译器默认生成的拷贝构造和构造和析构又不太一样,不会去区分内置类型和自定义类型成员,而是这两者都会处理:

1、内置类型,字节序的浅拷贝(就像memcpy完成拷贝)

2、自定义类型,会去调用它的拷贝构造完成拷贝

那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像
日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

class Stack
{
public:
	Stack(int capacity = 4)
    {
        _a = (int*)malloc(sizeof(int)*capacity);
        if(_a==NULL)
        {
            cout<<"malloc fail"<<endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }
    ~Stack()
    {
        free(_a);
        _top = 0;
        _capacity = 0;
    }
private:
    int* _a;
    int _top;
    int _capacity;
};
int main()
{
    Stack s1;
    Stack s2(s1);
    return 0;
}

自动调用拷贝构造函数是没有问题的,可以成功拷贝:

但是在析构构函数调用时会出问题:

s1和s2对象的成员变量_a都指向了一块空间,在调用析构函数时,先调用s2的析构函数将_a成员变量释放掉,然后再调用s1的析构函数将_a成员变量释放,这里就出问题了,两个对象的成员变量_a是指向同一块空间的指针,同一块空间释放了两次,所以这里会错误。所以编译器默认生成拷贝构造并不能解决所有的问题,像stack这样的类,编译器默认生成的拷贝构造就是浅拷贝,需要自己实现深拷贝拷贝构造函数进行解决

运算符重载函数

在C语言和C中,我们可以使用一系列的操作符,但是这是有前提的,需要操作数是内置类型,但是如果我们想要使用自定义类型去使用运算符怎么办呢?在C当中出现了运算符重载:

class Date
{
public:
    Date(int year=0, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
public:
    int _year;
    int _month;
    int _day;
};
int main()
{
   	Date d1(2021,10,11);
   	Date d2(2020,11,11);
   	Date d3(2021,11,11);
    
    return 0;
    
}

在上面的日期类当中我们定义了d1、d2、d3对象,那么我们比较两个对象,也就是比较两个日期该怎么比较呢?这样比较吗?

d1 == d2;
d1 < d2;

这样写现在是不行的,因为运算符默认都是给内置类型用的。自定义类型的变量想要用这些运算符,得自己进行运算符重载,运算符重载的意思就是我们得自己去写一个函数定义实现这里运算符的行为。运算符是给内置类型用的,编译器不知道怎么比较类对象的这个规则,所以需要我们自己写函数控制

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

参数:操作符的操作数有几个就有几个参数,参数类型是你要操作的对象的类型

返回值:看运算符运算后的返回值的类型

下面这个函数就是判断两个对象是否相等的运算符重载函数:

bool operator==(Date x1, Date x2)
{
    return x1._year == x2._year
        && x1._month == x2._month
        && x1._day == x2._day;
}
class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
public:
    int _year;
    int _month;
    int _day;
};
bool operator==(Date x1, Date x2)
{
    return x1._year == x2._year
        && x1._month == x2._month
        && x1._day == x2._day;
}
int main()
{
    Date d1(2021, 10, 11);
    Date d2(2020, 11, 11);
    Date d3(2021, 11, 11);
    return 0;

}

那么能编译过吗?

可以看到成功编译过了

但是有没有发现我们的成员是public,当我们成员写成public时可以编译过,我们现在先不说private的情况,一会再说,那么该怎么去调用它呢?有两种方式:

operator==(d1,d2);//正常这样调用,但是这样可读性很差,还不如写一个EqualDate函数

==运算符重载函数的函数名为operator==,operator==(d1,d2);那么这样调用是很正常的,容易理解的。但是这样可读性很差

d1==d2;//编译器去找有没有重载,有的话转化成operator==(d1,d2);去调用,没有的话就报错

这是另一种调用方式,编译器去找有没有重载,有的话转化成operator==(d1,d2);去调用,没有的话就报错

上面写的运算符重载函数还不够好,更加健壮的写法,传引用减少调用拷贝构造:

bool operator==(const Date& x1,const Date& x2)
{
    return x1._year == x2._year
        && x1._month ==x2._month
        && x1._day ==x2._day;
}

我们知道在函数传参的时候,形参相当于是实参的一份临时拷贝,此时会调用拷贝构造函数,所以我们想要减少调用拷贝构造函数我们就传引用,又因为我们不期望修改x1,x2对象,因为这只是判断一下这两个对象是否相等,所以我们加const

上面讲的是我们的成员变量是公有的情况,那么设置成私有呢?

这时就编译不过了:

因为成员变量是私有的,外面的函数我们无法访问他们,所以我们得出结论:运算符重载是可以写成全局的,但是面临访问私有成员变量的问题

那么如何解决呢?
将运算符重载函数弄成成员函数就可以解决

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator==(const Date& x1,const Date& x2)
    {
        return x1._year == x2._year
            && x1._month ==x2._month
            && x1._day ==x2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2021, 10, 11);
    Date d2(2020, 11, 11);
    Date d3(2021, 11, 11);

    d1 == d2;
    return 0;

}

我们编译一下看一看:

咦,还是有问题,此时编译她说operator==参数太多了,什么意思呢?是因为写成成员函数时,成员函数参数还有一个this指针,this指针指向当前对象。所以我们需要减少一个函数参数:

bool operator==(const Date& x)
//bool operator==(Date* this, const Date& x)
{
    return _year == x._year
        && _month ==x._month
        && _day ==x._day;
}

那么写成成员函数后怎么调用呢?可以这样调用:

d1.operator==(d2);

或者:

d1 == d2;//d1 == d2; -> d1.operator==(d2); -> d1.operator==(&d1,d2);

编译器看到d1 == d2这个代码时,首先会将它转化为->d1.operator==(d2);然后转化为->d1.operator==(&d1,d2);

我们来验证一下:

int main()
{
    Date d1(2021, 10, 11);
    Date d2(2021, 10, 11);
    Date d3(2021, 11, 11);

    if (d1 == d2)
    {
        cout << "相等" << endl;
    }
    else
    {
        cout << "不相等" << endl;
    }
    return 0;
}

接下来我们尝试模仿==运算符重载写一个<运算符重载,尝试理解分析它的调用过程:

bool operator<(const Date& x)
{
    if(this->_year < x._year)
        return true;
    else if(this->_year == x._year && this->_month < x._month)
        return true;
    else if(this->_year == x._year && this->_month == x._month &&this->day < x._day)
       	return true;
    else
        return false;
}

当当前对象年份小时,肯定小,返回true,年份相等并且月份小时,肯定小,返回true,当年份和月份都相等,天数小时,肯定小,返回true,其余情况就是x对象比当前对象小或者相等,同样归为else,返回false。

那么怎么调用它呢?

//调用:
d1.operator<(d2);
//或者:
d1 < d2;//d1 < d2 -> d1.operator<(d2) -> d1.operator<(&d1,d2);

和==运算符重载类型

运算符重载需要注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
    重载的运算符一定是C++有的运算符

  • 重载操作符必须有一个类类型或者枚举类型的操作数
    参数需要至少有一个是自定义类型

  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
    比如不要把==操作符重载成<操作符的含义

  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的,操作符有一个默认的形参this,限定为第一个形参
    这个我们上面已经很清楚的验证过了

  • . 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。/*
    *注意一下:/(解引用)是可以重载的,这里不能重载的是./,很多选择题会用/来做选项来迷惑

在C++的学习过程当中我们比较常用的是赋值操作符,下面我们来看赋值操作符的重载:

class Date
{
public:
    Date(int year=0, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void operator=(const Date& d)
    {
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
    }
    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
   	Date d1(2021,10,11);
   	Date d2(2020,11,11);
   	Date d3(2021,11,11);
    
    d1 = d3;
    
    d1.Print();
    d3.Print();
    
    return 0;
    
}

可以看到已经成功赋值

赋值运算符重载和拷贝复制有什么区别呢?

赋值运算符重载,用于两个已经定义出来的对象间拷贝复制

拷贝构造是在准备定义时用另一个对象来初始化它

Date d5 = d3;//虽然这里用了=,但是他这里是拷贝构造,因为这是对象在创建时。

虽然这里用了=,但是他这里是拷贝构造,因为这是对象在创建时。

我们上面写的赋值重载这样不够好,因为在内置类型中的赋值是可以连续赋值的,但是我们现在写的还不支持连续赋值:

int i = 1;
int j = 2;
int k = 3;
i = j = k;//连续赋值,这里先执行j=k,j=k返回值是j,i=j(这里的j是j=k表达式的返回值)

i=j=k;这个表达式是从右到左执行的,连续赋值,这里先执行j=k,j=k返回值是j,i=j(这里的j是j=k表达式的返回值)

所以支持连续赋值,是需要返回值的,我们上面写的赋值重载函数没有写返回值,下面我们进行改进:

Date operator=(const Date& d)
{
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
    return *this;//返回被赋值的对象,也就是该表达式的值
}

*这时就支持了连续赋值,但是还是不够好,因为在return时会调用拷贝构造函数,我们之前说过,返回时不会返回它本身,而是临时创建临时变量,拷贝给临时变量,返回的其实是临时变量,这里会调用拷贝构造,/this是对象,出了这个函数作用域还在,这时就可以返回引用,就减少了调用拷贝构造函数

调用拷贝构造的演示:

可以看到当我们是值返回时,调用了两次拷贝构造函数。

所以我们还可以继续改进(使用引用返回):

Date& operator=(const Date& d)
{
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
    return *this;
}

上面我们已经写的足够好了,但是还有一个问题,当自己赋值给自己,是没有意义的,所以我们防止自己赋值给自己,再加一句代码:

Date& operator=(const Date& d)
{
    if(this != &d)
    {
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
    }
    return *this;
}

上面的代码就是完美的赋值重载函数了。

那么赋值重载也有这样的一个问题:如果我们不写,赋值重载函数和构造函数、析构函数、拷贝构造函数一样,编译器会默认生成默认赋值重载。

那么默认的赋值重载是什么样子呢?跟拷贝构造的行为类似,内置类型成员会完成值拷贝,而自定义类型成员会去调用它的赋值重载

我们验证一下试试:

class A
{
public:
    A& operator=(const A& d)
    {
        cout << "A& operator=(const A& d)" << endl;
        return *this;
    }
};
class Date
{
public:
    Date(int year = 0, 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;
    A _a;
};
int main()
{
    Date d1(2021, 10, 11);
    Date d2;
    d2 = d1;
    d2.Print();
    return 0;
}

我们发现d2初始化为0/1/1,但是现在和d1一样,很明显完成了赋值,并且调用了自定义类型的赋值重载函数。

我们选择写运算符重载的原则:编译器默认生成的能完成我们要的功能就可以不写,不能完成的就写一个

比如日期类,只需自己写构造函数就可以,其他的编译器生成的默认的就可以完成功能,但是栈这个类,构造函数、析构函数、拷贝构造、赋值重载都需要我们自己写。默认生成都有问题(因为我们数据存储在堆区,会造成调用各个对象的析构函数造成重复释放),所以不能用

日期类的实现

class Date
{
public:
    Date(int year=0, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void operator=(const Date& d)
    {
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
    }
    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2021,10,11);
    d1.Print();
    
    Date d2(2021,2,29);
    Date d3(2020,2,29);
    Date d4(2021,13,29);
    d2.Print();
    d3.Print();
    d4.Print();
    return 0;
}

我们发现我们日期中出现了非法的日期但是还是初始化了,故我们还需再构造函数里面写一个判断日期是否合法的函数:

//获取某年某月的天数
int GetMonthDay(int year,int month)
{
    assert(month>0&&month<13);
    static int monthDays[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;
    }
    return monthDays[month];
}
Date(int year=0, int month=1, int day=1)
{
    _year = year;
    _month = month;
    _day = day;
    
    //判断日期是否合法
    if (_year < 0 || _month <= 0 || _month > 12
        || _day <= 0 || _day > GetMonthDay(_year, _month))
    {
        cout << _year << "/" << _month << "/" << _day;
        cout << "非法日期" << endl;
    }
}

可以看到现在我们可以判断日期是不是合法

C++为了增强程序的可读性,提出了运算符重载,并且让我们可以实现运算符重载函数控制,这个运算符行为,一个类到底要重载哪些运算符,是看你需要哪些运算符,并且要考量重载了这个运算符有没有意义,运算符重载和函数重载,都用了重载这个词,但是他们之间没有关联

现在我们可以写很多运算符重载函数:

日期类的运算符重载函数
bool operator<(const Date& d);//小于运算符重载
bool operator>(const Date& d);//大于运算符重载
void operator=(const Date& d);//等于运算符重载
bool operator>=(const Date& d);//大于等于运算符重载
bool operator<=(const Date& d);//小于等于运算符重载
Date operator+(int day);//日期+天数
Date operator-(int day);//日期-天数
Date& operator+=(int day);//日期+=天数
Date& operator-=(int day);//日期-=天数
int operator-(const Date& d);//日期-日期

我们首先写大于运算符重载或者小于运算符重载和等于运算符重载,<或者>、>=、<=、!=都可以进行复用:

//大于运算符重载
bool operator>(const Date& d)//大于运算符重载
{
    if(this->_year>d._year)
        return true;
    else if(this->_year==d._year && this->_month>d._month)
        return true;
    else if(this->_year==d._year && this->_month==d._month && this->_day>d._day)
        return true;
    else
        return false;
}
//小于运算符重载
bool operator<(const Date& d)
{
    if(this->_year<d._year)
        return true;
    else if(this->_year==d._year && this->_month<d._month)
        return true;
    else if(this->_year==d._year && this->_month==d._month && this->_day<d._day)
        return true;
    else
        return false;
}
//等于运算符重载
void operator=(const Date& d)
{
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
}

<、>=、<=、!=都可以进行复用:

bool operator>=(const Date& d)//大于等于运算符重载
{
	return *this > d || *this==d;
}
bool operator<(const Date& d)//小于运算符重载
{
    return !(*this>=d);
}
bool operator<=(const Date& d)//小于等于运算符重载
{
    return !(*this>d);
}
bool operator!=(const Date& d)//不等于运算符重载
{
    return !(*this==d);
}

上面这些运算符重载我们写完后,我们再看下面这个代码:

int main()
{
    Date d1;
    d1+100;
    return 0;
}

我们定义了一个日期对象,我们如果想要知道一个日期后一百天的日期是多少,就可以重载+运算符这个函数:

Date operator+(int day)
{
    Date temp(*this);
    temp._day+=day;
    while(temp._day>GetMonthDay(temp._year,temp._month))
    {
        temp._day-=GetMonthDay(temp._year,temp._month);
        ++temp._month;
        if(temp._month==13)
        {
            ++temp._year;
            temp._month=1;//月置为1
        }
    }
    return temp;
}

因为我们是日期+天数,所以不能操作this所指向的对象,我们只是想要算出这个日期+天数的结果,并不是加到该对象上

如果我们想让一个日期+天数,并让它等于这个结果,这时我们可以重载+=运算符

Date& operator+=(int day)//日期+=天数
{
    _day += day;
    //日期不合法,进位
    while(_day>GetMonthDay(_year,_month))
    {
        _day-=GetMonthDay(_year,_month);
        ++_month;
        if(_month==13)//月不合法,年进位
        {
            ++_year;
            _month=1;//月置1
        }
    }
	return *this;
}

因为+和+=是相似的,所以我们可以进行复用:+运算符重载函数可以复用+=运算符重载函数,+=运算符重载函数也可以复用+运算符重载函数:

//+复用+=
Date operator+(int day)
{
    Date ret = *this;
    ret += day;
    return ret;
}

*+=是直接修改/this,所以我们使用+复用+=时,将this所指向的对象保存起来,然后ret+=day,最后返回ret就可以完成+运算符重载。

//+=复用+
Date& operator+=(int day)
{    
	*this = *this + day;
    return *this;
}

那么这两个哪个好呢?

第一个比第二个更好一些,当+=复用+时,+运算符重载返回的是值,会调用拷贝构造,然后还要调用赋值重载,当+=复用+时,+=运算符重载返回的是引用,不需要调用拷贝构造。

那么为什么+=可以返回引用,而+不能返回引用呢?

因为+=返回的是this指针所指向的对象,出了作用域它还在,所以可以引用返回,而+返回的是临时变量,出了作用域它就不在了,所以不能引用返回。

完成了+和+=运算符重载,当然我们还可以完成-和-=运算符重载:

-运算符重载

Date operator-(int day)//日期-天数
{
    Date temp = *this;
    temp._day -= day;
    //日期不合法,借位
    while(temp._day<=0)
    {
        --temp._month;//向月借位
        if(temp._month == 0)
        {
            --temp._year;//向年借位
            temp._month=12;//月更新为12
        }
        temp._day+=GetMonthDay(temp._year,temp._month);
    }
}

当天数减了之后小于0,这时我们就需要向月借位,要是当前月是1月,借位之后是0月,显然不行,此时我们需要向年借位,然后将月更新为12月。

-=运算符重载

Date& operator-=(int day)//日期-=天数
{
    _day -= day;
    //日期不合法,借位
    while(_day<=0)
    {
		--_month;//向月借位
        if(_month == 0)
        {
            --_year;//向年借位
            _month=12;//月更新为12
        }
        _day+=GetMonthDay(_year,_month);
        }
    }
}

当然-和-=也可以复用是和+和+=是一样的思想:

//-复用-=
Date operator+(int day)
{
    Date ret = *this;
    ret -= day;
    return ret;
}
//-=复用-
Date& operator+=(int day)
{    
	*this = *this - day;
    return *this;
}

和+和+=一样的道理,-复用-=更好一些。

我们上面的关于+、+=、-、-=运算符重载写法还不够好,为什么呢?上面的写法当我们这样调用时会出错:

d1-(-100);
d1+(-100);

不排除有人会这样调用,所以我们需要进行优化

那么怎么修改呢?因为+是复用+=的,所以我们直接改+=就可以了:

Date& operator+=(int day)//日期+=天数
{
    if(day>0)
    {
        _day += day;
        //日期不合法,进位
        while(_day>GetMonthDay(_year,_month))
        {
            _day-=GetMonthDay(_year,_month);
            ++_month;
            if(_month==13)//月不合法,年进位
            {
                ++_year;
                _month=1;//月置1
            }
        }
    }
    else
    {
        _day -= -day;
        while(_day<=0)
        {
            --_month;
            if(_month==0)//month=1时,年借位
            {
                _month=12;
                --_year;
            }
            _day+=GetMonthDay(_year,_month);
        }
    }
	return *this;
}

当day是正数的时候,我们就按之前写的+=逻辑走,当day不是正数的时候,我们按照-=的逻辑走

Date& operator-=(int day)//日期-=天数
{
    if(day>0)
    {
        _day -= day;
        while(_day<=0)
        {
            --_month;
            if(_month==0)//month=1时,年借位
            {
                _month=12;
                --_year;
            }
            _day+=GetMonthDay(_year,_month);
        }
    }
    else
    {
        _day += -day;
        //日期不合法,进位
        while(_day>GetMonthDay(_year,_month))
        {
            _day-=GetMonthDay(_year,_month);
            ++_month;
            if(_month==13)//月不合法,年进位
            {
                ++_year;
                _month=1;//月置1
            }
        }
    }
	return *this;
}

当day是正数的时候,我们就按之前写的-=逻辑走,当day不是正数的时候,我们按照+=的逻辑走

当然其实这个也可以复用:

Date& operator+=(int day)//日期+=天数
{
    if(day>0)
    {
        _day += day;
        //日期不合法,进位
        while(_day>GetMonthDay(_year,_month))
        {
            _day-=GetMonthDay(_year,_month);
            ++_month;
            if(_month==13)//月不合法,年进位
            {
                ++_year;
                _month=1;//月置1
            }
        }
    }
    else
    {
    	return *this -= -day;
    }
}

**当day为负数时,/this -= -day; -day让day成为正数,然后/this再去-=这个正数。

Date& operator-=(int day)//日期-=天数
{
    if(day>0)
    {
        _day -= day;
        while(_day<=0)
        {
            --_month;
            if(_month==0)//month=1时,年借位
            {
                _month=12;
                --_year;
            }
            _day+=GetMonthDay(_year,_month);
        }
    }
    else
    {
    	return *this += -day;
    }
}

**当day为负数时,/this += -day; -day让day成为正数,然后/this再去+=这个正数。

接下来我们来看++和–运算符的重载:

前置和后置都完成了++,那么它们不同的地方在哪里呢?

不同的地方在于返回值不一样

class Date
{
public:
    Date(int year=0, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void operator=(const Date& d)
    {
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
    }
    void Print()
    {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2021,10,11);
    //前置++和后置++都完成了++,不同的地方在于返回值不一样
    d1++;//d1.operator++(&d1)
    d1.Print();

    ++d1;//d1.operator++(&d1)
    d1.Print();
    return 0;
}

我们创建d1对象,想要使用d1d1,我们需要重载d1d1,因为他们的运算符是一样的,函数名就是一样的,调用的都是d1.operator++(&d1),那么我们怎么区分呢?为了区分,C对后置做了特殊处理,加了一个int参数,这个参数仅仅是为了区分这样它们就形成函数重载,这个参数实际并没有什么用

d1++;//d1.operator++(&d1)
++d1;//d1.operator++(&d1,0)

前置++重载:

Date& operator++()
{
    *this +=1;//复用+=运算符重载
    return *this;
}

我们可以复用+=运算符重载

后置++重载:

Date operator++(int)
{
    Date temp(*this);
    *this += 1;//复用+=运算符重载
    return temp;
}

注意:后置返回的是前的值,所有我们需要有临时变量来保存它

前置–:

Date& operator--()
{
    *this -=1;复用-=运算符重载
    return *this;
}

同理我们可以复用-=运算符重载

后置–:

Date operator--(int)
{
    Date temp(*this);
    *this -= 1;//复用-=运算符重载
    return temp;
}

下面我们再来看一个重载:

日期-日期重载:

int operator-(const Date& d)//日期-日期
{
    //效率差别不大的情况下,尽量选择写可读性强的,简单的程序
    Date max  = *this;
    Date min = d;
    int flag=1;
    if(*this<d)
    {
        max = d;
        min = *this;
        flag = -1;
    }
    int n=0;
    while(min!=max)
    {
        ++min;
        ++n;
    }
    return n*flag;
}

**我们首先找出两个日期的较大的与较小的,首先我们假设/*this对象是较大者,d对象是较小者,二者相减是正数,我们定义一个flag等于1,然后再判断如果/*this对象小于d对象,那么将较大者给成d对象,较小者给成/this对象,然后它两相减是个负数,flag改为-1。然后给一个循环,++min,并计数n直到min和max相等,n/flag就是相减的结果

const成员函数

const修饰类的成员函数

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

同样的,我们拿日期类来举例:

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date(const Date& d)
    {
        cout << "Date(const Date& d)" << endl;
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
    }
    Date operator=(const Date& d)
    {
        this->_year = d._year;
        this->_month = d._month;
        this->_day = d._day;
        return *this;
    }
    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2021,10,13);
   	const Date d2(2021,10,13);
    d1,Print();
    d2.Print();
    return 0;
}

我们可以看到这样编译不过,那么const对象为什么调用不了Print函数呢?

d2.Print(&d2);//隐含的传参

是因为在调用时有隐含的传参,但是d2此时是const对象,将d2的地址传参,它的指针类型应该为const Date/*,编译器默认生成的形参为:

void Print(Date*this)
{
	cout << _year << "/" << _month << "/" << _day << endl;   
}

需要改为这样才正确:

void Print(const Date*this)
{
cout << _year << "/" << _month << "/" << _day << endl;
}

这里的问题是传参的过程当中存在权限放大了,但是因为这是隐含的传参,我们不能明确的修改

所以有了const修饰类的成员函数:

void Print() const
{
    cout << _year << "/" << _month << "/" << _day << endl;
}

总结:

成员函数加const,变成const成员函数是有好处的,这样const对象可以调用,非const对象也可以调用。

  • 那么是不是所有成员函数都要加const呢?
    不是,需要看成员函数的功能,如果成员函数是一个修改型,那就不能加,比如:operator+=()

如果只是一个只读型,那就可以加,比如Print(),所以只要不修改都把const加上

  1. const对象可以调用非const成员函数吗?
    const对象可以调用非const成员函数,因为非const成员函数的this指针参数是非const的,将const修饰的对象地址传参给非const的指针this,权限缩小了,所以是可以的。

  2. 非const对象可以调用const成员函数
    非const对象不可以调用const成员函数,因为const成员函数的this指针参数是const的,将非const修饰的对象地址传参给const的指针this,权限扩大了,所以是不可以的。

  3. const成员函数内可以调用其它的非const成员函数吗?
    const成员函数实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。

这里我们可以看到是不能调用的,因为this指针是const的,调用时会隐含的将this传过去,而Print1是非const,相当于权限放大了,所以是不可以的。

  1. 非const成员函数内可以调用其它的const成员函数吗?
    非const成员函数内是可以调用其它的const成员函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-le5qKZFL-1634287961388)(C:\Users\15191107746\AppData\Roaming\Typora\typora-user-images\image-20211014154246699.png)]

因为非const成员函数的this指针参数是非const的,将const修饰的对象地址传参给非const的指针this,权限缩小了,所以是可以的。

取地址及const取地址操作符重载

取地址操作符重载
Date* operator&()
{
    return this;
}

一般不需要写,默认生成的就可以用

const取地址操作符重载
const Date* operator&()const
{
    return this;
}

const成员函数,this是const修饰的,所以返回值要是const修饰的

如果不想获取对象地址:

Date* operator&()
{
    return nullptr;
}

const Date* operator&()const
{
    return nullptr;
}

这样可以,但是没有什么意义,实际中不会用到

下面我们来看这样的一道题:

以下代码共调用多少次拷贝构造函数?

widget f(widget u)
{  
    widget v(u);
    widget w=v;
    return w;
}
int main()
{
    widget x;
    widget y=f(f(x));
}

我们首先来看下面这个代码:

class widget
{
public:
	widget()
	{
		cout << "widget()" << endl;
	}
};
widget f1()
{
	widget w;
	return w;
}
int main()
{
	widget ret = f1();
	return 0;
}

我们在传值传参和传值返回都会生成一个拷贝对象,会调用拷贝构造函数。

一般情况下,一个对象在函数中返回时拷贝构造出一个临时对象,一个临时对象再去拷贝构造另一个对象,一般编译器会进行优化。中间对象优化掉,直接第一个拷贝构造第三个(要求:他是在一个表达式执行的连续步骤)

原本是这样:

编译器优化后:

直接第一个拷贝构造第三个(要求:他是在一个表达式执行的连续步骤)

当我们是连续的步骤时可以看到编译器已经优化:

此时拷贝构造调用了一次

我们再回到那个题:

widget f(widget u)
{  
    widget v(u);
    widget w=v;
    return w;
}
int main()
{
    widget x;
    widget y=f(f(x));
}

首先第一次是x去构造u,第二次是u构造v,第三次是v构造w,第四次时,w作为返回值然后又要去构造u,这时是表达式执行的连续步骤,编译器会优化,所以这里算一次,然后第五次是u再去构造v,第六次是v再去构造w,最后return时又是一次优化,是第七次,故故最后的答案为:7次

匿名对象:

class widget
{
public:
	widget()
	{
		cout << "widget()" << endl;
	}
	widget(const widget& w)
	{
		cout << "widget(const widget& w)" << endl;
	}
	~widget()
	{
		cout << "~widget()" << endl;
	}
};
widget f1()
{
	widget w;
	return w;
}
void f2(widget w)
{}
int main()
{
	widget();
	//f2(widget());
	return 0;
}
widget();//匿名对象,他没有名字,特点是生命周期只在这一行

我们通过调试可以看到,定义的这一行完了后,直接就调用了析构函数,验证了生命周期只在这一行:

class widget
{
public:
	widget()
	{
		cout << "widget()" << endl;
	}
	widget(const widget& w)
	{
		cout << "widget(const widget& w)" << endl;
	}
};
void f2(widget w)
{}
int main()
{
	f2(widget());
	return 0;
}

本来是需要先调用构造函数,然后调用拷贝构造,但是这里编译器进行了优化,只调用构造函数,没有拷贝构造,直接就把这个匿名对象给了w

下面这种不连续的情况它会先调用构造,然后再调用拷贝构造:

int main()
{
	widget w;
	f2(w);
	return 0;
}

总结:
在传值传参和传返回值的过程中,只要是在一个表达式调用连续步骤中,构造、拷贝构造,会被编译器优化合并

欢迎大家学习交流!

相关文章