(三)C++学习 | 类和对象(2)


1. 构造函数

1.1 构造函数的基本定义与使用

类的构造函数是成员函数的一种,其主要特点有:

  • 名字与类名相同,可以有参数,但不能有返回值( v o i d {\rm void} 也不行);
  • 作用是对对象进行初始化,如给成员变量赋值;
  • 如果定义类时没写构造函数,则编译器自动生成一个默认的无参构造函数且不做任何操作
    如果定义了构造函数,则编译器不会生成无参构造函数;
  • 对象生成时构造函数自动被调用(对象以任何形式生成),而对象一旦生成就再也不能在其上执行构造函数;
  • 一个类可以有多个参数列表不同的构造函数,即构造函数的重载

为什么需要构造函数? 考虑两点:构造函数执行必要的初始化工作,有了构造函数,就不必专门去书写初始化函数,也不用担心忘记调用初始化函数;对象没被初始化就使用会导致程序出错。下面以例子说明构造函数的使用:

class Complex {
private:
	double real, imag;
};	// 没有自定义构造函数,编译器自动生成无参构造函数

Complex c1;	// 因为对象生成时就会调用构造函数,这里调用默认的构造函数,下同
Complex* c2 = new Complex;	// 默认构造函数将被调用
class Complex {
private:
	double real, imag;
public:
	Complex(double r, double i = 0);
};	// 有自定义构造函数,编译器不会生成构造函数
Complex::Complex(double r, double i) {
	real = r;
	imag = i;
} // 构造函数给成员变量赋值

Complex c1;	// 出错,程序中不存在无参构造函数,下同
Complex* c2 = new Complex;	// 类"Complex"不存在默认构造函数
Complex c3(2);	// 成功
Complex c4(2, 4), c5(3, 5);	// 成功
Complex* c6 = new Complex(3, 4); // 成功

1.2 构造函数的重载

构造函数也是函数,可以进行函数重载,以参数个数或类型区别不同构造函数。以下面例子说明:

class Complex {
private:
	double real, imag;
public:
	Complex(double r, double i);
	Complex(double r);
	Complex(Complex c1, Complex c2);
};
Complex::Complex(double r, double i) {	# 1
	real = r;
	imag = i;
}
Complex::Complex(double r) {	# 2
	real = r;
	imag = 0;
}
Complex::Complex(Complex c1, Complex c2) {	 # 3
	real = c1.real + c2.real;
	imag = c1.imag + c2.imag;
}
Complex c1(3);		// 调用# 1  c1={3,0};
Complex c2(1, 2);	// 调用# 2  c2={1,2};
Complex c3(c1, c2);	// 调用# 3  c3={4,2};

1.3 构造函数在对象数组中的使用

在使用对象数组时,构造函数的调用形式同一般变量。以下面例子说明:

class C {
public:
	int a, b;
public:
	C() {		// 无参构造函数
		cout << "Constructor 1 Called!" << endl;
	}
	C(int m) {	// 有参构造函数 # 1
		a = m;
		cout << "Constructor 2 Called!" << endl;
	}
	C(int m, int n) {	// 有参构造函数 # 2
		a = m;
		b = n;
		cout << "Constructor 3 Called" << endl;
	}
};
C a[2];					// 调用两次无参构造函数
C b[2] = { 1, 2 };		// 调用两次有参构造函数# 1
C c[2] = { 3 };			// 先调用有参数构造函数# 1,再调用无参构造函数
C* d = new C[2];		// 调用两次无参构造函数
C e[3] = { 4, C(5,6) };					// 先调用有参数构造函数# 1,再调用有参数构造函数# 2,
										// 最后调用无参构造函数
C f[3] = { C(7,8), C(9,10), 11 };		// 先调用有参数构造函数# 2,再调用有参数构造函数# 2,
										// 最后调用有参数构造函数# 1
C* g[3] = { new C(12), new C(13,14) }; 	// 先调用有参数构造函数# 1,再调用有参构造函数# 2

注意最后一条语句,语句左端声明的是 C {\rm C} *类型的指针数组,如果没有右端的初始化,不会生成对象。右端是对指针数组的初始化, g {\rm g} 中前两个元素使用 n e w {\rm new} 的返回值即指针初始化,则分别调用有参构造函数 # 1 {\rm \# 1} # 2 {\rm \# 2} ;最后一个元素没有指定, g [ 2 ] {\rm g[2]} 没有初始化,不会调用构造函数。


2. 复制构造函数

2.1 复制构造函数的基本定义与使用

首先来看复制构造函数的特点

  • 只有一个参数,即对同类对象的引用,其形式为C::C(C& c)C::C(const C& c)
  • 如果没有定义复制构造函数,编译器会自动生成默认的构造函数,完成复制功能(这里与前面的构造函数相区别,无参构造函数即默认构造函数不一定存在(当自己声明了其他的有参构造函数时);而默认构造函数一定存在);
  • 一个程序中有且只存在一个复制构造函数,要么是编译器自动生成的;要么是自己写的。
class Complex {
public:
	double real, imag;
};
Complex c1;			// 调用编译器生成的默认构造函数初始化
Complex c2(c1);		// 调用编译器生成的默认复制构造函数初始化,将c2初始化为c1
class Complex {
public:
	double real, imag;
	Complex() {		// 自己写的无参构造函数,即默认构造函数

	}
	Complex(const Complex& c) {		// 自己写的复制构造函数
		real = c.real;
		imag = c.imag;
	}
};
Complex c1;			// 调用无参构造函数初始化
Complex c2(c1);		// 调用自己定义的复制构造函数初始化,将c2初始化为c1

复制构造函数起作用的三种情况

  • 用一个对象去初始化另一个对象时,有以下两种形式:Complex c2(c1);Complex c2=c1;
  • 如果函数的某个参数是类的对象,在该函数被调用时,如函数形式为void fun(Complex c){}
  • 如果函数的返回值是类的对象,在该函数返回时,如函数形式为void fun(){return c}

注意:对象间的复制并不导致复制构造函数的调用。以下面例子说明:

class C {
public:
	int n;
	C() {

	}
	C(C& c) {			// 自己定义的复制构造函数,
		n = 2 * c.n;	// 作用是将被初始化对象初始化为当前对象的2倍	
	}
};
C c1, c2;
c1.n = 1;	// 给c1对象赋值
c2 = c1;	// 不是对象间的初始化而只是赋值,不会调用复制构造函数
C c3(c1);	// 用一个对象初始化另一个对象,调用复制构造函数

最后c2.n的值是 1 1 c3.n的值是 2 2

2.2 深复制和浅复制

前面提到了赋值构造函数的作用是赋值对象,而默认构造函数也完成同样的功能,为什么需要自己写复制构造函数? 首先来看一个关于深拷贝浅拷贝的例子程序:

class MyString {
public:
	char* pName;
	MyString(char* pN="noString") {		// 构造函数
		cout << "name=" << pN << "constructed" << endl;
		pName = new char[strlen(pN) + 1];
		strcpy(pName, pN);
	}
	~MyString() {						// 析构函数
		cout << "name=" << pName << "destructed" << endl;
		delete[] pName;
	}
};
MyString s1("Shanghai");	// 用"Shanghai"初始化s1对象,调用构造函数
MyString s2(s1);			// 用s1初始化s2,调用默认复制构造函数

上述程序的输出:
在这里插入图片描述

图1:浅复制

这个和我们预想的结果不一样,上述程序在初始化 s 2 {\rm s2} 没有调用构造函数且第二个析构函数也没有正常执行。这里产生该结果的原因是对象调用了 C {\rm C++} 的默认复制构造函数,而默认复制构造函数仅复制了对象本体。如下图:

在这里插入图片描述

图2:对象初始化

根据下一部分内容,后构造的先析构,则先析构 s 2 {\rm s2} ,将存在"Shanghai"内存空间释放了。 轮到 s 1 {\rm s1} 调用析构函数时,此时"Shanghai"内存空间已经不存在,此时执行delete将发生不可预测的错误。为了实现对对象的整体复制,需要定义自己的复制构造函数,即深复制

MyString(const MyString& s) {		// 深复制
	cout << s.pName << "copy constructed" << endl;
	pName = new char[strlen(s.pName) + 1];
	strcpy(pName, s.pName);
}

对深拷贝和浅拷贝的总结 ( 1 ) (1) 对象传递绝不能简单地传递对象本体,尤其是在对象具有指针指向对象时,必须定义自己的复制构造函数; ( 2 ) (2) s 1 {\rm s1} 初始化 s 2 {\rm s2} 不调用构造函数(与上面 ( 1.1 ) (1.1) ( 1.3 ) (1.3) 对比)。

2.3 类型转换构造函数

这一部分来看另一种构造函数,类型转换构造函数。首先介绍类型转换构造函数的特点

  • 定义类型转换构造函数的目的是实现类型的转换
  • 只有一个参数,而且不是复制构造函数
  • 当需要的时候,编译器会自动调用类型转换构造函数。如下面的例子:
class Complex {
public:
	double real, imag;
	Complex(double r, double i) {	// 自己定义的有参构造函数
		real = r;
		imag = i;
	}
	Complex(int x) {				// 不是复制构造函数,是类型转换构造函数
		real = x;
		imag = 0;
	}
};
Complex c1(1, 2);
Complex c2 = 3;	// 调用类型转换构造函数,c2.real=3、c2.imag=0
c1 = 4;			// 调用类型转换构造函数,将4转化成类的临时对象并赋值给c1,
				// c1.real=4、c1.imag=0

3. 析构函数

3.1 析构函数的基本定义与使用

构造函数在对象初始化时调用相对应,析构函数在对象消亡时调用。首先来看析构函数的特点

  • 名字与类名相同,同时在类名前加上~,没有返回值,一个类有且只能有一个析构函数;
  • 析构函数在对象消亡时自动被调用,因此我们可以在对象消亡时做一些善后工作,如释放内存;
  • 如果没有定义析构函数,编译器自动生成默认析构函数,默认析构函数什么也不做。如下面的例子:
class String {
public:
	char* p;
	String() {			// 在对象初始化时动态分配空间
		p = new char[2];
	}
	~String() {			// 在对象消亡时自动释放空间
		delete[]p;
	}
};

3.2 构造函数和析构函数调用程序示例

class Demo {
public:
	int id;
	Demo(int i) {	// 构造函数用于给成员变量id赋值同时输出当前id
		id = i;
		cout << "id=" << id << "constructed" << endl;
	}
	~Demo() {		// 析构函数,输出当前id
		cout << "id=" << id << "destructed" << endl;
	}
};
Demo d1(1);				// 全局对象d1
void Func() {
	static Demo d2(2);	// 静态局部对象d2,函数调用完成后不会消亡,程序结束才消亡
	Demo d3(3);			// 局部对象d3,函数调用完成后d3消亡
	cout << "func" << endl;
}
int main() {
	Demo d4(4);			// 局部对象d4
	d4 = 6;				// 将6转化为临时对象并赋值给4,该临时对象赋值完成后会消亡,
						// 从而调用析构函数
	cout << "main" << endl;
	{
		Demo d5(5);		// 局部对象d5,大括号结束后d5消亡
	}
	Func();
	cout << "main ends" << endl;
	return 0;
}

程序输出结果:
在这里插入图片描述

图3:构造函数和析构函数调用结果

最后,再总结析构函数需要注意的几点:

  • 在程序中,new出来的对象,如果不手动调用delete的话,该对象不会消亡即不会调用析构函数
  • 先构造的后析构,后构造的先析构。如上述程序中的全局对象d1静态对象d2

4. 总结

最后,借用钱能老师对构造函数、复制构造函数、析构函数的总结当我们展开类机制的学习时,我们可曾想过语言内部只一个类机制就要做这么多的事。况且,这才刚刚开始,才接触了类机制的一小部分,那都是一个个孤立的类,没有对类之间相互牵扯的事情。后面我们将会看到,一个个孤立的类只是基础,是构架类的层次结构的基础,只有形成了的类的层次,才有真正资源重用的意义。而且,因为类的层次结构,导致了更复杂的类机制和更方便的编程。我们将一针见血地直指面向对象编程的实质。


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.
  2. 钱能. C++程序设计教程(第二版)[M]. 北京:清华大学出版社,2005.9.


猜你喜欢

转载自blog.csdn.net/Skies_/article/details/105551787