C++ 在.h文件中包含头文件和在.cpp文件中包含头文件有什么区别?前置声明

发布时间:2023年12月20日

现有两个文件Test.h 和Test.cpp
#include 在Test.h中包含 和在Test.cpp中包含有什么区别?

1、在cpp文件中包含.h文件,要么你要用到这个头文件中的函数或者类,要么就是实现这个头文件;

2、.h —就是为了放一堆声明所产生的东西。
如果是定义放在.h中。 如果.h被重复包含多次,那么则会被报重定义。所以在.h 中都要—如果函数就要是inline ,如果是变量就要 selectany (windows)才不会被报错。

3、#include尽量写到cpp文件里。两个文件在.h文件里相互include,就会产生编译错误,而两个文件在.c文件互相include,就不会有该问题,因此在.h文件include就要避免互相包含的问题,而.cpp文件就不需要考虑

4、
1)在 .h 里面 include 的好处是:
如果很多.c,.cpp文件,都包含一批头文件,
如果复制很容易遗漏
如果输入,很容易出错

如果全部在一个.h, include 那么每个.c,.cpp文件只需要一个#include 语句
这样不仅输入量减少,
而且代码也美观多了
代码也主次分明了
毕竟,.c.cpp, 里面
要实现的函数,才是主要代码

2)主要缺陷,
可能会包含完全不需要的头文件,
增加编译工作量

5、如果你在a.h头文件中include了“stdio.h”,“iostream”,……一大堆
那么你的a.cpp源文件只要include你的a.h,就相当于include了“stdio.h”,“iostream”,……一大堆
但是当其他文件include你的a.h的同时也就包含了“stdio.h”,“iostream”,……一大堆

这个要看你个人需要,如果你需要让其他文件也include一大堆,那么写在a.h中就可以,其他文件包含a.cpp简单整洁无脑
如果只有a.cpp需要include一大堆,那么还是建议在a.cpp中include一大堆

6、如果a.c包含了头文件a.h,a.h包含了头文件b.h,b.c也包含了b.h,那么当b.h发生改变时,a.c和b.c都会重新编译

也就是所有包含了b.h的都会重新编译,无论是直接包含,还是间接包含

7、2点原则:

第一个原则:如果可以不包含头文件,那就不要包含了,这时候前置声明可以解决问题。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象(非指针),也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知(C++编译器自上而下编译源文件的时候,对每一个数据的定义,总是需要知道定义的数据的类型的大小)

第二个原则:尽量在CPP文件中包含头文件,而非在头文件中。假设类A的一个成员是是一个指向类B的指针,在类A的头文件中使用了类 B的前置声明并编译成功,那么在A的实现中我们需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类B的头文件而非声明部分(H文件)

C++的类声明、前置声明、定义及各自优势、使用场景

当一个class的成员变量为另外一个类的指针类型的时候,此时,我们可以不在头文件中来包含这个类的头文件,即不使用include“.h”,而是采用前置声明的形式,对这个类进行一个类型的前置声明。

类的前置声明形式上:只有class关键字+类名,后面不带任何符号,即{}
白话解释:哎,b是个类,我要用它,你别管,它在某个地方是个真实的存在(也可能不存在,我就tm忽悠你的), 总之你不要管,我要用它”。这样跟编译器打过招呼之后,就能保证代码编译通过。
如下代码段

//文件a.h
 class B;
 class A
 {
 public:
 B *b;    
 };
 ?
 //文件b.h
 class B
 {
 public:
     void func1();
 };

a.hb.h这两个文件中定义了两个类分别是A和B。其中A类有一个成员b是Class B的指针,这个时候可使用类前置声明

前置声明省去了#include的处理、降低文件之间的编译依赖,从而避免不必要的编译时间浪费。

利用前置声明的利弊

  • 优点:避免#include, 避免修改一个被包含的头文件,导致整个文件重新编译。

  • 缺点:(摘自Google)如果前置声明关系到模板,typedefs, 默认参数和 using 声明,就很难决定它的具体样子了。

前置声明作用:
告诉编译器,这个类在其它地方定义,真正使用类的定义的时候才进行编译。

Note:类A在编译的时候不需要拿到类B的定义是因为这里面定义的是指针,而对于指针是不需要定义就可以进行内存布局的,在编译A的类的声明的时候,在进行内存布局的时候是不需要拿到B的定义的

但是在A的成员函数中如果使用这个指针b,则就需要在A对应的.cpp文件中引入B的定义。也就是在.cpp中需要include b.h

这里面对于前置声明,还需要注意一个事情,就是如果另外有一个类,这个类使用了类A,并且通过类A访问使用了A的成员变量b,比如下边这种情况

//文件c.h
 #include "a.h"
 class C
 {
 public:
     void print(){
         a.b->func1();
    }
    A a;
 };

编译时,讲会报错:不允许使用不完全(incomplete)类型A。
Because:C中访问了a成员b的成员函数。所以,此时也需要类B的完整定义,即include

比较正规的做法就是在C的头文件中也引入B的定义,这样编译器就能找到B的定义,从而编译通过

如法1

1
//文件c.h
 #include "a.h"
 #include "b.h"
 class C
 {
    ...
 }2
 //文件a.h
 #include "b.h"
 //class B;
 class A
 {
 public:
 B *b;    
 };

法2
在类A的头文件中引入b.h头文件
这种写法,就是放弃了前置声明,直接在A中引入B的头文件,一次性将B的全部声明引入到A的头文件中。

这样也可以编译通过,但是会带来一个问题。就是以后所有引用A的头文件的其他文件,在编译的时候都需要编译一遍B,而在这些文件中可能压根就没有访问成员变量b。也就是说这些文件其实根本就不需要B的定义。这样会导致编译这些文件的时候,还需要编译一遍不相关的类B。这会导致编译缓慢

所以,这里面正确的做法是采用前一种方法。也就是说,头文件中能尽量少的引入头文件就应该尽量少的引用收文件,能用前置声明的就应该尽量使用前置声明

几种可以使用前置声明的 地方

1.以指针或引用的形式来引用类型

class A;
class B
{
    A *pa;//ok
    A &ra;//ok

    A * f(const A * pa);//ok
    A & f(const A & pa);//ok
};

2.友元

class A;
class B
{
    friend A; //ok
    friend class A;//ok
};

3.作为返回值或者参数

class A;
class B
{
    void f(A a);//ok

    A g();//ok
};

不可以使用前置声明的类型
1.使用完整的类引用

class A;
class B
{
    //A a; //error
    
};

2.被当作父类

class E;
class F: public E  //error VC6.0--'E': base class undefined  Qt5.6-invalid use of incomplete of 'class C'
{};

使用前置声明和没有使用前置声明的编译过程及其差异

假设有两个类 A 和 B,A 类包含了 B 类的一个指针成员,并且在 A.cpp 文件中使用了 B 类的成员函数。我们将比较两种情况下的编译过程和差异。

情况1:使用前置声明
A.h

#ifndef A_H
#define A_H

class B; // 前置声明

class A {
public:
    A();
    void doSomethingWithB();
private:
    B* ptrB; // 使用B类的指针成员
};

#endif // A_H

A.cpp

#include "A.h"
#include "B.h" // 包含B类的头文件

A::A() {
    ptrB = new B(); // 使用B类的指针成员
}

void A::doSomethingWithB() {
    ptrB->someFunction(); // 调用B类的成员函数
}

情况2:没有使用前置声明

A.h

#ifndef A_H
#define A_H

#include "B.h" // 直接包含B类的头文件

class A {
public:
    A();
    void doSomethingWithB();
private:
    B* ptrB; // 使用B类的指针成员
};

#endif // A_H

A.cpp

#include "A.h"

A::A() {
    ptrB = new B(); // 使用B类的指针成员
}

void A::doSomethingWithB() {
    ptrB->someFunction(); // 调用B类的成员函数
}

编译过程及差异:

  • 使用前置声明(情况1):

    • A.h 中只有 B 类的前置声明,A.cpp 中包含了 A.h B.h
    • 编译器在编译 A.h 时会知道 B 类的存在,但不知道 B 类的具体内容。编译 A.h 时不需要 B 类的完整定义,只需要知道它是一个类的声明即可。
    • 如果 B 类的成员函数在 A.cpp 中被调用,则编译器需要知道这些函数的声明,因此需要在 A.cpp 中包含 B.h
    • 编译器将编译 A.cpp,生成目标文件 A.o
  • 没有使用前置声明(情况2):

    -A.h直接包含了 B.h,因此 A.h A.cpp 都会有 B 类的完整定义。

    • 在编译 A.cpp 时,编译器会将 B 类的完整定义和 A 类的定义都放入编译过程中。
    • 如果 B 类的头文件发生变化,会导致包含了 B.h 的文件都需要重新编译。
    • 编译器将编译 A.cpp,生成目标文件 A.o。

差异:

  • 使用前置声明:减少了对 B 类的完整定义的依赖,只需要类的声明即可,从而减少了编译时间。但需要在使用 B 类成员函数时,在 A.cpp 中包含 B.h。
  • 没有使用前置声明:导致 A.h 中包含了 B.h 的完整定义,增加了对 B 类的依赖,编译时间可能更长,并且如果 B.h 发生变化,可能会导致更多文件重新编译。

原文链接:https://blog.csdn.net/qq_42681507/article/details/130921276
原文地址:https://blog.csdn.net/xueruifan/article/details/50569639

文章来源:https://blog.csdn.net/qq_40178082/article/details/135083110
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。