C++构造函数和析构函数的总结

1 前言

创建一个对象时候,常常需要作一些初始的工作,就像买房子的话,售房小姐就会问你是否需要家具,是否要精装修等等的问题。注意,类的成员不能在声明类的时候初始化的。
image

image

为了解决这个问题,C++编译器提供了一个特殊的函数---构造函数(construction)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他的函数不同,她不需要用户来调用它,而是在建立对象时自动执行的。

2 构造函数和析构函数

2.1 构造函数和析构函数的概念

  1. 有关于构造函数的定义

    • C++中类可以定义和类名相同的特殊的成员函数,这种与类名一致的成员函数叫做构造函数
    • 构造函数在定义时可以有参数
    • 没有任何返回类型的声明
  2. 构造函数的调用

    • 自动调用:一般情况下C++编译器会自动调用构造函数
    • 手动调用: 在一些情况下则是需要手动调用构造函数
  3. 析构函数的定义

    • C++类可以定义一个特殊的成员函数来对类进行清理,清理可以不太严谨,应该是类销毁后需要进行的行为,语法为~ClassName()
    • 析构函数没有参数,也没有返回值
    • 析构函数在对象销毁时自动被调用
    • 析构函数自动被C++编译器调用

3 C++编译器构造析构方案的优势

3.1 设计构造函数和析构函数的原因

其实构造函数和析构函数的思想是从生活中而来的一种概念,生活中所有的对象,像手机、汽车出厂的时候必须进行初始化设定才可以到用户的手中让用户使用。所以初始的状态是对象普遍存在的一种状态,那么使用构造函数可以让编译来根据我们编写的构造函数来初始化我们的对象,这样不需要我们手动的对对象进行初始化,提高生产的效率。下面我们对比一下普通的方案和使用构造函数的方案。

普通方案:

  1. 为类提供一个publicinitializ函数
  2. 对象创建后立即调用initializ函数进行初始化工作

优缺点分析

  • initializ只是一个普通的函数,必须显示的调用
  • 一旦忘记或者失误导致对象没有初始化,那么结果是可以错误或者不确定

3.2 构造函数的分类以及调用规则

  • C++编译器为我们使用者提供了三种对象初始化的方案
#include <iostream>
using namespace std;

class Test
{
public:
	Test();
	Test(int x);
	Test(int x, int y);

	~Test();

private:
	int x;
	int y;
};
// 无参造函数
Test::Test()
{
	this->x = 0;
	this->y = 0;
}
Test::Test(int x) 
{
	this->x = x;
}
// 有参造函数
Test::Test(int x1,int y1) 
{
	this->x = x1;
	this->y = y1;
}
// 析构函数
Test::~Test()
{
}

int main()
{
	// 1 在初始化直接调用,括号法
	Test t1(1, 2);
	// 2 C++编译器对于=操作符进行了加强,可以使用=来进行操作
	Test t2 = 3;
	Test t3 = (4, 5);	
	// 3 直接显式的调用构造函数
	Test t4 = Test(1); 
	return 0;  
}

3.3 拷贝构造函数的调用时机

为了可以更好的理解拷贝构造函数和构造函数的概念,我找了几个典型的使用场景来举例子给大家参考。

第一个场景和第二个场景

#include "iostream"
using namespace std;

class Test
{
public:
	Test() //无参构造函数 默认构造函数
	{
		cout << "我是无参构造函数" << endl;
        value = 0; 
	}
	Test(int test_val) //无参构造函数 默认构造函数
	{
		cout << "我是有参构造函数" << endl;
		value = test_val;
	}
	Test(const Test& obj)
	{
		cout << "我也是构造函数,我是通过另外一个对象obj2,来初始化我自己" << endl;
		value = obj.value + 10;
	}
	~Test()
	{
		cout << "我是析构函数,自动被调用了" << endl;
	}
	void getTestValue()
	{
		cout << "value的值" << value << endl;
	}
protected:
private:
	int value;
};
//单独搭建一个舞台来进行测试
void ObjShow01()
{
    /* 定义一个变量 */
	Test t0(5);
    Test t1(20);
    /* 使用赋值法来对t2进行初始化*/
    Test t2 = t0;
    t2.getTestValue();
    t1 = t0;  
    t1.getTestValue();
}
int main()
{
    ObjShow01();
    return 0;
}

image

结果显然是不同,使用一个对象初始化和在初始化后使用=号,前一个C++编译器会自动调用类中的拷贝构造函数,而后一个只是简单的赋值,浅拷贝而已。同样第一个场景也是类似的分析,只是和第一个场景不同是使用括号初始化。

第三个场景

#include "iostream"
using namespace std;

class Test
{
public:
	Test() //无参构造函数 默认构造函数
	{
		cout << "我是无参构造函数" << endl;
        value = 0; 
	}
	Test(int test_val) //无参构造函数 默认构造函数
	{
		cout << "我是有参构造函数" << endl;
		value = test_val;
	}
	Test(const Test& obj)
	{
		cout << "我也是构造函数,我是通过另外一个对象obj2,来初始化我自己" << endl;
		value = obj.value + 10;
	}
	~Test()
	{
		cout << "我是析构函数,自动被调用了" << endl;
	}
	void getTestValue()
	{
		cout << "value的值" << value << endl;
	}
protected:
private:
	int value;
};

void testFunction(Test obj)
{
	cout<<"test_function"<< endl;
	obj.getTestValue();
}
//单独搭建一个舞台来进行测试
void ObjShow03()
{
    /* 定义一个变量 */
	Test t1(10);
	testFunction(t1);
}
int main()
{
	ObjShow03();
    return 0;
}

这个案例是测试在函数调用对象时候,构造函数和析构函数是什么一个过程,这个过程其实分析一下并不难。在ObjShow03先定义了Test t1(10);首先编译会自动调用构造函数进行初始化,然后调用testFunction(t1);

这里使用了Test中的拷贝构造函数来构造参数obj然后当testFunction运行完后运行obj对象的析构函数来析构obj最后退出ObjShow03函数调用t1的析构函数,输出的结果如下图

image

第四种场景,也是一种比较有意思的场景

#include "iostream"
using namespace std;

class Test
{
public:
	Test() //无参构造函数 默认构造函数
	{
		cout << "我是无参构造函数" << endl;
        value = 0; 
	}
	Test(int test_val) //无参构造函数 默认构造函数
	{
		cout << "我是有参构造函数" << endl;
		value = test_val;
	}
	Test(const Test& obj)
	{
		cout << "我也是构造函数,我是通过另外一个对象,来初始化我自己" << endl;
		value = obj.value + 10;
	}
	~Test()
	{
		cout << "我是析构函数,自动被调用了" << endl;
	}
	void getTestValue()
	{
		cout << "value的值" << value << endl;
	}
protected:
private:
	int value;
};

void ObjShow04()
{
	/* 第一种方式接 */
	Test test1;
	test1 = GetTest();
	/* 第二种方式接 */
	//Test test2 = GetTest();
}

/* 注意:初始化操作 和 等号操作 是两个不同的概念 */
int main()
{
	ObjShow04();
    return 0;
}

这次我们直接放出两种不同方式接的效果吧,第一张为第一种接的效果,第二张为第二种接的效果。为什么了,第一种接在函数return匿名对象会发生析构,而第二种接编译器会比较智能,匿名对象会不析构直接转正,编译可以知道你后续要用这个对象来拷贝初始化新对象,于是直接用匿名对象取代新对象,这是一个编译器的优化代码的功能。

image

image

3.4 默认构造函数

两个特殊的构造函数

  • 默认的无参的构造函数

    当类当中是没有定义构造函数时,编译器会默认一个无参的构造函数,并且函数体为空

  • 默认的拷贝构造函数

    当类中没有定义拷贝构造函数时,编译器会提供一个默认的拷贝构造函数,简单的进行成员变量的值的复制(注意这里是值的复制)

3.5 构造函数的调用规则的研究

  1. 当类当中是没有定义任意一个构造函数的时候,编译会提供默认的无参构造函数和默认的拷贝构造函数
  2. 当类当中提供了拷贝构造函数时,C++不会提供无参的拷贝构造函数
  3. 默认的拷贝构造函数成员是简单的赋值(这就涉及到深拷贝和浅拷贝的区别)

总结:只要你有手动的写构造函数,那么你就必须用

构造函数阶段性总结

  • 构造函数是C++编译器用于初始化对象的特殊函数
  • 构造函数在对象创建时会自动被调用
  • 构造函数和普通的函数都必须遵守重载的原则
  • 拷贝构造函数是函数正确初始化的重要保障
  • 必要的时候,必须手工的编写拷贝构造函数来满足我的需求

4 深拷贝和浅拷贝

为什么会出现深拷贝和浅拷贝的情况

  • 默认的复制构造函数可以完成的是对于成员变量的值的简单复制
  • 当对象的数据是指向堆的指针时,默认拷贝构造函数也只是对指针的值进行简单的值的复制

我做了一个图可以很好的表示深拷贝的过程

成员2
成员2
成员1
成员1
成员指针1
成员指针1
堆地址A
堆地址A
对象1
对象1
成员2
成员2
成员1
成员1
成员指针1
成员指针1
对象2
对象2
Viewer does not support full SVG 1.1

这两个对象分指针都指向同一个堆地址,这显然是不是我们希望的,这样导致一个对象对指针的内容进行修改,则另外对象也同样发生改变。

那么如何解决深拷贝和浅拷贝的问题?

  • 显式的提供copy构造函数
  • 显式的提供重载=操作,不使用编译器提供的浅拷贝构造函数
class Name
{
public:
	Name(const char *pname)
	{
		size = strlen(pname);
		pName = (char *)malloc(size + 1);
		strcpy(pName, pname);
	}
	Name(Name &obj)
	{
		//用obj来初始化自己
		pName = (char *)malloc(obj.size + 1);
		strcpy(pName, obj.pName);
		size = obj.size;
	}
	~Name()
	{
		cout<<"开始析构"<<endl;
		if (pName!=NULL)
		{
			free(pName);
			pName = NULL;
			size = 0;
		}
	}

	void operator=(Name &obj3)
	{
		if (pName != NULL)
		{
			free(pName);
			pName = NULL;
			size = 0;
		}
		cout<<"测试有没有调用我。。。。"<<endl;

		//用obj3来=自己
		pName = (char *)malloc(obj3.size + 1);
		strcpy(pName, obj3.pName);
		size = obj3.size;
	}  

protected:
private:
	char *pName;
	int size;
};

//对象的初始化 和 对象之间=号操作是两个不同的概念
void playObj()
{
	Name obj1("obj1.....");
	Name obj2 = obj1; //obj2创建并初始化

	Name obj3("obj3...");

	//重载=号操作符
	obj2 = obj3; //=号操作
	cout<<"test"<<endl;

}
void main61()
{
	playObj();
	syste

5 多个对象的构造和析构

5.1 对象的初始化列表

对象初始化列表出现原因

1.必须这样做:

如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,

如果没有初始化列表,那么他将无法完成第一步,就会报错。

2.类成员中若有const修饰,必须在对象初始化的时候,给const int m 赋值

当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,

因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。

3.C++中提供初始化列表对成员变量进行初始化

语法规则

Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)

{

// some other assignment operation

}

3.注意概念

初始化:被初始化的对象正在创建

赋值:被赋值的对象已经存在

4.注意

成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关

初始化列表先于构造函数的函数体执行

6 构造函数和析构函数的调用顺序研究

构造函数与析构函数的调用顺序

  1. 当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数
  2. 析构函数的调用顺序与对应的构造函数调用顺序相反

7 对象的动态建立和释放

7.1 对象的动态建立和释放

new和delete基本语法

  1. 在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数mallocfree来分配和撤销内存空间的。C++提供了较简便而功能较强的运算符new和delete来取代mallocfree函数。

  2. 注意: newdelete是运算符,不是函数,因此执行效率高。

  3. 虽然为了与C语言兼容,C++仍保留malloc和free函数,但建议用户不用malloc和free函数,而用new和delete运算符。new运算符的例子:
    new int; //开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针)
    new int(100); //开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址
    new char[10]; //开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址
    new int[5][4]; //开辟一个存放二维整型数组(大小为5*4)的空间,返回首元素的地址
    float *p=new float (3.14159); //开辟一个存放单精度数的空间,并指定该实数的初值为//3.14159,将返回的该空间的地址赋给指针变量p

  4. newdelete运算符使用的一般格式为:

image

用new分配数组空间时不能指定初值。如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。

image

7.2 类对象的动态建立和释放

使用类名定义的对象都是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。

​ C++中,可以用new运算符动态建立对象,用delete运算符撤销对象

比如:

  • Box *pt; //定义一个指向Box类对象的指针变量pt
  • pt=new Box; //在pt中存放了新建对象的起始地址
    在程序中就可以通过pt访问这个新建的对象。如
    cout<<pt->height; //输出该对象的height成员
    cout<<pt->volume( ); //调用该对象的volume函数,计算并输出体积
    C++还允许在执行new时,对新建立的对象进行初始化。如
    Box *pt=new Box(12,15,18);

这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。这样更精炼。

新对象中的heightwidthlength分别获得初值12,15,18。调用对象既可以通过对象名,也可以通过指针。

​ 在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C++编译系统都使new返回一个0指针值。只要检测返回值是否为0,就可判断分配内存是否成功。

ANSI C++标准提出,在执行new出现故障时,就“抛出”一个“异常”,用户可根据异常进行有关处理。但C++标准仍然允许在出现new故障时返回0指针值

原文地址:https://www.cnblogs.com/Kroner/p/15516278.html