详解C++ 前置声明

前置声明是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++ 前置声明的资料请关注我们其它相关文章!

(0)

相关推荐

  • 浅析C++中前置声明的应用与陷阱

    前置声明的使用有一定C++开发经验的朋友可能会遇到这样的场景:两个类A与B是强耦合关系,类A要引用B的对象,类B也要引用类A的对象.好的,不难,我的第一直觉让我写出这样的代码: 复制代码 代码如下: // A.h#include "B.h"class A{ public:    A(void);    virtual ~A(void);};//A.cpp#include "A.h"A::A(void){}A::~A(void){}// B.h#include &qu

  • C++ 前置声明详解及实例

    C++ 前置声明详解及实例 [1]一般的前置函数声明 见过最多的前置函数声明,基本格式代码如下: #include <iostream> using namespace std; void fun(char ch, int *pValue, double dValue); void main() { int nValue = 100; double dValue = 111.22; fun('a', &nValue, dValue); system("pause")

  • 详解C++ 前置声明

    前置声明是C/C++开发中比较常用的技巧,主要用在三种情形: 变量/常量,例如extern int var1;; 函数,例如void foo();,注意类的成员函数无法单独做前置声明: 类,例如class Foo;,也可以前置声明模板类:template class<typename T1, int SIZE>Foo;.如果类包含在名字空间中,需在名字空间内做前置声明:namespace tlanyan {class Foo;};,而不能这样:class tlanyan::Foo;. 前置声明

  • 详解Javascript函数声明与递归调用

    Javascript的函数的声明方式和调用方式已经是令人厌倦的老生常谈了,但有些东西就是这样的,你来说一遍然后我再说一遍.每次看到书上或博客里写的Javascript函数有四种调用方式,我就会想起孔乙己:茴字有四种写法,你造吗? 尽管缺陷有一堆,但Javascript还是令人着迷的.Javascript众多优美的特性的核心,是作为顶级对象(first-class objects)的函数.函数就像其他普通对象一样被创建.被分配给变量.作为参数被传递.作为返回值以及持有属性和方法.函数作为顶级对象,

  • PHP各版本中函数的类型声明详解

    PHP7开始支持标量类型声明,强类型语言的味道比较浓.使用这个特性的过程中踩过两次坑:一次是声明boolean,最近是声明double.为避免以后继续犯类似错误,就把官方文档翻了一次.本文是看完后对PHP函数的类型声明使用做的一次总结. 从语法上,PHP的函数定义经过了几个时期: 远古时代(PHP 4) 定义一个函数非常的简单,使用 function name(args) {body} 的语法声明.不能指定参数和返回值类型,参数和返回值类型有无限种可能.这是到目前为止最常见的函数声明方式. 数组

  • java中变量和常量详解

    变量和常量 在程序中存在大量的数据来代表程序的状态,其中有些数据在程序的运行过程中值会发生改变,有些数据在程序运行过程中值不能发生改变,这些数据在程序中分别被叫做变量和常量. 在实际的程序中,可以根据数据在程序运行中是否发生改变,来选择应该是使用变量代表还是常量代表. 变量 变量代表程序的状态.程序通过改变变量的值来改变整个程序的状态,或者说得更大一些,也就是实现程序的功能逻辑. 为了方便的引用变量的值,在程序中需要为变量设定一个名称,这就是变量名.例如在2D游戏程序中,需要代表人物的位置,则需

  • PHP中的函数声明与使用详解

      函数 1.  函数名是标识符之一,只能有字母数字下划线,开头不能是数字: 函数名的命名,必须符合"小驼峰法则"FUNC(),func(),Func(); 函数名不区分大小写; 函数名不能与已有函数同名,不能与内置函数名同名: 2.   function_exists("func");用于检测函数是否已经声明: 注意传入的函数名,必须是字符串格式,返回结果为true/false: echo打印时,true为1,false不显示:               [ph

  • 关于Java变量的声明、内存分配及初始化详解

    实例如下: class Person { String name; int age; void talk() { System.out.println("我是: "+name+", 今年: "+age+"岁"); } } public class TestJava2_1 { public static void main(String args[]) { Person p; if (p == null) { p = new Person(); }

  • 关于js二维数组和多维数组的定义声明(详解)

    声明一维数组:var goodsArr = []; 赋值:goodsArr[0] = 'First Value'; 这个毫无争议,因为平时使用PHP比较多,而php语法是可以直接使用goodsArr[0] = 'First Value'; 这种方法声明数组并赋值的,但js不能这样使用,必须先声明数组存在.同理,如果是二维和多维数组在使用前也必须声明二维和多维的数组,举例二维数组: var goodsArr[0] = []; 必须先这样声明一下二维数组才能使用二维数组,否则会出错的. 以上就是小编

  • 基于Java class对象说明、Java 静态变量声明和赋值说明(详解)

    先看下JDK中的说明: java.lang.Object java.lang.Class<T> Instances of the class Class represent classes and interfaces in a running Java application. An enum is a kind of class and an annotation is a kind of interface. Every array also belongs to a class tha

  • 详解js中let与var声明变量的区别

    ES6 新增了let命令,用来声明局部变量,所声明的变量,只在let命令所在的代码块内有效,而且有暂时性死区的约束. 1.ES6可以用let定义块级作用域变量 代码如下: function f1(){ { var a = 10; let b = 20; } console.log(a); // 10 console.log(b); // Uncaught ReferenceError: b is not defined } f1(); 说明:在ES6之前只有全局作用域和函数作用域,在ES6中新增

随机推荐