理解C語言(一) 數組、函數與指針



1 指針

一般地,計算機內存的每個位置都由一個地址標識,在C語言中我們用指針表示內存地址。指針變量的值實際上就是內存地址,而指針變量所指向的內容則是該內存地址存儲的內容,這是通過解引用指針獲得。聲明一個指針變量並不會自動分配任何內存。在對指針進行間接訪問前,指針必須初始化: 要么指向它現有的內存,要么給它分配動態內存。

對未初始化的指針變量執行解引用操作是非法的,而且這種錯誤常常難以檢測,其結果往往是一個不相關的值被修改,並且這種錯誤很難調試,因而我們需要明確強調: 未初始化的指針是無效的,直到該指針賦值后,才可使用它。 

int *a;
*a=12; //只是聲明了變量a,但從未對它初始化,因而我們沒辦法預測值12將存儲在什么地方

int *d=0; //這是可以的,0可以視作為零值

int b=12;
int *c=&b;

另外C標准定義了NULL指針,它作為一個特殊的指針常量,表示不指向任何位置,因而對一個NULL指針進行解引用操作同樣也是非法的。因而在對指針進行解引用操作的所有情形前,如常規賦值、指針作為函數的參數,首先必須檢查指針的合法性- 非NULL指針。

解引用NULL指針操作的后果因編譯器而異,兩個常見的后果分別是返回置0的值及終止程序。總結下來,不論你的機器對解引用NULL指針這種行為作何反應,對所有的指針變量進行顯式的初始化是種好做法。

  • 如果知道指針被初始化為什么地址,就該把它初始化為該地址,否則初始化為NULL
  • 在所有指針解引用操作前都要對其進行合法性檢查,判斷是否為NULL指針,這是一種良好安全的編程風格

1.1 指針運算基礎

在指針值上可以進行有限的算術運算和關系運算。合法的運算具體包括以下幾種: 指針與整數的加減(包括指針的自增和自減)、同類型指針間的比較、同類型的指針相減。例如一個指針加上或減去一個整型值,比較兩指針是否相等或不相等,但是這兩種運算只有作用於同一個數組中才可以預測。如float指針加3的表達式實際上使指針的值增加3個float類型的大小,即這種相加運算增加的是指針所指向類型字節大小的倍數。參考理解C語言(零)導讀(上)2.5.1小節

對於任何並非指向數組元素的指針執行算術運算是非法的,但常常很難被檢測到。

  • 如果對一個指針進行減法運算,產生的指針指向了數組中第1個元素前面的內存位置,那么它是非法的。
  • 加法運算稍微不同,如果產生的指針指向了數組中最后一個元素后面的那個內存地址,它是合法的,但不能對該指針執行解引用操作,不過之后就不合法了(這和STL中迭代器尾部元素可指向尾部元素的下一個位置是一樣的道理)

關於指針的運算操作將會在數組中的應用中更深入地介紹。

1.2 typedef和C++中的引用

C語言中用typedef說明一種新類型名,來代替已有類型名。它的作用是給已存在的類型起一個別名,原有類型名仍然有效。如下:

typedef float REAL;
REAL a,b;

typedef char*  PCHAR;
PCHAR p;

那么和#define有什么區別呢?

typdef int* int_t;
#define int_d int*;

它們的區別主要在於:

  • 前者在於聲明一個類型的別名,在編譯時處理有類型檢查;而后者只是簡單的宏文本替換,無類型檢查
  • 從使用上來說,int_t a,b這兩個變量都是int *類型的,而int_d a,b中b是int類型的

為了更好的理解指針,所以也有必要把C++中的一些概念引入進來作對比。C++中所謂的引用實際上是一個特殊的變量,這個變量的內容是綁定在這個引用上面的對象的地址,而使用這個變量時,系統自動根據這個地址去找到它綁定的變量,再對變量操作。即引用的本身只是一個對象的別名,在引用的操作實際是對變量本身的操作。

本質上說,引用還是指針,只不過該指針不能修改,一旦定義了引用,就必須跟一個變量綁定起來,且無法修改此綁定。盡管使用引用和指針都可間接訪問某個值,但它們還是有區別的。

  • 引用被創建時,它必須初始化(引用不能為空);指針可以為空值,可在任何時候被初始化
  • 一旦引用被初始化為指向某個對象,它就不能改變為另一個對象的引用;指針可以在任何時候指向另一個對象
  • 不能有NULL引用,必須確保引用是和一塊合法的存儲單元關聯
  • sizeof(引用)得到的是所指向變量的大小;sizeof(指針)得到的是指針本身的大小

在函數中傳遞實參時,對於非引用類型的形參的任何修改僅作用於局部副本,並不影響實參本身(指針作為參數傳遞時仍然是傳值調用,傳遞的副本是指針變量的值)。在C++中,為了避免傳遞副本帶來的開銷,將形參指定為引用類型,可見這樣效率更高。但是也帶來了對引用形參的任何修改會直接影響實參本身的副作用。

所以既要利用引用提高效率,又要保護傳遞的函數參數在函數中不被改變,就應使用常引用,定義一個普通變量的只讀屬性的別名,避免實參在函數中意外被改變。

const int ival=10;
const int &ref=ival; //必須使用const引用

1.3 各種指針

該小節主要講述二級指針、通用指針和函數指針,與數組相關的指針在后面第2章中會具體解釋。

1.3.1 指向指針的指針

指針本身也是可用指針指向的內存對象。指針占用內存空間存放其值(值作為地址),因而指針的存儲地址可存放在指針中,通過間接訪問的方式。只要當確實需要時,才應該是多級指針。

我們在實現二叉樹時經常會遇到如何插入節點,在C中由於涉及到了指針,經常使我們對節點間究竟有沒有鏈接成功產生混淆,特別是不清楚什么時候使用二級指針,什么時候又是一級指針。它的結構描述如下:

typedef int T;
typedef struct tree_node {
    T data;
    struct tree_node *lchild;
    struct tree_node *rchild;
} bstree_node;

下面分析調用該插入節點的方法,能否成功構建二叉樹。

void insert_node(node *root,T element);
  • Step 1: 函數調用參數前,root=NULL

當傳遞的參數是指針時,我們仍然可以把指針看做變量,即傳遞的是指針值的副本,即產生了一個和實參地址不同的形參地址,但它們的內容是相同的(這里為NULL),並不指向任何位置

  • Step 2: 調用函數並修改形參的內容,為root分配了新地址
if(root==NULL)
	root=new_node(data);

可看出函數結束后,形參root的內容(指針本身的值)發生了變化,由NULL變成了0x4567的地址(只是為了說明情形,該地址表示並不准確)。可知root已指向一塊含有數據的堆內存,而實參root仍為NULL,不指向任何內存位置。

因而一級指針作為參數傳遞時,在這種方法下形參的變化並未使實參發生任何變化,因而下一次調用插入節點函數時,實參root值始終為NULL,這種方法不能建立起二叉樹。那么要成功地構建二叉樹,使實參指向的內容發生真正改變呢,有3個方法:

A. 初始化的root結點不為空,即根結點始終不為空

  • Step 1: 函數調用前實參和形參指向

  • Step 2: 函數調用后實參和形參指向

回想一下,這種情形是不是很像單鏈表中的頭結點,它極大地簡化了插入和刪除操作,實現上更為簡潔。

**B. 插入函數定義為:bstree_node *insert_node(bstree_node *root,element) **

返回函數操作中變化的形參地址,再把返回值賦值給實參地址(root初始化可以允許為NULL),這樣函數結束后實參和形參均指向了相同內容

root=insert_node(root,element); 

這種方法確實有效,但也可看出有一缺點: 需要重新調整指針的指向,無法在程序執行中自動修改root的地址,而且還占用內存空間。所以要想在插入和刪除節點的操作過程中,二叉樹能動態地變化而無需指定返回root地址,該用什么樣的方法呢。於是二級指針就上場了

**C. 插入函數定義為:void insert_node(bstree_node **root,T element) **

利用二級指針無需返回值便可動態修改二叉樹,這種實現是最有效的。下面請看函數執行前后實參和形參的變化圖(始終要記住: 函數的參數傳遞始終是傳值調用(不包括C++中的引用),即傳遞的始終是參數的拷貝,一個副本而已

  • Step 1: 函數調用前實參和形參指向

  • Step 2: 函數調用后實參和形參指向

當根結點為空時, *root=new_node(element) 表明執行函數后形參指向的二級指針root的內容發生了變化,重新分配了地址,從而導致指向的結點內容發生了變化,這樣實參指向的指針所指向的結點內容同樣也發生了變化。

當根結點不為空時,根結點的地址不會發生變化,只會通過鏈接的形式鏈上了左右子樹。

**D 總結 **

對比,我們在實現單鏈表時使用虛擬頭結點。優點之一是方便我們簡化插入和刪除操作,它會動態鏈接上節點或刪除節點。其實它還有一個優點:

不管鏈表是否為空,頭結點始終存在。如果不使用頭結點,插入和刪除操作就必須要保證一個結點存在使結點鏈接上,否則就必須使用返回結點地址(這會占用空間)或者使用二級指針(抽象,使用起來容易出問題)。因而使用虛擬頭結點就可避免這些問題了

void insert(linknode *list,int data); //list是虛擬頭結點,推薦使用
void insert(linknode **list,int data);//使用二級指針,難懂
linknode *insert(linknode *list,int data);//需要重新調整指針指向,占用內存空間

1.3.2 void *指針

C中提供一個特殊的指針類型: void *,它可以保存任何類型對象的地址:

double obj=3.14;
double *pd=&obj;
void *pv=&obj;
pv=pd;

void *表明該指針與一地址值相關,但不清楚存儲在此地址上的對象的類型。void *指針只支持以下幾種操作:

  • 與另一個指針比較
  • 給另外一個void *指針賦值
  • void *指針當函數參數或返回值

不允許使用void *指針操作它指向的對象,值得注意的是函數返回void *類型時返回一個特殊的指針類型,而不是向返回void 類型那樣無返回值。

1.3.3 函數指針

函數指針是指指向函數的指針,函數類型由其返回類型及形參表確定,與函數名無關,有時候還用typedef簡化函數指針的定義。

bool (*pf)(int *,int *); 
typedef bool (*cmpfcn)(int *,int *); //cmpfcn是一種指向函數的指針類型的名字,該類型為指向返回bool類型並帶有兩個整型指針參數的函數的指針。

在引用函數名但又沒有調用該函數,函數名自動解釋為指向函數的指針,並且直接引用函數名就等價於在函數名應用取地址操作符。

bool lencmp(int *,int *);
bool (*)(int *,int *);
cmpfcn pf1=0;
pf1=lencmp;
cmpfcn pf2=&lencmp; 

函數指針只能通過同類型的函數名或者函數指針或者0值常量進行初始化和賦值。初始化為0表示該指針不指向任何函數,只有當初始化后才能調用函數。調用它可以直接使用函數名或者直接利用函數指針,不用解引用符號或者使用解引用符號,如下:

cmpfcn pf=lencmp;
lencmp(a,b); //調用1
pf(a,b); //調用2
(*pf)(a,b); //調用3

另外函數的形參也可以是指向函數的指針,這個通常被稱為回調函數。允許形參是一個函數類型,它對應的實參被自動轉換為指向相應函數類型的指針,注意函數的返回類型不能是函數。

int (*ff(int))(int ,int); //返回指向函數的指針
typedef int (*PF)(int ,int );
PF ff(int); //函數ff返回一個函數指針

typdef int func(int ,int); //func是一個函數,而不是一個函數指針
void f1(func); //正確,f1的形參是一個函數指針,func自動轉換為函數指針
func f2(int); //錯誤,無法被自動轉換
func *f3(int); //正確,f3返回一個函數指針

int (*a[10])(int); //一個有10個指針的數組,每個指針指向一個函數,接收一個整型參數返回一個整型
int (*(*p)[10])(int); //聲明一個指向10個元素的數組指針,每個元素是一個函數指針,接收一個整型參數返回一個整型。


2 數組與指針

人們在使用數組時經常會把等同於指針,並自然而然地假定在所有的情況下數組和指針都是等同的。為什么出現這樣的混淆? 因為我們在使用時經常可以看到大量的作為函數參數的數組和指針,在這種情況下它是可以互換的,但是人們容易忽視它只是發生在一個特定的上下文環境中。如在main函數的參數中有這樣的char **argvchar *argv[]的形式,因為argv是一個函數的參數,它誘使我們錯誤地總結出指針和數組是等價的。如下面一個程序:

#include< stdio.h >

int len(int arr[]){
	return sizeof(arr);
}

int main(){
	int arr[]={1,2,3,4,5};
	printf("%d\n",sizeof(arr)); //sizeof計算類型或變量或類的存儲字節大小
    printf("%d\n\n",len(arr)); //同樣使用sizeof,為什么結果不同
	
	printf("arr=%p\n",(void *)arr); //指針表示法取地址:arr+0..len-1
	printf("&arr[0]=%p\n\n",(void *)&arr[0]); //數組表示法取地址: &arr[0..len-1]

	int *p=(int *)(&arr+1);//&arr+1與arr+1為什么有區別
    int *p1=(int *)(arr+1);
	printf("arr+1=%p\n",(void *)p1);
    printf("&arr+1=%p\n\n",(void *)p);
    return 0;
}

結果如圖所示:

圖1

可以看出:

  • 使用sizeof計算數組的時候數組名有時候當指針來看,有時候又當整個數組來看待
  • 數組表示法有時候和指針表示法等價,但數組名前加一個&運算符,它卻不等同於指針的使用。

可知數組和指針並不全都相同。那么數組什么時候等同於指針,什么時候不等同於指針呢?

2.1 區分定義和聲明

  • extern聲明說明編譯器對象的類型和名字,描述了其他地方的創建對象
  • 定義要求為對象分配內存:定義指針時編譯器並不為指針所指向的對象分配空間,它只是分配指針本身的空間
int a[100];
extern int a[]; //正確並且無需提供關於數組長度的信息
extern int *a;//錯誤,人們總是錯誤地認為數組和指針非常類似

2.2 數組和指針是如何訪問的

X=Y:左值在編譯時可知,表示存儲結果的地址;右值表示Y的內容

也就是說編譯器為每個變量分配一個左值,該地址在編譯時可知,而變量在運行時一直保存於這個地址,而右值只有在運行時才可知。如需用到變量中存儲的值,編譯器就發出指令從指定地址讀入變量值並將它存入寄存器中。如果編譯器需要一個地址,可能要加上偏移量來執行某種操作,它就可以直接進行操作,並不需要增加指令取得具體的地址。相反對於指針,必須先在運行時取得它的值然后才能對它解引用。

下面分別是對數組下標的引用和對指針的引用的描述:

char a[9]=”abcdefgh”; c=a[i];

編譯器符號表具有一個地址9980,運行時
步驟1:取i的值,將它與9980相加
步驟2:取地址(9980+i)的內容

char *p; c=*p;

編譯器符號表有一個符號p,它的地址為4624,運行時
步驟1:先得到地址p 的內容,即5081
步驟2:將5081作為字符的地址並取得它的內容

可以看出指針的訪問明顯靈活很多,但需要增加一次額外的提取

2.3 數組和指針的引用

2.3.1 定義為指針,但以數組方式引用

指針定義編譯器會告訴你這是一個指向字符的指針,相反數組定義則告訴你是一個字符序列。

char *p=”hello”;c=p[i];

2.3.2 定義為數組名,但以指針方式引用

  • 數組名變量代表了數組中第一個元素的地址,它並不是一個指針但卻表現得像一個不能被修改的常指針 。因而它不能被賦值
  • 對數組下標的引用總是可以寫成一個指向數組起始地址的指針加上偏移量
int a[100];
int *p=a; p[i]或*(p+i); 

通常情況下,數組下標是在指針的基礎上,所以優化器可以把它轉化為更有效率的指針表達形式,並生成相同的機器指令,所以C語言采用指針形式就是因為指針和偏移量是底層硬件所使用的基本模型,但在處理一維數組時指針見不得比數組更快

2.3.3 為什么要把數組作為函數的參數傳遞當作指針

作為形參的數組和指針等同起來是出於效率的考慮,數組名自動改寫成指向數組第一個元素的指針形式,而不是整個數組的拷貝,並且如果要操作數組的元素千萬不要在數組名上進行操作,形式應如下:

char *test(char a[]) {
	char *p=a;
}

在C語言規定中,所有非數組形式的數據均以傳值形式(即對實參做一份拷貝並傳遞給調用的函數,函數不能修改作為實參的實際變量的值而只能修改它的那份拷貝)。

因而有些人喜歡把它理解成數組和函數是傳址調用,缺省情況下都是傳值調用,數據也可以傳址調用,即加&地址運算符,這樣傳遞給函數的是實參的地址而不是實參的拷貝。 但嚴格意義上傳址調用也不十分准確,因為編譯器的機制是在被調用的函數中,你擁有的是一個指向變量的指針,而不是變量本身,傳遞的參數只是指針變量值本身的拷貝。

傳值調用的拷貝是指分配了棧上的空間地址,內容和實參值一樣而形參的地址肯定與實參地址不一樣,因而當指針作為函數參數,你只需要測試指針變量值的實參和形參地址是否不一樣就可以知道傳遞的究竟是指針變量值本身的副本還是該指針指向的變量的副本。

例如下面的程序:

#include <stdio.h>
#include <stdlib.h>

void f2(int *a){
        printf("執行函數f2(a):\n");
	printf("形參變量a的地址=%p\n",&a);
	printf("形參變量a的值=%p\n\n",a);
        *a=15;
}

int main(){
      int *a=(int *)malloc(sizeof(int));
      *a=10;
      printf("previous *a=%d\n",*a);
      printf("實參變量a的地址%p\n",&a);
      printf("實參變量a的值%p\n\n",a);
      f2(a);
      printf("after *a=%d\n",*a);
      printf("存儲*a變量的地址%p\n",a);
      printf("存儲指針變量a的地址%p\n",&a);
      return 0;
}

結果如下:
圖2

2.3.4 指針數組與數組指針

指針數組: 一個數組里裝着指針,即指針數組是一個數組,如int *a[10]
數組指針: 一個指向數組的的指針,即它還是個指針,但指向的是整個數組,如int (*p)[10]

二維數組的數組名是一個數組指針,若有:

int a[4][10];
int (*p)[10];
p=a //a的類型就是int (*)[10]

可知p指向含4個數組元素的數組首地址(p=a),但要注意的是a是常量,不可以進行賦值操作。再如:

int a[10];
int (*p)[10]=&a;//注意此處是&a,不是a
int *q=a; //a的類型是int *,&a的類型是int (*)[10]

可以看出p和q雖然都指向數組的第一個元素,但兩者類型是不同,p是指向有10個整型元素的指針,p+1要跳過40個字節;而q是指向一個整型元素,p+1跳過4個字節。

注意到數組作為函數實參傳遞時,傳遞給函數的是數組首元素的地址;而將數組某個元素的地址當做實參時,傳遞的是此元素的地址,可理解傳遞的是子數組(以此元素作為首元素的子數組)首元素的地址。如下題,sum(&aa[i])傳遞的是以第 i個元素為首元素的子數組,結果輸出為4

#include <stdio.h>

void sum(int *a){
	a[0]=a[1];
}

int main(){
	int aa[10]={1,2,3,4,5,6,7,8,9},i;
	for(i=2;i>=0;i--)
		sum(&aa[i]);
	printf("%d\n",aa[0]); //輸出為4
	return 0;
}

2.4 二維數組

當提到C語言中的數組時,就把它看做一個向量,數組的元素也可以是另一個數組。因而多維數組可以看成數組的數組。 數組下標的規則告訴我們元素的存儲和引用都是線性排列在內存中的。 在C和C++中二維數組按照行優先順序連續存儲,一般二維數組a[x][y]在一維數組b中,它們的轉換關系如下:

a[x][y]=b[x*列數+y]

如果想動態創建一個二維數組a[m][n],使用后再釋放,操作如下:

int **a=new int*[m];
for(int i=0;i< m;i++)
	a[i]=new int[n];

/*釋放內存*/
for(int i=0;i< m;i++)
	delete []a[i];
delete []a;

如果你想初始化二維字符串數組,一般利用指針數組初始化字符串常量:

char *p[]={“1heh”,”ghh”,...};

而其他非字符串類型的指針數組不能直接初始化,它的定義如下

int r1[]={3,4,5};
int r2[]={0,9,8,4,3};
int r3[]={0};
int *weight[]={r1,r2,r3};

上面這種長度不一的數組,我們稱之為鋸齒狀數組。在這里有很多處理技巧,例如:

 char *ip[len];
 char hello[]=”world”;
 ip[i]=&hello[0];  //共享字符串,直接使用現有的
 ip[j]=malloc(strlen(hello)+1);
 strcpy(ip[j],hello); //拷貝字符串,通過分配內存創建一份現有字符串的新鮮拷貝,僅傳遞指針

還有如,在指針數組的末尾增加一個NULL指針,該NULL指針使函數在搜索這個表格時能夠檢測到表的結束,而無需預先知道表的長度,如查詢C源文件中關鍵字的個數:

const char *keyword[]={"do","for",...,NULL};

2.4.1 數組的內存布局與定位

若要計算pea[i][j], 則是要先找到pea[i]的位置,再根據偏移量取得字符,因而pea[i][j]解析為:
*(*(pea+i)+j)。 如下圖:

圖3

2.4.2 多維數組作為參數是如何傳遞的

當多維數組作為參數時,數組作為實參總是被改寫對應指針的形式的,實參和形參關系如下:

實參 形參
數組的數組char c[8][10] 數組指針char (*)[10]
指針數組char *c[15] 指針的指針char **c
數組指針(行指針)char (*c)[64] 數組指針char (*)[64],不改變
指針的指針char **c 指針的指針char **c,不改變

所以在main函數中看到char **argv這樣的參數,是因為argv是個指針數組char *argv[],這個表達式被編譯器改寫為指向數組第一個元素的指針,即指向指針的指針。事實上如果argv參數被聲明為數組的數組,將會改寫為char (*)[len]而不是char **argv

2.5 指針的運算

例如二維數組int a[4][5],它的指針運算說明如下(一定要明確對應形式的類型,它指向的是什么,才能知道它自增運算跳過的字節大小)

形式 類型說明
&a int (*)[4][5]數組的首地址,&a+1將跳過整個數組
a+i int (*)[5]數組指針類型,指向第i個數組的指針
*(a+i) int *類型 ,它表示a[i]
*(*(a+i)+j) int類型 ,它表示a[i][j]

在這里需要注意,數組下標可以使用負號,如:

cp[-1]=*(cp-1); 
cpp[-1][-1]=*(*(cp-1)-1);

2.6 數組和指針的異同點

指針的特點:

  • 保存數據的地址,間接訪問數據,首先取得指針的內容,把它當做地址,加上偏移量作為新地址提取數據
  • 通常用於動態數據結構,如malloc、free,用於指向匿名數據(指針操作匿名內存)
  • 可用下標形式訪問指針,一般都是指針作為函數參數,但你要明確實際傳遞給函數的是一個數組

數組的特點:

  • 直接保存數據,以數組名+偏移量訪問數據
  • 通常用於固定數目的元素
  • 數組作為函數參數會當做指針看待

另外從變量在內存的位置來說:

  • 數組要么在靜態存儲區被創建,如全局數組,要么在用戶棧中被創建。
  • 數組名對應着一塊內存(而非指向),其地址與容量在生命期內保持不變,只有其內容可以改變。
  • 指針可以隨時指向任意類型的內存塊,所以我們常用指針來操作動態分配的內存,但使用起來也容易出錯

下面以字符串為例比較指針與數組的特性,程序為test.c:

#include <stdio.h>
#include <stdlib.h>

void ex1(){
    char a[]="hello";// 字符數組,a的內容可變,如a[0]='X'
    a[0]='X';
    printf("%c\n",*a);
    char *p="world"; //指針p指向常量字符串"world\0"(存儲於靜態存儲區),內容不可以被修改
    p[0]='X'; //編譯時尚不能發現錯誤,在運行時發現該語句企圖修改常量字符串內容而導致運行錯誤
    printf("%s\n",p);
}

void ex2(){
    /* 數組與數組內容復制與比較 */
    char a[]="hello";
    char b[10];
    strcpy(b,a); //數組與數組的復制不能用b=a,否則產生編譯錯誤
    if(strcmp(b,a)==0) //數組與數組內容的比較同樣不能用b=a
        printf("內容相同\n");


    /* 數組與指針內容復制與比較 */
    int len=strlen(a);
    char *p=(char *)malloc(sizeof(char)*(len+1));
    strcpy(p,a); //復制不能用p=a,否則產生編譯錯誤
    if(strcmp(p,a)==0) //數組與數組內容的比較同樣不能用p=a,用p=a比較的是地址
        printf("內容相同\n");
}

int main()
{
    // ex1();
    ex2();
    return 0;
}

可以了解到常量字符串的內容是不可以被修改的,而字符數組的內容是可以被修改的;並且如果想要復制或比較數組內容,不能簡單用b=a或b==a等來判斷,需要使用如程序里所描述的strcpy和strcmp函數

注意也有例外, 就是把數組當做一個整體來考慮,而對數組的引用不能作為指向該數組第一個元素的指針來代替,看參見介紹中程序arr.c的執行結果:

  • 數組作為sizeof的操作數,顯示要求的是整個數組的大小,但注意當數組作為函數形參時,自動退化為指針,在函數內部計算sizeof,結果只是計算指針類型的大小,這一般與機器字長有關,兩者並不矛盾。通常可以在頭文件定義一個宏語句:#define TABLESIZE(arr) (sizeof(arr)/sizeof(arr[0]))
  • 使用&獲取字符數組的地址

3 實現動態數組

當我們想周期性地聚合一堆數據時,我們需要一個數組,並且這個長度是不確定的,可以動態增長。C++的vector便滿足這個需求,那么對於C來說呢?一般來說C語言中的數組是靜態數組,它的長度在編譯期就確定了。如果你預先不知道數組的長度,想在程序運行的時候根據需要動態擴充數組的大小,這里可以設計一個動態數組的ADT。

它的基本思路是使用如malloc/free等內存分配函數得到一個指向一大塊內存的指針,以數組的方式引用這塊內存或者直接調用動態數組的接口,根據其內部的實現機制自行擴充空間,動態增長並能快速地清空數組,對數據進行排序和遍歷。

3.1 動態數組的結構和接口定義

動態數組的數據結構定義如下:

/**
 * 動態數組的結構定義
 * data:  指向一塊連續內存的指針;type_size: 元素類型的大小(動態執行時才能確定類型)
 * capacity: 動態數組的容量大小,最大可用空間
 * index: 動態數組的實際大小
 * int (*comp)(const void *,const void *): 元素的大小比較函數
 */
typedef struct {
	void *data; 
	int capacity;
	int index;
	int type_size;
	int (*comp)(const void *,const void *);
} array_t;

動態數組常見的接口函數設計:

/*為動態數組分配內存*/
array_t *array_alloc(int capacity,int type_size,int (*comp)(const void *,const void *));

/*為動態數組分配默認容量大小的內存*/
array_t *array_alloc_default(int type_size,int (*comp)(const void *,const void *));

/*釋放動態數組的內存*/
void array_free(array_t *arr);

/*判斷數組是否為空*/
bool array_empty(array_t *arr);

/*返回數組存儲的元素個數*/
int array_size(array_t *arr);

/*借助函數指針遍歷數組中每個元素*/
void array_foreach(array_t *arr, void (*visit)(void *elt));

/**
 * 插入一個元素,根據實際空間決定是否擴充或縮減容量
 * 默認范圍內,保持不變;超過默認容量,則擴充
 */
void array_push(array_t *arr,void *elt);

/*把尾部元素拿掉*/
void *array_pop(array_t *arr);

/*成功找到pos位置上的元素,否則返回NULL*/
void *array_get(array_t *arr, int pos);

/*把位置pos上的內容設置成item對應的內容*/
void array_set(array_t *arr,void *item,int pos);

/*動態數組排序*/
void array_sort(array_t *arr);

3.2 動態數組的實現

具體代碼如下:
dynarr.h : 頭文件實現

#ifndef _DYNARR_H_
#define _DYNARR_H_

#include <stdbool.h>

#define DEFAULT_CAPACITY 16

/**
 * 動態數組的結構定義
 * data:  指向一塊連續內存的指針;type_size: 元素類型的大小(動態執行時才能確定類型)
 * capacity: 動態數組的容量大小,最大可用空間
 * index: 動態數組的實際大小
 * int (*comp)(const void *,const void *): 元素的大小比較函數
 */
typedef struct {
	void *data; 
	int capacity;
	int index;
	int type_size;
	int (*comp)(const void *,const void *);
} array_t;

/*為動態數組分配內存*/
array_t *array_alloc(int capacity,int type_size,int (*comp)(const void *,const void *));

/*為動態數組分配默認容量大小的內存*/
array_t *array_alloc_default(int type_size,int (*comp)(const void *,const void *));

/*釋放動態數組的內存*/
void array_free(array_t *arr);


bool array_empty(array_t *arr);

bool array_full(array_t *arr);

int array_size(array_t *arr);

void array_foreach(array_t *arr, void (*visit)(void *elt));

/**
 * 插入一個元素,根據實際空間決定是否擴充或縮減容量
 * 默認范圍內,保持不變;超過默認容量,則擴充
 */
void array_push(array_t *arr,void *elt);

/*把尾部元素拿掉*/
void *array_pop(array_t *arr);

/*成功找到pos位置上的元素,否則返回NULL*/
void *array_get(array_t *arr, int pos);

/*把位置pos上的內容設置成item對應的內容*/
void array_set(array_t *arr,void *item,int pos);

/*動態數組排序*/
void array_sort(array_t *arr);

#endif

dynarr.c : 動態數組接口實現

#include "dynarr.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*為動態數組分配內存*/
array_t *array_alloc(int capacity,int type_size,int (*comp)(const void *,const void *)){
	array_t *arr=malloc(sizeof(array_t));
	arr->data=malloc(capacity*type_size);
	arr->capacity=capacity;
	arr->index=0;
	arr->type_size=type_size;
	arr->comp=comp;
	return arr;
}

/*為動態數組分配默認容量大小的內存*/
array_t *array_alloc_default(int type_size,int (*comp)(const void *,const void *)){
	return array_alloc(DEFAULT_CAPACITY,type_size,comp);
}

/*釋放動態數組的內存*/
void array_free(array_t *arr){
	free(arr->data);
	free(arr);
}

bool array_empty(array_t *arr){
	return (arr->index==0)?true:false;
}

bool array_full(array_t *arr){
	return (arr->index==arr->capacity)?true:false;
}

int array_size(array_t *arr){
	return arr->index;
}

void array_foreach(array_t *arr, void (*visit)(void *elt)){
	for(int i=0;i<arr->index;i++){
		void *elt=(char *)arr->data+i*arr->type_size;
		visit(elt);
	}
	printf("\n");
}



/****************輔助函數**********************/
/*使用字節流從src復制size個字節到dst位置上*/
void copy(void *dst,void *src,int size){
	char *buf=malloc(size);
	memcpy(buf,src,size);
	memcpy(dst,buf,size);
	free(buf);
}

/*使用字節流交換v1和v2的size個字節大小*/
void swap(void *v1,void *v2,int size){
	char *buf=malloc(size);
	memcpy(buf,v1,size);
	memcpy(v1,v2,size);
	memcpy(v2,buf,size);
	free(buf);
}

void exchange(array_t *arr,int i,int j){
	void *v1=(char *)arr->data+i*arr->type_size;
	void *v2=(char *)arr->data+j*arr->type_size;
	swap(v1,v2,arr->type_size);

}

int compare(array_t *arr,int i,int j){
	void *v1=(char *)arr->data+i*arr->type_size;
	void *v2=(char *)arr->data+j*arr->type_size;
	return (arr->comp)(v1,v2);
}


void array_qsort(array_t *arr,int left,int right){
	if(left<right) {
		int last=left,i;
		exchange(arr,left,(left+right)/2); 
		for(i=left+1;i<=right;i++){
			if(compare(arr,i,left)<0){
				exchange(arr,++last,i);
			}
		}
		exchange(arr,left,last);
		array_qsort(arr,left,last-1);
		array_qsort(arr,last+1,right);
	}
}
/*******************************************/


/**
 * 插入一個元素,根據實際空間決定是否擴充或縮減容量
 * 默認范圍內,保持不變;超過容量,則擴充
 */
void array_push(array_t *arr,void *elt){
	if(array_full(arr)){
		/*實現復制函數*/
		void *new_data=malloc(arr->type_size*arr->capacity*2);
		void *src;
		void *dst;
		for(int i=0;i< arr->index;i++){
			src=(char *)arr->data + i*arr->type_size;
			dst=(char *)new_data + i*arr->type_size;
			copy(dst,src,arr->type_size);
		}
		free(arr->data);
		arr->data=new_data;
		arr->capacity *=2;

		/*使用realloc函數*/
		// arr->capacity *=2;
		// arr->data=realloc(arr->data,arr->type_size*arr->capacity);

	}
	void *new_elt=(char *)arr->data + arr->index * arr->type_size;
	copy(new_elt,elt,arr->type_size);
	arr->index++;
}


/*把尾部元素拿掉*/
void *array_pop(array_t *arr){
	--arr->index;
	void *elt=(char *)arr->data+arr->index*arr->type_size;
	return elt;
}

/**
 * 成功找到pos位置上的元素,否則返回NULL
 * 注意: 如果查找函數,需要定義元素大小的比較函數int (*comp)(const void *,const void *)
 */
void *array_get(array_t *arr, int pos){
	if(pos<0||pos>=arr->index){
		printf("Invalid position\n");
		return NULL;
	}
	void *elt=(char *)arr->data+pos*arr->type_size;
	return elt;
}

/*把位置pos上的內容設置成item對應的內容*/
void array_set(array_t *arr,void *elt,int pos){
	if(pos<0||pos>=arr->index){
		printf("Invalid position\n");
		return ;
	}
	void *new_elt=(char *)arr->data+pos*arr->type_size;
	copy(new_elt,elt,arr->type_size);
}


/*動態數組排序*/
void array_sort(array_t *arr){
	array_qsort(arr,0,arr->index-1);
}




免責聲明!

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



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