关于C++ .h文件和.cpp文件的知识梳理

1. C++程序的编译过程

C++程序的编译过程可以分为四个步骤,分别为预处理, 编译汇编链接。他们的工作内容分别为:

  • 预处理:展开头文件,去除源代码中的注释等。
  • 编译: 将预处理后的程序转换成汇编代码。
  • 汇编: 将汇编代码进一步转换成二进制机器码。
  • 链接:将多个目标文件及其所需的库文件链接生成可执行文件。

其中我们关注最多的是 预处理链接,头文件和预处理息息相关,而声明与定义的分离则和链接有着密切关系。

2. #pragma once 有什么作用

我们都知道 #pragma once 还有类似" #ifndef “、” #endif "的作用都是保护你的头文件只被目标文件加载一次,但这个保护的作用能有多大呢?首先我们需要知道头文件的作用,#include 一个文件(无论.h或是.cpp),仅仅是将文件中的代码复制一份到你的代码之中,头文件(.h)仅在编译过程中的预处理中起到作用。 知道了这个后我们便可以开始进行一些测试了。

测试样例一

代码:

//head.h
int a = 10;

//main.cpp
#include <iostream>
#include "head.h"
#include "head.h"//重复导入
int main()
{
    std::cout<<"a = "<<a<<std::endl;
    std::cout<<"a's address = "<<&a<<std::endl;
    return 0;
}

执行结果:

In file included from C:head.h:2:5: error: redefinition of 'int a'
 int a = 10;
     ^
In file included from C:head.h:2:5: note: 'int a' previously defined here
 int a = 10;

结果报错了,编译器提示错误,重复定义了int a变量,这是因为我们#include “head.h” 两次,相当于在我们的main.cpp中加了两行int a = 10 。我们在head.h文件中加上#pragma once 可以再运行试试。

//head.h
#pragma once
int a = 10;
//main.cpp
内容不变

执行结果:

a = 10
a's address = 0x40a010

程序编译通过了,因为#pragma once 让头文件只被导入了一次,但一个头文件往往不止会被一个源文件include,也有可能被多个文件include,到时候我们的写法还对吗?#pragma once 还能保证我们的程序安全,不会产生重复定义吗?我们来看接下来的示例。

测试样例2

代码:

//head.h
#pragma once
#include <iostream>

int a = 10;

void fun1();
void fun2();

//fun1.cpp
#include "head.h"

void fun1()
{
    std::cout<<"this is fun1"<<std::endl;
    std::cout<<"a = "<<a << std::endl;
    std::cout<<"a's = "<<&a <<std::endl;
}
//fun2.cpp
#include "head.h"

void fun2()
{
    std::cout<<"this is fun2"<<std::endl;
    std::cout<<"a = "<<a << std::endl;
    std::cout<<"a's = "<<&a <<std::endl;
}
//main.cpp
#include "head.h"

int main()
{
    fun1();
    fun2();
    return 0;
}

执行结果:

C:head.h:4: multiple definition of `a'
C:head.h:4: first defined here
C:head.h:4: multiple definition of `a'
C:head.h:4: first defined here

结果编译器报错了,因为在fun1.cpp 和fun2.cpp中对变量 a 进行了重复的定义,显然当有多个文件的时候#pragma once 并不能保证我们的程序中只include 该文件一次。也正是因为这个原因,才会有我们后面的规定–仅在.h 文件中进行变量和函数的声明而不进行定义(存在一些特例,比如class 的内敛成员函数,模板函数), 因为函数与变量的声明可以进行任意多次,而定义只能有一次。

总结:#pragma once 仅能保证在单一文件中对于头文件的include只会进行一次。

3. 在头文件中定义static变量和const常量

static 关键字作用于变量时无非两个作用,一个是延长变量的生命周期,另一个是限定变量仅能在定义的该变量的文件中使用。如果我们在头文件中定义static 变量会发生什么?还有很多人会喜欢在头文件中加一些const 常量,这些都是一些错误的用法。

代码:

//head.h
#pragma once
#include <iostream>

const int a = 10;
static int s = 9;

void fun1();
void fun2();
//fun1.cpp
#include "head.h"

void fun1()
{
    std::cout<<"this is fun1:"<<std::endl;
    std::cout<<"a = "<<a <<" a's = "<<&a << std::endl;

    s = 99;
    std::cout<<"s = "<<s <<" s's = "<<&s <<std::endl;
}
//fun2.cpp
#include "head.h"

void fun2()
{
    std::cout<<"this is fun2:"<<std::endl;
    std::cout<<"a = "<<a <<" a's = "<<&a << std::endl;
    std::cout<<"s = "<<s <<" s's = "<<&s <<std::endl;
}
//main.cpp
#include "head.h"

int main()
{
    fun1();
    fun2();
    return 0;
}

执行结果:

this is fun1:
a = 10 a's = 0x40b000
s = 99 s's = 0x40a010
this is fun2:
a = 10 a's = 0x40b030
s = 9 s's = 0x40a020

首先我们将该代码和上一节的代码比较发现,在头文件中定义一般的全局变量int 无法通过编译,而const int 和static int 都通过了编译,其中const int 通过编译是出乎我意料的,因为static 屏蔽其他文件对自己的变量访问,通过编译能够理解。事实上const int 通过编译的情况仅是在c++ 编译器下,在c编译器下,则是无法通过编译的。
尽管如此我们通过观察执行结果能够发现,fun1 和 fun2 中变量的无论是a 还是s 都不是同一变量,所以这种写法虽然能够通过编译,但会极大的浪费内存空间,而且执行结果可能还会与你的预期大相径庭。

总结:在头文件中定义const 和static 变量是能够通过编译的,但这样做会导致每一个include 该头文件的源文件都会定义一个该变量,造成内存的浪费。

4.正确声明和定义变量的方法–extern

函数声明和定义的分离十分简单,我们只需要在.h文件中写出函数返回值类型函数名、接受的参数,再到.cpp文件中去写函数的具体实现就行了。而变量的声明和定义的分离就要靠关键字extern 了。extern 主要有两个作用,其中一个是对指定单元强制使用C 的编译器进行编译,另一个便是声明变量的作用范围的关键字,其声明的函数和变量可以在本编译单元或其他编译单元中使用。 规范声明和定义变量的形式应该如下所示:

代码:

//head.h
#pragma once
#include <iostream>

extern int a;
extern const std::string str;

void fun1();
void fun2();

//head.cpp
#include "head.h"

int a = 10;
const std::string str = "hello world";

//fun1.cpp
#include "head.h"

void fun1()
{
    std::cout<<"this is fun1:"<<std::endl;
    std::cout<<"a = "<<a <<" a's address= "<<&a << std::endl;
    std::cout<<"str = "<<str<<" str's address= "<< &str<<std::endl;
    a = 99;
}

//fun2.cpp
#include "head.h"

void fun2()
{
    std::cout<<"this is fun2:"<<std::endl;
    std::cout<<"a = "<<a <<" a's address= "<<&a << std::endl;
    std::cout<<"str = "<<str<<" str's address= "<< &str<<std::endl;

}

//main.cpp
#include "head.h"

int main()
{
    fun1();
    fun2();
    return 0;
}

执行结果:

this is fun1:
a = 10 a's address= 0x40a010
str = hello world str's address= 0x410060
this is fun2:
a = 99 a's address= 0x40a010
str = hello world str's address= 0x410060

总结:当你需要定义一个变量给所有include 该头文件的单元共同使用时,你应该在.h文件中使用extern 关键字,表示该变量将在include这个头文件的编译单元中使用(即进行“声明”)。同时另外新建一个.cpp文件对该头文件中声明的变量进行统一定义(定义变量的文件实际上可以是任意include该头文件的.cpp文件,但为了避免混乱,我们通常约定是在和头文件同名的.cpp文件中进行定义,可以认为是一种规范)。

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值