学习c++一段时间了,入门教材是〈〈c++基础教程(第2版)〉〉(清华大学出版社;(美)Herbert Schildt 著,王军 译),花两周粗粗地看了一遍,感觉有点印象了。后来又看〈〈c,c++程序员实用大全〉〉,看〈〈标准c++宝典〉〉,感觉越来越糊涂,特别对const的使用和类的构造函数,析构函数,拷贝构造函数和赋值函数的认识有些摸棱两可,于是决定写篇文章,做一个阶段性的总结,同时希望拙文能够给各位初学者一些帮助。
1.1 构造函数与析构函数的起源
作为比C更先进的语言,C++提供了更好的机制来增强程序的安全性。C++编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。
根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。Stroustrup在设计C++语言时充分考虑了这个问题并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。
构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行。Stroustrup的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就加前缀‘~’以示区别。
除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。构造函数与析构函数的使命非常明确,就象出生与死亡,光溜溜地来光溜溜地去。如果它们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(引自〈〈高质量c++编程指南〉〉)
1.2 构造函数概述
创建对象实例时,程序通常初始化对象的数据成员,为了简化初始化对象的过程,c++使用了一个特殊的函数——构造函数,程序每次创建对象实例时,自动执行构造函数。在正式讲解之前,我们先看看c++对构造函数的一个基本定义。
1.C++规定,每个类必须有构造函数,没有构造函数就不能创建对象。
2.若没有提供任何构造函数,那么c++提供自动提供一个默认的构造函数,该默认构造函数是一个没有参数的构造函数,它仅仅负责创建对象而不做任何有效的赋值操作。
3.只要类中提供了任意一个构造函数,那么c++就不再自动提供默认构造函数。
4.类对象的定义和变量的定义类似,使用默认构造函数创建对象的时候,如果创建的是静态或者是全局对象,则对象的位模式全部为0,否则将会是随机的。 (引自〈〈C++面向对象编程入门〉〉)
1.3 构造函数的种类
(1)默认构造函数
.若程序员没有提供任何构造函数,那么c++提供自动提供一个默认的构造函数,该默认构造函数是一个没有参数的构造函数(此时创建带参数的类对象是错误的),它仅仅负责创建对象而不做任何有效的赋值操作(将为类成员数据赋予随机值)。
例如:
#include <iostream>
using namespace std;
class Date{
int da, mo, yr; //私有成员数据
public:
int num; //公有成员数据
void display()
{
cout << "num =" << num;
cout << "\n" << mo << "-" << da << "-" << yr << endl;
}
};
int main()
{
Date a;
a.display();
getchar();
return 0;
}
在这个程序中,我们没有创建构造函数,所以c++提供自动提供一个默认的构造函数,程序编译时不会报错,但会输出一些随机数,不同的编译器输出不一定相同,如在我的dev-c++编译器中输出:
num=2359608
68353060-2009116333-2088809675。
有文章说“类对象的定义和变量的定义类似,使用默认构造函数创建对象的时候,如果创建的是静态或者是全局对象,则对象的位模式全部为0,否则将会是随即的。”但我在测试如下程序时编译器却报错了:
#include <iostream>
using namespace std;
class Date{
static int num;
public:
void display()
{
cout << "num =" << num;
}
};
int main()
{
Date a;
a.display();
getchar();
return 0;
}
因此,相信默认的构造函数是没有道理的,应该自己创建构造函数。构造函数可以带任意多个的形式参数,这一点和普通函数的特性是一样的。
(2)程序员创建构造函数
我们先来看不带参数的构造函数,请看例子:
#include <iostream>
using namespace std;
class Date{
int da, mo, yr;
public:
Date(); //无参数构造函数
void display()
{
cout << "\n" << mo << "-" << da << "-" << yr;
}
};
Date::Date() //无参数构造函数
{
cout << "No parameter: ";
da = mo = yr = 0;
}
int main()
{
Date a;
// Date b(3, 4, 5); //这里是错误的,因为没有带参数的构造函数
a.display();
getchar();
return 0;
}
在类中的定义的和类名相同,并且没有任何返回类型的Date()就是构造函数,这是一个无参数的构造函数,它在对象创建的时候自动调用,如果去掉Date()函数体内的代码那么它和c++的默认提供的构造函数等价的。
多数情况下我们使用的是带参数的构造函数,请看例子:
#include <iostream>
using namespace std;
class Date{
int da, mo, yr;
public:
Date(int d, int m, int y) //有参数的构造函数
{
cout << "Have parameter: ";
da = d;
mo = m;
yr = y;
}
void display()
{
cout << "\n" << mo << "-" << da << "-" << yr;
}
};
int main()
{
Date a(3, 4, 5);
// Date b; //这里是错误的,因为没有无参数的构造函数
a.display();
getchar();
return 0;
}
在创建对象的时候一定要注意类的构造函数的参数,当类中只有一个不带参数的构造函数,而没有带参数构造函数的时候,系统将无法创建带参数的对象。同理,当类中只有带参数的构造函数,而没有不带参数构造函数的时候,系统将无法创建不带参数的对象。
(3)重载构造函数
如果需要在上述两种情况下都能够执行,就需要重载构造函数,如下例所示:
#include <iostream>
using namespace std;
class Date{
int da, mo, yr;
public:
Date() //无参数构造函数
{
cout << "No parameter: ";
da = mo = yr = 0;
}
Date(int d, int m) //有2个参数的构造函数
{
cout << "Have two parameters: ";
da = d;
mo = m;
yr = 2006;
}
Date(int d, int m, int y) //有3个参数的构造函数
{
cout << "Have three parameters: ";
da = d;
mo = m;
yr = y;
}
void display()
{
cout << "\n" << mo << "-" << da << "-" << yr;
}
};
int main()
{
Date a;
Date b(1, 2);
Date c(3, 4, 5);
a.display();
b.display();
c.display();
getchar();
return 0;
}
程序将正确运行并输出:
No parameter: Have two parameters:Have three parameters:
0-0-0
2-1-2006
4-3-5
创建一个同名的Date() 无参数函数,以重载方式区分调用,由于构造函数和普通函数一样具有重载特性,所以编写程序的人可以给一个类添加任意多个构造函数,来使用不同数目的参数初始化对象。
(4)默认参数的构造函数
除此之外,还可以利用默认参数来简化构造函数,即用带多个参数的构造函数来代替不带参数(或参数较少)构造函数的功能,如下例所示:
#include <iostream>
using namespace std;
class Date{
int da, mo, yr;
public:
Date(int d = 0, int m = 0, int y = 2006) //有3个参数的构造函数
{
// cout << "Have three parameters: ";
da = d;
mo = m;
yr = y;
}
void display()
{
cout << "\n" << mo << "-" << da << "-" << yr;
}
};
int main()
{
Date a;
Date b(1, 2);
Date c(3, 4, 5);
a.display();
b.display();
c.display();
getchar();
return 0;
}
程序将正确运行并输出:
0-0-2006
2-1-2006
4-3-5
(5)嵌套类的构造函数
现在我们来说一下,一个类对象是另外一个类的数据成员的情况,如果有点觉得饶口,那么可以简单理解成类成员的定义可以相互嵌套定义,一个类的成员可以用另一个类进行定义声明。
c++规定如果一个类对象是另外一类的数据成员,那么在创建对象的时候系统将自动调用那个类的构造函数。
下面我们看一个例子:
#include <iostream>
using namespace std;
class Teacher
{
char *director;
public:
Teacher()
{
cout << "class Teacher:";
director = new char[10];
strcpy(director, "王大力");
}
char* GetMember()
{
return director;
}
void Show()
{
cout << "director = " << director << endl;
}
};
class Student
{
int number;
int score;
Teacher teacher;//这个类的成员teacher是用Teacher类进行创建并初始化的
public:
Student()
{
cout << "class Student:";
number = 1;
score = 100;
}
void Show()
{
cout << "\nteacher = " << teacher.GetMember() << endl;
cout << "number = " << number << endl;
cout << "score = " << score << endl;
}
};
int main()
{
Student a;
Teacher b;
a.Show();
b.Show();
getchar();
return 0;
}
程序将正确运行并输出:
class Teacher:: class Student: class Teacher:
teacher = 王大力
number = 1
score = 100
director = 王大力
上面代码中的Student类成员中teacher成员是的定义是用类Teacher进行定义创建的,那么系统碰到创建代码的时候就会自动调用Teacher类中的Teacher()构造函数对对象进行初始化工作。请注意程序中是先创建类Teacher的对象teacher,再创建类Student的其他成员数据的。
这个例子说明类的分工很明确,只有碰到自己的对象的创建的时候才自己调用自己的构造函数。
2.1 析构函数概述
一个类中的构造函数被引用后要负责为新对象向操作系统申请内存空间,当引用该类对象的分程序结束时,应当能够释放该对象对内存的占用从而尽量减少内存碎片的出现;有时候一个类可能需要在构造函数内动态分配资源,那么这些动态开辟的资源就需要在对象不复存在之前被销毁掉。c++类的析构函数就是实现这一功能的特殊函数,可以说析构函数是构造函数的清洁工。析构函数的特性是在程序结束的时候逐一调用,那么正好与构造函数的情况是相反,属于互逆特性,所以定义析构函数因使用~符号(逻辑非运算符),表示它为逆构造函数,加上类名称来定义。
与构造函数一样,析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能置于private区中,不能随意调用,也没有重载,只有在类对象的生命期结束的时候,由系统自动调用。
析构函数与构造函数最主要的不同在于调用期不同。此外构造函数可以有参数,可以重载,但析构函数不行。
我们前面例子中的Teacher类中就使用new操作符进行了动态堆内存的开辟,由于上面的代码缺少析构函数,所以在程序结束后,动态开辟的内存空间并没有随着程序的结束而消失,如果没有析构函数在程序结束的时候逐一清除被占用的动态堆空间那么就会造成内存泄露,使系统内存不断减少,系统效率将大大降低!
因此我们改进上面的代码:
#include <iostream>
using namespace std;
class Teacher
{
char *director;
public:
Teacher()
{
cout << "class Teacher:";
director = new char[10];
strcpy(director, "王大力");
}
~Teacher()
{
cout << "释放堆区director内存空间\n";
delete[] director;
cin.get();
}
char* GetMember()
{
return director;
}
void Show()
{
cout << "director = " << director << endl;
}
};
class Student
{
int number;
int score;
Teacher teacher;//这个类的成员teacher是用Teacher类进行创建并初始化的
public:
Student()
{
cout << "class Student:";
number = 1;
score = 100;
}
~Student()
{
cout << "释放class Student 内存空间\n";
cin.get();
}
void Show()
{
cout << "\nteacher = " << teacher.GetMember() << endl;
cout << "number = " << number << endl;
cout << "score = " << score << endl;
}
};
int main()
{
Student a;
Teacher b;
a.Show();
b.Show();
getchar();
return 0;
}
程序将正确运行并输出:
class Teacher:: class Student: class Teacher:
teacher = 王大力
number = 1
score = 100
director = 王大力
释放堆区director内存空间
释放class Student 内存空间
释放堆区director内存空间
(请注意调用的顺序)
上面的代码中我们为Teacher类添加了一个名为~Teacher()的析构函数用于清空堆内存。
建议大家编译运行代码观察调用情况,程序将在结束前也就是对象生命周期结束的时候自动调用~Teacher()。
~Teache()中的delete[] director;就是清除堆内存的代码,delete操作符只能清空堆空间而不能清楚桟空间,如果强行清除栈空间内存的话将导致程序崩溃!(引自〈〈C++面向对象编程入门〉〉)
实际上,析构函数除了可以(用delete)释放在构造函数中指明占用的内存(堆内存)外,还要释放整个对象的所有成员所占用的内存,只是这些不必写进去罢了(如Student类中的构造函数便可以不写),因为C++编译器将自动为类产生一个默认的析构函数~A(void),用来释放该对象占用的内存。
2.2 构造和析构的次序
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
评论