1. 程式人生 > >C語言之漫談指標(下)

C語言之漫談指標(下)

C語言之漫談指標(下)

在上節我們講到了一些關於指標的基礎知識:

詳見:C語言之漫談指標(上)

本節大綱:

  • 零.小tips
  • 一.字元指標
  • 二.指標陣列與陣列指標
  • 三.陣列傳參與指標傳參
  • 四.函式指標及函式指標陣列
  • 五.回撥函式
  • 六.例題講解

 

零.小tips

在正式開始下節之前,我們先來穿插兩個小tips:

1.列印函式哪家強!

//假設有下面兩個列印函式,我們應該首選哪個?
struct person
{
    char name[20];
    int age;
    char sex[5];
    char tele[13];
    char addr[20];
};


void print_1(struct person peo)
{
    printf("This is print_1\n");
    printf("%s\n", peo.name);
    printf("%d\n", peo.age);
    printf("%s\n", peo.sex);
    printf("%s\n", peo.tele);
    printf("%s\n", peo.addr);
    printf("\n");
}

void print_2(struct person* peo)
{
    printf("This is print_2\n");
    printf("%s\n", peo->name);
    printf("%d\n", peo->age);
    printf("%s\n", peo->sex);
    printf("%s\n", peo->tele);
    printf("%s\n", peo->addr);
    printf("\n");
}

int main()
{
    struct person peo = { "zhangsan",18,"male","12345678","hualalalala" };
    print_1(peo);
    print_2(&peo);
    return 0;
}

在上述兩個列印函式,我們應該首選 print_2() 函式

我們先來看看兩個函式的傳參是什麼:

void print_1(struct person peo):引數是整個結構體

void print_2(struct person* peo):引數是該結構體的地址

我們不妨想一想當我們要傳遞的值非常多時,如果採用傳遞變數自身的模式,

變數傳遞到函式裡將會產生一份臨時拷貝,那會將使用多少的記憶體,且該記憶體的使用毫無意義,僅僅只是為了列印,這並不划來

而方式二採用傳地址的方式,記憶體不僅使用的更少了一些,而且效率也變得更高!

2.  *  與  [ ] 的優先順序關係:

如圖:

 

 

 我們看到 [ ]運算子的優先順序是高於 * 的!

如 int* arr[3]   這個arr首先於[3]結合再與*結合。

 

一.字元指標

在上節的指標型別中,我們見到了一種為char*的指標,他指向的空間內容為 char 型,且解引用時只能解引用1個位元組。

並且我們一般的用法如下:

int main()
{
    char ch = 'a';
    char* p = &ch;
    *p = 'w';
    return 0;
}

但是如果我們這樣寫 char* p = "abcdef"; 它算不算字元指標呢?

我們可以解引用列印一下: printf("%c\n", *p); 我們會發現它列印了一個字元 a 

所以 char* p = "abcdef"; 這種型別也算字元指標,它指向的是首元素的地址

也就是說把 a 的地址放在了 p 中

但是我們發現當我們這樣寫的時候 printf("%s\n", p); 執行結果會出現整個字串,那這是為什麼呢?

 

 

 那我們繼續回到剛才的話題,既然 char* p = "abcdef";  是一個字元指標,那我們可不可以對放在裡面的值進行修改呢?

我們試試看:

int main()
{

    char* p = "abcdef";
    printf("%s\n", p);

    *p = 'c';

    return 0;
}

我們執行時會發現編譯器執行到一半會崩,並彈出: 寫入訪問許可權衝突。的錯誤。

這又是為什麼呢?

這又得回到我們上節所提到的計算機儲存器的一些知識了

見圖:

 

 

 所以我們要想修改,應該怎麼做?

如果想要修改字串的內容,就需要對它的副本進行操作。如果在儲存器的非只讀區域建立了字串的副本,就可以修改它的字母了。

簡而言之:建立一個字元陣列來接收它即可

int main()
{

    char ch[] = "abcdef";
    printf("%s\n", ch);


    *ch = 'c';
    printf("%s\n", ch);
    return 0;
}

我們會發現執行結果為:

abcdef

cbcdef

我們同樣再來看一下原理:

 

 

[tips]:  

  所以若我們要寫出 char* p = "abcdef"; 這樣的字元指標,最好在前面加一個 const 修飾符

  即: const char* p = "abcdef"; ,因為 p 指向的內容不可修改。

 

有了以上的知識,我們來看一道題:

#include <stdio.h>
int main()
{
    char str1[] = "hello world.";
    char str2[] = "hello world.";
    char* str3 = "hello world.";
    char* str4 = "hello world.";

    if (str1 == str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");

    if (str3 == str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");
    return 0;
}

這道題的結果是什麼呢?

我們來分析一下:

 

 

 所以,執行結果會是:

str1 and str2 are not same
str3 and str4 are same

我們來驗證一下:

 

 

 

二.指標陣列與陣列指標

1.指標陣列

在上節中我們便提到了指標陣列的概念,我們再次來複習一下

字元陣列:存放字元的陣列

整形陣列:存放整形的陣列

指標陣列:存放指標的陣列

比如:

int main()
{
    int a = 1;
    int b = 2;
    int c = 3;

    int* arr[3] = { &a,&b,&c };//arr便是一個指標陣列

    return 0;
}

對於一個單純的指標陣列並沒有太多的知識

2.陣列指標

陣列指標,末尾兩字為“指標”,所以它就是個指標,用來指向陣列的指標。

那它怎麼表示呢? int (*p2)[10]; 

我們可以在這解釋一下:

 

 

 我們還可以這樣理解:

 

 

 3.陣列名與&陣列名

在上節課中,我們舉過這樣的一個例子:

#include <stdio.h>
int main()
{

    int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };

    printf("%p\n", arr);

    printf("%p\n", &arr[0]);

    return 0;
}

我們當時發現他們倆的地址相同,於是我們下了一個結論:陣列名錶示的就是陣列首元素地址

那麼陣列名與&陣列名呢,我們看看下面這個例子

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

我們發現結果也是相同的,但我們能不能所這兩個東西是一樣的?

我們將上面的程式碼微做調整:(我們分別再進行加一)

#include <stdio.h>
int main()
{

    int arr[10] = { 0 };

    printf("arr = %li\n", arr);
    printf("&arr= %li\n", &arr);

    printf("arr+1 = %li\n", arr + 1);
    printf("&arr+1= %li\n", &arr + 1);

    return 0;
}

注:為了方便觀察地址值的差異,筆者在這用 %li 來列印

 

我們會發現,結果並不一樣,這就說明,它們倆並不是一個東西;

那它倆究竟有何不同呢?

  &arr 表示的是陣列的地址,而不是陣列首元素的地址,加1就跳過整個陣列

  arr表示的是陣列首元素的地址,加1跳過1個元素,到陣列的第2個元素

所以我們來看看結果:

 

 

 arr與arr+1剛好差4個位元組

而&arr與&arr+1剛好差 我們所定義的 一個有10個整形的陣列的大小即40個位元組

所以:到這大家明白它倆的區別了嗎?

4.指標[整數]與  *(指標±整數)

對此我們看一個例子:

int main()
{
    int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
    int* p = arr;
    int i = 0;
    printf("指標[整數]---- *(指標±整數)\n");
    for (i = 0; i < 10; i++)
    {
        printf("     %d    ----      %d   \n",p[i],*(p+i));
    }

    return 0;
}

執行結果:

 

 

 我們發現    指標[整數]  與  *(指標±整數)  的效果相同

 

此外再提一點,指標地址的強制型別轉換:

  我們都知道,指標的型別決定了指標一次可以解引用幾個位元組,所以指標的強制型別轉換隻是改變了該指標一次可以解引用幾個位元組!

如:

#include<stdio.h>

int main()
{
    double a = 1;//首先 dp  ip  cp 儲存的地址相同

    double* dp = &a;//只是 dp 解引用可以訪問8個位元組,所以dp+1跳過8個位元組

    int* ip = (int*)&a;// ip 解引用可以訪問4個位元組,ip+1跳過4個位元組

    char* cp = (char*)&a;// cp 解引用可以訪問1個位元組,cp+1跳過一個位元組

    return 0;// 此外,並無區別
}

 

對上述知識我們先來看一道題:

int main()
{

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

    int* ptr = (int*)(&a + 1);

    printf("%d,%d",*(a + 1), *(ptr - 1));

    return 0;
}
//程式的結果是什麼?

  1.首先a是一個數組名,也就是陣列的首元素地址,加一,跳過一個元素,指向陣列的第二個元素,此時再進行解引用,所獲得的值就是陣列的第二個元素

   所以*(a+1)的值就是 2;

  2.ptr=int*(&a+1),首先這個陣列名加了 & 符號,所以它加一就跳過整個陣列然後將其轉為int*,最後再減一,此時指標的型別為 int * ,所以指向的位置就再往前走一個int 的位置,

  指向陣列的最後一個元素,所以*(ptr - 1)的值就為 5;

 

然後,我們再來看這一道題:

int main()
{
    int a[4] = { 1,2,3,4 };

    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a + 1);

    printf("%x %x\n", ptr1[-1], *ptr2);
    return 0;
}

它的結果是什麼呢?

  1.首先&a+1就跳過整個陣列,然後ptr[-1]就是指跳過整個陣列之後,再向前移一位,也就是陣列元素4,

   所以ptr[-1]就是 4 

  2.對於ptr2來說,我們先看  int(a) + 1,這部分,首先a是一個數組名,那它就是陣列首元素的地址,總之就是一個地址,現在將他強制型別轉換為 int ,再加一,此時就是簡簡單單的加一

   然後又將其強制型別轉換為 int* 型,應為再強轉之前加了一,所以現在指向的是陣列首元素的第二個位元組,然後按照你編譯器的大小端模式所儲存的記憶體,再往後讀取3個位元組,

   再按找大小端模式拿出來,就是*ptr2的值

 

對於第二點的一些疑問,筆者整理了一張圖:

 

 

 這樣就應該容易理解第二點了。對於大小端的儲存模式詳見:C語言之資料在記憶體中的儲存

 

5.陣列指標的使用

再學習了概念之後,我們就開始正式使用了:

#include <stdio.h>
int main()
{

    int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };

    int(*p)[10] = &arr;//把陣列arr的地址賦值給陣列指標變數p

    //但是我們一般很少這樣寫程式碼

    return 0;
}

 

使用,這裡我們用到的例子是:依據  C語言之三字棋的實現及擴充套件 改編的一些函式;

如我們的棋盤列印函式:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)//寫成這種形式,大家肯定都能明白,但是寫成下面那樣呢?
{

    int i = 0;
    int j = 0;

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

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

            printf("%d ", arr[i][j]);
        }

        printf("\n");
    }
}
void print_arr2(int(*arr)[5], int row, int col)//首先arr是一個數組指標,它指向的是一個有五個 int 元素的陣列,所以現在的arr就相當於二維陣列第一行的地址
{//那麼,arr+1 便表示第二行首元素的地址,以此類推

    int i = 0;
    int j = 0;

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

        for (j = 0; j < col; j++)
        {
       //printf("%d ", *(*(arr+i)+j));//他們倆效果相同
            printf("%d ", arr[i][j]);//這裡之前已經說了,指標[整數] = *(指標±整數)
        }

        printf("\n");
    }
}
int main()
{

    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };

    print_arr1(arr, 3, 5);

    //陣列名arr,表示首元素的地址

    //但是二維陣列的首元素是二維陣列的第一行

    //所以這裡傳遞的arr,其實相當於第一行的地址,是一維陣列的地址

    //可以陣列指標來接收

    print_arr2(arr, 3, 5);

    return 0;
}

 在此之後,我們在辨認一下下面分別都是宣告型別

int arr[5];//陣列,大小為5,內容為int
int* parr1[10];//陣列,大小為10,內容為int*
int(*parr2)[10];//指標,指向一個大小為10,內容為int的陣列
int(*parr3[10])[5];//對於這個,似乎就麻煩了一點
                   //我們把它分開來看,int(*)[5] 是型別,指元素為5個元素指標陣列
                   // parr3[10]就是它的名稱
            //換種寫法就是  
//typedef int(*foo)[5];
//foo parr3[10];

而且,在這有一個問題:我們可以這樣寫嗎? parr2 = &parr1; 

我們再來看一下:

parr1裡放的是int*,而parr2裡放的是int,兩者型別不一樣,所以當然不可以這樣寫

要是parr2這樣寫便可以了 int*(*parr2)[10] 

 

三.陣列傳參與指標傳參

接下來就是傳參了,

要是隻在主函式裡這樣改改去去,那多沒意義;我們要做的就是寫一個函式,在函式裡進行改變。

1.一維陣列傳參

C 語言中,當一維陣列作為函式引數的時候,編譯器總是把它解析成一個指向其首元素首地址的指標。

注意陣列傳參主函式裡要傳陣列名

函式裡可以寫成指標的形式也可以寫為陣列的形式,注意[ ]裡可以不寫大小,

實際傳遞的陣列大小與函式形參指定的陣列大小沒有關係。

例:

#include <stdio.h>

void test(int arr[])//√
{}
void test(int arr[10])//√
{}
void test(int* arr)//√
{}
void test2(int* arr[20])//√
{}
void test2(int** arr)//√
{}
int main()
{

    int arr[10] = { 0 };

    int* arr2[20] = { 0 };

    test(arr);

    test2(arr2);
}

 

 

2.二維陣列傳參

C 語言中,當一維陣列作為函式引數的時候,編譯器總是把它解析成一個指向其首元素首地址的指標。

這條規則並不是遞迴的,也就是說只有一維陣列才是如此,當陣列超過一維時,將第一維改寫為指向陣列首元素首地址的指標之後,後面的維再也不可改寫。

比如:a[3][4][5]作為引數時可以被改寫為(*p)[4][5]。二維陣列傳參是要注意函式形參的設計只能省略第一個[]的數字。

void test(int arr[3][5])//√
{}
void test(int arr[][])//×
{}
void test(int arr[][5])//√
{}
//總結:二維陣列傳參,函式形參的設計只能省略第一個[]的數字。
//因為對一個二維陣列,可以不知道有多少行,但是必須知道一行多少元素。
//這樣才方便運算。

void test(int* arr)//×
{}
void test(int* arr[5])//×
{}
void test(int(*arr)[5])//√
{}
void test(int** arr)//×
{}
int main()
{

    int arr[3][5] = { 0 };

    test(arr);
}

 

3.一級指標傳參

一級指標傳參主要是用來傳陣列首元素的地址,以及需要改變的值(如之前提到的swap()交換倆整數的值)

所以我們遇到函式時就要想想它都可以傳什麼值進去

void test1(int* p)
{}
//test1函式能接收什麼引數?
void test2(char* p)
{}
//test2函式能接收什麼引數?

 

4.二級指標傳參

 同樣二級指標也是如此;

平常寫函式時就要想想這種函式除了可以傳我現在需要的變數型別,還可以傳什麼型別的變數,如此下去我們對於傳參的理解肯定會愈來愈高!

四.函式指標及函式指標陣列

1.函式指標

1.函式指標的定義

正所謂函式指標:那就是指向函式的指標變數

我們先來看一個函式指標長什麼樣:

char* (*fun1)(char* p1, char* p2);

fun是它的名字

char* 是它所指向函式的返回型別

(char* p1, char* p2)是它所指向的函式引數

 

2.函式指標的使用

現在我們知道了它長什麼樣子,那我們現在來使用一下它

void print()
{
    printf("hehe\n");
}

int main()
{
    void(*p)() = &print;//p是一個函式指標,所指向的函式無參無返回值

    (*p)();

    return 0;
}

我們發現,螢幕上出現了 hehe ,這就是它的一個基本使用

當然在這  void(*p)() = &print; 賦值的時候,可以不必寫&號;

這是因為函式名被編譯之後其實就是一個地址,所以這裡兩種用法沒有本質的差別。

在這看一個例子:

void test()
{
    printf("hehe\n");
}

int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

我們發現他倆列印之後的結果相同;

同樣,在函式呼叫的時候, (*p)(); 也可以不用寫 * ;

(你見過誰呼叫函式的時候還帶個 * )

即我們在寫的時候,這兩種其實都可以

void print()
{
    printf("hehe\n");
}

int main()
{
    void(*p)() = &print;
    void(*ch)() = print;

    p();
    (*ch)();

    return 0;
}

接下來我們再來看一個函式指標:

(*(void(*) ())0)()

乍一看,好複雜,我們來仔細分析一下

 

 

 這樣是不是就清楚了許多呢

我們再來看一個:

void(*signal(int, void(*)(int)))(int);

我們再來分析一下:

這是一個函式宣告

宣告的函式叫signal,signal函式有2個引數,第一個引數型別是int,  第二個引數型別是一個函式指標,

該函式指標指向的函式引數是int,返回型別是void

signal函式的返回來型別是一個函式指標,該函式指標指向的函式引數是int,返回型別是void

我們也可以把上面那個函式宣告這樣寫:

typedef void(*pfun_t)(int);//型別為指向一個引數為int,無返回值的函式指標
pfun_t signal(int, pfun_t);//用上述型別,聲明瞭一個函式

這樣是不是明瞭了許多

2.函式指標陣列

函式指標陣列那然是儲存函式指標的陣列了啊

我們來看一個例子:

int Add(int x, int y)
{
    return x + y;
}

int Sub(int x, int y)
{
    return x - y;
}

int main()
{

    //函式指標的陣列
    int (*pf1)(int, int) = Add;
    int (*pf2)(int, int) = Sub;
    int (*pf)(int, int);//函式指標

    int(* pfA[4])(int, int);//函式指標的陣列

    //函式指標陣列

    //pfArr2就是函式指標陣列,陣列的型別為 int(*)(int,int)
    int (* pfArr[2])(int, int) = { Add, Sub };

    return 0;
}

在此之上,我們再來回憶一下用多分支寫出的一個簡易計算器

//計算器 - 加、減、乘、除
void menu()
{
    printf("****************************\n");
    printf("**** 1. add   2. sub    ****\n");
    printf("**** 3. mul   4. div    ****\n");
    printf("**** 0. exit            ****\n");
    printf("****************************\n");
}

int Add(int x, int y)
{
    return x + y;
}

int Sub(int x, int y)
{
    return x - y;
}

int Mul(int x, int y)
{
    return x * y;
}

int Div(int x, int y)
{
    return x / y;
}

//函式傳參-函式指標
//回撥函式

void calc(int (*p)(int, int))
{
    int x = 0;
    int y = 0;
    int ret = 0;
    printf("請輸入2個運算元:>");
    scanf("%d%d", &x, &y);
    ret = p(x, y);
    printf("ret = %d\n", ret);
}

int main()
{
    int input = 0;

    do
    {
        menu();
        printf("請選擇:>");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            calc(Add);//計算器
            break;
        case 2:
            calc(Sub);//計算器
            break;
        case 3:
            calc(Mul);
            break;
        case 4:
            calc(Div);
            break;
        case 0:
            printf("退出計算器\n");
            break;
        default:
            printf("選擇錯誤\n");
            break;
        }
    } while (input);

    return 0;
}

我們會發現,在switch下出現了許多贅餘的語句,我們來用函式指標來改寫一下:

void menu()
{
    printf("****************************\n");
    printf("**** 1. add   2. sub    ****\n");
    printf("**** 3. mul   4. div    ****\n");
    printf("**** 0. exit            ****\n");
    printf("****************************\n");
}


int Add(int x, int y)
{
    return x + y;
}

int Sub(int x, int y)
{
    return x - y;
}

int Mul(int x, int y)
{
    return x * y;
}

int Div(int x, int y)
{
    return x / y;
}

int main()
{
    int input = 0;
    int x = 0;
    int y = 0;
    int ret = 0;
    //函式指標陣列 - 轉移表
    int (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };

    do
    {
        menu();
        printf("請選擇:>");
        scanf("%d", &input);//1
        if (0 == input)
        {
            printf("退出程式\n");
            break;
        }
        else if (input>=1 && input<=4)
        {
            printf("請輸入2個運算元:>");
            scanf("%d%d", &x, &y);
            ret = pfArr[input](x, y);
            printf("ret = %d\n", ret);
        }
        else
        {
            printf("選擇錯誤\n");
        }
    } while (input);

    return 0;
}

這樣是不是簡潔了不少呢

3.指向函式指標陣列的指標

指向函式指標陣列的指標,說白了,它就是個指標,所指向的內容為函式指標陣列

int main()
{
    //函式指標
    int(*p)(int, int);
    //函式指標的陣列,陣列元素型別為 int(*)(int, int)
    int(*pArr[4])(int, int);
    //ppArr是指向函式指標陣列的指標
    int(*(*ppArr)[4])(int, int) = &pArr;

    return 0;
}

這裡給一個使用

//這裡的函式作用都是講傳遞的字串給打印出來
char* fun1(char* p)
{
    printf("%s\n", p);
    return p;
}

char* fun2(char* p)
{
    printf("%s\n", p);
    return p;
}

char* fun3(char* p)
{
    printf("%s\n", p);
    return p;
}
int main()
{
    char* (*a[3])(char* p);//定義一個函式指標陣列a,陣列元素型別為char(*)(char*p)
    char* (*(*pf)[3])(char* p);//定義一個指向函式指標陣列的指標,所指向的陣列型別為 char*(*)(char*p)

    pf = &a;//將a的地址賦予pf

    //分別賦值
    a[0] = fun1;
    a[1] = &fun2;
    a[2] = &fun3;

    //分別使用
    (*pf)[0]("fun1");
    pf[0][1]("fun2");
    pf[0][2]("fun3");
    return 0;
}

注:

  函式指標不允許 ± 運算

五.回撥函式

1.回撥函式

  回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一
個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。回撥函式不是由該
函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或
條件進行響應。

 

如果學習過python裝飾器的同學,或許對這個概念有點熟悉

我們先用python來寫一個裝飾器:

import time
def timing_func(f):
    def wrapper():
        start = time.time()
        f()
        stop = time.time()
        return (stop - start)
    return wrapper


def fun1():
    print("lalala")
    time.sleep(1)
fun1=timing_func(fun1)

@timing_func
def fun2():
    print("hehehe")
    time.sleep(1)

print(fun1())
print(fun2())

 

 

 

接下來我們再看 c 的回撥函式的一個例子

#include<stdio.h>
#include<time.h>
#include <Windows.h>

void print()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", i);
        Sleep(100);
    }
    printf("\n");
}

void print_time(void (*fun)())
{
    SYSTEMTIME tm;

    GetLocalTime(&tm);
    printf("函式開始前的時間為:\n%d-%d-%d %d:%d:%d:%d\n", tm.wYear, tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond, tm.wMilliseconds);

    printf("\n函式執行中……\n");
    fun();

    GetLocalTime(&tm);
    printf("\n函式結束後的時間為:\n%d-%d-%d %d:%d:%d:%d\n", tm.wYear, tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond, tm.wMilliseconds);
}

int main()
{
    print_time(print);

    return 0;
}

 

 

 

 

2.qsort

  qsort便是一個用到了回撥函式的庫函式

首先qsort是一個用來排序的函式:

我們來看看人家的宣告:

 

 

 最後的 int(*compar)(const void*, const void*)便是一個我們使用時,需要傳遞的函式指標

 它的返回值為int ,引數為兩個void*  ,使用我們設定函式是,要和它的型別一致

 接下來我們繼續來看看它各個引數的含義:

 

 

 以及要注意的一點:

   使用時要包含 stdlib.h  這個標頭檔案 

 接下來看他的一個使用:

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

int values[] = { 40, 10, 100, 90, 20, 25 };

int compare(const void* a, const void* b)
{
    return (*(int*)a - *(int*)b);
}

int main()
{
    int n;
    qsort(values, 6, sizeof(int), compare);
    for (n = 0; n < 6; n++)
        printf("%d ", values[n]);
    return 0;
}

在這個例子中,我們要注意的就是在compare所指向的函式中,我們要將兩個引數進行強制型別轉換為我們要排序的型別

接下來就舉幾個例子:

#include<string.h>

struct stu  //假定一個結構體,來寫它各成員型別的排序函式
{
    int num;
    char name[20];
    int score;
};

int int_compare(const void* _1, const void* _2)//進行 int 型的比較
{
    return (*(int*)_1) - (*(int*)_2);
}

int char_compare(const void* _1, const void* _2)// 進行char型的比較
{
    return (*(char*)_1) - (*(char*)_2);
}

//進行我所自定義結構體各成員元素的排序
int stu_cmp_num(const void* _1, const void* _2)//按照  num 來排序
{
    return ((struct stu*)_1)->num - ((struct stu*)_2)->num;
}

int stu_cmp_score(const void* _1, const void* _2) //按照 score 來排序
{
    return ((struct stu*)_1)->score - ((struct stu*)_2)->score;
}

int stu_cmp_name(const void* _1, const void* _2) // 按照 name 來排序
{
    return strcmp(((struct stu*)_1)->name, ((struct stu*)_2)->name);
}

 

六.例題講解

接下來就到了我們的例題環節了,我們再來複習一下剛剛學過的東西

struct Test
{
    int Num;
    char* pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}*p;
//假設p 的值為0x100000。 如下表表示式的值分別為多少? int main() { printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; }

 在這用到了結構體記憶體對齊的知識,詳見:C語言之結構體記憶體的對齊

簡單得知該結構體在32位機器上的大小為20個位元組

首先p 是一個結構體指標 p+1 = 0x100000+20 = 0x100014

(unsigned long)p + 0x1 = 0x100000 + 1 = 0x100001

(unsigned int*)p + 0x1 = 0x100000 + 4 = 0x100004

 

2.

#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int* p;
    p = a[0];
    printf("%d", p[0]);
    return 0;
}

 這要注意的就是陣列元素為逗號表示式,逗號表示式的值為最後一個值 

所以陣列初始化後的值為 {1,3,5,0,0,0}

又因為p指向的是a[0]的地址,也就是a[0]這一行的首元素地址

所以p[0]最後的值為 1

3.

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}

 這裡要注意的就是p所指向的是一個數組大小為4的整形陣列

而我們的arr[5][5]是一行為5個整形元素的陣列,共五行

又因為陣列在記憶體中是順序儲存的

所以畫出圖:

 

 

 

 所以最後的結果為4

4.

#include <stdio.h>
int main()
{
  char *a[] = {"work","at","alibaba"};
  char**pa = a; 
  pa++;
  printf("%s\n", *pa);
  return 0;
}

 首先a裡面分別存著三串字串首字母的地址,現在pa指向了a,然後pa+1就指向了a首元素的下一位,也就是at中的a的地址

所以最後打印出來是at

int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int* ptr1 = (int*)(&aa + 1);
    int* ptr2 = (int*)(*(aa + 1));
    printf("%d,%d",*(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

這裡的&aa是直接跳過了一整個陣列,而aa+1是跳過了一行也就是5個元素

所以最後的結果為 10,5

int main()
{
    char* c[] = { "ENTER","NEW","POINT","FIRST" };
    char** cp[] = { c + 3,c + 2,c + 1,c };
    char*** cpp = cp;
    
    printf("%s\n", **++cpp);
    printf("%s\n", *-- * ++cpp + 3);
    printf("%s\n", *cpp[-2] + 3);
    printf("%s\n", cpp[-1][-1] + 1);

    return 0;
}

 對於這道題就顯得複雜了一些

c  裡面存著,E、N、P、R的地址

cp 與 c 反了過來,存的是R、P、N、R的地址

cpp 指向了 cp ,cpp裡存的是cp的首地址,即R的地址

解析如圖:注意前置++是先加後用

 

 

 

 所以,最後的結果為:

 

 

 

 

 

 

 

|---------------------------------------------------

 到此,對於指標的講解便結束了!

若有錯誤之處,還望指正!

&n