程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> [C和指針]第二部分,指針第二部分

[C和指針]第二部分,指針第二部分

編輯:關於C語言

[C和指針]第二部分,指針第二部分


聲明:原創作品,轉載時請注明文章來自SAP師太博客,並以超鏈接形式標明文章原始出處,否則將追究法律責任!

第四章     指針... 1
第五章     函數... 14
第六章     數組... 17
第七章     字符(串)/節... 25

第四章     指針


指針代表地址,即使用名字來替換地址,它存儲的就是一個地址值。名字與內存位置之間的關聯並不是硬件所提供的,它是由編譯器為我們實現的。這些指針變量名給了我們一種更方便的方法記住地址——硬件仍然通過地址訪問內存的位置。
 
每個變量都是包含了一序列內容為0或1的位,它們可以被解釋為整數,也可以被解釋為浮點數,這取決於被使用的方式。如果使用的是整型算術指令,這個值就被解釋為整數,如果使用的是浮點型指令,它就是個浮點數。
 
 
指針圖示:
注意箭頭起始於方框內部,因為它代表存儲於該變量的值。同樣,箭頭指向一個位置,而不是存儲於該位置的值,這種記法提示跟隨箭頭執行間接訪問操作的結果將是一個左值(即某個變量的地址,如&a,而不是某個變量a的值)。
 
 
    int *a;
    *a =12;
上面的程序是極為危險與錯誤的。這個聲明創建了一個名叫a的指針變量,後面那個賦值語句把12存儲在a所指向的內存位置。因為從未對a進行初始化,所以我們沒有辦法預測12這個值將存儲於什麼地方。雖然如果變量a是靜態的它會被初始化為0,變量為自動的,它根本不會被初始化,但無論是哪種情況,聲明一個指向整型的指針都不會“創建”用於存儲整型值的內存空間(只有聲明一個非指針變量時才會分配存儲空間)。該程序在不同的編譯器中會產生不同的結果,運氣好的話,提示內存錯誤,運行不好的話,程序表面上會正常結束,而且*a值是正確的,但是這裡隱藏了一個極為嚴重的錯誤並且一旦出現很難發現:a會覆蓋內存中原先隨機初始化指向的某個位置的值。所以,在對指針進行間接訪問之前,必須非常小心,確保它們已被初始化,如:
    int b, *a;//此時的指針隨機指向某個存儲空間
    printf("%d\n", a);//隨機指向的位置:2147348480
    a = &b;//初始化指針,讓它指向某個確定的內存空間
    printf("%d\n", a);//現在指針所指向的位置為:2293576
    *a = 12;//將12常量存儲到a所指向的某個內存位置
    printf("%d\n", *a);//12
    printf("%d", b);//12
 
NULL指針不指向任何東西。要使指針變量為NULL,你可以給它賦一個零。為了測試一個指針變量是否為NULL,你可將它與零值進行比較。之所以選擇零這個值是因為一種約定,就不同機器內部而言,NULL指針實際值可能不是零,在這種情況下,只要使用NULL,編譯器將負責零值和內部值之間的翻譯轉換工作。
 
如果對一個NULL指針進行間接訪問會發生什麼情況呢?結果會因編譯器而異,在有些機器上,它會訪問位置為零的地址中的內容,但這是極其錯誤的;在其他機器上,對NULL指針進行間接訪問將引發一個錯誤,並終止程序,這種情況下程序員就很容易提示發現錯誤,如XP就是這樣的:
    //將字符數組(字符串就是數組)首地址賦給p1
    char *p1="a" ;
    printf("%c",*p1);//a
    //將指針指向內存地址為0的位置
    char *p2=NULL ;
    printf("%c",*p2);//!!出錯 
 
      int a;
    *&a=12;
上面程序的結果就是將12賦值給變量a。&操作符產生變量a的地址,它是一個指針常量,接著,*操作符訪問其操作數所表示的地址,在這個表達式中,操作數是a的地址,所以25就存儲到a中。這與  a = 12 有區別嗎?從結果上講沒有區別,但它涉及到更多的操作。
 
假定a存儲於位置100,下面這條語句合法嗎?
*100 = 12;
這是錯誤的,因為字面值100的類型是整型值,而間接訪問操作只能作用於指針類型表達式。如果你確實想把12存儲於位置100,你必須使用強制類型轉換:
*(int *)100 = 12;//將100從整型轉換為指向整型的指針類型
雖然強轉後合法,但通常我們不會這樣作,因為我們無法預測編譯器會把某個特定的變量放在內存中的什麼位置,所以你無法預先知道變量a的地址。這個技巧除非用來訪問內存中某個特定的位置,不是某個變量,而是訪問硬件本身。
 
 
二級指針(指針的指針):
    int a = 12;
    int *b = &a;
    int**c = &b; 

表達式
等效的結果
a
12
b
&a
*b
a , 12
c
&b
*c
b , &a
**c
*b , a, 12
 
 
 
 
 
 
各種指針表達式示例:
    char ch = 'a';
    char *cp = &ch;
現在我們有了兩個變量,它們初始化如下:

 

表達式

右值

左值

描述

ch

作為右值使用時,粗橢圓表示變量ch的值就是表達式的值(右值為讀取);但是,當這個表達式作為左值使用時,它是這個內存的地址而不是該地址所包含的值,此時該位置用粗方框標記(左值為寫入,可存儲值),表示這個位置就是表達式的結果,另外,它的值並未顯示,因為它並不重要,事實上,這個值將被某個新的值所取代。

&ch

非法

作為右值,這個表達式的值是變量ch的地址,這個值同變量cp中所存儲的值一樣,但這個表達式並沒有涉及到cp變量,所以這個結果值並不是因為它而產生的,所以圖中橢圓並沒有畫在cp的箭頭周圍;作為左值時,當表達式&ch進行求值時,它的結果肯定是存儲在計算機的某個地方了,但我們無法知道,所以這個表達式的結果未標識機器內存中的特定位置,所以不是合法左值。

cp

右值就是cp的值,左值就是cp所處的內存位置。

&cp

非法

該表達式為指向指針的指針。這個值存儲位置並未清晰定義,所以不是一個合法左值

 

 

 

*cp

現在加入了間接訪問符*,所以現在使用實線

*cp+1

非法

圖中虛線表示表達式求值時數據的移動過程。這個表達式的最終結果存儲位置並未清晰定義,所以不是一個合法左值。

*(cp+1)

指針加法運算的結果是一個右值,因為它存儲位置並未清晰定義。如果這裡沒有間接訪問操作*,這個表達式將不是一個合法的左值。然而,間接訪問跟隨指針訪問一個特定的位置,這樣*(cp+1)就可以作為左值使用,盡管 cp+1 本身並不是左值。間接操作符是少數幾個其結果為左值的操作符之一。

++cp

非法

cp本身先加1,再拷貝,橢圓即為拷貝出來的內容。

cp++

非法

先拷貝cp,再將cp本身加1,橢圓即為拷貝出來的內容。

*++cp

 

*cp++

 

++*cp

非法

 

(*cp)++

非法

 

++*++cp

非法

虛線橢圓為++cp的中間結果,虛線方框為*++cp的中間結果,實線橢圓為 ++*++cp 最終結果

++*cp++

非法

 

 


求字符串長度(指針數組的副作用): 
#include<stdio.h>
//指針數組名就是一個二級(多級)指針
int find_char(char ** strings, int value) {
    //循環列表中的每個字符串
    while (*strings != NULL) {
       //判斷字符串中的每個字符,查是否已經找到
       while (**strings != '\0') {
           //strings為第一級指針,即數組名,可以指向每個元素。*strings為第二級指針,指向了元素中的某個具體位置。(*strings)++二級指針可以在元素中移動,這裡就修改了二級指針,並將修改後的值保存在了指針數組中,所以不能重新調用。
           char c = *(*strings)++;
           if (c == value) {
              return 1;
           }
       }
       //strings為第一級指針,strings++在元素之間移動,這裡修改了一級指針,但主函數調用時是傳值,傳遞的是第一級指針的值,並未修改主函數中指針數組首元素地址
       strings++;
    }
    return 0;
}
int main(int argc, char **argv) {
    /*
     * 注,不能這樣定義 char **c = { "a", "b", "c", NULL };
     * 原因就像:int *a; *a =12;一樣,定義指針變量時並沒有為
     * 他創建用於存儲相應內容的空間,這些指針的值是隨機的,所
     * 以上面這樣直接賦值是不可預測的,一般會出現內存讀寫錯誤
     */
    char *c[] = { "abcd", "efgh", "ijkl", "mnop", NULL };
    //指針數組名就是一個二級(多級)指針
    char **b = c;//只能先經過上面定義後,再將數組名賦給二級指針
    printf("%d", find_char(c, 'j'));
    printf("%d", find_char(b, 'j'));//不能進行第二次查詢?
}
第一次調用find_char函數,內存情況如下: 
第一次剛進入find_char函數,內存情況如下: 
find_char函數第一次調用,內存情況如下: 
第二次剛進入find_char函數,內存情況如下: 
從上面內存調度來看,第一級指針未修改,但第二級指針已被修改,所以修改了主調函數中的源數組。如果不想有這樣的副作用,則可以這樣:
int find_char(char ** strings, int value) {
    char *string;//當前正在查找的字符串,使用一個臨時變量為存儲第二維各元素(字符串)指針,這樣在各字符串中通過該臨時指針移動時,不會改變原數組本身的各元素(字符串)指針位置。
    //循環列表中的每個字符串
    while ((string = *strings++) != NULL) {
       //判斷字符串中的每個字符,查是否已經找到
       while (*string != '\0') {
           char c =*string++;
           if (c == value) {
              return 1;
           }
       }
    }
    return 0;
}
 
常量數組與地址
常量數組不代表地址,這與字符串常量是不一樣的,如下:
    int a1[] = { 1, 2, 3, 0 };
    int a2[] = { 4, 5, 6, 0 };
    int a3[] = { 7, 8, 0 };
    /*
     * 注,不能這樣初始化  int *c[] = { { 1, 2, 3, 0 }, { 4, 5, 6, 0 }
     * , { 7, 8, 0 }, NULL }; 原因就是{ 1, 2, 3, 0 }並不能表示數組本
     * 身地址,這與字符串常是有區別的
     */
    int*c[] = { a1, a2, a3, NULL };
 
二級指針與二維數組
二級指針與不等同於二維數組名,數組(無論是多少維的)名永遠是第一個元素的地址,如果是多維,則所有維度的首元素地址都相等(所有維度都是對齊的),測試如下:
    //一維數組
    char a[9] = { 0 };
    sprintf(a,"%p",&a[0]);
    //二維數組
    char aa[][9] = { { 0 } };
    sprintf(aa[0],"%p",&aa[0][0]);
    //三維
    char aaa[][1][9] = { { { 0 } } };
    sprintf(aaa[0][0],"%p",&aaa[0][0][0]); 
指向數組的指針(數組指針)都是一級指針,而不是多級指針(即數組的維度數並不等於指針的級別數):
    char a[2] = { 0 };
    char *p1 = a; //指向一維數組的指針
    *(p1 + 1) = 'a';//修改第二個元素的值
    char a1 = *(p1 + 1);
    char aa[][2] = { { 0 } };
    char (*p2)[2] = aa; //指向二維數組的指針
    /*
     * 不能寫成*(p2 + 1),否則報不能將int類型轉換成char[2]類型
     */
    *(*p2 + 1) = 'b'; //修改第二個元素的值
    char a2 = *(*p2 + 1);
    char aaa[][1][2] = { { { 0 } } };
    char (*p3)[1][2] = aaa; //指向三維數組的指針
    /*
     * 不能寫成*(p3 + 1),否則報不能將int類型轉換成char[1][2]類型
     */
    *(**p3 + 1) = 'c'; //修改第一個元素的值
    char a3 = *(**p3 + 1); 
  
#include<stdio.h>
int main() {
    //多維數組只有第一維可以省略,第三個元素沒有指定內容,這裡會自動初始化為0
    int a[][3] = { { 11, 12,14 }, { 21, 22, 23 } };
    int (*p)[3] = a;//數組指針
    //可以有以下四種方式訪問a[1][0]元素,方括號[]比*優
    //先級高,所以下面是可以省略的
    printf("1、%d %d\n", a[1][0],p[1][0]);//21
    /*
     * (a+1)指向二維數組中的第二個元素數組;(a + 1)[0]在第二個元素數組中移動,
     * 即指向第二個元素數組中的首元素,結果是一個地址;*((a + 1)[0])取第二
     * 個元素數組中的首元素
     */
    printf("2、%d %d\n", *((a + 1)[0]),*((p + 1)[0]));//21
    /*
     * (a+1)指向二維數組中的第二個元素數組;(*(a + 1))取第二個元素數組,結果是
     * 一個一維數組;(*(a + 1))[0]取第二個元素數組中的首元素
     */
    printf("3、%d %d\n", (*(a + 1))[0],(*(p + 1))[0]);//21
    /*
     * a[1]指向第二個元素數組, *(a[1])取第二個元素數組中的首元素
     */
    printf("4、%d %d\n", *(a[1]),*(p[1]));//21
    /*
     * (a+1)指向二維數組中的第二個元素數組,*(a+1)取第二個元素數組,
     * **(a + 1)取第二個元素數組中的首元素
     */
    printf("5、%d %d\n", **(a + 1),**(p + 1));//21
 
    //在定義時可以不指定指針移動的單位,即指針單位長度
    //但如果不指定,則不能對指針進行移動操作,否則編譯出錯
    int (*p1)[] = a;
    //在不移動指針的條件下可以訪問第一個元素
    printf("6、%d\n", **p1);//11
    //編譯不能通過,因為上面在定義p1時未指定指針的最小移動單位
    //所以不能移動
    //  printf("%d", **(p1 + 1));
 
    //一維數組指針
    int b[]={1,2,3};
    int *p2=b;
    printf("7、%d\n", *(p2 + 1));//2
 
    //指定長度後就可以對指針進行移動
    int (*p3)[3] = a;
    /*
     * 以3個int為單位進行移動。(p3 + 1)在第一維中移動,
     * (*(p3 + 1)+1)在第二維中移動
     */
    printf("8、%d\n", *(*(p3 + 1)+1));//22
 
    //編譯時警告。以2個int單位進行移動
    int (*p4)[2] = a;
    printf("9、%d\n", **(p4 + 1));//0

  
高效數組循環方法
float values[5];
float *vp;
for (vp = &values[0]; vp < &values[5];) {
    *vp++ = 0;
}
for語句使用了一個關系測試來決定是否結束循環,這是合法的,因為vp和指針常量都指向同一數組中的元素(事實上,這個指針常量所指向的是數組最後一個元素後面的那個內存位置,雖然在最後一次比較時,vp也指向了這個位置,但由於我們此時未對vp執行間接訪問操作,所以它是安全的)
上面的循環等同於下面的循環:
for (vp = &values[5]; vp > &values[0];) {
    *--vp = 0;
}
但不能使用以下循環:
for (vp = &values[4]; vp >= &values[0];) {
    *vp-- = 0;
}
原因就是:比較表達式vp >= &values[0];的值未定義,因為vp移到了數組的邊界之外。標准允許指向數組的指針與指向數組最後一個元素後面的那個內存位置的指針進行比較,但不允許與指向數組第一個元素之前的那個內存位置的指針進行比較。所以最後不要這樣使用,但在大多數的編譯器中能順利的完成任務,但為了可移植性與穩定性,不能這樣使用。
 
 
 
 
 
聲明一個指針變量並不會自動分配任何內存。在對指針執行間接訪問前,指針必須進行初始化:或者使它指向現有的內存,或者給它分配動態的內存。對未初始化的指針變量執行間接訪問操作是非法的,而且這種錯誤常常難以檢測。其結果常常是一個不相關的值被修改。這種錯誤是很難被調度發現的。
 
NULL指針就是不指向任何東西的指針。它可以賦給一個指針,用於表示那個指針並不指向任何值。對NULL指針執行間接訪問操作的後果因編譯器而異,兩個常見的後果分別是返回內存位置零的值以及終止程序。
 
對指針執行間接訪問操作所產生的值也是個左值,因為這種表達式標識了一個特定的內存位置。

第五章     函數


K&R C:
int *
find_int(key,array,array_len)
int key;
int array;
int array_len;
{}
但不能省略函數的大括號,所以不支持函數原型聲明。
 
為了與ANSI標准之前的程序兼容性,沒有參數的函數的原型應該寫成下面這樣:
int func(void);
 
當程序調用一個無法見到原型的函數時,編譯器便認為該函數返回一個整型值。所有函數都應該具有原型,尤其是那些返回值不是整形的函數。
 
可變參數
可以使用<stdarg.h>中的一組宏定義來操縱可參數。
 
用法:     
(1)首先在函數裡定義一具va_list型的變量,這個變量是指向參數的指針
(2)然後用va_start宏初始化變量剛定義的va_list變量,這個宏的第二個參數是省略號前最後一個有名字的參數。
(3)然後用va_arg返回可變的參數,va_arg的第二個參數是你要返回的參數的類型。如果函數有多個可變參數的,依次調用va_arg獲取各個參數。
(4)最後用va_end宏結束可變參數的獲取。
 
由於參數列表中的可變參數部分並沒有原型,所以,所有作為可變參數傳遞給函數的值都將執行缺省參數類型提升(char、short、float會默認提升為int,float會默認提升為double)。
 
參數列表中至少要有一個命名參數,如果邊一個命名參數也沒有,你就無法使用va_start。這個參數提供了一種方法,用於查找參數列表的可變部分。
 
#include<stdio.h>
#include<stdarg.h>
/*求和*/
int sum(int first, ...) {
    int sum = 0, i = first;
    va_list marker;//定義一具VA_LIST型的變量,這個變量是指向參數的指針
    /*first一般表示可變參數列表中的前面最近第一個有名參數,如果前面有多個有
     * 名參數,即使這裡指定為前面最近第二個有名參數,但可變參數列表還是從最
     * 後一個有名參數後面算起,對無名可變參數列表沒有影響,但編譯時會出警告,
     * 另外,參數列表中至少要有一個命名參數,如果連一個命名參數也沒有,就無法使用可變參數*/
    va_start( marker, first ); /* 用VA_START宏初始化變量剛定義的VA_LIST變量,
                            這個宏的第二個參數是第一個可變參數的前一個參數,
                            是一個固定的參數。*/
 
    while (i != -1) {
       sum += i;
       i = va_arg( marker, int);//VA_ARG返回可變的參數,VA_ARG的第二個參數是你要返回的參數的類型。
    }
    va_end( marker ); /* VA_END宏結束可變參數的獲取     */
    return sum;
}
 
main(void) {
    /* Call with 3 integers (-1 is used as terminator). */
    printf("sum is: %d\n", sum(2, 3, 4, -1));//9
    /* Call with 4 integers. */
    printf("sum is: %d\n", sum(5, 7, 9, 11, -1));//32
    /* Call with just -1 terminator. */
    printf("sum is: %d\n", sum(-1));//0
}
 
 
C語言中的數據封裝
 
/*
** 地址列表模塊的聲明。
*/
 
/*
** 數據特征
**
** 各種數據的最大長度(包括結尾的NULL字節)和地址最大數量。
**
*/
#define    NAME_LENGTH   30     /* 允許出現的最長名字
#define    ADDR_LENGTH   100    /* 允許出現的最長地址
#define    PHONE_LENGTH  11     /* 允許出現的最長電話號碼
#define    MAX_ADDRESSES 1000       /* 允許出現的最多地址個數
 
/*
** 接口函數
**
** 給出一個名字,查找對應的地址
*/
charconst *
lookup_address(charconst *name);
 
/*
** 給出一個名字,查找對應的電話號碼
*/
charconst *
lookup_phone(charconst *name);
                        ——addrlist.h
 
/*
** 用於維護一個地址列表的抽象數據類型
*/
 
#include"addrlist.h"
#include<stdio.h>
 
/*
** 每個地址的三個部分,分別保存於三個數組的對應元素中
*/
staticchar name[MAX_ADDRESSES][NAME_LENGTH];
staticchar address[MAX_ADDRESSES][ADDR_LENGTH];
staticchar phone[MAX_ADDRESSES][PHONE_LENGTH];
 
/*
** 這個函數在數組中查找一個名字並返回查找到的位置的下標
** 如果這個名字在數組中並不存在,函數返回-1
*/
staticint find_entry(charconst *name_to_find) {
    int entry;
 
    for (entry = 0; entry < MAX_ADDRESSES; entry += 1)
       if (strcmp(name_to_find, name[entry]) == 0)
           return entry;
 
    return -1;
}
 
/*
** 給定一個名字,查找並返回對應的地址
** 如果名字沒有找到,函數返回一個NULL指針
*/
charconst *
lookup_address(charconst *name) {
    int entry;
 
    entry = find_entry(name);
    if (entry == -1)
       return NULL;
    else
       return address[entry];
}
 
/*
** 給定一個名字,查找並返回對應的電話號碼
** 如果名字沒有找到,函數返回一個NULL指針
*/
charconst *
lookup_phone(charconst *name) {
    int entry;
 
    entry = find_entry(name);
    if (entry == -1)
       return NULL;
    else
       return phone[entry];
}
                        ——addrlist.c

第六章     數組


字符數組與字符指針的區別:雖然字符指針指向的字符串內容是存放在某個數組中的,但是這與定義的字符數組是不樣的,即字符指針所指向的數組的內容是不能被修改的;另外數組是一個常量指針,一旦初始化後,數組名就能不再指向其他數組了。
 
數組名並不是表示整個數組,它是一個指針常量,也就是數組第一個元素的地址,它的類型取決於數組元素的類型:如果它們是int類型,那麼數組名的類型就是“指向int的常量指針”
 
數組具有確定的數量的元素,而指針只有是一個標量值,只有當數組名在表達式中使用時,編譯器才會為它產生一個指針常量,注意,這個值是指針常量,而不是指針變量,你不能修改常量的值,即數組一旦分配內存後,位置就定下來了,不會再被移動。
 
函數如果要返回一個數組,則數組只能以指針的形式返回,而不能以數組的形式返回:
int * fun() {
    int a[]={1,2};
    return a;
}
int main(int argc, char **argv) {
    printf("%d", fun()[1]);//2
}
 
 
只有在兩種場合下,數組名並不用常量指針來表示——就是當數組名作為sizeof操作符或單目操作符&的操作數時,所以下面 a與&a是相等的,但普通的指針變量不是這樣:
int a[] = { 1, 2, 3, 4, 5 };
    printf("%p\n", a);//0022FF30
    printf("%p\n", &a);//0022FF30
    printf("%d\n", sizeof a);//20
 
    intconst * const p = &b;
    printf("%p\n", p);//0022FF2C
    printf("%p\n", &p);//0022FF28
 
不能將一個數組名賦給另一個數組名,因為數組名是一個常量指針,如果需要拷貝數組,只能使用循環來一個個元素進行拷貝。
 
除了優先級之外,下標引用和間接訪問完全相同,下面兩個表達式是等同的:
    array[subscript]
    *(array + (subscript))
 
 
int main() {
    int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int *ap = array + 2;
    //ap{0022FF1C} == (array + 2){0022FF1C} == &array[2]{0022FF1C}
    printf("ap{%p} == (array + 2){%p} == &array[2]{%p}\n", ap, array + 2,
           &array[2]);
    //*ap{2} == *(array + 2){2} == array[2]{2}
    printf("*ap{%d} == *(array + 2){%d} == array[2]{%d}\n", *ap, *(array + 2),
           array[2]);
    //ap[0]{2} == *(ap + (0)){2} == array[2]{2}
    printf("ap[0]{%d} == *(ap + (0)){%d} == array[2]{%d}\n", ap[0],
           *(ap + (0)), array[2]);
    //ap+6{0022FF34} == (array + 8){0022FF34} == &array[8]{0022FF34}
    printf("ap+6{%p} == (array + 8){%p} == &array[8]{%p}\n", ap + 6, array + 8,
           &array[8]);
    //*ap+6{8} == array[2]+6{8}
    printf("*ap+6{%d} == array[2]+6{%d}\n", *ap + 6, array[2] + 6);
    //*(ap+6){8} == ap[6]{8}== array[8]{8}
    printf("*(ap+6){%d} == ap[6]{%d}== array[8]{%d}\n", *(ap + 6), ap[6],
           array[8]);
    //ap[-1]{1}==array[1]{1}
    printf("ap[-1]{%d}==array[1]{%d}",ap[-1],array[1]);
}
 
C語言中數組下標在編譯時會轉換為等效的指針運算:
int main() {
    int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    printf("%d\n", array[2]);//2
    printf("%d\n", 2[array]);//2
    printf("%d\n", *(2 + (array)));//2
    printf("%d\n", *(array + 2));//2
}
所以2[array]與array[2]是等效的,這個詭異的技巧之所有可行,緣於C實現下標的方法,對編譯器來說,2[array]== *(2 + (array))== *(array + 2)==array[2]
 
 
數組下標與指針效率的比較:
    int array[10], i;
    for (i = 0; i < 10; i++) {
       array[i] = 0;
    }
為了對下標表達式求值,編譯器在程序中插入指令,取得i的值,並把它與整型的長度(也就是4)相乘。這個乘法需要花費一定的時間和空間。現使用指針來實現同樣的功能:
    int array[10], *p;
    for (p = array; p < array + 10; p++) {
       *p = 0;
    }
現在這個乘法出現在for語句的調整部分,1這個值必須與整型的長度相乘,然後現與指針相加,但這裡存在一個重大區別:循環每次執行時,執行乘法運算的都是兩個相同的數(1和4),結果,這個乘法只在編譯時執行一次,程序在運行時並不執行乘法運算,所以在運行時所需要的指令就少一些。
 
 
最高效的數組訪問:
#define SIZE 50
int x[SIZE];
int y[SIZE];
//數組拷貝
int main() {
    registerint *p1, *p2;
    for (p1 = x, p2 = y; p1 < &x[SIZE];) {
       *p1++ = *p2++;
    }
}
結論:當你根據某個固定數目的增量在一個數組中移動時,不如使用指針變量將比使用下標產生效率更高的代碼;聲明為寄存器變量的指針通常比位於靜態內存和堆棧中的指針效率更高;如果你可以通過測試一些已經初始化並經過調整的內容來判斷循環是否該應該終止,那麼你就不需要使用一個單獨的計數器。
 
指針和數組
    int a[5];
    int *b; 
聲明一個數組時,編譯器將根據聲明所指定元素數量為數組保留內存空間,然後再創建數組名,它的值是一個常量,指向這段空間的起始位置。聲明一個指針變量時,編譯器只為這個指針本身保留內存空間,它並不為任何整型值分配內存空間,而且,指針變量並未初始化為指向現有的有意義的空間。所以 *a 是完全合法的,但表達式 *b 卻是非法的。*b將訪問內存中某個不確定的位置,或者導致程序終止,另外,表達式b++可以通過編譯,但a++卻不行,因為a的值是個常量。
 
 
數組參數
數組名的值就是一個指向數組第一個元素的指針,所以傳遞給函數的是一份該指針的拷貝。函數如果執行了下標引用,實際上是對這個指針執行間接訪問操作,並且通過這種間接訪問,函數可以訪問和修改主調程序的源數組元素。
 
傳址調用是通過傳遞一個指向所需元素的指針,然後在函數中對該指針執行間接訪問操作實現對數據的訪問,而作為參數的數組名是個指針,下標引用實際執行的就是間接訪問,所以傳遞數組時“好像”體現了傳址,但本身卻是傳值,那數組的傳值行為表現在什麼地方呢?傳遞給函數的是參數的一份拷貝(指向數組起始位置的指針的拷貝),所以函數可以自由地操作它的指針形參,而不必提心會修改對應的作為實參的指針。所以並不矛盾:所有的參數都是通過傳值方式傳遞的。
 
傳遞一個數組時,正確的函數形參應該是怎樣的?它是應該聲明為一個指針還是一個數組?調用函數時實際傳遞的是一個指針。但為了使用程序員新手更容易上手一些,編譯器也接受數組形式的函數形參,下面兩個函數原型是相等的:
int strlen(char * string);
int strlen(char string[]);
這個相等性暗示指針和數組名實際上是相等的,但千萬不要被它糊弄了,這兩個聲明確實相等,但只是在當前這個上下文環境中。哪個“更加准確”呢?答案是指針,因為實際上是個指針,而不是數組,同樣,表達式sizeof string的值是指向字符的指針的長度,而不是數組的長度:
int size1(int * a){
    returnsizeof a;
}
int size2(int  a[]){
    returnsizeof a;
}
int main() {
    int a[5];
    printf("%d\n",sizeof a);//20
    printf("%d\n",size1(a));//4
    printf("%d\n",size2(a));//4
}
現在你應該清楚為什麼函數原型中的一維數組形參無需寫明它的元素數目(如果是多維數組,則也只能省略第一維的大小),因為函數並不為數組參數分配內存空間。形參只是一個指針,它指向的是已經在其他地方分配好的內存的空間,它可以與任何長度的數組匹配。另一方面,這種實現方法使函數無法知道數組的長度,如果函數需要知道數組的長度,它必須作為一個顯示的參數傳遞給函數。
 
不完整的初始化
    int vector[5] = { 1, 2, 3, 4, 5, 6 };
    int vector[5] = { 1, 2, 3, 4 };
第一個聲明是錯誤的,我們沒有辦法把6個整型值裝到5個整型變量中去,但第二個聲明是合法的,最後一個元素初始化為0。另外,也只能省略最後元素的值,不能省略中間的。
 
自動計算數組長度
    int vector[] = { 1, 2, 3, 4 };
如果聲明中並未給出數組的長度,編譯器就把數組的長度設置為剛好容納所有的初始值的長度。
 
字符數組的初始化
    char string1[] = { 'H', 'e', 'l', 'l', 'o', 0 };
    char string2[] = "Hello";
第二種看上去是一個字符串常量,實際上並不是(只是一個初始化列表),它只是第一個聲明的另一種寫法。那什麼情況下"Hello"是一個字符串常量呢?要根據它所處的上下文來區分:當用於初始化一個字符數組時,它就是一個初始化列表,在其他任何地墳,它都表示一個字符串常量。
 
    char string1[] = "Hello";
    char *string2 = "Hello";
這兩個初始化看上去很像,但它們具有不同的含義。第一個初始化一個字符數組的元素,而後者則是一個真正的字符串常量,這個指針變量被初始化為指向這個字符串常量的存儲位置,並且這個位置的內容不能被修改: 
 
  多維數組
多維數組在內存分配上與一維數組一樣,在物理上沒有區別,也是連續的,只是多維數組在邏輯上將元素劃分為多個邏輯單位。
 
    int matrix[6][10];
    int *mp;
    matrix[3][8] = 38;
    matrix[3][9] = 39;
    matrix[4][0] = 40;
    mp = &matrix[3][8];
    printf("First value is %d \n", *mp);//38
    printf("Second value is %d \n", *++mp);//39
    printf("Third value is %d \n", *++mp);//40
這個例子使用一個指向整型的指針遍歷存儲了一個二維整型數組元素的內存空間,這個技巧被稱為壓扁數組,它實際上是非法的,因此從某行移到下一行後就無法回到包含第1行的那個子數組,盡管通常沒有問題,但有可能的話還是應該避免這樣使用。
 
多維數組名:
    int matrix[3][10];
matrix可以看作是一個一維數組,包含3個元素,只是每個元素又是包含了10整型元素的數組。matrix這個名字是一個指向它第1個元素的指針,所以matrix是一個指向一個包含10個整型元素的數組的指針
 
matrix[1][5] == *(*(matrix + 1) + 5) == *(matrix[1] + 5)
matrix 指向第一行,(matrix+1)指向第二行,*(matrix+1)代表第二行元素(第二維數組),既然*(matrix+1)又是一個數組,所以該表達式又代表了(matrix+1)指向的第二行子數組的首元素的地址,所以(*(matrix+1)+5)表示在(matrix+1)指向的第二行子數組中移動到第6個元素位置上,所以*(*(matrix+1)+5)代表了該位置上的元素。
 
int matrix[3,10];
在其他語言中表示二維數組,但在C中不是的,它是一個逗號表達式,最終的結果為matrix[10],而不是二維數組了
 
    int vector [10], *vp = vector;
    int matrix[3][10], *mp = matrix;
第一個表達式合法,vp與vector具有相同的類型,都是指向整型的指針;第二個聲明非法,因為matrix並不是一個指向整型的指針,而是一個指向整型數組的指針,正確寫法如下:
         int (*p)[10]= matrix;
(*p)[10]由於間接訪問*的優先級低於數組下標訪問,所以使用括號,這樣就使 *p[10]從數組類型轉換成了一個指針類型,p是一個指向擁有10個整型元素的數組的指針,使用它可以在matrix數組中一行一行的移動。上述表達式指向了matrix數組的第一行。
如果你需要一個指針逐個訪問整型元素而不是逐行在數組中移動,你應該這樣:
    int *pi=&matrix[0][0]; //第一個元素的地址
    int *pi=matrix[0];//第一個元素的地址也就是數組名
上面兩個聲明都創建了一個簡單的整型指針,並以兩種不同的方式進行初始化,指向matrix的第1個整型元素。
 
如果你打算在指針上執行任何指針運算,應該避免這種類型的聲明:
    int (*p)[] = matrix;
p仍然是一個指向整型數組的指針,但 數組的長度沒有,當與某個整型運算時,它的值將根據空數組來進行調整,即與零乘,這不是我們想要的。有些編譯器要以捕捉到這類錯誤,但有些編譯器卻不能。
 
int matrix[3][10];
如果傳遞給一個函數,則函數的原型聲明可以有以下兩種:
void func(int(*p)[10]);
void func(int p[][10]);
但決不能是:
void func(int **p);
因為**p的類型為指向指針的指針,而(*p)[10]表示指向一個具有10元素的數組的指針。
 
多維數組的初始化可以像一維數組那樣:
    int matrix[2][3] = { 1, 2, 3, 4, 5, 6 };
但Java不允許這樣
 
    charconst* keyword[] = { "do", "for", "if", "register", "return",
           "switch", "while" };
    charconst**kwp = keyword;
keyword是一個指針數組,數組名keyword是一個二級指針(每個元素都是指針),其內存結構如下: 
 
charconstkeyword[][9] = { "do", "for", "if", "register", "return",
           "switch", "while" };
其內存結構如下: 
第七章     字符(串)/節
C語言並沒有的字符串數據類型,因為字符串是以字符串常量的形式出現或者存儲於字符數組中。所有字符串都必須存儲於字符數組或動態分配的內存中。
 
字符串定義方式:
    char str1[10] = "abc";
    char *str2 = "abc";
 
NUL字節是字符串的終止符,但它本身並不是字符串的一部分,所以字符串的長度並不包括NUL字節。
 
庫函數strlen的原型如下:
size_t strlen(charconst *string);
注意:strlen返回一個類型為size_t的值,這個類型是在頭文件stddef.h中定義的,它是一個無符號整數類型。如果表達式中使用無符號數可能導致不可預料的結果。例如下面兩個表達式看上去相同:
if(strlen(x) >= strlen(y))...
if(strlen(x) -strlen(y) >=0)...
但事實上它們是不相同的,第一個語句會是正確的,但第二個語句的結果永遠為真,因為strlen的結果是個無符號數,所以操作符 >= 左邊的表達式也將是無符號數,而無符號數絕不可能是負的。
 
char *strcpy(char *dst, charconst *src);
由於dst參數將進行修改,所以它必須是個字符數組或者是一個指向動態分配內存的數組的指針不能使用字符串常量。即使新的字符串比dst原先的內容短,由於新字符串是以NUL字節結尾,所以老字符串最後剩余的幾個字符也會被有效地刪除。另外,必須確保目的字符數組的空間中以容納需要復制的字符串,如果字符串比數組長,多余的字符仍被復制,它們將覆蓋原先存儲於數組後面的內存空間的值,這是危險的。
 
char *strcat(char *dst, charconst *src);
這個也需要考慮目標參數的可修改性與長度。它找到dst字符串的末尾,並把src字符串的一份拷貝添加到這個位置。
 
    strcat(strcpy(dst, a), b);
等同於下面兩句:
    strcpy(dst, a);
    strcat(dst, b);
 
 
int strcmp(charconst *s1, charconst *s2);
s1小於s2返回小於零,大於返回大於零,相等返回零,所以下面的條件表達式是錯誤的:
if(strcmp(a,b))
 
char *strncpy(char *dst, charconst *src, size_t len);
它總是正好向dst寫入len個字符。如果strlen(src)的值小於len,dst數組就用額外的NUL字節填充到len長度。如果strlen(src)的值大於或等於len,那麼只有len個字符被復制到dst中,此時它的結果將不會以NUL字節結尾,所以,在使用不受限的函數之前,你首先必須確定字符串實際上是以NUL字節結尾的,確保以後可以將字符串用在不受限函數中,如:
    char buffer[BSIZE];
    strncpy(buffer, name, BSIZE);
    buffer[BSIZE - 1] = '\0';
如果name的內容可以容納於bufer中,最後那個賦值沒有任何效果,但是,如果name太長,這條賦值語句可以保證buffer中的字符串是以NUL結尾的,以後對這個數組使用strlen或其他不受限制的字符串函數將能夠正確的工作。
 
char *strncat(char *dst, charconst *src, size_t len);
盡管strncat也是一個長度受限制的函數,但它和strncpy不同,它從src中最多復制len個字符到目標數組的後面,但是,strncat總是在結果字符串後面添加一個NUL字節,而且它不會像strncpy那樣對數組用NUL字節進行填充。strncat最多向數組復制len個字符(再加一個結尾的NUL字節),它不管目標參數除去原先存在的字符串之後留下的空間夠不夠。
 
char *strchr(charconst *str, int ch);
char *strrchr(charconst *str, int ch);
第一個從前往後找,第二個從後往前找,返回第一次出現的字符位置。如果不存在就返回NULL。
 
char *strpbrk(charconst *str, charconst * group);
這個函數返回一個指向str中第一個匹配group中任何一個字符的字符位置,如果不存在,返回NULL。
 
char *strstr(charconst *s1, charconst *s2);
在s1中查找出現s2的子串,如果不存在返回NULL,如果第二個是一個空字符串,則返回s1。
 
函數庫中沒有strrstr函數,我們自已來實現一個:
/*
** 在字符串s1中查找字符串s2最右出現的位置,並返回一個指向該位置的指針
*/
#include<string.h>
 
char *
my_strrstr(charconst *s1, charconst *s2) {
    registerchar *last;
    registerchar *current;
 
    /*
     ** 將指針初始化為我們已經找到的前一次匹配位置
     */
    last = NULL;
 
    /*
     ** 只在第2個字符串不為空時才進行查找,如果s2為空,返回null
     */
    if (*s2 != '\0') {
       /*
        ** 查找s2在s1中第一次出現的位置
        */
       current = strstr(s1, s2);
 
       /*
        ** 每次找到字符串時,指向它的起始位置,然後查找該字符串下一個匹配位置。
        */
       while (current != NULL) {
           last = current;
           current = strstr(last + 1, s2);
       }
    }
    /*
     ** 返回指向我們找到的最後一次匹配的起始位置的指針
     */
    return last;
}
 
查找一個字符串前綴:
size_t strspn(charconst *str, charconst *group);
返回str起始部分匹配group中任意字符的字符數。例如,如果group包含了空格、制表符等空白字符,這個函數將返回str起始部分空白字符數目。下面的代碼將計算一個指向字符串中第一個非空白字符的指針:
    char * ptr = buffer + strspn(buffer, "\n\r\f\t\v");
 
size_t strcspn(charconst *str, charconst *group);
strcspn正好與strspn相反,它對str字符串起始部分中不與group中任何字符匹配的字符進行計數。
示例:
    char buffer[] = "25,142,330,Smith,J,239-4123";
    int len1, len2;
    len1 = strspn(buffer, "0123456789");
    printf("%d\n", len1);//2
    len2 = strcspn(buffer, "ith");
    printf("%d\n", len2);//13
 
查找標記:
char * strtok(char * str,constchar *set)
strtok函數使用set字符串中的標記將字符串str分隔成不同的獨立部分,每次可以返回這個獨立部分。strtok函數找str中的標記後,使用NUL替換,並返回一個指向標記前的鄰近字符串指針。如果函數的第一個參數str不為NULL,函數將查找字符串的第一個標記,如果為NULL,函數就從上一次查找的位置後開始查找標記。通常,第一次調用時傳遞一個字符串指針,以後,這個函數被重復調用(第一參數傳遞NULL),直到它返回NULL:
void print_tokens(char *line) {
    staticchar whitespace[] = " ,";
    char *token;
    for (token = strtok(line, whitespace); token != NULL; token = strtok(NULL,
           whitespace)) {
       printf("Next token is %s\n", token);
    }
}
int main() {
    char buffer[] = "a b,c";
    print_tokens(buffer);
}
由於strtok函數保存了處理過的函數的局部狀態信息,所以不能同時解析兩個字符串(也不能用在多線程中)。因此,如果for循環體內調用了一個在內部調用strtok函數,上面的程序將不能正常執行。另外,該函數會修改它所處理的字符,如果源字符串不能被修改,那就先拷貝一份,再將這份拷貝傳遞給該函數。
 
全局錯誤碼所對應的描述信息
char *strerror(int error_number);
當你調用一些函數,請求操作系統執行一些功能,如果出現錯誤,操作系統是通過設置一個外部整型變量errno進行錯誤代碼報告的。而strerror函數把其中一個錯誤代碼作為參數並返回一個指向用於描述錯誤的字符串的指針。
    printf("%s\n", strerror(1));//Operation not permitted
    printf("%s\n", strerror(2));//No such file or directory
    printf("%s\n", strerror(3));//No such process
 
內存函數
內存函數可以處理任何字節,包括字符串中的NUL字節,所以如果某個字符串中含有NUL字節,就需要使用內存函數來處理。
void *memcpy(void *dst, voidconst *src, size_t length);
void *memmove(void *dst, voidconst *src, size_t length);
void *memcmp(voidconst *a, voidconst *b, size_t length);
void *memchr(voidconst *a, int ch, size_t length);
void *memset(void *a, int ch, size_t length);
每個原型最後一個參數表示需要處理的字節數。與strn開頭的函數不同,它們在遇到NUL字節時並不會停止操作。這些函數可以用於任何類型,因為參數為void*型指針,而任何類型的指針都可以轉換為void*類型指針。對於長度大於一個字節的數據(如拷貝整型數組),要確保把數量和數據類型的長度相乘:
    memccpy(saved_answers, answers, count * sizeof(answers[0]));
你也可以使用這種技巧復制結構或結構數組。
 
memmove的行為與memcpy差不多,只是它的源和操作數的內存可以重疊,它可能比 memcpy慢一些,但是,如果源和目標參數可能存在重疊,就應該使用 memmove
    int a[5] = { 1, 2, 3, 4 };
    memmove(a + 1, a, 4 * sizeof(a[0])); 
  
memcmp按照無符號字符逐字節進行比較,函數的返回類型和strcmp函數一樣。由於這些值是根據一串無符號字節進行比較的,所以如果用於比較的不是單字節的數據如整數或浮點數就可以能出現不可預料的結果。
 
memchr與strchr相似,不過可以查找NULl字節以後的內容,並返回一個指向該位置的指針。
    char a[] = { 'a', 0, 'b' };
    printf("%c\n", strchr(a, 'b') == NULL ? '0' : *strchr(a, 'b'));//0
    printf("%c\n", memchr(a, 'b', 3) == NULL ? '0'
           : *(char *) memchr(a, 'b', 3));//b
 
memset函數把從aga vck r length個字節都設置為字符值ch,例如:
    memset(buffer, 0, SIZE);
把buffer的前個字節都初始化為0。
 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved