C語言高階篇 - 1.C語言複雜表示式和指標高階應用
1.指標陣列與陣列指標
1.1、字面意思來理解指標陣列與陣列指標
(1)指標陣列的實質是一個數組,這個陣列中儲存的內容全部是指標變數。
(2)陣列指標的實質是一個指標,這個指標指向的是一個數組。
1.2、分析指標陣列與陣列指標的表示式
(1)int *p[5]; int (*p)[5]; int *(p[5]);
(2)一般規律:int *p;(p是一個指標); int p[5];(p是一個數組)
總結:我們在定義一個符號時,關鍵在於:首先要搞清楚你定義的符號是誰(第一步:找核心);其次再來看誰跟核心最近、誰跟核心結合(第二步:找結合);以後繼續向外擴充套件(第三步:繼續向外結合直到整個符號完)。
(3)如何核心和*結合,表示核心是指標;如果核心和[]結合,表示核心是陣列;如果核心和()結合,表示核心是函式。
(4)用一般規律來分析3個符號:
第一個:int *p[5]; 核心是p,p是一個數組,陣列有5個元素大,陣列中的元素都是指標,指標指向的元素型別是int型別的;整個符號是一個指標陣列。
第二個,int (*p)[5];
核心是p,p是一個指標,指標指向一個數組,陣列有5個元素,陣列中存的元素是int型別; 總結一下整個符號的意義就是陣列指標。
第三個,int *(p[5]);
解析方法和結論和第一個相同,()在這裡是可有可無的。
注意:符號的優先順序到底有什麼用?其實是決定當2個符號一起作用的時候決定哪個符號先運算,哪個符號後運算。
遇到優先順序問題怎麼辦?第一,查優先順序表;第二,自己記住(全部記住都成神了,人只要記住[] . ->這幾個優先順序比較好即可)。
1.3、總結1:優先順序和結合性是分析符號意義的關鍵
(1)在分析C語言問題時不要胡亂去猜測規律,不要總覺得c語言無從捉摸,從已知的規律出發按照既定的規則去做即可。
1.4、總結2:學會逐層剝離的分析方法
(1)找到核心後從內到外逐層的進行結合,結合之後可以把已經結合的部分當成一個整體,再去和整體外面的繼續進行結合。
1.5、總結3:基礎理論和原則是關鍵,沒有無緣無故的規則
2.函式指標與typedef
2.1、函式指標的實質(還是指標變數)
(1)函式指標的實質還是指標,還是指標變數。本身佔4位元組(在32位系統中,所有的指標都是4位元組)
(2)函式指標、陣列指標、普通指標之間並沒有本質區別,區別在於指標指向的東西是個什麼玩意。
(3)函式的實質是一段程式碼,這一段程式碼在記憶體中是連續分佈的(一個函式的大括號括起來的所有語句將來編譯出來生成的可執行程式是連續的),所以對於函式來說很關鍵的就是函式中的第一句程式碼的地址,這個地址就是所謂的函式地址,在C語言中用函式名這個符號來表示。
(4)結合函式的實質,函式指標其實就是一個普通變數,這個普通變數的型別是函式指標變數型別,它的值就是某個函式的地址(也就是它的函式名這個符號在編譯器中對應的值)
2.2、函式指標的書寫和分析方法
(1)C語言本身是強型別語言(每一個變數都有自己的變數型別),編譯器可以幫我們做嚴格的型別檢查。
(2)所有的指標變數型別其實本質都是一樣的,但是為什麼在C語言中要去區分它們,寫法不一樣呢(譬如int型別指標就寫作int *p; 陣列指標就寫作int (*p)[5],函式指標就得寫得更復雜)
(3)假設我們有個函式是:void func(void); 對應的函式指標:void (*p)(void); 型別是:void (*)(void);
(4)函式名和陣列名最大的區別就是:函式名做右值時加不加&效果和意義都是一樣的;但是陣列名做右值時加不加&意義就不一樣。
(5)寫一個複雜的函式指標的例項:譬如函式是strcpy函式(char *strcpy(char *dest, const char *src);),對應的函式指標是:char *(*pFunc)(char *dest, const char *src);
2.3、typedef關鍵字的用法
(1)typedef是C語言中一個關鍵字,作用是用來定義(或者叫重新命名型別)
(2)C語言中的型別一共有2種:一種是編譯器定義的原生型別(基礎資料型別,如int、double之類的);第二種是使用者自定義型別,不是語言自帶的是程式設計師自己定義的(譬如陣列型別、結構體型別、函式型別·····)。
(3)我們今天講的陣列指標、指標陣列、函式指標等都屬於使用者自定義型別。
(4)有時候自定義型別太長了,用起來不方便,所以用typedef給它重新命名一個短點的名字。
(5)注意:typedef是給型別重新命名,也就是說typedef加工出來的都是型別,而不是變數。
2.4、總結:函式指標的分析方法也是源於優先順序與逐層剝離的基本理論
#include <stdio.h>
#include <string.h>
void func1(void)
{
printf("I am func1.\n");
}
// 這句重新命名了一種型別,這個新型別名字叫pType,型別是:char* (*)(char *, const char *);
typedef char* (*pType)(char *, const char *);
// 函式指標陣列
typedef char* (*pType[5])(char *, const char *);
// 函式指標陣列指標
typedef char* (*(*pType)[5])(char *, const char *);
int main(void)
{
char* (*p1)(char *, const char *);
char* (*p2)(char *, const char *);
pType p3; // 等效於 char* (*p3)(char *, const char *);
pType p4;
p3 = p1;
/*
char a[5] = {0};
char* (*pFunc)(char *, const char *);
pFunc = strcpy;
pFunc(a, "abc");
printf("a = %s.\n", a);
*/
/*
void (*pFunc)(void);
//pFunc = func1; // 左邊是一個函式指標變數,右邊是一個函式名
pFunc = &func1; // &func1和func1做右值時是一模一樣的,沒任何區別
pFunc(); // 用函式指標來解引用以呼叫該函式
*/
/*
int *p;
int a[5];
p = a; // 般配的,型別匹配的,所以編譯器不會警告不會報錯。
//p = &a; // 型別不匹配,p是int *, &a是int (*)[5];
int (*p1)[5] ;
p1 = &a; // p1型別是int (*)[5],&a的型別也是int (*)[5]
*/
return 0;
}
3.函式指標實戰1
3.1、用函式指標呼叫執行函式
(1)最簡單的函式指標來呼叫函式的示例,在上節課中已經演示過了。
(2)本節演示的是用函式指標指向不同的函式來實現同一個呼叫執行不同的結果。
(3)如果學過C++或者Java或者C#等面向物件的語言,就會知道面向物件三大特徵中有一個多型。多型就是同一個執行實際結果不一樣,跟我們這裡看到的現象其實是一樣的。
(4)剛才的除錯過程,可以得到很多資訊:
第一:當程式出現段錯誤時,第一步先定位段錯誤。定位的方法就是在可疑處加列印資訊,從而鎖定導致段錯誤的語句,然後集中分析這句為什麼會段錯誤。
第二:linux中命令列預設是行緩衝的,意思就是說當我們程式printf輸出的時候,linux不會一個字一個字的輸出我們的內容,而是將其緩衝起來放在緩衝區等一行準備完了再一次性把一行全部輸出出來(為了效率)。linux判斷一行有沒有完的依據就是換行符'\n'(windows中換行符是\r\n, linux中是\n,iOS中是\r)。也就是說你printf再多,只要沒有遇到\n(或者程式終止,或者緩衝區滿)都不會輸出而會不斷緩衝,這時候你是看不到內容輸出的。因此,在每個printf列印語句(尤其是用來做除錯的printf語句)後面一定要加\n,否則可能導致誤判。
第三:關於在linux命令列下用scanf寫互動性程式碼的問題,想說以下幾點:
1. 命令列下的互動程式純粹是用來學習程式設計用的,幾乎沒有實踐意義,別浪費時間了。
2. scanf是和系統的標準輸入打交道,printf和標準輸出打交道。要完全搞清楚這些東西得把標準輸入標準輸出搞清楚。
3. 我們使用者在輸入內容時結尾都會以\n結尾,但是程式中scanf的時候都不會去接收最後的\n,導致這個回車符還存留在標準輸入中。下次再scanf時就會先被拿出來,這就導致你真正想拿的那個數反而沒機會拿,導致錯誤。
#include <stdio.h>
int add(int a, int b);
int sub(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
// 定義了一個型別pFunc,這個函式指標型別指向一種特定引數列表和返回值的函式
typedef int (*pFunc)(int, int);
int main(void)
{
pFunc p1 = NULL;
char c = 0;
int a = 0, b = 0, result = 0;
printf("請輸入要操作的2個整數:\n");
scanf("%d %d", &a, &b);
printf("請輸入操作型別:+ | - | * | /\n");
do
{
scanf("%c", &c);
}while (c == '\n');
// 加一句除錯
//printf("a = %d, b = %d, c = %d.\n", a, b, c);
switch (c)
{
case '+':
p1 = add; break;
case '-':
p1 = sub; break;
case '*':
p1 = multiply; break;
case '/':
p1 = divide; break;
default:
p1 = NULL; break;
}
result = p1(a, b);
printf("%d %c %d = %d.\n", a, c, b, result);
return 0;
}
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int divide(int a, int b)
{
return a / b;
}
4.函式指標實戰2
主題:結構體內嵌函式指標實現分層
(1)程式為什麼要分層?因為複雜程式東西太多一個人搞不定,需要更多人協同工作,於是乎就要分工。要分工先分層,分層之後各個層次由不同的人完成,然後再彼此呼叫組合共同工作。
#ifndef __CAL_H__
#define __CAL_H__
typedef int (*pFunc)(int, int);
// 結構體是用來做計算器的,計算器工作時需要計算原材料
struct cal_t
{
int a;
int b;
pFunc p;
};
// 函式原型宣告
int calculator(const struct cal_t *p);
(2)本程式要完成一個計算器,我們設計了2個層次:上層是framework.c,實現應用程式框架;下層是cal.c,實現計算器。實際工作時cal.c是直接完成工作的,但是cal.c中的關鍵部分是呼叫的framework.c中的函式來完成的。
(3)先寫framework.c,由一個人來完成。這個人在framework.c中需要完成計算器的業務邏輯,並且把相應的介面寫在對應的標頭檔案中發出來,將來別的層次的人用這個標頭檔案來協同工作。
#include "cal.h"
// framework.c中應該寫實際業務關聯的程式碼
// 計算器函式
int calculator(const struct cal_t *p)
{
return p->p(p->a, p->b);
}
(4)另一個人來完成cal.c,實現具體的計算器;這個人需要framework層的工作人員提供標頭檔案來工作(但是不需要framework.c)
#include "cal.h"
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int divide(int a, int b)
{
return a / b;
}
int main(void)
{
int ret = 0;
struct cal_t myCal;
myCal.a = 12;
myCal.b = 4;
myCal.p = divide;
ret = calculator(&myCal);
printf("ret = %d.\n", ret);
return 0;
}
(5)總結:
第一:本節和上節實際完成的是同一個習題,但是採用了不同的程式架構。
第二:對於簡單問題來說,上節的不分層反而容易理解,反而簡單;本節的分層程式碼不好理解,看起來有點把簡單問題複雜化的意思。原因在於我們這個問題本身確實是簡單問題,而簡單問題就應該用簡單方法處理。我們為什麼明知錯誤還要這樣做?目的是向大家演示這種分層的寫程式碼的思路和方法。
第三:分層寫程式碼的思路是:有多個層次結合來完成任務,每個層次專注各自不同的領域和任務;不同層次之間用標頭檔案來互動。
第四:分層之後上層為下層提供服務,上層寫的程式碼是為了在下層中被呼叫。
第五:上層注重業務邏輯,與我們最終的目標相直接關聯,而沒有具體幹活的函式。
第六:下層注重實際幹活的函式,注重為上層填充變數,並且將變數傳遞給上層中的函式(其實就是呼叫上層提供的介面函式)來完成任務。
第七:下層程式碼中其實核心是一個結構體變數(譬如本例中的struct cal_t),寫下層程式碼的邏輯其實很簡單:第一步先定義結構體變數;第二步填充結構體變數;第三步呼叫上層寫好的介面函式,把結構體變數傳給它既可。
5.再論typedef
5.1、C語言的2種類型:內建型別與使用者自定義型別
(1)內建型別ADT、自定義型別UDT
5.2、typedef定義(或者叫重新命名)型別而不是變數
(1)型別是一個數據模板,變數是一個實在的資料。型別是不佔記憶體的,而變數是佔記憶體的。
(2)面向物件的語言中:型別就是類class,變數就是物件。
5.3、typedef與#define巨集的區別
typedef char *pChar;
#define pChar char *
5.4、typedef與結構體
(1)結構體在使用時都是先定義結構體型別,再用結構體型別去定義變數。
(2)C語言語法規定,結構體型別使用時必須是struct 結構體型別名 結構體變數名;這樣的方式來定義變數。
(3)使用typedef一次定義2個型別,分別是結構體變數型別,和結構體變數指標型別。
5.5、typedef與const
(1)typedef int *PINT; const PINT p2; 相當於是int *const p2;
(2)typedef int *PINT; PINT const p2; 相當於是int *const p2;
(3)如果確實想得到const int *p;這種效果,只能typedef const int *CPINT; CPINT p1;
5.6、使用typedef的重要意義(2個:簡化型別、創造平臺無關型別)
(1)簡化型別的描述。
char *(*)(char *, char *); typedef char *(*pFunc)(char *, char *);
(2)很多程式設計體系下,人們傾向於不使用int、double等C語言內建型別,因為這些型別本身和平臺是相關的(譬如int在16位機器上是16位的,在32位機器上就是32位的)。為了解決這個問題,很多程式使用自定義的中間型別來做緩衝。譬如linux核心中大量使用了這種技術.
核心中先定義:typedef int size_t; 然後在特定的編碼需要下用size_t來替代int(譬如可能還有typedef int len_t)
(3)STM32的庫中全部使用了自定義型別,譬如typedef volatile unsigned int vu32;
6.二重指標
6.1、二重指標與普通一重指標的區別
(1)本質上來說,二重指標和一重指標的本質都是指標變數,指標變數的本質就是變數。
(2)一重指標變數和二重指標變數本身都佔4位元組記憶體空間,
6.2、二重指標的本質
(1)二重指標本質上也是指標變數,和普通指標的差別就是它指向的變數型別必須是個一重指標。二重指標其實也是一種資料型別,編譯器在編譯時會根據二重指標的資料型別來做靜態型別檢查,一旦發現運算時資料型別不匹配編譯器就會報錯。
(2)C語言中如果沒有二重指標行不行?其實是可以的。一重指標完全可以做二重指標做的事情,之所以要發明二重指標(函式指標、陣列指標),就是為了讓編譯器瞭解這個指標被定義時定義它的程式設計師希望這個指標被用來指向什麼東西(定義指標時用資料型別來標記,譬如int *p,就表示p要指向int型資料),編譯器知道指標型別之後可以幫我們做靜態型別檢查。編譯器的這種靜態型別檢查可以輔助程式設計師發現一些隱含性的程式設計錯誤,這是C語言給程式設計師提供的一種編譯時的查錯機制。
(3)為什麼C語言需要發明二重指標?原因和發明函式指標、陣列指標、結構體指標等一樣的。
6.3、二重指標的用法
(1)二重指標指向一重指標的地址
(2)二重指標指向指標陣列的
(3)實踐程式設計中二重指標用的比較少,大部分時候就是和指標陣列糾結起來用的。
(4)實踐程式設計中有時在函式傳參時為了通過函式內部改變外部的一個指標變數,會傳這個指標變數的地址(也就是二重指標)進去
6.4、二重指標與陣列指標
(1)二重指標、陣列指標、結構體指標、一重指標、普通變數的本質都是相同的,都是變數。
(2)所有的指標變數本質都是相同的,都是4個位元組,都是用來指向別的東西的,不同型別的指標變數只是可以指向的(編譯器允許你指向的)變數型別不同。
(3)二重指標就是:指標陣列指標
#include <stdio.h>
void func(int **p)
{
*p = (int *)0x12345678;
}
int main(void)
{
int a = 4;
int *p = &a; // p指向a
printf("p = %p.\n", p); // p打印出來就是a的記憶體地址
func(&p); // 在func內部將p指向了別的地方
printf("p = %p.\n", p); // p已經不指向a了,所以打印出來不是a的地址
*p = 23; // 因為此時p指向0x12345678,但是這個地址是不
// 允許訪問的,因此會段錯誤。
/*
int *p1[5];
int *p2;
int **p3;
//p2 = p1;
p3 = p1; // p1是指標陣列名,本質上是陣列名,陣列名做右值表示陣列首元素
// 首地址。陣列的元素就是int *型別,所以p1做右值就表示一個int *
// 型別變數的地址,所以p1就是一個int型別變數的指標的指標,所以
// 它就是一個二重指標int **;
*/
/*
char a;
char **p1; // 二重指標
char *p2; // 一重指標
printf("sizeof(p1) = %d.\n", sizeof(p1));
printf("sizeof(p2) = %d.\n", sizeof(p2));
p2 = &a;
//p1 = &a; // p1是char **型別,&a是char *型別。
// char **型別就是指標指向的變數是char *型別
// char *型別表示指標指向的變數是char型別。
p1 = &p2; // p2本身是char *型別,再取地址變成char **型別,和p1相容。
*/
return 0;
}
7.二維陣列
7.1、二維陣列的記憶體映像
(1)一維陣列在記憶體中是連續分佈的多個記憶體單元組成的,而二維陣列在記憶體中也是連續分佈的多個記憶體單元組成的。
(1)從記憶體角度來看,一維陣列和二維陣列沒有本質差別。
(2)二維陣列int a[2][5]和一維陣列int b[10]其實沒有任何本質差別。我們可以把兩者的同一單元的對應關係寫下來。
a[0][0] a[0][1] a[0][4] a[1][0] a[1][1] a[1][4]
b[0] b[1] b[4] b[5] b[6] b[9]
(3)既然二維陣列都可以用一維陣列來表示,那二維陣列存在的意義和價值在哪裡?明確告訴大家:二維陣列a和一維陣列b在記憶體使用效率、訪問效率上是完全一樣的(或者說差異是忽略不計的)。在某種情況下用二維陣列而不用一維陣列,原因在於二維陣列好理解、程式碼好寫、利於組織。
(4)總結:我們使用二維陣列(C語言提供二維陣列),並不是必須,而是一種簡化程式設計的方式。想一下,一維陣列的出現其實也不是必然的,也是為了簡化程式設計。
7.2、哪個是第一維哪個是第二維?
(1)二維陣列int a[2][5]中,2是第一維,5是第二維。
(2)結合記憶體映像來理解二維陣列的第一維和第二維的意義。首先第一維是最外面一層的陣列,所以int a[2][5]這個陣列有2個元素;其中每一個元素又是一個含有5個元素的陣列(這個陣列就是第二維)。
(3)總結:二維陣列的第一維是最外部的那一層,第一維本身是個陣列,這個陣列中儲存的元素也是個陣列;二維陣列的第二維是裡面的那一層,第二維本身是個陣列,陣列中存的元素是普通元素,第二維這個陣列本身作為元素儲存在第一維的陣列中。
#include <stdio.h>
int main(void)
{
int a[2][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}};
//int a[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("a[1][3] = %d.\n", a[1][3]);
printf("a[1][3] = %d.\n", *(*(a+1)+3));
return 0;
}
7.3、二維陣列的下標式訪問和指標式訪問
(1)回顧:一維陣列的兩種訪問方式。以int b[10]為例, int *p = b;。
b[0] 等同於 *(p+0); b[9] 等同於 *(p+9); b[i] 等同於 *(p+i)
(2)二維陣列的兩種訪問方式:以int a[2][5]為例,(合適型別的)p = a;
a[0][0]等同於*(*(p+0)+0); a[i][j]等同於 *(*(p+i)+j)
7.4、二維陣列的應用和更多維陣列
(1)最簡單情況,有10個學生成績要統計;如果這10個學生沒有差別的一組,就用b[10];如果這10個學生天然就分為2組,每組5個,就適合用int a[2][5]來管理。
(2)最常用情況:一維陣列用來表示直線,二維陣列用來描述平面。數學上,用平面直角座標系來比擬二維陣列就很好理解了。
(3)三維陣列和三維座標系來比擬理解。三維陣列其實就是立體空間。
(4)四維陣列也是可以存在的,但是數學上有意義,現在空間中沒有對應(因為人類生存的宇宙是三維的)。
總結:一般常用最多就到二維陣列,三維陣列除了做一些特殊與數學運算有關的之外基本用不到。(四軸飛行器中運算飛行器角度、姿態時就要用到三維陣列)
8.二維陣列的運算和指標
4.4.8.1、指標指向二維陣列的陣列名
(1)二維陣列的陣列名錶示二維陣列的第一維陣列中首元素(也就是第二維的陣列)的首地址
(2)二維陣列的陣列名a等同於&a[0],這個和一維陣列的符號含義是相符的。
(3)用陣列指標來指向二維陣列的陣列名是型別匹配的。
8.2、指標指向二維陣列的第一維
(1)用int *p來指向二維陣列的第一維a[i]
8.3、指標指向二維陣列的第二維
(1)二維陣列的第二維元素其實就是普通變量了(a[1][1]其實就是int型別的7),已經不能用指標型別和它相互賦值了。
(2)除非int *p = &a[i][j];,類似於指標指向二維陣列的第一維。
#include <stdio.h>
int main(void)
{
int a[2][5] = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}};
//int a[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printf("a[1][3] = %d.\n", a[1][3]);
printf("a[1][3] = %d.\n", *(*(a+1)+3));
//int *p1 = a; // 型別不匹配
//int **p2 = a; // 型別不匹配
// 指標指向二維陣列的陣列名
int (*p3)[5]; // 陣列指標,指標指向一個數組,陣列有5個int型別元素
p3 = a; // a是二維陣列的陣列名,作為右值表示二維陣列第一維的陣列
// 的首元素首地址,等同於&a[0]
p3 = &a[0];
printf("a[0][3] = %d.\n", *(*(p3+0)+3));
printf("a[1][4] = %d.\n", *(*(p3+1)+4));
// 指標指向二維陣列的第一維
//int *p4 = &a[0]; // 不可以
int *p4 = a[0]; // a[0]表示二維陣列的第一維的第一個元素,相當於是
// 第二維的整體陣列的陣列名。陣列名又表示陣列首元素
// 首地址,因此a[0]等同於&a[0][0];
int *p5 = &a[0][0];
printf("a[0][4] = %d.\n", *(p4+4));
int *p6 = a[1];
printf("a[1][1] = %d.\n", *(p6+1));
// 指向二維陣列的第二維
return 0;
}
總結:二維陣列和指標的糾葛,關鍵就是2點:
1、陣列中各個符號的含義。
2、陣列的指標式訪問,尤其是二維陣列的指標式訪問。