100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > 函数实现不放在头文件的原因 及何时可以放头文件的情况

函数实现不放在头文件的原因 及何时可以放头文件的情况

时间:2020-11-11 01:34:01

相关推荐

函数实现不放在头文件的原因 及何时可以放头文件的情况

1、引子

在平常的C/C++开发中,几乎所有的人都已经习惯了把类和函数分离放置,一个.h的头文件里放声明,对应的.c或者.cpp中放实现。从开始接触,到熟练使用,几乎已经形成了下意识的流程。尽管这样的做法无可厚非,而且在不少情况下是相对合理甚至必须的,但我还是要给大家介绍一下把实现全部放置到头文件中的方式,给出可供大家使用的另一个选择。同时针对这一做法,也顺便说一下其优缺点以及需要注意的情况。

我是一个很喜欢简洁的人,多年以来甚至养成了这样的癖好,如果一个功能是能够用一条语句实现的,那就不要用两条语句。在我看来,如果给别人提供一份可以复用的代码的话,最优雅的状态莫过于仅仅提供一个头文件就全部搞定。之所以不太喜欢引入源文件,最重要的原因是源文件往往会带来工程文件的变化;而且,在使用过程中也会增加一些额外的操作,例如,在一个组织良好的工程里,头文件和源文件很有可能是位于不同的目录,这样就会多带来一次文件复制操作。

2、正文

2.1顾虑

我遇到有不少人不使用头文件来包含实现,往往是出于以下几种顾虑:

1、暴露了实现细节

2、头文件被包含到不同的源文件中,会导致链接冲突

3、头文件被包含到不同的源文件中,会导致有多份实现被编译出来,增大可执行体的体积

如果有顾虑1,那很显然应该在第一时间抛弃完全在头文件中实现的念头。不过我遇到的情形里,通常后两种顾虑占据了绝对的比例。而这种顾虑,通常是由于对C/C++没有足够的了解导致的。

有顾虑2的,经常会是一些有C语言开发经验的程序员。他们所担心的也往往是出现的全局函数的情况。例如有以下头文件c_function.h(清晰起见,防卫宏之类的代码没有列出):

[cpp]

intinteger_add(constinta,constintb)

{

returna+b;

}

如果在同一工程中,有a.c(或者是.cpp)和b.c两个(或两个以上)源文件包含了此头文件,则在链接时期就会发生冲突,因为在两个源文件编译得到的目标文件中都有一份integer_add的函数实现,导致链接器不知道对于调用了此函数的调用者,应该使用哪一个副本。

2.2着手

解决的办法有两个,各自为两个关键字,一个是inline,另一个是static。使用这两个关键字的任意一个来修饰integer_add函数,都会消除上述的冲突问题,然而本质却大不相同。

如果使用inline,则意味着编译器会在调用此函数的地方把函数的目标代码直接插入,而不是放置一个真正的函数调用,实际作用就是这个函数事实上已经不再存在,而是像宏一样被就地展开了。使用inline的副作用,首先在于毋庸置疑地,代码的体积变大了;其次则是,这个关键字严格算起来并不是C语言的关键字,使用它多少会带来一些移植性方面的风险,尽管主流的C语言编译器都可以支持inline。对于GCC,inline功能关键字就是inline本身,而对于微软的编译器,应该是__inline(注意有两个前导下划线)。而且,根据惯例,inline通常都是对编译器的某种暗示而非强制要求,编译器有权力在你不知情的情况下把它实现为非inline的状态(可能的原因有,函数太大或者复杂度过高)。这样的后果是什么,不好意思,我没有测试过。

如果是使用static,那么至少结果是可预料的。所有包含此头文件的源文件中都会存在此函数的一份副本。虽然代码也有一定程度的膨胀,但好就好在互相不冲突,因为static关键字保证了该函数的可见度为单个源文件之内。

以上的讨论虽然看起来主要聚焦在C语言上,但由于C++是C语言的超集,并且在这些方面并没有做太多的修改,因此讨论结果同样也适用于C++。

2.3继续

对于C语言来讲,上面的改进几乎已经走到了尽头,没有继续发展的余地。然而对于C++则不同,我们还可以进一步把它做得更漂亮。

首先,我们做以下的改动:

[cpp]

classInteger

{

public:

intadd(inta,intb)

{

returna+b;

}

};

这样的形式,几乎连C++的初学者都能看出来,确实不会再发生链接冲突的问题了。不过也有一个问题,我们如果要计算两个整数的和的话,需要这样写:

Integerop;

op.add(i,j);

而这显然不是一种可接受的状态,之前很简单的一条函数语句的调用,现在却必须定义一个类的对象实例。于是我们再次求助于static(inline是不适用的,因为它不能去掉定义对象实例这一步,而且事实上,把实现写到类定义之内的函数缺省就是inline的)。现在,类就像这个样子:

[cpp]

classInteger

{

public:

staticintadd(inta,intb)

{

returna+b;

}

};

调用方式也相应地简化为:

Integer::add(i,j);

尤其需要注意的就是这里,C++类中的static函数和全局static函数的行为是有差异的,它编译之后仅产生一份实现代码,并不会由于被多个源文件包含而产生多份副本。

这距离我们的终极目标已经不远了(我们的终极目标是:add(i,j)就可以搞定)。于是我们再次高举起宏这杆大旗,在头文件里添加以下定义:

#defineinteger_addInteger::add(后注:突然想到,似乎定义const函数指针也可以达到相同的目的)

上面解决的其实仅仅是C++中全局函数的头文件复用问题,那么类呢?类的情况要复杂一些。如果是static方法,那么正好是和上述我们对全局函数的变通实现是一致的;如果是inline的方法(不管有没有inline关键字),则其状态几乎理论上等同于前面所述的inline全局函数的情况。那么还有最后的一种情况,virtual函数。对于virtual函数,我们等到的是一个好消息:它总是生成一份代码(甚至你显式使用inline关键字修饰)。这里面有个玄机:virtual函数的地址会被写到类的v-table里,是要能够在运行期被调用的(其核心在于,其调用者以及调用时机在编译时是不明确的),所以绝对不能生成为全部就地展开的形式。以此可以做一个推论:所有会被求址的成员函数,都会生成一份函数实体,而不能单纯地去符合内联的修饰关键字。

3、后记

当然,把实现全部放在头文件中并不是万金油,不是放之四海而皆准的准则,正如本文开头所说,这仅仅是一种选择,只不过你之前没有想到过可以这么做,而现在知道了。它最适合的场合是一些规模较小的工具类的实现。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。