【c++笔记】类和对象流食般投喂(中)

声明:以下知识相关资料来自比特官网和小编手搓~
类和对象(中)

++1、类的默认成员函数++

++2、构造函数++

++3、析构函数++

++4、拷贝构造函数++

++5、赋值运算符重载++

5.1 运算符重载

5.2 赋值运算符重载

5.3 日期类实现

++6、取地址运算符重载++

6.1 const成员函数

6.2 取地址运算符重载

1、类的默认成员函数

默认成员函数的定义就是用户没有显示实现,编译器自己生成的成员函数。对于一个类,他的默认成员函数是有六种,我们重点学习掌握前四种,后两种仅作了解,此外,在C++11之后,又新增了移动构造和移动赋值这两个默认成员函数,此篇不涉猎。

2、构造函数

构造函数的定位是属于特殊的成员函数,这个函数的名字虽然叫做构造,但他干的是 Init() 初始化的活,在类实例化出对象时对对象进行初始化,就像在房子建好后对你买的房子进行装修,这句形象的描述同时也可以解释一个很容易进的误区,就是构造函数并不会干开空间的活,不要被他的名字所迷惑,我们经常使用的局部对象在栈帧创建时,"系统"就给局部变量开好空间了。

构造函数的特点:包含了如何显示实现的一些规定,以及在类中的特殊用法。

1、函数与类同名

2、函数没有返回值(没有void等等啥的)

3、类实例化出对象时,系统会自动调用对应的构造函数

4、构造函数支持重载

5、如果用户没有显示实现构造函数,编译器会自动生成一个无参的默认构造函数

注意:默认构造函数并不是单单系统自己生成的,还包括用户显示实现的无参构造函数和全缺省的构造函数,其实默认构造函数应该叫做0参数构造函数。

6、不传实参就可以调用的构造就是默认构造函数。这三个函数都属于默认构造函数,但是在一个类里面只能存在一个,而且,用户显示实现的无参构造函数与全缺省构造函数是可以构成函数重载的,调用时会存在歧义。

7、对于内置类型,C++没有强制规定是否要初始化,这个就纯看编译器了;而对于自定义类型,比如类,会出现一个特殊情况,就是用户并没有显示实现任何一个构造函数,而编译器自己生成的默认构造函数又不能完全初始化好类实例化出的对象,此时就阴沟翻船了,此时就需要用初始化列表了,下一篇再流食般投喂。

cpp 复制代码
class Date
{
public:
    // 1.⽆参构造函数
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    // 2.带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // 3.全缺省构造函数
    /*Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }*/

private:
    int _year;
    int _month;
    int _day;
};
cpp 复制代码
int main()
{
    // 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉
    // 编译报错:error C2512: "Date": 没有合适的默认构造函数可⽤
    Date d1; // 调⽤默认构造函数
    Date d2(2025, 1, 1); // 调⽤带参的构造函数

    // 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法
    // 区分这⾥是函数声明还是实例化对象
    // warning C4930: "Date d3(void)": 未调⽤原型函数(是否是有意⽤变量定义的?)
    Date d3();

    return 0;
}
cpp 复制代码
class MyQueue
{
public:
    //编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
    Stack pushst;
    Stack popst;
};

3、析构函数

析构函数的定位同样是特殊的成员函数,他的功能是对资源的清理和释放,与构造函数功能相反,他并不是对对象本身进行销毁工作,这个工作是属于free的,比如存储数据的数据结构空间不够了,需要动态扩容申请空间,这申请来的空间就是资源,析构函数是对这些资源进行清理和释放,在C++中,对象的销毁会自动调用析构函数先将申请来的资源清理释放掉,再把对象本身销毁,这其实也是delete的底层,这里先提一嘴,后面再流食投喂,此外,一个对象并没有资源的申请,那其实是不需要调用析构函数的。

析构函数的特点:

1、析构函数名是在类名的前面加一个 ~

2、析构函数没有返回值(没有void啥的)

3、一个类只能有一个析构函数,用户没有显示实现析构函数,编译器会自动生成默认的析构函数

4、当一个对象生命周期结束时,系统会自动调用析构函数

5、跟构造函数类似,用户不显示实现析构函数,编译器自己生成的默认析构函数是对内置类型没要求的,自定义类型成员会调用他自己的析构函数。

6、对于自定义类型成员,C++强制规定,不论用户有没有显示写析构函数,自定义类型成员一定会调用他自己的析构函数。

7、对于有些对象并没有进行资源的申请,其实是可以不用写析构函数的,但是有资源申请的对象一定要写析构函数,不然会出现内存泄漏的情况。

8、一个局部域有多个对象,C++规定,后定义的先析构,其实这个析构函数调用的顺序是跟栈帧的销毁是一一对应的。

cpp 复制代码
// 两个Stack实现队列
class MyQueue
{
public:
    //编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
    // 显⽰写析构,也会⾃动调⽤Stack的析构
    /*~MyQueue()
    {}*/

private:
Stack pushst;
Stack popst;
};

4、拷贝构造函数

拷贝构造的定位是一种特殊的构造函数,是构造函数的一个重载,他的功能本质是对对象初始化,但是他有点特殊,他是用一个已有的同类型对象对要初始化的对象进行初始化工作。所以拷贝构造函数的第一个参数是自身类类型的引用,然后额外的参数规定都有默认值。

拷贝构造函数的特点:

1、拷贝构造函数是构造函数的一个重载

2、拷贝构造函数的第一个参数一定是自身类类型的引用,使用传值方式会因语法的设定触发无穷递归。拷贝构造函数也可以有多个参数,但是规定这些额外的参数都必须有缺省值。

3、C++规定,自定义类型的对象进行拷贝行为必须要调用拷贝构造函数,所以自定义类型传值传参和传值返回的那步拷贝,都是调用拷贝构造函数完成的。

4、用户没有显示实现拷贝构造函数,编译器会自动生成默认的拷贝构造函数,这个函数对于内置类型成员变量是有规定的,会一个字节一个字节的完成值拷贝(或者叫做浅拷贝),对于自定义类型成员变量会调用他自己的拷贝构造。

5、编译器自动生成的默认拷贝构造函数定位是属于浅拷贝,只适用于全部成员变量都是内置类型且没有资源的申请,以及全部成员变量都是自定义类型(默认拷贝构造会调用自定义类型成员变量自己的拷贝构造);一旦触及资源的申请,用户必须显示实现拷贝构造函数,所以用户显示实现了析构函数,那么用户也需要显示实现拷贝构造函数(定位属于深拷贝,需要把资源也拷贝一份)。

6、传值返回需要一个临时对象(未命名且只在代码的当前行是活的),调用拷贝构造;传值引用返回,返回的是目标对象的别名,不存在拷贝,但是目标对象的生命周期得长,出了函数局部域得存在,不存在的话,情况就如同返回的是野指针一样,这样就是纯纯野引用了。
对于特点二标红处解释:

他的理想情况:为了将d2拷贝构造成d1,调用拷贝构造函数,将d1传给形参d,编译器计划用形参d给d2初始化赋值的,但是意外出现,d1->d的过程是传值,伴生调用拷贝构造函数,现在的目的是将d拷贝构造成d1,为了完成目的,再调用拷贝构造函数,d1传给形参dd,编译器计划用dd给d初始化赋值的,但是意外又出现了,d1->dd的过程是传值,伴生调用拷贝构造函数,现在的目的是将dd拷贝构造成d1,为了完成目的,再再次调用拷贝构造函数,d1传给形参ddd,编译器计划用ddd给dd初始化赋值的,结果又又出意外了...

cpp 复制代码
int main()
{
    Date d1(2024, 7, 5);
    // C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥传值传参要调⽤拷⻉构造
    // 所以这⾥的d1传值传参给d要调⽤拷⻉构造完成拷⻉,传引⽤传参可以较少这⾥的拷⻉
    Func1(d1);

    cout << &d1 << endl;

    // 这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
    Date d2(&d1);
    d1.Print();
    d2.Print();
 
    //这样写才是拷⻉构造,通过同类型的对象初始化构造,⽽不是指针
    Date d3(d1);
    d2.Print();

    // 也可以这样写,这⾥也是拷⻉构造
    Date d4 = d1;
    d2.Print();

    // Func2返回了⼀个局部对象tmp的引⽤作为返回值
    // Func2函数结束,tmp对象就销毁了,相当于了⼀个野引⽤
    Date ret = Func2();
    ret.Print();

    return 0;
}
cpp 复制代码
// 两个Stack实现队列
class MyQueue
{
public:

private:
    Stack pushst;
    Stack popst;
};

int main()
{
    Stack st1;
    st1.Push(1);
    st1.Push(2);
    // Stack不显⽰实现拷⻉构造,⽤⾃动⽣成的拷⻉构造完成浅拷⻉
    // 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
    Stack st2 = st1;
    MyQueue mq1;
    // MyQueue⾃动⽣成的拷⻉构造,会⾃动调⽤Stack拷⻉构造完成pushst/popst
    // 的拷⻉,只要Stack拷⻉构造⾃⼰实现了深拷⻉,他就没问题
    MyQueue mq2 = mq1;
    return 0;
}

5、赋值运算符重载

5.1、运算符重载

我们之前在C语言学习的运算符全部都是内置类型的相互运算,编译器只认得内置类型,知道运算符在对内置类型进行什么样的运算,而对于自定义类型对象而言,就像是寄人篱下的孩子,编译器这个家长利益至上,需要给钱,编译器才能养,而运算符重载就像是一张黑卡,给了编译器之后,把自定义类型变量当作亲孩子一样对待了。

运算符重载的定位是具有特殊函数名的函数,特殊函数名由 operator + 要定义的运算符 构成,函数的其他东西他全部都有。

重载运算符函数的参数个数与运算符是几目是对应的,如果是二目操作符,左侧操作对象传参传给第一个参数,右侧操作对象传给第二个参数。

如果运算符重载写在类里面当一个成员函数,那么重载运算符函数的第一个参数就是this指针了,显示参数将会比操作对象少一个。

运算符重载后,优先级和结合性没有变化,其实是扩大了操作对象的范围。

运算符重载的都是之前已有的运算符,用户不能自己编啊。

,这五个运算符规定了就是不能重载的哦。

重载操作符的参数必须至少有一个是类类型参数,这个运算符重载本身就是给编译器钱,让编译器照顾自定义类型变量,结果你不把照顾对象送过去,这不纯送钱吗***,不能通过运算符重载对内置类型的含义进行重定义哦。

运算符重载需要考虑重载的运算符是否具有意义,我们不做无意义的事。

重载前置++以及后置++时,重载运算符函数的函数名时相同的,为了区分,我们给后置++重载时,添了一个int形参,让前置++和后置++的重载运算符函数构成函数重载,方便区分。

重载 << 和 >> 这两个运算符时,是不能在类中重载为成员函数的,因为this指针是第一个参数,会抢占 << 和 >> 操作符的左操作对象的位置,变成 对象<<cout/cin,这是不符合我们书写习惯的,那就把 << 和 >> 的重载函数写在全局中即可。

cpp 复制代码
// 编译报错:"operator +"必须⾄少有⼀个类类型的形参
int operator+(int x, int y)
{
    return x - y;
}
cpp 复制代码
typedef void(A::*PF)(); //成员函数指针类型
cpp 复制代码
int main()
{
    // C++规定成员函数要加&才能取到函数指针
    PF pf = &A::func;
    A obj;//定义ob类对象temp
    // 对象调⽤成员函数指针时,使⽤.*运算符
    (obj.*pf)();
    return 0;
}
cpp 复制代码
// 运算符重载函数可以显⽰调⽤
operator==(d1, d2);

// 编译器会转换成 operator==(d1, d2);
d1 == d2;
cpp 复制代码
class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    Date& operator++()
    {
        cout << "前置++" << endl;
        //...
        return *this;
    }

    Date operator++(int)
    {
        Date tmp;
        cout << "后置++" << endl;
        //...
        return tmp;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2024, 7, 5);
    Date d2(2024, 7, 6);

    // 编译器会转换成 d1.operator++();
    ++d1;
    // 编译器会转换成 d1.operator++(0);
    d1++;

    return 0;
}

5.2、赋值运算符重载

赋值运算符重载的定位是一个默认成员函数,他的操作对象是两个已经存在的对象,把一个已经存在的对象赋值给另一个已经存在的对象,与拷贝构造是有很大不同的,拷贝构造是对类实例化出对象时,用一个同类型对象进行拷贝初始化。

赋值运算符重载的特点:

1、赋值运算符重载是一个运算符重载,C++规定,赋值运算符重载必须重载为成员函数,这个重载运算符函数本质就是为了对象之间的互相拷贝,为了效率,自然建议用户将函数参数写成const当前类类型的引用。

2、重载赋值运算符函数是有返回值的,同样建议写成当前类类型的引用,以此提高效率,有返回值的设定就是为了支持连续赋值的场景。

3、用户没有显示实现赋值运算符重载,编译器会自动生成一个默认赋值运算符重载,他与拷贝构造类似,对内置类型成员变量是一个字节一个字节的浅拷贝(也叫做值拷贝),对于自定义类型的成员变量会调用他自己的赋值运算符重载。

4、成员变量全部是内置类型且没有对资源的申请或者成员变量全部是自定义类型的类,编译器自己生成的默认赋值运算符重载就够使了;只有触及资源的申请,用户才需要显示实现运算符重载,所以跟拷贝构造一样,看见析构显示实现,就需要显示实现赋值运算符重载。

cpp 复制代码
// 传引⽤返回减少拷⻉
// d1 = d2;
Date& operator=(const Date& d)
{
    // 不要检查⾃⼰给⾃⼰赋值的情况
    if (this != &d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    // d1 = d2表达式的返回对象应该为d1,也就是*this
    return *this;
}
cpp 复制代码
int main()
{
    Date d1(2024, 7, 5);
    Date d2(d1);

    Date d3(2024, 7, 6);
    d1 = d3;

    // 需要注意这⾥是拷⻉构造,不是赋值重载
    // 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值
    // ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
    Date d4 = d1;

    return 0;
}

6、取地址运算符重载

6.1、const成员函数

一个成员函数被const修饰了,那么这个函数被叫做const成员函数,const写法是写在成员函数参数列表的后面。

const实际修饰的是this指针,让this指针指向的内容也不能修改了,this指针由 Date* const this -> const Date* const this。

cpp 复制代码
// void Print(const Date* const this) const
void Print() const
{
    cout << _year << "-" << _month << "-" << _day << endl;
}
cpp 复制代码
int main()
{
    // 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
    Date d1(2024, 7, 5);
    d1.Print();
    
    const Date d2(2024, 8, 5);
    d2.Print();
    
    return 0;
}

6.2、取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,这里编译器自动生成的默认重载运算符函数就够我们使用了,只有在很特殊的场景才需要用户去显示实现取地址运算符重载,比如被人用我的函数,我想皮一下,不想让对方取到正确的函数地址,可以显示实现返回一个随机地址。

cpp 复制代码
class Date
{
public :
    Date* operator&()
    {
        return this;
        // return nullptr;
    }

    const Date* operator&()const
    {
        return this;
        // return nullptr;
    }
private :
    int _year ; // 年
    int _month ; // ⽉
    int _day ; // ⽇
};
相关推荐
csbysj20201 小时前
C 语言输入与输出(I/O)详解
开发语言
Huangjin007_1 小时前
【C++ STL篇(八)】set容器——零基础入门与核心用法精讲
开发语言·c++·学习
许长安1 小时前
Kafka 架构讲解:从提交日志到分区副本机制
c++·经验分享·笔记·分布式·架构·kafka
邪修king1 小时前
UE5 TA 核心修炼:材质与纹理艺术全解 —— 从 PBR 理论到工业级材质实战
c++·后端·游戏·ue5·材质
c#上位机1 小时前
C#项目中打包文件的三种方式
开发语言·c#
hehelm1 小时前
C++ 特殊类设计
开发语言·c++
吃好睡好便好1 小时前
在Matlab中绘制圆锥三维曲面图
开发语言·人工智能·学习·算法·matlab·信息可视化
摇滚侠1 小时前
并发编程 Java 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·开发语言
小袁说公考1 小时前
2026广东公考培训标杆深度解析:广东粉笔——科技赋能本土,领跑粤考赛道
大数据·人工智能·经验分享·笔记·科技·其他