构造函数与析构函数相关知识点

面向对象的程序设计

Posted by 谢玄xx on April 5, 2022

本期Blog素材来源: 北京大学郭炜老师——C++面向对象程序设计

链接戳这里



基本概念

  • 构造函数——名字为类名,可以有参数,不能有返回值,void也不行。
  • 作用:对对象进行初始化
  • 如果类定义时没有写构造函数,则编译器自动生成一个默认的无参构造函数。
  • 对象生成的那一刻,构造函数就自动被调用;对象一旦生成,就再也不能在其上执行构造函数了。对象所占用的存储空间不是由构造函数分配的。当调用构造函数时,对象的内存空间已经被分配好了(房子已经盖好了),构造函数只是做一些初始化工作(装修)。
  • 一个类可以有多个构造函数。
  • 在继承方面,子类不能继承父类的构造函数,只能调用父类的构造函数。

为什么需要构造函数

  • 构造函数可以执行必要的初始化工作。有了构造函数,我们就不用专门写初始化函数,也不用担心忘了调用初始化函数。
  • 请注意,初始化非常重要!未初始化的对象使用起来很有可能会出错。打个比方,在做LeetCode的时候,我经常因为忘记初始化而WA…郁闷。

子类构造函数

  • 父类为CStudent,子类为Cundergraduate。继承的写法为:class Cundergraduate :public CStudent{};
  • 子类不能继承父类的构造函数,只能调用父类的构造函数。
  • 如果子类也要调用构造函数,程序格式为:子类::子类(参数):父类(父类参数,但不包含数据类型)
class CStudent 
{
public:

	CStudent();
	~CStudent() {
		cout << "调用了析构函数" << endl;
	}
	CStudent(string a, int b);
protected:
	string name;
	int age;
};



CStudent::CStudent()
{
	cout << "缺省构造函数" << endl;
}
CStudent::CStudent(string a, int b) {
	name = a;
	age = b;
	cout << "调用这个构造函数" << endl;
	cout << age << endl;
}


class Cundergraduate :public CStudent
{
public:
	string course;
	Cundergraduate() { course = "大物"; cout << "大物" << endl; }
	Cundergraduate(string a, int b, string c);
};

Cundergraduate::Cundergraduate(string a, int b, string c):CStudent(a, b)//子类调用父类构造函数,同时定义自己的构造函数
{ 
	cout << "子类构造函数" << endl;
}
int main()
{
	//Cundergraduate aa("eee", 20);//不能这么写。子类不能继承父类的构造函数,只能调用。
	Cundergraduate aa("栈", 500, "ss");

	Sleep(1);
	return 0;
}

运行程序,输出结果为:

还有一个直观的例子,代码如下:

class A 
{
public:
	A();
	A(int a);
	~A() { cout << "父类析构函数" << endl; }
};

A::A()
{
	cout << "父类无参构造函数" << endl;
}

A::A(int a) {
	cout << "父类有参构造函数" << endl;
}

class B :public A 
{
public:
	B(){ cout << "子类无参构造函数" << endl; }
	//B() : A() { cout << "子类无参构造函数" << endl; }//也可以这么写
	B(int a) :A(a) { cout << "子类有参构造函数" << endl; }
	~B() { cout << "子类析构函数" << endl; }
};

int main() {
	//A a;
	//子类会先调用父类的构造函数,再调用子类自己的构造函数。
	B b;
	//而析构函数则相反。先调用子类析构函数,再调用父类析构函数。
	//有参数的同理
	//B c(4);
	return 0;
}

运行程序,输出结果为:

拷贝构造函数

调用常见场景

一个类有且仅有一个拷贝构造函数。

拷贝构造函数会在三种情况下起作用:

  1. 用一个对象去初始化同类的另一个对象时;
  2. 如果某函数有一个参数是类A的对象,那么该函数被调用时,类A的拷贝构造函数将被调用。有点绕,试着理解一下,很好理解的!
  3. 如果函数的返回值为类A的对象时,则函数返回时,A的拷贝构造函数被调用。
//第2条的解释
class A
{
public:
    A(){}
    A(A & a)
    {
        cout << "已调用拷贝构造函数" << endl;
    }
};

void func(A a1) {}  //这个a1也是个对象。他是如何被初始化的呢?调用拷贝构造函数
int main()
{
    A a2;
    func(a2);
    return 0;
}
//因此上述程序的运行结果就是:已调用拷贝构造函数
//第3条的解释
class A
{
    public:
    int v;
    A(int n)
    {
        v = n;
    }
    A(const A & a)
    {
        v = a.v;
        cout << "已调用拷贝构造函数" << endl;
    }
};

A Func()
{
    A b(4);
    return b;
}
int main()
{
    cout << Func().v << endl;
    return 0;
}

拷贝构造函数缺省

class Complex
{
private:
    double real, imag;    
};

Complex c1;  //自动调用缺省无参构造函数
Complex c1(c2);  //调用缺省无参拷贝构造函数,将c2初始化地和c1一样

不调用的场景

请注意,对象间的赋值并不导致拷贝构造函数被调用!

class CMyclass
{
public:
    int n;
    CMyclass() {}
    CMyclass( CMyclass & c)
    {
        n = 3 * c.n;
    }    
};

int main()
{
    CMyclass c1, c2;
    c1.n = 5;
    c2 = c1;   //注意,这是赋值语句,拷贝构造函数此时没调用
    CMyclass c3(c1);
    cout << "c2.n = " << c2.n << ",";
    cout << "c3.n = " << c3.n << endl;
    return 0;
    
}

上述程序执行结果为:c2.n = 5. c3.n = 10

拷贝构造函数已定义

class Complex
{
private:
    double real, imag;    
    Complex() {}
    Complex(const Complex &c) 
    {
        real = c.real;//把real初始化成和c一样
        imag = c.imag;
        cout << "已调用拷贝构造函数" << endl;
    }
};

Complex c1;  //
Complex c2(c1);  //调用自己定义的拷贝构造函数,输出"已调用拷贝构造函数"。
Complex c3 = c2; //这条语句和上一条完全等价。

类型转换构造函数

  • 定义转换构造函数的目的当然是为了实现类型的自动转换。
  • 只有一个参数,而且不是拷贝构造函数的构造函数,一般就可以看成转换构造函数;
  • 如需要,编译系统会自动调用转换构造函数,建立一个无名的临时对象/临时变量。

析构函数

  • 名字与类名相同,在前面加”~”即可。没有参数和返回值。
  • 一个类最多只能有一个析构函数。
  • 在对象消亡时自动调用析构函数。可以自己定义析构函数用来在对象消亡前做善后工作(如释放内存)。这个时候,系统就不自己生成析构函数了;
  • 如果定义class的时候没有写析构函数,那么编译器自己生成缺省析构函数。这个缺省析构函数只是个空壳子,它什么也不做。

析构函数的应用场景

  • 对于对象数组而言(即数组中每个元素均为对象时),对象数组生命周期结束时,每个元素的析构函数均会被调用。如果自己没有写析构函数,那么有几个元素,缺省析构函数就调用几次。示例如下:
class Ctest
{
public:
	~Ctest()
	{
		cout << "调用析构函数!" << endl;
	}
};
int main()
{
	Ctest arr[2];
	cout << "结束主函数" << endl;
	return 0;
}

上述程序的输出结果如下: 结束主函数 调用析构函数! 调用析构函数!

  • 析构函数在对象作为函数返回值返回后被调用

Talk is cheap. 先来看代码:

class CMyclass
{
public:
	~CMyclass()
	{
		cout << "调用析构函数" <<endl; 
	}
};

CMyclass obj;    //对象1
/*下面这个函数参数是个对象,返回值也是个对象*/
/*这个sobj是临时对象,会调用拷贝构造函数*/
CMyclass fun(CMyclass sobj) //参数对象消亡也会导致析构函数被调用
{
	return sobj;		//函数调用返回时生成临时对象返回
}
//函数在结束时,局部变量和参数都会消亡。因此返回时,sobj就消亡了

int main()
{
	obj = fun(obj);		//函数调用的返回值(临时对象)消亡后,
	return 0;			//该临时对象析构函数被调用
}

上述程序的输出结果如下:

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

第一次调用析构函数——函数形参对象sobj消亡时调用; 第二次调用析构函数——函数调用的返回值(临时对象)消亡时调用; 第三次调用析构函数——整个程序结束后,全局对象obj消亡时调用。

构造函数和析构函数什么时候被调用

  • 请参考下述函数:
class Demo
{
	int id;
public:
	Demo(int i)
	{
		id = i;
		cout << "id = " << id << "的构造函数" << endl;
	}
	~Demo()
	{
		cout << "id = " << id << "的析构函数" << endl;
	}
};

Demo d1(1);		//这是个全局对象,在main之前就被初始化了。全局对象就会引发构造函数,因此这里是第1次调用构造函数。

void Func()
{
	static Demo d2(2);		//静态的局部对象。这里是第4次调用构造函数.这不是在主函数里,所以还不需要调用析构函数。
	Demo d3(3);				//普通局部对象。这里是第5次调用构造函数.
	cout << "func" << endl;
}
//Func函数结束后,静态局部对象不会消亡,整个程序结束后才消亡。因此只调用d3的析构函数。这里是第3次调用析构函数

int main()
{
//一般来说程序从这里开始。但由于前面有个全局对象d1,所以先执行那个。
	Demo d4(4);		//这是局部对象。这里是第2次调用构造函数。
	d4 = 6;			//按理来说类型不匹配,但系统自动分配了类型转换构造函数。6会自动转化为一个临时对象,赋给d4。这里是第3次调用构造函数。赋值之后,这个临时对象6就消亡了,这时会调用析构函数。这里是第1次调用析构函数
	cout << "main" << endl;
	{
		Demo d5(5);		//还是一个局部对象,它的生成会引发构造函数被调用,遇到后花括号它的生命周期结束,调用析构函数。这里是第2次调用析构函数
	}
	Func();			//接下来去执行上面定义的这个函数体内的语句。
	cout << "主函数结束!" << endl;			//执行完Func()后,输出本语句
	return 0;
}
//main函数结束后,先消亡的是对象d4。由于对象d4已被赋为6,因此id = 6.这里是第4次调用析构函数.
//先初始化的后消亡,所以静态对象d2先消亡,全局对象d1最后消亡。这是第5次和第6次调用析构函数.

输出结果如下: id = 1的构造函数
id = 4的构造函数
id = 6的构造函数
id = 6的析构函数
main
id = 5的构造函数
id = 5的析构函数
id = 2的构造函数
id = 3的构造函数
func
id = 3的析构函数
主函数结束!
id = 6的析构函数
id = 2的析构函数
id = 1的析构函数\

  • 请注意,new出来的对象如果没有delete,可以认为它是不消亡的..请看案例:
int main()
{
	A *p = new A[2];
	A *p2 = new A;		//如果只会没有delete这个p2,那么他是不会调用析构函数的,因为是new出来的。
	A a;				//局部对象在main函数结束时自动调用1次析构函数
	delete []p;		//delete []p调用了2次析构函数
}

//因此上述程序共调用了3次析构函数。