0


再谈类和对象相关知识

上一篇我们谈到了类和对象及默认成员函数,对其有了基本的认识。这一篇我们完善剩下的默认成员函数并补充一些周边知识

拷贝构造与赋值重载函数

什么是赋值重载函数

//对于内置类型,我们经常进行赋值操作:
int a;
int b = 10;
a = b;
//而对于自定义类型,需要进行这样的操作则需要我们重载赋值运算符:
Date d1;
Date d2(2022, 10, 9);
Date d3(d2); //拷贝构造
d1 = d2;     //赋值重载

拷贝构造与赋值重载函数之间的关系:

最直观的地方就是——二者共同行为都在进行拷贝,只是发生的时机不同:
对于拷贝构造函数来说:以前面d2、d3来举例,我们希望创建出d3变量的时候,能够同时用d2的值完成对d3的初始化操作;而对于赋值重载函数来说:我们已经创建好了d2变量并完成了初始化操作后,但是我们需要对d2进行修改,这时候可以用赋值操作符来对d2进行修改;所以说赋值重载函数发生在拷贝构造函数之后

且二者都是默认成员函数

//编译器支持如下写法:
Date d4 = d2; 
/*思考:这是拷贝构造还是赋值重载?
从意义上来说:该场景是创建出一个d4变量,同时用d2对d4进行初始化操作,所以更符合拷贝构造函数的意义*/
//类比于:
int a = 10;
int b = a;
//所以这是一种拷贝构造行为

如何写赋值重载函数

//如何写赋值重载函数,还是以日期类为例
class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        this->_year = year;
        _month = month;
        _day = day;
    }
    
    //重载运算符 "=",使其实现赋值功能
    void operator=(const Date& d)
    {
        this->_year = d._year;
        _month = d._month;
        _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

对于需要两个操作数的运算符来说,左右顺序有时候很重要。对于加法来说,加法交换律不影响最终结果,但是对于减法来说,左右顺序会影响结果,所以这时候我们需要严格控制具有两个操作数的运算符

//函数调用过程:
int main()
{
    Date d2(2022, 10, 14);
    Date d4;
    d4 = d2;
    return 0;
}

//对于d4 = d2,我们需要格外注意,赋值操作符需要两个操作数
//而这里是d2赋给d4,所以d4在左,d2在右,所以我们在写函数的时候需要注意左右顺序
//d4 = d2 ==> d4.operator=(d2),这里d4被this指针所接收,d2作为参数传入函数

值得注意的是:虽然赋值的基本功能已经完成,但是我们还需要支持链式赋值的场景

int i, j;
i = j = 10;
//从意义上来说,从右往左执行,规定j = 10之后返回一个值,然后作为右操作数再赋给i从而实现连续赋值
Date d0;
Date d1;
Date d2(2022, 10, 9);
d0 = d1 = d2; //d1 = d2的返回值应该是d2或者d1,二者都行,但是严格来说需要返回左操作数

//所以我们重新修改赋值重载函数:
Date& operator=(const Date& d) //我们本身不需要修改d的内容,所以使用const来缩小权限
{
    this->_year = d._year;
    _month = d._month;
    _day = d._day;
    
       return *this;
}
//因为*this的内容不随栈帧而销毁,所以使用传引用返回减少拷贝次数
//对比:
Date& operator=(const Date& d) //我们本身不需要修改d的内容,所以使用const来缩小权限
{
    this->_year = d._year;
    _month = d._month;
    _day = d._day;
    
       return d; //此处报错,因为返回d会扩大权限,原本d的权限被const的修饰,
//若想返回则需要使用const Date&返回,但是一般规定返回的值是可以被修改的,
//所以这里建议返回*this,而不返回d
}

既然赋值运算符重载函数时默认成员函数,那么我们不主动实现的时候,编译器是如何自行实现?

对于内置类型编译器完成值拷贝,类似于memcpy;而对于自定义类型编译器则会去调用其自身的赋值运算符重载函数,所以大多数情况下拷贝构造和赋值重载都不需要我们主动实现。

但是对于需要写析构函数的类,则需要我们主动实现拷贝构造和赋值重载函数,类似于Stack

typedef int STDatatype;
class Stack
{
public:
    Stack(int capacity = 4)
    {
        _a = (STDatatype*)malloc(sizeof(STDatatype) * capacity);
        _capacity = capacity;
        _top = 0;
    }
    Stack(Stack& d)
    {
        _a = (STDatatype*)malloc(sizeof(STDatatype) * d._capacity);
        memcpy(_a, d._a, sizeof(STDatatype) * d._capacity);
        _top = d._top;
        _capacity = d._capacity;
    }
    void Push(STDatatype x);
    void Pop();
    STDatatype Top();
    bool Empty();
    int Size();
    void Print();
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
private:
    STDatatype* _a;
    int _top;
    int _capacity;
};

//所以这里一样要进行深拷贝:
//操作如下:1.直接先释放掉st1的空间 2.重新开辟空间 3.再进行数据拷贝
//实现如下:
Stack& operator=(const Stack& st)
{
    free(this->_a);
    _a = (STDatatype*)malloc(sizeof(STDatatype) * st._top);
    if (_a == nullptr)
    {
        perror("malloc fail");
        exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * sizeof(_top));
    _top = st._top;
    _capacity = st._capacity;

    return *this;
}
//考虑如下情况:
int main()
{
    Stack st1;
    st1 = st1; //将自己赋值给自己会发生什么?如下图所示:
}

如图所示会发现,st1变成了随机值,为什么?

因为this指针和形参指向同一块空间,上来先将this指针所指向的空间释放,再去开辟空间进行拷贝,所以是随机值,存在越界问题

//我们对代码进行改善:
Stack& operator=(const Stack& st)
{
    //首先对地址进行判断,如果不相等在执行原本逻辑
    if(this != &st)
    {
        free(this->_a);
        _a = (STDatatype*)malloc(sizeof(STDatatype) * st._top);
        if (_a == nullptr)
        {
            perror("malloc fail");
            exit(-1);
        }
        memcpy(_a, st._a, sizeof(int) * sizeof(_top));
        _top = st._top;
        _capacity = st._capacity;
    }
    //最后不管this == &st || this != &st,都正常返回*this即可
    return *this;
}

重载流插入和流提取操作符

//我们有这样的使用场景:
int main()
{
    int a = 10;
    cout << a << endl; //内置类型的输出

    //定义一个日期类对象:
    Date d1;
    cout << d1 << endl //该如何实现日期类对象的输出?
    return 0;
}
//这时候我们需要重载流插入和流提取运算符:
//cin 是 istream 类型对象,cout 是 ostream 类型对象
int i = 1;
double d = 1.11;
int j;
cout << i; //流插入
cout << d; 
cin >> j //流提取
/*为什么cout 和 cin 能自动识别类型?
本质是使用函数重载和运算符重载来实现
i、d是内置类型,cout、cin是自定义类型,只不过是由库来实现,默认支持内置类型;但是识别不了自定义类型,所以需要我们自行实现*/
//和之前一样,我们将流插入、流提取运算符定义在类中:
class Date
{
    void operator<<(ostream& out)
    {
        out << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
}
int main()
{
    Date d1;
    cout << d1;
    return 0;
}
//但是如果我们定义在类中,会面临如下图问题:

//所以我们将<< 和 >> 定义在全局
void operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

注意:一般情况下全局函数不定义在头文件中,因为头文件如果被多处包含,编译的时候展开就会生成多处定义,会报链接错误。在学习C语言中我们就知道:同一个函数,可以多次声明 ,但是只能有一个定义,所以这里有两种解决方案:1、声明和定义分离 2、非要在头文件定义则可以加上static

//但是定义在全局,又会面临无法访问私有成员的问题,这里通过友元来解决:
//友元仅仅是打破了封装,类中声明普通函数加上friend,即可使用该函数访问私有成员
class Date
{
    //在头文件的类中声明友元函数(可以在类中任意位置):
    friend void operator<<(ostream& out, const Date& d);
    friend void operator>>(istream& in, Date& d);
private:
    int _year;
    int _month;
    int _day;
}

//在其他文件定义:
void operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
void operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day >>;
}

** 同样地我们还要支持链式流插入,流提取**

//我们还会遇到如下使用情形:
Date d1, d2;
cin >> d1 >> d2;
cout << d1 << d2 << endl;
//为了支持链式形式,我们需要定义函数的时候具有返回值:
ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    //返回左值(左操作数):
    return out;
}

instream& operator<<(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day >>;
    return in;
}

const成员函数

class Date
{
public
    void Print()
    {
        if ((_year >= 1)
            && (_month >= 1 && _month <= 12)
            && _day <= getMonthDay(_year, _month))
            cout << _year << "/" << _month << "/" << _day << endl;
        else
            cout << "日期非法" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
}

//或许会有这样的使用场景:
int main()
{
    Date d1;
    d1.Print();
    const Date d2; //定义一个const对象
    d2.Print();
    return 0;
}
//但是这样的程序会报错,如下图所示:

//我们还原类中Print函数的真实原形(实际并不能这样写):
void Print(Date* const this) //假设还原出被隐藏的this指针
    {
        if ((_year >= 1)
            && (_month >= 1 && _month <= 12)
            && _day <= getMonthDay(_year, _month))
            cout << _year << "/" << _month << "/" << _day << endl;
        else
            cout << "日期非法" << endl;
    }
/*
this指针最开始被const修饰,仅仅只是保证了this指针的指向不会被恶意修改,保证了传入对象地址的准确性;但是*this并没有被const修饰,所以this指针指向的内容是允许被修改的,这就是this指针正常情况下可读可写的权限。而
当我们外部传入的对象其内容不可被修改时,正常this指针的权限就过大了,所以我们需要额外使用const来修饰*this 
*/
//书写方法如下图:  

//我们对比两个const成员函数的使用例子:
class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        this->_year = year;
        _month = month;
        _day = day;
    }
    
    //d1 > d2
    bool operator>(const Date& d);
    //d1 < d2
    bool operator<(const Date& d);
    
    //d1 - d2
    int operator-(const Date& d)
    {
        Date longDay = *this, shortDay = d;
        int flag = 1;
        //这里接下来分为两种情况:
        if (*this < d) //或者d > *this ?
        {
            longDay = d;
            shortDay = *this;
            flag = -1;
        }
        int count = 0;
        while (longDay != shortDay)
        {
            ++count;
            ++shortDay;
        }
        return count * flag;
    }

    
    void Print() const 
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }

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

private:
    int _year;
    int _month;
    int _day;
};

所以为了解决这种权限放大情况,我们在重载函数后面加上const

//这里是声明和定义分离
bool Date::operator>(const Date& d) const
{
    //...
}

总结:凡是在成员函数内部不改变成员变量(即*this指向的内容),这些成员函数都应该加上const


构造函数与初始化列表

//我们知道对于日期类可以采用如下方法初始化对象:
class Date
{
public:
    Date(int year, int month, int day)
    {
        this->_year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
}
   
//接下来引入初始化列表:
class Date
{
public:
    //对于初始化列表书写格式如下:
    Date(int year, int month, int day)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
}

//栈的初始化列表如何写?
typedef int STDatatype;
class Stack
{
public:
    Stack(int capacity = 4)
        :_top(0)
        , _capacity(capacity)

    {
        _a = (STDatatype*)malloc(sizeof(STDatatype) * capacity);
        if (nullptr == _a)
        {
            perror("malloc fail");
            exit(-1);
        }
        memset(_a, 0, sizeof(int) * capacity);
    }
    Stack(Stack& d)
    {
        _a = (STDatatype*)malloc(sizeof(STDatatype) * d._capacity);
        memcpy(_a, d._a, sizeof(STDatatype) * d._capacity);
        _top = d._top;
        _capacity = d._capacity;
    }
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
private:
    STDatatype* _a;
    int _top;
    int _capacity;
};

看到这里应该对初始化列表有初步认识,那么初始化列表到底有什么特别之处,或者不可缺少之处?

//必须使用初始化列表场景之一 :const成员变量,以如下一段代码为例:

//情况1:没有初始化列表,在构造函数体内赋值
class B
{
public:
    B(int a, int ref)
    {
        _n = 10;
    }

private:
    const int _n;
};

//情况2:没有初始化列表,且构造函数体内不进行相关操作
class B
{
public:
    B(int a, int ref)
        
    {}

private:
    const int _n;
};

//情况3:在声明时给定缺省值
class B
{
public:
    B(int a, int ref)
        
    {}

private:
    const int _n = 10;
    int m = 10;
};

//情况4:给定缺省值,并且给定初始化列表
class B
{
public:
    B(int a, int ref)
        :_n(5)
        ,_m(3)
    {}

private:
    const int _n = 10;
    int _m = 10;
};
//这种情况下并不会采用缺省值

//必须使用初始化列表场景之二:当成员变量为自定义类型且该自定义类型没有合适的默认构造函数
class A
{
public:
    A(int a)
        :_a(a)
    {}
private:
    int _a;
};

class B
{
public:
    B(int a, int ref)
        :_n(a)
        ,_m(ref)
        ,_aa(3)
    {}

private:
    int _m = 10;
    A _aa;
};

//必须使用初始化列表场景之三:成员变量为引用
class A
{
public:
    A(int a)
        :_a(a)
    {}
private:
    int _a;
};

class B
{
public:
    B(int a, int ref)
        :_n(a)
        ,_m(ref)
        ,_aa(3)
        ,_ref(ref);
    {}

private:
    int _m = 10;
    A _aObject;
    int& _ref; //成员变量为引用
};

//引用与const一样,唯一且必须在定义的时候初始化,所以这时候只能使用初始化列表

总结:引用,const,自定义类型成员(且该自定义类型成员没有合适的默认构造),必须使用初始化列表进行初始化,所以以后不管什么场景尽量使用初始化列表,且尽量对一个类提供全缺省默认构造函数

标签: c++ 开发语言

本文转载自: https://blog.csdn.net/Lin_zhiyouyu/article/details/127319431
版权归原作者 Lin_zhiyouyu 所有, 如有侵权,请联系我们删除。

“再谈类和对象相关知识”的评论:

还没有评论