详解C++ 前置声明

所属分类: 软件编程 / C 语言 阅读数: 117
收藏 0 赞 0 分享

前置声明是C/C++开发中比较常用的技巧,主要用在三种情形:

  1. 变量/常量,例如extern int var1;;
  2. 函数,例如void foo();,注意类的成员函数无法单独做前置声明;
  3. 类,例如class Foo;,也可以前置声明模板类:template class<typename T1, int SIZE>Foo;。如果类包含在名字空间中,需在名字空间内做前置声明:namespace tlanyan {class Foo;};,而不能这样:class tlanyan::Foo;

前置声明作用

根据其用途,前置声明的主要作用为:

  1. 避免重复定义变量;
  2. 避免引入函数定义/声明文件,从而函数文件发生更改时不会重新编译依赖文件;
  3. 解决循环依赖问题。

前两种用途好理解,第三种稍微复杂点,但却是前置声明最重要的用途。其解决类A包含类B,同时类B包含类A的依赖问题。循环依赖一般是设计层面的问题,可通过接口、引入辅助类等手段化解。前置声明也能解决,只是架构上稍微别扭。

不管A和B是否定义在同一个文件中,c++永远无法解决如下形式的循环依赖(后文解释原因):

// file: A.hpp
#include "B.hpp"
class A {
 int id;
 B b;
};

// file: B.hpp
#include "A.hpp"
class B {
 ...
 A a;
};

前置声明解决该问题需要与指针配合,转换成另一种形式。要点如下:

  1. 至少将某类的变量类型转换成指针,例如A中将B转成B*;
  2. 类A中对B使用前置声明;
  3. 类A的定义文件中移除对类B文件的包含(做了包含保护则可忽略)。

使用前置声明后,以下是一种可行的解决形式(两个类均使用了前置声明):

// file: A.hpp
//3. 移除对B的包含(使用了#pragma once或者#ifndef B_HPP等保护措施则无必要)

// 2. 前置声明类B
class B;
class A {
 int id;
 // 1. 成员变量转换成指针
 B* b;
};

// file: B.hpp
// 3. 移除对A的包含(有包含保护则非必要)

// 2. 前置声明类A
class B {
 ...
 // 1. 成员变量转换成指针
 A* a;
};

深入前置声明

如果你有其他编程语言的经验,会发现c++有点怪异:Java/C#/Python/PHP等语言可以轻松做到循环引用,无需使用类似的前置声明技巧。这不禁让人思考:C++为何必须要用前置声明才能化解?

原因在于C++定义对象有两种方式:一种是A a形式,a即对象,调用成员变量或函数用.,对象在栈中分配;另一种是A* a,a是指针,调用成员变量或函数用->,其指向地址存储实际对象,对象在堆中分配。

分配对象需要知道具体的内存大小,但以下形式我们不能确定类A和类B对象的大小:

class A {
  B b;
};
class B {
  A a;
};

对于这个简单例子,你可以直观认为A和B占用同样的内存,例如1字节,但也可以是2字节,3字节等;根据内存对齐要求,一般是4字节,8字节等。无论哪种情况,编译器无法确定其对象占用内存,便会报错停止编译。所以你应该知道为什么C++永远不应该(不能)这样做了吧?

那为何前置声明加指针的组合能解决循环引用问题的呢?因为正常情况下,数据类型指针在同一机器的编译器里占同样的内存。指针一般是4或者8个字节,对应32和64位指针。用了指针,即使有循环引用,类的大小也能轻易的确定下来。这也是Java/C#/Python/PHP等可以轻松循环引用的原因:这些语言中,对象变量其实都是指针,也意味着对象变量都是引用传递。

如果不移除文件的相互包含,能否省去前置声明呢?答案是不能,原因如下:

  1. C++按照一个个编译单元(translation unit)进行编译,如果两个文件互相包含且没有#pragma once等包含保护措施,则会出现递归包含,编译器报错;
  2. 如果两个头文件都有文件包含保护,编译A时会把B包含进来,但因为B包含了A,A中的包含保护生效,导致B文件内的内容实际未引入A,于是报B为未知符号的错误。

总的来说,不管是否移除对方的头文件,前置声明都是必须的。实践中为了避免文件变动时重新编译的耗费,移除不必要的头文件是一个好习惯。

以上就是详解C++ 前置声明的详细内容,更多关于C++ 前置声明的资料请关注脚本之家其它相关文章!

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多