当前位置:首页 > 嵌入式培训 > 嵌入式学习 > 讲师博文 > 浅析C++的构造函数和析构函数

浅析C++的构造函数和析构函数 时间:2018-09-26      来源:未知

在现实世界中,每个事物都有其生命周期,会在某个时候出现也会在另外一个时候消亡。程序是对现实世界的反映,其中的对象就代表了现实世界的各种事物,自然也就同样有生命周期,也会被创建和销毁。一个对象的创建和销毁,往往是其一生中非常重要的时刻,需要处理很多复杂的事情。例如,在创建对象的时候,需要进行很多初始化工作,设置某些属性的初始值;而在销毁对象的时候,需要进行一些清理工作,重要的是把申请的资源释放掉,把打开的文件关闭掉,为了完成对象的生与死这两件大事,C++中的类专门提供了两个特殊的函数—— 构造函数(Constructor)和析构函数(Destructor),它们的特殊之处就在于,它们会在对象创建和销毁的时候被自动调用,分别用来处理对象的创建和销毁的复杂工作。

构造函数

由于构造函数会在对象创建的时候被自动调用,所以我们可以用它来完成很多不便在对象创建完成后进行的事情,比如可以在构造函数中对对象的某些属性进行初始化,使得对象一旦被创建就有比较合理的初始值。C++规定每个类都必须有构造函数,如果一个类没有显式地声明构造函数,那么编译器也会为它产生一个默认的构造函数,只是这个默认构造函数没有参数,也不做任何额外的事情而已。而如果我们想在构造函数中完成一些特殊的任务,就需要自己为类添加构造函数了。可以通过如下的方式为类添加构造函数:

class Teacher

{

public:

Teacher(参数列表)

{

// 对Teacher类进行构造,完成初始化工作

}

private:

string m_strName ;

};

因为构造函数具有特殊性,所以它的声明也比较特殊。

首先,在大多数情况下构造函数的访问级别应该是公有(public)的,因为构造函数需要被外界调用以创建对象。只有在少数的 特殊用途下,才会使用其他访问级别。

其次是返回值类型,构造函数只是完成对象的创建,并不需要返回数据,自然也就无所谓返回值类型了。

再其次是函数名,构造函数必须跟类同名,也就是用类的名字作为构造函数的名字。

后是参数列表,跟普通函数一样,在构造函数中我们也可以拥有参数列表,利用这些参数传递进来的数据来完成对象的初始化工作,从而可以用不同的参数创建得到有差别的对象。根据参数列表的不同,一个类可以拥有多个构造函数,以适应不同的构造方式。

如果Teacher类就没有显式地声明构造函数,就会使用编译器为它生成的默认构造函数,所以其创建的对象都是千篇一律一模一样的,所有新创建对象的m_strName成员变量都是那个在类声明中给出的固定初始值。换句话说,也就是所有“老师”都是同一个“名字”,这显然是不合理的。下面改写这个Teacher类,为它添加一个带有string类型参数的构造函数,使其可以在创建对象的时候通过构造函数来完成对成员变量的合理初始化,创建有差别的对象:

class Teacher

{

public:

// 构造函数

// 参数表示Teacher类对象的名字

Teacher(string strName) // 带参数的构造函数

{

// 使用参数对成员变量赋值,进行初始化

m_strName = strName;

};

void GiveLesson(); // 备课

private:

string m_strName = "qulu"; // 类声明中的初始值

// 姓名

};

现在就可以在定义对象的时候,将参数写在对象名之后的括号中,这种定义对象的形式会调用带参数的构造函数Teacher(string strName),进而给定这个对象的名字属性。

// 使用参数,创建一个名为“WangGang”的对象

Teacher MrWang("WangGang");

在上面的代码中,我们使用字符串“WangGang”作为构造函数的参数,它就会调用Teacher类中需要string类型 为参数的Teacher(string strName)构造函数来完成对象的创建。在构造函数中,这个参数值被赋值给了类的m_strName成员变量,以代替其在类声明中给出的固定初始值 “qulu”。当对象创建完成后,参数值“WangGang”就会成为MrWang对象的名字属性的值,这样我们就通过参数创建了一个有着特定“名字”的Teacher对象,各位“老师”终于可以有自己的名字了。

在构造函数中,除了可以使用“=”操作符对对象的成员变量进行赋值以完成初始化之外,还可以使用“:”符号在构造函数后引出初始化属性列表,直接利用构造函数的参数或者其他的合理初始值对成员变量进行初始化。其语法格式如下:

class 类名

{

public:

// 使用初始化属性列表的构造函数

类名(参数列表) : 成员变量1(初始值1),成员变量2(初始值2)…

// 初始化属性列表

{

}

// 类的其他声明和定义

};

在进入构造函数执行之前,系统将完成成员变量的创建并使用其后括号内的初始值对其进行初始化。这些初始值可以是构造函数的参数,也可以是成员变量的某个合理初始值。如果一个类有多个成员变量需要通过这种方式进行初始化,那么多个变量之间可以使用逗号分隔。例如,可以利用初始化属性列表将Teacher类的构造函数改写为:

class Teacher

{

public:

// 使用初始化属性列表的构造函数

Teacher(string strName) : m_strName(strName)

{

// 构造函数中无需再对m_strName赋值

}

private:

string m_strName;

};

使用初始化属性列表改写后的构造函数,利用参数strName直接创建Teacher类的成员变量m_strName并对其进行初始化,这样就省去了使用“=”对m_strName进行赋值时的额外工作,可以在一定程度上提高对象构造的效率。另外,某些成员变量必须在创建的同时就给予初始值,比如某些使用const关键字修饰的成员变量或引用类型的成员变量,这种情况下使用初始化属性列表来完成成员变量的初始化就成了一种必须了。所以,在可以的情况下,好是使用构造函数的初始化属性列表中完成类的成员变量的初始化。

这里需要注意的是,如果类已经有了显式定义的构造函数,那么编译器就不会再为其生成默认构造函数。例如,在Teacher类拥有显式声明的构造函数之后,如果还是想采用如下的形式定义对象,就会产生一个编译错误。

// 试图调用默认构造函数创建一个没有名字的老师

Teacher MrUnknown;

这时编译器就会提示错误,因为这个类已经没有默认的构造函数了,而唯一的构造函数需要给出一个参数,这个创建对象的形式会因为找不到合适的构造函数而导致编译错误。因此在实现类的时候,一般都会显式地写出默认的构造函数,同时根据需要添加带参数的构造函数来完成一些特殊的构造任务。

在C++中,根据初始条件的不同,我们往往需要用多种方式创建一个对象,所以一个类常常有多个不同参数形式的构造函数,分别负责以不同的方式创建对象。而在这些构造函数中,往往有一些大家都需要完成的工作,一个构造函数完成的工作很可能是另一个构造函数所需要完成工作的一部分。比如,Teacher类有两个构造函数,一个是不带参数的默认构造函数,它会给Teacher类的m_nAge成员变量一个默认值28,而另一个是带参数的,它首先需要判断参数是否在一个合理的范围内,然后将合理的参数赋值给m_nAge。这两个构造函数都需要完成的工作就是给m_nAge赋值,而第一个构造函数的工作也可以通过给定参数28,通过第二个构造函数来完成,这样,第二个构造函数的工作就成了第一个构造函数所要完成工作的一部分。为了避免重复代码的出现,我们只需要在某个特定构造函数中实现这些共同功能,而在需要这些共同功能的构造函数中,直接调用这个特定构造函数就可以了。这种方式被称为委托调用构造函数(delegating constructors)。例如:

class Teacher

{

public:

// 带参数的构造函数

Teacher(int x)

{

// 判断参数是否合理,决定赋值与否

if (0 < x && x <= 100)

m_nAge = x;

else

cout<<"错误的年龄参数"<

}

private:

int m_nAge;

}

// 构造函数Teacher()委托调用构造函数Teacher(int x)

// 这里我们错误地把出生年份当作年龄参数委托调用构造函数

// 直接实现了参数合法性验证并赋值的功能

Teacher() : Teacher(1982)

{

// 完成特有的创建工作

}

private:

int m_nAge; // 年龄

};

在这里,我们在构造函数之后加上冒号“:”,然后跟上另外一个构造函数的调用形式,实现了一个构造函数委托调用另外一个构造函数。在一个构造函数中调用另外一个构造函数,把部分工作交给另外一个构造函数去完成,这就是委托的意味。不同的构造函数各自负责处理自己的特定情况,而把基本的共用的构造工作委托给某个基础构造函数去完成,实现分工协作。

析构函数

当一个使用定义变量的形式创建的对象使用完毕离开其作用域之后,这个对象会被自动销毁。而对于使用new关键字创建的对象,则需要在使用完毕后,通过delete关键字主动销毁对象。但无论是哪种方式,对象在使用完毕后都需要销毁,也就是完成一些必要的清理工作,比如释放申请的内存、关闭打开的文件等。

跟对象的创建比较复杂,需要专门的构造函数来完成一样,对象的销毁也比较复杂,同样需要专门的析构函数来完成。同为类当中负责对象创建与销毁的特殊函数,两者有很多相似之处。首先是它们都会被自动调用,只不过一个是在创建对象时,而另一个是在销毁对象时。其次,两者的函数名都是由类名构成,只不过析构函数名在类名前加了个“~”符号以跟构造函数名相区别。再其次,两者都没有返回值,两者都是公有的(public)访问级别。后,如果没有必要,两者在类中都是可以省略的。如果类当中没有显式地声明构造函数和析构函数,编译器也会自动为其产生默认的函数。而两者唯一的不同之处在于,构造函数可以有多种形式的参数,而析构函数却不接受任何参数。下面来为Teacher类加上析构函数完成一些清理工作,以替代默认的析构函数:

class Teacher

{

public: // 公有的访问级别

// …

// 析构函数

// 在类名前加上“~”构成析构函数名

~Teacher() // 不接受任何参数

{

// 进行清理工作

cout<<"春蚕到死丝方尽,蜡炬成灰泪始干"<

};

// …

};

因为Teacher类不需要额外的清理工作,所以在这里我们没有定义任何操作,只是输出一段信息表示Teacher类对象的结束。一般来说,会将那些需要在对象被销毁之前自动完成的事情放在析构函数中来处理。例如,对象创建时申请的内存资源,在对象销毁后就不能再继续占用了,需要在析构函数中进行合理地释放,归还给操作系统。

注意析构函数只能销毁对象的非static成员,static成员要到程序结束后才会被释放。由于析构函数没有入参也没有返回值,所以析构函数不能被重载,对于给定的类只有唯一的一个析构函数,但是构造函数可以被重载。

什么时候会调用析构函数:

无论何时一个对象被销毁,就会自动调用其析构函数:

1. 变量在离开其作用域时被销毁。

2. 当一个对象被销毁时,其成员被销毁

3. 容器(不论是标准库容器还是数组)被销毁时,其元素被销毁

4. 对于动态分配的对象(new),当对指向它的指针应用delete运算符时被销毁

5. 对于临时对象,当创建它的完整表达式结束时被销毁

由于析构函数自动运行,我们的程序可以按需要分配资源,而通常无需要担心何时释放这些资源。认识到析构函数本身并不直接销毁成员是非常重要的,成员是在析构函数体后隐含的析构阶段中被销毁的,在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。

如果显示调用析构函数,析构函数相当于的一个普通的成员函数,执行析构函数体中的语句,并没有释放内存。

class aaa

{

public:

aaa(){}

~aaa(){cout<<"deconstructor"<

void disp(){cout<<"disp"<

private:

char *p;

};

void main()

{

aaa a;

a.~aaa();

a.~aaa();

a. disp();

}

这样的话,显示两次deconstructor,前两次调用析构函数相当于调用一个普通的成员函数,执行函数内语句,显示两次deconstructor。

真正的析构是编译器隐式的调用,增加了释放栈内存的动作,这个类未申请堆内存,所以对象干净地摧毁了。

class aaa

{

public:

aaa(){p = new char[1024];}

~aaa()

{cout<<"deconstructor"<

void disp(){cout<<"disp"<

private:

char *p;

};

void main()

{

aaa a;

a.~aaa();

a.~aaa();

a.disp();

}

这样的话,第一次显式调用析构函数,相当于调用一个普通成员函数,执行函数语句,释放了堆内存,但是并未释放栈内存,对象还存在(但已残缺,存在不安全因素);

第二次调用析构函数,再次释放堆内存(此时报异常)并打印。

后隐式执行析构过程释放栈内存,对象销毁。

上一篇:骆驼命名法,匈牙利命名法和帕斯卡命名法

下一篇:Android视频监控实现(二)

热点文章推荐
华清学员就业榜单
高薪学员经验分享
热点新闻推荐
前台专线:010-82525158 企业培训洽谈专线:010-82525379 院校合作洽谈专线:010-82525379 Copyright © 2004-2022 北京华清远见科技集团有限公司 版权所有 ,京ICP备16055225号-5京公海网安备11010802025203号

回到顶部