快速學習C語言一: Hello World
估計不會寫C語言的同學也都聽過C語言,從頭開始快速學一下吧,以後肯定能用的上。 如果使用過其它類C的語言,如JAVA,C#等,學C的語法應該挺快的。
先快速學習並練習一些基本的語言要素,基本類型,表達式,函數,循環結構, 基本字符串操作, 基本指針操作,動態分配內存,使用結構表示復雜數據, 使用函數指針實現靈活邏輯。
雖然C是一個規模很小的語言,但也得自己多設計一些練習練手才能學會。
基本類型
我就記得char, int, 別的都不常用吧應該,用的時候再搜索。
表達式
和JAVA, C#差不多吧,不用學基本,各種算數運算符,關系運算符,邏輯運算符,逗號, 括號等的意思應該也差不多,表達式最終的結果也有類型和值。
函數
函數是最基本的抽象,基本沒有什麼語言沒有函數的概念,它封裝一系列操作, 最簡單的Hello world,如下。
static void hello_world(){
printf("hello, world\n");
}
我們的練習都是隨手寫的函數,不需要被外部調用,所以前面加個static,表示只在 本文件內可見。
printf輸出一行的話,最後要加\n, 常見個格式化參數有%d,%c,%s,%p等,分別表示 輸出int, char, 字符串, 指針。
分支,循環結構
和別的語言差不多,不過i的聲明要放在函數開頭,c89就是這樣。
static void n_hello_world(int n){
int i = 0;
for (i = 0; i < n; i++) {
printf("hello, world\n");
}
}
字符串練習,獲取一個字符串的長度
庫函數strlen就是干這個的,不過我們自己可以寫一個練手,c沒有字符串類型, 用'\0'結尾的字符數組表示字符串,所以for循環從頭滾到'\0'位置就好了。
復制代碼
// 字符串練習, 計算字符串長度
static int w_strlen(const char* str){
int i;
// 向後滾動指針,同時遞增i,直到找到字符串結尾
for (i = 0; *str != '\0'; str++, i++) {
;
}
return i;
}
復制代碼
const 修飾符表示這個參數不能在函數裡進行更改,防止意外改動。char *就是傳說中 字符串了。 寫C程序得用好for語句,有各種慣用法,用好了可以寫出很緊湊的程序,比如上面for語句 的第2個分號後的逗號表達式可以遞增兩個變量。
理解字符串的存儲
第一種方式是在編譯時分配的內存,是字符串常量,指針s1指向的內存不能更改。 第二種方式應該是在棧上分配的內存(不確定),可以通過指針修改其中的字符。
復制代碼
static void change_str_test(){
// 常量不能修改
// char* s1 = "hello"; // will core dump
char s1[10] = "hello";
*s1 = 'p';
printf("%s\n", s1);
}
復制代碼
指針練習
指針可以進行加減的操作,每加一次就滾動過它指向的類型長度, 比如char指針就是 滾動1個字節。
復制代碼
// 指針練習, 反轉字符串
static char* reverse(char* str){
char* ret = str;
// 滾到字符數組末尾的\0之前
char* p = str + w_strlen(str) - 1;
char c;
// 兩個指針,一個從前往後滾,一個從後往前滾,直到就要交錯之前
// 滾動的過程中交換兩個指針指向的字符
for ( ; p > str; --p, ++str) {
printf("debug[reverse]: %p %p %c %c\n", p, str, *p, *str);
c = *p;
*p = *str;
*str = c;
}
return ret;
}
復制代碼
c = *p表示取出指針p指向的字符,賦值給變量c,*表示取值。
*p = *str相當於p[i] = str[i],右邊的取出來的是值,左邊的取出來的也是值, 值賦值給值,看起來有些詭異,但就是這樣寫的。反正p = *str肯定不對,因為p是 指針類型,*str是計算結果是字符類型。
動態分配內存
我記得TCPL前幾章都沒講malloc,free等內存分配的函數,好多任務只需要在編譯階段 分配內存就夠了,但比較大型復雜的程序應該都需要動態管理一些內存的。
C語言沒有GC,要手工釋放動態分配的內存,否則就會造成內存洩漏,所以一定要配平 資源,有malloc的地方,一定要想好它應該在哪裡free。
目前我了解到的原則就有兩種:
誰分配,誰釋放
誰使用,誰釋放
對了, malloc出來的內存要強轉成你需要的指針類型,然後free時指針要滾到你動態 分配內存的起始點。
復制代碼
// 內存申請相關,連接兩個字符串
static void concat_test(){
char* a = "hello";
char* b = "world";
//結果字符串長度為兩個字符竄長度加\0的位置
int len = w_strlen(a) + w_strlen(b) + 1;
// 動態分配內存
char* p = (char *)malloc(sizeof(char) * len);
char* result;
// 必須判斷是否分配到內存
if (p != NULL){
// 保存動態分配內存的開始指針,free時必須從這裡free
result = p;
//滾動p和a,直到a的末尾
while (*a != '\0') {
printf("debug[concat_test]:while a %p %c\n", a, *a);
*p++ = *a++;
}
//滾動p和b,直到b的末尾
while (*b != '\0') {
printf("debug[concat_test]:while b %p %c\n", a, *a);
*p++ = *b++;
}
// 末尾整個0
*p= '\0';
printf("concat_test: %s\n", result);
//釋放動態分配的內存
free(result);
}else{
printf("malloc error");
}
}
復制代碼
結構練習
C沒有類,要表達復雜的數據,就得用結構了, 結構也可以用指針來指,如果是結構變量 的話,引用成員用.,如果是指向結構的指針,引用成員用->
別的好像沒啥特別的,注意動態分配結構數組後,指針滾動的邊界,別使用了界外的 內存。如果結構的成員指向的內存是動態分配的花,也記得free。
沒有結構,估計寫不出大程序,結構應該會用的很多。
復制代碼
//結構練習,人員統計系統
struct customer {
char* name;
int age;
};
static void customer_manager() {
// 直接在棧上分配結構體
struct customer wawa;
struct customer* p_wawa;
struct customer* p_customers;
int n = 2;
char name[] = "wawa";
// char* name = "wawa"; //splint warning
char name2[] = "tiancai";
// 直接用結構名訪問成員
wawa.name = name;
wawa.age = 30;
printf("%s is %d years old\n", wawa.name, wawa.age);
// 用指針訪問結構成員
p_wawa = &wawa;
p_wawa->age = 31;
printf("%s is %d years old\n", wawa.name, wawa.age);
// 為員工數組動態分配內存
p_customers = (struct customer*)malloc(sizeof(struct customer) * n);
if (p_customers != NULL) {
// 設置數組第一項
p_customers->name = name;
p_customers->age = 10;
// 設置數組第二項
p_customers++;
p_customers->name = name2;
p_customers->age = 30;
// 滾動數組外面,然後反向循環到數組開始
p_customers++;
while(n-- > 0){
p_customers--;
printf("%s is %d years old\n", p_customers->name, p_customers->age);
}
// 釋放動態分配的內存,這時候p_customers已經位於起始位置了
// 結構體裡的name1, name2是在棧上分配的,不用釋放
free(p_customers);
}
}
復制代碼
函數指針練習
好多語言都有高階函數的特性,比如函數的參數或返回值還可以是個函數, C裡也有函數指針可以達到類似的效果,用來做回調函數等。
但C的函數指針寫起來比較詭異,不好記憶,不行就用typedef來重新命個名,寫起來 簡單一些。
下面用一個比較經典的冒泡排序來演示函數指針的使用,傳遞不同的比較函數可以 改變排序函數的行為,這是寫復雜靈活邏輯的一種很方便的方式。
復制代碼
// 函數指針練習, 排序
// 正序排序的比較函數
static int cmp_default(int a, int b){
return a - b;
}
// 反序排序的比較函數
static int cmp_reverse(int a, int b){
return b - a;
}
// int類型的冒泡排序算法,可傳入一個比較函數指針
// 類似回調函數,該函數需要兩個int參數且返回int
static void sort(int* arr, int n, int (*cmp)(int, int)){
int i, j, t;
int *p, *q;
p = arr;
for (i = 0; i < n; i++, p++) {
q = p;
for (j = i; j < n; j++, q++) {
// 調用函數指針指向的函數和使用函數一樣,貌似是簡單寫法
if (cmp(*p, *q) > 0) {
t = *p;
*p = *q;
*q = t;
}
}
}
}
// 測試排序函數
static void sort_test(){
int arr[] = {4, 5, 3, 1, 2};
int i, n = 5;
// 正向排序, 傳入cmp_default函數的地址,貌似不需要&取地址
sort(arr, 5, cmp_default);
for (i = 0; i < n; i ++) {
printf("%d%s", arr[i], i == n - 1 ? "" : ", ");
}
printf("\n");
//反向排序,同上
sort(arr, 5, cmp_reverse);
for (i = 0; i < n; i ++) {
printf("%d%s", arr[i], i == n - 1 ? "" : ", ");
}
printf("\n");
}