莱莱 发表于 2024-8-13 17:36:37

【C++类和对象(中)】—— 我与C++的不解之缘(四)

前言:

   接下来举行类和对象中的学习,相识类和对象的默认成员函数
https://i-blog.csdnimg.cn/direct/4edaf8db52004d1a8652a687cb8f0224.gif
一、类和对象默认成员函数

        默认成员函数就是用户没有显示实现,编译器会主动生成的成员函数。
一个类,我们不显示实现的环境下,编译器就会默认生成一下留个默认成员函数。
https://i-blog.csdnimg.cn/direct/ee304f42ca3740fca3976e4ab136050e.png
        这里前4个(构造函数、析构函数、拷贝构造和赋值重载)是重难点。
C++11以后还会增加两个默认成员函数,移动构造和移动赋值。
默认成员函数十分紧张,从以下两个方面去深入相识:
   
[*]我们不写时,编译器默认生成的函数行为是什么,是否满意我们的需求
[*]编译器默认生成的函数不满意我们的需求,我们需要直接实现,那么如何本身实现呢?
        1.1、构造函数

        构造函数,是特殊的成员函数;这里需要留意,虽然名字叫做构造函数,但是构造函数的主要任务不是开发空间创建对象(我们经常使用的局部对象是栈帧创建时,空间就已经开发 好了),而是对象实例化时初始化对象。
        这里构造函数本质上是替代实现的Stack和Data类中所写的 Init 函数,构造函数主动调用这一特点就完美替代了 Init 函数。
1.1.1、构造函数的特点

构造函数的特点如下:
   1、函数名和类名相同。
2、无返回值(返回值不需要写,void也不需要)。
3、对象实例化时体系会主动调用对应的构造函数。
4、构造函数可以重载
5、假如类没有显示界说构造函数,C++编译器会主动生成一个无参的默认构造函数;假如显示写了构造函数,编译器就不会再生成。
6、无参构造函数、全缺省构造函数、我们不写时编译器默认生成的构造函数,这三个都叫做默认构造函数。
7、我们不写,编译器默认生成的构造函数,对内置类型成员变量的初始化没有要求(是否初始看编译器);对于自界说类型成员变量,要求调用这个成员函数的默认构造函数初始化(假如这个成员变量没有默认构造函数,就会报错(这里要初始化这个成员变量,需要使用初始化列表来解决,后面会学习到))。
1.1.2、构造函数

        这里来看一下构造函数的前几个特点。
起首就是,构造函数的函数名和类名相同而且无返回值(不需要写返回值)
class Data
{
public:
        Data()
        {
                _year = 1;
                _month = 1;
                _day = 1;
        }

private:
        int _year;
        int _month;
        int _day;

};         这里Data类里面的Data函数就是显示实现的构造函数(显示实现了构造函数,编译器就不会默认生成);
再来看,构造函数可以重载,我们就可以这样写:
class Data
{
public:
        Data()
        {
                _year = 1;
                _month = 1;
                _day = 1;
        }
        Data(int year, int month, int day)
        {
                _year = year;
                _month = month;
                _day = day;
        }

private:
        int _year;
        int _month;
        int _day;

};         这里两个构造函数就形成了重载,再创建对象时,不给参数就会调用第一个构造函数,给参数就会调用第二个参数。
最后,来看对象实例化时会主动构造函数:
#include<iostream>
using namespace std;
class Data
{
public:
        Data()
        {
                cout << "Data()" << endl;
                _year = 1;
                _month = 1;
                _day = 1;
        }
        Data(int year, int month, int day)
        {
                cout << "Data(int year, int month, int day)" << endl;
                _year = year;
                _month = month;
                _day = day;
        }

private:
        int _year;
        int _month;
        int _day;

};
int main()
{
        Data d1;
        Data d2(2006, 7, 20);

        return 0;
} https://i-blog.csdnimg.cn/direct/7775d741bf0c4b38988f3a53d9c1f20e.png
1.1.3、默认构造函数

默认构造函数有三种:
        无参构造函数、全缺省构造函数和我们不写时编译器默认生成的构造函数。
这三个函数有且只有一个存在(不能同时存在);
        这里虽然无参构造函数和全缺省构造函数形成函数重载,但是在函数调用时会存在歧义:
#include<iostream>
using namespace std;
class Data
{
public:
        Data()
        {
                cout << "Data()" << endl;
                _year = 1;
                _month = 1;
                _day = 1;
        }
        Data(int year = 1, int month = 1, int day = 1)
        {
                cout << "Data(int year, int month, int day)" << endl;
                _year = year;
                _month = month;
                _day = day;
        }

private:
        int _year;
        int _month;
        int _day;

};
int main()
{
        Data d1;

        return 0;
} https://i-blog.csdnimg.cn/direct/5db18599a6f946bd8779ac8dc62bef86.png
这里总结一下,编译传实参就可以调用的构作育是默认构造。
1.1.4、编译器默认生成的默认构造函数        

        编译器默认生成的构造函数,对于内置类型(整型,浮点型,字符类型,指针等)初始化没有要求,大概会初始化,也大概不做任那边理;对于自界说类型成员变量初始化会调用这个成员变量的默认构造函数(假如不存在默认构作育报错)。
        以是,大多环境下我们都需要本身实现构造函数。
        1.2、析构函数

        析构函数与构造函数的功能相反,析构函数不是完成对象本身的销毁(局部对象是存在栈帧的,函数竣事栈帧就销毁了,局部对象就主动释放了);C++规定在销毁时会主动调用析构函数,完成对像中资源的清理释放工作。
        干系函数的功能类比之前 Stack 栈实现的DesTroy 销毁功能,对申请资源举行释放。
1.2.1、析构函数特点

   1、析构函数名是在类名前面加上字符 ~
2、无参数返回值(与构造函数一样,不需要加void)。
3、一个类只能有一个析构函数,假如没有显示界说,体系就会主动生成默认的析构函数。
4、对象生命周期竣事时,体系就会主动调用析构函数。
5、与构造函数类似,我们不显示写,编译器默认生成的对内置类型不做处理,自界说类型就会调用它的析构函数。
6、这里需要留意,我们显示写了析构函数,对于自界说类型也会调用它的析构函数(也就是说,无论说明环境下,自界说类型都会主动调用析构函数。
7、假如类没有申请资源,析构函数可以不写,直接使用编译器生成的默认析构函数,就比如Data(日期类);假如默认生成的析构就满意我们的需求,不用写,比如MyQueue(用两个栈实现的队列);假如有资源申请,一定要本身写析构,否则就会造成资源走漏,比如Stack(栈)。
8、一个局部域的多个对象,C++ 规定后界说的先调用析构
1.2.2、析构函数

本身实现析构函数,这里拿栈Stack类来举例:
class Stack
{
public:
        //构造函数
        Stack(int capacity = 4)
        {
                cout << "Stack" << endl;
                _arr = (int*)malloc(sizeof(int) * capacity);
                if (_arr == NULL)
                {
                        perror("malloc fail");
                        return;
                }
                _capacity = capacity;
                _top = 0;
        }

        //析构函数
        ~Stack()
        {
                cout << "~Stack" << endl;
                if (_arr)
                        free(_arr);
                _arr = nullptr;
                _capacity = _top = 0;
        }
private:
        int* _arr;
        int _top;
        int _capacity;
};         这里,对象生命周期竣事时会主动调用析构函数,看一下释放真的调用了?
int main()
{
        Stack st;

        return 0;
} https://i-blog.csdnimg.cn/direct/464a39ff3f0947cdbc9990cfca65a53b.png
1.2.3、自界说类型主动调用其析构函数

        对于自界说类型,无论我们写函数不写析构,都会主动调用其析构函数。
class Stack
{
public:
        //构造函数
        Stack(int capacity = 4)
        {
                cout << "Stack" << endl;
                _arr = (int*)malloc(sizeof(int) * capacity);
                if (_arr == NULL)
                {
                        perror("malloc fail");
                        return;
                }
                _capacity = capacity;
                _top = 0;
        }

        //析构函数
        ~Stack()
        {
                cout << "~Stack" << endl;
                if (_arr)
                        free(_arr);
                _arr = nullptr;
                _capacity = _top = 0;
        }
private:
        int* _arr;
        int _top;
        int _capacity;
};class MyQueue{public:private:        Stack pushst;        Stack popst;};int main(){        MyQueue mq;        return 0;} https://i-blog.csdnimg.cn/direct/bc987eb77f4545269a6f6c5e867358e9.png        

        这里可以看到,自界说类型构造和析构都调用了它的构造函数和析构函数。
        1.3、拷贝构造函数

        假如构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数(也就是拷贝构造函数是特殊的构造函数)。
1.3.1、拷贝构造的特点

   1、拷贝构造函数是构造函数的一个重载。
2、C++规定,自界说类的对象举行拷贝行为必须调用拷贝构造,以是这里自界说类型传值传参和传值返回都会调用拷贝构造完成。
3、拷贝构造的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归。
4、假如未显示界说拷贝构造,编译器会主动生成拷贝构造函数;
5、  像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构作育可以完 成需要的拷⻉,以是不需要我们显⽰实现拷⻉构造。
        像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,以是需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。
        像MyQueue这样的类型内部主要是⾃界说类型 Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现
   MyQueue的拷⻉构造。       6、    传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是假如返回对象是⼀个当前函数局部域的局部对象,函数竣事就销毁了,那么使⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以淘汰 拷⻉,但是⼀定要确保返回对象,在当前函数竣事后还在,才气⽤引⽤返回。    1.3.2、拷贝构造参数

        C++规定自界说类型对象拷贝时必须调用拷贝构造:
class Data
{
public:
        //构造
        Data(int year = 1 , int month = 1, int day = 1)
        {
                cout << "Data(int year, int month, int day)" << endl;
                _year = year;
                _month = month;
                _day = day;
        }
        //拷贝构造
        Data(Data& d)
        {
                cout << "Data(Data&)" << endl;
                _year = d._year;
                _month = d._month;
                _day = d._day;
        }
private:
        int _year;
        int _month;
        int _day;

};
int main()
{
        Data d1(2006, 7, 20);
        Data d2(d1);
        Data d3 = d1;
        return 0;
} https://i-blog.csdnimg.cn/direct/dd2f89e6d39645158f61bf8ff3af6d7d.png
        拷贝构造函数的第一个参数必须是类类型对象的引用,假如使用传值调用:
就会像下面这样,名称传参都会调用拷贝构造,调用完传参再次调用拷贝构造,无穷递归下去。
https://i-blog.csdnimg.cn/direct/e6ad079d15fa492da114167ba2d56b4c.png
1.3.3、编译器默认生成的拷贝构造函数

        编译器默认生成的拷贝构造,对内置类型成员会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自界说类型成员变量就会调用它的拷贝构造函数。
二、赋值运算符重载

        2.1、运算符重载

   1、当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的情势指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
2、运算符重载是具有特殊名字的函数,他的名字是由operator和后面要界说的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
3、重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
4、假如一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
5、运算符重载以后,其优先级和团结性与对应的内置类型运算符保持划一。
6、不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
7、.*    ::   sizeof   ?:   .   留意以上5个运算符不能重载。(选择题里面常考,要记一
重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int
operator+(int x,int y)
8、一个类需要重载哪些运算符,是看哪些运算符重载后故意义,比如Date类重载operator-就故意义,但是重载operator+就没故意义。
9、  重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。
        C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
10、重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。
有了运算符重载,对于自界说类型的对象,我们就可以像内置类型那样,直接举行+/-/+=/-= 等操作。(要 根据自界说类的需求写)。
应用举例:

        这里以日期类为例,实现一个运算符重载 ==
//运算符重载
// ==
bool Data::operator==(Data& d)
{
        if (_year == d._year && _month == d._month && _day == d._day)
        {
                return true;
        }
        return false;
} 常见的运算符重载

        我们根本上可以重载以是的算术运算符、关系运算符和赋值运算符等,
   算术运算符:+、-、*、/ ,用于自界说类型的算术运算。
关系运算符:==、!=、<、>、<=、>= ,用于自界说类型的比较操作。
赋值运算符:=,用于自界说类型的赋值操作。(当自界说类型(栈)包含动态分配的内存时,需要深拷贝以避免悬挂指针等问题。)
自增自减运算符:++、--,用于自界说类型的自增和自减操作。
下标运算符:[ ],用于自界说类型的数组或类似数组的操作。
流插入和提取运算符:<<、>>,用于自界说类型的输入输出操作。
函数调用运算符:(),允许自界说类型的对象像函数一样被调用。
成员访问运算符:->,一般 与智能指针或类似智能指针的类一起使用,用于访问指针所指向对象的成员。
前置++和后置++重载

C++规定
   后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
前置++ 先使用再+1;而后置++是先+1再使用。
//前置++
Data& operator++();
//后置++
Data& operator++(int);         2.2、赋值运算符重载

        赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要留意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
   1、赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成1.const 当前类类型引用,否则会传值传参会有拷贝
2、有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
3、没有显式实现时,编译器会主动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷3贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自界说类型成员变量会调用他的赋值重载函数。
4、  像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器主动生成的赋值运算符重载就4可以完成需要的拷贝,以是不需要我们显示实现赋值运算符重载。
        像Stack这样的类,虽然也都是内置类型,但是 a指向了资源,编译器主动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,以是需要我们本身实现深拷贝(对指向的资源也举行拷贝)。
        像MyQueue这样的类型内部主要是自界说类型Stack成员,编译器主动生成的赋值运算符重载会调用Stack的赋值运算符重载也不需要我们显示实现MyQueue的赋值运算符重载。
这里还有一个小技巧,假如一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
        现在就来哦实现日期类巩固这方面的知识。
        2.3、日期类的实现

Data.h:
#pragma once
#include<iostream>
#include<assert.h>
class Data
{
        friend std::ostream& operator<<(std::ostream& out, const Data& d);
        friend std::istream& operator>>(std::istream& in, Data& d);
public:
        //构造函数
        Data(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        //析构
       
        //拷贝构造
        Data(Data& d)
        {
                _year = d._year;
                _month = d._month;
                _day = d._day;
        }
        //输出
        void Print();

        //运算符重载
        bool operator==(Data& d);
        bool operator!=(Data& d);
        bool operator<(Data& d);
        bool operator<=(Data& d);
        bool operator>(Data& d);
        bool operator>=(Data& d);

        Data& operator+=(int day);
        Data& operator+(int day);
        Data& operator-=(int day);
        Data& operator-(int day);
        int operator-(Data& d);

        Data& operator++();
        Data& operator++(int);
        Data& operator--();
        Data& operator--(int);

        //ostream& operator<<(ostream& out);
private:
        int _year;
        int _month;
        int _day;
}; Data.cpp:
#include"Data.h"using namespace std;//得到当前月份天数int GetMonthDay(int year, int month){        assert(month > 0 && month < 13);        static int arr = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };        if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 40 == 0)))        {                return 29;        }        return arr;}//输出void Data::Print(){        cout << _year << "年" << _month << "月" << _day << "日" << endl;}//运算符重载
// ==
bool Data::operator==(Data& d)
{
        if (_year == d._year && _month == d._month && _day == d._day)
        {
                return true;
        }
        return false;
}// !=bool Data::operator!=(Data& d){        return !(*this == d);}// < bool Data::operator<(Data& d){        if (_year < d._year)        {                return true;        }        else if (_year == d._year && _month < d._month)        {                return true;        }        else if (_year == d._year && _month == d._month && _day < d._day)        {                return true;        }        return false;}//<=bool Data::operator<=(Data& d){        return (*this) == d || (*this) < d;}// >bool Data::operator>(Data& d){        if (_year > d._year)        {                return true;        }        else if (_year == d._year && _month > d._month)        {                return true;        }        else if (_year == d._year && _month == d._month && _day > d._day)        {                return true;        }        return false;}// +=bool Data::operator>=(Data& d){        return (*this) == d || (*this) > d;}Data& Data::operator+=(int day){        _day += day;        while (_day > GetMonthDay(_year, _month))        {                _day -= GetMonthDay(_year, _month);                _month++;                if (_month == 13)                {                        _month = 1;                        _year++;                }        }        return *this;}// +Data& Data::operator+(int day){        Data tmp = *this;        tmp += day;        return tmp;}// -=Data& Data::operator-=(int day){        _day -=day;        while (_day <= 0)        {                _month--;                if (_month == 0)                {                        _month = 12;                        _year--;                }                _day += GetMonthDay(_year, _month);        }        return (*this);}// - 天数Data& Data::operator-(int day){        Data d(*this);        d -= day;        return d;}// - 日期int Data::operator-(Data& d){        Data min(*this);        Data max(d);        if ((*this) > d)        {                min = d;                max = *this;        }int count = 0;        while (min != max)        {                count++;                min += 1;        }        return count;        }//++//前置++Data& Data::operator++(){        (*this) += 1;        return *this;}//后置++Data& Data::operator++(int){        Data d(*this);        (*this) += 1;        return d;}//前置--Data& Data::operator--(){        (*this) -= 1;        return *this;}//后置--Data& Data::operator--(int){        Data d(*this);        (*this) -= 1;        return d;}// <<//ostream& Data::operator<<(ostream& out)//{//        out << _year << "年 " << _month << "月 " << _day << "日 " << endl;//        return out;//}std::ostream& operator<<(std::ostream& out, const Data& d){        cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;        return out;}//>>std::istream& operator>>(std::istream& in, Data& d){        cout << "依次输入 年 月 日" << endl;        in >> d._year >> d._month >> d._day;        return in;}
三、取地址运算符重载

        3.1、const 成员函数

   1、 将const修饰的成员称之为从const成员函数,const成员放到成员函数参数列表的后面。
2、 const实际修饰该成员函数的this指针,表明在该成员函数中不能对类的任何成员举行修改。
3、 const修饰Data类的Print成员函数,
Print隐含的this指针由Data* const this 变为const Data* const this
#include<iostream>
using namespace std;
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }
        // void Print(const Date* const this) const
        void Print() const
        {
                cout << _year << "-" << _month << "-" << _day << endl;
        }
private:
        int _year;
        int _month;
        int _day;
};
int main()
{
        // 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
        Date d1(2024, 7, 5);
        d1.Print();
        const Date d2(2024, 8, 5);
        d2.Print();
        return 0;
}         3.2、 取地址运算符重载

           取地址运算符重载分为平凡取地址运算符重载和const取地址运算符重载,一般这两个函数编译器主动生成的就可以够我们用了,不需要去显示实现。
class Data
{
public:
        Data* operator&()
        {
                return this;
        }
        const Data* operator&()const
        {
                return this;
        }
private:
        int _year;
        int _month;
        int _day;
}; 这里我们不想要访问到类对象的地址,也可以返回nullptr。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【C++类和对象(中)】—— 我与C++的不解之缘(四)