博客主页 😁
CPP学习DAY5
CPP学习DAY5

Author:

ChaunceyCHI

©

Wordage:

共计 6807 字

needs:

约 9 分钟

Popular:

234 ℃

Created:

目 录

头文件(Head files)

头文件是什么?我们为什么需要它们?为什么它们在 C++ 中存在?

你或许已经习惯了很多其他语言,比如 java 或者 c#,它们实际上没有头文件这个概念,也没有存在两种不同的文件类型的概念:一种是我们的编译文件,比如说cpp文件,也就是一个翻译单元,还有一种就是头文件,一个奇怪的文件。我们经常到处 include 它们,为什么它会存在?

头文件实际上很有用,而且它们的用途不只是声明某种声明,以供你在多个CPP中使用而已。随着学习的继续,我们将要学习很多新概念,都需要头文件才能正常工作。

在c++基础中,头文件传统上是用来声明某些函数类型,以便可以用于整个程序中。

回想一下之前谈到的的c++编译器和链接器,为了让我们知道什么函数和类型存在,我们需要某种声明。比如说,我们在一个文件中创建函数,然后想要在另一个文件中使用,当我们尝试编译时那个文件时,C++甚至都不会知道它的存在,所以我们需要一个共同的地方来存放声明,而非定义。因为,记住,我们只能定义函数一次。我们需要这么一个共同的地方存放函数声明,只是声明,而没有实际的定义,没有函数的主体,只是一个地方说“嘿,这个函数存在!"

假设在文件 Main.cpp 中我有个函数叫log,它将打印信息到控制台,它接受一个 const char "message",然后单纯cout该信息。

#include <iostream>

void Log(const char* message)
{
    std::cout << message << std::endl;
}

int main()
{
    std::cout << "Hello world!" << std::endl;
    std::cin.get();
}

如果我继续创建另一个文件,我们将之命名为 log.cpp,然后也许这个文件有个什么要用到我们到 Log 的,并且会打印一句话到控制台

void InitLog()
{
    Log("hello hello hello");
}

我们将会得到一个错误:这个 Log 函数并不存在于该文件,该文件不知道 Log 函数的存在,如果回到 main 文件中,Log 函数就在这里。如果如果我按CTRL加f7试图编译我们的 Main.cpp,可以看到编译成功,没有错误然后回到 Log.cpp,如果我们试图编译这个文件,我们获得一个错误,因为显然对于这个文件来说,Log 函数不存在
所以 Log.cpp 到底需要什么东西才能不会有错误?我们该如何告诉它,Log 这个函数存在,只是它定义于别的什么地方?这就是函数声明用的用武之地!

如果我们回到我们的 Log.cpp 代码这里,只需要简单的声明,Log 函数存在。
如果我们回到main函数,然后看一下这个实际的签名,可以看到 Log 是一个返回 void、接受一个参数——也就是一个 const char 指针,所以这是函数签名,我们可以直接复制这个,回到 Log.cpp,粘贴进来,然后用一个分号结束它,就像下面这样:

void Log(const char* message);
void InitLog()
{
    Log("hello hello hello");
}

这个函数没有主体,正是说明这是一个函数的声明,我们还没有定义这个函数到底是什么样子,这个函数到底做什么,我们只是说,嘿,这边有一个函数叫 Log,返回类型是 void,接受一个 const char*,这个函数存在
这时就可以看到我们的 Intellisence 的错误已经消失了,如果我按 control 加 f7 ,我们可以成功编译,如果我们右击 Hello world,然后点 build 来 build 我们的程序,可以看到他也链接成功,因为这是可以的,他找到了那个log函数。这样我们就找到了一个告诉 Log.cpp 那个 log 函数存在于某处的办法。

那如果我创建另外一个文件呢?如果我创建了一个别的文件,然后需要用这个 log 函数呢?
这意味着我也需要把这个 void log 声明到处复制粘贴吗?嗯,答案是是的,你确实需要这么做,但是呢,有一个办法可以让这一切简单一点,那就是使用头文件。

头文件到底是什么?

因为这是 C++,你可以做任何事情,所以头文件一般是那种被 include 到 cpp 文件里的,基本上我们做的就是,把头文件里的内容 copy and paste 到 cpp 文件里,我们通过 #include 这个 pre processor 语句来实现,所以 #Include 有从文件复制粘贴到其他文件的能力,这正是我们需要在这里做的。我们需要把这个 Log 的声明,复制粘贴到任何我们需要用这个 Log函数的地方,所以让我们去创建一个头文件。我将右击 header files ,请注意这些文件夹其实是过滤器,它们不是真的文件夹,我也可以在 source files 底下创建一个头文件,将来可能也会有一些教程,关于 visual studio 是怎么工作的,但现在我们需要知道的就是,无论在什么地方右击创建头文件都没关系,我将在头文件上面创建,因为这更合理一点,但实际上其实无所谓,我将创建一个头文件叫 Log.h:

#pragma once
void Log(const char* message);

你会意识到它自动给你插入了一些代码,叫 #pragma once,我们等会很快会介绍他,在这里我将存放我们的声明,我将从 Log.cpp 把这个Log函数剪切,放入我们的头文件,现在呢,我们的头文件 Log.h,我可以在任何想用 Log 函数的地方 include 它,它会帮我们做那些我们不想人工做的事情,也就是复制粘贴这个到任何我们需要它的所有文件里。我不想自己去做这个复制粘贴的工作,所以就找了一个某种程度上让它看起来整洁一点,自动化一点的办法。回到 Log.cpp,你会看到我们也得到了一个错误,因为我们其实没有声明这个函数。但如果我输入 #include “log.h":

#include "log.h"
void InitLog()
{
    Log("hello hello hello");
}

看哪,我们没有任何错误,我们的文件可以编译。我们其实还可以做的是把它 include 到 Main.cpp,Main.cpp 包含实际的函数定义,所以其实他并不需要他,我们反正都可以调用Log,只是为了让你知道,我们把它include进来也不会造成什么问题,然后编译,这是完全没有问题的。
回到 Log.cpp,你可以看到我们定义了这个超屌的函数 InitLog,然后除了 Log.cpp,没人知道它,如果想从Main里面使用它,我需要拿到它的声明,如果我在这里调用 InitLog:

#include <iostream>
void InitLog(const char* message)
{
    std::cout << message << std::endl;
}
int main()
{
    std::cout << "Hello world!" << std::endl;
    std::cin.get();
}

我们会获得一个错误,这是因为他没有在任何地方被声明,所以让我们去 Log.h 把它的函数签名加进来,就像这样:

#pragma once
void Log(const char* message);
void InitLog();

要确保它和cpp文件里的实际签名一致,所以现在一切都看起来很好,然后我要去把这个 Log 函数拿到这个 Log.cpp 来,因为这样看起来更合理一点:

#include <iostream>
#include "log.h"

void InitLog(const char* message)
{
    std::cout << message << std::endl;
}

int 
main()
{   InitLog();
    std::cout << "Hello world!" << std::endl;
    std::cin.get();
}

回到Main.cpp,如果运行我们的程序,看我们实际上初始化了 Log 并打印了 Hello world! 到控制台。
我们回到那个头文件,看一下那个pragma once到底是什么。这里我们有一个语句是看起来是visual studio给我们生成的,叫#pragma once,这是什么? 任何以一个井号开头的语句都被称为预处理命令,或者叫预处理指令。也就意味着他将在这个文件被编译之前被c++的pre processor评估,pragma其实是一个被输入到编译器或者说预处理器的指令,pragma once其实意思就是说只include这个文件一次,Pragma once, 是一种被称为header guard(头文件保护符),他所做的就是防止我们把单个头文件多次include到一个单一翻译单元里,现在我说话其实很小心的,你得明白,他其实不会防止我们把头文件include到整个程序的各处,只是防止include到一个翻译单元里,也就是一个单独的cpp文件,原因是如果我们不小心把一个文件多次include到一个翻译单元里,我们会得到一个重复的错误,因为我们会多次复制和粘贴那个头文件。

一个比较好的办法来示意这种情况是我们创建一个结构体(struct)。比如说如果我在这里创建一个结构体叫做player:

// #pragma once
void Log(const char* message);
void InitLog();

struct player {};

我可以就让他空着,没关系,如果我将这个文件两次 include 到一个翻译单元,并且没有头文件保护符,他会真的include这个文件两次,也就是我会有两个结构体,他们有相同的名字—player,我们可以通过把这个pragma once注释掉来看一下这个例子。回到 Main.cpp,include Log.h两次:

#include <iostream>
#include "log.h"
#include "log.h"  //注意这里

int main()
{
    InitLog();
    std::cout << "Hello world!" << std::endl;
    std::cin.get();
}

这时如果我试图编译我的文件,你可以看到我们拿到了一个 player structure 重复定义的错误,因为我们在重新定义这个 player struct,而我们只能定义一个叫 player 的 struct,struct 需要唯一的名字。

所以你会问我为什么要include一个文件两次?这又回到了include到底是怎么工作的?记得吗,include的工作方法就是从一个文件复制粘贴到另一个文件,也就是说你可能会有一个一连串的 include,所以你可能有一个头文件叫player,他会include log,然后player又被include到其他的文件,然后可能那第3个文件会包含所有的include,所以如果我创建一个新的头文件,叫common,Common将会包含一些常用的 includes,比如它会include log。如果我回到log.h,确保program once被注释掉了,然后在log.cpp,我include log.h和common.h,如果我编译我的文件,猜猜会怎样?我仍然拿到那个错误,因为那个struct player被重新定义了,如果我们要解析预处理器到底做了什么,你可以看到他其实有把 Log include两次,然而回到Log.h,如果我们把注释去掉,并试图编译我们的文件,将不会有任何报错,因为它认识到log已经被include了,他不会第2次再include它。

还有另外一种方式来添加一个头文件保护符,用传统的办法添加头文件保护符,其实是通过 #ifdef。我们可以做的就是输入#ifndef,然后我们可以来检查某种符号,比如说_log_h,然后我们会定_log_h,然后在我们代码的最后,输入 endif,这个将要做的就是首先他会检查这个log h这个符号是否存在,如果没有被定义,他将把以下代码include到编译里,如果这个被定义了,那这些都不会被include,会全部被禁用,当我们通过了,第1个条件检查,我们会定义log h,也就是说下一次我们运行这段代码时,它是已经被定义的状态,所以不会再重复。

最简单的办法来描述这个东西就是复制粘贴整个文件,到我们的log.cpp文件,如果我们看一下它,我也把log.h注释掉,以及common.h,所以你们看在这里,第1次时,这一切都OK,他include了这个文件,一切正常,但第2次,他显示为灰色,因为log_h已经被定义了,所以这种形式的头文件保护符在过去经常使用,然而我们现在有这个新的预处理语句叫pragma once,所以我们现在大多数用它。从某种意义上来说,其实你用哪种都无所谓,但pragma once看起来更干净,所以在行业里这也是绝大部分人所用的,基本上现在所有的编辑器都支持pragma once,所以并不是只是适用于vs,gcc、clang、msbc,它们都支持pragma once。

话说回来,如果你有看到老代码,或者别人写过到不同风格的代码,你会碰到这个ifndef头文件保护符,所以需要知道一下它是啥。

你可能会发现,include语句间有一些差异,有些include语句用引号,有些include语句用方括号,所以到底啥情况?其实很简单,他们代表两件事,当我们编译我们的程序时,我们有办法告诉编译器某些include路径,这基本上就是在我们电脑里通往文件夹的路径,它们包含include文件。如果我们想要include的那个文件,在这些文件夹其中之一的话,我们可以用尖括号去让编译器在所有include路径里,去搜索这个文件,而引号用于include文件存在于该文件的相对位置,举例来说,如果我有一个文件叫做log.h,它存在于目前这个log.cpp文件的上一级目录,我可以用../来返回,而这会返回上一级目录,因为这是相对于本文件的位置,而对于方括号来说,永远没有说这个文件的相对位置,他们必须存在于所有include目录的某一个里,我们会在以后的学习中涉及到怎么设置include目录和这所有东西,但现在不需要涉及这么复杂,但这基本上就是他的工作原理。引号也可以用于include目录的位置,所以你其实可以在任何地方用引号,我可以把 iostream 这个尖括号换成引号,将完全没问题,所以尖括号只用于编译器的include路径,引号用于所有的路径。

最后一件事,你可能发现这个iostream看起来不像个文件,因为它没有扩展名,就叫iostream,这是怎么一回事呢?其实,它是个文件,只是没有扩展名,写标准库的人决定这么干的。为了区分c的标准库和c++标准库,c标准库里的头文件一般都有.h扩展名,而c++没有,所以这也是一个区分哪些属于c标准库,哪些属于c++标准库的办法,就是是否有.h扩展名。iostream跟其他东西一样,就是个文件,其实,在vs里,右击它,点击打开文件,你可以看到它带领我们来到这个iostream头文件如果我们在这个标签上右击,选择打开包含文件夹,可以看到它其实位于我们的计算机中某处。

这就是关于头文件的一些事情。

文章二维码
CPP学习DAY5
共计 0 条评论,点此发表评论
博客主页 CHI's blog 今春不见桃花
闽ICP备2022003806号 闽公网安备35012102500456号 本站由又拍云提供CDN加速/云存储服务 本站已运行 1 年 246 天 21 小时 31 分 自豪地使用 Typecho 建站,并搭配 MyDiary 主题 Copyright © 2022 ~ 2024. CHI's blog All rights reserved.
打赏图
打赏博主
欢迎
欢迎
欢迎访问CHI's blog
欢迎您来评论,但首次评论需经过审核才能显示,之后就不用啦^_^
搜 索
足 迹
分 类
  • 默认分类
  • 相册
  • 随想录
  • 技术向
  • 读书笔记
  • 生活小记