gdb調試
- gcc 源程序 -g;加gdb調試信息
- gdb可執行程序;(gdb調試)
- l(ist):查看源碼,按一下從main開始10行以此往后
- l n:查看n處上下10行的源碼
- run:運行程序
- b(reak)行號:加斷點
- i(nfo) b:查看當前斷點
- d(elete) 斷點序號:刪除斷點
- p(rint) 變量名:查看變量的值
- c(ontinue):程序繼續運行
- 單步運行程序:
- n(ext):往后執行一步,不進入函數內部
- s(tep):往下執行一步,進入函數內部執行
- quit:退出
變量的命名規則
- 程序中不得出現僅靠大小寫區分的相似的標識符
- 一個函數名禁止被使用于其他地方
- 所有宏定義,枚舉常量,只讀變量全用大寫字母命名,用下劃線分割
- 考錄到習慣性問題,局部變量中可采用通用的命名方式,但僅限于n,i,j 等作為循環變量使用
- 結構體被定義時必須有明確的結構體名
- 定義變量的同時,不忘初始化
- 不同類型數據之間的運算要注意精度擴展的問題,一般低精度向高精度數據擴展
- 禁止使用八進制的常數(0 除外,因為嚴格意義上來講 0 也是八進制數) 和八進制的轉義字符
- 在計算機中,任何以 0 開頭的數字都被以為是八進制格式的數(當然十六進制的0x不算)。所以當我們寫固定長度的數字時,會存在一定的風險。如 code[3] = 052;(對應十進制的42)
sizeof關鍵字
sizeof 在計算變量所占空間大小時,括號可以省略,而計算類型大小時,不能省略。且一般情況下,sizeof時在編譯時求值,所以
sizeof(i++)不會引起副作用,但是由于sizeof(i++)和sizeof(i)的結果一樣,所以沒有必要且不允許寫這樣的代碼。同樣“sizeof(i=1234)”這樣的代碼也不允許,**因為
i 的值沒有被改變,并沒有被賦值為1234**。sizeof操作符里面不要有其他運算符,否則不會達到預期的目的。在C99 中,計算柔性數組所占用空間大小時,sizeof是在運行時求值,此為特例。
★在測字符串長度時,strlen函數時計算字符串長度,并不包含字符串在最后的’\0’。
if、else組合
- bool變量與“零值”的比較:
bool bTestFlag = FALSE;
(A) if (bTestFlag == 0);
(B) if (bTestFlag == TRUE);
(C) if (bTestFlag)
(A) 容易產生歧義,會誤認為是整型變量。
(B) FALSE在編譯器里被定義為 0 ;但 TRUE 的值則不唯一,所以這種寫法不妥
(C) 既不會引起誤會,也不會由于 TRUE 或 FALSE 的不同定義值而出錯。
- float變量與“零值”進行比較
float fTestVal = 0.0
(A) if (fTestVal == 0.0);
(B) if ((fTestVal >= -EPSINON) && (fTestVal <= EPSINON));
//EPSINON為事先定義好的精度
分析:
float與double類型的數據都是有精度限制的,不能直接拿來與0.0比
EPSINON為實現定義好的精度,如果一個數落在[0.0-EPSINON, 0.0+EPSINON]這個閉區間內,我們認為在某個精度內它的值與零相等。擴展一下,把0.0替換為你想比較的任何一個浮點數,那我們就可以比較任意兩個浮點數的大小了,當然是在某個精度內
★不要在很大的浮點數和很小的浮點數之間進行運算,使用浮點數應遵循定義好的浮點數標準
五種類型的浮點異常是:無效運算、被零除、上溢、下溢和不精確
四種射入方向:向最接近的可表示的值、當有兩個最接近的可表示的值時,首選“偶數”值、向負無窮大(向下)和向正無窮大(向上)以及向0(截斷)
- 指針變量與“零值”進行比較
int* p = NULL; //定義指針一定要同時初始化,指針和數組部分會詳細講
(A) if (p == 0);
(B) if (p);
(C) if (NULL == p);
(A) 寫法:p時整型變量?容易引起誤會,盡管NULL的值和0一樣,但意義不同
(B) 寫法:p時bool型變量,容易引起誤會不好
(C) 正確
else到底與哪個if配對?
C語言中規定:else始終與同一括號內最近的未匹配的if語句結合
使用if語句的其他注意事項
規則1:先處理正常情況,再處理異常情況。
如果把執行概率更大的代碼放到后面,也就意味著if語句將進行多次無謂的比較。所以,把正常情況的處理放在if后面,而不要放在else后面規則2:確保if和else子句沒有弄反
規則3:賦值運算符不能使用再產生布爾值的表達式上。
任何被認為是具有布爾值的表達式上都不能使用賦值運算。
例如一下兩種情況:
if ((x = y) != 0)
{
foo();
}
或者:
if (x = y)
{
foo();
}規則4:所有if-else if 結構應該由else子句結束
不管何時一條fi語句后有一個或多個else if 語句都應該應用本規則;最后的else if 必須跟有一條else語句。
switch、case組合
既然有了if、else組合,為什么還需要switch、case組合
- 不要拿青龍偃月刀去削蘋果
規則1:每個case語句的結尾絕對不要忘了加break,否則將導致多個分支重疊(除非有使用多個分支重疊)
規則2:最后必須使用default分支。即使程序真的不需要default處理,也應該保留以下語句:
default:
break;
這樣做并非畫蛇添足,可以避免讓人誤以為你忘了default處理
規則3:再switch case組合中,禁止使用return語句。
規則4:switch表達式不應是有效的布爾值。例如:
switch (x == 0) // not compliant - effectively Boolean
{
...
}
- case關鍵字后面的值有什么要求么
記住: case后面只能是整型或字符型的常量或常量表達式(想想字符型數據在內存里是怎么存的)。
- case語句的排列順序
有以下幾種規則:
1. 按字母或數字順序排列各條case語句
2. 把正常情況放在前面,而把異常情況放在后面
3. 按執行頻率排列case語句
- 使用case語句的其他注意事項
有以下幾種規則:
1. 簡化每種情況對應的操作
2. 不要為了使用case語句而刻意制造一個變量
3. 將default子句只用于檢查真正的默認情況
- break與continue的區別(傳送門)
break關鍵字很重要,表示終止本層循環。當代碼執行到break時,本層循環便終止。
而continue表示終止本次(本輪)循環,當代碼執行到continue時,本輪循環終止,進入下一輪循環
- 循環語句的注重點:
建議使用以下規則:
1. 在多層循環中,如果有可能,應當將最長的循環放在內層,最短的循環放在外層,以減少CPU跨切循環層的次數。
2. 建議for語句的循環控制變量的取值采用“半開半閉區間”的寫法(左閉右開)。
3. 不能在for循環體內修改循環變量,防止循環失控。
4. 循環要盡可能短,要使代碼清晰,一目了然。
5. 把循環嵌套控制在3層以內。
6. for語句的控制表達式不能包含任何浮點類型的對象。
void關鍵字
- void a
void字面意思是“空類型”,void * 則為“空類型指針”,void * 可以指向任何類型的數據。void幾乎只有“注釋”和限制程序的作用,因為從來沒有人會定義一個void變量。
void真正發揮的作用在于:對函數返回的限定;對函數參數的限定
?任何類型的指針都可以直接賦值給void * ,無需進行強制類型轉換,但反之則不能,因為“空類型”可以包容“有類型”,而“有類型”則不能包容“空類型”,比如,我們可以說“男人和女人都是人”,但不能說“人是男人”或者“人是女人”。
- void修飾函數返回值和參數
規則1:如果函數沒有返回值,那么應該將其聲明為void類型。
在C語言中,凡不加返回值類型限定的函數,就會被編譯器作為返回整型值處理。如果函數沒有返回值,那么一定要聲明為void類型。這既是程序良好可讀性的需要,也是編程規范性的要求。規則2:如果函數無參數,那么應聲明其參數為void
在C語言中,可以給無參數的函數傳送任意類型的參數,但是在C++編譯器中編譯同樣的代碼則會出錯。在C++中,不能向無參數的函數傳送任何參數。所以無論在C還是在C++中,若函數不接受任何參數,則一定要指明參數為void。
- void指針
規則1: 千萬小心又小心地使用void指針類型。
按照ANSI標準,不能對void指針進行算法操作,之所以這樣認定,是因為它堅持:進行算法操所的指針必須是確定知道其指向數據類型大小的,也就是說必須知道內存目的地址的確切值。
在實際的程序設計中,為符合ANSI 標準,并提高程序的可移植性,我們可以這樣編寫實現同樣功能的代碼:
void* pvoid;
(char*)pvoid++; // ANSI: 正確; GNU:正確
(char*)pvoid += 1; // ANSI: 錯誤; GNU:正確
規則2:如果函數的參數可以是任意類型指針,那么應聲明其參數為void*
典型的如內存操作函數memcpy和memset的函數原型分別為:
void * memcpy(void * dest, const void * src, size_t len);
void * memset(void * buffer, int c, size_t num);
有趣的是,memcpy和memset函數返回的也是void * 類型。
- void 不能代表一個真實的變量
void 不能代表一個真是的變量。void 體現了一種抽象,這個世界上的變量都是“有類型”的,譬如一個人不是男人就是女人(人妖不算)。
void 的出現只是為了一種抽象的需要,如果你正確地理解了面向對象中“抽象基類”的概念,也很容易理解 void 數據類型。正如不能給抽象基類定義一個實例,我們不能定義一個 void(讓我們類比的稱void為“抽象數據類型”)變量。
return關鍵字
return 用來終止一個函數并返回其后面跟的值。
?注意:return語句不可返回指向“棧內存”的“指針”,因為該內存在函數體結束時自動銷毀。
const關鍵字也許該被替換為readonly (傳送門)
const 是 constant 的縮寫,是恒定不變的意思,也翻譯為常量和常數等。很不幸,正是因為這一點,很多人都認為被 const 修飾的值是常量。這是不精確的,精確來說應該是只讀的變量,其值在編譯時不能被使用,因為編譯器在編譯時不知道其存儲的內容。或許當初這個關鍵字應該被替換為 readonly。
const 推出的初始目的,正式為了取代預編譯指令,消除它的缺點,同時繼承它的優點。
- 節省空間,避免不必要的內存分配,同時提高效率
編譯器通常不為普通 const 只讀變量分配存儲空間,而是將它們保存在符號表中,這使得它成為一個編譯期間的值,沒有了存儲與讀內存的操作,使得它的效率也很高。例如:
#define M 3 // 宏常量
const int N = 5; // 此時并未將N放入內存中
...
int i = N; // 此時為N分配內存,以后不再分配
int I = M; // 預編譯期間進行宏替換,分配內存
int j = N; // 沒有內存分配
int J = M; // 再進行宏替換,又一次分配內存
const i 定義的只讀變量從匯編的角度來看,只是給出了對應的內存地址,而不是像 #define 一樣給出的時立即數,所以const定義的 只讀變量在程序運行過程中只有一份備份(因為它是全局的只讀變量,存放在靜態區),而#define 定義的宏常量在內存中有若干個備份。#define 宏是在 預編譯階段進行替換,而 const 修飾的只讀變量實在編譯的時候確定其值,#define 宏 沒有類型,而 const 修飾的只讀變量 具有特定的類型。
- 修飾指針
先忽略類型名(編譯器解析的時候也是忽略類型名),我們看const 離哪個近,”近水樓臺先得月“,離誰近就修飾誰
- 修飾函數的參數
const 修飾符也可以修飾函數的參數,當不希望這個參數值在函數體內被意外改變時使用。
- 修飾函數的返回值
const 修飾符也可以修飾函數的返回值,返回值不可被改變。
最易變的關鍵字——volatile
volatile int i = 10;
volatile 關鍵字告訴編譯器,i 時隨時可能發生變化的,每次使用它的時候必須從內存中取出 i 的值,因此編譯器生成的匯編碼會重新從 i 的地址處讀取數據。
這樣看來,如果 i 是一個寄存器變量,表示一個端口數據或者是多個線程的共享數據,那么就容易出錯,所以說 volatile 可以保證對特殊地址的穩定訪問。
最會戴帽子的關鍵字——extern
博客園中有一篇關于extern的講解(傳送門)
struct關鍵字
struct是個神奇的關鍵字,它將一些相關聯的數據打包成一個整體,方便使用。
在網絡協議,通信控制,嵌入式系統,驅動開發等地方,我們經常要傳送的不是簡單的字節流(char型數組),而是多種數據組合起來的一個整體,其表現形式是一個結構體。經驗不足的開發人員往往將所有需要傳送的內容依順序保存在char型數組中,通過指針偏移的方法傳送網絡報文等信息。這樣做編程復雜,易出錯,而且一旦控制方式和通信協議有所變化,程序就要進行非常機制的修改,非常容易出錯。
這個時候只需要一個結構體就能搞定。平時我們要求函數的參數盡量不多于4個,如果函數的參數多余4個使用起來非常容易出錯(包括每個參數的意義和順序都容易出錯),效率也會降低(與具體CPU有關,ARM芯片對于超過4個參數的處理就有講究,具體請參考相關資料)。這個時候,可以用結構體壓縮參數個數。
?struct 與 class 的區別:struct 的成員默認情況下的屬性是 public,而 class 成員的卻是 private。
空結構體大小一般為1,在GCC里面計算的值為0,
union關鍵字
union 關鍵字的用法與 struct 的用法非常相似。
union 維護足夠的空間來放置多個數據成員中的“一種”,而不是為每一個數據成員配置空間。在 union 中所有的數據成員公用一個空間,同一時間只能儲存其中一個數據成員,所有的數據成員具有相同的其實地址。
一個 union 只配置一個足夠大的空間來容納最大長度的數據成員,在C++里,union 的成員默認屬性為 public。union 主要用來壓縮空間。如果一些數據不可能在同一時間同時被用到,則可以使用 union。
柔性數組
這邊引用博客園中的一篇關于柔性數組的文章傳送門
大小端模式對union類型數據的影響
下面看一個例子:
union
{int i;char a[2];
}* p, u;p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p.i 的值應該為多少呢?
這里需要考慮存儲模式:大端模式和小端模式。
大端模式(Big_endian):字數據的高字節存儲在低地址中,而字數據的低字節存放在高地址中。
小端模式(Little_endian):字數據的高字節存儲在高地址中,而字數據的低字節則存放在低地址中。
union 型數據所占的空間等于其最大的成員所占的空間。對 union 型成員的存取都從相對于該聯合體基地址的偏移量為 0 處開始,也就是聯合體的訪問不論對哪個變量的存取都是從 union 的首地址位置開始。如此一解釋,上面的問題是否已經有了答案呢?
如何用程序確認當前系統的存儲模式
上述問題似乎還比較簡單,那來個有計數含量的:請寫一個 C 函數,若處理器是 Big_endian,則返回 0 ; 若是 Little_endian ,則返回 1。
先分析下,按照上面關于大小端模式的定義,假設 int 類型變量i被初始化為1.
以大端模式存儲,其內存布局如圖1.3所示。
以小端模式存儲,其內存布局如圖1.4所示。
變量 i 占 4 字節,但只有1個字節的值為 1,另外 3 個字節的值都為 0。如果取出低地址上的值為 0,毫無疑問,這是大端模式;如果取出低地址上的值為1,毫無疑問,這是小端模式。既然如此,我們完全可以利用 union 類型數據“所有成員的起始地址一致”的特點編寫程序。到現在,應該知道怎么寫了吧?參考文案如下:
int checkSystem()
{union check{int i;char ch;}c;c.i = 1;return (c.ch == 1);
}
現在你可以用這個函數來測試當前系統的存儲模式,當然你也可以不用功函數而直接去查看內存來確定當前系統的存儲模式,如圖1.5所示:
圖1.5中 0x01 的值存在低地址上,說明當前系統為小端模式。
不過要說明的一點是,某些系統可能同時支持這兩種存儲模式,你可以用硬件跳線或在編譯器的選項中設置其存儲模式。
留一個問題:
在 x86 系統下,以下程序輸出的值為多少?
#include <stdio.h>int main()
{int a[5] = {1,2,3,4,5};int *ptr1 = (int *)(&a+1);int *ptr2 = (int *)((int)a+1);printf("&x, %x", ptr1[-1], *ptr2);return 0;
}
位域
對于位域的使用和自定義的行為需要詳細說明,且在使用前需要用代碼check當前系統的模式(大端或小端模式)
enum關鍵字
枚舉類型的使用方法
一般的定義方式如下:
enum enum_type_name
{ ENUM_CONST_1,ENUM_CONST_2,...ENUM_CONST_n
}enum_variable_name;
注意:enum_type_name是自定義的一種數據類型名,而enum_variable_name為enum_type_name類型的一個變量,也就是我們平時常說的枚舉變量。實際上enum_type_name類型是對一個變量取值范圍的限定,而花括號內是它的取值范圍,即enum_type_name類型的變量enum_variable_name只能取值為花括號內的任何一個值(都是常量,一般用大寫),如果賦給該類型變量的值不在列表中,則會報錯或者警告。
enum變量類型還可以給其中的常量符號賦值,如果不賦值則會從被賦初值的那個常量開始依次加一;如果都沒有賦值,他們的值從0開始依次遞增1.
枚舉與#define宏的區別
- #define宏常量是在 預編譯階段 進行簡單替換;枚舉常量則是在 編譯 的時候確定其值。
- 一般在調試器里,可以調試枚舉常量,但是不能調試宏常量。
- 枚舉可以以此定義大量相關的常量,而#define宏一次只能定義一個。
留2個問題:
1. 枚舉能做的是,#define宏能不能做到?如果能,那為什么還需要枚舉?
2. sizeof(ColorVal)的值是多少?為什么?
enum Color
{GREEN = 1,RED,BLUE,GREEN_RED = 10,GREEN_BLUE
}ColorVal;