C++ 构造函数和析构函数

在C++的学习中,可以把类当作一个模具,类所实例化出来的对象就是根据这个模具所产生的实体,对象可以看作是我们自己创建的一个新的数据类型。本文主要介绍了类对象通过拷贝函数进行初始化,分析了类对象的内存模型,以及通过this指针来实现更复杂的功能。最后介绍了析构函数的基础知识。

1、构造函数

首先,由于类只是一个模板,因此我们在定义类时无法对成员变量初始化,比如下面代码就是错误的:

class circle{
	public:
		int m_L = 20;	// Error:不允许使用数据成员初始值设定项
};

因此,初始化只能发生在类创建对象的过程中,但是由于访问权限的原因,无法在类外访问某些成员变量,因此下面这种做法有时候是无效的:

circle C1;	// 实例化一个对象 C1
C1.m_L = 20;	// 通过创建的对象,来给对应变量初始化,但是如果m_L是private访问权限,则失效

为了解决这个问题,让程序员能像使用标准数据类型一样适用对象,在类内提供了一个特殊的成员函数——“构造函数”,专门用于在创建对象时初始化类对象。之所以说它特殊,是因为C++已经自动为构造函数提供了名称和使用语法,程序员只需要提供方法的定义即可,即:类名(形参列表)。具体来说,构造函数的定义如下:

class circle{
	public:
		int m_L;
	public: //成员函数(方法)
		circle(const int a)	//通过构造函数对成员变量进行赋值
		{
			m_L = a;
		}
};

circle C1(20);	//调用格式
cout << "C1.m_L:" << C1.m_L <<endl;

看上去似乎很简单,但是由于构造函数也是函数,因此所有C++中的形参传递方式,函数特性以及函数调用方法都能用于构造函数。 如前文所讲,C++会自动给类添加一个空的构造函数,但是如果自己在类中实现了有参构造函数,编译器便不再提供无参构造函数。举例如下:

class circle{
	public:
		int m_L;
	public: //成员函数(方法)
		circle(const int a)	//通过构造函数对成员变量进行赋值
		{
			m_L = a;
		}
};

circle C1(20);	//调用格式正确,能够通过构造函数赋值
circle C2; 		//错误,自己定义了有参构造函数,不再提供无参构造函数

构造函数可以重载,接着上面的例子,如果重载一个空的构造函数,那么两个调用格式都正确:

class circle{
	public:
		int m_L;
	public: //成员函数(方法)
		circle(){}	// 空构造函数
		circle(const int a)	//通过构造函数对成员变量进行赋值
		{
			m_L = a;
		}
};

circle C1(20);	//调用格式正确,能够通过构造函数赋值
circle C2; 		//正确,可以通过重载的空构造函数实现初始化

构造函数的参数不仅可以是标准数据类型,也可以是类。众所周知,在数值作为函数参数进行传递的时候,会重新拷贝出来一份数据作为参数传递用完即销毁,这种方式不仅浪费了内存空间,而且无法修改原始数据。为了结合这两者之间的优点,于是经常采取引用作为函数的参数。虽然引用是指针的一种特殊情况,但是指针太过于灵活,并且引用在形式上引用与普通的变量地用法并没有什么区别,因此使用起来更加方便。如果不清楚引用和指针地区别,请移步上一篇文章:C++中的访问权限 - ZhiboZhao - 博客园 (cnblogs.com)

class circle{
	public:
		int m_L;
	public: //成员函数(方法)
    	circle(){}	// 空构造函数
		circle(const int a)	//通过构造函数对成员变量进行赋值
		{
			m_L = a;
		}
		//引用作为函数参数传递,并用const修饰,节省空间的同时避免修改原数据
		circle(const circle& sub_circle)	
		{
			m_L = sub_circle.m_L;
		}
};

circle C1(20);	//调用格式正确,能够通过构造函数赋值
circle C2(C1); 	//正确,可以通过拷贝构造函数进行初始化

2、C++类的内存模型

C++中,一个类包括:

  1. 成员变量:静态成员变量和普通成员变量
  2. 成员函数:静态成员函数和普通成员函数

虽然为了集成,我们将其写到一个类里面,但是只有普通成员变量真正属于类的对象,类的所有对象共享一份静态成员函数,静态成员变量和普通成员函数。结合前面的内容 内存四区之代码区,全局区,栈区和堆区 - ZhiboZhao - 博客园 (cnblogs.com),我大致画出了内存模型,如下图所示:

为了进一步理解,我们举例如下:

2.1 只定义成员函数

class person{
public:
    // 定义一个空的构造函数
	person(int m_age,int m_ID){
	}
};
person p1(10,20);
cout << "p1 所占的空间为:" << sizeof(p1) << endl;

输出结果为:

p1 所占的空间为:1

这个题目在《剑指offer》一书中也提到过,由空类实例化出来的对象所占的内存空间是1个而不是0个字节,因为编译其给对象 p1 分配了一个地址,来表示不同的对象存储在不同的地址空间,因此占用1个字节。

2.2 往空类中添加静态成员变量

class person{
    static int age; //静态成员变量,存在全局区,不属于类对象的一部分
    static int ID;  //静态成员变量,存在全局区,不属于类对象的一部分
public:
    // 定义一个空的构造函数
	person(int m_age,20);
cout << "p1 所占的空间为:" << sizeof(p1) << endl;

输出结果为:

p1 所占的空间为:1

当向类中加入了成员函数与静态成员变量时,类的实例化对象仍然只占用1个字节的空间,足以证明这些函数和变量并不是类对象的一部分。

2.3 再加入非静态成员变量

class person
{
	static int age; //静态成员变量,存在全局区,不属于类对象的一部分
    static int ID;  //静态成员变量,存在全局区,不属于类对象的一部分
	int a;	//非静态成员变量,存在栈区,属于类对象的一部分
public:
	person(int m_age,int m_ID){
	}
};

输出结果为:

p 所占的空间为:4

因此当向类中加入了非静态成员变量时,类的实例化对象占用4个字节的空间,可以说明,非静态变量属于类对象的一部分。综上:同一个类所有实例化出来的对象共享同一份静态成员变量,所以一改全改。既然同一个类的不同对象共享同一份成员函数,那么成员函数怎么区分该访问哪个对象的普通成员变量呢?

3、this指针

接着上一小节的问题,this指针为上述问题提供了一个完美的解决方案,它指向用来调用成员函数的对象(被当作参数隐式地传递给成员函数),我们通过一张图来理解它:

此外,this指针的另一个用途是当成员函数需要返回对象时,用 return *this; 或者 return this,这种做法能够实现链式编程。比如:

p2.addPerson(p1).addPerson(p1);

首先,对象 p2 调用成员函数 addPerson(p1) ,其返回值继续调用 addPerson(p1),此时返回值就必须也是 person 类型才可以,因此使用 this 指针可以完成需求。先来看第一个例子:

class person{
public:
	int age;
	person(int age)
	{
		this->age = age; // this指针区分调用者
	}
	// 返回值为person类型,且参数加上了const限制,防止修改原数据
	person addPerson(const person& p)
	{
		this->age += p.age;	// 主要实现两个类对象年龄的相加
		return *this;	// 由于返回值是person,因此返回 *this
	}
};

person p1(20);
person p2(10);
person p3 = p2.addPerson(p1).addPerson(p1);
cout << "p1 age:" << p1.age << endl;
cout << "p2 age:" << p2.age << endl;
cout << "p3 age:" << p3.age << endl;

首先,通过构造函数分别对 p1,p2 赋了初值,然后 p2 调用函数 addPerson(p1) 修改自身的变量 age 。**由于函数通过值传递的方式返回 person 类型,所以将整个 person 类型复制了一份返回,返回值继续调用 addPerson(p1),**最后的结果赋值给了新的对象 p3。所以输出结果为:

p1 age:20
p2 age:30
p3 age:50

但是如果函数 addPerson() 修改为:

person& addPerson(const person& p)
{
    this->age += p.age;	// 主要实现两个类对象年龄的相加
    return *this;	// 虽然返回值是person&,返回值的类型也是 *this
}

person p1(20);
person p2(10);
person p3 = p2.addPerson(p1).addPerson(p1);
cout << "p1 age:" << p1.age << endl;
cout << "p2 age:" << p2.age << endl;
cout << "p3 age:" << p3.age << endl;

与上例唯一的区别就在于返回值的类型变成了引用,那么每次返回的就变成了该对象本身,而非在值传递中拷贝出来的那一份数据。那么输出就变成了:

p1 age:20
p2 age:50
p3 age:50

4、析构函数

用构造函数创建对象后,程序负责跟踪该对象,知道其过期为止。当对象过期时,程序自动调用析构函数完成清理工作。与构造函数一样,C++默认提供了一个空的析构函数,定义为:~类名( )由于开辟在栈区的变量程序会自动释放,因此不需要析构函数执行清理工作,但是当程序员在堆区开辟空间时,需要手动执行清理工作,这时候需要析构函数来释放堆区内存。比如:

~person()
{
	// 在析构函数内写入需要执行的代码
	cout << "调用析构函数" << endl;
}
person p1(20);
person p2(10);	// 在生命周期结束后自动调用析构函数执行清理工作

输出为:

调用析构函数
调用析构函数

原文地址:https://www.cnblogs.com/zhaozhibo

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


一.C语言中的static关键字 在C语言中,static可以用来修饰局部变量,全局变量以及函数。在不同的情况下static的作用不尽相同。 (1)修饰局部变量 一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存
浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组的一些区别,然而在某些情况下,指针和数组是等同的,下面讨论一下什么时候指针和数组是相同的。C语言标准对此作了说明:规则1:表达式中的数组名被编译器当做一个指向该数组第一个元素的指针; 注:下面几种情况例外 1)数组名作为sizeof的操作数
浅谈C/C++中的指针和数组(一)指针是C/C++的精华,而指针和数组又是一对欢喜冤家,很多时候我们并不能很好的区分指针和数组,对于刚毕业的计算机系的本科生很少有人能够熟练掌握指针以及数组的用法和区别。造成这种原因可能跟现在大学教学以及现在市面上流行的很多C或者C++教程有关,这些教程虽然通俗易懂,
从两个例子分析C语言的声明 在读《C专家编程》一书的第三章时,书中谈到C语言的声明问题,《C专家编程》这本书只有两百多页,却花了一章的内容去阐述这个问题,足以看出这个问题的重要性,要想透彻理解C语言的声明问题仅仅看书是远远不够的,需要平时多实践并大量阅读别人写的代码。下面借鉴《C专家编程》书中的两个
C语言文件操作解析(一)在讨论C语言文件操作之前,先了解一下与文件相关的东西。一.文本文件和二进制文件 文本文件的定义:由若干行字符构成的计算机文件,存在于计算机系统中。文本文件只能存储文件中的有效字符信息,不能存储图像、声音等信息。狭义上的二进制文件则指除开文本文件之外的文件,如图片、DOC文档。
C语言文件操作解析(三) 在前面已经讨论了文件打开操作,下面说一下文件的读写操作。文件的读写操作主要有4种,字符读写、字符串读写、块读写以及格式化读写。一.字符读写 字符读写主要使用两个函数fputc和fgetc,两个函数的原型是: int fputc(int ch,FILE *fp);若写入成功则
浅谈C语言中的位段 位段(bit-field)是以位为单位来定义结构体(或联合体)中的成员变量所占的空间。含有位段的结构体(联合体)称为位段结构。采用位段结构既能够节省空间,又方便于操作。 位段的定义格式为: type [var]:digits 其中type只能为int,unsigned int,s
C语言文件操作解析(五)之EOF解析 在C语言中,有个符号大家都应该很熟悉,那就是EOF(End of File),即文件结束符。但是很多时候对这个理解并不是很清楚,导致在写代码的时候经常出错,特别是在判断文件是否到达文件末尾时,常常出错。1.EOF是什么? 在VC中查看EOF的定义可知: #def
关于VC+ʶ.0中getline函数的一个bug 最近在调试程序时,发现getline函数在VC+ʶ.0和其他编译器上运行结果不一样,比如有如下这段程序:#include &lt;iostream&gt;#include &lt;string&gt;using namespace std;int
C/C++浮点数在内存中的存储方式 任何数据在内存中都是以二进制的形式存储的,例如一个short型数据1156,其二进制表示形式为00000100 10000100。则在Intel CPU架构的系统中,存放方式为 10000100(低地址单元) 00000100(高地址单元),因为Intel CPU
浅析C/C++中的switch/case陷阱 先看下面一段代码: 文件main.cpp#includeusing namespace std;int main(int argc, char *argv[]){ int a =0; switch(a) { case ...
浅谈C/C++中的typedef和#define 在C/C++中,我们平时写程序可能经常会用到typedef关键字和#define宏定义命令,在某些情况下使用它们会达到相同的效果,但是它们是有实质性的区别,一个是C/C++的关键字,一个是C/C++的宏定义命令,typedef用来为一个已有的数据类型
看下面一道面试题:#include&lt;stdio.h&gt;#include&lt;stdlib.h&gt;int main(void) { int a[5]={1,2,3,4,5}; int *ptr=(int *)(&amp;aʱ); printf(&quot;%d,%d&quot;,*(
联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的: 1)联合体是一个结构; 2)它的所有成员相对于基地址的偏移量都为0; 3)此结构空间要大到足够容纳最&quot;宽&quo
从一个程序的Bug解析C语言的类型转换 先看下面一段程序,这段程序摘自《C 专家编程》:#include&lt;stdio.h&gt;int array[]={23,34,12,17,204,99,16};#define TOTAL_ELEMENTS (sizeof(array)/sizeof(ar
大端和小端 嵌入式开发者应该对大端和小端很熟悉。在内存单元中数据是以字节为存储单位的,对于多字节数据,在小端模式中,低字节数据存放在低地址单元,而在大端模式中,低字节数据存放在高地址单元。比如一个定义一个short型的变量a,赋值为1,由于short型数据占2字节。在小端模式中,其存放方式为0X40
位运算和sizeof运算符 C语言中提供了一些运算符可以直接操作整数的位,称为位运算,因此位运算中的操作数都必须是整型的。位运算的效率是比较高的,而且位运算运用好的话会达到意想不到的效果。位运算主要有6种:与(&amp;),或(|),取反(~),异或(^),左移(&gt;)。1.位运算中的类型转换位
C语言文件操作解析(四)在文件操作中除了打开操作以及读写操作,还有几种比较常见的操作。下面介绍一下这些操作中涉及到的函数。一.移动位置指针的函数 rewind函数和fseek函数,这两个函数的原型是:void rewind(FILE *fp); 将位置指针移动到文件首 int fseek(FILE
结构体字节对齐 在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排
C语言文件操作解析(二)C语言中对文件进行操作必须首先打开文件,打开文件主要涉及到fopen函数。fopen函数的原型为 FILE* fopen(const char *path,const char *mode) 其中path为文件路径,mode为打开方式 1)对于文件路径,只需注意若未明确给出绝