构造函数系列——c++

25 篇文章 0 订阅

构造函数

构造函数:对象初始化

析构函数:对象清理

1,都是编译器是默认添加的。
2,只要创建对象和释放对象,那么系统就会默认的调用这两个函数。(不需要手动调用)
3,构造和析构都必须要声明在全局作用域下。也就是前面得有public:
4,构造函数没有返回值,也不用写void



构造函数介绍

1,函数名与类名相同。
2,可以有参数,可以有重载。
3,代码中的p是开辟到栈上的,函数执行完就会释放。构造函数是创建的时候调用的。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class Person
{
public:
   Person()
   {
   	cout << "构造函数已经被调用" << endl;
   }
};
int main()
{
   Person p;

   return 0;
}
//注意写类的时候最后的‘;’,不要忘了。



析构函数介绍

1,没有返回值,不用写void
2,函数名和类名相同,函数名前加~
3,不可以有参数,不可以发生重载
4,代码中的p是开辟到栈上的,函数执行完就会释放。构造函数是释放的时候调用的。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class Person
{
public:
	Person()
	{
		cout << "构造函数已经被调用" << endl;
	}
	~Person()
	{
		cout << "析构函数已经调用" << endl;
	}
};
int main()
{
	Person p;

	return 0;
}


构造函数的分类和调用


两种分类分类方式:

按照参数分类:分为无参构造(默认构造函数)和有参构造。

class Person
{
public:
	Person()
	{
		cout << "无参构造函数已经被调用" << endl;
	}
	Person(int a)
	{
		cout << "有参构造函数已经被调用" << endl;
	}
};

int main()
{
    Person p;//调用无参
	Person p(1);//调用有参
	return 0;
}



按照类型分类:分为普通构造函数 和 拷贝构造函数(除了拷贝构造函数以外的都叫普通构造函数)

拷贝构造函数定义

如果要拷贝复制一个张三出来(张三复制出来以后名字叫做李四),那么就得传进去一个张三,但是不能用值传递,需要使用引用传递,因为如果使用值传递,那么就是将张三放进去,然后经过改变之后出来就成李四了。而目的是复制一个张三,所以肯定不对,应该传进来去张三的引用,然后复制一个与张三具有相同特征的李四。还需要加个const防止对p进行修改。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class Person
{
public:
	Person(int a)
	{
		age = a;
		cout << "有参构造函数已经被调用" << endl;
	}
	Person()
	{
		cout << "无参构造函数已经被调用" << endl;
	}
	
	Person(const Person& p)//拷贝构造函数
	{
		age = p.age; 
        cout << "拷贝构造函数已经被调用" << endl;
        
	}

	int age;
};
int main()
{
	Person p(18);//有参构造
    //Person p;//无参构造,不需要加括号,加括号就不会看作是创建实例对象了,而是一个名叫p的函数。
    //Person(10);//匿名对象,没有名字,创建这个对象以后就会立即将这个对象回收。(执行完有参构造函数以后立即进行析构函数)
	Person p2(p);
	cout << "p2的年龄是:" << p2.age << endl;
	return 0;
}

根据这个例子可以看出,也就是将构造的对性p 的 年龄等特征克隆到新个体中。

1,注意:不要用拷贝构造函数来初始化匿名对象。

Person(p);//匿名构造函数。
//编译器认为这个是 Person p对象的实例化操作,如果在之前已经实例化过p,那么就会报错:p3重定义。

2,隐式法:

Person p5 = 10;//Person p5 = Person(10);
Person p6 = p5;
//用的比较少。


拷贝构造函数的调用时机

1,用已经创建好的对象来初始化新的对象。

Person p2 = Person(p1);
Person p2 = p1;
//这两种是一样的。

2,用值传递的方式来给函数的参数传值。

class Person
{
public:

	Person(int a)
	{
		age = a;
		cout << "有参构造函数已经被调用" << endl;
	}
	Person()
	{
		cout << "无参勾走函数已经被调用" << endl;
	}
	
	Person(const Person& p)//拷贝构造函数
	{
		age = p.age;
		cout << "拷贝构造函数已经被调用" << endl;
	}
	~Person()
	{
		cout << "析构函数已经被调用" << endl;
	}
	int age;
};

void doWork(Person p)
{
	//cout << p.age << endl;
}
int main()
{
	Person p1(18);//
	doWork(p1);
	return 0;
}

输出的结果是:

有参构造函数已经被调用
拷贝构造函数已经被调用
析构函数已经被调用
析构函数已经被调用

调用doWork函数的时候,就已经是在调用拷贝构造函数了,将p作为值传去当参数。值传递的本质就是调用它的拷贝构造函数。所以说拷贝的这个p是新的东西,和原来的p1不是相同的东西。


3,以值的方式返回局部对象

Person doWork2()
{
	Person p;
	return p;
}

void test2()
{
	Person p = doWork2();
}

输出的结果是:

无参勾走函数已经被调用
拷贝构造函数已经被调用
析构函数已经被调用
析构函数已经被调用

doWork2()这里的返回值是按照值的方式返回的,之前说不要返回一个局部对象的引用Person&(p这个局部对象马上就会被释放了)。但是如果返回的是一个值,返回的就不是p了,而是以据p创建出来的新的p2数据(本身是没有名字的,相当于构造了一个匿名对象),然后在执行test2()的时候给这个没有名字的匿名对象赋上名字。(这个匿名对象是return的时候创建出来的,原先的dowork中的p就会释放,而传出来的数据的寿命会延续到下个函数周期。)

注意:

以下是代码和运行之后的结果:代码区别是调用的函数有没有’&‘符号。结果的区别是有无构造拷贝函数的调用。而且在有&符号的函数中使得p的age改成100的话,那么main中的p的age会改变。而没有&符号的函数中p的age发生改变不会影响到main中age的改变。

调用doWork函数的时候,就已经是在调用拷贝构造函数了,将p作为值传去当参数。值传递的本质就是调用它的拷贝构造函数。

发现如果有&,表示上传的是p这个对象,只是另一个名字。而没有&,就表示值传递,传的仅仅只是p的数据,不会使得main中的p进行改变。

void doWork3(Person& p)
{
}

int main()
{
	Person p;
	doWork3(p);
	return 0;
}

无参构造函数已经被调用
析构函数已经被调用

void doWork3(Person p)
{
}

int main()
{
	Person 
	doWork3(p);
	return 0;
}

无参构造函数已经被调用
拷贝构造函数已经被调用
析构函数已经被调用
析构函数已经被调用

注意:如果自己写了一个有参的构造函数,那么系统就不会给你生成无参的(默认的)构造函数。如果需要就得自己写无参的构造函数。





深浅拷贝


构造函数的作用就是对对象的属性进行赋值

class Person
{
public:
	Person(char* name, int age)
	{
		m_name = (char*)malloc(strlen(name) + 1);//给名字在堆上申请空间,m_name指向这个空间
		strcpy(m_name, name);
		m_age = age;
	}
	int m_age;
	char* m_name;
};

image-20220205095913180

根据上面的代码:创建了p1这个对象,m_name保留着名字的地址,指向堆中的“德玛西亚”,年龄是20,然后使用默认拷贝构造函数通过p1创建了p2,指针相同,年龄也一样。然后改变了系统默认的析构函数,如果发现指针不是空,那么就释放掉。这样导致了p2的指针释放了以后再次检验p1 的指针发现不是空,但是如果释放的话就构成了释放重复。

错误的原因:它拷贝的时候用的是编译器的拷贝构造函数,自己写一个就行了。(利用深拷贝解决原来的简单的浅拷贝问题)

Person(const Person&p)//在这里加const是为了防止不小心将传进来的引用修改
{
    m_name = (char*)malloc(strlen(p.m_name)+1);
    strcpy(m_name,p.m_name);
    m_age = p.m_age;
}
//这就是自己创建的拷贝构造函数,然后自己又在堆上创建了空间,然后进行赋值。这样就有两个空间,释放的时候就不会重复了。

image-20220205101132721

完整代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;


class Person
{
public:
	Person(const char * name, int age)
	{
		m_name = (char*)malloc(strlen(name) + 1);//给名字在堆上申请空间,m_name指向这个空间
		strcpy(m_name, name);
		m_age = age;
	}
	~Person()
	{
		if (m_name != NULL)
		{
			cout << "Person析构调用" << endl;
			free(m_name);
			m_name = NULL;
		}
	}
	Person(const Person& p)
	{
		m_name = (char*)malloc(strlen(p.m_name) + 1);
		strcpy(m_name, p.m_name);
		m_age = p.m_age;
	}
	int m_age;
	char* m_name;
};

int main()
{
	const char* name = "德玛西亚";
	Person p(name, 20);
	Person p2(p);
	return 0;
}

//这样的结果是两个析构函数的调用,解决了重复析构的问题。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++ 默认构造函数是在没有显式定义构造函数的情况下自动生成的特殊成员函数。它通常用于在创建对象时进行初始化操作。默认构造函数无参数,不接受任何实参。当我们通过调用类的构造函数来创建对象时,如果没有提供实参,则编译器会自动调用默认构造函数。 默认构造函数的作用是确保对象的所有成员变量都被正确初始化。例如,如果一个类有一个int类型的成员变量,那么在默认构造函数中,可以将该成员变量初始化为0。如果没有默认构造函数,当我们创建对象时,该成员变量可能会未被初始化,导致程序运行时出现意外结果。 另一个重要的地方是,当我们定义了类的其他构造函数时(比如有参数的构造函数),默认构造函数依然会被生成。这是因为在某些情况下,我们可能只想使用默认构造函数来创建对象,而不希望传递实参。此时,默认构造函数就能满足需求。当我们重载构造函数时,可以使用默认参数来实现默认构造函数的功能。 需要注意的是,默认构造函数在一些特殊情况下可能不会被生成。例如,如果我们显式定义了有参数的构造函数,但没有提供默认构造函数,那么编译器将不会自动生成默认构造函数,这意味着我们不能再使用无参的方式来创建对象。 总之,理解C++默认构造函数的作用和用法对于编写高质量的代码至关重要。它可以帮助我们确保对象的正确初始化,并且在一些特殊情况下可以提供方便的使用方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小明同学啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值