音影片學習之路--C語言(1)

語言: CN / TW / HK

背景

這個系列是自學Android音影片系列。

前言

C和C++作為學習音影片技術首要具備的語言基礎,所以十分必要學習和複習一下之前學習的C語言基礎。

正文

C的入門大概會分成幾章學習,由於之前在大學期間學習過C,而且後面做過簡單的JNI開發,所以這裡就簡單回顧和複習一遍。

安裝IDE

記得很久之前開發C都是用的Visual Studio,不過我看有人推薦使用Clion這個IDE,風格和Android studio一樣,簡直無縫切換,這裡直接從官網下載,然後會發現需要購買,當然這裡推薦有能力的可以購買,我這裡找到一個生成啟用碼的地方:

https://33tool.com/idea/

有需要就直接啟用即可。

CLion的風格就這樣,不得不說JetBrains出品的產品還是很nice的。

image.png

配置環境

我這裡使用windows電腦進行開發,所以需要配置一下環境,當然不用配置也是可以的,使用CLion直接run也是能編譯的,但是我們還是要簡單瞭解一下。

其中編譯c語言的編譯器叫做gcc,這裡下載gcc非常方便,可以通過Cygwin64下載,選中gcc-core、make等幾個外掛即可,然後再配置系統變數,最後在命令列介面就可以使用gcc了。

這裡IDE預設的hello world程式,在控制檯ls發現只有一個main.c檔案,這個.c也就是源程式,

image.png

呼叫gcc命令,會生成exe檔案,

image.png

再執行exe檔案,我們第一個hello world就完成了。

image.png

C語言

Hello World

看一下C語言的Hello World如何列印:

```

include

int main() { printf("Hello, World!\n"); return 0; } ```

C直接使用main()作為程式的入口,而且方法和變數型別寫前面,和Java語言類似;其中使用#include來匯入標頭檔案,也就是匯入包;

關鍵字

不論啥語言都有自己的關鍵字,這裡我們來看看C語言中的一些常用關鍵字:

C關鍵字.png

其實還是蠻容易的,迴圈、判斷都是所有語言通用的,基本資料型別C中更加區分了,和Java有所不同,其他關鍵字都可以憑字面意思理解。

資料型別

對於資料型別在Java中我們很熟悉就倆種,一個是基本資料型別,一個是引用資料型別,具體看下圖:

image.png

在Java中陣列、介面、類和null都是引用資料型別,其他是基本資料型別,在C中基本也差不多,但是有所區別,如下圖:

C資料型別.png

這裡特殊之處我覺得是C有個函式型別,這個其實就是函式指標,在C中有大作用。

printf格式控制

為什麼要說這個呢,因為Java的基本資料型別就那幾個,但是C不一樣,C裡面有算術型別,而這個算術型別還巨多,範圍也不一樣,不區分一下還是很容易搞錯,剛好C有個sizeof方法可以檢視型別所儲存的大小。

對於printf函式列印算術型別資料也是很有講究,它可以理解為按xx格式讀取xx型別的整數/小說,賦值給xx型別,比如下面程式碼:

//讀取一個十進位制的整數,賦值給int printf("l1 : %d \n",1225422554); //讀取一個十進位制的整數,賦值給short printf("l2 : %hd \n",1225422554); //讀取一個十進位制的整數,賦值給long printf("l3 : %ld \n",1225422554); 這都是讀取一個十進位制的整數,是通過%d這個d來表示,但是賦值給的型別確不一樣,其中short能儲存的最大值是3萬多,所以這個列印第二行應該不對,列印是:

image.png

會發現l2是不對,這也是符合情理的。

總結如下,以後對於列印算術型別也要小心處理。

| 格式控制符 | 說明 | | --- | --- | | %c |讀取一個單一的字元 | %hd、%d、%ld |讀取一個十進位制整數,並分別賦值給 short、int、long 型別 | %ho、%o、%lo|讀取一個八進位制整數(可帶字首也可不帶),並分別賦值給 short、int、long 型別 | %hx、%x、%lx |讀取一個十六進位制整數(可帶字首也可不帶),並分別賦值給 short、int、long 型別 | %hu、%u、%lu |讀取一個無符號整數,並分別賦值給 unsigned short、unsigned int、unsigned long 型別 | %f、%lf |讀取一個十進位制形式的小數,並分別賦值給 float、double 型別 | %e、%le |讀取一個指數形式的小數,並分別賦值給 float、double 型別 | %g、%lg |既可以讀取一個十進位制形式的小數,也可以讀取一個指數形式的小數,並分別賦值給 float、double 型別 | %s |讀取一個字串(以空白符為結束)

C中的變數定義和宣告

在C語言中的變數定義、宣告有一點不一樣,變數宣告向編譯器保證變數以指定的型別和名稱存在。

但是分2種情況:

  • 預設是建立儲存空間的,比如int a在宣告的時候就建立了儲存空間。
  • 另一種是不需要的,通過extern關鍵字宣告變數但是不定義他,比如extern int a其中a可以在別的檔案中定義。

C中定義常量

啥是常量我們就不說了,相當於Java的final變數。主要有2種方式:

  • 使用#define前處理器。
  • 使用const關鍵字。

這裡需要注意一個前處理器,一個是關鍵字,關於啥是前處理器我也不是很明白,後面再說。

```

define age 18

void sizeofFun();

int main() { const int i = 19; printf("age = %d i = %d",age,i); return 0; } ``` 其中#define是預處理方式。

儲存類

啥子是儲存類呢,這個概念在Java中是沒有的,其實很簡單就是變數的幾種修飾符,作用就是定義這個變數的範圍和生命週期。

在前面的關鍵字小節中我們說了auto和register關鍵字,分別是本地變數和可以把變數儲存在暫存器中,其實還有2種,分別是static和extern:

  • static:也就是靜態的,這個和Java的靜態變數和靜態方法是一樣的,也就是生命週期是程式的生命週期,屬於全域性變數。 -extern:這個其實就是可以理解為導包,提供一個全域性變數的引用,這個變數可以在其他檔案中定義。

說道這裡不得不說C的執行,是按檔案執行的,所以變數的順序定義是有先後順序的,比如下面程式碼:

```

include

include "support.h"

int main() { //這裡編譯不過 int sum = add(a,b); printf("sum = %d",sum); return 0; }

int a = 10; int b = 20; ``` 這裡a、b變數在main()方法之後定義,就無法使用,必須在main()方法之前:

```

include

include "support.h"

int a = 10; int b = 20;

int main() { int sum = add(a,b); printf("sum = %d",sum); return 0; } 這樣才可以,但是這個總感覺很彆扭,所以可以使用extern來解決:

include

include "support.h"

int main() { extern int a; extern int b; int sum = add(a,b); printf("sum = %d",sum); return 0; }

int a = 10; int b = 20; ``` 這裡的extern也就相當於擴充套件了作用域。

當然除了在一個檔案中使用extern,在C中extern關鍵字最多的使用是多檔案處理時,比如下面是main.c檔案: ```

include

int a = 10; int b = 20; int add();

int main() { int sum = add(); printf("sum = %d",sum); return 0; } 定義了a、b2個變數和add函式,然後在addFun.c檔案中: extern int a; extern int b;

int add(){ return a + b; } ``` 按理說這個的add方法肯定無法執行得到a和b,因為不在一個檔案中,但是這裡使用extern關鍵字可以實現。

函式

函式其實和Java中的定義是一樣的,返回值在前,函式名和引數形成函式簽名,但是這裡說一個不一樣的,就是函式宣告,在Java中你定義一個函式,必須要有方法體,除非是介面,不然是無法定義成功的,但是在C中就不一樣了,比如下面程式碼:

```

include

//宣告一個max函式 int max(int,int );

int main() { printf("max = %d",max(10,20)); return 0; } //函式實現地方 int max(int num1,int num2){ return (num1 > num2)? num1 : num2; } ``` 這種函式宣告和函式主體的定義在Java中絕對是不可能的,在C中可以這樣實現,宣告和實現可以分開。

函式引數

如果函式要使用引數,必須宣告接受引數值的變數,這些變數被稱為函式的形式引數,形參就像區域性變數,在進入函式時被建立,退出函式時銷燬,這個和其他Java語言都是一樣的,不過這裡有個呼叫型別區別,也就是傳值呼叫和引用呼叫。

函式引數呼叫.png

這裡有了指標的概念,所以可以進行引用呼叫,直接修改該地址指向的內容。

在這裡我們可以對比一下Java,在Java中所有函式都是傳值呼叫,但是還要注意一下的是Java的型別分為基本型別和引用型別,其中基本型別不用說傳遞肯定是值傳遞,但是引用型別時需要注意即使是複製也是複製的是引用,假如引數是class型別,其中欄位還有引用型別,進行拷貝的話只是淺拷貝,裡面的引用型別的欄位還是一個,所以修改形參會影響傳遞的實參。

陣列

陣列就是一段記憶體連續的內容,其實沒啥好說的,定義還有賦值啥的和Java中基本一樣,但是這裡有幾點還是不同的,這裡來梳理一下。首先是指標的思想,在Java中陣列屬於引用資料型別,所以定義一個數組變數其實就是一個指向陣列的引用,在C中陣列的陣列名其實就是指向陣列的第一個元素的指標。根據這個思想,我們可以看一下如何傳遞陣列給函式:

  • 形參是一個指標

``` void testArray(int *param){

} ``` - 形參是一個已經定義大小的陣列

``` void testArray(int param[10]){

} ```

  • 形參是一個未定義大小的陣列

``` void testArray(int param[]){

} ```

指標

對於指標來說,這個就是C的靈魂所在,其實也非常的簡單就是地址,下面是簡單概述:

指標概述.png

這裡其實也比較簡單,只需要明白特定型別的指標就是指向特定資料型別的一個地址即可。

函式返回指標

既然瞭解了指標,這裡說一個C語言的強大之處,就是它的函式返回值可以是指標型別,但是注意不能返回區域性變數的地址,除非區域性變數定義為static。

看一波下面程式碼:

```

include

include

int * getRandom(){ //這裡的static修飾 static int r[10]; srand((unsigned) time(NULL)); for (int j = 0; j < 10; ++j) { r[j] = rand(); printf("[%d] : %d \n",j,r[j]); } return r; }

int main() { int p; p = getRandom(); for (int j = 0; j < 10; j ++) { printf("(p + [%d]) : %d \n",j,*(p + j)); } return 0; } ``` 這裡返回一個數組,前面說了陣列就是指向第一個元素的指標,由於陣列是連續地址,所以在利用陣列的指標獲取其中的值時可以直接對指標++,這也是很巧妙的做法,然後看一下列印結果:

image.png

是符合的。但是仔細一想有點不對,我這個返回的地址是r這個陣列的,但是r是區域性變數,按理說區域性變數會在函式結束後就釋放的,所以這個指向的值是空的才對,其實不然,這裡使用了static修飾,假如把static修飾給去掉: ``` int * getRandom(){ //這裡的static修飾去掉 int r[10]; srand((unsigned) time(NULL)); for (int j = 0; j < 10; ++j) { r[j] = rand(); printf("[%d] : %d \n",j,r[j]); } return r; }

int main() { int p; p = getRandom(); for (int j = 0; j < 10; j ++) { printf("(p + [%d]) : %d \n",j,*(p + j)); } return 0; } ``` 列印結果是:

image.png

這就不對了,但是為什麼第一個值是對的,按理說都被釋放了,這裡都不對才是,具體原因不知。

關於為什麼C不支援返回區域性非static的變數,因為區域性變數和Java一樣結構是儲存在棧中的,方法執行完就釋放了,但是static變數是存放在靜態資料區,不會隨著函式的執行結束而清除。

其實這就涉及到了C的儲存位置,這裡先不說了,由於只熟悉Java的,就不過多擴充套件了,後面有機會再探究。

函式指標

說起這個其實很有意思,我們必須要知道一個一個函式的型別是啥,也就是函式引數以及返回值,關於函式名你想叫啥就是啥,所以這裡函式指標就是指向函式的指標,熟悉kotlin中的高階函式的話,這個就非常容易理解。

直接看下面程式碼:

``` double max1(double num1,double num2){ return (num1 > num2) ? num1 : num2; }

int main() { //定義一個函式指標,返回值是double,引數是(double,double) double (p)(double ,double ) = max1; double a,b,c,d; printf("input 3 numbers: \n"); scanf("%lf %lf %lf",&a,&b,&c); d = p(p(a,b),c); printf("max: %lf \n",d); return 0; } ``` 這裡的函式指標p其實就相當於kotlin中的p:(double,double) -> double 這種型別,很好理解。

回撥函式

在Java中我們使用回撥會立馬想起使用介面,但是比較麻煩,其實在kotlin中我們就是使用了高階函式來進行回撥的,也就是定義一個變數,它的型別是高階函式型別,在需要實現的地方對這個變數進行處理,就會回撥到被呼叫地方。

而上面所說的函式指標,其實和這玩意差不多,所以使用函式指標來實現回撥函式很簡單。

下面來看一個非常簡單的例子:

```

include

include

include

void test(int array,size_t arraySize,int (p)(void )){ for (int i = 0; i < arraySize; ++i) { //p就是函式指標 array[i] = p(); } }

int getNextValue(){ return rand(); }

int main() { int array[10]; //直接傳遞函式名,也就是函式指標 test(array,10,getNextValue); for (int i = 0; i < 10; ++i) { printf("value : %d \n",array[i]); } return 0; } ``` 列印結果如下:

image.png

完全符合預期,這裡和kotlin的區別就是C這裡傳遞函式指標也就是函式名即可,只要方法簽名相同和返回值相同就可以。

字串

說起字串這個東西,Java就方便多了,因為在C中沒有String型別,字串是一個char型別的一維陣列,不僅如此,陣列最後一個位置還是null字元‘\0’,就比如下面:

``` char ch[] = {'h','e','l','l','o','\0'}; //簡寫 char ch1[] = "hello";

int main() { printf("ch size = %d \n", sizeof(ch)); printf("ch1 size = %d", sizeof(ch1)); return 0; } ``` 這裡的長度都是6:

image.png

注意這裡獲取陣列長度是通過sizeof,sizeof函式返回的是陣列的長度,但是字串的長度計算是不帶\0的,所以字串長度是5,字串長度的api是strlen函式,下面看一下: ``` char ch[6] = {'h','e','l','l','o','\0'}; char ch1[6] = "world"; char ch2[12];

int main() { //複製 strcpy(ch2,ch1); printf("ch2 : %s \n",ch2);

//拼接
strcat(ch,ch1);
printf("ch : %s \n",ch);

//返回長度
int len = strlen(ch);
printf("ch str size : %d \n",len);
int len1 = sizeof(ch);
printf("ch size : %d",len1);

return 0;

} ``` 這裡有3個字串,其中ch通過拼接,這時的長度肯定不止6了,但是依舊可以儲存,這裡的列印是:

image.png

還有其他的字串操作API,主要也就是判斷2個字串是否相同、返回某個字元在字串中第一次出現的位置等API。

總結

其實所有語言都是很類似的,設計思路很多都是通用的,不過C的指標還是Java語言無法對比的,確實好用,這篇文章先學習到這裡,下篇文章繼續。