extern,extern C,头文件中可否定义变量,static,const等汇总

11 篇文章 1 订阅
5 篇文章 0 订阅

extern , extern “C” 作用, 该不该在头文件中定义全局变量? 以及C中的const static,C++中的static 和 const区别联系.

extern

首先说明extern,一般来说extern的作用是告诉编译器,我要引用别的模块定义的变量或者函数啦. 即extern xxx;只用来声明,而不定义.
一般用法如下:
对于extern 变量:

A.c
#include <stdio.h>
extern int a;  //声明我要使用其他模块定义的a
int main() {printf("%d\n",a);}

B.c
int a = 10;  //定义a

即A.c中是的extern int a ;告诉编译器,我只声明这个变量a,它是其他模块的,我要引用它.而不定义它,你不用为它分配空间.
在连接阶段,即unix下是生成.o文件,windows下生成.obj文件.经过符号重定位,链接器将A.o中的aB.o中的符号a关联.成功引用.
注意这里,只有两个单独的c文件.在进行编译时,每个.c文件都是单独编译的,生成单独的.o文件.之后通过链接器进行符号解析和重定位等生成可执行文件.

对于extern 函数:

A.c
#include <stdio.h>
extern int a;
extern void f1();  //声明要使用其他模块定义的f1()
int main() {printf("%d\n",a); f1();}

B.c
int a = 10;
//定义f1()
void f1() {printf("this is f1()");}

其实对于变量还是函数,在链接器看来都是符号,extern的目的就是避免符号的重定义.

.h

再来谈谈.h,文件中定义全局变量.在实验时发现一个问题,就是假如我们在.h文件中定义一个全局变量.并在多个c文件中都包含这个.h文件.如下:

C.h
#ifndef __C_H_
#define __C_H_
int a = 10;
#endif

D.h
#include "C.h"

A.c
#include <stdio.h>
#include "C.h"
#include "D.h"
int main() {printf("%d\n",a);}

B.c
#include "C.h"
#include "D.h"

即使有头文件保护符,在编译阶段没出错,而在链接阶段出现问题.
unix/g++: multiple definition of 'a'
windows/msvc: 找到一个或多个重定义符号

我们来分析一下这个.h文件看看这个头文件保护符为何不起作用:
从源文件到可执行文件,要经历"预处理"->"编译"(高级语言.c->.s汇编语言)->"汇编"(汇编语言.s->.o二进制)->"链接(.o->可执行二进制文件)"
预处理步骤中:

  1. include的文件原封不动的替换.即这个时候就不存在.h文件了,全是.c/.cpp文件.
  2. 将宏进行替换.

所以我们发现,经过预处理后A.cppB.cpp文件由于保护符的作用,都是只含有一句int a = 10.好的,说明头文件保护符确实起了作用.
但是头文件保护符作用也仅限于此.
至此,编译阶段是没有任何问题的.由于源文件都是独立编译的,故也各自得到.o文件.即在A.o中有符号a,存在.data中,但而在B.o中也有符号a,存在.data中.
故到了链接阶段,要符号解析.这两个a都是全局的,强符号,那么就显然出现了符号重定义.

所以这也是为何我们不能在.h文件中定义全局变量的原因.那该如何使用.h文件呢? 我们只能在.h文件中进行声明,也就是extern int a;然后在相应的一个.cpp文件中进行定义int a;,其他模块想用这个变量的话,就包含这个.h文件即可.如下:

正确 √
C.h 
#ifndef __C_H_
#define __C_H_
extern int a;  //声明
#endif

A.cpp
#include "C.h"
//这个C.h就相当于 extern int a;这句话
int main() {cout<<a;}

B.cpp
#include C.h //包不包含都不影响
int a = 10;  //定义.

同样,.h文件也不能定义函数,否则同样会出现符号重定义.如下:

错误 ❌
C.h 
#ifndef __C_H_
#define __C_H_
void f1() {}  //定义了f1()函数符号.
#endif

A.cpp
#include "C.h"
int main() {return 0;}

B.cpp
#include C.h 

在链接阶段:报错,符号重定义.
正常使用.h文件: 在头文件中只声明这个函数,而在对应的源文件中去定义.而其他源文件要想使用这个函数,包括头文件即可.如下:

正确 √
C.h 
#ifndef __C_H_
#define __C_H_
extern void f1();  //声明f1()函数符号.extern 这里加不加无影响.
#endif

A.cpp
#include "C.h"
int main() { f1(); return 0;}

B.cpp
#include C.h 
void f1() {}  //定义f1()函数符号

但是注意有几个例外的情况,是可以在.h文件中定义的:

  • 在编译期间就知道值的const变量,可以在头文件中定义.
编译链接正确 √
C.h
#ifndef __C_H_
#define __C_H_
const int a = 10;  //并且在定义时就要初始化.
#endif

A.cpp
#include "C.h"
int main() {cout<<a;}

B.cpp
#include "C.h"

这里我们就发现和上面只有一个地方不同,即const int 和 int,那么为什么这个链接阶段就没有出现问题呢? 我们稍候分析!

  • 类的定义可以放在.h中,这种情况比较常见.
  • inline函数定义在.h中,这种情况也比较常见.

extern "C"

在c++的程序中,假如我们想使用一段C编写的代码或者是其他语言编写的代码,要使用另外一个编译器进行编译链接某段特定的代码时,我们该怎么做呢?

这时,我们就使用extern "C",这个"C",表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortranassembler等.

  • 注意,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern “C”,仍然要遵守C++的类型检测、参数转换规则.
  • 在C++源文件中的语句前面加上extern “C”,表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等.(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言).
  • 我们既然知道extern "C"是实现的类C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白C和C++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。
  1. C++的编译链接
    C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

    void print(int i);
    void print(char c);
    void print(float f);
    void print(char* s);
    

    编译为:

    _print_int
    _print_char
    _print_float
    _pirnt_string
    

    这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找_print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
    C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

  2. C的编译链接
    C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用print(3),链接器会去找_print_int(3)。而不会去链接_print,因此extern "C"的作用就体现出来了。

  3. C++中调用C代码
    示例如下:

    cfunc.h
    #ifndef __CFUNC_H
    #define __CFUNC_H
    void myprint(int i);
    #endif
    
    cfunc.c
    #include "cfunc.h"
    void myprint(int i) {printf("cheader %d",i);}
    
    main.cpp
    #include <stdio.h>
    //或者extern "C" void myprint(int i);不包含头文件
    extern "C" {
        #include "cfunc.h"
    }
    int main() {
        myprint(2);
        return 0;
    }
    
  4. C中调用C++代码
    C中调用C++代码,有略微不同,如下:

    cppfunc.h
    #ifndef __CPPFUNC_H
    #define __CPPFUNC_H
    #include <iostream>
    extern "C" void myprint(int i);
    #endif
    
    cppfunc.cpp
    #include "cppfunc.h"
    void myprint(int i) {std::cout<<"C++ "<<i;}
    
    main.c
    #include <stdio.h>
    //错误❌用法: extern "C" void myprint(int i); 编译不通过,"C"应输入标识符
    //错误❌用法: #inlcude cppfunc.h , 和上句一样.
    //正确用法: extern void myprint(int i);
    int main() {
        myprint(2);
        return 0;
    }
    
    1. gcc和g++
      对于34中,我们可能会疑惑使用gcc还是g++编译链接呢?
      这二者区别并不大:
    • gcc将编译:* .c / * .cpp文件分别看待为C和C ++.
    • g ++将编译:* .c / * .cpp文件,但它们都将被视为C ++文件.
    • 此外,如果您使用g ++链接目标文件,它会自动链接到std C ++库(gcc不会这样做).
    • gcc compiling *.cpp and g++ compiling .c/.cpp files has a few extra macros.
      总结来说:
      g++约等于gcc -xc++ -lstdc++ -shared-libgcc(第一个是编译器选项,第二个是链接器选项)。 这可以通过使用-v选项运行来检查(它显示正在运行的后端工具链命令)。
      这里只是约等,因为gcc -lstdc++没有对math,RTTI,expection information等的支持.
      故我们对于c++程序,直接用g++编译链接即可.如果是混合编程,就要注意链接时要关联stdc++的库.
      参考回答: https://stackoverflow.com/questions/172587/what-is-the-difference-between-g-and-gcc/172592#172592

C和C++中的const static

  1. C中的static关键字
    在探讨C中的static关键字中,有必要了解一下变量在内存存放的区域.
    一般来说可以内存结构如下:
    内核栈
    栈↓
    .
    .
    共享库代码
    .
    .
    堆↑
    .data(已初始化数据段) .bss(未初始化数据段)
    .text(代码段)
    保留区
    
    • 局部变量,函数参数等都存放在栈区.
    • 用户开辟(malloc new)的变量存放在堆区.
    • static变量(无论是否是局部还是全局), 全局变量都存放在.data和.bss中即数据区中.

所以static关键字作用其一: 修饰的变量,生命周期一直到程序结束.无论这个变量是局部的还是全局的.如下:

void fun() {
    static int i = 10;
    i++;
    printf("%d\n",i);
}

第一次调用fun()时,定义并初始化了一个static局部变量i,调用结束后.因为这个局部变量没有存放在栈中,故这个i不会消亡,而是一直存放在.data中. 下一次再调用func()时,就不会再定义初始化了,因为已经有i了,所以第二次调用会输出12.

在这里注意,在数据区.data .bss中,不存在符号重定义.即看如下例子:

static int i = 5;

void f1() {
  static int i = 2;
  printf("f1: %d\n", ++i);
}

void f2() {
  static int i = 3;
  printf("f2: %d\n", ++i);
}

int main(int argc, char **argv) {
  f1();
  f2();
  f1();
  f2();
  printf("%d\n", i);
  //fA();
  return 0;
}

虽然定义了3个都叫istatic变量,但在.data段中,是不同的符号,即i.1,i.2,i.3.故上述不存在重定义的问题.输出:

f1: 3
f2: 4
f1: 4
f2: 5
5

static关键字作用其二: 隔离作用域.
static修饰后的符号,都只能在当前模块定义并引用.其他模块无法访问引用.包括变量和函数.即:

A.cpp
static int i = 10;  //全局static变量,但只在A.cpp中可见
static void fA() {};

B.cpp
extern int i;  //错误,无法解析符号
extern void fA();  //错误,无法解析符号

所以,这实际上提供了保护功能.即两个文件如果有同名的函数,变量.也由于作用域隔离互不影响,因为只能在本文件内可见.

  1. C语言中的const:
    C中的const修饰的作用,只有一点.即使这个变量的值,在定义时确定,在整个生命周期中不能更改.
    const变量在定义时必须初始化,比如我们想要一个值为5的int常量.
const int i = 5;  //正确

const int i;
i = 5;  //错误,不能更改常量的值.
//实际上const int i;已经对i默认初始化了.

注意它与static的区别,const 修饰局部变量,那么这个变量仍然放在区中,而不是static修饰局部变量的效果.当然const 修饰全局变量的话,那这个变量就放在.data段中.

  1. C++语言中的static和const
    C++中的static和const继承C语言中的特性外,最重要的是它们在这个C中没有的东西上面的用法.
    故详情再看这篇博客,c++中的static和const.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值