C++中构造函数的超详细讲解

2023-04-11 10:28:54

C++在C语言的基础上增加了类和对象的概念,官方对类和对象的解释是:对象是类的实例化,类是对象的抽象,其实这个概念也很抽象,举一个简单的例子来说明这个关系:

在汽车生产车间我们要先画一张完整的汽车设计图后再通过组装零件来完成汽车的制造,而这里的图纸就相当于C++中的类,通过图纸生产的汽车就是对象,所以汽车就是对图纸的实例化,图纸就是汽车的抽象化,而构造函数就充当了一个很重要的角色。

重点:以上述例子为例:什么被称为汽车呢,那应该是具有了汽车的特征,如四个轮子,一个 发动机,一个方向盘等等,所以我们之所以称它为一辆汽车,是因为它有了汽车的所有特征,如果少了轮子或者方向盘那么都不叫一个完整的汽车,对象亦是如此,我们定义了一个类去实例化一个对象时那么这个对象就应该有该类所应有的特征,所以构造函数的功能就是帮我们在实例化一个对象时能够自动的完成这个对象的初始化,例如我们在定义一个Triangle 类,那么一个三角形的特征是什么呢,或者说我们需要什么才能够描述出一个三角形呢,那就是三角形的三条边 a,b,c,所以我们定义的Triangle 这个类中就应该有三角形的三条边a,b,c,假如没有使用构造函数,那么我们实例化好一个对象后,该对象的成员数据a,b,c 都会是一个不确定的值,既然是不确定的值那么它就很有可能不是一个三角形,因为三角形的成立条件是a+b>c ,所以就不能称之为三角形,那么这个对象的实例化就失败了。所以构造函数就能解决这个问题。

通过上诉例子,我们就来介绍构造函数,构造函数主要有两种(一共三种),普通构造,拷贝构造分别能够为不同种类的成员赋值(由于另一种移动构造设计运算符重载,所以会有一篇关于它的文章),其中构造函数有以下几个重要的特征:

1,构造函数名与类名相同,且无返回值(void也不行)

2,在每一个类中都有存在有默认的构造函数(系统自行提供),且在实例化对象时都会调用,        只是默认不做任何事情

3,构造函数会在实例化对象时自动调用,不允许显式调用

下面我们逐一进行详细介绍。

一,普通构造函数

普通构造函数中分为有参构造函数和无参构造函数,即带参数的构造函数和不带参数的构造函数,上面说到系统默认提供的构造函数就是无参构造,默认不做任何事,如果我们自己写了构造函数,系统便不再使用默认构造函数,而是调用我们自己写的构造函数,这就叫构造函数的重写,我们通过一个小小的例子来证明构造函数是系统自动调用的

#include<iostream>
using namespace std;
class Triangle          //三角形类
{
	public:
		Triangle()                 //重写构造函数
		{
			cout<<"无参构造"<<endl;
		}
		private:
			int a;
			int b;
			int c;
};
int main()
{
	Triangle A;                //实例化对象
}

执行结果如下:

 结论1:上述例子中我们并没有手动调用构造函数,仅仅只做了实例化对象这一个操作,但是构造函数还是被执行了,这就证明了构造函数是在实例化对象时系统自动调用的。

诶,通过上面的例子是不是就可以想到怎么利用构造函数来进行简单的成员变量初始化了,没错,我们在上述函数的基础上稍加修改就可以实现。

#include<iostream>
using namespace std;
class Triangle
{
	public:
		Triangle()
		{
			cout<<"无参构造"<<endl;
			a=3;
			b=4;
			c=5;
		}
		void getValue()                 //成员函数(打印成员变量值)
		{
			cout<<"a="<<a<<endl;
			cout<<"b="<<b<<endl;
			cout<<"c="<<c<<endl;
		}
		private:
			int a;
			int b;
			int c;
};
int main()
{
	Triangle A;
	A.getValue();                  //调用成员函数
}

 

 通过这种方法就可以实现最简单的成员变量初始化,但是基本不这样用,因为除了可以初始化之外没有任何意义,所以我们就来说普通构造的第二种,有参构造。

以上述例子为例,那么有参构造的形式如下:

Triangle(int a,int b,int c)                    //参数名随意
		{
			cout<<"有参构造"<<endl;
			this->a=a;                    //为每个成员赋值
			this->b=b;
			this->c=c;
		}

 通过传进来的参数来为成员变量赋值,那么程序的灵活性就会更高,调用方式也很简单,只需在实例化对象时给对象传入参数即可

Triangle A(3,4,5);                  //分别为对应参数传参

 结果如下:

 这样我们就能够根据需求在实例化对象时传入不同的参数来赋值,很简单吧。接下来介绍第二种

 二,拷贝构造函数

 在实际需求中我们不仅仅需要为对象赋予基本的数值,比如我现在又实例化了另一个对象,我想把这个对象中的所有成员或特定的成员赋值给另一个时,这时候使用普通构造函数就不能实现这个功能了,于是就有了拷贝构造函数,拷贝构造函数就是专门用于同一类对象的赋值,其实系统也有默认的拷贝构造函数,我们也来证明以下看看它是否真正存在。

#include<iostream>
using namespace std;
class Triangle
{
	public:
		Triangle()                   //无参构造
		{
			cout<<"无参构造"<<endl;
		}

		private:
			int a;
			int b;
			int c;
};
int main()
{
	Triangle A;                //使用无参构造初始化A
	Triangle B(A);               //使用拷贝构造初始化B
}

 有小伙伴是不是发现了什么,在上面我们明明实例化了两个对象啊,怎么只调用了一次无参构造呢,另外一个呢。其实我们的对象B调用了系统默认的拷贝构造,因为在现实需求中,这种初始化的方式很常见,所以系统就干脆弄了一个默认的拷贝构造供用户使用,同样,如果我们对拷贝构造函数进行重写,那么默认的就不再发挥作用而是使用我们自己写的,拷贝构造定义和调用如下:

 

#include<iostream>
using namespace std;
class Triangle
{
	public:
		Triangle(int a,int b,int c)          //有参构造           
		{
			this->a=a;
			this->b=b;
			this->c=c;
			cout<<"有参构造"<<endl;
		}
		Triangle(Triangle &obj)                 //拷贝构造
		{
			a=obj.a;                 //通过目标对象对每个成员变量赋值
			b=obj.b;
			c=obj.c;
			cout<<"拷贝构造"<<endl;
		} 
		void getValue()                   //成员函数
		{
			cout<<"a="<<a<<endl;
			cout<<"b="<<b<<endl;
			cout<<"c="<<c<<endl;
		}

		private:
			int a;
			int b;
			int c;
};
int main()
{
	Triangle A(3,4,5);                    //A调用了有参构造
	Triangle B(A);                        //B调用了拷贝构造
	B.getValue();
}

 这样就可以直接通过对象给对象赋值实现初始化了,很方便。但是人们后面又发现了这种拷贝构造存在一个缺陷,那就是如果对象中存在指针的时候就会发现一个问题,在对指针指向内存进行释放时会造成重复释放,这显然就有问题了,那么我们如何证明呢,这时就浅说以下析构函数,大家现在只需知道析构函数是用来干嘛的,析构函数是用于对象生命周期结束时自动释放对象所占资源的函数,它也是系统自动调用的,当然我们进行重写来展现效果ok,已经够我们证明之前的结论了。

假如我们已经定义了一个Mystring类,类中有指针成员p,现在我们实例化了两个对象str1,str2,为对象str2指针初始化为" hello ",假设字符串" hello "的地址为0x0001,现在我们通过拷贝构造使用str2来初始化str1,会进行如下操作: 

 这时我们就会发现str1的p和str2的p指向的不都是同一片地址吗,怎么释放了两次,这显然不得行,所以我们就把这种系统默认的拷贝构造函数称为浅拷贝,也就是我们之前说的拷贝构造都是浅拷贝,它只会对值进行赋值而不管值类型的特殊带来的后果,所以我们在对含有指针成员的对象进行拷贝构造初始化时就需要使用深拷贝(必须重写,不然就会默认使用系统的浅拷贝),深拷贝的原理如下:

 这样str1的p就不再和str2中的p指向同一片空间了,所以在对象被析构函数释放后也不会对新开辟的空间进行重复释放,就解决了浅拷贝中重复释放空间的问题,深拷贝具体代码如下:

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

class Mystring
{
	public:
		~Mystring()
		{
			delete p;
			cout<<"析构函数"<<endl;
		}
		Mystring(char* str)                   //有参构造 
		{
			p=new char[strlen(str)+1];
			strcpy(p,str);
		}
		Mystring(Mystring &obj)                     //深拷贝 
		{
			p=new char[strlen(obj.p)];             //申请新的空间 
			strcpy(p,obj.p);                    //将目标对象的内容复制到自身空间 
		}
		void print()
		{
			cout<<p<<endl;
		}
		private:
			char* p;
};
int main()
{ 
	Mystring str1("hello");                    //使用有参构造初始化str1
	Mystring str2(str1);                       //使用深拷贝用str1初始化str2
	str2.print();
}

 

 其实深拷贝和浅拷贝的区别就是有没有赋值之前为新对象的指针成员开辟空间

总结:1,系统提供了一种构造函数可以实现同类对象之间进行直接赋值初始化,这种叫浅拷贝

           2,如果需要赋值的对象成员有指针,那么就必须重写拷贝构造进行深拷贝,注意是必须,不然会造成内存泄露。

 

 

  • 作者:guishangppy
  • 原文链接:https://blog.csdn.net/guishangppy/article/details/125876729
    更新时间:2023-04-11 10:28:54