C語言進階指南(2)丨數組和指針、打樁

来源:https://www.cnblogs.com/L928/archive/2019/04/06/10662773.html
-Advertisement-
Play Games

三、指針和數組 儘管在某些上下文中數組和指針可相互替換,但在編譯器看來二者完全不同,並且在運行時所表達的含義也不同。 當我們說對象或表達式有類型的時候,我們通常想的是定位器值的類型,也叫做左值。當左值有完全non-const類型時,此類型不是數組類型(因為數組本質是記憶體的一部分,是個只讀常量,譯者註 ...


三、指針和數組

儘管在某些上下文中數組和指針可相互替換,但在編譯器看來二者完全不同,並且在運行時所表達的含義也不同。

當我們說對象或表達式有類型的時候,我們通常想的是定位器值的類型,也叫做左值。當左值有完全non-const類型時,此類型不是數組類型(因為數組本質是記憶體的一部分,是個只讀常量,譯者註),我們稱此左值為可修改左值,並且此變數是個值,當表達式放到賦值運算符左邊的時候,它被賦值。若表達式在賦值運算符的右邊,此變數不必被修改,變數成為了修改左值的的內容。若表達式有數組類型,則此表達式的值是個指向數組第一個元素的指針。

上文描述了大多數場景下數組如何轉為指針。在兩種情形下,數組的值類型不被轉換:當用在一元運算符&(取地址)或sizeof 時。參見C99/C11標準 6.3.2.1小節:

(Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue.)

除非它是sizeof或一元運算符&的操作數,再或者它是用於初始化數組的字元文本,否則有著“類型數組”類型的表達式被轉換為“指向類型”類型的指針,此指針指向數組對象的首個元素且指針不是左值。

由於數組沒有可修改的左值,並且在絕大多數情況下,數組類型的表達式的值被轉為指針,因此不可能用賦值運算符給數組變數賦值(即int a[10]; a = 1;是錯的,譯者註)。下麵是一個小示例:

short a[] = {1,2,3};

short *pa;

short (*px)[];

void init(){

    pa = a;

    px = &a;

    printf("a:%p; pa:%p; px:%p\n", a, pa, px);

    printf("a[1]:%i; pa[1]:%i (*px)[1]:%i\n", a[1], pa[1], (*px)[1]);

}

(譯者註:%i能識別輸入的八進位和十六進位)

a是 int 型數組,pa 是指向 int 的指針,px 是個未完成的、指向數組的指針。a 賦值給 pa前,它的值被轉為一個指向數組開頭的指針。右值表達式 &a並非意味著指向 int,而是一個指針,指向 int 型數組因為當使用一元符號&時右值不被轉換為指針。

表達式 a[1] 中下標的使用等價於 *(a+1),且服從如同 pa[1] 的指針算術規則。但二者有一個重要區別。對於 a 是數組的情況,a 變數的實際記憶體地址用於獲取指向第一個元素的指針。當對於 pa 是指針的情況,pa 的實際值並不用於定位。編譯器必須註意到 a 和 pa見的類型區別,因此聲明外部變數時,指明正確的類型很重要。

int a[];

int *pa;

但在另外的編譯單元使用下述聲明是不正確的,將毀壞代碼:

extern int *a;

extern int pa[];

3.1 數組作為函數形數

某些類型數組變為指針的另一個場合在函數聲明中。下述三個函數聲明是等價的:

void sum(int data[10]) {}

void sum(int data[]) {}

void sum(int *data) {}

編譯器應報告函數 sum 重定義相關錯誤,因為在編譯器看來上述三個例子中的參數都是 int型的。.

多維數組是有點棘手的話題。首先,雖然用了“多維”這個詞,C並不完全支持多維數組。數組的數組可能是更準確的描述。

typedef int[4] vector;

vector m[2] = {{1,2,3,4}, {4,5,6,7}};

int n[2][4] = {{1,2,3,4}, {4,5,6,7}};

變數 m 是長度為2的 vector 類型,vector 是長為4的 int 型數組。除了存儲的記憶體位置不同外,數組 n 與 m 是相同的。從記憶體的角度講,兩個數組都如同括弧內展示的內容那樣,排布在連續的記憶體區域。訪問到的和聲明的完全一致。

int *p = n[1];

int y = p[2];

通過使用下標符號 n[1],我們獲取到了每個元素大小為4位元組的整型數組。因為我們要定位數組的第二個元素, 其位置在多維數組中是數組開始偏移四倍的整型大小。我們知道,在這個表達式中整型數組被轉為指向 int 的指針,然後存為 p。然後 p[2] 將訪問之前表達式產生的數組中的第三個元素。上面代碼中的 y 等價於下麵代碼中的 z:

int z = *(*(n+1)+2);

也等價於我們初學C時寫的表達式:

int x = n[1][2];

當把上文中的二維數組作為參數傳輸時,第一“維”數組會轉為指針,指向再次陣列的數組的第一個元素。因此不需要指明第一維。剩餘的維度需要明確指出其長度。否則下標將不能正確工作。當我們能夠隨心所欲地使用下述表格中的任一形式來定義函數接受數組時,我們總是被強制顯式地定義最裡面的(即維度最低的)數組的維度。

void sum(int data[2][4]) {}

void sum(int data[][4]) {}

void sum(int (*data)[4]) {}

為繞過這一限制,可以轉換數組為指針,然後計算所需元素的偏移。

void list(int *arr, int max_i, int max_j){

    int i,j;

    for(i=0; i<max_i; i++){

        for(j=0; j<max_j; j++){

            int x = arr[max_i*i+j];

            printf("%i, ", x);

        }

        printf("\n");

    }

}

另一種方法是main函數用以傳輸參數列表的方式。main函數接收二級指針而非二維數組。這種方法的缺陷是,必須建立不同的數據,或者轉換為二級指針的形式。不過,好在它運行我們像以前一樣使用下標符號,因為我們現在有了每個子數組的首地址。

int main(int argc, char **argv){

    int arr1[4] = {1,2,3,4};

    int arr2[4] = {5,6,7,8};

    int *arr[] = {arr1, arr2};

    list(arr, 2, 4);

}

void list(int **arr, int max_i, int max_j){

    int i,j;

    for(i=0; i<max_i; i++){

        for(j=0; j<max_j; j++){

            int x = arr[i][j];

            printf("%i, ", x);

        }

        printf("\n");

    }

}

用字元串類型的話,初始化部分變得相當簡單,因為它允許直接初始化指向字元串的指針。

const char *strings[] = {

    "one",

    "two",

    "three"

};

但這有個陷阱,字元串實例被轉換成指針,用 sizeof 操作符時會返回指針大小,而不是整個字元串文本所占空間。另一個重要區別是,若直接用指針修改字元串內容,則此行為是未定義的。

假設你能使用變長數組,那就有了第三種傳多維數組給函數的方法。使用前面定義的變數來指定最裡面數組的維度,變數 arr 變為一個指針,指向未完成的int數組。

void list(int max_i, int max_j, int arr[][max_j]){

    /* ... */

    int x = arr[1][3];

}

此方法對更高維度的數組仍然有效,因為第一維總是被轉換為指向數組的指針。類似的規則同樣作用於函數指示器。若函數指示器不是 sizeof 或一元操作符 & 的參數,它的值是一個指向函數的指針。這就是我們傳回調函數時不需要 & 操作符的原因。

static void catch_int(int no) {

    /* ... */

};

 

int main(){

    signal(SIGINT, catch_int);

 

    /* ... */

}

四、打樁(Interpositioning)

打樁是一種用定製的函數替換鏈接庫函數且不需重新編譯的技術。甚至可用此技術替換系統調用(更確切地說,庫函數包裝系統調用)。可能的應用是沙盒、調試或性能優化庫。為演示過程,此處給出一個簡單庫,以記錄GNU/Linux中 malloc 調用次數。

/* _GNU_SOURCE is needed for RTLD_NEXT, GCC will not define it by default */

#define _GNU_SOURCE

#include <stdio.h>

#include <stdlib.h>

#include <dlfcn.h>

#include <stdint.h>

#include <inttypes.h>

 

static uint32_t malloc_count = 0;

static uint64_t total = 0;

 

void summary(){

    fprintf(stderr, "malloc called: %u times\n", count);

    fprintf(stderr, "total allocated memory: %" PRIu64 " bytes\n", total);

}

 

void *malloc(size_t size){

    static void* (*real_malloc)(size_t) = NULL;

    void *ptr = 0;

 

    if(real_malloc == NULL){

        real_malloc = dlsym(RTLD_NEXT, "malloc");

        atexit(summary);

    }

 

    count++;

    total += size;

 

    return real_malloc(size);

}

打樁要在鏈接libc.so之前載入此庫,這樣我們的 malloc 實現就會在二進位文件執行時被鏈接。可通過設置 LD_PRELOAD 環境變數為我們想讓鏈接器優先鏈接的全路徑。這也能確保其他動態鏈接庫的調用最終使用我們的 malloc 實現。因為我們的目標只是記錄調用次數,不是真正地實現記憶體分配,所以我們仍需要調用“真正”的 malloc 。通過傳遞 RTLD_NEXT 偽處理程式到 dlsym,我們獲得了指向下一個已載入的鏈接庫中 malloc 事件的指針。第一次 malloc 調用 libc 的 malloc,當程式終止時,會調用由 atexit 註冊的獲取和 summary 函數。看GNU/Linxu中打樁行為(真的184次調用!):

$ gcc -shared -ldl -fPIC malloc_counter.c -o /tmp/libmcnt.so

$ export LD_PRELOAD="/tmp/libstr.so"

$ ps

  PID TTY          TIME CMD

2758 pts/2    00:00:00 bash

4371 pts/2    00:00:00 ps

malloc called: 184 times

total allocated memory: 302599 bytes

4.1 符號可見性

預設情況下,所有的非靜態函數可被導出,所有可能僅定義有著與其他動態鏈接庫函數甚至模板文件相同特征標的函數,就可能在無意中插入其它名稱空間。為防止意外打樁、污染導出的函數名稱空間,有效的做法是把每個函數聲明為靜態的,此函數在目標文件之外不能被使用。

在共用庫中,另一種控制導出的共用目標的方式是用編譯器擴展。GCC 4.x和Clang都支持 visibility 屬性和 -fvisibility 編譯命令來對每個目標文件設置全局規則。其中 default 意味著不修改可見性,hidden 對可見性的影響與 static 限定符相同。此符號不會被放入動態符號表,其他共用目標或可執行文件看不到此符號。

#if __GNUC__ >= 4 || __clang__

  #define EXPORT_SYMBOL __attribute__ ((visibility ("default")))

  #define LOCAL_SYMBOL  __attribute__ ((visibility ("hidden")))

#else

  #define EXPORT_SYMBOL

  #define LOCAL_SYMBOL

#endif

全局可見性由編譯器參數指定,可通過設置 visibility 屬性被本地覆蓋。實際上,全局策略設置為 hidden,則所有符號會被預設為本地的,只有修飾__attribute__((visibility (“default”))) 才將被導出。

持續更新中。

另外筆者是一個有著7年工作經驗的架構師,對於c++,自己有做資料的整合,一個完整學習C語言c++的路線,學習資料和工具。可以進我的Q群7418,18652領取,免費送給大家。希望你也能憑自己的努力,成為下一個優秀的程式員!另外博主的微信公眾號是:C語言編程學習基地,歡迎關註!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Python PIL PIL (Python Image Library) 庫是Python 語言的一個第三方庫,PIL庫支持圖像存儲、顯示和處理,能夠處理幾乎所有格式的圖片。 一、PIL庫簡介 1. PIL庫主要有2個方面的功能: (1) 圖像歸檔:對圖像進行批處理、生產圖像預覽、圖像格式轉換等。 ...
  • 還有一些其他用法 ...
  • 1 line =9 2 while line >0: 3 #print ("*") 4 tmp =line 5 while tmp >0: 6 print ("#",end="") 7 tmp-=1 8 line -=1 9 print() 10 11 first =0 12 while first ...
  • 1 截取字元串有的時候我們在頁面中不需要顯示那麼長的字元串,比如新聞標題,這樣用下麵的例子就可以自定義顯示的長度<#if title.content?length lt 8> <a href>${title.content?default("")}</a> <#else> <a href title ...
  • 第四節 數據類型(列表、元祖) 今日內容 列表 元祖 1、列表 1.格式 2.公共方法 1.len 計算長度 2.索引 輸出某一個元素 3.切片 輸出某一段元素 4.修改(字元串/數字/布爾除外) 5.步長 選取列表中第幾個元素 6.for迴圈 註意:for和while的應用場景: 有窮盡優先使用f ...
  • 作業1import random#引入隨機數模塊xing=["小白","小黃","小王","小陳","小綠"]print("學號\t\t\t姓名\t\tJava\tC語言\tPython\t平均成績\t")listj=[]listc=[]listp=[]lista=[]for i in range(... ...
  • 經過一個多月的時間,終於基本完成了第一個項目,電子黑板 此項目主要適用於電子黑板的銷售和展示,功能並不複雜但是細節很多,前面留下的坑很多,之前ue做的爛,後面ue離職,導致ui做的一樣爛,本身邏輯不清晰導致浪費很多時間 項目主要利用的技術其實並不難,但對於我個人而言是之前沒使用過的所以在此總結一番: ...
  • 上篇我們做了一個WriterActor的例子,主要目的是示範WriterActor如何作為集群分片用persistentActor特性及event-sourcing模式實現CQRS的寫功能。既然是集群分片,那麼我們就在這篇講講WriterActor的部署和測試,因為這個裡面還是有些值得註意的地方。下 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...