析构/构造/拷贝构造函数


title: 析构/构造/拷贝构造函数
date: 2020-08-24 19:29:09
tags: c++
categories: c++

在C++中对象的初始化和清理是两个非常重要的安全问题,一个对象或者变量没有初始状态,对其使用后果是未知的,同理使用完一个对象或者变量,没有及时清理,也会造成一定的安全问题。在C++中利用了构造函数和析构函数解决了上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

1、构造函数

构造函数它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。构造函数的主要作用在于创建对象时为对象的成员属性赋值。如果程序员不提供构造函数,编译器会提供,只不过是空实现。如果一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成

下面构造函数的使用方法:

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:
        string name;
        int age;
        float high;

        //默认(无参)构造函数
        Persion()
        {
            cout<<"默认构造函数"<<endl;
        }
        //有参构造函数
        Persion(string m_name, int m_age, float m_high)
        {
            name = m_name;
            age = m_age;
            high = m_high;
            cout<<"有参构造函数"<<endl;
        }

};

int main(int argc, const char** argv) {

    Persion p;  //调用无参构造

    Persion p1("张三",18,1.72); //调用有参构造

    cout<<p1.name<<p1.age<<p1.high<<endl;

    return 0;
}
1.1 构造函数的重载

和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。上例中的Persion 构造函数就是重载的。构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定会调用。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。

1.2 构造函数的初始化列表

构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表。

语法:构造函数():属性1(值1),属性2(值2)... {}

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:
        string name;
        int age;
        float high;

        //普通成员变量赋值
        Persion(string m_name, int m_age, float m_high)
        {
            name = m_name;
            age = m_age;
            high = m_high;
        }
        //使用初始化列表辅助
		Persion(string m_name, int m_age, float m_high):name(m_name),age(m_age),high(m_high)
        {
        }

};

int main(int argc, const char** argv) {

    Persion p1("张三",18,1.72); 

    cout<<p1.name<<p1.age<<p1.high<<endl;

    return 0;
}

使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,不在对成员变量一一赋值,尤其在成员变量较多时,这种写法非常简单明了。

1.3初始化const成员变量

const 成员的初始化: 不能在定义处初始化,只能在构造函数列表中初始化,而且必须要有构造函数

原因:const数据成员只在某个对象生存期内饰常量,而对于整个类而言是可变的。 因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const数据成员的值是什么。

错误

class Persion
{
public:
        string name;
        int age;
        const float high = 1;	//可以这么用,但是错误的。使用const只是想修饰某个对象为常量。如果在声明时直接赋初值那么所有的对象都成一个常量值,这并不是我们想要的结果。
};

class Persion
{
public:
        string name;
        int age;
        const float high;
        //使用初始化列表赋值
        Persion(string m_name, int m_age, float m_high)
        {
            name = m_name;
            age = m_age;
            high = m_high;	//报错
        }

};

正确用法是类中const修饰的成员变量只能使用初始化列表的形式赋值

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:
        string name;
        int age;
        const float high;
        //使用初始化列表为每个对象的const成员赋值。
        Persion(string m_name, int m_age, float m_high):name(m_name),age(m_age),high(m_high)
        {
        }

};

int main(int argc, const char** argv) {

    Persion p1("张三",18, 17.5); 
    cout<<p1.name<<p1.age<<p1.high<<endl;
    return 0;
}
1.4 类成员变量初始化顺序
#include <iostream>
using namespace std;

class Student
{
public:
        int m_a;
        int m_b;
        void ShowAll(void)
        {
            cout << "m_a="<<m_a<<' '<< "m_b="<<m_b<< ' '<<"m_c="<<m_c<< ' '<<"m_d="<<m_d<<endl;
        }
        Student(int a,int b,int c, int d):m_b(b),m_d(m_b+1),m_c(m_d+1),m_a(m_c+1)
        {

        }
private:
        int m_c;
        int m_d;

};

int main(void)
{
    Student st1(1,2,3,4);
    st1.ShowAll();
    return 0;

}

在不看运行结果前我们分析分析,按照正常思维st1对象被创建后会调用Student类的构造函数。在构造函数中使用了初始化列表的形式对各个属性进行赋值,m_b=2,m_d=3,m_c=4,m_a=5,但是这个结果是错的。下面看看正确答案。

在这里插入图片描述

为什么结果和我们预想的不一样呢?这是因为成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。

在本例中我们定义成员变量的顺序是m_a, m_b, m_c, m_d而初始化列表的顺序是m_b,m_d,m_c,m_a。因此初始化的顺序和列表中排列的顺序无关,仅仅与定义的顺序有关。由于m_a先定义因此会对m_a先赋值m_a=m_c+1,m_c是个垃圾值,因此m_a也是个垃圾值。接下来是对m_b进行赋值m_d=b,也就是2。下面是m_c赋值,m_c =m_d+1,m_d还未赋值是个垃圾值,因此m_c也是垃圾值。最后是m_d,m_d=m_b+1,m_b刚刚被赋值为2,因此m_d就等于3。

如果不使用初始化列表初始化,在构造函数一个一个对其赋值初始化时,此时与成员变量定义的顺序无关。和在构造函数中赋值初始化的顺序有关

2、析构函数

创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。析构函数也是一种特殊的成员函数,没有返回值,不需要程序员显式调用,而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:
        int *m_age = NULL;

        Persion(int age)    //构造函数
        {
            int *m_age = new int;
            *m_age = age;


        }
        ~Persion()      //析构函数
        {
            delete m_age;
            cout<<"析构函数"<<endl;

        }
};

int main(int argc, const char** argv) {

    Persion p1(25);
    return 0;
}
3、拷贝构造函数

C++中还有一类特殊的构造函数,他就是拷贝构造函数。拷贝构造函数的常见形式如下:

classname (const classname &obj) 
{
   // 构造函数的主体
}

如果在类中没有显式定义拷贝构造函数,编译器会自行定义一个。编译器定义的拷贝构造函数会带来浅拷贝的问题。稍后再做说明,先看看拷贝构造函数用在什么地方。

拷贝构造函数常常用于以下三个地方

  • (1)使用一个已经创建完毕的对象来初始化一个新对象时

    class Persion
    {
    public:
    
            Persion(int age):m_age(age)   //构造函数
            {
            }
            ~Persion()    //析构函数
            {
            }
            Persion(const Persion &p)	//拷贝构造函数
            {
                this->m_age = p.m_age;
                cout<<"拷贝构造函数的调用";
            }
            int m_age;
    };
    
    int main(int argc, const char** argv) 
    {
        Persion p1(20);     //p1已经创建完毕的对象
        Persion p2(p1);     //使用一个创建完毕的对象p1,初始化一个新对象p2时,对调用拷贝构造函数
    
        cout<<p2.m_age<<endl;
    
        return 0;
    }
    
  • (2)对象作为参数以值传递的方式给函数传参时

    class Persion
    {
    public:
    
            Persion(int age,string name):m_age(age),m_name(name)   //构造函数
            {
            }
            ~Persion()    //析构函数
            {
            }
            Persion(const Persion &p)//拷贝构造函数
            {
                this->m_age = p.m_age;
                this->m_name = p.m_name;
                cout<<"调用"<<endl;
            }
            int m_age;
            string m_name;
    };
    
    void DoingSomething(Persion p)
    {
        cout<<p.m_name<<"DoingSomething"<<endl;
    
    }
    int main(int argc, const char** argv) {
    
        Persion p1(18,"张三");//建立一个对象
        
        DoingSomething(p1);//对象作为参数,以值传递方式传递给函数时,会调用拷贝构造函数
    
        return 0;
    }
    
  • (3)以值方式返回局部对象

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:

	Persion(int age, string name) :m_age(age), m_name(name)   //构造函数
	{
	}
	~Persion()    //析构函数
	{
	}
	Persion(const Persion &p)	//拷贝构造函数
	{
		this->m_age = p.m_age;
		this->m_name = p.m_name;
		cout << "调用" << endl;
	}
	int m_age;
	string m_name;
};

Persion test(void)
{
	Persion p(25, "李四");	//新建栈区局部对象
	return p;				//返回局部对象。会调用拷贝构造函数
}
int main(int argc, const char** argv) {

	Persion p1 = test();	//接收返回的局部对象

	cout << p1.m_name << p1.m_age << endl;

	return 0;
}
3.1深拷贝与浅拷贝问题

如果用户没有自己编写拷贝构造函数,使用编译器默认提供的拷贝构造函数,那么就会带来浅拷贝的问题。看下面这个例子:

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:

	Persion(int age, string name) //构造函数
	{
		m_name = name;
		m_age = new int(age);		//在堆区开辟内存
	}
	~Persion()    //析构函数
	{
		delete m_age;		//对象销毁前释放对象所占用的资源
	}
	int * m_age;				
	string m_name;
};
void Dosomething(Persion p)
{
	;
}

int main(int argc, const char** argv) {

	Persion p(17, "张三");
	Dosomething(p);			//触发调用拷贝构造函数
		
	return 0;
}

上述代码在Visual Studio 2017中运行出错。原因看下图,当触发调用拷贝构造函数时,由于程序员自己未定义拷贝构造函数,所以使用了编译器默认提供的拷贝构造函数。而编译提供的拷贝构造函数类似于下

Persion(const Persion &obj) { m_age = obj.m_age; m_name = obj.m_name; }

它只是简单的进行了赋值操作,而类中int * m_age指向的是堆区申请的空间,这就导致对象P和拷贝的对象P~中int * m_age成员都指向了同一块内存空间。而当这个对象销毁时,会执行析构函数,在析构函数中我们对堆区申请的内存进行释放。这就导致当对象P~执行析构时释放完堆区内存后,对象P在执行析构时又对堆区申请的这片内存进行了释放,但是第一次p~释放完这片内存后用户对这片内存已经没有访问操作的权限,P再次去释放时就属于非法操作。因此程序会出异常。这就是浅拷贝带来的问题。

在这里插入图片描述

3.2 解决浅拷贝带来的问题

下面针对上面的例子重新修正后再看。

#include <iostream>
#include <string>
using namespace std;
class Persion
{
public:

	Persion(int age, string name) //构造函数
	{
		m_name = name;
		m_age = new int(age);		//在堆区开辟内存
	}
	~Persion()    //析构函数
	{
		delete m_age;
	}
	Persion(const Persion &obj)	//显示定义拷贝构造函数
	{
		m_age = new int(*obj.m_age);
		m_name = obj.m_name;
	}
	int * m_age;				
	string m_name;
};
void Dosomething(Persion p)
{
	;
}

int main(int argc, const char** argv) {

	Persion p(17, "张三");
	Dosomething(p);			//触发调用拷贝构造函数
		
	return 0;
}

本例中仅仅添加了17-21行的代码,这里我们显式地定义了拷贝构造函数,它除了会将原有对象的所有成员变量拷贝给新对象,还会为新对象再分配一块内存,并将原有对象所持有的内存也拷贝过来。这样做的结果是,原有对象和新对象所持有的动态内存是相互独立的,更改一个对象的数据也不会影响另外一个对象。

在这里插入图片描述

我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。如果类的成员变量没有指针,一般浅拷贝足以。

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值