首页
/
每日頭條
/
圖文
/
c語言常出現的錯誤和改正方式
c語言常出現的錯誤和改正方式
更新时间:2024-04-28 10:06:36

c語言常出現的錯誤和改正方式?定義了指針變量,但是沒有為指針分配内存,即指針沒有指向一塊合法的内存淺顯的例子就不舉了,這裡舉幾個比較隐蔽的例子,今天小編就來說說關于c語言常出現的錯誤和改正方式?下面更多詳細答案一起來看看吧!

c語言常出現的錯誤和改正方式(常見的C語言内存錯誤及對策)1

c語言常出現的錯誤和改正方式

一、指針沒有指向一塊合法的内存

定義了指針變量,但是沒有為指針分配内存,即指針沒有指向一塊合法的内存。淺顯的例子就不舉了,這裡舉幾個比較隐蔽的例子。

1、結構體成員指針未初始化

struct student { char *name; int score; }stu,*pstu; int main() { strcpy(stu.name,"Jimy"); stu.score = 99; return 0; }

很多初學者犯了這個錯誤還不知道是怎麼回事。這裡定義了結構體變量stu,但是他沒想到這個結構體内部char *name 這成員在定義結構體變量stu 時,隻是給name 這個指針變量本身分配了4 個字節。name 指針并沒有指向一個合法的地址,這時候其内部存的隻是一些亂碼。所以在調用strcpy 函數時,會将字符串"Jimy"往亂碼所指的内存上拷貝,而這塊内存name 指針根本就無權訪問,導緻出錯。解決的辦法是為name 指針malloc 一塊空間。

同樣,也有人犯如下錯誤:

int main() { pstu = (struct student*)malloc(sizeof(struct student)); strcpy(pstu->name,"Jimy"); pstu->score = 99; free(pstu); return 0; }

為指針變量pstu 分配了内存,但是同樣沒有給name 指針分配内存。錯誤與上面第一種情況一樣,解決的辦法也一樣。這裡用了一個malloc 給人一種錯覺,以為也給name 指針分配了内存。

2、沒有為結構體指針分配足夠的内存

int main() { pstu = (struct student*)malloc(sizeof(struct student*)); strcpy(pstu->name,"Jimy"); pstu->score = 99; free(pstu); return 0; }

為pstu 分配内存的時候,分配的内存大小不合适。這裡把sizeof(struct student)誤寫為sizeof(struct student*)。當然name 指針同樣沒有被分配内存。解決辦法同上。

3、函數的入口校驗

不管什麼時候,我們使用指針之前一定要确保指針是有效的。

一般在函數入口處使用assert(NULL != p)對參數進行校驗。在非參數的地方使用if(NULL != p)來校驗。但這都有一個要求,即p 在定義的同時被初始化為NULL 了。比如上面的例子,即使用if(NULL != p)校驗也起不了作用,因為name 指針并沒有被初始化為NULL,其内部是一個非NULL 的亂碼。

assert 是一個宏,而不是函數,包含在assert.h 頭文件中。如果其後面括号裡的值為假,則程序終止運行,并提示出錯;如果後面括号裡的值為真,則繼續運行後面的代碼。這個宏隻在Debug 版本上起作用,而在Release 版本被編譯器完全優化掉,這樣就不會影響代碼的性能。

有人也許會問,既然在Release 版本被編譯器完全優化掉,那Release 版本是不是就完全沒有這個參數入口校驗了呢?這樣的話那不就跟不使用它效果一樣嗎?

是的,使用assert 宏的地方在Release 版本裡面确實沒有了這些校驗。但是我們要知道,assert 宏隻是幫助我們調試代碼用的,它的一切作用就是讓我們盡可能的在調試函數的時候把錯誤排除掉,而不是等到Release 之後。它本身并沒有除錯功能。再有一點就是,參數出現錯誤并非本函數有問題,而是調用者傳過來的實參有問題。assert 宏可以幫助我們定位錯誤,而不是排除錯誤。

二、為指針分配的内存太小

為指針分配了内存,但是内存大小不夠,導緻出現越界錯誤。

char *p1 = “abcdefg”; char *p2 = (char *)malloc(sizeof(char)*strlen(p1)); strcpy(p2,p1);

p1 是字符串常量,其長度為7 個字符,但其所占内存大小為8 個byte。初學者往往忘了字符串常量的結束标志“\0”。這樣的話将導緻p1 字符串中最後一個空字符“\0”沒有被拷貝到p2 中。解決的辦法是加上這個字符串結束标志符:

char *p2 = (char *)malloc(sizeof(char)*strlen(p1) 1*sizeof(char));

這裡需要注意的是,隻有字符串常量才有結束标志符。比如下面這種寫法就沒有結束标志符了:

char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};

另外,不要因為char 類型大小為1 個byte 就省略sizof(char)這種寫法。這樣隻會使你的代碼可移植性下降。

三、内存分配成功,但并未初始化

犯這個錯誤往往是由于沒有初始化的概念或者是以為内存分配好之後其值自然為0。未初始化指針變量也許看起來不那麼嚴重,但是它确确實實是個非常嚴重的問題,而且往往出現這種錯誤很難找到原因。

曾經有一個學生在寫一個windows 程序時,想調用字庫的某個字體。而調用這個字庫需要填充一個結構體。他很自然的定義了一個結構體變量,然後把他想要的字庫代碼賦值給了相關的變量。但是,問題就來了,不管怎麼調試,他所需要的這種字體效果總是不出來。我在檢查了他的代碼之後,沒有發現什麼問題,于是單步調試。在觀察這個結構體變量的内存時,發現有幾個成員的值為亂碼。就是其中某一個亂碼惹得禍!因為系統會按照這個結構體中的某些特定成員的值去字庫中尋找匹配的字體,當這些值與字庫中某種字體的某些項匹配時,就調用這種字體。但是很不幸,正是因為這幾個亂碼,導緻沒有找到相匹配的字體!因為系統并無法區分什麼數據是亂碼,什麼數據是有效的數據。隻要有數據,系統就理所當然的認為它是有效的。

也許這種嚴重的問題并不多見,但是也絕不能掉以輕心。所以在定義一個變量時,第一件事就是初始化。你可以把它初始化為一個有效的值,比如:

int i = 10; char *p = (char *)malloc(sizeof(char));

但是往往這個時候我們還不确定這個變量的初值,這樣的話可以初始化為0 或NULL。

int i = 0; char *p = NULL;

如果定義的是數組的話,可以這樣初始化:

int a[10] = {0};

或者用memset 函數來初始化為0:

memset(a,0,sizeof(a));

memset 函數有三個參數,第一個是要被設置的内存起始地址;第二個參數是要被設置的值;第三個參數是要被設置的内存大小,單位為byte。這裡并不想過多的讨論memset 函數的用法,如果想了解更多,請參考相關資料。

至于指針變量如果未被初始化,會導緻if 語句或assert 宏校驗失敗。這一點,上面已有分析。

四、内存越界

内存分配成功,且已經初始化,但是操作越過了内存的邊界。這種錯誤經常是由于操作數組或指針時出現“多1”或“少1”。比如:

int a[10] = {0}; for (i=0; i<=10; i ) { a[i] = i; }

所以,for 循環的循環變量一定要使用半開半閉的區間,而且如果不是特殊情況,循環變量盡量從0 開始。

五、内存洩漏

内存洩漏幾乎是很難避免的,不管是老手還是新手,都存在這個問題。甚至包括windows,Linux 這類軟件,都或多或少有内存洩漏。也許對于一般的應用軟件來說,這個問題似乎不是那麼突出,重啟一下也不會造成太大損失。但是如果你開發的是嵌入式系統軟件呢?比如汽車制動系統,心髒起搏器等對安全要求非常高的系統。你總不能讓心髒起搏器重啟吧,人家閻王老爺是非常好客的。

會産生洩漏的内存就是堆上的内存(這裡不讨論資源或句柄等洩漏情況),也就是說由malloc 系列函數或new 操作符分配的内存。如果用完之後沒有及時free 或delete,這塊内存就無法釋放,直到整個程序終止。

1、告老還鄉求良田

怎麼去理解這個内存分配和釋放過程呢?先看下面這段對話:

萬歲爺:愛卿,你為朕立下了汗馬功勞,想要何賞賜啊?

某功臣:萬歲,黃金白銀,臣視之如糞土。臣年歲已老,欲告老還鄉。臣乞良田千畝以蔭後世,别無他求。

萬歲爺:愛卿,你勞苦功高,卻僅要如此小賞,朕今天就如你所願。戶部劉侍郎,查看湖廣一帶是否還有千畝上等良田未曾封賞。

劉侍郎:長沙尚有五萬餘畝上等良田未曾封賞。

萬歲爺:在長沙撥良田千畝封賞愛卿。愛卿,良田千畝,你欲何用啊?

某功臣:謝萬歲。長沙一帶,适合種水稻,臣想用來種水稻。種水稻需要把田分為一畝一塊,方便耕種。

。。。。

2、如何使用malloc 函數

不要莫名其妙,其實上面這段小小的對話,就是malloc 的使用過程。malloc 是一個函數,專門用來從堆上分配内存。使用malloc 函數需要幾個要求:

内存分配給誰?這裡是把良田分配給某功臣。

分配多大内存?這裡是分配一千畝。

是否還有足夠内存分配?這裡是還有足夠良田分配。

内存的将用來存儲什麼格式的數據,即内存用來做什麼?

這裡是用來種水稻,需要把田分成一畝一塊。分配好的内存在哪裡?這裡是在長沙。

如果這五點都确定,那内存就能分配。下面先看malloc 函數的原型: (void *)malloc(int size) malloc 函數的返回值是一個void 類型的指針,參數為int 類型數據,即申請分配的内存大小,單位是byte。内存分配成功之後,malloc 函數返回這塊内存的首地址。你需要一個指針來接收這個地址。但是由于函數的返回值是void *類型的,所以必須強制轉換成你所接收的類型。也就是說,這塊内存将要用來存儲什麼類型的數據。比如: char *p = (char *)malloc(100); 在堆上分配了100 個字節内存,返回這塊内存的首地址,把地址強制轉換成char *類型後賦給char *類型的指針變量p。同時告訴我們這塊内存将用來存儲char 類型的數據。也就是說你隻能通過指針變量p 來操作這塊内存。這塊内存本身并沒有名字,對它的訪問是匿名訪問。

上面就是使用malloc 函數成功分配一塊内存的過程。但是,每次你都能分配成功嗎?

不一定。上面的對話,皇帝讓戶部侍郎查詢是否還有足夠的良田未被分配出去。使用malloc函數同樣要注意這點:如果所申請的内存塊大于目前堆上剩餘内存塊(整塊),則内存分配會失敗,函數返回NULL。注意這裡說的“堆上剩餘内存塊”不是所有剩餘内存塊之和,因為malloc 函數申請的是連續的一塊内存。

既然malloc 函數申請内存有不成功的可能,那我們在使用指向這塊内存的指針時,必須用if(NULL != p)語句來驗證内存确實分配成功了。

3、用malloc 函數申請0 字節内存

另外還有一個問題:用malloc 函數申請0 字節内存會返回NULL 指針嗎?

可以測試一下,也可以去查找關于malloc 函數的說明文檔。申請0 字節内存,函數并不返回NULL,而是返回一個正常的内存地址。但是你卻無法使用這塊大小為0 的内存。這好尺子上的某個刻度,刻度本身并沒有長度,隻有某兩個刻度一起才能量出長度。對于這一點一定要小心,因為這時候if(NULL != p)語句校驗将不起作用。

4、内存釋放

既然有分配,那就必須有釋放。不然的話,有限的内存總會用光,而沒有釋放的内存卻在空閑。與malloc 對應的就是free 函數了。free 函數隻有一個參數,就是所要釋放的内存塊的首地址。比如上例: free(p); free 函數看上去挺狠的,但它到底作了什麼呢?其實它就做了一件事:斬斷指針變量與這塊内存的關系。比如上面的例子,我們可以說malloc 函數分配的内存塊是屬于p 的,因為我們對這塊内存的訪問都需要通過p 來進行。free 函數就是把這塊内存和p 之間的所有關系斬斷。從此p 和那塊内存之間再無瓜葛。至于指針變量p 本身保存的地址并沒有改變,但是它對這個地址處的那塊内存卻已經沒有所有權了。那塊被釋放的内存裡面保存的值也沒有改變,隻是再也沒有辦法使用了。

這就是free 函數的功能。按照上面的分析,如果對p 連續兩次以上使用free 函數,肯定會發生錯誤。因為第一使用free 函數時,p 所屬的内存已經被釋放,第二次使用時已經無内存可釋放了。關于這點,我上課時讓學生記住的是:一定要一夫一妻制,不然肯定出錯。

malloc 兩次隻free 一次會内存洩漏;malloc 一次free 兩次肯定會出錯。也就是說,在程序中malloc 的使用次數一定要和free 相等,否則必有錯誤。這種錯誤主要發生在循環使用malloc 函數時,往往把malloc 和free 次數弄錯了。這裡留個 練習:

寫兩個函數,一個生成鍊表,一個釋放鍊表。兩個函數的參數都隻使用一個表頭指針。

5、内存釋放之後

既然使用free 函數之後指針變量p 本身保存的地址并沒有改變,那我們就需要重新把p的值變為NULL: p = NULL; 這個NULL 就是我們前面所說的“栓野狗的鍊子”。如果你不栓起來遲早會出問題的。比如:在free(p)之後,你用if(NULL != p)這樣的校驗語句還能起作用嗎?例如:

char *p = (char *)malloc(100); strcpy(p, “hello”); free(p); /* p 所指的内存被釋放,但是p 所指的地址仍然不變*/ … if (NULL != p) { /* 沒有起到防錯作用*/ strcpy(p, “world”); /* 出錯*/ }

釋放完塊内存之後,沒有把指針置NULL,這個指針就成為了“野指針”,也有書叫“懸垂指針”。這是很危險的,而且也是經常出錯的地方。所以一定要記住一條:free 完之後,一定要給指針置NULL。

同時留一個問題:對NULL 指針連續free 多次會出錯嗎?為什麼?如果讓你來設計free函數,你會怎麼處理這個問題?

六、内存已經被釋放了,但是繼續通過指針來使用

這裡一般有三種情況:

第一種:就是上面所說的,free(p)之後,繼續通過p 指針來訪問内存。解決的辦法就是給p 置NULL。

第二種:函數返回棧内存。這是初學者最容易犯的錯誤。比如在函數内部定義了一個數組,卻用return 語句返回指向該數組的指針。解決的辦法就是弄明白棧上變量的生命周期。

第三種:内存使用太複雜,弄不清到底哪塊内存被釋放,哪塊沒有被釋放。解決的辦法是重新設計程序,改善對象之間的調用關系。

上面詳細讨論了常見的六種錯誤及解決對策,希望讀者仔細研讀,盡量使自己對每種錯誤發生的原因及預防手段爛熟于胸。一定要多練,多調試代碼,同時多總結經驗。

,
Comments
Welcome to tft每日頭條 comments! Please keep conversations courteous and on-topic. To fosterproductive and respectful conversations, you may see comments from our Community Managers.
Sign up to post
Sort by
Show More Comments
推荐阅读
不為人知的護眼知識(漲知識不漲度數)
不為人知的護眼知識(漲知識不漲度數)
  近日,上海普瑞眼科醫院組織了一場“護眼指南·工會福利線上講座”。醫院視光主治醫師、工會小組成員王單為工會會員們科普了青少年科學用眼小常識,做到以下幾點,就能在“漲知識”的同時不漲度數。  Q:觀看屏幕需要注意什麼細節?   盡可能選擇較大的屏幕電子産品,在較遠的距離進行觀看。建議将視頻投影到大屏幕或鍊接到較大屏幕的電視上,增加觀看距離。盡量選擇屏幕分辨率...
2024-04-28
非誠勿擾如何對待感情(男生缺乏儀式感被心動女生拒絕)
非誠勿擾如何對待感情(男生缺乏儀式感被心動女生拒絕)
  一個身穿漢服的男嘉賓來到《非誠勿擾》舞台相親,他是一位漢服的服裝店老闆。也許是因為喜歡漢服,他選擇了台上一位同樣穿着漢服的女嘉賓作為心動女生,兩人無論是外表還是服飾上都非常的般配。不少女嘉賓也隐約感受到她是為了漢服女生而來,因此選擇滅燈讓位。本以為漢服的女生會留燈,沒想到她卻滅了燈,理由是男嘉賓在VCR裡說自己缺乏“儀式感”,而她則是比較看重儀式感這個東...
2024-04-28
模仿秀郭德綱盧鑫玉浩歡樂喜劇人(模仿郭德綱反而赢了)
模仿秀郭德綱盧鑫玉浩歡樂喜劇人(模仿郭德綱反而赢了)
  文/娛樂酸檸檬   3月29日晚間,《歡樂喜劇人》重新開賽,在經曆了長時間的調整後,各組喜劇人們明顯是有備而來,再加上賈冰和崔志佳的強勢助陣,本期競演的5個節目質量爆棚,使得東方衛視同一時段的收視率達到了0.8676%,位居各大衛視台榜首。      當晚的《歡樂喜劇人》競演,個人認為是7期節目中整體質量最好的一期,賈冰的小品切合當下環境,時效性極強;崔...
2024-04-28
荒野大镖客2線上版本還需要買嗎(救贖2OL部分不會影響GTAOL)
荒野大镖客2線上版本還需要買嗎(救贖2OL部分不會影響GTAOL)
  Take-Two似乎對《荒野大镖客:救贖2》的OL部分很有自信。在最近的季度财務電話會議上,Take-Two高層Strauss Zelnick被問到《荒野大镖客:救贖2》是否會推出OL部分以及與《GTAOL》的關系時,他是這麼回答的:      “娛樂的體驗是不同的,在這款遊戲中的體驗很難在另外一款遊戲中找到。你不會想‘我現在需要娛樂’。娛樂是你想要一直...
2024-04-28
曳引電梯家用井道尺寸(曳引家用電梯如何選擇)
曳引電梯家用井道尺寸(曳引家用電梯如何選擇)
  目前市面上主流的兩種家用電梯:曳引電梯和螺杆電梯。曳引電梯工作原理是通過永磁同步主機來帶動鋼絲繩或者鋼帶牽引實現轎廂上下移動,也是目前家用電梯比較常見的一種電梯類型。而曳引電梯又随着技術發展,分為了曳引鋼絲繩電梯和曳引鋼帶電梯兩大類。這兩類曳引家用電梯哪種更适合家庭使用,兩者又有啥區别呢?      傳統鋼絲繩技術成熟,用途廣泛,但是随着使用時間增加,鋼...
2024-04-28
Copyright 2023-2024 - www.tftnews.com All Rights Reserved