精华内容
下载资源
问答
  • 头文件和源文件的用法
    2019-05-14 00:29:54

    头文件和源文件分离

    概览

     非模板类型(none-template)模板类型(template)
    头文件(.h)1. 全局变量申明(带 extern 限定符)
    2. 全局函数的申明
    3. 带 inline 限定符的全局函数的定义
    4. 类的定义
    5. 类函数成员和数据成员的申明(在类内部)
    6. 类定义内的函数定义(相当于 inline )
    7. 带 static const 限定符的数据成员在类内部的初始化
    8. 带 inline 限定符的类定义外的函数定义
    1. 带inline限定符的全局模板函数的申明和定义
    2. 模板类的定义
    3. 模板类成员的申明和定义(定义可以放在类内或者类外,类外不需要写 inline)
    实现文件(.cpp)1. 全局变量的定义(及初始化)
    2. 全局函数的定义
    3. 类函数成员的定义
    4. 类带 static 限定符的数据成员的初始化
    (无)

    *申明:declaration *定义:definition

    头文件

    头文件的所有内容,都必须包含在

    #ifndef {Filename} 
    #define {Filename} 
    
    //{Content of head file} 
    
    #endif
    

    这样才能保证头文件被多个其他文件引用(include)时,内部的数据不会被多次定义而造成错误

    inline 限定符

    在头文件中,可以对函数用inline限定符来告知编译器,这段函数非常的简单,可以直接嵌入到调用定义之处。

    当然inline的函数并不一定会被编译器作为inline来实现,如果函数过于复杂,编译器也会拒绝inline。

    因此简单说来,代码最好短到只有3-5行的才作为inline。有循环,分支,递归的函数都不要用做inline。

    对于在类定义内定义实现的函数,编译器自动当做有inline请求(也是不一定inline的)。因此在下边,我把带有inline限定符的函数成员和写在类定义体内的函数成员统称为“要inline的函数成员”

    非模板类型

    全局类型

    就像前面笼统的话讲的:申明写在.h文件。

    对于函数来讲,没有实现体的函数,就相当于是申明;而对于数据类型(包括基本类型和自定义类型)来说,其申明就需要用extern来修饰。

    然后在.cpp文件里定义、实现或初始化这些全局函数和全局变量。

    不过导师一直反复强调:不许使用全局函数和全局变量。用了之后造成的后果,目前就是交上去的作业项目会扣分。当然不能用自有不能用的理由以及解决方案,不过不在目前的讨论范围内。

    自定义类型

    对于自定义类型,包括类(class)和结构体(struct),它们的定义都是放在.h文件中。其成员的申明和定义就比较复杂了,不过看上边的表格,还是比较清晰的。

    函数成员

    函数成员无论是否带有static限定符,其申明都放在.h文件的类定义内部。

    对于要inline的函数成员其定义放在.h文件;其他函数的实现都放在.cpp文件中。

    数据成员

    数据成员的申明与定义都是放在.h文件的类定义内部。对于数据类型,关键问题是其初始化要放在什么地方进行。

    对于只含有static限定符的数据成员,它的初始化要放在.cpp文件中。因为它是所有类对象共有的,因此必须对它做合适的初始化。

    对于只含有const限定符的数据成员,它的初始化只能在构造函数的初始化列表中完成。因为它是一经初始化就不能重新赋值,因此它也必须进行合适的初始化。

    对于既含有static限定符,又含有const限定符的数据成员,它的初始化和定义同时进行。它也是必须进行合适的初始化

    对于既没有static限定符,又没有const限定符的数据成员,它的值只针对本对象可以随意修改,因此我们并不在意它的初始化什么时候进行。

    模板类型

    C++中,模板是一把开发利器,它与C#,Java的泛型很相似,却又不尽相同。以前,我一直只觉得像泛型,模板这种东西我可能一辈子也不可能需要使用到。但是在导师的强制逼迫使用下,我才真正体会到模板的强大,也真正知道要如何去使用模板,更进一步是如何去设计模板。不过这不是三言两语可以讲完的,就不多说了。

    对于模板,最重要的一点,就是在定义它的时候,编译器并不会对它进行编译,因为它没有一个实体可用。

    只有模板被具体化(specialization)之后(用在特定的类型上),编译器才会根据具体的类型对模板进行编译。

    所以才定义模板的时候,会发现编译器基本不会报错(我当时还很开心的:我写代码尽然会没有错误,一气呵成),也做不出智能提示。但是当它被具体用在一个类上之后,错误就会大片大片的出现,却往往无法准确定位。

    因此设计模板就有设计模板的一套思路和方式,但是这跟本文的主题也有偏。

    因为模板的这种特殊性,它并没有自己的准确定义,因此我们不能把它放在.cpp文件中,而要把他们全部放在.h文件中进行书写。这也是为了在模板具体化的时候,能够让编译器可以找到模板的所有定义在哪里,以便真正的定义方法。

    至于模板类函数成员的定义放在哪里,导师的意见是放在类定义之外,因为这样当你看类的时候,一目了然地知道有那些方法和数据;我在用Visual Studio的时候查看到其标准库的实现,都是放在类内部的。

    可能是我习惯了C#的风格,我比较喜欢把它们都写在类内部,也因为在开发过程中,所使用的编辑器都有一个强大的功能:代码折叠。

    当然还有其他原因就是写在类外部,对于每一个函数成员的实现都需要把模板类型作为限定符写一遍,把类名限定符也要写一遍。

    更多相关内容
  • 主要介绍了C++中头文件和源文件详细介绍的相关资料,需要的朋友可以参考下
  • C++ 头文件和源文件

    2021-04-18 22:49:40
    C++中头文件(.h)和源文件(.cpp)都应该写些什么 头文件(.h): 写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。 在写头文件时需要注意,在开头和结尾处...

    from:https://www.cnblogs.com/fenghuan/p/4794514.html

    C++中头文件(.h)和源文件(.cpp)都应该写些什么

    头文件(.h):

        写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。

        在写头文件时需要注意,在开头和结尾处必须按照如下样式加上预编译语句(如下):

     

    #ifndef CIRCLE_H
    #define CIRCLE_H

    //你的代码写在这里

    #endif

     

        这样做是为了防止重复编译,不这样做就有可能出错。

        至于CIRCLE_H这个名字实际上是无所谓的,你叫什么都行,只要符合规范都行。原则上来说,非常建议把它写成这种形式,因为比较容易和头文件的名字对应。

       源文件(.cpp):

        源文件主要写实现头文件中已经声明的那些函数的具体代码。需要注意的是,开头必须#include一下实现的头文件,以及要用到的头文件。那么当你需要用到自己写的头文件中的类时,只需要#include进来就行了。

        下面举个最简单的例子来描述一下,咱就求个圆面积。

         第1步,建立一个空工程(以在VS2003环境下为例)。

         第2步,在头文件的文件夹里新建一个名为Circle.h的头文件,它的内容如下:

     

    #ifndef CIRCLE_H
    #define CIRCLE_H

    class Circle
    {
    private:
        double r;//半径
    public:
        Circle();//构造函数
        Circle(double R);//构造函数
        double Area();//求面积函数
    };

    #endif

       注意到开头结尾的预编译语句。在头文件里,并不写出函数的具体实现。

        第3步,要给出Circle类的具体实现,因此,在源文件夹里新建一个Circle.cpp的文件,它的内容如下:

     

    #include "Circle.h"

    Circle::Circle()
    {
        this->r=5.0;
    }

    Circle::Circle(double R)
    {
        this->r=R;
    }

    double Circle:: Area()
    {
        return 3.14*r*r;
    }

        需要注意的是:开头处包含了Circle.h,事实上,只要此cpp文件用到的文件,都要包含进来!这个文件的名字其实不一定要叫Circle.cpp,但非常建议cpp文件与头文件相对应

        最后,我们建一个main.cpp来测试我们写的Circle类,它的内容如下:

     

    #include <iostream>
    #include "Circle.h"
    using namespace std;

    int main()
    {
        Circle c(3);
        cout<<"Area="<<c.Area()<<endl;
        return 1;
    }

        注意到开头时有#include "Circle.h"的声明,证明我们使用到了我们刚才写的Circle类。

       至此,我们工程的结构为:

     

        运行一下,输出结果为:

     

       说明我们写的Circle类确实可以用了。

    1..h叫做头文件,它是不能被编译的。“#include”叫做编译预处理指令,可以简单理解成,在1.cpp中的#include"1.h"指令把1.h中的代码在编译前添加到了1.cpp的头部。每个.cpp文件会被编译,生成一个.obj文件,然后所有的.obj文件链接起来你的可执行程序就算生成了

    发现了没有,你要在.h文件中严格区分声明语句和定义语句。好的习惯是,头文件中应只处理常量、变量、函数以及类等等等等的声明,变量的定义和函数的实现等等等等都应该在源文件.cpp中进行

    至于.h和.cpp具有同样的主文件名的情况呢,对编译器来讲是没有什么意义的,编译器不会去匹配二者的主文件名,相反它很傻,只认#include等语句。但是这样写是一种约定俗成的编程风格,一个类的名字作为其头文件和源文件的主文件名比如Class1.h和Class1.cpp,这个类的声明在Class1.h中,实现在Class1.cpp中,我们人类看起来比较整齐,读起来方便,也很有利于模块化和源代码的重用

    为什么这个风格会约定俗成?有一句著名的话,叫“程序是为程序员写的”。

    2.h文件和cpp文件也就是说,在h文件中声明Declare,而在cpp文件中定义Define。 “声明”向计算机介绍名字,它说,“这个名字是什么意思”。而“定义”为这个名字分配存储空间。无论涉及到变量时还是函数时含义都一样。无论在哪种情况下,编译器都在“定义”处分配存储空间。对于变量,编译器确定这个变量占多少存储单元,并在内存中产生存放它们的空间。对于函数,编译器产生代码,并为之分配存储空间。函数的存储空间中有一个由使用不带参数表或带地址操作符的函数名产生的指针。定义也可以是声明。如果该编译器还没有看到过名字A,程序员定义int A,则编译器马上为这个名字分配存储地址。声明常常使用于extern关键字。如果我们只是声明变量而不是定义它,则要求使用extern对于函数声明, extern是可选的,不带函数体的函数名连同参数表或返回值,自动地作为一个声明。

    另篇:

    在C++编程过程中,随着项目的越来越大,代码也会越来越多,并且难以管理和分析。于是,在C++中就要分出了头(.h)文件和实现(.cpp)文件,并且也有了Package的概念。

    对于以C起步,C#作为“母语”的我刚开始跟着导师学习C++对这方面还是感到很模糊。虽然我可以以C的知识面对C++的语法规范,用C#的思想领悟C++中类的使用。但是C#中定义和实现是都在一个文件中(其实都是在类里面),而使用C的时候也只是编程的刚刚起步,所写的程序也只要一个文件就够了。因此对于C++的Package理解以及.h文件和.cpp文件的总是心存纠结。

    幸好导师有详细的PPT让我了解,一次对于Package的认识就明白多了。简单讲,一个Package就是由同名的.h和.cpp文件组成。当然可以少其中任意一个文件:只有.h文件的Package可以是接口或模板(template)的定义只有.cpp文件的Package可以是一个程序的入口。

    当然更具体详细的讲解,欢迎下载导师的教学PPT-Package来了解更多。

    不过我在这里想讲的还是关于.h文件和.cpp文件

    知道Package只是相对比较宏观的理解:我们在项目中以Package为编辑对象来扩展和修正我们的程序。编写代码时具体到应该把什么放到.h文件,又该什么放在.cpp文件中,我又迷惑了。

    虽然Google给了我很多的链接,但是大部分的解释都太笼统了:申明写在.h文件,定义实现写在.cpp文件。这个解释没有差错,但是真正下手起来,又会发现不知道该把代码往哪里打。

    于是我又把这个问题抛给了导师,他很耐心地给我详详细细地表述了如何在C++中进行代码分离。很可惜,第一次我听下了,但是没有听太懂,而且本来对C++就了解不深,所以也没有深刻的印象。

    经过几个项目的试炼和体验之后,我又拿出这个问题问导师,他又一次耐心地给我讲解了一遍(我发誓他绝对不是忘记了我曾经问过同样的问题),这次我把它记录了下来。

    为了不再忘记,我将它们总结在这里。

    概览

     非模板类型(none-template)模板类型(template)
    头文件(.h)
    • 全局变量申明(带extern限定符)
    • 全局函数的申明
    • inline限定符的全局函数的定义
    • 类的定义 ==> decl and def are really the same for struct/class
    • 类函数成员和数据成员的申明(在类内部)
    • 类定义内的函数定义(相当于inline)
    • static const限定符的数据成员在类内部的初始化
    • inline限定符的类定义外的函数定义
    • 模板类的定义
    • 模板类成员的申明和定义(定义可以放在类内或者类外,类外不需要写inline)
    实现文件(.cpp)
    • 全局变量的定义(及初始化)
    • 全局函数的定义
    (无)
    • 类函数成员的定义
    • 类带static限定符的数据成员的初始化

    *申明:declaration
    *定义:definition

    头文件

    头文件的所有内容,都必须包含在

    #ifndef {Filename} 
    #define {Filename} 

    //{Content of head file} 

    #endif

    这样才能保证头文件被多个其他文件引用(include)时,内部的数据不会被多次定义而造成错误

    inline限定符

    在头文件中,可以对函数用inline限定符来告知编译器,这段函数非常的简单,可以直接嵌入到调用定义之处。

    当然inline的函数并不一定会被编译器作为inline来实现,如果函数过于复杂,编译器也会拒绝inline。

    因此简单说来,代码最好短到只有3-5行的才作为inline。有循环,分支,递归的函数都不要用做inline。

    对于在类定义内定义实现的函数,编译器自动当做有inline请求(也是不一定inline的)。因此在下边,我把带有inline限定符的函数成员和写在类定义体内的函数成员统称为“要inline的函数成员”

    非模板类型

    全局类型

    就像前面笼统的话讲的:申明写在.h文件。

    对于函数来讲,没有实现体的函数,就相当于是申明;而对于数据类型(包括基本类型和自定义类型)来说,其申明就需要用extern来修饰。

    然后在.cpp文件里定义、实现或初始化这些全局函数和全局变量。

    不过导师一直反复强调:不许使用全局函数和全局变量。用了之后造成的后果,目前就是交上去的作业项目会扣分。当然不能用自有不能用的理由以及解决方案,不过不在目前的讨论范围内。

     

    自定义类型

    对于自定义类型,包括类(class)和结构体(struct),它们的定义都是放在.h文件中。其成员的申明和定义就比较复杂了,不过看上边的表格,还是比较清晰的。

    函数成员

    函数成员无论是否带有static限定符,其申明都放在.h文件的类定义内部。

    对于要inline的函数成员其定义放在.h文件;其他函数的实现都放在.cpp文件中。

    数据成员

    数据成员的申明与定义都是放在.h文件的类定义内部。对于数据类型,关键问题是其初始化要放在什么地方进行。

    对于只含有static限定符的数据成员,它的初始化要放在.cpp文件中。因为它是所有类对象共有的,因此必须对它做合适的初始化。

    对于只含有const限定符的数据成员,它的初始化只能在构造函数的初始化列表中完成。因为它是一经初始化就不能重新赋值,因此它也必须进行合适的初始化。

    对于既含有static限定符,又含有const限定符的数据成员,它的初始化和定义同时进行。它也是必须进行合适的初始化

    对于既没有static限定符,又没有const限定符的数据成员,它的值只针对本对象可以随意修改,因此我们并不在意它的初始化什么时候进行。

     

    模板类型

    C++中,模板是一把开发利器,它与C#,Java的泛型很相似,却又不尽相同。以前,我一直只觉得像泛型,模板这种东西我可能一辈子也不可能需要使用到。但是在导师的强制逼迫使用下,我才真正体会到模板的强大,也真正知道要如何去使用模板,更进一步是如何去设计模板。不过这不是三言两语可以讲完的,就不多说了。

    对于模板,最重要的一点,就是在定义它的时候,编译器并不会对它进行编译,因为它没有一个实体可用

    只有模板被具体化(specialization)之后(用在特定的类型上),编译器才会根据具体的类型对模板进行编译。

    所以才定义模板的时候,会发现编译器基本不会报错(我当时还很开心的:我写代码尽然会没有错误,一气呵成),也做不出智能提示。但是当它被具体用在一个类上之后,错误就会大片大片的出现,却往往无法准确定位。

    因此设计模板就有设计模板的一套思路和方式,但是这跟本文的主题也有偏。

     

    因为模板的这种特殊性,它并没有自己的准确定义,因此我们不能把它放在.cpp文件中,而要把他们全部放在.h文件中进行书写。这也是为了在模板具体化的时候,能够让编译器可以找到模板的所有定义在哪里,以便真正的定义方法。

    至于模板类函数成员的定义放在哪里,导师的意见是放在类定义之外,因为这样当你看类的时候,一目了然地知道有那些方法和数据;我在用Visual Studio的时候查看到其标准库的实现,都是放在类内部的。

    可能是我习惯了C#的风格,我比较喜欢把它们都写在类内部,也因为在开发过程中,所使用的编辑器都有一个强大的功能:代码折叠。

    当然还有其他原因就是写在类外部,对于每一个函数成员的实现都需要把模板类型作为限定符写一遍,把类名限定符也要写一遍。

     

    展开全文
  • 而.h文件则被称作C++头文件,里面放的也是C++的源代码。 C+ +语言支持“分别编译”(separate compilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的.cpp文件里。.cpp文件里的东西都是...

    一、C++编译模式

            通常,在一个C++程序中,只包含两类文件——.cpp文件和.h文件。其中,.cpp文件被称作C++源文件,里面放的都是C++的源代码;而.h文件则被称作C++头文件,里面放的也是C++的源代码。
            C+ +语言支持“分别编译”(separate compilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的.cpp文件里。.cpp文件里的东西都是相对独立的,在编译(compile)时不需要与其他文件互通,只需要在编译成目标文件后再与其他的目标文件做一次链接(link)就行了。比如,在文件a.cpp中定义 了一个全局函数“void a() {}”,而在文件b.cpp中需要调用这个函数。即使这样,文件a.cpp和文件b.cpp并不需要相互知道对方的存在,而是可以分别地对它们进行编译, 编译成目标文件之后再链接,整个程序就可以运行了。

            这是怎么实现的呢?从写程序的角度来讲,很简单。在文件b.cpp中,在调用 “void a()”函数之前,先声明一下这个函数“void a();”,就可以了。这是因为编译器在编译b.cpp的时候会生成一个符号表(symbol table),像“void a()”这样的看不到定义的符号,就会被存放在这个表中。再进行链接的时候,编译器就会在别的目标文件中去寻找这个符号的定义。一旦找到了,程序也就可以 顺利地生成了。

            注意这里提到了两个概念,一个是“定义”,一个是“声明”。

    • “定义”就是把一个符号完完整整地描述出来:它是变量还是函数(函数带实现,如cpp中的函数实现),返回什么类型,需要什么参数等等。
    • “声明”则只是声明这个符号的存在,即告诉编译器,这个符号是在其他文件中定义的(如.h中的函数声明,cpp中某个函数在头部先声明了,后面再实现),我这里先用着,你链接的时候再到别的地方去找找看它到底是什么吧。
    • 定义的时候要按C++语法完整地定义一个符号(变量或者函数)。
    • 声明的时候就只需要写出这个符号的原型了。

            需要注意的是,一个符号,在整个程序中可以被声明多次,但却要且仅要被定义一次。试想,如果一个符号出现了两种不同的定义,编译器该听谁的?

            这 种机制给C++程序员们带来了很多好处,同时也引出了一种编写程序的方法。考虑一下,如果有一个很常用的函数“void f() {}”,在整个程序中的许多.cpp文件中都会被调用,那么,我们就只需要在一个文件中定义这个函数,而在其他的文件中声明这个函数就可以了。一个函数还 好对付,声明起来也就一句话。但是,如果函数多了,比如是一大堆的数学函数,有好几百个,那怎么办?能保证每个程序员都可以完完全全地把所有函数的形式都 准确地记下来并写出来吗?

    二、什么是头文件

            很显然,答案是不可能。但是有一个很简单地办法,可以帮助程序员们省去记住那么多函数原型的麻烦:我们可以把那几百个函数声明语句全都先写好,放在一个文件里,等到程序员需要它们的时候,就把这些东西全部copy进他的源代码中。

             这 个方法固然可行,但还是太麻烦,而且还显得很笨拙。于是,头文件便可以发挥它的作用了。所谓的头文件,其实它的内容跟.cpp文件中的内容是一样的,都是 C++的源代码。但头文件不用被编译。我们把所有的函数声明全部放进一个头文件中,当某一个.cpp源文件需要它们时,它们就可以通过一个宏命令 “#include”包含进这个.cpp文件中,从而把它们的内容合并到.cpp文件中去。当.cpp文件被编译时,这些被包含进去的.h文件的作用便发 挥了。

            举一个例子吧,假设所有的数学函数只有两个:f1和f2,那么我们把它们的定义放在math.cpp里(这里不涉及到类,所以math.h中只是放了2个函数的声明):

    /* math.cpp */
    double f1()
    {
        //do something here....
        return;
    }
    double f2(double a)
    {
        //do something here...
        return a * a;
    }
    /* end of math.cpp */

    并把“这些”函数的声明放在一个头文件math.h中:

    /* math.h */
    double f1();
    double f2(double);
    /* end of math.h */

    在另一个文件main.cpp中,我要调用这两个函数,那么就只需要把头文件包含进来:

    /* main.cpp */
    #include "math.h"
    main()
    {
        int number1 = f1();
        int number2 = f2(number1);
    }
    /* end of main.cpp */

            这 样,便是一个完整的程序了。需要注意的是,.h文件不用写在编译器的命令之后,但它必须要在编译器找得到的地方(比如跟main.cpp在一个目录下)。 main.cpp和math.cpp都可以分别通过编译,生成main.o和math.o,然后再把这两个目标文件进行链接,程序就可以运行了。

    三、#include

            #include 是一个来自C语言的宏命令,它在编译器进行编译之前,即在预编译的时候就会起作用。#include的作用是把它后面所写的那个文件的内容,完完整整地、 一字不改地包含到当前的文件中来。值得一提的是,它本身是没有其它任何作用与副功能的,它的作用就是把每一个它出现的地方,替换成它后面所写的那个文件的 内容。简单的文本替换,别无其他。因此,main.cpp文件中的第一句(#include "math.h"),在编译之前就会被替换成math.h文件的内容。即在编译过程将要开始的时候,main.cpp的内容已经发生了改变:

    /* ~main.cpp */
    double f1();
    double f2(double);
    main()
    {
        int number1 = f1();
        int number2 = f2(number1);
    }
    /* end of ~main.cpp */

            不多不少,刚刚好。同理可知,如果我们除了main.cpp以外,还有其他的很多.cpp文件也用到了f1和f2函数的话,那么它们也通通只需要在使用这两个函数前写上一句#include "math.h"就行了。

    四、头文件中应该写什么

            通 过上面的讨论,我们可以了解到,头文件的作用就是被其他的.cpp包含进去的。它们本身并不参与编译,但实际上,它们的内容却在多个.cpp文件中得到了 编译。通过“定义只能有一次”的规则,我们很容易可以得出,头文件中应该只放变量和函数的声明,而不能放它们的定义(头文件中如果有类的定义,则算定义,而不是类的声明,但其内部的函数却算声明)。因为一个头文件的内容实际上是会被引 入到多个不同的.cpp文件中的,并且它们都会被编译。放声明当然没事,如果放了定义,那么也就相当于在多个文件中出现了对于一个符号(变量或函数)的定 义,纵然这些定义都是相同的,但对于编译器来说,这样做不合法。

            所以,应该记住的一点就是,.h头文件中,只能存在变量或者函数的声明, 而不要放定义。即,只能在头文件中写形如:extern int a;和void f();的句子(这里不涉及到类的情况)。这些才是声明。如果在.h文件中写上int a;或者void f() {}这样的句子,那么一旦这个头文件被两个或两个以上的.cpp文件包含的话,编译器会立马报错。(关于extern,前面有讨论过,这里不再讨论定义跟 声明的区别了。)

            但是,这个规则是有三个例外的。

    • 头文件中可以写const对象的定义。

             因为全局的const对象默认是没有extern的声明的(也就是不能带extern,也就是extern const int b;是不对的),所以它只在当前文件中有效。把这样的对象写进头文件a.h中,即使它被包含到其他多个.cpp文件中(b.cpp和c.cpp),这个对象也都只在包含它的 那个文件(b.cpp和c.cpp)中有效,对其他文件来说是不可见的,所以便不会导致多重定义。同时,因为这些.cpp文件中的该对象都是从一个头文件中包含进去的,这样也就保证 了这些.cpp文件中的这个const对象的值是相同的,可谓一举两得(因为是const的,所以在a.h中定义的是啥,那么在b.cpp和c.cpp中也是啥)。同理,static对象的定义也可以放进头文件。

    • 头文件中可以写内联函数(inline)的定义。

             因为inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的,而并非是普通函数那样可以先声明再链接的(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行。如果内联函数像普通函数一样只能定义一次的话,这事儿就难办了。因为在 一个文件中还好,我可以把内联函数的定义写在最开始,这样可以保证后面使用的时候都可以见到定义;但是,如果我在其他的文件中还使用到了这个函数那怎么办 呢?这几乎没什么太好的解决办法,因此C++规定,内联函数可以在程序中定义多次,只要内联函数在一个.cpp文件中只出现一次(同一个内联函数不能在同一个.cpp中定义多次,其实也没人这么干),并且在所有的.cpp文 件中,这个内联函数的定义是一样的,就能通过编译。那么显然,把内联函数的定义放进一个头文件中是非常明智的做法。

    • 头文件中可以写类 (class)的定义

           因为在程序(如main.cpp)中创建一个类class A的对象时A a,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的 定义的要求,跟内联函数是基本一样的。所以把类的定义放进头文件,在使用到这个类的.cpp文件中去包含这个头文件,是一个很好的做法。

            在这里,值得一提的是,类的定义中包含着数据成员和函数成员。数据成员是要等到具体的对象被创建时才会被定义(分配空间),但函数成员却是需要在一开始就被定义的,这也就是我们通常所说的类的实现。一般,我们的做法是,把类的定义放在头文件中,而把函数成员的实现代码放在一个.cpp文件中。这是可以的,也是很好的办法。 不过,还有另一种办法。那就是直接把函数成员的实现代码也写进类定义里面。在C++的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为内联的。因此,把函数成员的定义写进类定义体,一起放进头文件中,是合法的。注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中(也就是一个.h中,前面是class的定义,在类的定义之后(}之后),又写了其他函数的定义(即实现),是不行的), 这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的.cpp文件包含,这个函数成员就被重定义了。

    五、头文件中的保护措施

            考虑一下,如果头文件中只包含声明语句的话,它被同一个.cpp文件包含再多次都没问题——因为声明语句的出现是不受限制的。

            然而,上面讨论到的头文件中的 三个例外也是头文件很常用的一个用处。那么,一旦一个头文件中出现了上面三个例外中的任何一个,它再被一个.cpp包含多次的话,问题就大了。因为这三个 例外中的语法元素虽然“可以定义在多个源文件中”,但是“在一个源文件中只能出现一次”。

            设想一下,如果a.h中含有类A的定义,b.h中含有类B的定义(这个不叫声明),由于类B的定义依赖了类A,所以b.h中也#include了a.h。现在有一个源文件c.pp,它同时用到了类A和类B,于是程序员在这个源文件中既把 a.h包含进来了,也把b.h包含进来了。这时,问题就来了:类A的定义在这个源文件中出现了两次!于是整个程序就不能通过编译了。你也许会认为这是程序 员的失误——他应该知道b.h包含了a.h——但事实上他不应该知道。

            使用"#define"配合条件编译可以很好地解决这个问题。在一 个头文件中,通过#define定义一个名字,并且通过条件编译#ifndef.#define..#endif使得编译器可以根据这个名字是否被定义,再决定要不要继 续编译该头文中后续的内容。这个方法虽然简单,但是写头文件时一定记得写进去。

    本文实在C++中的头文件和源文件 - DoubleLi - 博客园基础之上进行了适当修改完成的。

    展开全文
  • 头文件和源文件的关系

    千次阅读 2017-01-16 22:00:35
    一、关于头文件和源文件之间的关系,理解把握要点在于:  1、头文件是给编程开发人员看的,而源文件是给编译器看的;  2、在#include头文件时,编译器直接把相应的头文件复制粘贴到源文件的相应位置;  3、...

    一、关于头文件和源文件之间的关系,理解把握要点在于:

      1、头文件是给编程开发人员看的,而源文件是给编译器看的;

      2、在#include头文件时,编译器直接把相应的头文件复制粘贴到源文件的相应位置;

      3、为了确保编译完成后,链接过程不会出现问题,在头文件中只可以对函数或变量进行声明,而不可以进行定义。(关于声明可以理解为告诉编译器存在这样一个符号,但是没有任何后续的动作;关于定义可以理解为让编译器为这个符号分配计算机资源,例如内存等。声明可以有多次,而定义只被允许发生且必须发生一次。)

    二、以下为转载内容正文(有删改等)

      1、预备知识

        简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程:

          1)预处理阶段 
          2)词法与语法分析阶段 
          3)编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 (.obj文件)
          4)链接阶段,将各个目标文件中的各段代码进行相对地址定位(原文为绝对地址定位。个人水平有限,可能有错),生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息。(生成.exe文件)

        编译器在编译时是以C文件(也就是源文件)为单位进行的,也就是说如果你的项目中一个C文件都没有,那么你的项目将无法编译。

        连接器是以目标文件(源文件经过编译器编译以后得到)为单位,它将一个或多个目标文件进行函数与变量的重定位,生成最终的可执行文件。

        在PC上的程序开发,一般都有一个main函数(作为入口函数),这是各个编译器的约定。

        当然,你如果自己写连接器脚本的话,可以不用main函数作为程序入口!!!!

       (main .c文件 目标文件 可执行文件 )

      2、头文件和源文件的关系

        有了这些基础知识,再言归正传。

        如预备知识中所述,源文件(即C文件等)经过编译器编译,生成目标文件;目标文件经过链接,生成可执行文件。而这些C文件中又需要一个main函数作为可执行程序的入口。

        那么我们就从一个C文件入手,假定这个C文件内容如下: 

    #include 
    #include "mytest.h"

    int main(int argc,char **argv) 

        test = 25; 
        printf("test.................%d/n",test); 
    }

        头文件内容如下: 
    int test;    //没错,就一行内容

        现在以这个例子来讲解编译器的工作: 
        1)预处理阶段:编译器以C文件作为一个单元,首先读这个C文件,发现第一句与第二句是包含一个头文件,就会在所有搜索路径中寻找这两个文件,找到之后,就会将相应头文件中再去处理宏,变量, 函数声明,嵌套的头文件包含等,检测依赖关系,进行宏替换,看是否有重复定义与声明的情况发生,最后将那些文件中所有的东东全部扫描进这个当前的C文件 中,形成一个中间“C文件”

        2)编译阶段:在上一步中相当于将那个头文件中的test变量扫描进了一个中间C文件,那么test变量就变成了这个文件中的一个全局变量,此时就将所有这个中间C文件的所有变量,函数分配空间,将各个函数编译成二进制码,按照特 定目标文件格式生成目标文件,在这种格式的目标文件中进行各个全局变量,函数的符号描述,将这些二进制码按照一定的标准组织成一个目标文件

        3)连接阶段:将上一步成生的各个目标文件,根据一些参数,连接生成最终的可 执行文件,主要的工作就是重定位各个目标文件的函数,变量等,相当于将个目标文件中的二进制码按一定的规范合到一个文件中再回到C文件与头文件各写什么内 容的话题上:理论上来说C文件与头文件里的内容,只要是C语言所支持的,无论写什么都可以的,比如你在头文件中写函数体,只要在任何一个C文件包含此头文 件就可以将这个函数编译成目标文件的一部分(编译是以C文件为单位的,如果不在任何C文件中包含此头文件的话,这段代码就形同虚设),你可以在C文件中进 行函数声明,变量声明,结构体声明,这也不成问题!!!那为何一定要分成头文件与C文件呢?又为何一般都在头件中进行函数,变量声明,宏声明,结构体声明 呢?而在C文件中去进行变量定义,函数实现呢??原因如下: 
          (1)如果在头文件中实现一个函数体,那么如果在多个C文件中引用它,而且又同时编 译多个C文件,将其生成的目标文件连接成一个可执行文件,在每个引用此头文件的C文件所生成的目标文件中,都有一份这个函数的代码,如果这段函数又没有定 义成局部函数,那么在连接时,就会发现多个相同的函数,就会报错;
          (2)如果在头文件中定义全局变量,并且将此全局变量赋初值,那么在多个引用此头文件的C文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段,会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变量,也就是仅为此变量分配一个空间,而不是多份空间,假定这个变量在头文件没有赋初值,编译器就会将之放入BSS段,连接器会对BSS段的多个同名变量仅分配一个存储空间;
          (3)如果在C文件中声明宏,结构体,函数等,那么我要在另一个C文件中引用相应的宏,结构体,就必须再做一次重复的工作,如果我改了一个C文件中的一个声明,那么又忘了改其它C文件中的声明,这不就出了大问题了,程序的逻辑就变成了你不可想象的了,如果把这些公共的东东放在一个头文件中,想用它的C文件就只需要引用一个就OK了!!!这样岂不方便,要改某个声明的时候,只需要动一下头文件就行了;
          (4)在头文件中声明结构体,函数等,当你需要将你的代码封装成一个库,让别人来用你的代码,你又不想公布源码,那么人家如何利用你的库呢?也就是如何利用你的库中的各个函数呢??一种方法是公布源码,别人想怎么用就怎么用,另一种是提供头文件,别人从头文件中看你的函数原型,这样人家才知道如何调用你写的函数,就如同你调用printf函数一样,里面的参数是怎样的??你是怎么知道的??还不是看人家的头文件中的相关声明啊!!!当然这些东东都成了C标准,就算不看人家的头文件,你一样可以知道怎么使用。

      3、c语言中.c和.h文件的困惑

        本质上没有任何区别,只不过一般: 
        .h文件是头文件,内含函数声明、宏定义、结构体定义等内容;

        .c文件是程序文件,内含函数实现,变量定义等内容。

        而且是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。你可以强制编译器把任何后缀的文件都当作c文件来编。这样分开写成两个文件是一个良好的编程风格。

        而且,比方说 我在aaa.h里定义了一个函数的声明,然后我在aaa.h的同一个目录下建立aaa.c,aaa.c里定义了这个函数的实现,然后是在main函数所在.c文件里#include这个aaa.h,然后我就可以使用这个函数了。main在运行时就会找到这个定义了这个函数的aaa.c文件。这是因为:main函数为标准C/C++的程序入口,编译器会先找到该函数所在的文件。假定编译程序编译myproj.c(其中含main())时,发现它include了mylib.h(其中声明了函数void test(),那么此时编译器将按照事先设定的路径(Include路径列表及代码文件所在的路径)查找与之同名的实现文件(扩展名为.cpp或.c,此例中为mylib.c)。如果找到该文件,并在其中找到该函数(此例中为void test())的实现代码,则继续编译;如果在指定目录找不到实现文件,或者在该文件及后续的各include文件中未找到实现代码,则返回一个编译错误。

        其实include的过程完全可以“看成”是一个文件拼接的过程,将声明和实现分别写在头文件及C文件中,或者将二者同时写在头文件中,理论上没有本质的区别。以上是所谓动态方式。

        对于静态方式,基本所有的C/C++编译器都支持一种链接方式被称为Static Link,即所谓静态链接。在这种方式下,我们所要做的,就是写出包含函数,类等等声明的头文件(a.h,b.h,...),以及他们对应的实现文件(a.cpp,b.cpp,...),编译程序会将其编译为静态的库文件(a.lib,b.lib,...)。在随后的代码重用过程中,我们只需要提供相应的头文件(.h)和相应的库文件(.lib),就可以使用过去的代码了。相对动态方式而言,静态方式的好处是实现代码的隐蔽性,即C++中提倡的“接口对外,实现代码不可见”。有利于库文件的转发.c文件和.h文件的概念与联系

        如果说难题最难的部分是基本概念,可能很多人都会持反对意见,但实际上也确实如此。我高中的时候学物理,老师抓的重点就是概念——概念一定要搞清,于是难题也成了容易题。如果你能分析清楚一道物理难题存在着几个物理过程,每一个过程都遵守那一条物理定律(比如动量守恒、牛II定律、能量守恒),那么就很轻松的根据定律列出这个过程的方程,N个过程必定是N个N元方程,难题也就迎刃而解。即便是高中的物理竞赛难题,最难之处也不过在于:

          (1) 混淆你的概念,让你无法分析出几个物理过程,或某个物理过程遵循的那条物理定律;
          (2) 存在高次方程,列出方程也解不出。而后者已经是数学的范畴了,所以说,最难之处还在于掌握清晰的概念;

        程序设计也是如此,如果概念很清晰,那基本上没什么难题(会难在数学上,比如算法的选择、时间空间与效率的取舍、稳定与资源的平衡上)。但是,要掌握清晰的概念也没那么容易。比如下面这个例子,看看你有没有很清晰透彻的认识。

    //a.h
    void foo();

    //a.c
    #include "a.h"   //我的问题出来了:这句话是要,还是不要?
    void foo()
    {
         return;
    }

    //main.c
    #include "a.h"
    int main(int argc, char *argv[])
    {
        foo(); 
       return 0;
    }                
        针对上面的代码,请回答三个问题:

            1)a.c 中的 #include "a.h" 这句话是不是多余的? 
            2)为什么经常见 xx.c 里面 include 对应的 xx.h? 
            3)如果 a.c 中不写,那么编译器是不是会自动把 .h 文件里面的东西跟同名的 .c 文件绑定在一起? 
      (请针对上面3道题仔细考虑10分钟,莫要着急看下面的解释。:) 考虑的越多,下面理解的就越深。)

        好了,时间到!请忘掉上面的3道题,以及对这三道题引发出的你的想法,然后再听我慢慢道来。

        正确的概念是:从C编译器角度看,.h和.c皆是浮云,就是改名为.txt、.doc也没有大的分别。换句话说,就是.h和.c没啥必然联系。

        .h中一般放的是同名.c文件中定义的变量、数组、函数的声明,需要让.c外部使用的声明。这个声明有啥用?只是让需要用这些声明的地方方便引用。因为 #include "xx.h" 这个宏其实际意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下子),所以用 #include "xx.h" 这个宏就简化了许多行代码——让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),至于 include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。
        这样你可能会说:啊?那我平时只想调用 xx.c 中的某个函数,却 include了 xx.h 文件,岂不是宏替换后出现了很多无用的声明?没错,确实引入了很多垃圾,但是它却省了你不少笔墨,并且整个版面也看起来清爽的多。鱼与熊掌不可得兼,就是这个道理。反正多些声明(.h一般只用来放声明,而放不定义,参见拙著“过马路,左右看”)也无害处,又不会影响编译,何乐而不为呢?

        翻回头再看上面的3个问题,很好解答了吧?

        答:不一定。这个例子中显然是多余的。但是如果.c中的函数也需要调用同个.c中的其它函数,那么这个.c往往会include同名的.h,这样就不需要为声明和调用顺序而发愁了(C语言要求使用之前必须声明,而include同名.h一般会放在.c的开头)。有很多工程甚至把这种写法约定为代码规范,以规范出清晰的代码来。 
        答:1中已经回答过了。 
        答:不会。问这个问题的人绝对是概念不清,要不就是想混水摸鱼。非常讨厌的是中国的很多考试出的都是这种烂题,生怕别人有个清楚的概念了,绝对要把考生搞晕。 

        搞清楚语法和概念说易也易,说难也难。窍门有三点:

            1)不要晕着头工作,要抽空多思考思考,多看看书; 
            2)看书要看好书,问人要问强人。烂书和烂人都会给你一个错误的概念,误导你; 
            3)勤能补拙是良训,一分辛苦一分才; 

    小结:

        (1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
        (2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

        (3)头文件用来存放函数原型。

      4、头文件如何来关联源文件?

        这个问题实际上是说,已知头文件“a.h”声明了一系列函数(仅有函数原型,没有函数实现),“b.cpp”中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”,那么c.cpp是怎样找到b.cpp中的实现呢? 
        其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。 
        谭浩强老师的《C程序设计》一书中提到,编译器预处理时,要对#include命令进行“文件包含处理”:将headfile.h的全部内容复制到#include “headfile.h”处。这也正说明了,为什么很多编译器并不care到底这个文件的后缀名是什么----因为#include预处理就是完成了一个“复制并插入代码”的工作。 
        程序编译的时候,并不会去找b.cpp文件中的函数实现,只有在link的时候才进行这个工作。我们在b.cpp或c.cpp中用#include “a.h”实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o或.obj文件),目标文件中,这些函数和变量就视作一个个符号。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成的.o或.obj文件),此时,连接器会去这个.o或.obj文件中找在b.cpp中实现的函数,再把他们build到makefile中指定的那个可以执行文件中。 
        在VC中,一帮情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。 
        通常,编译器会在每个.o或.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示“redefined”。

        转载自:http://blog.csdn.net/shi_869160/archive/2010/07/03/5710441.aspx

    展开全文
  • VScode中添加头文件和源文件方法

    千次阅读 多人点赞 2022-05-15 20:43:03
    在正常情况下,若同一文件夹下若头文件、源文件、和主要代码在同一文件夹下,则可以正常运行程序。 如图(此为Visual Studio 示例): 编译结果(无报错): 但在VScode中,同样的使用方式会...
  • C++头文件和源文件,编译过程

    万次阅读 多人点赞 2019-04-01 16:07:15
    1、系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。 #include <xxx.h> 2、用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后在到C++安装目录(比如VC中可以指定和...
  • 在QT中自定义头文件和源文件使用方法 最近想用QT来实现一个简单的功能,为了便于函数的集成需要将功能函数进行封装,自己补了些c++的函数封装方法,发现在QT中还不太一样。接来下简单介绍一下具体怎么实现。 假设...
  • c 语言头文件和源文件

    千次阅读 2016-07-24 00:40:28
    头文件和源程序h结尾的就是头文件,c结尾的就是源文件 把定义和实现分开,就可以通过头文件和源程序分开 在头文件中去定义,在源文件中去实现,然后main方法包含的时候只要包含头文件就可以了。 0:实际...
  • 简单的说其实要理解C文件头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句,再将之汇编...
  • 通过这三个源文件两个相应头文件来演示他们之间的关系 1、 main.c 2、input.c 3、calcu.c 1、input.h 2、calcu.h 总结: 1、关于#ifndef #define #endif用法需要你们自己去查(算了还是给你们贴出来吧) ...
  • 而 .h 文件则被称作 C++ 头文件,里面放的也是 C++ 的源代码。 C++ 语言支持"分别编译"(separatecompilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的 .cpp 文件里。.cpp 文件里的东西...
  • 简单的说其实要理解C文件头文件(即.h)有什么不同之处
  • 你可以打开真实目录下可发现头文件和源文件是放在同一个文件夹下面。(顺便提一下绝对路径通常有相同的开头,相对路径是相对于当前的工作目录。例如cd~和cd-分别是回到主目录和回到上一级目录,ls -a #列出你所在...
  • .h文件中能包含:  类成员数据的声明,但不能赋值  类静态数据成员的定义和赋值,但不建议,只是个声明就好。  类的成员函数的声明  非类成员函数的声明  常数的定义:如:constint a=5;  静态函数的定...
  • 创建新文件时:1.头文件和原文件路径保证相同 2.下方Debug,Release勾选 解决问题
  • 头文件和源文件的区别

    万次阅读 2016-05-28 23:37:39
    当一个工程的文件较多时应该将源文件与头文件分开目录存放,一般头文件存放在include或inc目录下,而源文件存放在source或src目录下,根据经验,一个工程的文件数超过30个时应该将源文件与头文件分开存放,当文件较...
  • Qt含各种头文件和源文件方法

    万次阅读 2016-08-20 09:58:29
    QT项目中可能有很多的文件夹需要包含进来,有的文件夹是一个独立的功能模块,这个项目可能需要而别的项目可能不需要,怎样将其包含进来呢。这里有两种方法
  • C语言中头文件和源文件的关系

    千次阅读 2019-06-27 10:34:51
    答案 #include预处理实际上就是完成了一个“复制代码并插入”的工作!...编译器以C文件作为一个单元,开始读C文件,读到包含头文件的语句时,从所有搜索路径寻找头文件。 找到后,处理宏,变量,函数声明,检测...
  • .h 文件,被称作 C++ 头文件,里面放的也是 C++ 的源代码。 C++ 语言支持 “分别编译”(separate compilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的 .cpp 文件里。.cpp 文件里的...
  • 而.h文件则被称作C++头文件,里面放的也是C++的源代码。 C+ +语言支持“分别编译”(separatecompilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的.cpp文件里。.cpp文件里的东西都是相对...
  • Source Insight技巧:头文件和实现文件快捷键打开的方式–.h .cpp 申明:本文转载纯粹只为方便自己以后需要的时候可以很容易找到,转载请注明出处 转自:http://blog.chinaunix.net/uid-27213819-id-3810619.html ...
  • c++中头文件、源文件的使用小记
  • 头文件与源文件 在源文件A1.c中调用A2.c 中的函数 有两种方法:  1.在A2.c中有完整的函数定义,在A1.c中添加一下要用到的函数原型(声明)就可以了,例如: 在A2.c中:有函数void A2(){…}; 在A1.c中:在文件前头...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,913
精华内容 2,765
热门标签
关键字:

头文件和源文件的用法