本節知識:
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- static int j=0;
- int k;
- void fun1()
- {
- j=0;
- j++;
- printf("fun1 %d\n",j);
- }
- void fun2()
- {
- static int i=0;
- //i=0;
- printf("fun2 %d\n",i);
- i++;
- }
- for(k=0;k<10;k++)
- {
- fun1();
- fun2();
- }
- return 1;
- }
- #include <stdio.h>
- #include <stdlib.h>
- void fun(int b[100])
- {
- printf("sizeof(b) is %d\n",sizeof(b));
- }
- int main(void)
- {
- int *p=NULL;
- int a[100];
- int b[100];
- printf("sizeof(p) is %d\n",sizeof(p));
- printf("sizeof(*p) is %d\n",sizeof(*p));
- printf("sizeof(a[100]) is %d\n",sizeof(a[100]));
- printf("sizeof(a) is %d\n",sizeof(a));
- printf("sizeof(&a) is %d\n",sizeof(&a));
- printf("sizeof(&a[0] is %d\n",sizeof(&a[0]));
- fun(b);
- return 1;
- }
a.對於bool類型的比較:FLASE都是0 TRUE不一定是1 所以應該用if(bool_num); if(!bool_num);
- cosnt int* func()
- {
- static int count = 0;
- count++;
- return &count;
- }
h.在看const修飾誰,誰不變的問題上,可以把類型去掉再看,代碼如下:
- struct student
- {
- }*str;
- const str stu3;
- str const stu4;
str是一個類型 ,所以在去掉類型的時候,應該都變成const stu3和const stu4了,所以說應該是stu4和stu3這個指針不能被賦值。
12.關鍵字volatile:
volatile搞嵌入式的,一定都特別屬性這個關鍵字,記得第一使用這個關鍵字的時候是在韋東山老師的,Arm裸機視頻的時候。volatile是告訴編譯不要對這個變量進行任何優化,直接在內存中進行取值。一般用在對寄存器進行賦值的時候,或修飾可能被多個線程訪問的變量。
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- int a[5]={1,2,3,4,5};
- int *p=(int *)(&a+1); //數組指針 加一 進行正常的指針運算 走到數
- 組尾
- int *d=(int *)((int)a+1);//地址加一 不是指針運算
- //printf("%x\n",*((char *)((int)a+1)-1));
- /*因爲是小端存儲 高地址 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 低地址*/
- /*變成了 0x02 0x00 0x00 0x00 */
- printf("%x,%x",p[-1],*d); /* 第二個值就是這麼存儲的0x02 0x00 0x00 0x00 低地址處 所以就是2000000*/
- int a=0x11223344;
- char *p=(char *)((int)&a);
- printf("%x\n%x\n",*(p+0),p+0);
- printf("%x\n%x\n",*(p+1),p+1);
- return 0;
- }
- #include <stdio.h>
- #include <stdlib.h>
- union
- {
- int i;
- char a[2];
- }*p,u;
- int main(void)
- {
- p=&u;
- p->i=0x3839;
- printf("%x\n",p->i);
- printf("a0p=%x,a1p=%x\n",&(p->a[0]),&(p->a[1]));
- printf("a0=%x,a1=%x\n",p->a[0],p->a[1]);
- return 0;
- }
枚舉enum其實就是int類型,用來保存枚舉常量的。enum枚舉類型,這個纔是真正的常量,定義常量一般用enum 。#define是宏定義是在預編譯期間單純的替換。#define宏定義無法調試,枚舉常量是可以調試的。#define宏定義是無類型信息的,枚舉類型是有類型信息的常量,是int型的。
a.typedef用於給一個已經存在的數據類型重新命名。
- typedef unsigned int int32;
typedef char* PCHAR; PCHAR p1,p2; //p1和p2都是 char*型
e.有一個知識點忘記了,嘿嘿,程序如下:
- typedef struct student
- {
- }str,*str1;
str1 abc; 就是定義一個struct student *類型
str abc; 就是定義一個struct student 類型
- 程序一:
- for(i=0; i<m; i++)
- {
- for(j=0; j<n; j++)
- {
- for(k=0; k<p; k++)
- {
- c[i][j] = a[i][k] * b[k][j];
- }
- }
- }
- 程序二:
- for(i=0; i<m; i++)
- {
- for(k=0; k<p; k++)
- {
- for(j=0; j<n; j++)
- {
- c[i][j] = a[i][k] * b[k][j];
- }
- }
- }
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- char a[1000];
- int i;
- for(i=0; i<1000; i++)
- {
- a[i] = (-1-i);
- }
- while(a[i])
- {
- printf("%d\n",a[i]);
- i++;
- }
- printf("%d\n",strlen(a));
- return 0;
- }
本節遺留問題:
本節接觸了,C語言中的三大蛋疼:符號優先級 ++i順序點 貪心法 (其實這裏面好多都是跟編譯器有關的,而且有好多問題都是可以通過良好的編程習慣避免的)
本節知識點:
1.註釋問題:
註釋不能把關鍵字弄斷,如:in/*註釋*/t
註釋不是簡單的剔除,而是使用空格替換
編譯器認爲雙引號括起來的內容都是字符串,雙斜槓也不例外。如:char *p = "heh//jfeafe" //不起註釋作用
2.接續符:
接續符\ ,常用於宏定義中
- #define SWAP(a,b) \
- { \
- int temp = a; \
- a = b; \
- b = temp; \
- }
反斜槓同時有接續符和轉義符兩個用途,當接續符使用的時候,可以直接在程序中出現。當轉義符使用的時候,必須是出現在字符串中。
接續符,也用與接續一個關鍵字,代碼如下, 注意: 但是直接連接\兩邊不能有空格。
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- cha\
- r a = 12;
- return 0;
- }
3.邏輯運算符:有一個短路規則
4.最容易忘記規則的兩個運算符:
三目運算符:(a?b:c) 當a的值爲真的時候 返回b的值,否則返回c的值
逗號表達式:a,b 表達式的值爲b的值
5.位運算:
對於左移和右移<< >>問題 :無符號的,和有符號左移,都是補0 ,對於有符號的在右移動的時候,正數補零,負數補什麼跟編譯器有關係。並且左移和右移的大小不能大於數據的長度,也不能小於0。
交換兩個數,有一種不借助中間變量的方法,就是異或,代碼如下:
- #include <stdio.h>
- #define SWAP1(a,b) \
- { \
- int temp = a; \
- a = b; \
- b = temp; \
- }
- #define SWAP2(a,b) \
- { \
- a = a + b; \
- b = a - b; \
- a = a - b; \
- }
- #define SWAP3(a,b) \
- { \
- a = a ^ b; \
- b = a ^ b; \
- a = a ^ b; \
- }
- int main()
- {
- int a = 1;
- int b = 2;
- SWAP1(a,b);
- SWAP2(a,b);
- SWAP3(a,b);
- return 0;
- }
6.i++,i--順序點:
只有 i++ i--纔有順序點 就是什麼時候開始加,什麼時候開始減。真心對於順序點 是搞不懂啊~~~ (++i)+(++i)+(++i) ,在gcc中是5+5+6(DEV C++) ,在vc中是6+6+6(vc++6.0) ,不同編譯器順序點不一樣。這個例子的順序點 在; 前。
a=((++i),(++i),(++i)) 它的順序點在每個逗號前面完成計算。我覺得特殊的順序點 是可以通過合理的順序佈局來避免的。
7.貪心法:
每一個符號應該儘可能多的包含字符
8.符號運算優先級問題:
個人覺得優先級不用記,好好的寫括號吧~~~
給一個易錯優先級表,如圖:
9.c語言中的類型轉換:
c語言中有兩種轉換類型,分別是:隱式轉換和顯示轉換(強制類型轉換)
隱式轉換的規則:
a.算術運算中,低類型轉換爲高類型
b.賦值運算中,表達式的類型轉換爲左邊變量的類型
c.函數調用時,實參轉換成形參的類型
d.函數返回值,return表達式轉換爲返回值的類型
隱式轉換的例子,代碼如下:
- #include <stdio.h>
- int main()
- {
- int i = -2;
- unsigned int j = 1;
- if( (i + j) >= 0 )
- {
- printf("i+j>=0\n");
- }
- else
- {
- printf("i+j<0\n");
- }
- printf("i+j=%d\n", i + j);
- return 0;
- }
注意:在使用C語言的時候,應該特別注意數據的類型是否相同,儘量避免隱式轉換帶來的不必要的麻煩~~~
本節知識點:
1.編譯過程的簡介:
預編譯:
a.處理所有的註釋,以空格代替。
b.將所以#define刪除,並展開所有的宏定義,字符串替換。
c.處理條件編譯指令#if,#ifdef,#elif,#else,#endif
d.處理#include,並展開被包含的文件,把頭文件中的聲明,全部拷貝到文件中。
e.保留編譯器需要使用的#pragma指令、
怎麼樣觀察這些變化呢?最好的方法就是在GCC中,輸入預處理指令,可以看看不同文件經過預處理後變成什麼樣了,預處理指令:gcc -E file.c -o file.i 注意:-C -E一起使用是預編譯的時候保留註釋。
編譯:
a.對預處理文件進行一系列詞法分析,語法分析和語義分析
詞法分析:主要分析關鍵字,標示符,立即數等是否合法
語法分析:主要分析表達式是否遵循語法規則
語義分析:在語法分析的基礎上進一步分析表達式是否合法
b.分析結束後進行代碼優化生成相應的彙編代碼文件 編譯指令:gcc -S file.c -o file.s
彙編:
彙編器將彙編代碼轉變爲機器可以執行的指令,每個彙編語句幾乎都對應一條機器指令,其實機器指令就是機器碼,就是2進制碼。彙編指令:gcc -c file.c -o file.o 注意:-c是編譯彙編不連接。
鏈接:
再把產生的.o文件,進行鏈接就可以生成可執行文件。連接指令:gcc file.o file1.o -o file 這句指令是鏈接file.o和file1.o兩個編譯並彙編的文件,並生成可執行文件file。
鏈接分兩種:靜態鏈接和動態鏈接,靜態鏈接是在編譯器完成的,動態鏈接是在運行期完成的。靜態鏈接的指令是:gcc -static file.c -o file對於一些沒有動態庫的嵌入式系統,這是常用的。
一般要想通過一條指令生成可執行文件的指令是: gcc file.c -o file
資料:這裏面說到了很多關於gcc的使用的問題,我提供一個gcc的學習資料,個人覺得還不錯,也不長,就是一個txt文檔,很全面。資源下載地址http://download.csdn.net/detail/qq418674358/6041183 Ps:嘿嘿,設了一個下載積分,因爲真的是沒分用了!希望大家見諒哈!
2.c語言中的預處理指令:#define、#undef(撤銷已定義過的宏名)、#include、#if、#else、#elif、#endif、#ifdef、#ifndef、#line、#error、#pragma。還有一些ANSI標準C定義的宏:__LINE__、__FILE__、__DATA__、__TIME__、__STDC__。這樣使用printf("%s\n",__TIME__); printf(__DATE__);
一個#undef的例子:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define X 2
- #define Y X*2
- #undef X
- #define X 3
- int main()
- {
- printf("%d\n",Y);
- return 0;
- }
這個輸出的是6,說明了#undef的作用
3.宏定義字符串的時候:應該是 #define HELLO "hello world" 記住是雙引號。還有就是一切宏都是不能有分號的,這個一定要切忌!!!
4.宏與函數的比較:
a.宏表達式在預編譯期被處理,編譯器不知道有宏表達式存在
b.宏表達式沒有任何的"調用"開銷
c.宏表達式中不能出現遞歸定義
5.爲什麼不在頭文件中定義全局變量:
如果一個全局變量,想要在兩個文件中,同時使用,那這兩個文件中都應該#include這個頭文件,這樣的話就會出現重複定義的問題。其實是重名的問題,因爲#include是分別在兩個文件中展開的,試想一下,如果在兩個文件中的開始部分,都寫上int a = 10; 是不是也會報錯。可能你會說那個#ifndef不是防止重複定義嗎?是的 ,那是防止在同一個文件中,同時出現兩次這個頭文件。現在是兩個文件中,所以都要展開的。全局變量就重名了!!!所以 對於全局變量,最好是定義在.c文件中,不要定義在頭文件中。
6.#pargma pack 設置字符對齊,看後面一節專門寫字符對齊問題的!!!
7.#運算符(轉換成字符串):
假如你希望在字符串中包含宏參數,那我們就用#號,它把語言符號轉換成字符串。
#define SQR(x) printf("the "#x"lait %d\n",((x)*(x)));
SQR(8)
輸出結果是:the 8 lait 64 這個#號必須使用在帶參宏中
有個小例子:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- /*在字符串中 加入宏參用的*/
- #define SCAN(N,String) scanf("%"#N"s",String); //N是截取的個數 String是存儲的字符串
- int main()
- {
- char dd[256];
- SCAN(3,dd) //記得沒有分號哈 自定義 任意格式輸入的scanf 截取輸入的前三個
- printf("%s\n",dd);
- return 1;
- }
8.##運算符(粘合劑)
一般用於粘貼兩個東西,一般是用作在給變量或函數命名的時候使用。如#define XNAME(n) x##n
XNAME(8)爲8n 這個##號可以使用在帶參宏或無參宏中
下面是一個##運算符的小例子,代碼如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define BL1 bb##ll##1
- #define BL(N) bbll##N
- int main()
- {
- int BL1=10;
- int BL(4)=15;
- printf("%d\n",bbll1);
- printf("%d\n",bbll4);
- return 1;
- }
注意:#號和##號都必須只能在宏定義中使用,不能使用在其他地方
9.其實預編譯這塊還有一些,不常用到的預編譯指令,也是盲點,但是不難理解,用到的時候查查就好。比如說#line、#error、#warning等。
很多人都覺得內存對齊這個問題很難,很不好算,總算錯,其實我想說只要你畫一畫就沒那麼難了。好了,進入正題。
本節知識點:
1.結構體爲什麼要內存對齊(也叫字節對齊):
其實我們都知道,結構體只是一些數據的集合,它本身什麼都沒有。我們所謂的結構體地址,其實就是結構體第一個元素的地址。這樣,如果結構體各個元素之間不存在內存對齊問題,他們都挨着排放的。對於32位機,32位編譯器(這是目前常見的環境,其他環境也會有內存對齊問題),就很可能操作一個問題,就是當你想要去訪問結構體中的一個數據的時候,需要你操作兩次數據總線,因爲這個數據卡在中間,如圖:
在上圖中,對於第2個short數據進行訪問的時候,在32位機器上就要操作兩次數據總線。這樣會非常影響數據讀寫的效率,所以就引入了內存對齊的問題。
另外一層不太重要的原因是:某些硬件平臺只能從規定的地址處取某些特定類型的數據,否則會拋出硬件異常。
2.內存對齊的規則:
a.第一個成員起始於0偏移處
b.每個成員按其類型大小和指定對齊參數n中較小的一個進行對齊
c.結構體總長度必須爲所有對齊參數的整數倍
d.對於數組,可以拆開看做n個數組元素
3.來幾個小例子,畫畫圖,有助於理解:
第一個例子,代碼如下:
- #include <stdio.h>
- struct _tag_str1
- {
- char a;
- int b;
- short c;
- }str1;
- struct _tag_str2
- {
- char a;
- short c;
- int b;
- }str2;
- int main()
- {
- printf("sizeof str1 %d\n",sizeof(str1));
- printf("sizeof str2 %d\n",sizeof(str2));
- return 0;
- }
看圖很自然就知道了str1爲12個字節,str2爲8個字節。
第二個例子,上面的那個例子有好多問題還沒有考慮到,比如說上面的那個例子在8字節對齊,和4字節對齊的情況都是一樣的。結構體中嵌套結構體的內存對齊怎麼算,所以就有了這個例子,代碼如下:
- #include <stdio.h>
- #pragma pack(8)
- //#pragma pack(4)
- struct S1
- {
- short a;
- long b;
- };
- struct S2
- {
- char c;
- struct S1 d;
- double e;
- };
- #pragma pack()
- int main()
- {
- struct S2 s2;
- printf("%d\n", sizeof(struct S1));
- printf("%d\n", sizeof(struct S2));
- printf("%d\n", (int)&(s2.d) - (int)&(s2.c));
- return 0;
- }
在4字節對齊的情況中,有一個問題值得注意:就是圖中畫1的地方。這裏面本應short是可以上去的。但是對於結構體中的結構體一定要十分警惕,S1是一體的,short已經由於long進行了內存對齊,後面還空了兩個字節的內存,其實此時的short已經變成了4個字節了!!!即結構體不可拆,不管是多少字節對齊,他們都是一體的。所有的圈都變成了叉。所以說結構體只能往前篡位置,不能改變整體。
我們在分析一些8字節對齊的情況,如圖:
同樣,到這裏又有一個字節對齊的原則要好好重申一下:就是以什麼爲對齊參數,首先我們要知道編譯器或者自己定義的是多少字節對齊的,這個數爲n。然後我們要看這個結構體中的各個數據類型,找到所佔字節數最大的類型,爲m。如果n大於m,就以m爲對齊參數,比如說一個4字節對齊的結構體中都是short,那這個結構體以什麼爲對齊參數,當然是2了,如果m大於n,就以n爲對齊參數,比如說在4字節對齊的情況下的double類型。
以上就是我對內存對齊的小總結,最最想要說明的就是兩大段紅色的部分。
1.int a=9,b=10,d=9;是可以的。
2.%*d ,在scanf中使用的時候,是1整數但不賦給任何變量,有個小代碼:
- #include <stdio.h>
- #include <malloc.h>
- int main()
- {
- int a=23,b=5,c=9;
- scanf("%*d%d%d",&a,&b,&c);
- printf("%d,%d,%d",a,b,c);
- return 0;
- }
a的值,你是賦值不進去的,僅僅佔位用的。
3.對於冒泡排序,怎麼在不完全執行完循環前就預先判斷,已經排序結束了:
在一次內層循環的時候,一次都沒有進行數據交換,就說名冒泡排序已經排序ok了。
4.不要總記得scanf,同樣還存在getchar()和gets()函數,gets能接收含有空格的字符串,這個是scanf不能做到的。
scanf("%ls",a); //接收有效字符串的第一個字符
scanf("%ns",a); //這個是格式化輸入,接收字符串的從頭開始的n個字符
其實我想說,scanf函數真心沒有什麼用,很不好的一個函數。
5.堆區分配內存是從兩頭開始增長的,不是單向增長的。
6.typedef int [10] 其實[10]就是int了,個人覺得這個代碼風格,很不好,千萬不能寫成這樣,可讀性很差!
7.要記住函數在傳遞參數的時候,其實是數據的拷貝,直接對形參進行改變或者賦值,是毫無意義的,實參是不會改變的。對於指針也是一樣的。只有通過指針,取得了當前這個指針指向的內容的時候,改變了這個內容,這樣實參纔會被改變。因爲是直接改變了內存地址中保存的數值。
舉個例子就是:在數據結構那節中的鏈表,creat函數就是一個典型的例子。仔細想想爲什麼不能在main函數中定義一個頭結點,再把這個頭結點的地址傳給creat函數呢?一定要通過creat返回一個頭結點指針呢?再想想,爲什麼在想通過形參獲得子函數中數據的時候,一定要傳入地址或者指針呢?然後再把想要獲得數據,寫入這個地址或者指針中去?
給一段代碼,幫助理解這個問題:
- #include <stdio.h>
- #include <malloc.h>
- typedef struct _tag_str
- {
- int a;
- int b;
- }str;
- void fun(str* str1)
- {
- str1 = (str* )malloc(sizeof(str));
- str1->a = 12;
- str1->b = 34;
- }
- int main()
- {
- /*str* strp;
- fun(strp);
- printf("%d\n",strp->a);
- printf("%d\n",strp->b);*/
- str str1;
- fun(&str1);
- printf("%d\n",str1.a);
- printf("%d\n",str1.b);
- }
想想,爲什麼子函數中賦值,在main中打印出來是不一樣的!!!
對於fun(strp)的過程是這樣的:在函數傳遞參數的時候,strp的值 賦值給了子函數的str1,這個過程就是函數參數拷貝的過程,然後str1的值在malloc的時候不幸被malloc改變了,所以在main中打印出來的不一樣。
對於fun(&str1)的過程是這樣的:在函數傳遞參數的時候,&str1的值 賦值給了子函數的str1,後面的過程跟上面一樣。所以在main中打印的也是不一樣的。
對於這種情況,最好的解決辦法就是利用函數返回值,把str1返回 回來就ok了!!!
注意:可能你會問了,那怎樣通過參數獲得子函數傳遞的值啊,其實很簡單,你在main中開闢好一段內存,然後把這個內存地址傳遞到子函數中去,然後對這個內存進行賦值,不要去改變這個指針的指向(即指針的值),僅僅改變指針指向的內存(即指針指向的內容),自然就獲得了你想要的值!
8.c語言文件操作的一個問題:
c語言中打開文件有兩種方式,一種是二進制方式,另一種是文本方式(ASCII碼方式)。這兩種方式有什麼區別?(對於Linux這種只有一種文件類型的操作系統來說是沒有區別的)
我們就以windows爲例說說區別:
a.以文本方式打開文件,若將數據寫入文件,如果遇到換行符'\n'(ASII 值爲10,0A),則會轉換爲回車—換行'\r\n'(ASCII值爲13,10,0D0A)存入到文件中,同樣讀取的時候,若遇到回車—換行,即連續的ASCII值13,10,則自動轉換爲換行符。
而以二進制方式打開文件時,不會進行這樣的處理。
b.還有如果以文本方式打開文件時,若讀取到ASCII碼爲26(^Z)的字符即0x1a,則停止對文件的讀取,會默認爲文件已結束,而以二進制方式讀取時不會發生這樣的情況。由於正常情況下我們手動編輯完成的文件是不可能出現ASCII碼爲26的字符,所以可以用feof函數去檢測文件是否結束。
所以,由於存在上面的兩個區別,我們在明確文件類型的時候,最好使用相對應的方式對文件進行打開。對於那些不明確文件類型的時候,最好使用二進制方式打開文件。
指針這一節是本書中最難的一節,尤其是二級指針和二維數組直接的關係。
本節知識點:
第二個意義是 數組名 sizeof(a) 爲整體數組有多少個字節
- #include <stdio.h>
- #include <stdlib.h>
- int main(int argc, char *argv[])
- {
- /* int a[20]={1,2,4};
- printf("%d\n",sizeof(a));
- printf("%p\n",a);
- printf("%p\n",&a);
- printf("%p\n",&a[0]);
- */
- /* int a[5]={1,2,3,4,5};
- int (*p)[5]=&a;
- printf("%d\n",*((int *)(p+1)-1));
- */
- int a[5]={1,2,3,4,5};
- int* p=(int *)(&a+1);
- // int *p=&a+1; //這個條語句是 把&a這個數組指針 進行了指針運算後 的那個地址 強制類型轉換成了 int *指針
- printf("%d\n",*(p-1));
- return 0;
- }
數組是數組,指針是指針,根本就是兩個完全不一樣的東西。當然要是在宏觀的內存角度看,那一段相同類型的連續空間,可以說的上是數組。但是你可以嘗試下,定義一個指針,在其他地方把他聲明成數組,看看編譯器會不會把兩者混爲一談,反過來也不會。
- char a[5]={'a','b','c','d','e'};
- char (*p)[3]=&a;
- #include <stdio.h>
- #include <assert.h>
- int strlen(const char* s)
- {
- return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );
- }
- int main()
- {
- printf("%d\n", strlen( NULL));
- return 0;
- }
- #include <stdio.h>
- #include <assert.h>
- char* strcpy(char* dst, const char* src)
- {
- char* ret = dst;
- assert(dst && src);
- while( (*dst++ = *src++) != '\0' );
- return ret;
- }
- int main()
- {
- char dst[20];
- printf("%s\n", strcpy(dst, "hello!"));
- return 0;
- }
f.補充問題,爲什麼對於字符串char a[256] = "hello";,在printf和scanf函數中,使用a行,使用&a也行?代碼如下:
- #include <stdio.h>
- int main()
- {
- char* p ="phello";
- char a[256] = "aworld";
- char b[25] = {'b','b','c','d'};
- char (*q)[256]=&a;
- printf("%p\n",a); //0022fe48
- //printf("%p\n",&a);
- //printf("%p\n",&a[0]);
- printf("tian %s\n",(0x22fe48));
- printf("%s\n",q); //q就是&a
- printf("%s\n",*q); //q就是a
- printf("%s\n",p);
- printf("%s\n",a);
- printf("%s\n",&a);
- printf("%s\n",&a[0]);
- printf("%s\n",b);
- printf("%s\n",&b);
- printf("%s\n",&b[0]);
- }
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int main(int argc, char *argv[])
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- printf("%d\n",sizeof(a[0]));
- printf("%d\n",*a[2]);
- printf("%d\n",*(a[0]+1));
- printf("%p\n",a[0]);
- printf("%p\n",a[1]);
- printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一樣 指針類型不一樣 &a[0]+1這個是數組指針 a[1]是&a[1][0] 是int*指針
- printf("%d\n",*((int *)(&a[0]+1)));
- printf("%d\n",*(a[1]+1));
- printf("%p\n",a);
- printf("%p\n",&a);
- printf("%p\n",&a[0]);
- printf("%d\n",sizeof(a)); //這是a當作數組名的時候
- printf("%d\n",*((int *)(a+1))); //此時 a是數組首元素的地址 數組首元素是a[0]
- //首元素地址是&a[0] 恰巧a[0]是數組名 &a[0]就變成了數組指針
- return 0;
- }
- int *q;
- q = (int *)a;
- printf("%d\n",*(q+6));
- int (*p)[3];
- p = a;
- printf("%d\n",*(*(p+1)+1));
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- int (*p)[3];
- int *q;
- printf("%d\n",*(*(a+1)+1)); //a *(&a[0]+1)
- p = a;
- q = (int *)a;
- printf("%d\n",*(*(p+1)+1));
- printf("%d\n",*(a[1]+1));
- printf("%d\n",a[1][1]);
- printf("%d\n",*(q+6));
- }
- <span style="font-family:Arial;BACKGROUND-COLOR: #ffffff"></span>
- <span style="color:#000000;">#include <stdio.h>
- int main()
- {
- int a[5][5];
- int(*p)[4];
- p = a;
- printf("%d\n", &p[4][2] - &a[4][2]);
- }</span>
- #include <stdio.h>
- #include <malloc.h>
- int reset(char**p, int size, int new_size)
- {
- int ret = 1;
- int i = 0;
- int len = 0;
- char* pt = NULL;
- char* tmp = NULL;
- char* pp = *p;
- if( (p != NULL) && (new_size > 0) )
- {
- pt = (char*)malloc(new_size);
- tmp = pt;
- len = (size < new_size) ? size : new_size;
- for(i=0; i<len; i++)
- {
- *tmp++ = *pp++;
- }
- free(*p);
- *p = pt;
- }
- else
- {
- ret = 0;
- }
- return ret;
- }
- int main()
- {
- char* p = (char*)malloc(5);
- printf("%0X\n", p);
- if( reset(&p, 5, 3) )
- {
- printf("%0X\n", p);
- }
- return 0;
- }
(2)函數中傳遞指針數組的時候,實參(指針數組)要退化成形參(二級指針)。
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int main(int argc, char *argv[])
- {
- char* p[4]={"afje","bab","ewrw"};
- char* *d=p;
- printf("%s\n",*(p+1));
- printf("%s\n",*(d+1)); //d &p[0] p[0]是"afje"的地址,所以&p[0]是保存"afje"字符串的char*指針的地址
- return 0;
- }
d.子函數malloc,主函數free,這是可以的(有兩種辦法,第一種是利用return 把malloc的地址返回。第二種是利用二級指針,傳遞一個指針的地址,然後把malloc的地址保存出來)。記住不管函數參數是,指針還是數組, 當改變了指針的指向的時候,就會出問題,因爲子函數中的指針就跟主函數的指針不一樣了,他只是一個複製品,但可以改變指針指向的內容。這個知識點可以看<在某培訓機構的聽課筆記>這篇文章。
13.數組作爲函數參數:數組作爲函數的實參的時候,往往會退化成數組元素類型的指針。如:int a[5],會退化成int* ;指針數組會退化成二級指針;二維數組會退化成一維數組指針;三維數組會退化成二維數組指針(三維數組的這個是我猜得,如果說錯了,希望大家幫我指出來,謝謝)。如圖:
二維數組作爲實參的例子:
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int fun(int (*b)[3]) //此時的b爲 &a[0]
- {
- printf("%d\n",*(*(b+1)+0));
- printf("%d\n",b[2][2]);// b[2][2] 就是 (*(*(b+2)+2))
- printf("%d\n",*(b[1]+2));
- }
- int main(int argc, char *argv[])
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- fun(a);//與下句話等價
- fun(&a[0]);
- return 0;
- }
數組當作實參的時候,會退化成指針。指針當做實參的時候,就是單純的拷貝了!
14.函數指針與指針函數:
a.對於函數名來說,它是函數的入口,其實函數的入口就是一個地址,這個函數名也就是這個地址。這一點用彙編語言的思想很容易理解。下面一段代碼說明函數名其實就是一個地址,代碼如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- void abc()
- {
- printf("hello fun\n");
- }
- int main(int argc, char *argv[])
- {
- void (*d)();
- void (*p)();
- p = abc;
- abc();
- printf("%p\n",abc);
- printf("%p\n",&abc);//函數abc的地址0x40138c
- p();
- (*p)();
- d = ((unsigned int*)0x40138c); //其實就算d= 0x40138c這麼給賦值也沒問題
- d();
- return 0;
- }
可見函數名就是一個地址,所以函數名abc與&abc沒有區別,所以p和*p也沒有區別。
b.我覺得函數指針最重要的是它的應用環境,如回調函數(其實就是利用函數指針,把函數當作參數進行傳遞)代碼如下,還有中斷處理函數(同理)詳細見<
ok6410學習筆記(16.按鍵中斷控制led)>中的 中斷註冊函數,request_irq。還有就是函數指針數組,第一次見到函數指針數組是在zigbee協議棧中。
回調函數原理代碼:
- #include <stdio.h>
- typedef int(*FUNCTION)(int);
- int g(int n, FUNCTION f)
- {
- int i = 0;
- int ret = 0;
- for(i=1; i<=n; i++)
- {
- ret += i*f(i);
- }
- return ret;
- }
- int f1(int x)
- {
- return x + 1;
- }
- int f2(int x)
- {
- return 2*x - 1;
- }
- int f3(int x)
- {
- return -x;
- }
- int main()
- {
- printf("x * f1(x): %d\n", g(3, f1));
- printf("x * f2(x): %d\n", g(3, &f2));
- printf("x * f3(x): %d\n", g(3, f3));
- }
注意:可以使用函數名f2,函數名取地址&f2都可以,但是不能有括號。
c.所謂指針函數其實真的沒什麼好說的,就是一個返回值爲指針的函數而已。
15.賦值指針的閱讀:
a.char* (*p[3])(char* d); 這是定義一個函數指針數組,一個數組,數組元素都是指針,這個指針是指向函數的,什麼樣的函數參數爲char* 返回值爲char*的函數。
分析過程:char (*p)[3] 這是一個數組指針、char* p[3] 這是一個指針數組 char* 是數組元素類型、char* p(char* d) 這個是一個函數返回值類型是char* 、char (*p)(char* d)這個是一個 函數指針。可見char* (*p[3])(char* d)是一個數組 數組中元素類型是 指向函數的指針,char* (* )(char* d) 這是函數指針類型,char* (* )(char* d) p[3] 函數指針數組 這個不好看 就放裏面了。(PS:這個看看就好了~~~當娛樂吧)
b.函數指針數組的指針:char* (*(*pf)[3])(char* p) //這個就看看吧 我覺得意義也不大 因爲這個邏輯要是一直下去 就遞歸循環了。
分析過程:char* (* )(char *p) 函數指針類型,char* (*)(char *p) (*p)[3] 函數指針 數組指針 也不好看 就放裏面了。
本節知識點:
通過上面的運行結果,可以分析得出:在同一個函數中,先定義的變量在高地址處,後定義的變量在低地址處,且他們的地址是相連的中間沒有空隙。定義的數組是下標大的在高地址處,下標小的在低地址處(由此可以推斷出malloc開闢出的推空間,也應該是下標大的在高地址處,下標小的在低地址處)。子函數中的變量,跟父函數中的變量的地址之間有很大的一塊空間,這塊空間應該是兩個函數的其他活動記錄,且父函數中變量在高地址處,子函數中的變量在低地址處。
l.最後說說數據結構中的棧,其實數據結構中的棧就是一個線性表,且這個線性表只有一個入口和出口叫做棧頂,還是LIFO(後進先出的)結構而已。
對於本節的函數內容其實就沒什麼難點了,但是對於函數這節又涉及到了順序點的問題,我覺得可以還是忽略吧。
本節知識點:
1.函數中的順序點:f(k,k++); 這樣的問題大多跟編譯器有關,不要去刻意追求。 這裏給下順序點的定義:順序點是執行過程中修改變量值的最後時刻。在程序到達順序點的時候,之前所做的一切操作都必須反應到後續的訪問中。
2.函數參數:函數的參數是存儲在這個函數的棧上面的(對於棧可以看上篇文章<內存管理的藝術>),是實參的拷貝。
3.函數的可變參數:
a.對於可變參數要包含starg.h頭文件。需要va_list變量,va_start函數,va_arg函數,va_end函數。對於其他函數沒什麼可說的,只有va_arg函數記得一定是按順序的接收。這裏有一個可變參數使用的小例子,代碼如下:
- #include <stdio.h>
- #include <stdarg.h>
- float average(char c,int n, ...)
- {
- va_list args;
- int i = 0;
- float sum = 0;
- va_start(args, n);
- for(i=0; i<n; i++)
- {
- sum += va_arg(args, int);
- }
- va_end(args);
- printf("%c\n",c);
- return sum / n;
- }
- int main()
- {
- char c = 'b';
- printf("%f\n", average(c,5, 1, 2, 3, 4, 5));
- printf("%f\n", average(c,4, 1, 2, 3, 4));
- return 0;
- }
b.可變參數的缺點:
(1).必須要從頭到尾按照順序逐個訪問。
(2).參數列表中至少要存在一個確定的命名參數。
(3).可變參數宏無法判斷實際存在的參數的數量。
(4).可變參數宏無法判斷參數的實際類型。
(5).如果函數中想調用除了可變參數以外的參數,一定要放在可變參數前面。
注意:va_arg中如果指定了錯誤的類型,那麼結果是不可預期的。
Ps:可變參數就說到這裏,可變參數最經典的應用就是printf,等分析printf實現的時候,再好好寫寫。
4.函數與宏的比較:
注意:宏有一個函數不可取替的功能,宏的參數可以是類型名,這個是函數做不到的!代碼如下:
- #include <stdio.h>
- #include <malloc.h>
- #define MALLOC(type, n) (type*)malloc(n * sizeof(type))
- int main()
- {
- int* p = MALLOC(int, 5);
- int i = 0;
- for(i=0; i<5; i++)
- {
- p[i] = i + 1;
- printf("%d\n", p[i]);
- }
- free(p);
- return 0;
- }
5.函數調用中的活動記錄問題:包含參數入棧、調用約定等問題。見上篇文章<內存管理的藝術>。
6.遞歸函數:遞歸函數有兩個組成部分,一是遞歸點(以不同參數調用自身),另一個是出口(不再遞歸的終止條件)。
對於遞歸函數要有一下幾點注意:
a.一定要有一個清晰的出口,不然遞歸就無限了。
b.儘量不要進行太多層次的遞歸,因爲遞歸是在不斷調用函數,要不斷的使用棧空間的,很容易造成棧空間溢出的,然後程序就會崩潰的。比如說:對一個已經排好序的結構進行快速排序(因爲快排需要使用遞歸,且對排好順序的結構排序是最壞情況,遞歸層數最多),就很容易造成棧空間溢出。一般不同的編譯器分配的棧空間大小是不一樣的,所以允許遞歸的層數也是不一樣的!
c.利用遞歸函數,實現不利用參數的strlen函數。代碼如下:
- /*這是自己實現 strlen*/
- /*
- #include <stdio.h>
- #include <stdlib.h>
- #include <assert.h>
- int my_strlen(const char *str)
- {
- int num=0;
- assert(NULL!=str);
- while(*str++)
- {
- num++;
- }
- return num;
- }
- int main(int argc, char *argv[])
- {
- char *a="hello world";
- printf("%d\n",my_strlen(a));
- return 0;
- }*/
- /*這是不用變量 實現strlen 使用遞歸*/
- #include <stdio.h>
- #include <stdlib.h>
- #include <assert.h>
- int my_strlen(const char *str)
- {
- assert(NULL!=str);
- return ('\0'!=*str)?(1+my_strlen(str+1)):0; //這裏之所以 是加1 不是++ 我是擔心順序點的問題
- }
- int main(int argc, char *argv[])
- {
- char *a="hello world";
- printf("%d\n",my_strlen(a));
- return 0;
- }
7.使用函數時應該注意的好習慣:
a.如果函數參數是指針,且僅作爲輸入參數用的時候,應該加上const防止指針在函數體內被以外改變,如:
- void str_copy(char *strDestination,const char *strSource);
b.在函數的入口處,應儘可能使用assert宏對指針進行有效性檢查,函數參數的有效性檢查是十分必要的。不用assert也行,if(NULL == p)也可以。
c.函數不能返回指向棧內存的指針
d.函數不僅僅要對輸入的參數,進行有效性的檢查 。還要對通過其他途徑進入函數體的數據進行有效性的檢查 ,如全局變量,文件句柄等。
e.不要在函數中使用全局變量,儘量讓函數從意義上是一個獨立的模塊
f.儘量避免編寫帶有記憶性的函數。函數的規模要小,控制在80行。函數的參數不要太多,控制在4個以內,過多就使用結構體。
g.函數名與返回值類型在語言上不可以衝突,這裏有一個經典的例子getchar,getchar的返回值是int型,會隱藏這麼一個問題:
- char c;
- c=getchar();
- if(XXX==c)
- {
- /*code*/
- }
如果XXX的值不在char的範圍之內, 那c中存儲的就是XXX的低8位 ,if就永遠不會成立。但是getchar當然不會惹這個禍了,因爲getchar獲得的值是從鍵盤中的輸入的,是滿足ASCII碼的範圍的,ASCII碼是從0~127的,是在char的範圍裏面的,就算是用char去接getchar的值也不會有問題,getchar還是相對安全的。可是對於fgetc和fgetchar就沒這麼幸運了,他們的返回值類型同樣是int,如果你還用char去接收,那文件中的一些大於127的字符,就會造成越界了,然後導致你從文件中接收的數據錯誤。這裏面就有隱藏的危險了!!!對於字符越界問題可以看看這篇文章<c語言深度解剖讀書筆記(1.關鍵字的祕密)>
8.陳正衝老師還有一個第七章是講文件的我覺得總結不多,就寫在這裏了:
a.每個頭文件和源文件的頭部 ,都應該包含文件的說明和修改記錄 。
b.需要對外公開的常量放在頭文件中 ,不需要對外公開的常量放在定義文件的頭部。
9.最終的勝利,進軍c++(唐老師的最後一課,講了些c++的知識,總結如下):
a.類與對象:
b.c++中類有三種訪問權限:
(1).public 類外部可以自由訪問
(2).protected 類自身和子類中可以訪問
(3).private 類自身中可以訪問
小例子:
- #include <stdio.h>
- struct Student
- {
- protected:
- const char* name;
- int number;
- public:
- void set(const char* n, int i)
- {
- name = n;
- number = i;
- }
- void info()
- {
- printf("Name = %s, Number = %d\n", name, number);
- }
- };
- int main()
- {
- Student s;
- s.set("Delphi", 100);
- s.info();
- return 0;
- }
注意:上面這段代碼要在c++的編譯器中進行編譯,在gcc中會報錯的,因爲c標準中是不允許struct中有函數的。
c.繼承的使用,如圖:
小例子:
- #include <stdio.h>
- struct Student
- {
- protected:
- const char* name;
- int number;
- public:
- void set(const char* n, int i)
- {
- name = n;
- number = i;
- }
- void info()
- {
- printf("Name = %s, Number = %d\n", name, number);
- }
- };
- class Master : public Student
- {
- protected:
- const char* domain;
- public:
- void setDomain(const char* d)
- {
- domain = d;
- }
- const char* getDomain()
- {
- return domain;
- }
- };
- int main()
- {
- Master s;
- s.set("Delphi", 100);
- s.setDomain("Software");
- s.info();
- printf("Domain = %s\n", s.getDomain());
- return 0;
- }
Ps:以上6篇文章終於更新完了,是我對陳正衝老師的<c語言深度解剖>一書和國嵌唐老師c語言課程的一些總結和理解,針對c語言,後面的一點c++僅僅是做個筆記而已,望大牛莫噴~~~
本節知識點:
- assert(dst && src);
- assert((NULL != dst) && (NULL != src));
3.給一個考指針運算的面試題吧:
- #include <stdio.h>
- void main()
- {
- int TestArray[5][5] = { {11,12,13,14,15},
- {16,17,18,19,20},
- {21,22,23,24,25},
- {26,27,28,29,30},
- {31,32,33,34,35}
- };
- int* p1 = (int*)(&TestArray + 1);
- int* p2 = (int*)(*(TestArray + 1) + 6);
- printf("Result: %d; %d; %d; %d; %d\n", *(*TestArray), *(*(TestArray + 1)),
- *(*(TestArray + 3) + 3), p1[-8],
- p2[4]);
- }
4.看看下面的代碼,感受下安全編程的重要性:
- #include<stdio.h>
- int main(int argc, char *argv[])
- {
- int flag = 0;
- char passwd[10];
- memset(passwd,0,sizeof(passwd));
- strcpy(passwd, argv[1]);
- if(0 == strcmp("LinuxGeek", passwd))
- {
- flag = 1;
- }
- if( flag )
- {
- printf("\n Password cracked \n");
- }
- else
- {
- printf("\n Incorrect passwd \n");
- }
- return 0;
- }
- strncpy(passwd,argv[1],9);
-
最近對c語言的總結學習可以告一段落了!覺得這種邊學邊思考邊總結的方式,還不錯,還是有一定的進步的!但是對於日後的c語言學習還遠遠沒有停止。所以寫了這篇文章來督促自己對c語言的學習,告訴自己還有很多不錯的書沒有去讀。過一段時間,再回頭看看。
1.對於c語言描述的數據結構的學習。
2.林銳老師的<高質量程序設計指南>,聽說他的<大學十年>也很不錯,有時間應該讀一讀。
3.<c和指針> <c陷阱與缺陷> <c專家編程> <c++沉思錄>
4.仔細研讀 <c primer plus>這本書,這本書中有很多細節,很多標準(c99標準)值得學習,應該好好看看!
5.還有就是找一本介紹c 表庫函數的書(像字典一樣),看看c庫函數都有什麼,c庫中有多少頭文件等。
6.還有就是一些當作補充的書籍:<C語言的科學和藝術> <你必須知道的495個c語言問題> <c語言進階:重點、難點與疑點解析> <攻破C語言筆試與機試難點V0.3>
7.最後在回頭看看,帶我最初接觸c語言的,譚浩強的<c語言程序設計>。