解析C++多文件编程问题

2021-10-26

目录
  • 一、多文件编程是什么
  • 二、如何防治头文件被重复引入
    • 1. 使用宏定义避免
    • 2. 使用 #pragma once 避免
    • 3. 使用 _Pragma 操作符
  • 三、命名空间如何应用在多文件编程中
    • 四、const常量如何在多文件编程中使用
      • 1. 将 const 常量定义在 .h 头文件中
      • 2. 借助 extern 先声明再定义 const 常量
      • 3. 借助 extern 直接定义 const 常量
    • 五、多文件项目如何用 g++ 命令执行
      • 六、多文件编程的底层原理

        一、多文件编程是什么

        为了方便后期的维护,分散代码应遵循一个基本原则:实现相同功能的代码应存储在一个文件中。

        C++ 代码文件根据后缀名的不同,大致可以分为如下几类:

        .h:头文件
        .hpp:头文件,header plus plus 的缩写,混杂着 .h 的声明 .cpp 的定义,OpenCV 采用
        .cpp:源文件,windows
        .cc:源文件,Unix/Linux

        对于一些系统提供的库,出于版权和保密考虑,大多是已经编译好的二进制文件,可能仅包含 .h 文件。

        // student.h
        class Student{
            // ...
        };
        // student.cc
        #include "sudent.h"
        // Student 定义
        // main.cc
        #include "student.h"
        
        int main(){
            // ...
        }

        二、如何防治头文件被重复引入

        1. 使用宏定义避免

        #ifndef _NAME_H
        #define _NAME_H
        
        //头文件内容
        
        #endif

        _NAME_H 是宏的名称。需要注意的是,这里设置的宏名必须是独一无二的,不要和项目中其他宏的名称相同。

        // student.h
        #ifndef _STUDENT_H
        #define _STUDENT_H
        class Student{
            // ...
        };
        #endif

        2. 使用 #pragma once 避免

        使用 #pragma one 指令,将其附加到指定文件的最开头位置,则该文件就只会被 #include 一次。

        #ifndef 是通过定义独一无二的宏来避免重复引入的,这意味着每次引入头文件都要进行识别,所以 效率不高。但考虑到 C 和 C++ 都支持宏定义,所以项目中使用 #ifndef 规避可能出现的“头文件重复引入”问题,不会影响项目的可移植性。

        #pragma once 不涉及宏定义,当编译器遇到它时会立刻知道当前文件只引入一次,所以效率很高。但值得一提的是,并不是每个版本的编译器都能识别 #pragma once 指令,一些较老版本的编译器就不支持该指令(执行时会发出警告,但编译会继续进行),即 #pragma once 指令的兼容性不是很好。

        #pragma once 只能作用于某个具体的文件,而无法向 #ifndef 那样仅作用于指定的一段代码。

        #pragma once
        class Student{
            // ...
        };

        3. 使用 _Pragma 操作符

        _Pragma 操作符可以看做是 #pragma 的增强版,不仅可以实现 #pragma 所有的功能,还能和宏搭配使用。

        这里仅介绍用 _Pragma 操作符避免头文件重复引入。

        当处理头文件重复引入问题时,可以将如下语句添加到相应文件的开头:

        _Pragma("once")

        _Pragma("once");
        class Student{
            // ...
        };

        在某些场景中,考虑到编译效率和可移植性,#pragma once 和 #ifndef 经常被结合使用来避免头文件被 重复引入。比如说:

        #pragma once
        #ifndef _STUDENT_H
        #define _STUDENT_H
        class Student{
            // ...
        };
        #endif

        当编译器可以识别 #pragma once 时,则整个文件仅被编译一次;反之,即便编译器不识别 #pragma once 指令,此时仍有 #ifndef 在发挥作用。

        三、命名空间如何应用在多文件编程中

        当进行多文件编程时,命名空间常位于 .h 头文件中。

        // student_li.h
        #ifndef _STUDENT_LI_H
        #define _STUDENT_LI_H
        namespace Li{
            class Student{
                // ...
            };
        }
        #endif
        // student_li.cc
        #include "student_li.h"
        #include <iostream>
        void Li::Student::display(){
        
        }
        // student_han.h
        #ifndef _STUDENT_HAN_H
        #define _STUDENT_HAN_H
        namespace Han{
            class Student{
                // ...
            };
        }
        #endif
        // student_han.cpp
        #include "student_han.h"
        #include <iostream>
        void Han::Student::display(){}

        注意,当类的声明位于指定的命名空间中时,如果要在类的外部实现其成员方法,需同时注明所在命名空间名 和类名(例如本项目中的 Li::Student::display() )。

        四、const常量如何在多文件编程中使用

        用 const 修饰的变量必须在定义的同时进行初始化操作(除非用 extern 修饰)

        C++ 中 const 关键字的功能有 2 个,除了表明其修饰的变量为常量外,还将所修饰变量的可见范围 限制为当前文件。这意味着,除非 const 常量的定义和 main 主函数位于同一个 .cpp 文件,否则该 const 常量只能在其所在的 .cpp 文件中使用。

        那么,如何定义 const 常量,才能在其他文件中使用呢?

        1. 将 const 常量定义在 .h 头文件中

        // demo.h
        #ifndef _DEMO_H
        #define _DEMO_H
        const int num = 10;
        #endif
        // main.cc
        #include <iostream>
        #include "demo.h"
        int main(){
            std::cout << num << std::endl;
            return 0;
        }

        2. 借助 extern 先声明再定义 const 常量

        借助 extern 关键字,const 常量的定义也可以遵循“声明在 .h 文件,定义在 .cpp 文件”。

        // demo.h
        #ifndef _DEMO_H
        #define _DEMO_H
        extern const int num;   // 声明 const 常量
        #endif
        // demo.cc
        #include "demo.h"
        const int num = 10;
        // main.cpp
        #include <iostream>
        #include "demo.h"
        int main(){
            std::cout << num << std::endl;
            return 0;
        }

        3. 借助 extern 直接定义 const 常量

        C++ 编译器在运行项目时,会在预处理阶段直接将 #include 引入的头文件替换成该头文件中的内容(复制粘贴),因此可以对上节代码做修改:

        // demo.cpp
        extern const int num = 10;
        // main.cpp
        #include <iostream>
        extern const int num;
        int main(){
            std::cout << num << std::endl;
            return 0;
        }

        五、多文件项目如何用 g++ 命令执行

        在 Linux 平台上,虽然也有很多可用的 C++ IDE,但执行 C++ 程序更常采用的方式是使用 g++ 命令。

        除此之外,Linux 平台还经常编写 makefile 来运行规模较大的 C++ 项目。

        C++ 程序的执行过程分为 4 步,依次是预处理、编译、 汇编和链接。在执行 C++ 项目时,头文件是不需要经历以上这 4 个阶段的,只有项目中的所有源文件才必须经历这 4 个阶段。

        假设有这个一个 C++ 项目

        // studetn.h
        class Student{
            // ...
        };
        // student.cc
        #include <iostream>
        #include "student.h"
        void Student::say(){
            std::cout << name << "的年龄是" << age << ",成绩是" << score << std::endl;
        }
        // main.cc
        #include "student.h"
        int main(){
            Student *pStu = new Student;
            // ...
            delete pStu;
            return 0;
        }

        预处理阶段,执行如下命令:

        [root@bogon ~]# g++ -E main.cc -o main.i
        [root@bogon ~]# g++ -E student.cc -o student.i

        • -E 选项用于限定 g++ 编译器只进行预处理而不进行后续的 3 个阶段;
        • -o 选项用于指定生成文件的名称。

        编译阶段,进一步生成响应的汇编代码文件:

        [root@bogon ~]# g++ -S main.i -o main.s
        [root@bogon ~]# g++ -S student.i -o student.s

        • -S 选项用于限定 g++ 编译器对指定文件进行编译,得到的汇编代码文件通常以“.s”作为后缀名。

        将汇编文件转换成可执行的机器命令:

        [root@bogon ~]# g++ -c main.s -o main.o
        [root@bogon ~]# g++ -c student.s -o student.o

        • 如果不用 -o 指定可执行文件的名称,默认情况下会生成 a.out 可执行文件。Linux 系统并不以文件的扩 展名开分区文件类型,所以 a.out 和 student.exe 都是可执行文件,只是文件名称有区别罢了。

        最终执行:

        [root@bogon ~]# ./student.exe

        从头开始直接生成可执行文件:

        [root@bogon ~]# g++ main.cpp student.cpp -o student.exe

        六、多文件编程的底层原理

        实际上,在编译阶段,编辑器会对源文件生成一个符号表,源文件中看不到的定义的符号就会存在这个表中。在链接阶段,编译器会在别的目标文件中寻找这个符号的定义,如何没有找到,会出现链接错误。

        定义,指的是就是将某个符号完整的描述清楚,它是变量还是函数,变量类型以及变量值是多少,函数的参数有哪些以及返回值是什么等等;而“声明”的作用仅是告诉编译器该符号的存在,至于该符号的具体的含义,只有等链接的时候才能知道。

        C++ 中一个符号允许被声明多次,但只能被定义一次。

        基于声明和定义的不同,才有了多文件编程的出现。

        所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码,唯一的区别在于头文件不 用被编译。我们把所有的函数声明全部放进一个头文件中,当某一个 .cpp 源文件需要时,可以通过 #include 宏命令直接将头文件中的所有内容引入到 .cpp 文件中。这样,当 .cpp 文件被编译之前(也就是预处理阶段),使用 #include 引入的 .h 文件就会替换成该文件中的所有声明。

        通常一个头文件的内容会被引入到多个不同的源文件中,并且都会被编译,所以头文件中一般只存放变量或者函数的声明,不要放定义。但存在3种情况属于定义范畴,但应该放在 .h 文件种:

        • 头文件中定义 const 对象、static 对象头文件中定义内联函数,编译器必须在编译时就找到内联函数的完成定义头文件中可以定义类。
        • 类的内部通常包含成员变量和成员函数,成员变量是要等到具体的对象被创建时才会被定义(分配空间),但成员函数却是需要在一开始就被定义的,这也就是类的实现。
        • 通常的做法是将类的定义放在头文件中,而把成员函数的实现代码放在一个 .cpp 文件中。也可以直接实现在类内,作为内联函数。

        到此这篇关于C++多文件编程的文章就介绍到这了,更多相关C++多文件编程内容请搜索北冥有鱼以前的文章或继续浏览下面的相关文章希望大家以后多多支持北冥有鱼!

        《解析C++多文件编程问题.doc》

        下载本文的Word格式文档,以方便收藏与打印。