程序文件与预处理

1.程序文件分类
(1)头文件
    (1)后缀:c和c++头文件都是以.h结尾
    (2)作用
        (1)存放各种声明
            (1)#开头的各种预处理指令的声明,比如宏定义,#include包含其它文件
            (2)各种类型的声明,比如结构体,联合体,类类型,全局变量定义
            (3)各种函数声明

    (2)头文件还可以包含普通函数定义和内联函数定义        


(2)源文件
    (1)后缀:.c/.cc/.cpp/.cxx都可以
    (2)作用
        包含需要被编译成机器码的字符形式的程序代码,主要内容就是各种自定义算法函数           

2.程序编译链接过程
(1)编译链接的4个过程
    不管是c还是c++,编译链接分成了4个过程
    (1)预处理(预编译)
        (1)实现命令
            gcc/g++  -E  a.c/a.cpp -o  a.i 

    (2)处理程序:预处理器

    (3)作用:处理所有#开头的预处理指令,其中最重要的一项就是就将#include
        包含的文件全部展开到源文件文件中。

    (4)得到的.i文件
        得到后.i文件成为扩展后的源文件,是真正以上的完整的源文件
        ,也成为一个编译单元(转换单元)。编译器是以一个.i文件为单
        元进行比那一工作的。


(2)编译
    (1)实现命令
        gcc/g++  -S  a.c/a.cpp/a.i -o  a.s 

    (2)处理程序:编译器
        当被编译文件是a.c/a.cpp时会默认进行预处理的操作

    (3)作用:将字符编码的源C代码编译成为字符码编码的机器汇编
        在编译的过程中会将不同的符号标记上链接属性,无连接,
        内链接,外链接,留给链接器链接时使用。         

    (4)得到的.s: 机器汇编


(3)汇编
    (1)实现命令
        gcc/g++  -c  a.c/a.cpp/a.i/a.s -o  a.o 

    (2)处理程序:汇编器

    (3)作用:将机器汇编编程二进制的机器码

    (4)得到的.o
        称为目标文件(object文件),在uix和linux是ELF格式的文件。


(4)链接
    (1)实现命令
        gcc/g++  *.c/*.cpp/*.i/*.s/*.o  -o   a.out 

    (2)处理程序:链接器
        (1)如果制定被链接的文件不是.o文件的话,或默认执行
            前面的预处理,编译,汇编等操作。


    (3)将机器码的多个.o目标文件最后链接成为一个可执行文件,.o文件
        被分成了很多的段,不同.o文件的各个端段会被组合在一起。

        链接的过程主要做如下两件事:
        (1)符号统一:
            根据编译时留下的各种符号的链接属性,将强弱符号
            合并。程序中符号拥有的不同链接属性在链接时就决
            定了不同符号(函数,变量)的作用域的问题。

            链接的种类:内链接/外链接/无连接   

        (2)地址重定位:
            因为.o文件中所有的机器码都被定为从0地址开始运行的,
            但是实际上链接时需要根据运行环境来决定组合在一起得
            到的可执行文件从什么地址开始运行。


    (4)默认得到的a.out的可执行文件
        默认得到a.out文件,可以通过-o指定为随意的名字,在windows
        下,可执行文件往往以.exe结尾。

        可执行文件与目标文件一样,也是ELF格式文件。


3. 符号的作用域
    在前面讲函数/全局变量和局部变量时,我们已经讲到了有关作用域的问题,作用域
指的就是名称起作用的范围。


(1)名称的隐藏
在c中遇到这种情况时,局部变量会屏蔽(隐藏)全局变量,这时在函数内部
是不能访问全局变量的,如果想访问,只能将它们定义成为不同的名称。

但是在c++中情况有所变化,我们可以通过::访问外部作用域符号,在上面描述的
例子中,我们可以使用::访问全局变量。

例子1
    #include <iostream>
    #include <string>

    using namespace std;

    int a = 100;
    int main(int argc, char **argv)
    {           
            int a = 200;
            cout << "inner a = " << a << endl;
            cout << "outer a = " << ::a << endl;

                return 0;
    }    


(2)全局作用域再讨论
(1)什么是全局作用域
    定义在全局作用域的符号的作用域。

(2)全局作用域分类
    (1)从形式上看,全局作用域由什么决定
        (1)定义位置
        (2)声明
        (3)受到关键字的影响,extern/static

    (2)全局作用域分为两种
        (1)文件内作用域
            符号只在编译单元内有效。

        (2)跨文件作用域
            除了在本编译单元内有效外,还其它编译单元内也有效,但是需要做相应的声明。


4. 再次讨论只能有一个定义,但是可以有多个声明的规则
(1)强弱符号总结

    “定义”为强符号,“声明”为弱符号,链接时,编译器会根据同名强弱符号的链接属性
    将它们做合并。

    生成为一个可执行文件时,每个符号必须唯一的。但是在强弱符号的定义中,强符号(定义)
    只能有一个,弱符号(声明)有很多。

    (1)全局变量
        (1)强符号(定义):被初始化了的
        (2)弱符号(声明):未被初始化的

    (2)全局函数
        (1)强符号:包含{ }的函数定义
        (2)弱符号:函数声明


    对于普通函数来说,默认情况下其作用域为跨文件作用域,默认是extern
    修饰的,当使用static修饰时,全局作用域范围被限制在了一个转换单元
    内部。

    注意内联函数是一个特殊的例子,这在讲内联函数时已经提到过,内联函数在
    一个转换单元内只能有一个定义,但是在跨文件的全局作用域里面可以有多个定义。

    对于结构体类类型定义来说,有着与内联函数形同的情况,在一个转换单元内
    只能有一个定义,但是在跨文件的全局作用域里面可以有多个定义。

    防止头文件包含的重要性:
    通常情况下,像类结构体和宏定义等的声明,以及内联函数的定义都是
    放在了头文件中,这些虽然允许在跨文件的全局作用域中重复定义,但是
    在一个转换单元内(本文件内)是不允许重复定义的。这些声明往往都是
    存放在了头文件中,因此防止头文件包含就是为了防止这些被重复定义。
    使用如下条件编译是非常重要的,因为可以有效防止在一个转换单元中
    同一个头文件重复包含。


5. 链接属性与符号作用域
(1)链接属性与符号作用域的对应关系
    (1)无链接:对应局部作用域,典型的就是局部变量的作用域
    (2)内链接:在一个编译单元内有效的全局作用域,典型的就是staic修饰的

    为什么需要内链接,或者说为什么需要static修饰函数或者全局变量?
    答:防止命名冲突。

(3)外链接:外链接包含内链接,全局作用域为跨文件作用域,典型就是extern修饰的函数和全局变量
    ,其它文件想要使用时,需要做声明。


(2)符号的作用域是由链接属性决定
g++进行编译链接时,链接符号的过程是这样的。

(1)编译器编译时留下每个符号的链接属性
    编译器操作的对象是单个的单元,编译不能将多个文件编译成为一个文件。
    编译时:
    (1)如果在代码块内找到符号定义,该符号为无链接。
    (2)在编译单元内的全局作用域内找到该符号,该符号为内链接
    (3)如果在本编译单元内没有找到该符号
        (1)如果该符号没有声明,编译报错,表示该符号无法找到
        (2)如果发现了该符号的声明,将该符号标记为外链接

(2)链接器根据每个符号的链接属性进行符号合并

    链接时:
    (1)将所有内链接属性的定义与声明进行符号统一。
    (2)到其它编译单元中去寻找标记为外链接属性的符号,如果没有链接报错,否者将强弱符号统一。


我们常常称为的作用域其实就是链接域,只是为了学习的方便我们习惯以作用域进行称呼。


6. 函数全局变量局部变量总结
(1)内存的各种不同的管理方式
这在上一章已经讲解过。


(2)全局变量和局部变量对比分析
这里忽略c++中成员变量的情况。

(1)全局变量
    (1)存储位置:静态数据区.bss或者.data中       
    (2)生命周期:由存储的位置决定,因为存在静态存储区,表明其生命周期为整个程序运行期间都有效
    (3)作用域:为全局作用域   
        (1)本文件作用域:对应内链接域,从定义的位置到本文件末尾,可以被声明扩展
        (2)跨文件作用域:对应外链接域(包含内链接域),其它文件需要使用这个全局变量时,必须声明

    (4)与全局变量声明和定义相关的关键字
        (1)static修饰:表示全局变量只能在本文件有效,其它文件不能使用,换句话说只有内链接,无外链接
        (2)extern修饰:默认就是extern的,表示该全局变量的有跨文件作用域,也就是说有外链接,可以被其他文件
                访问,但是需要做相应的声明,比如 extern int a;,这个声明可以放在函数外部,也可以
                放在某函数内部,放在函数内部时,说明这个a只能在这个函数内有效,其他函数不能访问。


(2)局部变量
    所谓局部变量指的是代码块{ }内定义的变量,实际上函数只是代码块
    的一种,类,循环,switch都是代码块,里面定义的变量都是局部变量。
    所以这里讨论的局部变量不包含结构体成员,类成员,联合体成员的讨论。
    因为这些的成员受到包含它们的对象的影响。

    (1)自动局部变量
        (1)存储位置:函数栈
        (2)生命周期:由存储的位置决定,栈决定了它的生命周期为函数运行期间有效,之后释放
        (3)作用域:对应无链接,定义的位置到函数结尾,不能通过声明改变作用域
        (4)相关关键字
            (1)auto:默认就是auto修饰的,表示自动局部表变量


    (2)静态局部变量:
        (1)存储位置:空间开辟于静态数据区.bss或者.data中
        (2)生命周期:存储的位置决定了声明周期为整个程序运行期间都有效
        (3)作用域:定义的位置到函数结尾,不能通过声明改变作用域,无链接
        (4)相关关键字
            (1)static:静态局部变量

(3)总结
    这里讨论的变量不包含函数成员变量,函数成员变量的作用域和有效期主要是受到对象的限制。

    (1)不管是全局变量还是局部变量,生命周期(有效期)由存储的位置决定
    (2)作用域由定义的位置和声明决定(局部变量无声明),声明与定义与static/extern密切相关

    (2)static关键字在修饰全局变量/全局函数和局部变量时,作用大不相同
        (1)修饰局部变量时,改变的是存储的位置,进而改变的是生命周期。
        (2)修饰全局变量时,改变的是链接域,与存储位置无关,这一点与修饰函数同。


(4)讲解过程中直接举例子说明



7. c++中的命名空间
    (1)全局命名空间,如果名称在代码块外部,并且没有制定特殊的命名空间的话

为什么c++引入命名空间?
回忆:
(1)哪些只在编译时有效的定义
    类定义/结构体定义/宏定义/内联函数定义,这些定义在同一个编译单元中
    不能冲突,所以在同一个编译单元中防止同文件重复包含非常重要。

    但是可以在不同的编译单元被重复包含,比如a.cpp和b.cpp完全可以包含同
    一个头文件c.h。
    之所以是这样的原因,因为这些类型定义只在编译和预编译期间起到类型说
    明作用,一旦编译变成.o文件后,在进行链接时,这些类型将不复存在。

    所有我们知道,对于类定义/结构体定义/宏定义/内联函数定义来说,只要我们
    能够防止它们在同一个编译单元被重复定义即可,我们不用担心它们的命名空间
    的问题,因为他们只在编译时有效。

(2)哪些在链接时有效的定义
    普通函数定义/全局变量定义,这些定义在链接需要和它们的声明进行符号统一,
    这些符号在不同文件中都会重复出现,但是定义只能有一个,否者会照成二义性
    ,因此必须防止定义名字的的冲突。

    因此我们知道,命名空间这个概念实际上对于普通函数和全局变量来说时很重要的
    ,在c中由于命名空间的划分过于简单化,只有全局和局部命名空间,非常容易出
    现相同函数和全变量定义重名的情况,c中引入了static进行命名空间的简单限制
    ,在c++中引入了更加精确的命名空间的概念。

    定义函数或者全局变量虽然重名了,但是只要属于不同的命名空间,就没有任何
    问题。

从上面讲述中,我们不难看出,于命名空空间密切相关的实际上是普通函数的定义/全局变量的
定义,为类类型/结构体/内联函数/宏定义几乎不需要给其设定命名空间。


(2)c++中的命名空间
为了缓解c语言中命名空间的问题,c++中引入了自定义命名空间。

命名空间的定义和声明
举例:
命名空间定义:
b.cpp
    #include <iostream>
    #include <string>

    namespace bregion
    {
            extern const int a = 10;
    }
注意需要使用extern进行修饰。        

命名空间声明
在另一个cpp文件中使用时,比如a.cpp中使用时,需要声明命名空间。
#include <iostream>
#include <string>

namespace bregion
{
        extern const int a;
}

int main(int argc, char **argv)
{                   
        std::cout << bregion::a << std::endl;
        return 0;
}         

(3)使用using声明
在前面的例子中,如果希望使用希望在使用a时,不想加上bregion::
的繁琐前缀的话,我们可以使用using进行声明下。
上例可以改为如下:
#include <iostream>
#include <string>

namespace bregion
{
        extern const int a;
}
using bregion::a;
//或者使用using指令
//using namespace bregion;  

int main(int argc, char **argv)
{                   
        std::cout << a << std::endl;
        return 0;
} 

例子说明:
本例子中打印a时,不需要指定域名,因为using bregion::a;做了
声明,同时使用using namespace bregion;这种方式也是可以的,这两者的区别
是,前一种之声明了a,后一种表示声明了bregion中所有的符号,这与如下。
    using namespace std::cout;
    using namespace std;
是一回事。   


(4)函数与命名空间
(1)函数直接定义在命名空间中,比如下面的例子。

b.cpp文件
    #include <iostream>
    #include <string>

    namespace bregion
    {
            extern const int a = 10;

            extern int mymax(int a, int b)
            {
                    return a>b ? a : b;
            }

            extern int mymin(int a, int b)
            {
                    return a<b ? a : b;
            }

    }

a.cpp文件
    #include <iostream>
    #include <string>

    namespace bregion
    {
            extern int mymax(int a, int b);
            extern int mymin(int a, int b);
    }

    using namespace bregion;//using指令

    int main(int argc, char **argv)
    {                  
            std::cout << mymax(10, 20) << std::endl;
            std::cout << mymin(50, 40) << std::endl;

            return 0;
    }

    例子说明:
    在本例子中,是直接将函数定义写在了命名空间中,实际上我们完全可以将
    函数的定义写在命名空间的外部,而只是将声明写在命名空间里面,而且
    很多时候我们都是将命名空间放在了头文件中。

    比如可以将上面的例子改成下面的样子。

    b.h文件
        #ifndef H_B_H
        #define H_B_H

        namespace bregion
        {
                extern const int a;

                extern int mymax(int a, int b);
                extern int mymin(int a, int b);
        }
        #endif

    b.cpp文件
        #include <string>
        #include "b.h"

        extern int bregion::mymax(int a, int b)
        {
                return a>b ? a : b;
        }

        extern int bregion::mymin(int a, int b)
        {
                return a<b ? a : b;
        }

    a.cpp文件
        #include <iostream>
        #include <string>
        #include "b.h"

        using namespace bregion::mymax;//using声明
        using namespace bregion::mymin;//using声明
        //或者使用using指令
        //using namespace bregion;

        int main(int argc, char **argv)
        {                  
                std::cout << bregion::a << std::endl;
                std::cout << mymax(10, 20) << std::endl;
                std::cout << mymin(50, 40) << std::endl;

                return 0;
        }


(5)函数模板与命名空间
这与函数与命名空间其实是一样的,但是在一般情况下,模板会直接定义
在命名空间中,因为分开定义的和声明的话,在模板定义时需要制定export
关键字,但是很多编译器并不兼容该关键字,所以我们就不分开定义了。

a.h文件
    #ifndef H_B_H
    #define H_B_H

    namespace bregion
    {
            template <class T> T mymax(T a, T b)
            {
                    return a>b ? a : b;
            }

            template <class T> T mymin(T a, T b)
            {
                    return a<b ? a : b;
            }       
    }
    #endif

a.cpp文件
    #include <iostream>
    #include <string>
    #include "a.h"

    using namespace bregion;

    int main(int argc, char **argv)
    {
            std::cout << mymax(10, 20) << std::endl;
            std::cout << mymin(50, 40) << std::endl;

            return 0;
    }


(6)命名空间是可以扩展
比如在a.h中

namespace  A
{   

}

namespace  B
{   

}

/* 这是对第一个namespace  A扩展 */
namespace  A
{   

}
在多个地方进行命名空间内内容的声明都可以

(7)没有名字的命名空间
(1)定义格式
    namespace 
    {   
    }

(2)限制
    每一个文件(转换单元)只能有一个没有名字的命名空间,在本文件中
    如果出现了其它的没有名字的命名空间,认为是对第一个未命名空间的扩展。

(3)意义   
    c++中没有名字的命名空间表示这个命名空间的名称只在本文件(转换单元)
    内有效,其它文件无法使用,这与在c中直接使用static将全局变量和全局
    函数进行封锁是一样的效果,只是在c++中即延续了static的做法,又增加了
    没有名字命名空间的做法。



(8)给命名空间起别名
(1)给别名的目的
    当有些命名空间的名字很长的时候,就可以给一个非常简短的别名,
    便于记忆和使用。

(2)例子
    a.h文件
        #ifndef H_B_H
        #define H_B_H

        namespace project_model2_plane1
        {
                template <class T> T mymax(T a, T b)
                {
                        return a>b ? a : b;
                }

                template <class T> T mymin(T a, T b)
                {
                        return a<b ? a : b;
                }
        }

        /* 注意要放在命名空间的定义后面 */
        namespace alias = project_model2_plane1;
        #endif


    a.cpp文件
        #include <iostream>
        #include <string>
        #include "a.h"

        using namespace alias;

        int main(int argc, char **argv)
        {
                std::cout << mymax(10, 20) << std::endl;
                std::cout << mymin(50, 40) << std::endl;

                return 0;
        }

    例子说明:
    在本例子中给命名空间project_model2_plane1起了一个别名叫alias。
    使用命名空间时,使用alias时就可以。


(9)命名空间也可以嵌套包含
比如看下面这个例子:

a.h文件
    #ifndef H_B_H
    #define H_B_H

    namespace outer
    {
            template <class T> T mymax(T a, T b)
            {
                     return a>b ? a : b;
            }

            namespace inner
            {
                    template <class T> T mymin(T a, T b)
                    {
                            return a<b ? a : b;
                    }
            }
    }

    #endif

a.cpp文件
    #include <iostream>
    #include <string>
    #include "a.h"

    //using namespace outer;
    //using namespace outer::inner;

    int main(int argc, char **argv)
    {
            std::cout << outer::mymax(10, 20) << std::endl;
            std::cout << outer::inner::mymin(50, 40) << std::endl;

            return 0;
    }   
        例子分析:
        在本例中,outer是外部命名空间,在里面还有一个叫inner的内部命名空间
        ,在调用这两个命名空间里面的函数时,需要加outer::和outer::inner::
        的前缀。

    如果不希望加繁琐的亲啊最也可是使用using声明
    using namespace outer::mymax;
    using namespace outer::inner::mymin;
    或者using指令
    using namespace outer;
    using namespace outer::inner;


8. 预处理器详解
(1)作用
    预处理操作,这在前面已经提到过,是编译链接过程的第一个步骤。
    实际上预处理器是编译器的一个部分。

预处理的目的是为了处理所有#的预处理指令,预处理指令之所要加#,主要是
为了和c/c++中的正常指令区分开。

与处理完成后得到**.i文件才是真正完整意义上的c源文件。


(2)预处理指令汇总
    以下这些指令在c和c++中的作用完全一致。

(1)#include:文件包含,常见的是头文件包含 
(2)条件编译指令
    (1)
        #if
        #elif
        #else
        #endif
    (2)
        #ifdef 
        #ifndef

        #if defined
        #if !defined

(3)宏定义
    (1)#define:定义一个宏
    (2)#undef:删除一个宏

(4)#line:重新定义当前行号和文件名


(5)#error:输出编译出错消息,并且终止编译   

(6)#pragma:特殊操作指令


9. 预处理之文件包含#include
    (1)一般情况下,#include都是用来包含头文件的,但是实际上#include可以
        包含任何文件,只要程序能够通过,包含任何文件都可以,比如可以包
        含卡它的.c/.cpp文件。

    (2)预处理完成后,“#include 文件”语句会被展开后的文件内容替代。

    (4)一个#include只能包含一个文件,而且允许嵌套包含(间接包含)

    (5)包含头文件时<>和""的异同
        <>表示直接到指定目录下查找头文件,""表示先在自己制定的目下查看
        找不到才到系统目下查找,具体到系统那个目下查找往往是由编译器决定的。

        #include "/aa/dd/aa.h"
        表示先在制定的绝对路径下查找,找不到再到系统目录下查找。

        #include "aa.h"//同 #include "aa.h"
        表示现在当前目录下查找,找不到再到系统目录下查找。



10. 预处理之宏定义
(1)宏定义的作用范围
    宏定义的作用范围从定义的位置到本文件的末尾或者遇到#undef符号为止

(2)宏定义规则
    (1)宏名惯用大写字母,与变量名区别,小写命名
        宏定义也有,少见,比如stdin,stdout,__func__
    (2)宏定义不是语句,不需要在末尾加分号
    (3)只做替换,不分配空间


(3)宏定义需要注意的问题
    (1)宏定义不做类型和正确性检查
        经典例子:
        #define pi 3.1415q
        不会对3.1415q合法性做检查,只会替换,至于该数据合不合理只能由
        后面的编译检查。


(2)不能自己嵌套自己,但是可以嵌套其它宏定义
    嵌套其它宏定义时,其它宏定义的位置可以在后面,因为宏定义只做替换
    (1)嵌套自己是错误的
        #define R R*3

    (2)嵌套别的宏是可以的
        #define R 3.0
        #define PI 3.1415926
        #define AREAR PI*R*R

        下面这种写法也是对的
        #define AREAR PI*R*R
        #define R 3.0
        #define PI 3.1415926



(3)宏定义取消方法
    #define R 3.0


    #undef R

(4)宏定义到底有哪些好处
    (1)对于使用频繁的名称,可以使用宏定义替换,防止大量修改
    (2)简化复杂表达式,有利于降低代码复杂度
    (3)实现有条件编译,实现跨平台程序的实现
    (4)对于常量数使用宏定义来代替,提高代码的可理解性


(5)无参宏定义
(1)只有宏名没有宏体的宏,比如
    #define BUG

    这样的宏大都用于条件编译中,这在后面讲条件编译的时候会使用。

(2)有宏体宏
    #define PI 3.1415926
    #define myname "张三"

    常常用于定义符号常量. 

(6)带参宏定义
(1)带参宏举例
#define max(a, b) ((a)>(b)?(a):(b))

(2)带参宏作用:常常用于简化复杂问题

(3)使用带参数宏需要注意的一些问题
    (1)宏名与参数列表之间不要隔空格

    (2)带参宏定义中一定要使用括号保证正确性
        #define  POWER(R,R)  R*R 

        int va = POWER(1+2, 2+3);
        宏替换完成后,就变成了
        int va = 1+2 * 2+3;

        所以宏定义应该改为:
        #define  POWER  ((R)*(R)) 

    (4)带参宏与函数的区别
        (1)处理时间
            (1)宏的处理时间实在预编译的时候
            (2)函数是在运行时被处理的

        (2)有关参数
            (1)宏定义的参数只是为了实现替换,因此
                没有类型,因为没有类型,也就不会分配
                空间存放参数值,因为凡是有类型都会涉及
                变量或者常量空间。


            (2)函数有参数类型,有变量空间,如果传参是引
                用的话,实参形参共用变量空间。

    (5)是否改变程序长度
        (1)宏替换会改变程序长度
        (2)函数没有展开的过程,只有调用的过程,不会改变程序长度

    (6)是否占用运行时间
        (1)宏定义是在被预处理时处理的,不占用运行时间
        (2)显然函数是要占用运行时间

(2)带参宏与函数与内联函数
    (1)调用频繁的简短函数(2-5代码,没有循环),常常使用带参宏
        替换,因为没有函数的调用开销,但是程序的代码量会升高。
        所以是否使用带参宏替换函数,要认真权衡效率和内存消耗。

        #define MAX(a, b) ((a)>(b)?(a):(b))
        可以替换
        int max(int a, int b) {
            return a>b?a:b;
        }


    (2)带参宏替代简短函数的功能常被内联函数替代
        内联函数兼具带参宏的和函数的共同特点。
        (1)内联函数像向宏一样替换展开
        (2)内联函数跟普通函数一样会做参数类型检查,但是参数不会分配空间


    (3)带参宏都建函数复杂原型时,是内联函数不能替代的
        比如讲系统编程遇到的例子
        #ifndef ___RECODE_LOCK__
        #define ___RECODE_LOCK__

        #define read_lock(fd, l_whence, l_start, l_len) \
            lock_set(fd, F_SETLK, F_RDLCK, l_whence, l_start, l_len)
        #define read_lockw(fd, l_whence, l_start, l_len) \
            lock_set(fd, F_SETLKW, F_RDLCK, l_whence, l_start, l_len)
        #define write_lock(fd, l_whence, l_start, l_len) \
            lock_set(fd, F_SETLK, F_WRLCK, l_whence, l_start, l_len)
        #define write_lockw(fd, l_whence, l_start, l_len) \
            lock_set(fd, F_SETLKW, F_WRLCK, l_whence, l_start, l_len)
        #define unlock(fd, l_whence, l_start, l_len) \
            lock_set(fd, F_SETLK, F_UNLCK, l_whence, l_start, l_len)


        int lock_set(int fd, int l_ifwset, short l_type, short l_whence, \
            off_t l_start, off_t l_len)
        {   
            int ret = -1;
            struct flock f_lock;

            f_lock.l_type = l_type;
            f_lock.l_whence = l_whence;
            f_lock.l_start = l_start;
            f_lock.l_len = l_len;

            return(fcntl(fd, l_ifwset, &f_lock));

        }
        #endif


(3)带参数宏实现复杂表达式
    (1)例子1
        #define FUN(a,b) (((a)+(b))*((a)-(b)))/a*b


    (2)例子2
        #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

        #define container_of(ptr, type, member) ( { \
                const typeof( ((type *)0)->member ) *__mptr = (ptr); \
                (type *)( (char *)__mptr - offsetof(type,member) ); } )


11. 预处理之条件编译
(1)条件编译的作用
    (1)注销代码
    (2)防止文件重复包含
    (3)有选择性的编译代码内容,实现综合性的跨平台代码


(2)#if #elif #else #endif
    (1)集中组合
        (1)组合1      
            #if 条件 
                需要条件编译代码
            #endif


    (2)组合2
        #if 条件
            需要条件编译代码
        #else 
            需要条件编译代码
        #endif


    (3)组合3
        #if 条件
            需要条件编译代码
        #elif 条件
            需要条件编译代码
        #elif 条件
            ...
        #else 
            需要条件编译代码
        #endif


(2)需要注意的地方
    (1)如果条件为真,编译该条件下的代码。
    (2)如果多个条件为真,只有第一个为真的条件下的代码得到编译。
    (3)为真的条件必须是整形常量(非0为真,0为假)
    (4)为了增加条件的可读性,可以将常量定义为宏常量
    (5)不要将变量作为条件否者直接作为假条件
    (6)必须使用#endif结尾

(3)例子
    #define COND1 1
    #define COND2 3

    int a = 10;

    int main(int argc, char **argv)
    {
        #if a
                printf("这是满足编译条件1时的代码提示的内容\n");
        #elif COND2
                printf("这是满足编译条件2时的代码提示的内容\n");
        #elif 0
                printf("这是满足编译条件3时的代码提示的内容\n");
        #else
                printf("这是满足编译条件4时的代码提示的内容\n");
        #endif  
                return 0;
    }

    例子分析:
    #if a
    这里使用了a作为预编译的判断条件,这是不允许的,出现这样的情况
    的时候会直接将条件人为为假。

    #elif COND2
    这里使用了宏常量作为编译条件,COND2宏值为3,显然为真,主要是为
    了提高代码可识别性。

    #elif 0
    这里直接使用整形数0表示条件,显然0代表条件为假。

    #else
    当所有条件都不满足时就条件编译#else后面的内容。


(3)#ifdef #else #endif 与 #ifndef #else  #endif
(1)组合
    (1)#ifdef
        #ifdef 条件
            需要条件编译代码
        #endif

        或者

        #ifdef 条件
            需要条件编译代码
        #else
            需要条件编译代码
        #endif

    (2)#ifndef
        #ifndef 条件
            需要条件编译代码
        #endif

        或者

        #ifndef 条件
            需要条件编译代码
        #else
            需要条件编译代码
        #endif


(2)例子
    #include <iostream>
    #include <string>

    //#define MCU51 
    #define MCUSTM32 

    int a;
    int main(int argc, char **argv)
    {
        #ifdef MCUSTM32
                printf("用于51上的代码\n");
        #endif

        #ifndef MCU51
                printf("用于stm32上的代码\n");
        #endif

            return 0;
    }

    例子分析:

    #ifdef MCUSTM32
            printf("用于51上的代码\n");
    #endif
    如果定义了MCUSTM32宏,就编译接着的这段代码,显然这个宏是存在的。

    #ifndef MCU51
            printf("用于stm32上的代码\n");
    #endif
    #ifndef是以MCU51不存在为真,显然该宏定义不存在。


(3)需要注意的地方
    (1)#ifdef与#ifndef都是以判断宏名存不存在作为真假判断,所以
        有没有宏体没有任何关系,所以在上面的例子中,宏都是没有
        宏体的。

        不要使用变量作为判断条件,否者直接人为条件为假。

    (2)必须以#endif结尾
    (3)#ifdef常用用于构建跨平台的综合性程序
    (4)#ifndef常用于防止头文件重复包含。
        #ifndef H_XXX_H
        #denfine H_XXX_H
            头文件的内容
        #endif

        这样的定义可以有效的防止在一个转换单元中,同一个头文件被多次包含。



(3)#if defined #else #endif 与 #if !defined #else  #endif
(1)组合
    这个与#ifdef等的情况非常的类似。

(2)例子
    #include <iostream>
    #include <string>

    //#define MCU51 
    #define MCUSTM32 
    #define MCUARM

    int a;
    int main(int argc, char **argv)
    {
        #if defined MCU51
                printf("用于51上的代码\n");
        #endif

        #if defined MCU51 && MCUARM
                printf("用于51和MCUARM上的代码\n");
        #endif

        #if !defined MCU51
                printf("用于stm32上的代码\n");
        #endif

            return 0;
    }

(3)需要注意的地方
    (1)同样使用宏名是否存在作为判断真假的条件,所以可以没有宏体
    (2)不要使用变量作为条件,否者直接认为条件为假
    (3)从例子中我们很容易看出,完全可以替代#ifdef和#ifndef,
        但是反过来就不行,因为#if defined和#if !defined的方式是可以实现条
        件的&&和||等运算的,但是#ifdef和#ifndef却不行。

12. 预处理之常见标准预处理宏
(1)标注预处理宏清单
(1)__LINE__
    当前代码行号,十进制整形数

(2)__FILE__
    源文件的名称,字符串常量(字符串字面量)

(3)__func__
    当前所处的函数的名称

(4)__DATE__
    源文件被处理的日期,是字符串常量,格式为mmm dd yyyy

(5)__TIME__
    源文件被编译的时间,字符串常量,格式为hh::mm::ss

(2)例子
#include <iostream>
#include <string>

int main(int argc, char **argv)
{   
        std::cout << "file:" <<  __FILE__ << std::endl;
        std::cout << "line:" <<  __LINE__ << std::endl;
        std::cout << "func:" <<  __func__ << std::endl;
        std::cout << "date:" <<  __DATE__ << std::endl;
        std::cout << "time:" <<  __TIME__ << std::endl;

        return 0;
}

运行结果:
file:a.cpp
line:9
func:main
date:Aug  3 2016
time:11:48:05



13. 预处理之#error和#pragram预处理指令
(1)#error
打印预处理时导致的错误,并且终止程序编译

(2)例子
比如我想知道在某个头文件中是否有定义某个名称的宏,比较笨的做法就是
打开头文件一行一行的查找,还有一个办法就是#error打印提示,但是需要
和#ifdef结合使用。

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>

#ifdef stdin
#error stdin be defined!!
#else
#error stdin not be defined!!
#endif

int main(int argc, char **argv)
{               
        return 0;
}

例子分析:
显然这个stdin在stdio.h的头文件中是有定义的,因此#error的输出结果为
#error "stdin be defined!!"。


(2)#pragram
    作用非常多,比如实现变量存储空间的手动对其,一般都是自动对齐,这一个的用法略。


14. 断言assert()
(1)为什么使用断言
    assert()是一个宏,c和c++都支持这个关键字,往往用于程序非常关键的位置
    进行异常提示的,所谓断言就是我断定某件事情在正常的程序逻辑中是不应该
    发生的,但是还是发生了,那么这就是非常严重的错误,需要处理,这种严重
    的错误就需要提示具体信息,在这种情况下我们常常使用断言来操作。

使用断言需要包含cassert宏。


(2)例子
#include <iostream>
#include <string>
#include <cassert>

int main(int argc, char **argv)
{               
        int a = 5;

        assert(a < 5);

        return 0;
}

运行结果:
int main(int, char**): Assertion `a < 5' failed.已放弃

例子分析:
程序中断言a应该小于5的,但是a实际上违反了这个规则,所以断言及时终止
了程序,并提示该处断言被打破。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值