类与对象(中)

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

初始化和清理以及拷贝复制是最重要的。

析构函数不是在销毁对象,而是在清理空间。

2. 构造函数

2.1 概念

对于以下Date类:

2.2 特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。

其特征如下:

1. 函数名与类名相同。

2. 无返回值。(也不需要写void)

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。(这里的重载指的是函数重载)

为什么构造函数支持函数重载?因为它可能有多种初始化的方式。重载有一个限定:同一个作用域,底层用了函数名修饰规则,如果不使用函数名修饰规则,编译器在找地址的时候区分不开它们两个成员函数。

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

6. 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??  

class Date
{
public:
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d;
    d.Print();
    return 0;
}

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型。

如:int/char/double/指针等,自定义类型就是我们使用class/struct/union等自己定义的类型

我们不写,编译器默认生成构造函数,内置类型不做处理(有些编译器也会处理,但是那是个性化行为,不是所有编译器都会处理),自定义类型会调用它的默认构造。 

1.一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的

2.全部都是自定义类型成员,可以考虑让编译器自己生成

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

class Date
{
public:
    void Print()
    {
        cout << _year << " " << _month << " " << _day << endl;
    }
private:

     //这里不是初始化,这里给的是缺省值,是给编译器生成的默认构造函数用的
    int _year = 2024;
    int _month = 10;
    int _day = 17;
};
int main()
{
    Date d;
    d.Print();
    return 0;
}

 不可以这样创建对象,会和函数声明有冲突。d1是函数名,参数列表是空,返回值是Date。

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。不传参就可以调用的函数就叫做默认构造函数。

3.析构函数

3.1 概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。 

3.2 特性

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

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

析构函数对于内置类型不做处理,自定义类型会调用它的析构函数。

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

4. 拷贝构造函数

4.1 概念

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。

4.2 特征

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

1.拷贝构造函数是构造函数的一个重载形式。

2.拷贝构造函数的参数只有一个且必须是类类型对象的引用。使用传值方式编译器直接报错, 因为会引发无穷递归调用(这里编译器强制检查了,如果不强制检查会引发无穷递归的)。C++规定自定义类型传参是要调用拷贝构造的,自定义类型的传参是要调拷贝构造完成的。内置类型不用管,内置类型是直接拷贝。注:用传引用可以解决问题,用指针也可以解决问题。

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // Date(const Date& d)   // 正确写法

    //传引用建议加const,就怕不小心改动了数据
    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;
    Date d2(d1);
    d2.Print();
    return 0;
}

最上面是自定义类型传参,下面的内置类型传参。 

3.若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。自定义类型会处理,内置类型也会处理。

1.内置类型成员会完成浅拷贝/值拷贝

2.自定义成员会调用它的拷贝构造

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。如果是动态开辟空间的拷贝,如果使用浅拷贝的话,程序会崩溃,问题就出现在析构的时候,对一块空间进行两次析构,并且指向同一块空间的话,一个值的修改会影响另一个。对于栈,后进先出,最后定义的对象先析构。

深拷贝的实现,简单了解一下。 

4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5.赋值运算符重载

5.1 运算符重载

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

哪些日期类运算符是有意义的?

日期类的对象比较大小是有意义的,日期减日期应该是个天数,是有意义的,日期加日期没有意义。是否要重载运算符取决于这个运算符对这个类是否有意义。没意义的也可以重载,但是用到的地方少,用处不大。

友元能不用就不用,因为友元会破坏封装,一般到万不得已才用友元。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:

不能通过连接其他符号来创建新的操作符:比如operator@ 重

载操作符必须有一个类类型参数

用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义

作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

.* 、:: 、sizeof 、?:、 . (.是结构体成员访问操作符)注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

5.2 赋值运算符重载

1. 赋值运算符重载格式

这个表达式如何执行?

0先赋值给了k,这个表达式还有一个返回值,这个返回值的值就是k,这个返回值又做了j的操作数,这个表达式的结果是j,j又做了i的操作数。

如果赋值运算符这样定义的话,不会支持连续赋值操作,并且赋值的时候还会调用拷贝构造。

但是这种Date返回的代价太大,可以修改一下使用传引用返回。

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

自己给自己赋值,是可以通过的,建议加断言,避免自己给自己赋值。

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

其它的重载既可以写成全局的,也可以写成成员函数。成员变量即使是公有的也不行。复制运算符重载是默认成员函数,只能写成成员函数。但是可以声明和定义分离。任何的成员函数都可以类里面给声明,类外面给定义。默认成员函数只能是类的,因为不写会自动生成,它会有其它的一些问题。

class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
    if (&left != &right)
    {
        left._year = right._year;
        left._month = right._month;
        left._day = right._day;
    }
    return left;
}

// 编译失败: // error C2801: “operator =”必须是非静态成员

 原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。默认生成的也可以完成连续赋值。Date和Myqueue不需要我们自己实现赋值重载。

默认生成赋值重载跟拷贝构造行为一样:

1.内置类型成员完成值拷贝/浅拷贝

2.自定义类型成员会调用它的复制重载

两个重载的运算符(前置++和后置++)可以构成函数重载,因为他们就是两个普通函数,只是他们两个的函数名比较特殊而已。Date& operator++();//前置++、Date& operator++(int);//后置++,增加这个int参数不是为了接受具体的值,仅仅是占位,跟前置++构成函数重载。

对于前置++和后置++,编译器会自动转化其对应的样子。 

Date.h

#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
    //友元函数声明,加上友元就可以访问私有成员变量,友元可以定义在类中的各个位置,不考虑访问限定符
    //friend void operator<<(ostream& out, const Date& d);
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& out, Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1);
    //Date(const Date& d)
    //{
    //    cout << "Date(const Date& d)" << endl;
    //    _year = d._year;
    //    _month = d._month;
    //    _day = d._day;
    //}
    Date(const Date& x)
    {
        _year = x._year;
        _month = x._month;
        _day = x._day;
    }
    void Print()const //这个地方的const修饰的是*this
    {
        cout << _year << " " << _month << " " << _day << endl;
    }
    bool operator<(const Date& x);
    bool operator==(const Date& x);
    bool operator<=(const Date& x);
    bool operator>(const Date& x);
    bool operator>=(const Date& x);
    bool operator!=(const Date& x);
    //加上static在类外面就可以调用,不用对象,用类域就可以调,并且没有this指针
    static int GetmonthDay(int year, int month);
    Date& operator+=(int day);
    Date operator+(int day);
    Date& operator++();//前置++
    Date& operator++(int);//后置++
    Date& operator-=(int day);
    Date operator-(int day);
    Date& operator--();
    Date operator--(int);
    int operator-(const Date& d);
    //为什么流插入不能写成成员函数?
    //因为Date对象默认占用了第一个参数,就是做了左操作数
    //写出来一定是下面这个样子,但是不符合使用习惯
    //d1 << cout;//转换成为d1.operator<<(cout);
private:
    int _year;
    int _month;
    int _day;
};

//void operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

Date.cpp

#include"Date.h"
Date::Date(int year , int month , int day)
{
    if (month > 0 && month < 13
        && day>0 && day < GetmonthDay(year, month))
    {
        _year = year;
        _month = month;
        _day = day;
    }
    else
    {
        cout << "非法日期" << endl;
        assert(false);
    }
}

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

bool Date::operator==(const Date& x)
{
    if (_year == x._year && _month == x._month && _day == x._day)
    {
        return true;
    }
    return false;
}
//复用
bool Date::operator>(const Date& x)
{
    return !(*this <= x);
}

bool Date::operator<=(const Date& x)
{
    return *this == x || *this < x;
}

bool Date::operator>=(const Date& x)
{
    return !(*this < x);
}
bool Date::operator!=(const Date& x)
{
    return !(*this == x);
}

int Date::GetmonthDay(int year, int month)
{
    static int dayArr[13] = { 0,31,28,31,30,31,31,30,31,30,31,30,31 };
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    {
        return 29;
    }
    else
    {
        return dayArr[month];
    }
}
//其实这里实现的是加等,如果是加的话,还要有个赋值操作
//同时加等支持连续赋值操作
//int i = 0, j = 0;
//j += i += 10;
Date& Date::operator+=(int day)
{
    //用加复用的代码,不建议用复用的代码
    //*this = *this + day;
    //return *this;
    if (day < 0)
    {
        return *this -= -day;
    }
    _day += day;
    while (_day > GetmonthDay(_year, _month))
    {
        _day -= GetmonthDay(_year, _month);
        ++_month;
        if (_month == 13)
        {
            ++_year;
            _month = 1;
        }
    }
    return *this;
}

Date Date::operator+(int day)
{
    //用加等复用的代码,建议用复用的代码
    //Date tmp(*this);
    //tmp += day;
    //return tmp;
    Date tmp(*this);
    tmp._day += day;
    while (tmp._day > GetmonthDay(tmp._year, tmp._month))
    {
        tmp._day -= GetmonthDay(tmp._year, tmp._month);
        ++tmp._month;
        if (tmp._month == 13)
        {
            ++tmp._year;
            tmp._month = 1;
        }
    }
    return tmp;
}


Date& Date::operator++()
{
    *this += 1;
    return *this;

}

Date& Date::operator++(int)
{
    *this += 1;
    return *this;
}

Date& Date::operator-=(int day)
{
    if (day < 0)
    {
        return *this -= -day;
    }
    _day -= day;
    while (_day <= 0)
    {
        --_month;
        if (_month == 0)
        {
            _month = 12;
            --_year;
        }    
        _day += GetmonthDay(_year, _month);
    }
    return *this;
}

Date Date::operator-(int day)
{
    Date tmp = *this;
    tmp -= day;
    return tmp;

}

Date& Date::operator--()
{
    *this -= 1;
    return *this;
}

Date Date::operator--(int)
{
    Date tmp = *this;
    *this -= 1;
    return tmp;
}

int Date::operator-(const Date& d)
{
    Date max = *this;
    Date  min = d;
    int flag = 1;
    if (max < min)
    {
        max = d;
        min = *this;
        flag = -1;
    }
    int n = 0;
    while (min != max)
    {
        ++min;
        ++n;
    }
    return n * flag;
}


//void operator<<(ostream& out, const Date& d)
//{
//    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
//}
ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    return out;//返回cout,其实out就是cout
}
//如果加上const修饰,就会报错
//因为流插入要插入数据,如果加上const就不能插入数据了
//void operator<<(const ostream& out, const Date& d)
//{
//    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
//}

//流提取也不能加const,因为流插入是一个对象,要修改对象里面的状态值
istream& operator>>(istream& in, Date& d)
{
    int year, month, day;
    in >> year >> month >> day;
    if (month > 0 && month < 13
        && day>0 && day < Date::GetmonthDay(year, month))//这个地方不考虑静态的也可以用d对象去调
    {
        d._year = year;
        d._month = month;
        d._day = day;
    }
    else
    {
        cout << "非法日期" << endl;
        assert(false);
    }
    return in;
}

text.cpp

#include"Date.h"
int main()
{
    Date d1(2024, 10, 20);
    //d1--;
    //d1.Print();
    //Date d2(d1 + 100);
    //d1.Print();
    //d2.Print();
    //Date d3(1, 1, 2);
    //d3 = d3 + 100;
    //d3.Print();
    //Date d1(2024, 10, 20);
    //Date d2(2024, 5, 5);
    //cout << d1 - d2 << endl;
    //cout << d2 - d1 << endl;
    //d1 << cout;//转换成为d1.operator<<(cout);
    //cout << d1;//转化成为operator<<(cout, d1);
    Date d2(2024, 10, 21);
    Date d3(2024, 11, 11);
    //刚刚定义的函数void operator<<(ostream& out, const Date& d)不支持连续赋值
    //如果想支持连续赋值就要求返回值是ostream,我们会说ostream没定义啊,其实是库里面定义了
    cout << d2 << d3 << d1;
    cin >> d2 >> d1;
    return 0;
}

比较的时候,内置类型转换成指令,自定义类型去调用它的函数。 

 cout<<对象,<<流插入的操作符有两个操作数,一个是cout,一个是日期类对象。cout是一个类对象,是一个ostream的类对象ostream是库定义的,是哪个库定义的,是iostream这个头文件定义的,cin是istream类型的对象。为什么内置类型能直接支持流插入呢?其实没有自动识别类型,它会转换成调用这个函数,运算符重载是让自定义类型支持运算符。

1.可以支持内置类型是库里面实现了

2.可以支持自定义类型是因为函数重载

 cin是istream的对象

6.const成员

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

如果d2是const修饰的对象,发现调用普通的Print函数调不动了,其实问题就发生传参上,第一个传参是Date* this,第二个是const Date* this,想当于指针指向的内容不能发生改变,第一个是权限的平移,第二个是权限的放大。

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。 

取地址及const取地址操作符重载,这两个也是默认成员函数,自定义类型不能直接用相关的运算符,内置类型可以用相关的运算符。

Date* operator&()
{
    cout << "Date* operator&()" << endl;
    return this;
}
const Date* operator&()const
{
    cout << "const Date* operator&()const" << endl;
    return this;
}

Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;

如果这样写,编译器会报错,并且我们没有写默认构造,要求const对象必须要初始化的,因为const对象只有一次初始化的机会

如果我们给了缺省值,编译器会生成默认构造,这个时候调用就可以了。其实我们不写,编译器也生成了默认构造,只是对内置类型啥都没做。

其实两个函数用的很少,什么时候用呢?

就是不想让别人取普通对象的地址

类的六个默认成员函数(默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、赋值运算符)不能定义为全局函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋悠然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值