C陷阱与缺陷学习笔记
生活随笔
收集整理的這篇文章主要介紹了
C陷阱与缺陷学习笔记
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
導讀
程序是由符號(token)序列所組成的,將程序分解成符號的過程,成為“詞法分析”。
符號構成更大的單元--語句和聲明,語法細節最終決定了語義。
詞法陷阱
符號(token)指的是程序的一個基本組成單元,其作用相當于一個句子中的單詞。編譯器中負責將程序分解為一個一個符號的部分,稱作“詞法分析器”。 在C語言中,符號之間的空白(/b /t /n...)將被忽略。 #include <stdio.h> int main() {if(1)printf("Hello World\n");return0; }C語言中char類型都是當做int類型來處理的
如果一個整型常量的第一個字符是數字0,那么該常量將被視為八進制數。 因此10與010的含義截然不同。 用雙引號引起來的字符串,代表的是一個指向無名數組起始字符的指針, 該數組被雙引號之間的字符以及一個額外的二進制為零的字符'\0'初始化。 #include <stdio.h>int main() {printf("%c\n", "hello"[0]);printf("%c\n", "hello"[1]);printf("%c\n", "hello"[2]);printf("%c\n", "hello"[3]);printf("%c\n", "hello"[4]); }輸出 h e l l o
printf("%s\n", "hello");
char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", str);
二者是等價的。
語法陷阱
函數調用: void fun() { printf("Hello World"); } 在C語言中,函數調用即使不帶參數,也應該包括參數列表。 調用方式:fun();(這僅僅是一種簡寫的形式) 而實際上真正的調用語法為:(*fun)(); 理解函數聲明:(使用添加括號的方式)語義陷阱
單純意義上的數組只有兩種操作: 1.確定數組的大小sizeof(arr) / sizeof(arr[0]) 2.獲得指向該數組下標為0的元素的指針。 其他操作都是通過指針進行的。 關于數組名取地址: 在C中, 在幾乎所有使用數組的表達式中,數組名的值是個指針常量,也就是數組第一個元素的地址。 它的類型取決于數組元素的類型: 如果它們是int類型,那么數組名的類型就是“指向int的常量指針“。在以下兩種場合下,數組名并不是用指針常量來表示,就是當數組名作為sizeof操作符和單目操作符&的操作數時。 sizeof返回整個數組的長度,而不是指向數組的指針的長度。 取一個數組名的地址所產生的是一個指向數組的指針,而不是一個指向某個指針常量的指針。所以&a后返回的指針便是指向數組的指針,跟a在指針的類型上是有區別的。 #include <stdio.h> int main() {int arr[] = {1, 2, 3, 4, 5};int *p = arr;printf("%d\n", *p);p++;printf("%d\n", *p);int (*ptr)[5];ptr = &arr;//&arr的類型為 int(*)[5]printf("%d\n", **ptr);p = *ptr;p++;printf("%d\n", *p);return 0; }ptr/&arr類型為:int(*)[5]; *ptr/p類型為:int*; arr類型為:int* const p;//指針常量 #include <stdio.h>int main() {int a[5] = {1, 2, 3, 4, 5};int *ptr = (int*)(&a + 1);int array[2][3] = {{1, 2, 3}, {4, 5, 6}};printf("%d %d %d\n", a, &a, ptr);printf("%d %d\n", *(a + 1), *(ptr - 1));printf("%d %d %d %d\n", array, array[0], &array[0][0], &array);printf("%d %d %d %d\n", array + 1, array[0] + 1, &array[0][0] + 1, &array + 1); }從以上輸出我們可以看出:數組名和數組名取地址在數值上是相同的,均表示數組第一個元素的地址。但是二者的顆粒度不同。
當數組是一維數組時,數組名是以一個數組元素為顆粒度,表現為“當數組名加1時,這里的1表示一個數組元素單元”,例子中的數組元素為整數,所以數組名加1時地址加4;而數組名取地址&以整個數組為顆粒度,表現為“當數組名取地址&加1時,這里的1是表示整個數組單元”,例子中的數組為有5個元素的整型數組,所以數組名取地址&加1時,地址加20.
當數組是二維數組時,數組名array、array[0]、&array[0][0]以及數組名取地址&在數值上是相同的,同樣各個之間的顆粒度不同。其中array[0]以及 &array[0][0] 的顆粒度相同,均是以一個數組元素為顆粒度,所以它們加1后,地址加4;而數組名和數組名取地址&顆粒度不同,前者以一行元素為顆粒度,后者以整個數組單元為顆粒度,所以前者加1,地址加3*4,后者加1,地址加6*4.
二維數組:以數組作為元素的數組。 計算機中只有一維數組,二維數組只是虛擬出來的概念。 int arr[4][5]; arr的類型為:int(*)[5] int (*ptr)[5]; ptr = arr;//arr->指向數組的指針。
#define NULL ? ?((void *)0)
#include <stdio.h> int main() {char *p = NULL;if (p == 0){puts("YES");}return 0; }輸出:YES 原書: char *p = "Hello"; p[0] = 'h'; 這樣的操作是是錯誤的,因為p的類型為const char *,即指向常量的指針。 ANSI標準禁止對string literal作出修改。 數組作為參數,數組名立刻被轉換成指向該數組第一個元素的指針,因為C語言中我們沒有辦法將一個數組 作為函數參數直接傳遞。 所以: char *str = "12345"; printf("%s\n", str);完全等價于:printf("%s\n", &str[0]);
在C語言中,string literal代表了一塊包括字符串中所有字符以及一個空字符('\0')的內存區域的地址。 C99允許變長數組,所以一下操作也是正確的(C89則錯誤): #include <stdio.h> #include <string.h> int main() {char *p1 = "12345";char *p2 = "67";char arr[strlen(p1) + strlen(p2)];//memset(arr, 0, sizeof(arr));strcpy(arr, p1);//strcat(arr, p1);strcat(arr, p2);puts(arr);return 0; }
c語言中存在兩類整數算術運算,有符號運算和無符號運算。在無符號運算里,沒有了符號位,所以是沒有溢出的概念的。
所有的無符號運算都是以2的n次方為模。如果算術運算符的一個操作數是有符號書,另一個是無符號數,那么有符號數
會被轉換為無符號數(表示范圍小的總是被轉換為表示范圍大的),那么溢出也不會發生。但是,當兩個操作數都是有符號數
時,溢出就有可能發生。而且溢出的結果是未定義的。當一個運算的結果發生溢出時,任何假設都是不安全的。
例如,假定a和b是兩個非負的整型變量(有符號),我們需要檢查a+b是否溢出,一種想當然的方式是:
if (a + b < 0)
? ? ? 溢出;
實際上,在現實世界里,這并不能正常運行。當a+b確實發生溢出時,所有關于結果如何的假設均不可靠。比如,在某些
機器的cpu,加法運算將設置一個內部寄存器為四種狀態:正,負,零和溢出。在這種機器上,c編譯器完全有理由實現以上
的例子,使得a+b返回的不是負,而是這個內存寄存器的溢出狀態。顯然,if的判斷會失敗。
一種正確的方式是將a和b都強制轉換為無符號整數:
if ( (unsigned)a + (unsigned)b ?> INT_MAX)
? ? ? 溢出;
這里的int_max值為有符號整型的最大值。在一般的編譯器里是一個預定義的常量。ANSI C在limits里定義了INT_MAX,值為
2的31次方-1.?
不需要用到無符號算數運算的另一種可行方法是:
if (a > INT_MAX - b )
? ? ?溢出;
PS : 有符號數的最高位(31位)為符號位,最高位為0的時候,表示正,為1的時候表示負。運算時,符號位不參加運算,但是如果兩個數相加,30位需要進1時,那么即表示溢出。
| 如何檢測整型相加溢出(overflow) 前言: 本文主要討論如何判斷整型相加溢出(overflow)的問題. 我們知道計算機里面整型一般是有限個字節(4 bytes for int)表示, 正是因為只能用有限個字節表示一個整型變量, 由此帶來一個可能的問題: 溢出(overflow). 所謂整型溢出(overflow), 是說一個整數的值太大或者太小導致沒有用給定的有限個(比如四個字節沒法存超過2^31 – 1的有符號正整數)字節存儲表示. 這個整型溢出(overflow)問題一般的時候不會注意到也并不危險, 但是在做整型加法或者乘法的時候就有可能出現并且給程序帶來未定義的行為. 這里我們主要討論如何判斷整型相加溢出(overflow)的兩種方法以及各自優缺點. 整型相加溢出(overflow)的原因: 前言里面也已經提到了, 計算機中的的整數是用有限個字節表示的, 假設用k個字節表示一個整型變量, 那么這個變量可以表示的有符號整數的范圍是-2^(8k-1) ~ 2^(8k-1) – 1 ?, ?那么兩個正整數或者兩個負整數相加就有可能超過這個整型變量所能表示的范圍, 向上超出>2^(8k-1) – 1我們稱之為向上溢出, 向下超出<-2^(8k-1), 我們稱之為向下溢出. 注意這里兩個整數符號相同是整型相加溢出(overflow)的必要條件, 也就是說只有符號相同的兩個整數相加才有可能產生溢出(overflow)問題. 這個可以這么理解: 你想要是兩個符號不同的兩個整數, 他們相加, 那么這個和的值的絕對值一定是比單個相加數和被相加數都小, 既然相加數和被相加數都能用現有整型變量表示, 那么兩個不同整數的相加結果怎么樣都可以用現有的整型范圍的變量存儲下來而不溢出(overflow). 所以結論: 只有符號相同的整數相加才有可能才生溢出(overflow). 整型相加溢出(overflow)的檢測: 那么接下來的問題就是如何檢測到溢出(overflow)的產生的, 更具體的, 給定兩個整型, 比如int a, int b, 我們做加法a+b, 如何去判斷這個相加的結果是正確結果還是說是溢出(overflow)的結果. 下面我們給出兩種方法(后面我們會討論方法二是更好的方法), 并且做出解釋或者說不太嚴格的證明方法的正確性, 也就是為什么這么做就能保證溢出(overflow)的檢測的正確性. 方法一, 計算相加的結果, 判斷結果的符號, 兩個正整數相加結果為負數, 或者兩個負整數相加結果為正數, 那么就是溢出(overflow)了. 實現代碼如下: int addInt(int a, int b) { ? ? int res = a + b; ? ? if(a > 0 && b > 0 && res < 0) throw overflow_exception; ? ? if(a < 0 && b < 0 && res > 0) throw overflow_exception; ? ? return res; } 這個方法的原理是這樣, 計算機里面有符號整數是利用補碼的形式表示的, 第一位是符號位, 0表示整數, 1表示負數. 我們拿一個字節的整型來舉例, 一個字節的有符號數可以表示的范圍就是-128 ~ 127, 那么兩個一個字節的正整數相加的最大范圍就是254, 那么其中128 ~ 254就是溢出(overflow)的值, 是不能用一個字節存儲下的值, 這個值用一個字節表示的時候最高位是1, 在有符號整數系統里面這個值其實被當成了負數. 同理, 負數相加的時候最小可以到達-256, 根據補碼的表示對應的正整數取反加1就是對應的補碼, 那么對應的正整數的最高位是1, 現在取反以后就變成0, 也就是說兩個比較大的負數相加的結果其實變成了正數. 這就是上述方法的理論基礎. 方法二, 使用減法, 利用現有整型的最大或者最小極值減去某個加數(減法相當于變號, 從而保證沒有溢出(overflow)的發生), 和另一個加數比較大小進行判斷. 實現代碼如下: int addInt(int a, int b) { ? ? if(a > 0 && b > 0 && a > INT_MAX - b) throw overflow_exception; ? ? if(a < 0 && b < 0 && a < INT_MIN - b) throw overflow_exception; ? ? return a + b; } 這個方法其實不用太多的解釋, 簡單的數學知識就能解釋其中的原理, 由于減法保證了不會溢出(overflow), 又前面我們保證了兩個數都是正整數, 所以形如 a > INT_MAX – b的判斷是安全并且總是正確的. 而且這個檢測方法的正確性可以通過移位就看得懂了, 不像方法一, 需要一定的計算機底層的知識才能解釋說通. 方法一和方法二比較: 我自己一開始的時候思考利用方法一這樣的結論去判斷溢出(overflow), 但是我心里其實不放心, 因為方法一的前提是”兩個正整數相加溢出的充要條件是符號位變成1, 也就是結果變成了負數”, 這樣的結論或者事實對于我或者一般人來講其實并不是那么的直觀或者理所當然, 當然了我自己又用那個一個byte的例子試著去解釋, 結論還是正確的, ?所以方法一相比較方法二而言并不直觀. 另一方面有說法說是溢出的時候結果其實不確定, 上面在方法一里面我們的分析只是理論上的分析, 編譯器有可能做出相關的優化或者對溢出結果做出調整, 那么可能就出現未定義的行為了, 所以綜上所述, 方法二應該是比較更為安全和合理并且更為直觀的首選檢測整數相加溢出(overflow)的方法. 更新: 感謝網友Stanley的留言, 提供了第三種方法的檢測, 其實也就是方法一的bit operation版本, 通過位操作, 我們可以判斷求和結果x是否與a和b還同號, 如果同時不同號(也就是sign bit不相同了), 那我們就相當于檢測到了溢出. Stanley的版本稍微反了反, 我認為應該是下面這種情況才是溢出, 如有錯誤, 敬請指正. Thanks! x = a + b; if ((x^a) < 0 && (x^b) < 0) { ? ? //overflow, do something } 結束語: 本文主要討論了如何判斷整型相加溢出(overflow)的問題, 主要總結了整型相加溢出(overflow)的原因, 并給出了兩種檢測整型相加溢出(overflow)的方法, 方法一基于計算結果的正負, 方法二基于把加法轉化為減法. 本文同時給出了兩種方法的比較, 并且指出方法二應該是首選的檢測方法. |
main函數的返回值可以在cmd下echo %errorlevel%得到
連接
一個典型的連接(鏈接)實例: add.h #ifndef ADD_H #define ADD_H int add(int, int); #endifadd.c#include "add.h" #include <stdio.h>int add(int x, int y) {return (x + y); }test.c
#include "add.h"int main(void) {printf("%d\n", add(10, 20));return 0; } extern int x;//是聲明,不是定義 這就顯式的說明了x的存儲空間是在程序的其他地方分配的。
所謂的函數鏈接錯誤:1、找到個一個函數的兩個實現。 2、一個實現也沒有找到。
庫函數
FILE *fp;fp = fopen("test.txt", "r+");
為了保持與過去不能同時進行讀寫操作的程序的向下兼容性,一個輸入操作不能隨后直接緊跟著一個輸出操作, 反之亦然。
根本原因就是文件指針的移動問題,使用fseek可以移動文件指針,實現讀寫同時進行的操作。
預處理器
宏:只是對程序的文本起作用,即簡單的替換。 #ifndef max #define max(a,b) (((a) > (b)) ? (a) : (b)) #endif#ifndef min #define min(a,b) (((a) < (b)) ? (a) : (b)) #endif 優點:普通函數的調用涉及空間的申請,參數入棧、出棧,這樣必然帶來系統開銷。而宏沒有函數調用的開銷 缺點:由于只是簡單的文本替換,沒有類型檢查,很容易形成看似正確實則錯誤的代碼。 c++的inline函數的涉及必然也是想保留宏的優點而擯棄宏的缺點。 1、不能忽視宏定義中的空格。 2、宏不是函數。 3、使用時最好把每個參數都用括號括起來。 4、宏不是類型定義,定義新類型最好使用typedef。 assert宏的正確定義: #define assert(e) \((void)((e) || _assert_error(__FILE__, __LINE__)))__FILE__, __LINE__是內建于C語言預處理器中的宏,它們會被擴展為所在文件的文件名和所處代碼行的行號。轉載于:https://www.cnblogs.com/lgh1992314/p/6616389.html
總結
以上是生活随笔為你收集整理的C陷阱与缺陷学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: logstash 利用drop 丢弃过滤
- 下一篇: 二叉树经典题之二叉树最近公共祖先(Lee