0
点赞
收藏
分享

微信扫一扫

C++的默认前三个成员函数小结

SDKB英文 2022-01-15 阅读 31
c++后端

文章目录

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

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

不写,编译器自动生成;写了就不生成。

image-20220115145308297

构造函数主要完成初始化的工作

析构函数主要完成清理的工作

拷贝构造是使用同类对象初始化创建对象

赋值重载主要是把一个对象赋值给另一个对象

取地址重载主要是普通对象和const对象取地址,这两个很少会自己实现

  • 我们不实现时,编译器生成的默认构造函数和析构函数,针对成员变量:内置类型不处理,自定义类型会调这个成员对象的构造和析构
  • 我们不实现时,编译器生成拷贝构造和operator=,会完成按字节的值拷贝(浅拷贝)
  • 也就是说有些类,我们是不需要去实现拷贝构造和operator=的,因为编译器默认生成的可以用。

1.构造函数

构造函数的概念

构造函数—>在对象构造时调用的函数,这个函数完成初始化的工作。注意完成的不是构造!!

特性

  • 没有返回值
  • 函数名和类名相同
  • 实例化时自动调用
  • 构造函数可以重载
  • 对于当前的类,如果没有显式构造函数,编译器会生成默认无参的构造函数(语法坑,c++的双标),一旦用户显式定义编译器不再生成。而该默认构造函数会做如下的事。
    • 针对内置类型的成员变量没有做处理
    • 针对自定义类型的成员变量,调用它的默认构造函数初始化。不过对于该成员变量的类来说,默认构造函数初始化有三种。不过对应编译器自己生成的我们没法验证
  • 无参的构造函数和全缺省的构造函数是重载,语法上可以同时存在,但是都是默认构造函数。编译错误–编译器无法知道是用哪个默认构造函数
    • 调用默认构造函数有三种
      • 自己实现无参的构造函数
      • 自己实现的全缺省构造函数
      • 我们不写,编译器自己生成
      • 以上的特点都是不用传参数

实例

构造函数的存在场景
#include<iostream>
using namespace std;
class Date
{
    public:
    	void Init(int year,int month,int day)
        {
            //c++要用必须要先声明。但是这里却没有
            _year=year;//这里凭空访问_year,这里的参数实际是隐含的this指针.
            _month=month;
            _day=day;
        }
     	//void Init(Data* this,int year,int month,int day)
    	//this->_year=year;
    	void P()
        {
            cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
        }
    	//编译器处理后实际代码
    	void P(Date* this)
        {
            cout<<this->_year<<"-"<<this->_month<<"-"<<this->_day<<endl;
        }
    private:
	 int _year;
     int _month;
     int _day;
};
int main()
{
	Date d1;
    d1.Print();//在没有初始化INIT前调用了,会出现随机值,很危险。c++通过6个默认成员函数来处理
    d1.Init(2020,4,7);
    
    Date d2;
    d2.Iint(2020,4,7);
    
    d1.Print();
    d2.Print();
}
构造函数的重载
class Date
{
    public:
    	Date(int year,int month,int day)
        {
            _year=year;
            _month=month;
            _day=day;
        }
   		Date()
        {
            _year=0;
            _month=0;
            _day=0;
        }
    	void Print()
        {
			cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
        }
    private:
    	int _year,_month,_day;
};
int main()
{
	Date d1(2020,4,8);
    d1.Print();
    //构造函数可以重载不带参数的。
    //Note:无参是不能加括号的
    Date d2();//error
    Date d2;//
    d2.Print();
}
全缺省方式及注意

既然有一个写了就没有无参了,要写两个,有没有更好的方式呢。

更加推荐全缺省方式。

class Date
{
    //更好的方式---全缺省
    Date(int year=0,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=month;
    }
};

注意:Date()的全缺省和无参是构成重载的,但是两者不能一起。因为调用的时候会不明确

//等价
class Date()
{
    Date()
    {
        _year=0;
        _month=1;
        _day=1;
    }
    Date(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=month;
    }
};
///Date()的全缺省和无参是构成重载的,但是两者不能一起。因为调用的时候会不明确
int main()
{
    Date d1;//可以调用全缺省的也可以调用无参的。这种选择是不能发生的。
    d1.Print();
}

注意:对于全缺省和无参的直接使用类名+对象名实例化,没有类名+对象名()的用法

至于编译通过是编译器把这个当成函数的声明了;

class Date()
{
    Date(int year=1,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=month;
    }
};
int main()
{
    Date d1;
    Date d2();
    d2.Print();
}
编译器生成的默认构造函数的作用

C++里面把类型分为两类:内置类型(基本类型),自定义类型。

内置类型:int/char/double/指针/内置类型数组等等

自定义类型:struct/class的类型

对于自定义类型,回去调用它的默认构造函数(无参数/全缺省)进行初始化。

class Time
{
    public:
    	Time()
        {
            _hour=0;
            _minute=0;
            _second=0;
            cout<<"Time()"<<endl;
        }
    private:
    	int _hour;
    	int _minute;
    	int _second;
};
class Date
{
    public:
    	void Print()
        {
			cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
        }
    private:
    	int _year;
    	int _month;
    	int _day;
    	Time _t;
};
int main()
{
	Date d1;
    d1.Print();//此时发现还是随机值,似乎这个构造函数还不如不存在。好像没做任何事情
    //但是我们再来一个类
    //此时进行调试,发现_t对象是构造了的,初始化好了,虽然d1的其他变量没有初始化。
}
class A{
public:
        A(int a=10)
        {
            cout<<"A()"<<endl;
            _a=a;
        }
private:
        int _a;
};
class Date{
public:
private:
        int _year;
        int _month;
        int _day;

        A _a;
};
int main(void){
        Date d1;
}

image-20220115160511649

成员变量的命名风格

如果这样处理,根据就近原则,两个变量都是函数的参数。

class Date
{
    Date(int year=0,int month=1,int day=1)
    {
        year=year;
        month=month;
        day=month;
    }
};

因此一般对成员变量前加_达到区分的目的

class Date
{
    Date(int year=0,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=month;
    }
};

2.析构函数

2.1概念

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

对象生命周期结束的时候调用。

2.2特性

  • 析构函数名是在类名前面加上字符
  • 无参数无返回值
    • 因此无法重载
  • 一个类有且只有一个析构函数,若未显式定义,系统会生成默认的析构函数
  • 对象生命周期结束时,c++编译系统自动调用析构函数
  • 关于编译器自动完成的析构函数,是否会完成一些事情
    • 和构造函数的双标特性一样
析构函数存在场景

将类里面的动态开辟的内存清理了。

对于日期类来说,其成员变量在对象生命周期结束时自动释放函数(main)栈帧,因此析构函数写不写都无所谓。

class Date
{
    public: 
    Date(int year=0,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=month;
    }
    ~Date()
    {
        cout<<"~Date()"<<endl;
    }
    private:
    	int _year;
    	int _month;
   		int _day;
};

int main()
{
	//析构:对象声明周期到了以后,自动调用。完成对象里面的资源清理工作,不是完成d1和d2的销毁。d1,d2的销毁是该对象所在的函数栈帧结束的时候销毁
    Date d1;
    Date d2;
}

但是对放在堆区上的内存,需要我们在析构函数中手动释放。

构造析构的顺序

至于构造函数的执行顺序和析构函数的执行顺序。考虑到是栈帧。

先构造d1,d2,s1,s2,析构先析构s2,再析构s1

class Date
{
    public: 
    Date(int year=0,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=month;
    }
    ~Date()
    {
        cout<<"~Date()"<<endl;
    }
    private:
    	int _year;
    	int _month;
   		int _day;
};
class Stack
{
    public:
    	Stack(int n=10)
        {
            _a=(int*)malloc(sizeof(int)*n);
            cout<<"malloc:"<<_a<<endl;
            _size=0;
            _capacity=n;
        }
    	~Stack()
        {
            if(_a)
            {
                free(_a);
                cout<<"free:"<<_a<<endl;
                _a=nullptr;
                _size=_capacity=0;
            }
        }
    private:
    	int *_a;
    	int _size;
    	int _capacity;
};
int main()
{     
    Date d1;
    Date d2;
    
    Stack s1;
    Stack s2;
}
编译器生成的默认析构函数的作用

当前类由编译器自动生成的析构函数是否会完成什么事情

  • 对当前类的内置类型、基本类型 int/char 不会处理
  • 对当前类中的自定义类型,调用它的析构函数
class Time
{
  public:
    	~Time()
        {
			cout<<"~Time()"<<endl;
        }
};
class Date
{
    public:
    	Date(int year=0,int month=0,int day=0)
        {
			//....
        }
    private:
    	int _year;
   		int _month;
    	int _day;
};

image-20220115170752722

编译器生成的默认析构函数价值

当一个类包含了其他类,比如leetcode里面的题目的时候,我们可以不写当前的类的默认构造,加入其他类,这样就能自然而然地初始化其他类和销毁其他类

class MyQueue{
    public:
    	void Push(){}
    private:
    	Stack s1,s2;
}

3.拷贝构造函数

3.1概念

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

3.2特征

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
  • 默认生成的是浅拷贝
  • 浅拷贝是存在问题的,对于像Stack这样的类,会导致同一块空间析构的时候释放两次。所以有些类是要手动实现深拷贝。
    • 假如不析构就会产生内存泄漏,因此一定要浅拷贝。

3.3拷贝构造的无穷递归

原因是这样的,如果没有&,当调用拷贝构造的时候,由之前的函数栈帧的学习中,对于拷贝构造函数的参数,将d1拷贝给参数发生的。而拷贝这个过程就是拷贝构造函数要进行的过程。因此发生了语义上的无穷递归。

class Date
{
    public:
    	Date(int year=0,int month=1,int day=1)
        {
            _year=year;
            _month=month;
            _day=day;
        }
    	//Date d2(d1)
    	Date(Date d)//error:存在递归拷贝
        {
            _year=d._year;
            _month=d._month;
            _day=d._day;
        }
       
    private:
    	int _year=0;
    	int _month=0;
    	int _day=0;
};
int main()
{
	Date d1(2020,5,12);
    Date d2(2020,5,12);//如果上面发生更改,下面也要改。就很恶心
    //于是产生了拷贝构造
    Date d2(d1);<----->Date d2=d1;//***
}

image-20220115173215780

下面说明的是,进行func的时候,需要将d1拷贝给参数d。

于是将d1拷贝给参数d的这个过程,需要调用拷贝构造函数。

于是可想而知,假如func函数是拷贝构造函数,就会存在自身无穷地调用自身。

而进行引用作为参数的时候,是不进行传值拷贝

void func(Date d)
{
    
}
void fund(Date& d)
{
    
}
int main()
{
    Date d1(2020,5,12);
    func(d1);//这里调试可以发现跳到Date的拷贝构造函数
    fund(d1);//这里调用直接就进入fund了,因为是引用
}

递归的过程:

//Date d2(d1) //对象初始化的时候自动调用构造函数(此时为拷贝构造)
//调用之前先传参:Date d(d1)--->此时的传参又发生拷贝构造。如此就递归下去了   
Date(Date d)//error:存在递归拷贝
        {
            _year=d._year;
            _month=d._month;
            _day=d._day;
        }

由此说明传值的时候要用拷贝构造

因此解决方案是:传引用来处理。&d和&d1的地址同。

 Date(Date& d)//sol:引用解决
        {
            _year=d._year;
            _month=d._month;
            _day=d._day;
        }

3.4加上const的原因

同时忘了防止自己不小心赋值反了,把传引用的部分用const。导致拷贝错误还改变了自身的值。

Data(const Data& d)
{
    d._year=_year;
    _month=d._month;
    _day=d._day;
}

3.5编译器默认生成的的拷贝构造——浅拷贝

因此对于malloc的类我们要手动实现进行深拷贝。

class Date
{
    public:
    	Date(int year=0,int month=1,int day=1)
        {
            _year=year;
            _month=month;
            _day=day;
        }
    private:
    	int _year=0;
    	int _month=0;
    	int _day=0;
};
int main()
{
	Date d1(2020,5,12);
    Date d2(d1);
}

image-20220115181612284

浅拷贝是传值拷贝,所以对于指针相关的就会导致将指针的值传过去。也就是指向了同一个地址。因此会导致同一块空间析构两次,abort操作系统发送信号终止进程。

class Stack
{
    public:
    	Stack(int n=10)
        {
            _a=(int*)malloc(sizeof(int)*n);
            cout<<"malloc:"<<_a<<endl;
            _size=0;
            _capacity=n;
        }
    	~Stack()
        {
            if(_a)
            {
                free(_a);
                cout<<"free:"<<_a<<endl;
                _a=nullptr;
                _size=_capacity=0;
            }
        }
    private:
    	int *_a;
    	int _size;
    	int _capacity;
};
int main()
{     
    //浅拷贝问题
	Stack st1(10);
	Stack st2(st1);
	Stack st3(30);
	//st1=st3;(赋值operatro=)
}

image-20220115181940134

3.6编译器默认生成的拷贝构造作用

默认生成的拷贝构造:

  1. 对于内置类型成员,会完成按字节序的拷贝(浅拷贝)
  2. 对于自定义类型成员,会调用该成员的类拷贝构造函数。

拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理细节是不一样的,这个和构造和析构是不一样的

#include<iostream>
#include<malloc.h>
using namespace std;

class A{
  public:
    A(){}
    A(const A& a){
      cout<<"拷贝:A()"<<endl;
    }
};
class Data{
  public:
   Data(int year=1,int month=1,int day=1)
   {
        _year=year;
        _month=month;
        _day=day;
   }
   ~Data()
   {
     cout<<"~Data()"<<endl;
   }
  
  private:
    int _year;
    int _month;
    int _day;
    A _aa;
};
int main()
{     
  Data d1(2022,1,1);
  Data d2(d1);
  return 0;
}

image-20220115183407373

举报

相关推荐

0 条评论