由friend用法引出的声明与定义那些事儿
今天遇到了一个问题,大致描述一下就是有两个类A和B。我想达到如下效果:B是A的友元,同时A是B的类类型成员。
第一次尝试,在B.h中包含A.h,在A.h中包含B.h,在A类中声明friend class B,在B类的定义中加入A a;
这一次尝试必然失败,编译报错,缺少分号什么的,原因是相互包含头文件。google了一下,找到一篇文章,解释的很好。
=================================华丽的引用线============================
一、类嵌套的疑问
C++头文件重复包含实在是一个令人头痛的问题,前一段时间在做一个简单的数据结构演示程序的时候,不只一次的遇到这种问题。假设我们有两个类A和B,分别定义在各自的有文件A.h和B.h中,但是在A中要用到B,B中也要用到A,但是这样的写法当然是错误的:
class B; class A { public: B b; }; class B { public: A a; };
因为在A对象中要开辟一块属于B的空间,而B中又有A的空间,是一个逻辑错误,无法实现的。在这里我们只需要把其中的一个A类中的B类型成员改成指针形式就可以避免这个无限延伸的怪圈了。为什么要更改A而不是B?因为就算你在B中做了类似的动作,也仍然会编译错误,表面上这仅仅上一个先后顺序的问题。
为什么会这样呢?因为C++编译器自上而下编译源文件的时候,对每一个数据的定义,总是需要知道定义的数据的类型的大小。在预先声明语句class B;之后,编译器已经知道B是一个类,但是其中的数据却是未知的,因此B类型的大小也不知道。这样就造成了编译失败,VC++6.0下会得到如下编译错误:
error C2079: 'b' uses undefined class 'B'
将A中的b更改为B指针类型之后,由于在特定的平台上,指针所占的空间是一定的(在Win32平台上是4字节),这样可以通过编译。
二、不同头文件中的类的嵌套
在实际编程中,不同的类一般是放在不同的相互独立的头文件中的,这样两个类在相互引用时又会有不一样的问题。重复编译是问题出现的根本原因。为了保证头文件仅被编译一次,在C++中常用的办法是使用条件编译命令。在头文件中我们常常会看到以下语句段(以VC++6.0自动生成的头文件为例):
#if !defined(AFX_STACK_H__1F725F28_AF9E_4BEB_8560_67813900AE6B__INCLUDED_) #define AFX_STACK_H__1F725F28_AF9E_4BEB_8560_67813900AE6B__INCLUDED_ //很多语句…… #endif
其中首句#if !defined也经常做#ifndef,作用相同。意思是如果没有定义过这个宏,那么就定义它,然后执行直到#endif的所有语句。如果下次在与要这段代码,由于已经定义了那个宏,因此重复的代码不会被再次执行。这实在是一个巧妙而高效的办法。在高版本的VC++上,还可以使用这个命令来代替以上的所有:
#pragma once
它的意思是,本文件内的代码只被使用一次。
但是不要以为使用了这种机制就全部搞定了,比如在以下的代码中:
1 //文件A.h中的代码 2 #pragma once 3 4 #include "B.h" 5 6 class A 7 { 8 public: 9 B* b; 10 }; 11 12 //文件B.h中的代码 13 #pragma once 14 15 #include "A.h" 16 17 class B 18 { 19 public: 20 A* a; 21 };
这里两者都使用了指针成员,因此嵌套本身不会有什么问题,在主函数前面使用#include "A.h"之后,主要编译错误如下:
error C2501: 'A' : missing storage-class or type specifiers
仍然是类型不能找到的错误。其实这里仍然需要前置声明。分别添加前置声明之后,可以成功编译了。代码形式如下:
//文件A.h中的代码 #pragma once #include "B.h" class B; class A { public: B* b; }; //文件B.h中的代码 #pragma once #include "A.h" class B; class B { public: A* a; };
这样至少可以说明,头文件包含代替不了前置声明。有的时候只能依靠前置声明来解决问题。我们还要思考一下,有了前置声明的时候头文件包含还是必要的吗?我们尝试去掉A.h和B.h中的#include行,发现没有出现新的错误。那么究竟什么时候需要前置声明,什么时候需要头文件包含呢?
三、两点原则
头文件包含其实是一想很烦琐的工作,不但我们看着累,编译器编译的时候也很累,再加上头文件中常常出现的宏定义。感觉各种宏定义的展开是非常耗时间的,远不如自定义函数来得速度。我仅就不同头文件、源文件间的句则结构问题提出两点原则,仅供参考:
第一个原则应该是,如果可以不包含头文件,那就不要包含了。这时候前置声明可以解决问题。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象(非指针),也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知。
第二个原则应该是,尽量在CPP文件中包含头文件,而非在头文件中。假设类A的一个成员是是一个指向类B的指针,在类A的头文件中使用了类B的前置声明并便宜成功,那么在A的实现中我们需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类B的头文件而非声明部分(H文件)。
=================================华丽的引用线=================================
之后找了几个网友的评论,很有借鉴意义
“两个类相互引用,不管哪个类在前面,都会出现有一个类未定义的情况。而类的声明就是提前告诉编译器,所要引用的是个类,但此时后面的那个类还没有定义,因此无法给对象分配确定的内存空间,因此只能使用类指针。”
“如果光是指针,可以前置声明
如果互相引用实体,那一定是错误的设计,需求本身就不正确。”
“如果在A的头文件中你不需要知道B的细节,那么压根不需要包含B的头文件。”
“按照C++规范
1 任何类可以声明多次
2 多次声明如果存在冲突, 那么不同编译器的处理方式可能不同,不过从开发者角度有义务保证多次声明不冲突。
3 任何类都必须先声明再引用,但是不必先定义。”
这里需要额外指出的是,前向声明的类在定义之前是一个不完全类型,不能定义该类型的对象,不完全类型只能用于定义指向该类型的指针及引用,或者用于声明使用该类型作为形参类型或返回类型的函数。
所以,如果要在类B中访问类A的私有变量,那么可以将类A作为类B成员函数的参数(指针或者引用)传递给B,设a为A的实例,利用a.x或a->x来访问A中私有成员变量,类似的,私有成员函数通过a.x()和a->x()访问。(不可用双冒号作用域符访问,必须用对象,除非是静态方法)
friend声明其实类似于前向声明,在此基础上多了访问权限。下面是我从stackoverflow上找的问答,可以很好解释这个问题。
=================================华丽的引用线=================================
Q:
I was wondering if you have to #include "Class1.h" in a class that is using that as a friend. For example the .h file for the class that is granting permission to Class1 class.
class Class2 {
friend class Class1;
}
would you need to #include "Class1.h" or is it not necessary? Also in the Class2 class, Class1 objects are never created or used. Class1 just manipulates Class2 never the other way around.
A:
The syntax is:
friend class Class1;
And no, you don't include the header.
More generally, you don't need to include the header unless you are actually making use of the class definition in some way (e.g. you use an instance of the class and the compiler needs to know what's in it). If you're just referring to the class by name, e.g. you only have a pointer to an instance of the class and you're passing it around, then the compiler doesn't need to see the class definition - it suffices to tell it about the class by declaring it:
class Class1;
This is important for two reasons: the minor one is that it allows you to define types which refer to each other (but you shouldn't!); the major one is that it allows you to reduce the physical coupling in your code base, which can help reduce compile times.
To answer Gary's comment, observe that this compiles and links fine:
class X;
class Y
{
X *x;
};
int main()
{
Y y;
return 0;
}
There is no need to provide the definition of X unless you actually use something from X.
=================================华丽的引用线=========================================
这里还有一篇讨论friend,前向声明与相关命名空间的问题,之后再抽时间研究,这里先mark一下。