【编程语言】C++类和对象、构造函数和析构函数

类是对某一事物的抽象描述,具体地讲,类是C++中的一种构造的数据类型。它即可包含描述事物的数据,又可包含处理这些数据的函数,类在程序运行时是被用作样板来建立对象的。所以要建立对象,首先必须定义类。

定义类

定义一个类的一般格式为:

class 类名{
    private:
        成员表1;
    public:
        成员表2;
    protected:
        成员表3;
};

其中,成员表可以是数据说明或者是函数说明,这与结构体类型的说明是一样的。

class Person {
	private:
		char Name[12];
		int Age;
		char Sex[4];
	public:
		void RegisterPerson(const char*, int, const char*);
		void GetName(char*);
		int GetAge(void);
		void GetSex(char *);
};

关键字private、public、protected的作用是限定成员的访问权限,这三个关键字在类中的使用先后顺序无关紧要,并且每一个关键字在类体中可使用多次。同样的,类体中成员和成员函数的定义顺序,也无关紧要。

  • 关键字private限定的成员称为私有成员,对私有成员限定在该类的内部使用,即只允许该类中的成员函数存取私有的成员数据;对私有成员函数,只能被该类中的成员函数调用;
  • 关键字public限定的成员称为公有成员,这种成员不仅允许该类中的成员函数存取公有成员数据,还允许该类之外的函数存取公有成员数据;公有成员函数不仅能被该类的成员函数调用,而且还允许该类之外的函数调用;
  • 关键字protected限定的成员称为保护成员,它允许该类的成员函数存取保护成员数据,可调用保护成员函数,也允许该类的派生类的成员函数存取保护成员数据或者调用保护成员函数。但其他函数不能存取该类的保护成员数据,也不能调用保护成员函数。

在类体中,当省略关键字private时,系统默认为所定义的成员数据为私有成员,即在类体中没有明确地指出成员的访问权限时,系统约定这些成员为私有成员。

在类中,仅仅给出了成员函数的函数原型,并没有给出成员函数的函数定义。在使用这些成员函数前,必须先给出这些函数的定义。定义一个类的成员函数的一般格式为:

数据类型 类名::函数名(参数){
    ...
}

其中,“::”称为作用域运算符,它指出该函数是某一个类的成员函数。

void Person::RegisterPerson(const char* name, int age, const char* sex) {
	strcpy(Name, name);
	Age = age;
	strcpy(Sex, sex);
}

void Person::GetName(char* name) {
	strcpy(name, Name);
}

int Person::GetAge(void) {
	return Age;
}

void Person::GetSex(char* sex) {
	strcpy(sex, Sex);
}

这边先总结一下“::”的使用:

  • 全局作用域符,在局部变量和全局变量冲突的时候,使用全局变量;
  • 结构体作用域符,结构体中的静态成员,必须在文件作用域中的某个位置对静态的成员进行定义性说明;
数据类型 结构体类型名::静态成员名;  

  • 类作用域符,类成员函数的具体定义;
  • 命名空间作用域符,比如想调用namespace std中的cout成员,就可以写成std::cout;
  • 在派生类由多个基类派生,发生同名成员冲突时,使用作用域运算符来限定成员属于哪一个基类。

在定义一个类的时候,需要注意的点:

  • 类具有封装性,并且类只是定义了一种结构,所以在类中的任何成员数据均不能使用auto、extern、register限定其存储类型;
  • 成员函数可以直接使用本类中的任一成员,包括数据成员和函数成员;
  • 在定义类时,只是定义了一种构造数据类型,并不为类分配存储空间,所以在定义类中的数据成员时,不能对其初始化。

类和结构体类型

从类的定义格式可以看出,类与结构体类型时类同的,类的成员可以是数据成员或函数成员,结构体中的成员与此类似,并且在结构体中,也可以使用关键字private、public和protected限定其成员的访问权限。

实际上,在C++中,结构体类型只是类的一个特例。结构体类型和类的唯一区别在于:在类中,其成员的缺省的存取权限是私有的;而在结构体类型中,其成员的缺省的存取权限是公有的。

注意一下:在C语言中,结构体中是不能定义函数的,但是可以通过函数指针来实现函数的功能。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。struct能包含成员函数、能限定成员的访问权限、能继承、能实现多态。 

当然,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

内联成员函数

当定义一个类时,其成员函数的函数体的定义也可以在类的类体中直接定义,即在类中直接定义成员函数。

class Person {
	private:
		char Name[12];
		int Age;
		char Sex[4];
	public:
		void RegisterPerson(const char* name, int age, const char* sex) {
			strcpy(Name, name);
			Age = age;
			strcpy(Sex, sex);
		}
		void GetName(char* name) {
			strcpy(name, Name);
		}

		int GetAge(void) {
			return Age;
		}

		void GetSex(char* sex) {
			strcpy(sex, Sex);
		}
};

类中定义成员函数和用“::”类外定义成员函数,使用效果相同。两者的区别在于:

  • 在定义类时,在类体中直接定义成员函数的函数体时,这种成员函数在编译时是作为内联函数来实现的,所以也称为内联成员函数;
  • 用“::”类外定义成员函数则不作为内联函数来实现。

通常当函数的功能比较简单时,定义为内联成员函数;而当函数的实现比较复杂,则不使用内联成员函数。

定义内联成员函数的另一种方法是:在类体中同样只给出成员函数的原型说明,在类体外定义成员函数时,与定义一般的内联函数一样,在成员函数的定义前面加上关键字inline。


对象

对象的说明

对象也称作是类的实例,和结构体变量一样,对象必须先定义后使用。说明对象的一般格式为:

类名 对象名;
Person p1, p2;

系统并不为类分配存储空间,当说明对象时,系统才为对象分配相应的存储空间。为对象分配存储空间的大小,取决于定义类时所定义的成员的类型和成员的多少。

不同的对象占据内存的不同区域,它们所保存的数据各不相同,但对成员数据进行操作的成员函数的程序代码均是一样的。也就是说,p1.Name和p2.Name的值可能是不同的,但是p1.GetName()和p2.GetName()的代码是相同的。

为了减少成员函数所占用的空间,在建立对象时,只为对象分配保存成员数据的内存空间,而成员函数的代码为该类的每一个对象所共享。通常,类中定义的成员函数的代码被存放在计算机内存的一个公共区内,并供该类的所有对象共享。这是C++实现对象的一种方法。只是逻辑上,我们一般仍将每一个对象理解成由独立的成员数据和各自的成员函数代码组成。

说明对象的方法与说明结构体变量的方法一样,也有三种:

  • 先定义类的类型,再说明对象;
  • 在定义类的同时说明对象;
  • 直接说明对象,而不定义类的类名。

在定义对象时,当类中的数据成员的访问权限指定为公有时,定义对象时允许对它的成员数据进行初始化;当类中的数据成员的访问权限指定为私有或者保护时,则定义对象时不允许对它的成员数据进行初始化。

对象的使用

  • 对象通过选择运算符“.”来访问对象的成员。当访问一个成员函数时,也称为向对象发送一个消息。用成员选择运算符“.”只能访问对象的公有成员,而不能访问对象的私有成员或者保护成员。若要访问对象的私有成员或者保护成员,只能通过对象的公有成员函数来获取;
  • 同类型的对象之间可以整体赋值;
  • 对象作为函数的参数时,属于赋值调用;函数可以返回一个对象;
  • 一个类的对象可以作为另一个类的成员。

类的嵌套

在定义一个类时,在其类体中又包含了一个类的定义,称为类的嵌套。比如:

class Outer{
    public:
        class Inner{
            public:
                int x,y;
        };
        Inner c1,c2;
        float a,b;
};

可以将嵌套类看作是一种成员类,系统并不为嵌套类分配内存空间,说明该嵌套类的成员时,也不为其分配内存空间。仅仅是定义外层类的对象时,才会为嵌套类的成员分配内存空间,但此时嵌套类还是没有内存空间。嵌套类的类名作用域从其定义开始,到其外层类的定义结束时结束。


成员函数的重载

类中的成员函数与普通函数一样,成员函数可以带有缺省参数,也可以重载成员函数。

例子(线性表):

class ListClass {
	private:
		int* List;
		unsigned nMax;                //线性表最大长度
		unsigned nElem;                //表中的数据个数
	public:
		void Init(int n = 10) {            //初始化线性表
			List = new int[n];
			nMax = n;
			nElem = 0;
		}
		int Elem(int n) {                //返回第n个元素的值
			if ((n >= 0) && (n < nElem))
				return List[n];
			else
				return 0;
		}
		int &Elem(unsigned n) {            //返回第n个元素的引用
			return List[n];
		}
		unsigned Elem(void) {            //返回线性表长度
			return nElem;
		}
};


this指针

用对象的成员函数来访问对象的成员时,在成员函数的实现中,只要给出成员名就可实现对该对象成员的访问;但在成员函数之外要访问某一个成员时,须指明访问的是哪一个对象的成员。

实际上,当调用一个成员函数时,系统会自动地向它传递一个隐含的参数,该参数是一个指向接受该函数调用的对象的指针,在成员函数的函数体中可以直接使用关键字this来访问这个指针。在成员函数的实现中,当访问该对象的某一个成员时,系统自动地使用了该隐含的this指针。比如:

		int Elem(int n) {                //返回第n个元素的值
			if ((n >= 0) && (n < this->nElem))
				return this->List[n];
			else
				return 0;
		}

this指针很具有如下形式的缺省说明:

类名* const this;
ListClass* const this;

即把该指针说明为const指针,只允许在成员函数体中使用该指针,但不允许改变该指针的值。

一般而言,我们无需关心该指针,它是由系统自动维护的。但在某些特殊的应用场合下,可能会用到该指针。比如,线性表中需要一个拷贝线性表的函数:

		void CopyList(ListClass L) {
			nMax = L.nMax;
			nElem = L.nElem;
			if (List)
				delete []List;        //A
			List = new int[nMax];
			for (int i = 0; i < nElem; i++)
				List[i] = L.Elem(i);
		}

一看没有什么问题,但是一旦出现某线性表自己拷贝自己的情况,执行到A行的时候,就销毁线性表的内存空间,再new运算符为线性表动态分配存储空间。也就是说,线性表中的数据全部丢失了,所以数据的拷贝肯定无法实现。为了防止这种情况:

		void CopyList(ListClass L) {
			if (&L != this) {
				nMax = L.nMax;
				nElem = L.nElem;
				if (List)
					delete[]List;
				List = new int[nMax];
				for (int i = 0; i < nElem; i++)
					List[i] = L.Elem(i);
			}
		}


构造函数

在产生对象时,对对象的数据成员进行初始化的方法有三种:第一种是使用初始化数据列表的方法,第二种是通过构造函数实现初始化,第三种是通过对象的拷贝初始化函数来实现。

第一种只能对类的公有数据成员初始化,而不能对私有的或保护的数据成员进行初始化。通常使用构造函数来实现对对象的数据成员的初始化较为常见。构造函数是类的成员函数,系统约定构造函数名必须与类名相同。

定义构造函数

在定义一个类时,可根据需要定义一个或多个构造函数(重载构造函数)。构造函数与类的成员函数一样,可以在类中定义结构体,也可在类外定义函数体。在类中定义构造函数的一般格式为:

类名(参数列表){
    ...
}

在类外定义构造函数的一般格式为:

类名::类名(参数列表){
    ...
}

对构造函数,须说明以下几点:

  • 构造函数的函数名必须与类名相同。构造函数的主要作用:是完成初始化对象的成员数据以及其他初始化工作;
  • 因为构造函数是由系统自动调用的,构造函数与其他成员函数不一样,在定义构造函数时,不能指定函数返回值的类型,也不能指定为void类型;
  • 构造函数可以不带参数,也可以带若干个参数,也可以指定参数的缺省值。在定义多个构造函数时,必须满足函数重载的原则,即所带的参数个数或参数的类型是不同的;
  • 若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其他类时,则可将构造函数定义为保护的成员函数。

构造函数和对象的初始化

当定义了类的构造函数后,在产生该类的一个对象时,系统根据定义对象时给出的参数自动调用对应的构造函数,完成对象的成员数据的初始化工作。由于构造函数属于类的成员函数,它对私有成员数据、保护的成员数据和公有的成员数据均能进行初始化。比如:

#include <iostream>
using namespace std;

class Rectangle {
	private:
		int Left, Right, Top, Bottom;
	public:
		Rectangle(int L, int R, int T, int B) {
			Left = L;
			Right = R;
			Top = T;
			Bottom = B;
		}
		Rectangle() {
			Left = 0;
			Right = 0;
			Top = 0;
			Bottom = 0;
		}
};

int main()
{
	Rectangle r1(1, 2, 3, 4);        //A:调用带参数的构造函数
	Rectangle r2;            //B:调用不带参数的构造函数
	Rectangle r3();            //C:一个不带参数,返回值为Rectangle类型的函数声明

	system("pause");
	return 0;
}

需要注意一下:

  • 如果需要调用不带参数的构造函数(B行),定义类的对象时,后面不需要给出一对括号;
  • 如果在定义对象的时候,后面加上一对括号,这就不表示定义对象了,更不表示调用不带参数的构造函数。表示的是不带参数,返回值为类对象类型的声明。

只有遵循这一种约定之后,系统才能够区分是对不带参数的函数的原型说明,还是定义对象。

全局对象、静态对象、局部对象的定义:

  • 定义全局对象,构造函数在main()函数执行之前就被调用;
  • 定义静态对象,仅会调用一次构造函数;
  • 定义局部对象,每次声明都会调用一次。

注意:这里说的是定义的时候,只针对这种情况的语句。

构造函数与new运算符

可以使用new运算符来动态地建立对象。用new运算符建立对象时,同样的也要自动调用构造函数,以便完成对象的成员数据初始化。比如:

Rectangle* r1 = new Rectangle(1, 2, 3, 4);

当使用new运算符建立一个动态的对象时,new运算符首先为类Rectangle的对象分配一个内存空间,然后自动地调用构造函数来初始化对象的成员数据,最后返回该动态对象的起始地址。

注意,用new运算符产生的动态对象,在不再使用这种对象时,必须用delete运算符来释放对象所占用的存储空间。即:

delete r1;

也就是说,使用new运算符动态创建的对象,最终返回的是一个指针类型的数据;而不使用new运算符,就直接创建一个对象。而且指针类型用“->”来引用对象的成员,而类的对象用“.”来引用对象的成员。除此之外,new运算符创建出来的对象,要及时delete释放掉。

缺省的构造函数

在定义类时,若没有定义类的构造函数,则编译器自动产生一个缺省的构造函数,其格式为:

类名::类名(){

}

从定义格式上来看,这是一个函数体为空的构造函数,即在产生对象时,尽管也调用缺省的构造函数,但函数什么事也不做。所以缺省的构造函数并不对产生对象的数据成员赋初值,也就是说,尽管产生对象时调用了缺省的构造函数,但新产生的对象的数据成员的值依然是不确定的。

需要注意的是:在定义类时,一旦定义了类的构造函数,编译器就不会产生缺省的构造函数。并且缺省的构造函数只能有一个。缺省的构造函数包括两种情况:没有参数的构造函数或者各参数均有缺省值。

什么意思呢?也就是说,如果在定义了一个类的时候,同时也定义了非缺省的构造函数,那么就一定要手动补上缺省的构造函数,除非不要使用非缺省的构造函数来创建对象。

例如:

#include <iostream>
using namespace std;

class Rectangle {
	private:
		int Left, Right, Top, Bottom;
	public:
		Rectangle(int L, int R, int T, int B) {
			Left = L;
			Right = R;
			Top = T;
			Bottom = B;
		}
};

int main()
{
	Rectangle r1;            //出错:定义了类的构造函数,编译器就不产生缺省的构造函数

	system("pause");
	return 0;
}
#include <iostream>
using namespace std;

class Rectangle {
	private:
		int Left, Right, Top, Bottom;
	public:
		Rectangle(int L=0, int R=0, int T=0, int B=0) {
			Left = L;
			Right = R;
			Top = T;
			Bottom = B;
		}
		Rectangle() {
			Left = 0;
			Right = 0;
			Top = 0;
			Bottom = 0;
		}
};

int main()
{
	Rectangle r1;                    //出错:在编译时出现两个缺省的构造函数,产生二义性

	system("pause");
	return 0;
}


析构函数

产生对象时,系统要为对象分配存储空间;在对象结束其生命周期或结束其作用域(静态存储类型的对象除外)时,系统要回收对象所占用的存储空间,即要撤销一个对象。这个工作是由析构函数来完成的

定义析构函数

析构函数也是类的成员函数,定义析构函数的格式为:

类名::~类名(){
    ...
}

关于析构函数,须说明以下几点:

  • 析构函数名必须与类名相同,并在其前面加上字符“~”,以便和构造函数名相区分;
  • 析构函数不能带有任何参数,不能有返回值。换言之,析构函数是唯一的,析构函数不允许重载; 
  • 析构函数是在撤销对象时由系统自动调用的,它的作用是在撤销对象前做好结束工作。在析构函数内要终止程序的执行时,不能使用库函数exit(),但可以使用函数abort()。因为exit()函数要做程序前的结束工作,它又要调用构造函数,形成无休止的递归;而abort函数不做终止前的结束工作,直接终止程序的执行;
  • 在程序的执行过程中,当遇到某一个对象的生存周期结束时,系统自动调用析构函数,然后再回收为该对象所分配的存储空间。

另外:如果在构造函数中,使用了new运算符为对象的某个指针成员分配了动态申请的内存空间,那么必须定义一个析构函数,来使用delete运算符删除new出来的成员的内存空间。

同时:当使用delete运算符删除由new运算符动态生成的对象时,它首先调用该对象的析构函数,然后再释放该对象所占用的内存空间。这与new运算符创建动态对象的过程正好相反。这是因为:系统不能自动调用析构函数来撤销动态生成的对象。

上面两句话,可能会比较绕,它们之间的核心区别是:一个是类中的某个成员是new出来的,那么就需要在析构函数中将这个成员delete掉;一个是类的对象是new出来的,那么就在delete这个对象时调用析构函数。

比如前者:

class String {
	private:
		char* Buffer;
	public:
		String(char* s) {
			if (s) {
				Buffer = (char*)new char[strlen(s) + 1];
				strcpy_s(Buffer, strlen(s) + 1, s);
			}
			else Buffer = 0;
		}
		~String() {
			if (Buffer) {
				delete[]Buffer;
			}
		}
};

比如后者:

#include <iostream>
using namespace std;

class Rectangle {
	private:
		int Left, Right, Top, Bottom;
	public:
		Rectangle(int L=0, int R=0, int T=0, int B=0) {
			Left = L;
			Right = R;
			Top = T;
			Bottom = B;
		}
		~Rectangle() {
			cout << "调用析构函数!" << endl;
		}
		void point() {
			cout << Left << '\t' << Right << '\t' << Top << '\t' << Bottom << endl;
		}
};

int main()
{
	Rectangle* p = new Rectangle(1, 2, 3, 4);
	p->point();
	delete p;
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

这段程序的运行结构为:

1       2       3       4
调用析构函数!
退出主函数!
请按任意键继续. . .

用delete运算符撤销动态生成的对象数组

用delete运算符撤销单个对象与撤销对象数组时,其用法有所不同。比如:

#include <iostream>
using namespace std;

class Rectangle {
private:
	int Left, Right, Top, Bottom;
public:
	Rectangle(int L = 0, int R = 0, int T = 0, int B = 0) {
		Left = L;
		Right = R;
		Top = T;
		Bottom = B;
		cout << "调用构造函数!" << endl;
	}
	~Rectangle() {
		cout << "调用析构函数!" << endl;
	}
};

int main()
{
	Rectangle* p = new Rectangle[2];
	delete []p;
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

这段程序的运行结构为:

调用构造函数!
调用构造函数!
调用析构函数!
调用析构函数!
退出主函数!
请按任意键继续. . .

首先,使用new运算符来动态地建立对象数组,会依次自动调用构造函数。但是在使用delete运算符来释放指向的对象数组所占用的存储空间时,在指针变量的前面必须加上[]。如果不加,仅仅释放对象数组的第0个元素,即只调用数组的第0个元素的析构函数,其他元素的占用空间不释放。但在VC++环境下,将产生运行错误。

缺省的析构函数

与缺省的构造函数一样,若在类中没有显式地定义析构函数,则编译器自动地产生一个缺省的析构函数,其格式为:

类名::~类名(){

}

缺省的析构函数的函数体为空,即该缺省的析构函数什么也不执行。实际上,任何对象都有构造函数和析构函数。

在产生对象时,若不对数据成员进行初始化,可以不显式地定义构造函数;当撤销对象时,若不做任何结束工作,可以不显式地定义析构函数。但在撤销对象时,如果要释放的对象拥有用new运算符分配内存空间的成员,则必须显式地定义析构函数。


不同存储类型的对象调用构造函数和析构函数

通常在产生对象的时候调用构造函数,在撤销对象的时候调用析构函数。但对于不同存储类型的对象,调用构造函数与析构函数的情况有所不同。

  • 对于全局定义的对象,在程序开始执行时,调用构造函数,到程序结束时,调用析构函数;
  • 对于局部定义的对象,当程序执行到定义对象时,调用构造函数,在退出对象作用域时,调用析构函数;
  • 对于static定义的局部变量,在首次到达对象的定义时,调用构造函数,在程序结束时,调用析构函数;
  • 对于用new运算符动态生成的对象,在产生对象时调用构造函数,只有使用delete运算符来释放对象时,才调用析构函数。也就是说,系统不能自动调用析构函数来撤销动态生成的对象。


实现类型转换与拷贝的构造函数

实现类型转换的构造函数

下面通过举例来说明何时需要显式地实现类型转换的构造函数,何时使用隐式地实现类型转换的构造函数。

#include <iostream>
using namespace std;

class Ex1 {
	private:
		int x;
	public:
		Ex1(int a) {
			x = a;
			cout << x << " 调用构造函数!" << endl;
		}
		~Ex1() {
			cout << x << " 调用析构函数!" << endl;
		}
};

int main()
{
	Ex1 x1(50);                        //A
	Ex1 x2 = 100;                        //B
	x2 = 200;                            //C
	cout << "退出主函数!" << endl; 

	system("pause");
	return 0;
}

这段程序的运行结果是:

50 调用构造函数!
100 调用构造函数!
200 调用构造函数!
200 调用析构函数!
退出主函数!
200 调用析构函数!
50 调用析构函数!
请按任意键继续. . .

首先先补充一点:当构造函数只有一个数值参数时,下面两句是一样的意思:

Ex1 x2 = 100;
Ex1 x2(100);

注意:在这种情况下的等号是传递单个数值到构造函数的另一种方法,这是初始化对象而不是赋值!

上面这段程序只定义了两个对象,却出现了调用三次构造函数,三次析构函数的情况。解释一下:

运行A、B两句的时候,进行对象的初始化,调用构造函数,没有什么问题。到了C句,这是一个赋值语句,不是初始化语句。此时应该这样理解:x2应该接受一个Ex1类型的对象(只有同类型的对象之间才可以赋值)。这是编译器会调用构造函数将200转换成Ex1类型的对象,即产生一个临时的对象,并将该对象赋给x2。为此,第三次调用构造函数。一旦完成这种赋值,立即撤销该临时对象,即调用析构函数。接下来的程序就比较简单了。

那么什么是显式地实现类型转换的构造函数,什么是隐式地实现类型转换的构造函数?比如:

x2 = 200;                    //隐式
x2 = Ex1(200);                //显示

又该何时使用何种:只有当构造函数只有一个参数时,才能隐式地实现类型转换的构造函数;当构造函数有多个参数时,必须显式地实现类型转换的构造函数。

用构造函数进行类型转换的一般格式:

对象名 = 类名(构造函数的参数列表);

作用是:首先产生一个临时的对象,完成赋值后,立即撤销该临时的对象。

要注意和生成对象的区别:

类名 对象名(构造函数的参数列表);

完成拷贝功能的构造函数

完成拷贝功能的构造函数的一般格式为:

类名::类名(类名 &形参){
    ...
}

实现拷贝功能的构造函数的参数是该类类型的引用。显然,用这种构造函数来创建一个对象时,必须用一个已产生的同类型对象作为实参。

例如:

#include <iostream>
using namespace std;

class Test {
	private:
		int x, y;
	public:
		Test(int a, int b) {
			x = a;
			y = b;
			cout << "调用构造函数!" << endl;
		}
		Test(Test &t) {
			x = t.x;
			y = t.y;
			cout << "调用完成拷贝的函数!" << endl;
		}
};

int main()
{
	Test t1(50,100);
	Test t2 = t1;            //A
	Test t3(t1);            //B
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

这段程序的运行结果为:

调用构造函数!
调用完成拷贝的函数!
调用完成拷贝的函数!
退出主函数!
请按任意键继续. . .

执行到A行和B行时,自动将A行转换为:

	Test t2(t1);

因此,A行和B行都调用了完成拷贝的构造函数,初始化新的对象。

其实如果在Test类中不定义一个完成拷贝功能的构造函数,也照样没有问题。这是因为编译器会自动生成一个隐式的完成拷贝功能的构造函数:

		Test(Test &t) {
			x = t.x;
			y = t.y;
		}

也就是说,由编译器为每个类产生的这种含有隐含的完成拷贝功能的构造函数,会依次完成类中对于成员数据的拷贝。但是在产生对象时,如果只要拷贝类中部分成员的数据,或者类中的某些成员是使用new运算符动态申请存储空间进行的,那么就必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确完成拷贝。

前者是比较好理解,后者为什么成员new出来的就不行呢?比如:

#include <iostream>
using namespace std;

class String {
	private:
		char* Buffer;
	public:
		String(char* s) {
			if (s) {
				Buffer = (char*)new char[strlen(s) + 1];
				strcpy_s(Buffer, strlen(s) + 1, s);
			}
			else Buffer = 0;
			cout << "调用了构造函数!" << endl;
		}
		String(String &s) {
			if (s.Buffer) {
				Buffer = (char*)new char[strlen(s.Buffer) + 1];
				strcpy_s(Buffer, strlen(s.Buffer) + 1, s.Buffer);
			}
			else Buffer = 0;
			cout << "调用了拷贝功能的构造函数!" << endl;
		}
		~String() {
			if (Buffer) {
				delete[]Buffer;
			}
		}
};

int main()
{
	String s1("Hello");
	String s2(s1);                    //A
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

这段程序的运行结果是:

调用了构造函数!
调用了拷贝功能的构造函数!
退出主函数!
请按任意键继续. . .

当然,如果将拷贝功能的构造函数删掉,使用默认的。也就是:

String(String &temp){
    Buffer = temp.Buffer;
}

那这样的话,执行A句的时候,将s1的数据成员Buffer赋值给s2,那么s1和s2就只想同一个存放“Hello”字符串的地址。那么,在程序结束,调用析构函数的时候,一个地址被释放了两次,必然会引起运行错误。

同时,如果在main()函数改成:

int main()
{
	String s1("Hello");
	String s2(s1);                    //A
        s2 = s1;                        //B
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

尽管在A行能够成功赋值完成内容的拷贝,但是在B行的时候,还是讲s1和s2指向了用一个地址,析构函数也同样会出现问题。尽管s2=s1语法上没有错误。解决这个问题,需要用到运算符重载,以后会讲到。

也就是说,完成拷贝功能的构造函数是为了在初始化的时候,完成内容的拷贝,而不是地址的拷贝;“=”运算符重载是为了在赋值的时候,完成内容的拷贝,而不是地址的拷贝。

总结

  • 实现类型转换的构造函数(用原本的构造函数--赋值):
x2 = 200;                    //隐式
x2 = Ex1(200);                //显示

  • 实现类型转换的构造函数的一般格式:无。
  • 实现拷贝功能的构造函数(用新的构造函数--初始化):
String s2 = s1;                //隐式
String s2(s1);                //显式

  • 完成拷贝功能的构造函数的一般格式为:
类名::类名(类名 &形参){
    ...
}


构造函数与对象成员

在定义一个新类时,可把一个已定义类的对象作为该类的成员。产生新定义类的对象时,须对它的对象成员进行初始化,且只能通过新类的构造函数来对它的所有成员数据初始化。对对象成员进行初始化,必须通过调用其对象成员的构造函数来完成。

例如:

#include <iostream>
using namespace std;

class A {
	private:
		int x, y;
	public:
		A(int a, int b) {
			x = a;
			y = b;
			cout << "调用A的构造函数!" << endl;
		}
};

class B {
        private:
	        int Length, Width;
	        A a1;                                //A
        public:
	        B(int a, int b, int c, int d):a1(c, d) {            //B
		        Length = a;
		        Width = b;
		        cout << "调用B的构造函数!" << endl;
	        }
};



int main()
{
	B b1(1, 2, 3, 4);
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

这段程序的运行结果是:

调用A的构造函数!
调用B的构造函数!
退出主函数!
请按任意键继续. . .

也就是说,在B类中有A类的对象作为成员,那么在对B类的构造函数时,需要指定哪些参数是为了作为A类构造函数的参数。

在一个类中,说明对象成员的一般步骤为:

class 类名{
    private:
        已定义类名1 成员名1;
        已定义类名2 成员名2;
    public:
        类名(类名构造函数的参数列表):成员名1(已定义类名1的参数列表),成员名2(已定义类名2的参数列表){
            ...
        }
}

注意:在“:”后面为成员初始化列表,用“,”隔开的,“已定义类名1的参数列表”和“已定义类名2的参数列表”中的参数,其实都来自于“类名构造函数的参数列表”。并且在“类名构造函数的参数列表”中必须要有类型说明,而“已定义类名1的参数列表”和“已定义类名2的参数列表”无需提供类型说明。

这里的顺序问题:

  • 如果一个类有几个对象成员,那么对对象成员的构造函数的调用顺序:取决于这些对象成员在类中说明的顺序,与它们在“类名构造函数的参数列表”和成员初始化列表中的顺序无关;
  • 类的构造函数、类对象成员的构造函数的调用顺序:当建立起类的对象时,先调用各个对象成员的构造函数,初始化对应的对象成员,然后再去执行类的构造函数,初始化其他非对象成员的成员。析构函数的调用顺序与构造函数正好相反。

来一条练习题:

#include <iostream>
using namespace std;

class Obj {
	private:
		int val;
	public:
		Obj() {
			val = 0;
			cout << val << "调用Obj缺省的构造函数!" << endl;
		}
		Obj(int i) {
			val = i;
			cout << val << "调用Obj的构造函数!" << endl;
		}
		~Obj() {
			cout << "调用Obj的析构函数!" << endl;
		}
};

class Con {
	private:
		Obj one, two;                        //A
		int data;
	public:
		Con() {
			data = 0;
			cout << data << "调用Con缺省的构造函数!" << endl;
		}
		Con(int i, int j, int k):two(i+j),one(k) {            //B
			data = i;
			cout << data << "调用Con的构造函数!" << endl;
		}
		~Con() {
			cout << "调用Con的析构函数!" << endl;
		}
};

int main()
{
	Con c(100, 200, 400);
	cout << "退出主函数!" << endl;

	system("pause");
	return 0;
}

这段程序的运行结果是:

400调用Obj的构造函数!
300调用Obj的构造函数!
100调用Con的构造函数!
退出主函数!
调用Con的析构函数!
调用Obj的析构函数!
调用Obj的析构函数!
请按任意键继续. . .
注意一下顺序:当建立C的对象时,先调用其对象成员one、two的构造函数,而one和two的顺序取决于类中说明的顺序(A行,不是B行)。也就是说,先创建one对象、再two对象、再c对象。析构函数的顺序正好相反。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/80525894