C語言強、弱符號,強、弱引用


C語言強、弱符號,強、弱引用

符號定義

在編程中我們經常碰到符號重復定義的情況,當我們在同一個作用域內重復定義同一個變量時,有時是因為誤寫,有時是文件之間的沖突,編譯器的處理方式就是報錯:

redefinition of 'xxx'

注意,這里針對於同一作用域才會有沖突,如果是不同作用域,比如全局和局部,即使是相同變量名,也是不會報錯,編譯器會默認根據一定的優先級處理,總是更小作用域的變量覆蓋更大作用域的變量,前提是這兩個變量的作用域是包含與被包含的關系。

初學C語言的朋友對於作用域的划分有一定的誤解,認為函數是划分作用域的界限,函數外的就是全局,函數內的就是局部。
事實上,在C語言中,作用域的分類方式是代碼塊作用域和文件作用域,文件作用域即定義在函數之外的的變量可以跨文件訪問,代碼塊作用域指的是由花括號限定的作用域,它不僅僅限於函數體,在C99中將其擴展到了for,while,if語句所控制的代碼,甚至可以在函數內以單獨的花括號存在,我們不妨看看以下的示例:
示例1:

int main()
{
    {
        int x=1;
    }
    x = 2;
}

編譯輸出的結果是:

error: ‘x’ undeclared (first use in this function)

示例2:

for(int i=0;i<4;i++){}
i = 1

輸出的結果是:

error: ‘x’ undeclared (first use in this function)

在C語言中,我們可以簡單地認為花括號是文件內作用域的分隔符。

強符號和弱符號

在同一作用域下不能定義同一個變量或函數,很多C語言學習者都理所當然地這么認為。

這個其實是是有所偏頗的,GNU C對標准C語言進行了擴展,在GCC中,對於符號(在編譯時,變量和函數都被抽象成符號)而言,存在着強符號和弱符號之分。
是的,是否支持這個特性是由不同的C語言標准決定的。

對於C/C++而言,編譯器默認函數和已初始化的全局變量為強符號,而未初始化的全局變量為弱符號,在編程者沒有顯示指定時,編譯器對強弱符號的定義會有一些默認行為,同時開發者也可以對符號進行指定,使用"attribute((weak))"來聲明一個符號為弱符號。

定義一個相同的變量,當兩者不全是強符號時,gcc在編譯時並不會報錯,而是遵循一定的規則進行取舍:

  • 當兩者都為強符號時,報錯:redefinition of 'xxx'
  • 當兩者為一強一弱時,選取強符號的值
  • 當兩者同時為弱時,選擇其中占用空間較大的符號,這個其實很好理解,編譯器不知道編程者的用意,選擇占用空間大的符號至少不會造成諸如溢出、越界等嚴重后果。

在默認的符號類型情況下,強符號和弱符號是可以共存的,類似於這樣:

int x;
int x = 1;

編譯不會報錯,在編譯時x的取值將會是1.

但是使用__attribute__((weak))將強符號轉換為弱符號,卻不能與一個強符號共存,類似於這樣:

int __attribute__((weak)) x = 0;
int x = 1;

編譯器將報重復定義錯誤。

強引用和弱引用

除了強符號和弱符號的區別之外,GNUC還有一個特性就是強引用和弱引用,我們知道的是,編譯器在編譯階段只負責將源文件編譯成目標文件(即二進制文件),然后由鏈接器對所有二進制文件進行鏈接操作。

在分離式編譯中,當編譯器檢查到當前使用的函數或者變量在本模塊中僅有聲明而沒有定義時,編譯器直接使用這個符號,將工作轉交給鏈接器,鏈接器則負責根據這些信息找到這些函數或者變量的實體地址,因為在程序執行時,程序必須確切地知道每個函數和全局變量的地址。如果沒有找到該符號的實體,就會報undefined reference錯誤,這種符號之間的引用被稱為強引用.

編譯器默認所有的變量和函數為強引用,同時編程者可以使用__attribute__((weakref))來聲明一個函數,注意這里是聲明而不是定義,既然是引用,那么就是使用其他模塊中定義的實體,對於函數而言,我們可以使用這樣的寫法:

__attribute__((weakref)) void func(void);

,然后在函數中調用func(),如果func()沒有被定義,則func的值為0,如果func被定義,則調用相應func,在《程序員的自我修養》這本書中有介紹,它是這樣寫的:

__attribute__((weakref)) void func(void);
void main(void)
{
    if(func) {func();}
}

但是在現代的編譯系統中,這種寫法卻是錯誤的,編譯雖然通過(有警告信息),但是卻不正確:

warning: ‘weakref’ attribute should be accompanied with an ‘alias’ attribute [-Wattributes]

警告顯示:weakref需要伴隨着一個別名才能正常使用。

既然書籍有版本問題,那么唯一的辦法就是去查官方文檔,在官方文檔中是這樣指出的:

The weakref attribute marks a declaration as a weak reference. Without arguments, it should be accompanied by an alias attribute naming the target symbol. Optionally, the target may be given as an argument to weakref itself. In either case, weakref implicitly marks the declaration as weak. Without a target, given as an argument to weakref or to alias, weakref is equivalent to weak.
At present, a declaration to which weakref is attached can only be static.

貼出稍為重要的部分,通俗地解釋就是:

  • weakref需要伴隨着一個別名,別名不需要帶函數參數,如果對象函數沒有定義,我們可以使用別名來實現函數的定義工作,如果不指定別名,weakref作用等於weak。在后面我們會給出相應的示例以助理解。
  • weakref的聲明必須為靜態。

強/弱符號和強/弱引用的作用

這種弱符號、弱引用的擴展機制在庫的實現中非常有用。我們在庫中可以使用弱符號和弱引用機制,這樣對於一個弱符號函數而言,用戶可以自定義擴展功能的函數來覆蓋這個弱符號函數。

同時我們可以將某些擴展功能函數定義為弱引用,當用戶需要使用擴展功能時,就對其進行定義,鏈接到程序當中,如果用戶不進行定義,則鏈接也不會報錯,這使得庫的功能可以很方便地進行裁剪和組合。

應用示例

在示例中,我們使用一個靜態庫作為示例,來展現弱符號和弱引用的用法。

弱符號的使用

test.c:

#include <stdio.h>
void __attribute__((weak)) weak_func(void)
{
    printf("defualt weak func is running!\n");
}

void test_func(void)
{
    weak_func();
}

test.h:

void test_func(void);

main.c:

#include <stdio.h>
#include "test.h"

void weak_func(void)
{
    printf("custom strong func override!\n");
}

int main()
{
    test_func();
    return 0;
}

將test.c編譯成靜態庫;

gcc -c test.c
ar -rsc libtest.a test.o

編譯main.c:

gcc main.c test.h -L. -ltest -o test

-L表示指定靜態庫的目錄,如果不指定目錄,編譯器會去系統目錄查找,如果系統目錄沒有將報錯。

-l表示鏈接對應的靜態庫,在linux下靜態庫的名稱統一為libxxx.a,在鏈接時只需要使用-lxxx即可。
執行程序:

./test

輸出結果:

custom strong func override!

很明顯,在test庫中,我們定義了weak_func函數,且weak_func是一個弱符號,且在test_func中被調用,然后在main.c中,我們重新定義了test_func函數,且是個強符號,所以在鏈接時,鏈接器選擇鏈接main.c中的test_func函數。

如果我們將main.c中weak_func函數定義去掉,它的執行結果將是:

defualt weak func is running!

喜歡動手的朋友可以試試。

弱引用的使用

test.c:

#include <stdio.h>
static __attribute__((weakref("test"))) void weak_ref(void);
void test_func(void)
{
    if(weak_ref){
        weak_ref();
    }
    else{
        printf("weak ref function is null\n");
    }
}

test.h:

void test_func(void);

main.c:

#include <stdio.h>
#include <stdarg.h>
#include "test.h"
void test(void)
{
    printf("running custom weak ref function!\n");
}

int main()
{
    test_func();
    return 0;
}

將test.c編譯成靜態庫;

gcc -c test.c
ar -rsc libtest.a test.o

編譯main.c:

gcc main.c test.h -L. -ltest -o test


./test

輸出結果:

running custom weak ref function!\n

在test靜態庫中,我們僅僅聲明了靜態的weak_ref函數,且聲明時使用了函數別名test,因為是靜態聲明,這個函數名的作用域被限定在本模塊內,所以即使是我們在其他模塊中定義了weak_ref函數,也無法影響到當前模塊中的weak_ref函數。

官方提供的方法是我們可以定義它的別名函數來代替,如上所示weak_ref的別名為test,所以在main.c中定義了test函數就相當於定義了weak_ref函數,所以在test_func的判斷分支中,test_func中不為null,執行if(test_func)分支,如果在main.c中去除weak_ref的定義,函數的執行結果是這樣的:

weak ref function is null\n

喜歡的動手的朋友的試試,同時試試帶參數的函數,這樣可以加深理解。
至於為什么要使用別名來實現,將目標函數聲明為靜態,而不是直接使用當前名稱實現,博主尚未找到答案,如果有路過的大神希望指點一二。

好了,關於C語言中強弱引用和強弱符號的討論就到此為止啦,如果朋友們對於這個有什么疑問或者發現有文章中有什么錯誤,歡迎留言

原創博客,轉載請注明出處!

祝各位早日實現項目叢中過,bug不沾身.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM