【一、构造函数与析构函数】深度解析C++类的构造函数与析构函数调用机制


一、构造函数与析构函数

1.构造函数定义

构造函数是一种特殊的成员函数,它不需要用户手动调用(在某些情况下需要手动调用),而是在创建对象的的时候自动调用,构造函数的作用是初始化对象中的数据成员。

2.构造函数特点

①构造函数的名称必须与类名相同;
②构造函数没有返回值类型;
③构造函数可以重载(无参构造函数、有参构造函数、拷贝构造函数);

3.析构函数定义

析构函数是清理对象资源的一类特殊成员函数,析构函数的名称是类名前加~号,它在对象释放前自动调用,无参数(因为没有参数,所以无法重载—函数重载的判断依据是参数类型、参数顺序、参数个数)、无返回值类型且析构函数种禁止使用return语句。

二、构造函数分类与调用机制

1.无参构造函数

如果类中没有定义任何构造函数,编译器会提供一个默认无参构造函数(和默认拷贝构造函数);若类中定义了任意一个构造函数,编译器不会提供默认无参构造函数。下面结合代码详细分析无参构造函数的调用机制,代码如下:

#include <iostream>
using namespace std;

class MyClassA
{
public:
    MyClassA()
    {
        my_a = 1;
        my_b = 2;
        cout << "无参构造函数调用" << endl;
    }

    ~MyClassA()
    {
        cout << "析构函数调用" << endl;
    }

public:
    void PrintData()
    {
        cout << "my_a = " << my_a << " my_b = " << my_b << endl;
    }

private:
    int my_a;
    int my_b;
};

void ClassTest1()
{
    MyClassA A1; //创建对象的时候调用构造函数,默认无参构造函数
    A1.PrintData();
} //在对象的生命周期结束的时候调用析构函数

void ClassTest2()
{
	MyClassA A1, A2; //先调用A1构造函数,后调用A2构造函数//先定义先构造
} //先析构A2,后析构A1//后定义先析构

int main()
{
    ClassTest1(); //加断点单步调试来观察构造函数与析构函数的调用机制
    ClassTest2();

    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

2.有参构造函数

有参构造函数有三种调用场景,现结合代码分析,代码如下:

#include <iostream>
using namespace std;

class MyClassA
{
public:
    MyClassA(int a)
    {
        my_a = a;
        my_b = a;
        cout << "一个参数的有参构造函数调用" << endl;
    }

    MyClassA(int a, int b)
    {
        my_a = a;
        my_b = b;
        cout << "两个参数的有参构造函数调用" << endl;
    }

    ~MyClassA()
    {
        my_a = 0;
        my_b = 0;
        cout << "析构函数调用" << endl;
    }

public:
    void PrintData()
    {
        cout << "my_a = " << my_a << " my_b = " << my_b << endl;
    }

private:
    int my_a;
    int my_b;
};

void ClassTest1()
{
    //MyClassA A1; //因为已经自己定义了有参构造函数,不再提供默认无参构造函数
    			   //错误	C2512	“MyClassA” : 没有合适的默认构造函数可用
    //A1.PrintData();
}

void ClassTest2()
{
    //有参构造函数的第一种调用场景
	MyClassA A1(1, 2);
	A1.PrintData();

    //有参构造函数的第二种调用场景//这里调用的是一个参数的构造函数!!!
    						 //(3, 4)逗号表达式,值为4
    MyClassA A2 = (3, 4);
    A2.PrintData();
	MyClassA A22 = 4; //与A2的定义等价
	A22.PrintData();

    //有参构造函数的第三种调用场景
    MyClassA A3 = MyClassA(5, 6);
    A3.PrintData();
}

int main()
{
    //ClassTest1();
    ClassTest2(); //加断点单步调试来观察构造函数与析构函数的调用机制

    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

3.拷贝构造函数

如果类中没有定义任何构造函数,编译器会提供一个默认拷贝构造函数;如果类中没有定义拷贝构造函数,编译器会提供一个默认拷贝构造函数,并执行浅拷贝操作。

(1)拷贝构造函数的三种调用场景

#include <iostream>
using namespace std;

class MyClassA
{
public:
    MyClassA()
    {
        my_a = 0;
        my_b = 0;
        cout << "无参构造函数" << endl;
    }

    MyClassA(int a, int b)
    {
        my_a = a;
        my_b = b;
        cout << "有参构造函数调用" << endl;
    }

    MyClassA(const MyClassA& A)
    {
        my_a = A.my_a;
        my_b = A.my_b;
        cout << "拷贝构造函数调用" << endl;
    }

    ~MyClassA()
    {
        my_a = 0;
        my_b = 0;
        cout << "析构函数调用" << endl;
    }

public:
    void PrintData()
    {
        cout << "my_a = " << my_a << " my_b = " << my_b << endl;
    }

private:
    int my_a;
    int my_b;
};

//第一种调用场景:用一个对象初始化另一个对象
void ClassTest1()
{
    MyClassA A1(1, 2); //调用有参构造函数

    MyClassA A2(A1); //括号法初始化调用拷贝构造函数
    A2.PrintData();

    MyClassA A3 = A1; //等号法初始化调用拷贝构造函数
    A3.PrintData();

    MyClassA A4; //调用了无参构造函数
    A4 = A1; //这里是等号赋值!!!和等号法初始化是两个概念
             //不调用构造函数,而是执行赋值操作,把A1的数据赋值给A4(默认=浅拷贝,将在后面介绍)
    A4.PrintData();


}

void ParaFuncTest1(MyClassA A)
{
    A.PrintData();
}

void ParaFuncTest2(MyClassA& A)
{
	A.PrintData();
}

void ParaFuncTest3(MyClassA* A)
{
	A->PrintData();
}

//第二种调用场景:类定义对象做函数参数
void ClassTest2()
{
    MyClassA A1(1, 2);

    ParaFuncTest1(A1); //实参A1初始化形参A对象元素的时候,会调用拷贝构造函数
    ParaFuncTest2(A1); //不会调用拷贝构造函数,因为引用是变量别名,引用传递并没有出现新对象,
    				   //只是给现有对象起个别名进行传递
    ParaFuncTest3(&A1); //不会调用拷贝构造函数,因为传递的是对象A1的地址,并没有新的对象元素出现
}

MyClassA RetuFuncTest1()
{
    MyClassA A(1, 2);
    return A; //创建一个匿名对象,并把匿名对象返回出去,此时调用拷贝构造函数
} //而A的生命周期到此结束,被析构

//MyClassA& RetuFuncTest2()
//MyClassA* RetuFuncTest3()
//{
//	MyClassA A(1, 2);
//	return &A; //A是局部变量,不能返回它的地址
//}

//第三种调用场景:函数返回类型为类定义的元素
void ClassTest3()
{
    RetuFuncTest1(); //如果不用变量来接这个函数,那么会在 RetuFuncTest1() 函数的 return A;
                     //语句处调用拷贝构造函数,并立即执行析构函数
                     //这是因为,函数返回一个对象元素,而局部变量A的生命周期只在函数体内,
                     //不能返回出来,所以会在return时创建一个匿名对象,主调函数种若没有
                     //对象元素来接,那么会立即调用析构函数把匿名对象析构
    //RetuFuncTest2();
    //RetuFuncTest3();

    MyClassA A1 = RetuFuncTest1(); //这里不会再次调用拷贝构造函数,
    							   //因为编译器会把函数RetuFuncTest1()
                                   //返回出来的匿名对象直接转化为A1,因此匿名对象不会被析构,
                                   //在RetuFuncTest1()函数结束时只调用一次析构函数来析构局部变量A
                                   //匿名对象已经分配好了资源,并直接转化为A1,
    							   //所以A1初始化不需要再次调用拷贝构造函数
    A1.PrintData();

    MyClassA A2; //调用无参构造函数
    A2 = RetuFuncTest1(); //这是等号赋值操作!!!此时匿名对象也不会立即析构,而是在执行完这句话,
                          //对A2赋值完之后,执行析构函数,析构匿名对象
    					  //(区别于匿名对象初始化A1,匿名对象转为A1,不会析构)
    A2.PrintData();
} //生命周期结束,析构所有局部变量 A1(匿名对象) A2

int main()
{
    ClassTest1(); //加断点单步调试来观察构造函数与析构函数的调用机制
    ClassTest2();
    ClassTest3();

    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

(2)拷贝构造函数中的深拷贝与浅拷贝

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
using namespace std;

class MyClassB
{
public:
    MyClassB(const char* p)
	{
		my_len = strlen(p);
		my_str = (char*)malloc(my_len + 1);
		strcpy(my_str, p);
		cout << "无参构造函数" << endl;
	}

	~MyClassB()
	{
		if (my_str != NULL)
		{
			free(my_str);
			my_str = NULL; //避免野指针
		}
		my_len = 0;
		cout << "析构函数调用" << endl;
	}

public:
	void PrintData()
	{
		cout << "my_len = " << my_len << endl;
		cout << "my_str = " << my_str << endl;
	}

private:
	int     my_len;
	char*   my_str;
};

void ClassTestB()
{
	MyClassB B1("hello C++ word!");
	B1.PrintData();

	MyClassB B2 = B1; //调用默认拷贝构造函数
	B2.PrintData();
} //程序会在此挂掉,原因分析 --- 默认拷贝构造函数的浅拷贝问题
  //类B中没有定义拷贝构造函数,会使用编译器提供的默认拷贝构造函数
  //默认拷贝构造函数只是简单的数值赋值,当类中含有指针时
  //只是单纯的把B1.my_str指针的值赋给了B2.my_str,而没有给B2.my_str分配内存,
  //这时两个指针指向同一个内存块,当函数执行完,在调用B2析构函数时,把B2.my_str
  //所指向的空间析构掉了,B1.my_str也指向这块内存,而内存已经被释放了,
  //这时,B1.my_str变成了野指针,当调用B1的析构函数析构B1.my_str指向的内存时,程序挂掉

class MyClassA
{
public:
    MyClassA()
    {
        ; //MyClassA A3; 需要无参构造函数
    }

    MyClassA(const char* p)
    {
        my_len = strlen(p);
        my_str = (char*)malloc(my_len + 1);
        strcpy(my_str, p);
        cout << "无参构造函数" << endl;
    }

    //自定义拷贝构造函数,实现深拷贝
    MyClassA(const MyClassA& A)
    {
        my_len = A.my_len;
        my_str = (char*)malloc(my_len + 1);
        strcpy(my_str, A.my_str);
    }

    ~MyClassA()
    {
        if (my_str != NULL)
        {
            free(my_str);
            my_str = NULL; //避免野指针
        }
        my_len = 0;
        cout << "析构函数调用" << endl;
    }

public:
    void PrintData()
    {
        cout << "my_len = " << my_len << endl;
        cout << "my_str = " << my_str << endl;
    }

private:
    int     my_len;
    char*   my_str;
};

void ClassTestA()
{
    MyClassA A1("hello C++ word!");
    A1.PrintData();

    MyClassA A2 = A1;
    A2.PrintData();

   // MyClassA A3; 
   // A3 = A1; //默认等号也是浅拷贝
   // A3.PrintData();
}

int main()
{
    //ClassTestB();
    ClassTestA(); //加断点单步调试来观察构造函数与析构函数的调用机制

    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

4.总结

只有当类中没有定义任何构造函数时,才会生成默认无参构造函数,但凡类中有定义的任何一个构造函数,都不会生成默认无参构造函数。
只要类中没有定义拷贝构造函数,就会默认生成一个拷贝构造函数,执行浅拷贝,只要定义了一个拷贝构造函数,就不会在生成默认拷贝构造函数。

三、构造函数的初始化列表

应优先使用初始化列表来对数据成员进行初始化,因为就算不显示使用初始化列表,程序也会在执行构造函数的函数体之前将所有数据成员通过默认方式初始化,使用初始化列表可以避免二次赋值。

#include <iostream>
using namespace std;

class MyClassA
{
public:
	MyClassA(int a) //前提是类A中必须含有相应的有参构造函数
	{
		this->a = a;
		cout << "构造函数调用" << endl;
	}

	~MyClassA()
	{
		cout << "析构函数调用" << endl;
	}

private:
	int a;
};

//必须使用构造函数参数初始化列表的两种情况
class MyClassB
{
public:
	MyClassB(int b, int m, int n, int k) : a1(m), a2(n), c(k)
	{
		this->b = b;
		cout << "构造函数调用" << endl;
	}

	~MyClassB()
	{
		cout << "析构函数调用" << endl;
	}

private:
	int b;
    //1.一个类的成员中含有另一个类对象,需使用初始化列表来给成员类初始化
	MyClassA a1, a2;
    //2.const类型的变量必须使用初始化参数列表来初始化
	const int c;
};

void ClassTest()
{
	MyClassB B1(1, 2, 3, 4);
}

class MyClassA2
{
public:
	MyClassA2()
	{
		cout << "构造函数调用" << endl;
	}

	~MyClassA2()
	{
		cout << "析构函数调用" << endl;
	}

private:
	int a;
};

class MyClassA3
{
public:
	MyClassA3(int a1, int a2)
	{
        this->a1 = a1;
        this->a2 = a2;
		cout << "构造函数调用" << endl;
	}

	~MyClassA3()
	{
		cout << "析构函数调用" << endl;
	}

private:
	int a1;
    int a2;
};

class MyClassB2
{
public:
    //错误(活动)	E0289	没有与参数列表匹配的构造函数
    //因为MyClassA2 MyClassA3中均没有一个参数的构造函数,所以报错,没有匹配的构造函数
	MyClassB2(int b, int m, int n) : a2(m), a3(n) 
	{
        this->b = b;
		cout << "构造函数调用" << endl;
	}
    
    //正确做法,根据MyClassA2 MyClassA3定义的构造函数来设置MyClassB2的初始化列表
    /*
    MyClassB2(int b, int m, int n) : a2(), a3(m, n)
	{
		this->b = b;
		cout << "构造函数调用" << endl;
	}
    */

	~MyClassB2()
	{
		cout << "析构函数调用" << endl;
	}

private:
	int b;
    MyClassA2 a2;
    MyClassA3 a3;
};

void ClassTest2()
{
    MyClassB2(1, 2, 3);
}

int main()
{
	ClassTest(); //加断点单步调试来观察构造函数与析构函数的调用机制
    ClassTest2();

	std::cout << "Hello World!\n";
	system("pause");
	return 0;
}

总结

学习C++构造函数与析构函数的调用时机、调用顺序、参数匹配,对象生命周期等,最好的方法就是在代码中进行调试实验,通过断点单步调试,一步步观察程序的执行。

系列文章

二、new/delete详解

posted @ 2022-02-19 15:48  Mindtechnist  阅读(43)  评论(0编辑  收藏  举报  来源