栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【C++】类和对象(第二站)默认成员函数+操作符重载

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【C++】类和对象(第二站)默认成员函数+操作符重载

本篇是类和对象的第二站

主要讲述类的几个默认成员函数,以及操作符重载

本篇博客会涉及到很多之前C++专栏里面提到的知识点,建议连起来观看。

  • 感谢你关注慕雪,欢迎来我的寒舍坐坐❄慕雪的寒舍
  • 我用游戏语音软件创建了一个代码学习社区,欢迎加入!链接

文章目录
  • 默认成员函数
  • 1.构造函数
    • 1.1特性
    • 1.2基本使用
    • 1.3编译器默认生成的构造函数
    • 1.4规范命名类的成员变量
  • 2.析构函数
    • 2.1特性
    • 2.2基本使用
  • 3.拷贝构造
    • 3.1特性和使用
    • 3.2外置类型拷贝问题
    • 3.3深拷贝
      • 3.3.1new和delete
      • 3.3.2深拷贝实现
      • 3.3.3深拷贝效果
  • 4.运算符重载
    • 4.1定义
    • 4.2基本使用
    • 4.3赋值运算符重载
    • 4.4拷贝构造和赋值重载的调用问题
  • 5.const成员
    • 5.1用const修饰类的成员函数
      • ①实例-权限问题
      • ②什么时候需要使用?
    • 5.2取地址及对const取地址重载
  • 日期类的实现
  • 结语

默认成员函数

当我们创建一个类的时候,即便类里面啥都不放,都会自动生成下面6个默认成员函数

它们都有啥功能呢?且听我一一道来

1.构造函数

众所周周知,当我们写C语言的顺序表、链表等代码的时候,一般都会写一个Init函数来初始化内容。

void Init()
{
	a=(int*)malloc(sizeof(int)*4);
	size=0;
	capa=4;
}

但是这样有一个缺点,就是不够智能,需要我们自己来调用它进行初始化。

于是C++就整出来了一个构造函数来解决这个问题

1.1特性

构造函数:名字和类名相同,创建类对象的时候编译器会自动调用,初始化类中成员变量,使其有一个合适的初始值。构造函数在对象的生命周期中只调用一次

构造函数有下面几个特性:

  1. 函数名和类名相同
  2. 无返回值
  3. 构造函数可以重载
  4. 对象实例化的时候,编译器会自动调用对应的构造函数
  5. 如果你自己不写构造函数,编译器会自己创建一个默认的构造函数
1.2基本使用

下面用一个队列来演示一下构造函数

class Queue{
public:
    Queue()
    {
        cout<<"Queue Init"<
        cout< 

可以看到,在创建对象q1的时候,编译器就自动调用了类中的构造函数,帮我们初始化了这个队列


除了上面这种最基本的无参构造函数以外,一般写构造函数的时候,我们都会带一个有缺省值的参数,这样可以更好地灵活使用这个队列

Queue(int Capacity=4)
{
    _a=(int*)malloc(sizeof(int)*Capacity);
    _size=0;
    _capa=Capacity;
}

调用这种构造函数也更加灵活,我们可以根据数据类型的长度,来创建不同容量的队列,避免多次realloc造成的内存碎片

Queue q1;//调用无参的构造函数
Queue q2(100);//调用带参的构造函数

多种构造函数是可以同时存在的,不过!它们需要满足函数重载的基本要求

当你调用一个无参的函数,和一个全缺省的函数的时候,编译器会懵逼!

Queue();
Queue(int Capacity=4);
//这两个函数不构成重载,会报错

正确的重载应该是下面的情况

Queue();
Queue(int Capacity);

编译器在创建对象的时候,就会智能选择这两个构造函数其中之一进行调用。但是同一个对象只会调用一个构造函数

1.3编译器默认生成的构造函数

上面提到过,如果我们不写构造函数,编译器会自己生成一个。

但测试过以后,你会发现,这个默认生成的构造函数,好像啥事都没有干——或者说,它把_a _b _c 都初始化成了随机值!

实际上,编译器默认生成的构造函数是不会处理内置类型的

  • 内置类型:int、char、float、double……
  • 外置类型:自定义类型(其他的类)

在处理的时候,编译器忽略内置类型;外置类型会调用它的构造函数

class Date{
public:
    //默认构造函数:不传参就能调用的
    //1.全缺省 2.无参 3.编译器自动生成
    //可以是半缺省的,但是不实用
    Date(int year=2022,int month=2,int day=30)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    void Print()
    {
        cout<<_year<<"-"<<_month<<"-"<<_day< 

可以看到,编译器调用了自己的构造函数的同时,还调用了外置类型Queue的构造函数,搞定了它的初始化

如果我们去掉Date的构造函数,就能看到下面的情况。Queue成功初始化,但是内置类型的年月日都是随机值

一般情况下一个C++类都需要自己写构造函数,下面这两个情况除外

  1. 类里面的成员都是自定义类型成员(且有自己的构造函数)
  2. 如果还有内置类型成员,声明时给了缺省值

注:只有类在声明变量的时候才可以给缺省值

//下面的情况就不需要写
class MyS{
    
private:
      Queue q1;//自定义类型
      Queue q2;
      int a=1;//内置类型声明的时候给了缺省值
};


1.4规范命名类的成员变量

为了更好的使用构造函数,以及区分类内外的函数类型

一般我们定义类中的成员变量的时候,都会使用一个下划线进行标明_YEAR

在一些地方,你会看到函数名前面也带了一个_,这一般表明该函数是另外一个函数的子函数,同样是用于区分的。

不同人的代码风格不同,你可以选择你自己喜欢的风格,但不能影响我们程序的正常使用

比如下面这种情况,就会影响类的构造了

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

请问year=year里面的这个year,到底是成员变量,还是构造函数的传参呢?编译器又双懵逼了

实际上,编译器在找year的时候,会先在当前{ }中找,找到了传参的year,就不会去找其他地方的year了。所以这个语句实际上是传参过来的year自己给自己赋值,编译器会报错。


2.析构函数

和构造函数相对应,析构函数是对象在出了生命周期后自动调用的函数,用来爆破对象里的成员(如进行free操作)

生命周期是离这个对象最近的{ }括号

2.1特性
  • 析构函数名是在类名前加~
  • 无参数,无返回值
  • 一个类只能有一个析构函数
  • 如果你没有自己写,编译器会自动生成一个析构函数

和构造函数一样,编译器自己生成的析构函数不会处理内置类型;会调用外置类型的析构函数

2.2基本使用

析构函数的定义和我们在外部写的Destroy函数一样,主要执行free操作

#include
#include
using namespace std;

class Queue{
public:
    Queue()
    {
        cout<<"Queue"<
        cout<
        //析构函数
        free(_a);
        _a=nullptr;
        _size=_capa=0;
        cout<<"distory:"< 

假设我们在main函数里面定义了两个对象,你能说出q1和q2谁先进行析构函数的调用吗?

可以看到,先调用的是q2的析构函数

因为在底层操作中,编译器会给main函数开辟栈帧

栈遵从后进先出的原则,q2是后创建的,所以在析构的时候会先析构


3.拷贝构造 3.1特性和使用

拷贝构造是一个特殊的构造函数,它的参数是另外一个Date类型。在用已有的类类型对象来创建新对象的时候,由编译器自动调用

因为拷贝的时候我们不会修改d的内容,所以传的是const

另外,我们必须进行传引用调用!

这里补充说明一下,下面的这个函数,在传参的时候,编译器会去调用Date的拷贝构造

void func(Date d);

如果你没有写拷贝构造,或者拷贝构造里面不是传引用,编译器会就递归不断创建新的对象进行值拷贝构造,程序就死循环辣

//拷贝构造,如果不写的时候,编译器会默认生成一个
    //对内置类型进行值拷贝(浅拷贝)
    Date(const Date& d)
    {
        _year=d._year;
        _month=d._month;
        _day=d._day;
        //外置类型会调用外置类型的拷贝构造
        Queue b(_A);
    }

和构造、析构不同的是,编译器自己生成的拷贝构造终于有点用了

  • 它会对内置类型进行按内存存储的字节序完成拷贝,这种称为值拷贝(又称浅拷贝)
  • 对外置类型会调用它的构造函数

3.2外置类型拷贝问题

但是!如果你使用了外置类型,该类型中包含malloc的时候,编译器默认生成的构造函数就不能用辣!

因为这时候,编译器默认生成的拷贝构造会进行值拷贝,拷贝完了之后,就会出现q1和q2指向同一个空间的情况。修改q2会影响q1,free的时候多次释放同一个空间会报错,不符合我们的拷贝构造的要求

注意注意,malloc不行的原因是,数据是存在堆区里面,拷贝的时候,q2的_a得到的是一个地址,而不是拷贝了新的数据内容。

如果你在类里面定义了一个int arr[10]数组,这时候拷贝构造就相当于memcpy,是可以完成拷贝的工作的。

如何解决这个问题呢?我们需要使用深拷贝

这里我还没有学到那个地方,后续写深浅拷贝的博客的时候,再来填上这个坑

黑马16分钟视频速成完毕,前来填坑

3.3深拷贝 3.3.1new和delete

这里先给大家从C语言转到C++,讲解一下new和delete关键字,它们分别对应malloc和free

非常简单!比malloc的使用简单多了!

int main()
{
    int*p1=new int;//开辟一个int类型的空间
    int*p2=new int(10);//开辟一个int类型的空间,并初始化为10
    int*p3=new int[10];//开辟10个int类型的空间
    //注意后两个的括号区别!
    
    delete p1;//销毁p1指向的单个空间
    delete p2;//同上
    
    //delete p3;//销毁p3指向的第一个空间,不能用于数组
    delete[] p3;//销毁p3指向的数组
    
    return 0;
}

怎么样?是不是超级简单!


3.3.2深拷贝实现

在上面写道过,编译器会自动生成拷贝构造函数,完成值拷贝工作。但是队列的代码里面包含堆区的空间,需要我们正确释放。这时候就需要自己写一个拷贝构造完成深拷贝

//拷贝构造
Queue(const Queue& q)
{
    _a=new int[*q._a];//注意解引用
    memcpy(_a, q._a, q._capa*sizeof(int));//拷贝内容
    _size=q._size;
    _capa=q._capa;
}

用下面这个队列和日期类的混合代码用来测试深拷贝

#include
#include
#include
using namespace std;

class Queue{
public:
    Queue()
    {
        cout<<"Queue Init"<
            _a[i]=i+1;
        }
    }
    //拷贝构造
    Queue(const Queue& q)
    {
        cout<<"Queue Copy"<
        cout<<"this:"<
            cout<<_a[i]<<" ";
        }
        cout<
        //析构函数
        //free(_a);
        delete[] _a;
        _a=nullptr;
        _size=_capa=0;
        cout<<"distory:"<
public:
    Date(int year=2022,int month=2,int day=30)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    void Print()
    {
        cout<<_year<<"-"<<_month<<"-"<<_day<
        _year=d._year;
        _month=d._month;
        _day=d._day;
        //外置类型会调用外置类型的拷贝构造
        //这时候我们没有写队列的拷贝构造,编译器自动生成
        //结果就是,两个队列的int*a会指向同一个空间
        //析构函数free的时候报错
        Queue b(_A);
    }
private:
    //内置类型:int,char,double等等
    int _year;
    int _month;
    int _day;
    //外置类型:自定义类型
    Queue _A;
};

int main()
{ 
    Queue q1;
    q1.Print();
    cout< 
3.3.3深拷贝效果 

先注释掉Queue的拷贝构造函数析构函数(不然会报错)

看一看,发现在不写拷贝构造函数的时候,q2和q1的_a指向了同一个地址

取消析构函数的注释,可以看到两次释放同一片空间,发生了报错

如果我们把写好的深拷贝构造加上,就不会出现这个问题

当你加上给_a里面初始化一些数据,以及打印_a数据的函数后,就可以看到,不仅q2的_a有了自己全新的地址,其内部的值也和q1一样了

这样写出来的拷贝构造,即便把队列中的int* _a修改为char*或者其他类型,都能正确完成拷贝工作

这里有一个小点哈,就是打印char* _a的地址的时候,咱需要用printf而不是cout,因为cout会把_a直接当作字符串打印了,效果就变成了下面这样

用printf来控制输出格式为%x即可

printf("_a:%x ",_a);

4.运算符重载 4.1定义

在讲解赋值运算符重载之前,我们可以来认识一下完整的运算符重载

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

函数名为:关键字 operator运算符,如operator=

函数原型:返回值类型 operator操作符(参数列表),如Date operator=();

下面有几点注意:

  • 重载操作符必须有一个自定义类型的操作数(即操作符重载对内置类型无效)
  • 不能通过其他符号来创建新的操作符
  • 对于类类型的操作符重载,形参比操作数少一个传参(因为有一个默认的形参this指针)
  • 这5个操作符是不能重载的:.*、::、sizeof、? :、.
4.2基本使用

以下是在全局定义的操作符重载,用于判断日期是否相等

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

当我们在main函数中使用d1==d2的时候,编译器就会自动调用该操作符重载

当然,你也可以自己来传参使用,如if(operator==(d1,d2))但是这样非常不方便,和调用一个而普通函数没啥区别,压根算不上操作符重载


当我们把它放入类Date中间,就需要修改成下面这样

bool operator==(const Date& d2)
{
    return d1._year == d2._year;
    && d1._month == d2._month
    && d1._day == d2._day;
}

编译器在调用的时候,会优化成下面这样

bool operator==(Date* this, const Date& d2)
//显示调用为 d1.operator==(d2);

后续会以日期类为样板,实现更多的操作符重载

4.3赋值运算符重载

因为每一个类都有不同的成员,编译器不可能智能的进行赋值操作。这时候就需要我们自己写一个赋值运算符重载来进行赋值操作了

以日期类为例,赋值操作其实就是把内置类型成员一一赋值即可

Date& operator=(const Date& d){
    if(this != &d)//避免自己给自己赋值
    {
        _year=d._year;
        _month=d._month;
        _day=d._day;
    }
    return *this;
}

编写赋值重载代码的时候,需要注意下面己点:

  • 返回值和参数类型(注意要引用传参,不然会调用拷贝构造)
  • 检测是否自己给自己赋值(避免浪费时间)
  • 因为返回的是*this,出了函数后没有销毁,所以可以用传引用返回
  • 一个类如果没有显式定义赋值运算符重载,编译器也会自己生成一个,完成对象按字节序的值拷贝。

如果类中有自定义类型,编译器会默认调用它的赋值运算符重载(这里也会涉及到深浅拷贝的问题,后面会在深浅拷贝的博客里详解)

4.4拷贝构造和赋值重载的调用问题

当赋值操作符和拷贝构造同时存在的时候,什么时候会调用赋值,什么时候会调用拷贝构造呢?

在这两个函数中添加cout进行打印提示,可以看到:

  • 如果对象在之前已经存在,就会调用赋值重载
  • 如果是一个全新的变量在定义的时候初始化,就调用的是拷贝构造

5.const成员 5.1用const修饰类的成员函数

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

基本的修饰方法如下,在函数的括号后加const即可

void Print()const
{
	cout<<_year< 

实际修饰的是该函数隐含的this指针

this指针本身是Date*const类型的,修饰后变为const Date* const类型

void Print(const Date* const this)
{
	cout<<_year<<"-"<<_month<<"-"<<_day< 

①实例-权限问题

这么说好像有点迷糊,我们用实例来演示一下为什么需要const修饰成员函数

class Date{
public:
    Date(int year=2022,int month=2,int day=30)
    {
        _year=year;
        _month=month;
        _day=day;
    }
    void Print()
    {
        cout<<_year<<"-"<<_month<<"-"<<_day< 

假设我们需要在函数中调用Print函数,在main中是可以正常调用的

int main()
{
	Date d1(2022,5,10);
    d1.Print();
    return 0;
}

但当你用一个函数来进行这个操作的时候,事情就不一样了

void TEST(const Date& d)
{
	d.Print();//d.Print(&d) -->const Date*
}
int main()
{
	Date d1(2022,5,10);
    d1.Print();//d1.Print(&d1) -->Date*
    TEST(d1);
    
    return 0;
}

这时候我们进行了引用调用,因为在TEST中我们不会修改d1的内容,所以用const进行了修饰

  • 这时候TEST中的d.Print()函数调用,传入的是const Date*指针,指针指向的内容不能被修改
  • main中的d1.Print();函数调用,传入的是Date*指针

于是就会发生权限冲突问题

这时候如果我们在函数后面加了const,就可以避免此种权限放大问题。这样不管是main函数还是TEST函数中对Print()函数的调用,就都可以正常打印了!


②什么时候需要使用?

众所周周知,const修饰指针有下面两种形式

  • 在*之前修饰,代表该指针指向对象的内容不能被修改(地址里的内容不能改)
  • 在*之后修饰,代表该指针指向的对象不能被修改(指向的地址不能改)

this指针本身就是类型名* const类型的,它本身不能被修改。加上const之后,this指向的内容,既类里面的成员变量也不能被修改了。

知道了这一点后,我们可以合理的判断出:只要是需要修改类中成员变量的函数,就不需要在()后面加const修饰


5.2取地址及对const取地址重载

最后两个默认成员函数,编译器会自动生成。这两个函数一般都不需要重载,毕竟返回的本身就是一个this指针,没有什么奇怪的地方

class Date{ 
public :
     Date* operator&()
     {
         return this ;
     }
    
     const Date* operator&()const
     {
     	return this ;
     }
private :
     int _year ; 
     int _month ;
     int _day ;
};

只有特殊情况,我们需要让&只获取特定内容的时候,才需要手动重载这两个函数


日期类的实现

类和对象第一站中提到过,在项目协作的时候,我们一半要用定义和声明分离的形式来些一个项目。

下面就让我们用日期类来演示这样的操作

在类中定义的函数会被默认设置为内联,我们的目标就是:短小函数在.h中定义,长函数在.h中声明,在.cpp中定义

至于源码和解析嘛……大家直接来我的gitee仓库看吧!【传送门】

注释写的很详细了⏲有啥问题可以在下面留言哦

结语

最后的最后,今天是5月20日,用下图给大家送上祝福

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/920262.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号