注册 登录
编程论坛 C++教室

关于头文件

humy 发布于 2012-08-22 14:46, 1582 次点击
1.书上说:设计头文件应使其能被多次包含在同一源文件中所以只有声明不定义 a. 那我如下写的类定义是不是错的?
#include <string>
#include <iostream>
using namespace std;
class screen{
public:
typedef string ::size_type index;
char get() const{ return contents[ cursor];}
inline char get(index ht,index wd) const;
index get_cursor() const;
screen(index hght, index wdth,const string & cntnts);
screen& move (index r,index c);
screen& set (char);
screen& display(ostream & os);
private :
string contents;//b. 是不是这些变量前都要在前面加上extern?如果这样是不是在cpp里还要把这部分写一遍啊,只是不加extern?
index cursor;
index height;
index width;
};
2.又有一章说:类定义放在.h里,成员函数定义一般放在同名的源文件里 a. 结合问题1,变量声明在头文件,这说函数定义在源文件。那变量定义在哪? b. 我们写main时包含了头文件,但没提 其同名源文件的事啊,编译器会自动去找? c. 我用vs2010 的添加选项:除了创建项里有头文件,还有一个创建类。那用这个创建类的选项和 书上所说,用头文件,源文件的方式有区别吗?有的话哪个好? d. 创建的头文件不在当前的工程下,要包含这个头文件仍是直接#include"screen"就好了? 谢谢
21 回复
#2
lonmaor2012-08-22 19:07
a.可以。如果你的main.cpp文件中同样包含iostream和string头文件,不用担心重复定义。防止头文件被重复包含用的是宏命令:#ifndef/#endif
b. 不用加extern

a.类成员的变量定义和函数声明习惯性写在.h头文件中
b.main.cpp只要包含头文件就可以了。.h中的函数声明会自动到同名源文件中搜索函数定义
c.通过创建类选项,vs2010会自动创建.h和同名的.cpp文件。比较方便。
d.vs2010的项目有个include属性,表示编译的时候到这些目录中去搜索头文件,你只要添加了相应的路径就可以搜索到了。
一般编译器是通过环境变量INCLUDE来确定头文件的搜索路径,你可以进入命令行窗口后打set回车,可以看到当前的环境变量设置。
#3
pangding2012-08-22 22:00
如 lonmaor 所说。防止头文件被重复包含一般是用条件预处理命令:
程序代码:
#ifndef XXX
#define XXX

/* content of this header */

#endif
这样第一次包含这个文件的时候,由于 XXX 没有其它文件定义,因而 if 和 endif 之间的内容会被执行,从而定义了 XXX,和头文件中本来需要定义的东西。
之后如果再包含这个头文件,由于 XXX 已经定义过了。会跳过 if 和 endif 之间的内容。相当于什么都没干。

但这依然不是万能的解决方法。因为如果一个变量既在 a.h 中有所定义,又在 b.h 中有所定义。那么最终会因为重复定义而产生错误。
所以一般只能用 exterm 声明变量,而在对应的 cpp 中定义。因为 cpp 不会被其它文件包含,从而保证变量最多被定义一次。
但使用这种技术不表代就不会出问题。比如如果设计不当,就可能会出现循环包含。比如 a.h 包含 b.h,而 b.h 又包含 a.h。这种情况也许比较少见,但工程复杂之后间接的循环包含却可能会出现。设计良好的头文件不应该出现任何循环包含。


你 b 问的那个问题,contents 其实只是类 screen 定义的一部分。并不是在单独定义变量,因此不用加 extern。而在 cpp 里,也只用包含这个头文件就可以了。不用再重新定义。
按道理来讲,类和变量一样,不应该定义在 .h 里。但实际上人们却不是这么做的。
对于一个变量来说,知道它的类型,编译器就等于知道了它的一切。但是对于类来说,编译器如果见不到它的完整定义,根本无法确定它的有关信息。比如应该为它分配几个字节的内存,或者它有哪些成员函数等等。
所以类必须得定义在头文件里,包含之后才能使用。不过这样的话 a.h 和 b.h 里都定义了同名的类,那就会出错。名空间的一个目的就是为了减少这种情况带来的冲突,但需要精心的设计。这和刚才说头文件的设计一样,问题没有万能的解决方法。

2.又有一章说:类定义放在.h里,成员函数定义一般放在同名的源文件里 a. 结合问题1,变量声明在头文件,这说函数定义在源文件。那变量定义在哪?
变量应该定义在 cpp 里。原因如上述。

b. 我们写main时包含了头文件,但没提 其同名源文件的事啊,编译器会自动去找?
只要编译能获得类型相关的全部信息,它就可以完成编译的工作。比如编译器只要知道 int func(int, int) 这个函数原型,就能正确处理传参数、正确处理返回值的问题。并生成正确的函数调用的代码。而不需要知道函数的定义。
比如你包含的系统库中的函数,有些就只有函数原型,没有定义。但这不影响编译的过程。
所有源文件都编译完成之后,还有链接的的工作。它负责把函数调用和函数的定义联系在一起。它会在一些指定的地方查找函数的定义,比如系统库文件的位置和当前工程指定的位置。如果这时链接器发现有该找的函数定义找不到,就会产生链接错误。这一般和语法错误有明显区别。
头文件和源文件同不同名,其实对于编译和链接来说都无关紧要。一个头文件中声明的函数,即使定义在数个源文件里也是可以的。但是人喜欢看同名的。因为之后找起来方便,逻辑关系也更清楚些。

c. 我用vs2010 的添加选项:除了创建项里有头文件,还有一个创建类。那用这个创建类的选项和 书上所说,用头文件,源文件的方式有区别吗?有的话哪个好?
自动创建和手动创建没有本质区别。但前者由于自动化,可以节约一些时间和精力。至于哪个好,是个人品味的问题。如果用得习惯,当然是自动化的方法好喽。

d. 创建的头文件不在当前的工程下,要包含这个头文件仍是直接#include"screen"就好了?
如果可以搜索得到,包含一下就行了。如果不能,讲按 2楼 讲的方法进行设置。


有些问题讲得稍微深了点。楼主尽量理解就好。


[ 本帖最后由 pangding 于 2012-8-22 22:09 编辑 ]
#4
有容就大2012-08-22 22:58
说的好 帮顶
#5
humy2012-08-23 10:20
回复 3楼 pangding
非常感谢两位的回答,很满意。但恕我才疏学浅,再追问几句:

1.书上在说完  以防重复定义所以头文件只声明   后,就讲了条件预处理命令。两者是   并列关系  即  是两种处理 多次定义  的方法?
2.“contents 其实只是类 screen 定义的一部分。并不是在单独定义变量,因此不用加 extern。而在 cpp 里,也只用包含这个头文件就可以了。不用再重新定义。”是说类定义里的对变量的操作都不是定义?是声明?那怎么还会“因为如果一个变量既在 a.h 中有所定义,又在 b.h 中有所定义。那么最终会因为重复定义而产生错误。所以一般只能用 exterm 声明变量,而在对应的 cpp 中定义。”?   没理解,所以这两句意思感觉矛盾。。。   “变量应该定义在 cpp 里。”到底怎么定义呢?
3“.vs2010的项目有个include属性,表示编译的时候到这些目录中去搜索头文件,你只要添加了相应的路径就可以搜索到了”如图: 是这个吗?包含目录后面的内容是:$(VCInstallDir)include;$(VCInstallDir)atlmfc\include;$(WindowsSdkDir)include;$(FrameworkSDKDir)\include;没看懂。。。这些是路径?
4.“你可以进入命令行窗口后打set回车,可以看到当前的环境变量设置。”  请问命令行窗口指什么?是图中的吗?怎么没呢?
5“main.cpp只要包含头文件就可以了。.h中的函数声明会自动到同名源文件中搜索函数定义”和“它负责把函数调用和函数的定义联系在一起。它会在一些指定的地方查找函数的定义,比如系统库文件的位置和当前工程指定的位置。”说的不一样,还是后者对吧。但“一些指定的地方”是?我是可以随便在一个工程里建一个定义函数的cpp吗?只要有,他就能找到是吗?
只有本站会员才能查看附件,请 登录
只有本站会员才能查看附件,请 登录
#6
lonmaor2012-08-23 11:25
1.书上在说完  以防重复定义所以头文件只声明   后,就讲了条件预处理命令。两者是   并列关系  即  是两种处理 多次定义  的方法?
一般似乎只在.h文件中添加#ifndef/#endif预定义语句,同名的.cpp中并不包含#ifndef/#endif语句。变量定义和函数声明写在头文件里,这是个好习惯。

 3“.vs2010的项目有个include属性,表示编译的时候到这些目录中去搜索头文件,你只要添加了相应的路径就可以搜索到了”如图: 是这个吗?包含目录后面的内容是:$(VCInstallDir)include;$(VCInstallDir)atlmfc\include;$(WindowsSdkDir)include;$(FrameworkSDKDir)\include;没看懂。。。这些是路径?
诸如 $(VCInstallDir)、$(WindowsSdkDir)都是一些宏定义变量,在用到的时候前者会被展开为类似 c:/program files/visual studio/vc10/的绝对路径。

 4.“你可以进入命令行窗口后打set回车,可以看到当前的环境变量设置。”  请问命令行窗口指什么?是图中的吗?怎么没呢?
呃,这里命令行方式不是指vs的命令行方式,是windows程序的附件中的命令提示符,用win+R打开运行窗口,输入cmd回车也是一样的。

5“main.cpp只要包含头文件就可以了。.h中的函数声明会自动到同名源文件中搜索函数定义”和“它负责把函数调用和函数的定义联系在一起。它会在一些指定的地方查找函数的定义,比如系统库文件的位置和当前工程指定的位置。”说的不一样,还是后者对吧。但“一些指定的地方”是?我是可以随便在一个工程里建一个定义函数的cpp吗?只要有,他就能找到是吗?
这个暂以p版的解答为准吧。你说的方法可以试试,即使可以用,但没有这个习惯。
#7
humy2012-08-23 13:47
回复 6楼 lonmaor
1.
添加#ifndef/#endif预定义语句,感觉函数定义什么的就可以直接写在头文件里也没事了吧,那为何还要建源文件?



4.
这里命令行方式不是指vs的命令行方式,是windows程序的附件中的命令提示符,用win+R打开运行窗口,输入cmd回车也是一样的。
只有本站会员才能查看附件,请 登录
吧?

非常感谢。
#8
humy2012-08-23 14:24
还有“比如如果设计不当,就可能会出现循环包含。比如 a.h 包含 b.h,而 b.h 又包含 a.h”
书上有个例子:Message 类里有一个set<Folder*>保存message所在所有文件夹的指针。而类Folder里有一个set<Message*>保存文件夹里所有文件的指针
如Folder类定义前写一个Message的声明,不就解决了循环包含吗?
程序代码:
#include<set>
#include<string>
class Message
class Folder
{//...}
#9
lonmaor2012-08-23 14:55
嵌套包含的实例我也没用过。
大致是类似于
程序代码:
class a;
class b
{
   ...
   a* pa;
}
class a
{
   ...
   b* pb;
}


的东西
#10
humy2012-08-23 14:56
程序代码:
Folder& Folder::operator=(const Folder&f){
    if(& f!=this){
        remove_Fl_from_Msg();
        messages=f.messages;
        put_Fl_in_Msg(messages);
     }
    return *this;
}
还有为什么返回值类型Folder&前没有Folder::
#11
lonmaor2012-08-23 16:35
Folder&表示返回的是一个类类型的引用,就跟int&一样。
Folder::operator=表示operator=归属于Folder类的,是一个重载的成员函数。这里是类作用域标识符。
#12
pangding2012-08-23 23:10
1.书上在说完  以防重复定义所以头文件只声明   后,就讲了条件预处理命令。两者是   并列关系  即  是两种处理 多次定义  的方法?
    两者相辅相成吧。还是用例子可能说得清楚些:
程序代码:
/* a.h */
#ifndef A_H
#define A_H

#include <iostream>

int a = 1;

#endif
程序代码:
/* b.h */
#ifndef B_H
#define B_H

#include <iostream>

int a = 1;

#endif
程序代码:
#include "a.h"
#include "b.h"
using namespace std;

int main()
{
    cout << a << endl;

    return 0;
}
    虽然,两个头文件都使用了条件编译,但还是不能避免重定义。
    不过它有另外的一些好处。比如如果 iostream 这个头文件也使用了这个技术的话(确实使了),本来可能会展开两次的的头文件,就只展开了一次。这意味着编译器可以少分析数千行的代码(这还得假设重复包含不会出现其它问题)。

    另一方面,如果 a.h 和 b.h 里都用的是外部声明。而其实 a 这个变量是定义在比如 a.c 这个文件里的话。那么包多少遍 a.h 和 b.h 都没问题。当然了,如果你又写了一个 b.c 也定义了 a 可能还是不行。

2.“contents 其实只是类 screen 定义的一部分。”是说类定义里的对变量的操作都不是定义?是声明?那怎么还会“因为如果一个变量既在 a.h 中有所定义,又在 b.h 中有所定义。那么最终会因为重复定义而产生错误。所以一般只能用 exterm 声明变量,而在对应的 cpp 中定义。”?   没理解,所以这两句意思感觉矛盾。。。   “变量应该定义在 cpp 里。”到底怎么定义呢?
    假如写 class A {}; 那么,无论你在那个括号里写什么东西,都是类 A 的定义,类的定义就是比较长而已。虽然有些语句看上去像是在定义变量,但其实都不是在定义变量。而是在定义一个类的成员,只是类定义语句的一部分。类的定义只能放在 .h 里,原因昨天我有解释,这不重复了。又果有什么不明白的地方可以再问。
    而后半段说的其实是上面那个问题。当时可能语序搞的不是很清楚,引起了一些歧义。正确的写法就是头文件里写 extern int a; cpp 里写 int a = 1; 这样。类的定义不受这个限制。

3“.vs2010的项目有个include属性。。。这些是路径?
4.“你可以进入命令行窗口后打set回车。。。怎么没呢?
    见 L版 的解释。

5但“一些指定的地方”是?我是可以随便在一个工程里建一个定义函数的cpp吗?只要有,他就能找到是吗?
    “一些地方”指的就是像 include 一样,你设置的地方。你不告诉它哪找,它自己是不会自动搜索整个硬盘的。如果你指定的地方它找不到,它就认为是找不到了。
    如果你没指定过,其实就是用的默认的,一般就是库文件,和当前工程。如果你指定它到另一个地方去找(这种做法有点像指定了另一个地方也是库文件一样),vc 应该是可以正确链接并生成可执行文件的。只是一般习惯上可能还是把相关的东西拉到当前工程里。我没怎么用过 vc,所以不是很清楚 vc 的做法。

感觉函数定义什么的就可以直接写在头文件里也没事了吧,那为何还要建源文件?
    最重要的,一个头文件可能不止插到一个源代码里。和重定义变量一样,比如 a.h 里定义了一个函数 func(),而 main.cpp 和 src.cpp 都包含了 a.h,那么 func 这个函数就会被定义两次。编译也许可以过,但会引发链接错误。
    不过这么做还涉及别的考虑。人们普遍意识到,代码版本升级等行为,一般都会对函数的实现做出改动。但函数的原型却很少改动。这即是说,函数的原型比函数的实现更稳定。
    把稳定的东西插入到另一段代码里或者给其他人看是合适的。而把经常可能会改动的东西给别人看就不太好。因为人们一但知道某个函数是如何实现的,可能潜意识就会或多或少地利用这个函数的实现机里写自己的代码。但之后实现变了,也不会通知这个人,这个人也可能之后没再看过升级后的实现了。这就可能使得他写的代码兼容性下降。
    把定义写在源文件里还有其它效率上的原因,比如在编译时,编译器会分析代码的逻辑。比如我有 100 个源文件,我只改动了其中的三个后,要求重新构建。这时如果某个代码本身没有发生过任何变化,而且它包含的所有头文件也没有发生过变化。那么这些代码本质上就没有发生任何变化,因此没有重新编译的必要。只用重新编译那三个改动了的代码,再把这些重新编译后的东西链接回原来的代码里就行了。如果你动不动就要改头文件,那么所有包含这些头文件的代码,虽然代码本身没有变化,但由于原来在头文件里声明的东西有可能会发生变化,编译器无法确定这些变化会有哪些影响,只好把它们全都重新编译一遍。

   
如Folder类定义前写一个Message的声明,不就解决了循环包含吗?
    这种技术叫做 forward declaration,也许可以译做提前声明。是不完整声明的一种。这种声明是有限制的。如果一个类在声明的过程中,需要知道另一个类的存在,而不需要定义这个类的对象,也不需要知道这个类的任何细节。就可以用这个语法项。
    具体讲,提前声明的类型只能用于声明其它的内容,但不能用于定义任何和它自己有关的东西。比如可以用这个方法声明友元类:
程序代码:
class A;    // forward declaration.

extern A a;    // OK; 声明一个外部对象。只是声明没有问题,没有定义。
class B { friend class A; };    // OK; 声明一个友元类,只要知道 A 是个类就行了。
class C { A *pa; };    // OK; 声明一个指向 A 类的指针。这个有点难理解,因为毕竟这里定义了一个对象,但是只用到了指向 A 的指针,没有用到和 A 有关的任何细节。
class D { A a; };    // error; 定义与 A 有关的对象。不行。

int main()
{
    return 0;
}
因此这种语法无法实现循环包含的情况。没有任何方法能实现循环包含。因为先定义的那个类,无法看见另一个类的定义,从而无法定义和那个类有关的任何对象。

还有为什么返回值类型Folder&前没有Folder::
    返回值前不需要加 Folder::,因为编译器在这之前看见过 Folder 的定义了,知道 Folder,或者  Folder& 是什么。那个 Folder::operator= 是用来限定这个函数名的。L版 回答的很清楚。
    其实可以把 Folder::operator= 看成是这个函数的全名。在类内部使用函数时,默认就是用本类内部的函数,可以省去。在外面的时候就必须得加上了。注意成员函数定义在类的外面,因此要加 :: 限定。如果是在外面调用,那要用 f.operator= 类似的语法,因为编译器知道 f 是 Folder 的一个对象,才知道你要调用的是这个类的 operator= 而不是其它的。


[ 本帖最后由 pangding 于 2012-8-23 23:20 编辑 ]
#13
pangding2012-08-23 23:11
楼主好学,而且显然是自己动脑筋了。回答这种人问的问题,细致点不浪费。
#14
TonyDeng2012-08-23 23:13
以下是引用pangding在2012-8-23 23:11:36的发言:

楼主好学,而且显然是自己动脑筋了。回答这种人问的问题,细致点不浪费。

带女人头的当然要细致点啦
#15
pangding2012-08-23 23:18
以下是引用TonyDeng在2012-8-23 23:13:11的发言:


带女人头的当然要细致点啦

这层原因戳破了就没意思了
#16
humy2012-08-25 17:53
回复 12楼 pangding
“正确的写法就是头文件里写 extern int a; cpp 里写 int a = 1; 这样。类的定义不受这个限制。”
恩。。。。关于声明,定义。int  a;是定义吧?那在类的定义里写,还是定义不?如:class A{ int a;}中的算是声明?如果是定义,可大家都这么些的啊。。
谢谢
#17
pangding2012-08-27 01:26
class A{ int a;}中的算是声明?
这个语句比较长,但只定义了一个类 A,它包含了一个属性为 private,类型为 int 的成员,名为 a。
类定义的语法形式是 class A { ... }; 大括号里的看成整体先不算,它只有一个分号。因此和 int a; 是一样的。不管类的定义多长,都只是一条语句。这么说明白了?
编译器在定义任何类 A 的对象之前,必须见到类 A 的完整定义,否则它还是不知道类 A 的的细节。因此如果不把类 A 的完整定义写在 .h 里是没有用的,编译器无法从其它文件中查到。

我这么说吧,合理的做法是这样,任何对象都只在 .cpp 中定义,而在 .h 中声明。至于为什么这样做合理,之前我分析过了,应该你也有所明白了。
但类的定义不属于这个范畴。因为类的定义,其实是在告诉编译器一个类型,而不是在声明对象。相当于我在告诉编译器 int 是什么东西。这不定义任何对象,只是定义这么一个概念。当然了 int 是内置的基本类型,这个“内置”就是指,它已经知道了,不用你教。但类是你自己脑子中的东西,需要有一种严格的语法来教会编译器。这个语法就是类定义。而一旦你类定义好了,就可以用这个类的概念定义对象了,比如和 int i; 一样,现在也可以写 A a; 了。这两个语句所谓的定义是对等的概念,必须要放在 cpp 里。而“定义”类这个所谓的定义,是一种观念上的东西,与实际的定义不同。


[ 本帖最后由 pangding 于 2012-8-27 01:27 编辑 ]
#18
humy2012-08-27 08:24
回复 17楼 pangding
恩。。谢谢。。其实我想问的是class A{int a;};中对于a 这个变量是定义还是对a的声明。。。
#19
TonyDeng2012-08-27 08:42
以下是引用humy在2012-8-27 08:24:25的发言:

恩。。谢谢。。其实我想问的是class A{int a;};中对于a 这个变量是定义还是对a的声明。。。

那是占位,就如struct A { int a; };一样。其实,class和struct在原理上是相同的,通常视struct为没有函数的class,事实上,C++的class概念就是从struct中引申出来的,所以到现在,基本上都用class取代struct了。
#20
pangding2012-08-27 12:41
以下是引用humy在2012-8-27 08:24:25的发言:

恩。。谢谢。。其实我想问的是class A{int a;};中对于a 这个变量是定义还是对a的声明。。。
看来我还是没说清楚。
这个语句既没有声明变量 a,也没有定义变量 a。它根本不是一个独立的语句,只是类 A 定义中的一小部分。不过长的有点像一个声明语句。

另外在从概念上讲,所有的变量定义都是变量声明。声明是个更广泛的概念,其中会引发内存分配的声明语句称作变量定义。
#21
humy2012-08-27 23:00
回复 20楼 pangding
奥。。明白了。但这是有点矛盾吗?就像函数在类的定义里是分定义和声明的。。。还是只是这样说其实他们也不是定义。。。。
#22
pangding2012-08-27 23:19
我只印象里函数直接在类里定义的就是隐式内联,在外面的不是。其它区别还真是不太清楚。

我又去查了标准,虽然标准里定义声明语句的时候没有提成员声明的事,但后面讲类成员的时候,又定义了成员声明的概念。也就是说其实成员声明也应该算声明语句。以这个为准吧。
其实这没啥可较真的,你纠结的问题,不是哪个语句是声明哪个语句是定义的事。你只是想知道,什么应该写 .h 里,什么应该写 .cpp 里而已。其它的都是浮云,学习的时候要分清主次。
说到底,哪个写哪不是规定,只是这么写有好处罢了。等你将来能写大规模一点的程序的时候出很多错误,你在研究怎么避免出错的过各中,就会知道应该怎么写了。这些东西只有经验说话管用,知识没有任何意义。
我之前说的好多东西,现在看来只是想为某个简单的原则辩护而已,但现在看来也许原则本来就没有那么简单。
1