C/C++编程:前置声明

1059 篇文章 280 订阅

什么是前置声明

所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.

一般的前置函数声明

#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");
}
 
void fun(char ch, int *pValue, double dValue)
{
  return;
}

自定义类型的前置声明

声明一个将稍后在此作用域定义的类类型。直到定义出现前,此类名具有不完整类型。这些类之间允许彼此引用:

class Vector; // 前置声明
class Matrix {
    // ...
    friend Vector operator*(const Matrix&, const Vector&);
};
class Vector {
    // ...
    friend Vector operator*(const Matrix&, const Vector&);
};

自定义类型的前置声明,由于编译器不知道类型的大小,所以不可以声明类型的对象。只可以利用类型声明指针和引用:

class B;

int main()
{
    B b; // error: aggregate 'B b' has incomplete type and cannot be defined
    B b1 = new B; // error: variable 'B b1' has initializer but incomplete type

    return 0;
}

#include <iostream>
using namespace std;
 
class B;
 
class A 
{
private:
  // 内置类型
  int m_nInt;
  int& m_nRInt;
  int* m_pInt;
 
  // 自定义类型
//  B b; // error!
  B* m_pB;
  B& m_b;
 
public: 
  A (B *pBPara = NULL) : m_nInt(100)
    , m_nRInt(m_nInt)
    , m_pInt(NULL)
    , m_pB(NULL)
    , m_b((NULL == pBPara) ? (*m_pB) : (*pBPara))
  {
    cout << "A()" << endl;
  }
  ~A()
  {
    cout << "~A()" << endl;
  }
 
  void funA()
  {
//    m_pB->doAnything(); // build error C2027: use of undefined type 'B'
  }
};
 
class B
{
private:
  int m_n;
 
public: 
  B (int n = 100) : m_n(n)
  {
    cout << "B()" << endl;
  }
  ~B()
  {
    cout << "~B()" << endl;
  }
  void doAnything()
  {
    cout << "B::anythig()" << endl;
  }
};
 
void main()
{
  A objA;
  system("pause");
}

如上,利用前置类型的指针想调用其成员函数,会报编译错误!那么,肿么办?请看下文。

声明与实现分离

代码如下,声明头文件:

/*
 * TestForwardDeclar.h
 */
#ifndef D_TESTFORWARDDECLAR_H_
#define D_TESTFORWARDDECLAR_H_
 
#include <iostream>
 
class B; // 前置声明自定义类型
 
class A 
{
private:
  // 内置类型
  int m_nInt;
  int& m_nRInt;
  int* m_pInt;
 
  // 自定义类型
//  B b; // error!
  B* m_pB;
  B& m_b;
 
public: 
  A (B *pBPara = NULL);
  ~A ();
  void funA();
};
 
class B
{
private:
  int m_n;
 
public: 
  B (int n = 100);
  ~B ();
  void doAnything();
};
 
#endif

代码如下,定义文件:

/*
 * TestForwardDeclar.cpp
 */
 
#include "TestForwardDeclar.h"
#include <iostream>
 
A::A (B *pBPara) 
  : m_nInt(100)
  , m_nRInt(m_nInt)
  , m_pInt(NULL)
  , m_pB(NULL)
  , m_b((NULL == pBPara) ? (*m_pB) : (*pBPara))
{
  std::cout << "A()" << std::endl;
}
 
A::~A()
{
  std::cout << "~A()" << std::endl;
}
 
void A::funA()
{
  m_pB->doAnything(); // 分开头文件和实现文件即可
}
 
 
B::B (int n) : m_n(n)
{
  std::cout << "B()" << std::endl;
}
 
B::~B()
{
  std::cout << "~B()" << std::endl;
}
 
void B::doAnything()
{
  std::cout << "B::anythig()" << std::endl;
}

代码如下:测试文件:

#include "TestForwardDeclar.h"
  
 void main()
 {
  A objA;
}

编译成功,运行结果是期望效果。

总结

  • 自定义类型前置声明时,只可以利用类型名声明指针和变量(谨记不可以声明对象或new 对象,均因为类型大小不确定,编译器无能为力)。
    • 前置声明的类是不完全类型(incomplete type),我们只能定义指向该类型的指针或引用,或者声明(但不能定义)以不完全类型作为参数或者返回类型的函数。
    • 毕竟编译器不知道不完全类型的定义,我们不能创建其类的任何对象,也不能声明成类内部的数据成员。
  • 若需要利用指针或引用调用前置类型的接口,必须按照声明和实现分离的方式进行编码。
  • 而且如果特性的源文件仅使用到该类的指针和引用,也令减少#include的依赖:
// 在 MyStruct.h 中
#include <iosfwd> // 含有 std::ostream 的前置声明
struct MyStruct {
    int value;
    friend std::ostream& operator<<(std::ostream& os, const S& s);
    // 其定义在 MyStruct.cpp 文件中提供,该文件使用 #include <ostream>
};

前置声明出现在局部作用域

如果前置声明出现在局部作用域,则它隐藏其外围作用域中可出现的先前声明的相同名字的类、变量、
函数,以及所有其他声明:

struct s { int a; };
struct s; // 不做任何事(s 已定义于此作用域)
void g() {
    struct s; // 新的局部类“s”的前置声明
              // 它隐藏全局的 struct s 直至此块结尾
    s* p;     // 指向局部 struct s 的指针
    struct s { char* p; }; // 局部 struct s 的定义
}

注意

注意,通过作为其他声明一部分的详述类型说明符,也可以引入新的类名,但仅当名字查找无法找到先前声明的有此名的类时才行。

class U;
namespace ns{
    class Y f(class T p); // 声明函数 ns::f 并声明 ns::T 与 ns::Y
    class U f(); // U 指代 ::U
    Y* p; T* q; // 可使用到 T 和 Y 的指针及引用
}

实施建议:倾向于使用#include,而不是前置声明

建议: 倾向于使用#include,而不是前置声明

原因:

  • 前置声明最大的好处是节省编译时间
    • 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
    • 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
  • 但是带来的问题更大
    • 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
    • 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
    • 前置声明来自命名空间 std:: 的 symbol 时,其行为未定义。
    • 举个例子:
      • 一个类的实现着需要把这个类改个名字/换个命名空间。出于兼容性原本可以在原命名空间里/用原名通过using来起一个别名指向新类,然而别名不能被前置声明。。内网有一份代码改动一下子试图修改总计265个头文件,就是实现者为了要改这个类的名字而不得不去改所有的调用处。想一想,如果这265个文件分属于50个不同的团队,你得拿到50个人的同意才能提交这份改动,想不想打人?
      • 再举一个code style中提到的,更为严重的例子——它可能导致运行时出现错误的结果:
// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // calls f(B*)

若把#include换成前置声明,由于声明时不知道D是B的子类,test()中f(x)就会导致f(void*)被调用,而不是f(B*)。

再比如,C++标准5.3.5/5中规定,delete一个不完整类型的指针时,如果这个类型有non-trivial的析构函数,那么这种行为是未定义的。把前置声明换成#include则能保证消除这种风险。

什么时候该用前置声明,什么时候该用 #include

  • 尽量避免前置声明那些定义在其他项目中的实体
  • 函数:总是使用 #include.
  • 类模板:优先使用 #include.
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值