C C extern keyword

最近在參與面試的時候,我方考了 extern 關鍵字,雖然這個關鍵字很常用,但是很少有應試者答的完整。因為自己也是韌體新手,因此就參照 ISO/IEC 9899:2011(C11) 的標準,把 extern 的定義和用法紀錄一下。

Declaration & Definition

在討論 extern 之前,先釐清 C 的 declaration 和 definition 差異性。在 C11: 6.7 Declarations 中有提到:

Declaration

A declaration specifies the interpretation and attributes of a set of identifiers.

Definition

A definition of an identifier is a declaration for that identifier that:

  • for an object, causes storage to be reserved for that object;
  • for a function, includes the function body;
  • for an enumeration constant or typedef name, is the (only) declaration of the identifier.

其中明顯的差異,除了 function definition 會包含 body { } 之外,對 variable 來說,declaration 僅是標示了 variable 的名稱和特性,definition 才會真正分配對應的儲存空間。

此外,相同 variable 可以多次 declaration,但是 definition 只能有一次。舉例來說:

1// parent.h
2#ifndef _PARENT_H
3#define _PARENT_H
4
5extern int a; // declaration
6
7void print_parent(void);
8#endif
 1// parent.c
 2#include <stdio.h>
 3#include "parent.h"
 4
 5int a = 1; // definition
 6
 7void print_parent(void)
 8{
 9 printf("parent %d",a);
10}
 1// child.c
 2#include <stdio.h>
 3#include "parent.h"
 4
 5int a = 2; // definition
 6
 7void print_child(void)
 8{
 9printf("child %d",a);
10}

gcc 時會出現:

1/usr/bin/ld: /tmp/ccSHHFEz.o:(.data+0x0): multiple definition of `a'; /tmp/ccjpc0mC.o:(.data+0x0): first defined here
2collect2: error: ld returned 1 exit status

由於在 parent.cchild.c 皆有 a definition,因此 gnu gcc 在 link 階段會產生重複 definition 的錯誤訊息。

我們把 child.c code 改成:

1#include <stdio.h>
2#include "parent.h"
3
4extern int a; // declaration
5
6void print_child(void)
7{
8  printf("child %d",a);
9}

即使在 parent.hchild.c 都有 extern int a;,但因為兩者都是 declaration,因此可以正確 compile。

在 C11: 6.2.2 Linkages of identifiers 提到:

An identifier declared in different scopes or in the same scope more than once can be made to refer to the same object or function by a process called linkage.

根據以上敘述,就能知道上述例子可以 compile 成功的原因,及即使在多個 source file 有相同命名的 variable declaration,在 link 階段會將他們關聯為同一個物件。

我們透過觀察 link map (gcc -Wl,-M) 來查看 variable a link 狀況:

1# section, memory, size
2 .data 0x0000000000004010 0x0 /tmp/ccALo656.o # child.o
3 .data 0x0000000000004010 0x4 /tmp/cc3dGS27.o # parent.o
4       0x0000000000004010     a

由於 a definition 是在 parent.c,因此可以看到 0x4 的空間分配,並且 child.o 和 parent.o 的 variable a 會連結成相同物件。

Linkages of identifiers

 由於 extern 關鍵字與 Linkages 有關係,因此也來複習一下 Linkages:

  1. external linkage:一個 identifier 具有 external linkage 屬性時,同一個 program 中不同 translation unit 如果包含此 identifier,則會視為同一個物件。
  2. internal linkage:反之,如果是 internal linkage,則同一個 translation unit 的 同名 identifier 會視為同一個物件。
  3. no linkage:像是 function 的參數或是 local 變數,則歸類為 no linkage 屬性。

例子:

1int external_var = 2; // external linkage
2static int internal_var = 1; // internal linkage
3
4int add(int a, int b) // no linkage
5{
6  return a + b;
7}

extern keyword

結合上述的資訊,現在來看 extern 關鍵字有什麼特性:

1.

extern declarations, which don’t change the linkage of an identifier if a previous declaration established it (more).

If the prior declaration specifies internal or external linkage, the linkage of the identifier at the later declaration is the same as the linkage specified at the prior declaration. If no prior declaration is visible, or if the prior declaration specifies no linkage, then the identifier has external linkage (C11: 6.2.2 Linkages of identifiers).

1// example from https://en.cppreference.com/w/c/language/extern
2static int i4 = 2; // definition, internal linkage
3extern int i4;     // declaration, refers to the internal linkage definition

2.

A variable declaration that uses extern and has no initializer is not a definition.

參考一開始所給的例子,即使在不同 tranlation unit 中有 extern int a;,但因為都沒有 initializer,所以只是 declaration,在 link 階段會將這些同名 declaration 連結成同一個物件。

3.

extern is a storage-class specifiers. Items declared with the extern specifier have global lifetimes.

在 C11 中,extern 其實是 storage-class specifiers,也就是標示生命週期和 linkage 類型。

The storage-class specifiers determine two independent properties of the names they declare: storage duration and linkage. (more)

預設情況下,使用 extern 代表此 identifer 具有 global lifetimeexternal linkage 兩個屬性。

Tentative definitions

先看以下的例子:

1# parent.h
2#ifndef _PARENT_H
3#define _PARENT_H
4
5int a;
6
7void print_parent(void);
8
9#endif
 1# parent.c
 2#include <stdio.h>
 3#include "parent.h"
 4
 5int a = 1;
 6
 7void print_parent(void)
 8{
 9  printf("parent %d",a);
10}
 1# child.c
 2#include <stdio.h>
 3#include "parent.h"
 4
 5int a;
 6
 7void print_child(void)
 8{
 9  printf("child %d",a);
10}
11
12int main(void)
13{
14  print_parent();
15  print_child();
16  return 0;
17}

即使在多處 declare int a;,但可以正確 compile,且最後output 會是 parent 1child 1,也就是 parent 和 child 的 translation unit 都是使用同一個 a 變數。其中最主要的原因是 int a; 是一個 tentative definition

在 C11: 6.9.2 External object definitions 提到:

A declaration of an identifier for an object that has file scope without an initializer, and without a storage-class specifier or with the storage-class specifier static, constitutes a tentative definition.

Tentative definition 是 C 特別的規則 (C++ 無此規則),compiler 會合併相同命名的 tentative definition 成一個 definition,所以實作上可以透過此方式來達到多個 translation unit 使用同一個變數的目的。此用意主要是能兼容早先沒有 extern keyword 的 c 程式。

結語

exten 是 storage-class specifiers 的一種,其涉及到 storage duration 和 linkage。實務上我們用 extern 來達成不同 translation unit 共用同一個 global variable 的目的,而在了解 extern 背後運作原理後,對於 variable definition 也會有更清楚的認知~