C++进阶剖析( 九)之对象的构造和销毁

本文围绕C++的构造函数和析构函数展开。介绍了构造函数的引出、初始化、特点、类型(无参、拷贝等)及应用场景,还提及构造函数的调用和数组类开发。同时阐述了析构函数的作用、定义准则、调用顺序等,强调其对防止内存泄漏的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.1构造函数
1.1.1构造的引出
下面的代码中,类的成员函数的初始值是多少?

class Test
{
private:
	int a;
	int b;
public:
	void print()
	{
		printf("a =%d\n",a);
		printf("b =%d\n",b);
	}
};

Test t3;
int main()
{
	Test t1;
	t1.print();
	Test*  t2 = new Test;
	t2->print();
	t3.print();
	delete t2;
	return 0;
}

结果:
栈上分配内存: 随机值
堆上分配内存:随机值
全局区分配内存: 0

结论:
从程序设计的角度,对象只是变量,因此:

  • 上创建对象,成员变量初始值为随机值
  • 上创建对象,成员变量初始值为随机值
  • 静态存储区创建对象时,成员变量初始为0
  • 类其实是一种自定义的类型,类似于C语言中的基本数据类型int .double,char等

上面的问题引出了对象的初始化

1.1.2 初始化

  • 生活中的对象都是在初始化后上市的,
  • 初始状态(出厂设置)是对象普遍存在的一个状态
  • 那么程序如何对一个对象进行初始化呢?
  • 一般而言,对象都需要一个确定的初始状态

解决方案

  • 在类中提供一个public的initialize 函数
  • 在对象创建以后立即调用initialize 函数进行初始化

1.1.3 上面解决方案存在的问题

  • 调用初始化函数必须显示调用
  • 如果创建对象的时候我忘记调用了initialize 函数,那对象的初始值就是随机值(栈上,堆上创建的时候)。

1.1.4 构造函数
C++中可以定义与类名同名的特殊成员函数

  • 这种特殊的成员函数叫构造函数
    (1)构造函数没有任何返回类型的声明
    (2)构造函数在对象定义的时候自动被调用
  • 构造函数代码初探
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
class Test
{
private:
	int a;
	int b;
public:
	Test(int a = 0,int b =0)
	{
		this->a =a;
		this->b =b;
	}
	void print()
	{
		printf("a =%d\n",a);
		printf("b =%d\n",b);
	}
};

Test t3;
int main()
{
	Test t1;
	t1.print();
	Test*  t2 = new Test;
	t2->print();
	t3.print();
	return 0;
}

//结果全是 0;

1.2 构造函数进阶
1.2.1 构造函数

  • 构造函数和普通函数有啥区别
    (1)构造函无返回值
    (2)构造函数与类同名
    (3)构造函数的参数和普通函数是一样的,都可以带参数,也可以不带参数
    (4)构造函数也可以像普通函数那样进行重载,构造函数的重载遵循C++重载的规则(重载发生在同一个作用域中,构造函数的重载发生在同一个类中)

1.2.2 构造函数是自动调用的

class Test
{
public:
	Test(){ printf("Test()\n");}
	Test(int v){ printf("Test(int v), v = %d\n", v);}
};
int main()
{
	Test t1   ;  //  调用 Test()
	Test t2(10);   //  调用 Test(int v)
	Test t3 =1;    //  调用 Test(int v)
	int  a =1;  //初始化
	a  = 2;  //赋值
	return 0;
}
注意 : 初始化和赋值是两个不同的概念,在C语言中(面向过程编程),初始化和赋值没有什么区别, 但是在面向对象的编程中,初始化和赋值是由很大区别的,初始化会调用构造函数。 那么赋值的时候会怎样呢?(后面说)
class Test
{
public:
	Test(){ printf("Test()\n");}
	Test(int v){ printf("Test(int v), v = %d\n", v);}
	Test(const Test &t){printf("Test(const &t)\n");}
};
int main()
{
	Test t1   ;  //  调用 Test()
	Test t2(10);   //  调用 Test(int v)
	Test t3 =1;    //  调用 Test(int v)
	Test t4 =t1; //初始化 调用Test(const Test &t)
	t1 =t2;    // 不会调用copy构造函数
	return 0;
}

在这里插入图片描述

1.3特殊的构造函数
1.3.1 无参构造函数

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

1.3.2 拷贝构造函数

  • 参数为 const class_name & 的构造函数
  • 当类中没有定义copy 构造函数时,编译器默认提供一个copy构造函数,简单进行成员变量的值复制。

1.3.3 对象定义和对象声明不同

  • 对象定义 - 申请对象的空间并调用构造函数
  • 对象声明- 告诉编译器存在这样一个对象
Test  t;  //定义对象并调用构造函数
int main()
{
	extern Test t; //告诉编译器存在名为 t的Test对象
	return 0;
}

1.3.4copy构造函数的四种应用场景
1 copy构造函数第一种应用场景

Test  t1(1,2);
Test t2 =t1 ;// 调用copy构造函数

2 copy构造函数第二种应用场景

Test  t1(1,2);
Test t2(t1) ;// 调用copy构造函数

3 copy构造函数第三种应用场景
实参初始化形参(形参是非引用的形式),调用copy构造函数。(如果形参是引用,那么就不会调用copy 构造函数了)。
4 copy构造函数第四种应用场景

Location g()
{
Location A(1,2);
return A;  //
}

g() 返回一个元素,return A的时候会调用一个copy构造函数形成一个匿名对象

函数的调用方法
1、Location B= g() //若返回的匿名对象来初始化另外一个同类型的对象,那么匿名对象会直接转成新的对象。
2、Location B ; // 创建对象
B = g(); // 用匿名对象赋值给B对象,然后匿名对象被析构。

1.3.5 构造函数的调用

  • 一般情况下,构造函数在对象定义时被自动调用
  • 一些特殊情况下,需要手工调用构造函数
    • 如何创建一个对象数组?
class Test
{
private:
	int m_value;
public:
	Test(){
		printf("Test()\n");
		m_value =0;
	}
	Test(int v){ 
		printf("Test(int v), v = %d\n", v);
		m_value  = v;
		}
	int getValue()
	{
		return m_value;
	}
};
int main()
{
	//根据需要手工调用构造函数,
	Test t[3] ={Test(),Test(10),Test(20)};
	for(int i =0;i<3;i++)
	{
		printf("t[%d] ;value =%d \n",i,t[i].getValue());
	}
	Test t2 = Test(100);  //手工调用构造函数。
	printf("t2.m_value = %d\n",t2.getValue());  
	return 0;
}

结果
在这里插入图片描述

1.3.6 开发数组类解决原生数组的安全性问题

  • 提供函数获取数组的长度
  • 提供函数获取数组的元素
  • 提供函数设置数组元素
class InitArray{
private:
	int m_length;
	int * m_array;
public:
	
	InitArray(int length =0)
	{
		m_length = length;
		m_array = new int[m_length];
		for(int i =0;i<m_length;i++)
		{
			m_array[i] = 0;
		}
	}
	int getLen()
	{
		return m_length;
	}
	bool setValue(int index,int value)
	{
		bool ret = true;
		if(index>=m_length || index <0) return false;
		m_array[index] = value;
		return ret ;
	}
	bool getValue(int index,int &value)
	{
		bool ret = true;
		if(index>=m_length || index <0) return false;
		value  =m_array[index] ;
		return ret ;
	}
	~InitArray()
	{
		delete [] m_array;
	}


};
int main()
{
	InitArray arr(10);
	int value ;
	for(int i =0;i<10;i++)
	{
		arr.getValue(i,value);
		printf("arr[%d] =%d\n",i,value);
	}
	for(int i =0;i<10;i++)
	{	
		value = i+10;
		arr.setValue(i,value);
	}
	printf("==========after===================\n ");
	for(int i =0;i<10;i++)
	{
		arr.getValue(i,value);
		printf("arr[%d] =%d\n",i,value);
	}
	return 0;
}

结果:
在这里插入图片描述

1.4 特殊的构造函数
无参构造函数 和 copy构造函数

1.4.1 无参构造函数
1.4.1.1无参构造函数

  • 当类中没有定义构造函数时(包括有参数,无参数,copy构造函数),编译器默认提供一个无参数构造函数,并且其构造体为空
  • 当类中定义了构造函数的时候,编译器不会提供默认的无参构造函数

1.4.1.2实例代码

class Test
{
};
//这个类中有什么
//至少有一个无参构造函数,copy构造函数,析构函数
#include <stdio.h>
#include <stdlib.h>
class Test{
private:
        int i;
        int j;
public:
        int getI(){
                return i;
        }
        int getJ()
        {
                return j;
        }
        /*
        Test(const Test&obj) //定义了copy 构造函数 C++就不会提供默认的无参数构造函数,当Test t1;的时候,就会报错
        {
        	i = obj.i;
        	j = obj.j;
        }
        */
};


int main()
{
        Test t1;
        Test t2 ;
        printf("t1.i =%d, t1.j =%d\n",t1.getI(),t1.getJ());
        printf("t2.i =%d, t2.j =%d\n",t2.getI(),t2.getJ());
        Test t3 = t1;
        printf("t3.i =%d, t3.j =%d\n",t3.getI(),t3.getJ());
        return 0;
}

结果: 注意 t1和t3的结果是相同的,结果说明C++会提供一个默认的copy构造函数
: Test t1 可以不报错说明当没有定义任何构造函数(包括无参数构造函数,有参数构造函数,copy构造函数)的时候,C++会提供一个默认的无参构造函数
在这里插入图片描述
定义了copy 构造函数 C++就不会提供默认的无参数构造函数,当Test t1;的时候,就会报错
在这里插入图片描述
1.4.2 copy构造函数的意义
1.4.2.1copy构造函数

  • 当类中没有定义copy构造函数时,编译器默认提供一个copy构造函数,简单的进行成员变量的值复制

1.4.2.2 copy构造函数的意义

  • 浅拷贝
  • (1) copy后对象的物理状态相同
  • 深拷贝
    (2)深拷贝后对象的逻辑状态相同

注意:物理状态相同,即如果类的成员变量中有指针的时候,仅仅是将地址进行了一份复制也就是两个对象的成员变量指向同一块内存空间。逻辑状态相同是当类的成员变量中有指针的时候,开辟了一段内存空间,并将第一个对象的指针成员所指向内存空间的内容copy了一份。

在这里插入图片描述
1.4.3 实例说明

class Test{
private:
        int i;
        int j;
        int *p;
public:
        int getI(){
                return i;
        }
        int getJ()
        {
                return j;
        }
        int * getP()
        {
                return p;
        }
        Test(int v)
        {
                i =10;
                j =20;
                p =new int;
                *p = v;
        }
        void free()
        {
                delete p;
        }
};
int main()
{
        Test t1(30);

        printf("t1.i =%d, t1.j =%d,t1.p =%p\n",t1.getI(),t1.getJ(),t1.getP());
        Test t3 = t1;
        printf("t3.i =%d, t3.j =%d,t3.p =%p\n",t3.getI(),t3.getJ(),t1.getP());
        t1.free();
        t3.free();
        return 0;                                                                                                                          }

结果数据打印出来了,在释放内存的时候报错了。因为同一个内存空间释放了两次。
在这里插入图片描述

  • 内存四区图
    在这里插入图片描述
    上面问题的解决方法,自己搜共打造copy构造函数
        Test(const Test&obj)
        {
                i =obj.i;
                j = obj.j;
                p = new int;
                *p = *obj.p;
        }

在这里插入图片描述

1.4.4深copy的一般性原则

  • 对象中有成员指代了系统中的资源
  • 成员指向了动态内存 空间
  • 成员打开了外存中的文件
  • 成员使用了系统中的网络端口

一般性原则 : 自定义copy构造函数,必然要实习深copy

数组类的优化,由于是重复的代码,所以只写了非重复部分

	InitArray(const InitArray&obj)
	{
		m_length = obj.m_length;
		m_array = new int[m_length];
		for(int i =0;i<m_length;i++)
		{
			m_array[i] = obj.m_array[i];
		}
	}

1.5 析构函数
1.5.1析构函数作用
堆上分配内存用delete销毁对象,栈上生成对象,对象在生命周期结束后销毁,然后调用析构函数进行清理。
析构函数是为了清理而不是销毁对象的,在创建对象的时候,如果调用了系统资源(如new 一个对象),这样我们在销毁对象的时候就必须释放这个资源,如果我们手工释放的时候,我们可能在写了一堆代码的时候忘记释放资源了,这时候就有了内存的泄漏,这时候就需要自定义析构函数来进行重写。在析构函数中释放内存,防止内存泄漏。

1.5.2什么函数具有销毁功能:
delete 函数具有销毁功能,或者是局部对象生命期结束。

1.5.3 总结
对象的创建在栈上,直接就是 Test t,在堆上就是 new Test;
构造函数是进行初始化的,析构函数是在对象销毁前进行清理的。
一般在申请了系统资源的时候,会自定义析构函数。
析构函数在对象销毁前被调用,析构函数是对象释放系统资源的重要保证。
对象的创建和对象的初始化是两个不同的概念。

1.5.4 析构函数的定义准则

  • 当泪中自定义了构造函数,并且构造函数中使用了系统表资源(如:内存申请,文件打开等);则需要自定义析构函数

1.5.5 关于析构的答案

  • 对于栈对象和全局对象,类似于入栈和出栈的顺序,最后构造的对象最先析构、
  • 对对象的析构发生在使用delete的时候,与delete的使用顺序相关。

1.6对象创建的时候构造函数调用顺序
1.6.1调用顺序

  1. 调用父类中的构造函数
  2. 调用成员变量的构造函数(调用顺序和声明顺序有关和初始化顺序无关
  3. 调用自身的构造函数

概括:构造函数:先父母,再他人,后自己,析构函数和构造函数相反

1.6.2 案例

class Parent
{
private:
	int mi;
public:
	Parent(int i)
	{
		this->mi =i;
		printf("this is parent Parent(int i) fuction \n");
	}
	~Parent()
	{
		printf("this is parent ~Parent() fuction \n");
	}
};

class Test
{
private:
	int value;
public:
	Test(int value)
	{
		this->value =value;
		printf("this is Test(int value) : value = %d\n",value);
	}
	~Test()
	{
		printf("this is ~Test() : value = %d\n",value);
	}
};
class Child:public Parent
{
private:
	Test v2;
	Test v1;
public:
	Child(int i,int j,int k):Parent(i),v1(j),v2(k)
	{
		printf("this is child(int i,int j,int k)\n");
	}
	~Child()
	{
			printf("this is ~Child()\n");
	}
};
int main()
{	
	Child c1(1,2,3);
	return 0;
}

运行结果如下:在这里插入图片描述

参考一 :狄泰软件学院C++进阶剖析
如有侵权:请联系邮箱 1986005934@qq.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值