zoukankan      html  css  js  c++  java
  • Coding之路——重新学习C++(3):对于编译和链接的重新认识

    1.C++的源代码是怎么变成程序的。

      (1)我们在编写完源代码后,首先需要把源代码交给编译器,编译器首先进行预处理,也就是处理宏,把#include指令引进的头文件全部引进,产生编译单元。编译单元是编译器的真正工作对象,是真正意义上的C++对象。

      (2)一般的编译模式会采用分别编译,这时我们必须保证所有的声明具有一致性,连接器程序帮助我们把所有编译的部分都约束在一起,让所有的对象、函数没有二义性,这些都在程序运行前结束。当然,也有可以再程序运行后加入新代码(动态连接)。

    2.连接时的二三事(必须保证所有的声明引用同一实体)

      (1)当一个变量前面有extern时是声明,但是没有extern之后就是定义。声明可以有多次,但是必须一致,定义只能存在一次。

    //file1.c
    int x = 1;
    int b = 1;
    extern int c;
    
    //file2.c
    int x;                    //错误:相当于 int x = 0,这样x定义了两次
    extern double b;          // 错误:声明的类型与file1中定义的类型不一致
    extern int c;             // 错误:只有声明,没有定义
            

      (2)一个inline函数需要在每个使用它的编译单元中定义,并且定义必须相同。所以inline函数不能使用extern声明。

    //file1.c
    inline int f(i){return i;}
    extern inline int g(i);
    int h(int i){return g(i);}    //错误:g(i)没在这个编译单元中定义
    
    //file2.c
    inline int f(i){return i+1;} //错误:file2与file1中的f(i)定义不同

      (3)const与typedef都是内部连接,也就是说作用域只在名字所在的编译单元中使用,一般为了保证一致性,把全局const和inline函数放在头文件中。通过extern可以使const获得外部链接属性。

    //file1.c
    extern const int a = 77;
    
    //file2.c
    extern const int a;
    
    void f(){
        cout<<a<<endl;
    }

      (4)当你在没有名字的namespace中也会获得内部连接属性。

    3.头文件怎么用

      (1)"#include<T>"代表的是包含的标准库头文件,“ include"T.h" ”表示包含的头文件来自本地文件。

      (2)在头文件中绝对不能有常规函数定义、常规变量定义、数组定义、没有名字的namespace和导出的模板定义。

      (3)单一定义规则(ODR):标准中一个类、模板等只能有唯一的定义。

    //file1.c
    struct S{        //正确:1.两个struct S定义在不同的编译单元中
        int a;             //正确:2.它们按照单词一一对应相同
        int b;             //正确:3.单词的语义在两个文件中也一致
    }
    void f(S*);
    
    //file2.c
    struct S{        //所以这两个struct S定义被接受为同一个唯一定
        int a;           //义实例
        int b;
    }
    void f(S* p){/*...*/};

      (4)当只存在一个声明时,可以使用export模板。

    //file1.c
    export template<class T> T twice(T t){return t+t;}
    
    //file2.c
    template<class T>  T twice(T t);
    int g(int i ){return twice(i);}

      (4)为了避免同一个头文件包含多次,我们使用头文件保护符。

    //error.h 
    //当error.h在编译时第一次被看到,它的内容被读入,给CALC_ERROR一个值
    //当再次编译时,它的内容就被忽略
    #ifndef CALC_ERROR_H #define CALC_ERROR_H namespace error{ //... } #endif

    但是我们还是要尽量减少包含依赖,头文件保护符使用过度会引入大量的声明和宏定义并且导致编译时间过长。

    4.怎么连接非C++代码(以C为例)

      我们一般用extern给出与非C++代码的连接约定。例如想要连接C语言的代码,我们使用extern "C"来作为一种连接约定,并不影响函数语义,但是仍然得遵守C++的类型检查和参数转换。我们还采用连接块包含C头文件和代码,使整个文件能被C++使用。

    extern "C"{
        #include <string.h>
        char *strcpr(char *, const char*);
        int g1;
        extern int g2;
    }

      下面是使用函数指针与连接的方式:

    extern "C" {
        typedef int (*CFT)(const void*, const void*);
        void qsort(void *p, size_t n, size_t sz, CFT cmp); //cmp具有C连接  
    }
    
    int compare (const void*, const void*);    //具有C++连接
    extern "C" int c_compare (const void*, const void*);    //具有C连接
    
    void f(char * v, int sz){
        qsort(v, sz, 1, compare);    //错误
        qsort(v, sz, 1, c_compare);    //正确
    }

    5.全局变量还是少用

      对于不同编译单元中的全局变量,初始化的顺序没有人能保证,尽量减少全局变量的使用。通过函数返回的引用可以作为全局变量的替代方式:

    int& count(){
        static int uc = 0;
        return uc;
    }
    
    void f(){
        for(int i = 0; i <10; i++){
            cout<<++count()<<endl;
        }
    }

      

  • 相关阅读:
    1G→2G→3G→4G→5G:一部波澜壮阔的移动通信史
    Android-X86 创始人
    如果Android 8.0的代码重新改写,那么Fuchsia OS的意义何在?
    (OK) Install php 5.6 in fedora-27
    父亲
    魏永明: MiniGUI的涅槃重生之路
    LeetCode 374. Guess Number Higher or Lower
    LeetCode 278. First Bad Version
    LeetCode 35. Search Insert Position
    查找算法-二分查找
  • 原文地址:https://www.cnblogs.com/xskCoder/p/3989605.html
Copyright © 2011-2022 走看看