【2018深信服 醒獅計劃】《C陷阱與缺陷》學習筆記

2018深信服“醒獅計劃”筆記

先自我介紹一下,湖大研一計算機的菜雞,本科網絡工程的,大學裏不務正業一直在做應用,大一自學過一段時間的MFC,Windows網絡編程,感覺比控制檯好看多了,然後大二開始搞Android,做過一些小的demo,大三的時候參與過一個智能家居的創業項目,拿到了投資,種種原因,最後還是失敗了,於是乎把代碼開源了,在我的github上面,有興趣的可以拿走。三年下來東搞西搞,做過MFC,JAVA WEB,Android,Python後端,但是啥都只學到了皮毛。於是乎決定讀研,希望能夠沉澱一下,不過一直很迷茫,到底做那個方向好,正好看到深信服開設了“醒獅計劃”,於是就報名參加了,因爲讀研期間接觸到的項目都跟Linux相關,並且之前有過python後端的一點經驗,結合自己的興趣,於是乎選擇了Linux,希望自己能夠堅持學下去,也希望跟各位大佬多多交流,後附GitHub和blog地址,雖然沒什麼乾貨,歡迎互粉。

CSDN GitHub

第1周(4.22-4.29)

課程 必修 選修 基本要求
C語言進階 《C陷阱與缺陷》 《C和指針》、《C++沉思錄》 掌握C語言的指針和內存管理機制,瞭解常見的編碼錯誤和陷阱

C陷阱與缺陷(全書188頁)

影印版不是很清楚,我自己在網上找了一本高清的,內容相同,壓縮排版,總共79頁。後附鏈接。


第一章 .詞法陷阱(day1)

1.賦值 = ,比較 == 的誤用

本意跳過 空格 水平製表符 換行 ,結果c==’\t’ 誤寫成了 c=’\t’ 有可能造成了死循環

while (c == ’ ’ || c = ’\t’ || c == ’\n’)
    c = getc (f);

error無法得到執行,

if((filedesc == open(argv[i], 0)) < 0)
    error();

2. & | && ||的不同

3. 語法分析中的“貪心”

連續符號是連起來對待還是拆分之後對待

規則:每個符號儘可能多的包含更多的字符

含義相同

a---b
a-- -b

含義不同

a- --b

將x的值除以p賦值給y

錯誤的識別成了註釋

y = x /*p / * p points at the divisor */;

改進的寫法,加空格 ,闊號 。更加的清晰

y = x / *p /* p points at the divisor */;

y = x/(*p) /* p points at the divisor */;

4. 老版本C中允許使用=+ 代替 +=

a =- 1;

上面代碼將被編譯器理解成

a =a-1;

看上/* 是註釋的開始,然後在老的編譯器中會解釋成

a=/*b

中間多一個空格

a=/ *b;

組合賦值運算符如+=實際上是兩個記號。下面兩個是同樣的

a + /* strange */ = 1
a += 1

5.整型常量的坑

如果一個整型常量的第一個字符數字是0,那麼就被視爲八進制
10與010完全不同

gcc 提示報錯:invalid digit "8" in octal constant

8 9也會被當做八進制來處理,0195將會被處理爲1×8^2+9×8^1+5×8^0

坑!有時候無意中就寫成了下面這種情況

  struct{
    int part_number;
    char *descriptionion;
  )parttab[]={
    046 “left handed widget”
    047 “right handed widget” ,
    125 “frammis”
  };

6.字符和字符串

一般混用編譯器會檢查到,並且報錯

char *slash='/';

warning: initialization makes pointer from integer without a cast


在一個ASCII實現中,’a’和0141或97表示完全相同的東西。而一個包圍在雙引號中的字符串,只是編寫一個有雙引號之間的字符和一個附加的二進制值爲零的字符所初始化的一個無名數組的指針的一種簡短方法。

下面的兩個程序片斷是等價的:

printf("Hello world/n");
char hello[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '/n', 0 };
printf(hello);

使用一個指針來代替一個整數通常會得到一個警告消息(反之亦然),使用雙引號來代替單引號也會得到一個警告消息(反之亦然)

某些C語言編譯器不會對函數參數類型進行檢查,例如printf()

如果用

printf('\n');

代替

printf("\n");

會產生難以預料的錯誤,書上說4.4有詳解,那先挖個坑。

'/n'表示一個整數,它被轉換爲了一個指針,這個指針所指向的內容是沒有意義的


由於一個整數通常足夠大,以至於能夠放下多個字符,一些C編譯器允許在一個字符常量中存放多個字符。

這意味着用'yes'代替"yes"將不會被發現。後者意味着“分別包含yes和一個空字符的四個連續存儲器區域中的第一個的地址”,而前者意味着“在一些實現定義的樣式中表示由字符yes聯合構成的一個整數”。這兩者之間的任何一致性都純屬巧合。


第二章 .語法陷阱(day2)

1.理解函數聲明

what?

(*(void(*)())0)();

這表示((f))求值爲float並且因此,通過推斷,f也是一個float。

float ((f));

表示表達式ff()是一個float,因此ff是一個返回一個float的函數

float ff();

表示*g()(*h)()都是float表達式。由於()*優先級高,*g()*(g())表示同樣的東西:g是一個返回指float指針的函數,而h是一個指向返回float的函數的指針。

float *g(), (*h)();

聲明h,是一個指向一個返回值是float的函數指針

float(*h)()
(float(*)())

有了這些知識的武裝,我們現在可以準備解決(*(void(*)())0)()

首先,假設我們有一個變量fp,它包含了一個函數指針,並且我們希望調用fp所指向的函數。可以這樣寫:

(*fp)();

如果fp是一個指向函數的指針,則 *fp就是函數本身,因此(*fp)()是調用它的一種方法,(*fp)中的括號是必須的,否則這個表達式將會被分析爲*(fp())


第二步,我們現在要找一個適當的表達式來替換fp。如果C可以讀入並理解類型,我們可以寫:

(*0)();

但這樣並不行,因爲*運算符要求必須有一個指針作爲它的操作。另外,這個操作數必須是一個指向函數的指針,以保證*的結果可以被調用。因此,我們需要將0轉換爲一個可以描述指向一個返回void的函數的指針的類型。

如果fp是一個指向返回void的函數的指針,則(*fp)()是一個void值,並且它的聲明將會是這樣的:

void (*fp)();

因此,我們需要寫:

void (*fp)();
(*fp)();

來聲明一個啞變量。一旦我們知道了如何聲明該變量,我們也就知道了如何將一個常數轉換爲該類型:只要從變量的聲明中去掉名字即可。因此,我們像下面這樣將0轉換爲一個指向返回void的函數的指針

(void(*)())0

因此,我們用(void(*)())0來替換fp

(*(void(*)())0)();

結尾處的分號用於將這個表達式轉換爲一個語句

在這裏,我們解決這個問題時沒有使用typedef聲明。通過使用它,我們可以更清晰地解決這個問題:

typedef void (*funcptr)();
(*(funcptr)0)();

此問題並不是孤立的,經常存在,書中P18有一個signal的例子


2.運算符的優先級問題

常見的優先級使用錯誤引起的bug

假設有一個聲明瞭的常量FLAG,它是一個整數,其二進制表示中的某一位被置位(換句話說,它是2的某次冪),並且你希望測試一個整型變量flags該位是否被置位。通常的寫法是:

if(flags & FLAG)
{
  ....
}

if語句測試括號中的表達式求值的結果是否爲0。出於清晰的目的我們可以將它寫得更明確:

if(flags & FLAG != 0)
{
  ...
}

這個語句現在更容易理解了。但它仍然是錯的,因爲!=&的優先級更高,因此它被分析爲:

if(flags & (FLAG != 0))
{
  ...
}

除了FLAG是恰好是1的情況,其他時候都是錯誤的


假設你有兩個整型變量,hilow,它們的值在0和15(含0和15)之間,並且你希望將r設置爲8位值,其低位爲low,高位爲h。一種自然的寫法是:

r = hi << 4 + 1ow;

不幸的是,這是錯誤的。加法比移位綁定得更緊密,因此這個例子等價於:

r = hi << (4 + low);

正確的方法有兩種:

r = (h << 4) + l; //加括號
r = h << 4 | l; //邏輯或

一個方法是將所有的東西都用括號括起來,但表達式中的括號過多就會難以理解,因此最好還是是記住C中的優先級。

ps:道理是這麼講,但是真的很難記,這麼多:

優先級

運算符

名稱或含義

使用形式

結合方向

說明

1

[]

數組下標

數組名[常量表達式]

左到右

()

圓括號

(表達式)/函數名(形參表)

.

成員選擇(對象)

對象.成員名

->

成員選擇(指針)

對象指針->成員名

++

後置自增運算符

++變量名

單目運算符

後置自減運算符

–變量名

單目運算符

2

-

負號運算符

-表達式

右到左

單目運算符

(類型)

強制類型轉換

(數據類型)表達式

++

前置自增運算符

變量名++

單目運算符

前置自減運算符

變量名–

單目運算符

*

取值運算符

*指針變量

單目運算符

&

取地址運算符

&變量名

單目運算符

!

邏輯非運算符

!表達式

單目運算符

~

按位取反運算符

~表達式

單目運算符

sizeof

長度運算符

sizeof(表達式)

3

/

表達式/表達式

左到右

雙目運算符

*

表達式*表達式

雙目運算符

%

餘數(取模)

整型表達式/整型表達式

雙目運算符

4

+

表達式+表達式

左到右

雙目運算符

-

表達式-表達式

雙目運算符

5

<<

左移

變量<<表達式

左到右

雙目運算符

>>

右移

變量>>表達式

雙目運算符

6

>

大於

表達式>表達式

左到右

雙目運算符

>=

大於等於

表達式>=表達式

雙目運算符

<

小於

表達式<表達式

雙目運算符

<=

小於等於

表達式<=表達式

雙目運算符

7

==

等於

表達式==表達式

左到右

雙目運算符

!=

不等於

表達式!= 表達式

雙目運算符

8

&

按位與

表達式&表達式

左到右

雙目運算符

9

^

按位異或

表達式^表達式

左到右

雙目運算符

10

|

按位或

表達式|表達式

左到右

雙目運算符

11

&&

邏輯與

表達式&&表達式

左到右

雙目運算符

12

||

邏輯或

表達式||表達式

左到右

雙目運算符

13

?:

條件運算符

表達式1? 表達式2: 表達式3

右到左

三目運算符

14

=

賦值運算符

變量=表達式

右到左

/=

除後賦值

變量/=表達式

*=

乘後賦值

變量*=表達式

%=

取模後賦值

變量%=表達式

+=

加後賦值

變量+=表達式

-=

減後賦值

變量-=表達式

<<=

左移後賦值

變量<<=表達式

>>=

右移後賦值

變量>>=表達式

&=

按位與後賦值

變量&=表達式

^=

按位異或後賦值

變量^=表達式

|=

按位或後賦值

變量|=表達式

15

,

逗號運算符

表達式,表達式,…

左到右

從左向右順序運算

這有15個,太困難了。然而,通過將它們分組可以變得容易


一元運算符

運算符中的最高優先級

一元運算符是右結合的,因此*p++表示*(p++),而不是(*p)++

二元運算符

其中算數運算符具有最高的優先級,然後是移位運算符、關係運算符、邏輯運算符、賦值運算符,最後是條件運算符。

  1. 所有的邏輯運算符的優先級比所有關係運算符都低。
  2. 移位運算符比算數運算符的優先級低,但比關係運算符高。

有一些奇怪的地方

乘法、除法和求餘具有相同的優先級,加法和減法具有相同的優先級,以及移位運算符具有相同的優先級

還有就是六個關係運算符並不具有相同的優先級:==!=的優先級比其他關係運算符要低。這就允許我們判斷ab是否具有與cd相同的順序,例如:

a < b == c < d

在邏輯運算符中,沒有任何兩個具有相同的優先級。按位運算符比所有順序運算符綁定得都緊密,每種與運算符都比相應的或運算符綁定得更緊密,並且按位異或(^)運算符介於按位與和按位或之間。

三元運算符

比我們提到過的所有運算符的優先級都低。這可以保證選擇表達式中包含的關係運算符的邏輯組合特性,如:

z = a < b && b < c ? d : e

複合賦值運算符具有相同的優先級並且是從右至左結合

a = b = c

等價於

b = c; a = b;

最低優先級的是逗號運算符

這很容易理解,因爲逗號通常在需要表達式而不是語句的時候用來替代分號


賦值是另一種運算符,通常會混用的優先級。例如,考慮下面這個用於複製文件的循環:

while(c = getc(in) != EOF)
    putc(c, out);

這個while循環中的表達式看起來像是c被賦以getc(in)的值,接下來判斷是否等於EOF以結束循環。不幸的是,賦值的優先級比任何比較操作都低,因此c的值將會是getc(in)EOF比較的結果,並且會被拋棄。因此,“複製”得到的文件將是一個由值爲1的字節流組成的文件。

上面這個例子正確的寫法並不難:

while((c = getc(in)) != EOF)
    putc(c, out);

然而,這種錯誤在很多複雜的表達式中卻很難被發現。

隨UNIX系統一同發佈的lint程序通常帶有下面的錯誤行:

if (((t = BTYPE(pt1->aty) == STRTY) || t == UNIONTY)
{
  ...
}

這條語句希望給t賦一個值,然後看t是否與STRTYUNIONTY相等。而實際的效果卻大不相同


3.語句的結束符號

C中的一個多餘的分號通常會帶來一點點不同:或者是一個空語句,無任何效果;或者編譯器可能提出一個警告消息,可以方便除去掉它。一個重要的區別是在必須跟有一個語句的ifwhile語句中。考慮下面的例子

if(x[i] > big);
    big = x[i];

這不會發生編譯錯誤,但這段程序的意義與:

if(x[i] > big)
    big = x[i];

就大不相同了。


遺漏一個分號,也是十分尷尬的

if (n<3)
  return
logrec.date = x[0];
logrec.time = x[0];
logrec.code = x[0];

這個代碼一樣能夠照常通過,編譯不會報錯,只是吧logrec.date = x[0];當作了return的操作數,相當於:

if (n<3)
  return logrec.date = x[0];
logrec.time = x[0];
logrec.code = x[0];

這裏樣可能會出現一個隱藏bug,如果n>3,就會跳過return,這個代碼就尷尬了,而且這種bug還很難發現。當然如果函數聲明的返回值是void,最終返回的卻不一樣,這樣就會報錯


還有這種情況:

struct logrec{
  int date;
  int time;
  int code;
}

main()
{
  ...
}

logrec結尾少寫了一個分號,使得代碼變成了,main函數的返回值是logrec


4.switch語句

優點缺點都在於break的使用,最好在沒寫break的地方加上註釋

case SUBTRACT:
    opnd2 = -opnd2;
    /* no break; */
case ADD:

5.函數調用

和其他程序設計語言不同,C要求一個函數調用必須有一個參數列表,但可以沒有參數。因此,如果f是一個函數

f();

上面語句是對函數進行調用,下面求函數地址,但不會調用它

f();

6.懸掛的else

注意:else與最近的if結合,這一問題不是C語言所獨有的,但它仍然傷害着那些有着多年經驗的C程序員。看下面代碼。

if(x == 0)
    if(y == 0) error();
else {
    z = x + y;
    f(&z);
}

實際上elseif(y == 0)結合了,這就尷尬了,解決辦法也很簡單,加上{}進行封裝就好了

if(x == 0) {
  if(y ==0)
    error();
}
else {
  z = z + y;
  f(&z);
}

代價就是代碼稍微長了一點,感覺可以接受,括號反而使得代碼更加的清晰調理了。


有些C語言大佬會用宏定義來解決這個問題,簡直瑟瑟發抖

#define IF    {if(
#define THEN  ){
#deflne ELSE  }else{
#define FI    }}


IF x==0
THEN IF Y==0
  THEN error():
  FI
ELSE  z=x+y:
f(&z):
FI

第三章 .語義陷阱(day3)

1.指針與數組

  1. 確定數組的大小
  2. 獲得指向該數組下標爲0元素的指針

重點:數組與指針的轉換,C99中允許變長數組VLA.GCC編譯器中實現了變長數組。


聲明一個含有17個元素的數組,每個元素是一個結構體

struct{
  int p[4];
  double x;
}b[17];

聲明一個名字爲calendar的數組,該數組擁有12個數組元素,每個元素含有31個整型的數組。shape=(12,13)

int calendar[12][31];

如果兩個指正指向同一個數組,指針相減也是有意義的,如下代碼q-p=i

int *q = p + i

數組名可以直接賦個指正,指向0的位置

p = a;

但是,在ANSI C 中,下面代碼是非法的,應爲&a是一個指向數組的指針,而p是指向整形變量的指針

p=&a;

下面兩種寫法相同的

p=p+1;
p++;

重點: 數組與指針轉換

*(a+i)是指數組a中的第i元素a[i],實際上a+ii+a的含義是一樣的,因此a[i]i[a]也具有相同的含義

sizeof(calendar[4])的結果是31*sizeof(int)


如下代碼所表達的意思是一樣的

i=calendar[4][7];
i=*(calendar[4]+7);

進一步可以改寫成

1=*(*(Calendar+4)+7);

和明顯可以發現帶括號下標的寫法要簡明的多


這個是非法的,calendar是數組的數組,在此處上下文中會轉回成指向數組的指針,而p是一個整型變量的指針。

p=calendar

我們可以申請一個指向數組的指針,來存放calendar的指針

int(*ap)[31];

2.非數組的指針

下面字符串拷貝的代碼,看上面貌似Ok但是包含3個bug
1. malloc可能無法請求內存
2. r的地址沒有釋放
3. strlen並沒有計算\n的位置,所以其實應該分配+1個內存空間

char *r *malloc();
r=malloc(strlen(s)+strlen(t));
strcpy(r, s);
strcat(r, t);

正確的寫法大概是這個樣子

char *r *malloc();
r=malloc(strlen(s)+Strlen(t)+1);
if(!r)(
  complain();
  exit(1);
)
strcpy(r s);
strcat(r t);
/*一段時間後調用*/
free(r);

3.作爲參數的數組聲明

C語言中,我們沒辦法將數組作爲函數參數直接傳遞,例如下面這個代碼只是將第一個元素的指針傳了進去

char hello[] = “hello”;

下面兩個代碼完全等效

printf("%s\n", hello);
printf("%s\n", &hello[0]);

同理,下面兩個也是一個意思

int strlen(char s[]) {
  ...
}
int strlen(char *s)
{
  ...
}

下面代碼有天壤之別,挖坑,後邊講

extern char *hello;
extern char hello[];

如果指針並不實際代表一個數組,則會產生誤導..

如果代表數組,則下面代碼是等價的

main(int argc, char* argv[])
main(int argc,char** argv)

雖然意思相同,但是前一種寫法明顯的看出argv是數組的第一個元素指針,選擇最清楚的表達。

4.避免“舉隅(yu)法”

沒文化,真可怕,打了半天沒打出隅這個字….
這裏寫圖片描述

ANSIC標準中禁止對stringliteral做出修改,C語言編譯器還是允許q[1]=’y’這種修改行爲

5.空指針並非空字符串

常數0經常用一個符號來代替

#define NULL 0

合法

if(p == (char *)0) ...

非法

if(strcmp(p, (char *)0) == 0) ...

原因是strcmp會檢查指針指向內存中的內容,如果p是一個空指針,gg

printf(p);
printf("%s", p);

gcc上打印(null)類似語句在不同計算機上有不同的效果。

6.邊界計算不對稱邊界

緩衝區聲明

#define N 1024
static char buffer[N];

設置一個指針指向緩衝區

static char *bufptr

這裏寫圖片描述

把字符c放到緩衝區中,buffer有加一,指向下一個沒有被使用的空間

*bufptr++ = c;

初始化緩衝區可以這樣寫

bufptr=&buffer[0];

更簡潔點可以這樣

bufptr=buffer;

bufwrite的代碼

void
bufwrite(char *p, int n)
{
  while(--n >= 0){
    if(bufptr==&buffer[N])
      flushbuffer();
    *bufptr++= *p++;
}

由於不對稱邊界原則,雖下面兩個代碼是等價的,但是應該寫成後一個

if(bufptr==&buffer[N])
if(bufptr > &buffer[N-1];

手寫一個內存拷貝,面試必考

void memcpy(char *dest const char *source
{
  while(--k >=0)
    *dest++ = *source++;
}

7.求值順序

會產生一個除0的錯誤

if(count != 0 && sum/count < smallaverage)
  printf(“average < %g\n”, smallaverage);

C語言定義規定a < b首先被求值。如果a確實小於bc < d必須緊接着被求值以計算整個表達式的值。但如果a大於或等於b,則c < d根本不會被求值。

a < b && c < d

要對a < b求值,編譯器對ab的求值就會有一個先後。但在一些機器上,它們也許是並行進行的。

C中只有四個運算符&&||?:和,指定了求值順序。&&||最先對左邊的操作數進行求值,而右邊的操作數只有在需要的時候才進行求值。而?:運算符中的三個操作數:abc,最先對a進行求值,之後僅對bc中的一個進行求值,這取決於a的值。,運算符首先對左邊的操作數進行求值,然後拋棄它的值,對右邊的操作數進行求值


坑,其中的問題是y[i]的地址並不保證在i增長之前被求值。在某些實現中,這是可能的;但在另一些實現中卻不可能,這就尷尬了。

i = 0;
while(i < n)
    y[i] = x[i++];

正確的寫法是下面這種

i = 0;
while(i < n) {
    y[i] = x[i];
    i++;
}

簡寫成這樣也是OK的

for(i = 0; i < n; i++)
    y[i] = x[i];

8.運算符 && ||

10 || f()中的f()也不會被求值

9.整數溢出

假設ab是兩個非負整型變量,你希望測試a + b是否溢出。一個明顯的辦法是這樣的:

if(a + b < 0)
    complain();

然而這是有問題的,一旦a + b發生了溢出,對於結果的任何判斷都是沒有意義的。一種正確的做法是這樣

if((int)((unsigned)a + (unsigned)b) < 0)
    complain();

10.爲函數main提供返回值

None

第四章 .連接(day4)

1. 什麼是連接器

一個C程序可能有很多部分組成,它們被分別編譯,並由一個通常稱爲連接器、連接編輯器或加載器的程序綁定到一起。由於編譯器一次通常只能看到一個文件,因此它無法檢測到需要程序的多個源文件的內容才能發現的錯誤。

2. 聲明與定義

外部整型變量,顯示的說明a的存儲空間在程序的其他地方分配

extern int a;

3. 命名衝突與static修飾符

static 是常用來減少類命名衝突的有用工具

static int a;

static函數

static int fun(int x)
{
  ...
}

4. 形參、實參與返回值

如果函數在不同文件中,調用之前需要在所調用的文件中聲明

double square(double);//square在其他文件中實現的

main()
{
  printf(“%g\n”, square(0.3));
}

square(2)的寫法是合法的,因爲2會被自動轉換成一個雙精度的類型,square((double)2)square(2.0),這兩種寫法也是可以的


一個輸入、輸出的錯誤例子

#include<stdio.h>
main()
{
  int i;
  char c;
  for(i = 0; i < 5; i++){
    scanf("%d", &c);
    printf("%d", i);
  }
  printf("\n");
}

表面上應該輸出

0 1 2 3 4

實際上輸出的卻是

0 0 0 0 0 1 2 3 4

因爲c的聲名是char而不是int。當你令scanf()去讀取一個整數時,它需要一個指向一個整數的指針。但這裏它得到的是一個字符的指針。但scanf()並不知道它沒有得到它所需要的:它將輸入看作是一個指向整數的指針並將一個整數存貯到那裏。由於整數佔用比字符更多的內存,這樣做會影響到c附近的內存。c附近確切是什麼是編譯器的事;在這種情況下這有可能是i的低位。因此,每當向c中讀入一個值,i就被置零。當程序最後到達文件結尾時,scanf()不再嘗試向c中放入新值,i纔可以正常地增長,直到循環結束。

5. 檢查外部類型

如果C程序被劃分爲兩個文件

int n;
long n;

這不是一個有效的C程序,因爲一些外部名稱在兩個文件中被聲明爲不同的類型。然而,很多實現檢測不到這個錯誤,因爲編譯器在編譯其中一個文件時並不知道另一個文件的內容。


這個程序運行時實際會發生什麼?這有很多可能性:
1. C編譯器足夠聰明,能夠檢測到類型衝突。則我們會得到一個診斷消息,說明n在兩個文件中具有不同的類型。
2. 也有可能你所使用的實現將intlong視爲相同的類型。典型的情況是機器可以自然地進行32位運算。在這種情況下你的程序或許能夠工作,好象你兩次都將變量聲明爲long(或int)。但這種程序的工作純屬偶然。
3. n的兩個實例需要不同的存儲,它們以某種方式共享存儲區,即對其中一個的賦值對另一個也有效。這可能發生,例如,編譯器可以將int安排在long的低位。不論這是基於系統的還是基於機器的,這種程序的運行同樣是偶然。
4. n的兩個實例以另一種方式共享存儲區,即對其中一個賦值的效果是對另一個賦以不同的值。在這種情況下,程序可能失敗。

6. 頭文件

最好的解決辦法就是使用頭文件,只在頭文件中聲明,例如定義一個file.h,他聲明瞭

extern char filename[];

在需要使用的地方加上頭文件

#include “file.h”
char filename[] = "/etc/passwd";

第五章 .庫函數(day5)

1. 返回整數的getchar函數

看看下面這個例子

#include <stdio.h>
main()
{
  char c;
  while((c=getchar()) != EOF)
    putchar(c);
}

這段程序看起來好像要將標準輸入複製到標準輸出。實際上,它並不完全會做這些。原因是c被聲明爲字符而不是整數。這意味着它將不能接收可能出現的所有字符包括EOF。因此這裏有兩種可能性。

  1. 有時一些合法的輸入字符會導致c攜帶和EOF相同的值,有時又會使c無法存放EOF值。
  2. 在前一種情況下,程序會在文件的中間停止複製。在後一種情況下,程序會陷入一個無限循環。
  3. 實際上,還存在着第三種可能:程序會偶然地正確工作。

C語言參考手冊嚴格地定義了表達式

((c = getchar()) != EOF)

當一個較長的整數被轉換爲一個較短的整數或一個char時,它會被截去左側;超出的位被簡單地丟棄。

2. 更新順序文件

看上去貌似可以同時進行讀寫操作,其實不然

FILE *fp;
fp=fopen(file, "r+");

若真正要實現同時讀寫,需要插入fseek函數的調用

FILE *fp;
struct record rec;
...
while(fread((char*)&rec, sizeof(rec), 1, fp) == 1){
  /*rec執行某些操作*/
  if(/*rec必須被重寫寫入*/){
    fseek(fp, -(long)sizeof(rec), 1);
    fwrite((char*)&rec, sizeof(rec), 1, fp);
  }
}

上面這個函數看上去貌似沒有什麼問題,sizeof(res)也被轉換成了long類型,因爲int可能無法存放一個文件的大小,sizeof返回的是一個unsigned,所以必須先將其轉換爲有符號類型才能將其反號,但是這段代碼依然有可能會出錯

問題情況:如果記錄需要被重新寫入文件,fwriteh獲得執行,這個操作的下一個操作就是fread,之間就缺少了一個fseek函數,解決辦法如下:

while(fread((char*)&rec, sizeof(rec), 1, fp) == 1){
  /*rec執行某些操作*/
  if(/*rec必須被重寫寫入*/){
    fseek(fp, -(long)sizeof(rec), 1);
    fwrite((char*)&rec, sizeof(rec), 1, fp);
    fseek(fp, 0L, 1);
  }
}

第二個fseek看上去啥也沒做,但是它卻改變了文件的狀態,是的文件現在可以正常的進行讀取了

運行測試代碼:

#include <stdio.h>
int main()
{
  FILE *fp;
  struct record{
  char a;
  char b;
};

struct record rec;
if((fp=fopen("test", "rb+"))==NULL)
{
  printf("open error!\n");
}
while(fread((char*)&rec, sizeof(rec), 1, fp) == 1){
  /*rec執行某些操作*/
  printf("record.a=%d\n", rec.a);
  printf("record.b=%c\n", rec.b);
  if(1){
    fseek(fp, -(long)sizeof(rec), 1);
    rec.a=rec.b;
    fwrite((char*)&rec, sizeof(rec), 1, fp);
    fseek(fp, 0L, 1); /*
    */
    }
  }
  fclose(fp);
}

3. 緩衝輸出與內存分配

程序員可以通過setbuf函數來控制生產數據量,如果buf是一個適當的字符數組

setbuf(stdout, buf);

上段代碼將告訴I/O庫寫入到stdout中的輸出要以buf作爲一個輸出緩衝,並且等到buf滿了或程序員直接調用fflush()再實際寫出。緩衝區的合適的大小定義爲BUFSIZ,在<stdio.h>中。


下面一個使用setbuf的例子

#include
main() {
    int c;

    char buf[BUFSIZ];
    setbuf(stdout, buf);
    while((c = getchar()) != EOF)
        putchar(c);
}

不幸的是這個程序是錯誤的,因爲一個細微的原因。原因在於,我緩衝區最後一次刷新是在主程序完成之後,庫將控制交回到操作系統之前所執行的清理的一部分。在這一時刻,緩衝區已經被釋放了!


解決的辦法有很多,比如定義靜態的緩衝區

static char buf[BUFSIZ];

另一種可能的方法是動態地分配緩衝區並且從不釋放它:

char *malloc();
setbuf(stdout, malloc(BUFSIZ));

注意在後一種情況中,不必檢查malloc()的返回值,因爲如果它失敗了,會返回一個空指針。而setbuf()可以接受一個空指針作爲其第二個參數,這將使得stdout變成非緩衝的。這會運行得很慢,但它是可以運行的。

4. 使用errno檢測錯誤

在系統調用中,如果調用失敗通常會通過一個名爲errno的外部變量來通知程序失敗,下面這個程序貌似可以處理錯誤情況,然後卻是有問題的

/*調用庫*/
if (errno)
  /*處理錯誤*/

原因是系統調用沒有失敗的情況下,沒有強制要求errno設置爲0 ,改進一下

errno = 0;
/*調用庫*/
if (errno)
  /*處理錯誤*/

上面代碼依然有問題,原因是系統調用成功的情況下,沒有強制要求errno設置爲0 ,但是也沒有禁止設置errno。系統調用可能回去調用其他的系統調用,其它調用也有可能去修改errno的值。


正確的寫法是:先檢測錯誤指示的返回值,確定程序執行失敗後在檢查errno

/*調用庫*/
if(返回的錯誤值)
  檢查errno

5. 庫函數signal

頭文件

#include <signal.h>

調用

signal(signal type, handler function);

signal處理函數中使用longjmp退出,是不安全的

異步操作是很容易留bug,很麻煩,儘量簡單的使用…

第六章 .預處理器(day6)

1. 不能忽略宏定義中的空格

多加了個空格,意思理解有歧義

#define f (x) ((x) - 1)

f(x)代表什麼呢

((x) - 1)

還是

(x) ((x) - 1)

第二種答案是正確的,下面這個寫法要好一些

#define f(x) ((x) - 1)

2. 宏並不是函數

因爲宏的一些寫法,容易被認爲與函數等同,比如下面兩種

#define abs(x) (((x)>=0)?(x): -(x))

#define max(a, b) ((a))(b)?(a):(b))

使用過多的括號是爲了防止出現優先級不清晰的問題

一個混合了宏和遞增運算符的寫法,看上去就要炸

#define putc(x, p) (--(p)->_cnt>=0?(*(p)->_ptr++=(x)):_flsbuf(x,p))

這樣寫是很危險的,putc()的第一個參數是一個要寫入到文件中的字符,第二個參數是一個指向一個表示文件的內部數據結構的指針。注意第一個參數完全可以使用如*z++之類的東西,儘管它在宏中兩次出現,但只會被求值一次。而第二個參數會被求值兩次(在宏體中,x出現了兩次,但由於它的兩次出現分別在一個:的兩邊,因此在putc()的一個實例中它們之中有且僅有一個被求值)。由於putc()中的文件參數可能帶有副作用,這偶爾會出現問題。不過,用戶手冊文檔中提到:“由於putc()被實現爲宏,其對待stream可能會具有副作用。特別是putc(c, *f++)不能正確地工作。”但是putc(*c++, f)在這個實現中是可以工作的。


宏的另外一個缺點是可能會產生巨大的表達式

#define max(a, b) ((a) > (b) ? (a) : (b)) 

假設我們這個定義來查找a、b、c和d中的最大值。如果我們直接寫:

max(a, max(b, max(c, d))) 

它將被擴展爲:

((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ? 
(a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d)))))) 

這種寫法還不如寫if else

3. 宏並不是語句

宏的定義很不直觀,經常產生一些隱藏bug,舉一個例子

#define assert(e) if(!e) assert_error(__FILE__, __LINE__)

下面代碼貌似Ok

if(x > 0 && y > 0)
  assert(x > y);
else
  assert(y > x);

但是展開後就是這個鬼樣子

if(x > 0 && y > 0)
  if(!(x > y)) assert_error("foo.c", 37);
else
  if(!(y > x)) assert_error("foo.c", 39);

很明顯if else會出問題,於是乎準備價格{}試試水

#define assert(e) \
{ if(!e) assert_error(__FILE__, __LINE__); }

然後就變成了這樣

if(x > 0 && y > 0)
  { if(!(x > y)) assert_error("foo.c", 37); };
else
  {if(!(y > x)) assert_error("foo.c", 39); };

看上去十分的詭異。

4.宏並不是類型定義

宏的一個通常的用途是保證不同地方的多個事物具有相同的類型:

#define FOOTYPE struct foo 
FOOTYPE a; 
FOOTYPE b, c; 

這樣如果要改代碼中的某個變量類型就很方便了,而且所有的C編譯器都支持它,所以使用這樣的宏定義還有着可移植性的優勢


多變量聲明是有bug,下面一個栗子

#define T1 struct foo * 
typedef struct foo * T2; 

多變量聲明

T1 a, b; 
T2 c, d; 

然後被擴展成

struct foo * a, b; 

這裏a被定義爲一個結構指針,但b被定義爲一個結構(而不是指針)。相反,第二個聲明中c和d都被定義爲指向結構的指針,因爲T2的行爲好像真正的類型一樣。

第七章 .可移植性缺陷(day6)

1. 應對C語言標準的變更

標準往往需要很長時間才能得到編譯器的支持,然後即使支持了,用戶也懶得去升級他們的編譯器,所以編寫程序時候,選擇那個標準也是很尷尬的,這個有點類似與Android開發裏面的api level的選擇,儘可能的選擇用戶覆蓋範圍廣的那個版本

2. 標識符號名稱的限制

並不是所有都區分標示符名稱的大小寫,C標準只要求能夠區分6個字符不同的外部名稱

3. 整數的大小

C爲程序員提供三種整數尺寸:shortintlongC語言定義對各種整數的大小不作任何保證:
1. 整數的三種尺寸是非遞減的,後面的能容納前面的。
2. 普通整數的大小要足夠存放任意的數組下標。
3. 字符的大小應該體現特定硬件的本質。

4. 字符是有符號整數還是無符號整數

有一種誤解是認爲當c是一個字符變量時,可以通過寫(unsigned)c來得到與c等價的無符號整數。這是錯誤的,因爲一個char值在進行任何操作(包括轉換)之前轉換爲int。這時c會首先轉換爲一個int,這可能會產生奇怪的結果。

正確的方法是寫(unsigned char)c,無需轉換成int,直接進行轉換。

5. 移位運算符

常見的有兩個問題
1. 在右移運算中,空出的位是用0填充還是用符號位填充.
2. 移位的範圍就是位數

第一個問題的答案很簡單,但有時是實現相關的。如果要進行移位的操作數是無符號的,會移入0。如果操作數是帶符號的,則實現有權決定是移入0還是移入符號位。如果在一個右移操作中你很關心空位,那麼用unsigned來聲明變量。這樣你就有權假設空位被設置爲0。

第二個問題很簡單,(例如32位,移位範圍n[0,31])

移位操作最好不要用除法來實現,效率低

6. 內存位置0

不同b編譯器有不同情況,比如:結束,讀到垃圾數據,讀取到關鍵位置的文件(操作系統)

#include <stdio.h>
int main()
{
    char *p;
    p = NULL;
    printf("Location 0 contain %d\n", *p);
    return 0;
}

我電腦上運行報錯:Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

7. 除法運算時發生的截斷

#include <stdio.h>
main()
{
  int a=-3, b=2;
  int q,r;
  q = a / b;
  r = a % b;
  printf("q=%d, r=%d\n", q, r);
}

帶符號整數的除法與餘數

8. 隨機數的大小

rand

RAND_MAX

9. 大小寫轉換

toupper()tolower(),他們最初都被實現爲

#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')

這些宏確實有一個缺陷,即:當給定的東西不是一個恰當的字符,它會返回垃圾。下面這個是無法工作的。

int c;
while((c = getchar()) != EOF)
    putchar(tolower(c));

必須寫成

int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);

後來AT&T的大佬有重寫了宏

#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))

但要知道,這裏c的三次出現都要被求值,這會破壞如toupper(*p++)這樣的表達式。因此,可以考慮將toupper()和tolower()重寫爲函數,大概想這個樣子。

int toupper(int c) {
  if(c >= 'a' && c <= 'z')
    return c + 'A' - 'a';
  return c;
}

後面考慮到有的人不願意付出效率上的損失,有寫了,但是用了新名字

#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')

10. 首先釋放,然後重新分配

三個內存分配函數

malloc
malloc(n)
realloc

釋放後又分配是合法的

free (p);
p = realloc(p, newsize);

釋放一個鏈表中所有元素

for(p = head; p != NULL; p = p->next)
  free((char *)p);

11. 可移植性問題的一個栗子

一個移植的例子,它將整數轉換位十進制數,並用代表其中每一個數字的字符來調用給定的函數。

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)(n % 10 + '0');
}

這個程序——由於它的簡單——具有很多可移植性問題。首先是將n的低位數字轉換成字符形式的方法。用n % 10來獲取低位數字的值是好的,但爲它加上’0’來獲得相應的字符表示就不好了。這個加法假設機器中順序的數字所對應的字符數順序的,沒有間隔,因此’0’ + 5和’5’的值是相同的,等等。儘管這個假設對於ASCII和EBCDIC字符集是成立的,但對於其他一些機器可能不成立。避免這個問題的方法是使用一個串:

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}

另一個問題發生在當n < 0時。這時程序會打印一個負號並將n設置爲-n。這個賦值會發生溢出,因爲在使用2的補碼的機器上通常能夠表示的負數比正數要多。例如,一個(長)整數有k位和一個附加位表示符號,則-2k可以表示而2k卻不能。
一個簡單的方法是將這個程序劃分爲兩個函數.

void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        printneg(n, p);
    }
    else
        printneg(-n, p);
}

void printneg(long n, void (*p)()) {
    if(n <= -10)
      printneg(n / 10, p);
    (*p)("0123456789"[-(n % 10)]);
}

我們使用n / 10和n % 10來獲取n的前導數字和結尾數字(經過適當的符號變換)。調用整數除法的行爲在其中一個操作數爲負的時候是實現相關的。因此,n % 10有可能是正的!這時,-(n % 10)是負數,將會超出我們的數字字符數組的末尾。
爲了解決這一問題,我們建立兩個臨時變量來存放商和餘數。

void printneg(long n, void (*p)()) {
  long q;
  int r;
  if(r > 0) {
    r -= 10;
    q++;
  }
  if(n <= -10) {
    printneg(q, p);
  }
  (*p)("0123456789"[-r]);
}

爲了滿足可移植性,我們需要付出很高的代價!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章