你所不知道的C語言高級用法

2021-01-08 51CTO

整形溢出和提升

大部分 C 程式設計師都以為基本的整形操作都是安全的其實不然,看下面這個例子,你覺得輸出結果是什麼:

int main(int argc, char** argv) {     long i = -1;      if (i < sizeof(i)) {          printf("OK\n");     }     else {          printf("error\n");     }      return 0; } 

當一個變量轉換成無符號整形時,i的值不再是-1,而是 size_t的最大值,因為sizeof操作返回的是一個 size_t類型的無符號數。在C99/C11標準裡寫道:

「If the operand that has unsigned integer type has rank greater orequal to the rank of the type of the other operand, then the operandwith signed integer type is converted to the type of the operand withunsigned integer type.」

在C標準裡面 size_t至少是一個 16 位的無符號整數,對於給定的架構 size_t 一般對應long,所以sizeof(int)和size_t至少相等,這就帶來了可移植性的問題,C標準沒有定義 short, int,long,longlong的大小,只是說明了他們的最小長度,對於 x86_64 架構,long在Linux下是64位,而在64位Windows下是32位。一般的方法是採用固定長度的類型比如定義在C99頭文件stdint.h中的uint16_t,int32_t,uint_least16_t,uint_fast16_t等。

如果 int可以表示原始類型的所有值,那麼這個操作數會轉換成 int,否則他會轉換成 unsigned int。下面這個函數在 32 位平臺返回 65536,但是在 16 位系統返回 0。

uint32_t sum() {     uint16_t a = 65535;     uint16_t b = 1;     return a+b; } 

對於char 類型到底是 signed 還是 unsigned 取決於硬體架構和作業系統,通常由特定平臺的 ABI(Application Binary Interface) 指定,如果是 signed char,下面的代碼輸出-128 和-127,否則輸出 128,129(x86 架構)。

char c = 128; char d = 129; printf("%d,%d\n",c,d); 

內存管理和分配

malloc 函數分配製定字節大小的內存,對象未被初始化,如果 size 是 0 取決與系統實現。malloc(0)返回一個空指針或者 unique pointer,如果 size 是表達式的運算結果,確保沒有整形溢出。

「If the size of the space requested is 0, the behavior isimplementation- defined: the value returned shall be either a nullpointer or a unique pointer.」

size_t computed_size;  if (elem_size && num > SIZE_MAX / elem_size) {     errno = ENOMEM;     err(1, "overflow"); }  computed_size = elem_size*num; 

malloc不會給分配的內存初始化,如果要對新分配的內存初始化,可以用calloc代替malloc,一般情況下給序列分配相等大小的元素時,用calloc來代替用表達式計算大小,calloc 會把內存初始化為 0。

realloc 用來對已經分配內存的對象改變大小,如果新的 size 更大,額外的空間沒 有 被 初 始 化 , 如 果 提 供 給 realloc 的 指 針 是 空 指 針 , realloc 就 等 效 於malloc,如果原指針非空而 new size是0,結果依賴於作業系統的具體實現。

「In case of failure realloc shall return NULL and leave provided memoryobject intact. Thus it is important not only to check for integeroverflow of size argument, but also to correctly handle object size ifrealloc fails.」

下面這段代碼可以帶你領會malloc,calloc,realloc,free的用法:

#include <stdio.h> #include <stdint.h> #include <malloc.h> #include <errno.h>  #define VECTOR_OK            0 #define VECTOR_NULL_ERROR    1 #define VECTOR_SIZE_ERROR    2 #define VECTOR_ALLOC_ERROR   3  struct vector {     int *data;     size_t size; };  int create_vector(struct vector *vc, size_t num) {      if (vc == NULL) {         return VECTOR_NULL_ERROR;     }      vc->data = 0;     vc->size = 0;      /* check for integer and SIZE_MAX overflow */     if (num == 0 || SIZE_MAX / num < sizeof(int)) {         errno = ENOMEM;         return VECTOR_SIZE_ERROR;     }      vc->data = calloc(num, sizeof(int));      /* calloc faild */     if (vc->data == NULL) {         return VECTOR_ALLOC_ERROR;     }      vc->size = num * sizeof(int);     return VECTOR_OK; }  int grow_vector(struct vector *vc) {      void *newptr = 0;     size_t newsize;      if (vc == NULL) {         return VECTOR_NULL_ERROR;     }       /* check for integer and SIZE_MAX overflow */     if (vc->size == 0 || SIZE_MAX / 2 < vc->size) {         errno = ENOMEM;         return VECTOR_SIZE_ERROR;     }      newsize = vc->size * 2;      newptr = realloc(vc->data, newsize);      /* realloc faild; vector stays intact size was not changed */     if (newptr == NULL) {         return VECTOR_ALLOC_ERROR;     }      /* upon success; update new address and size */     vc->data = newptr;     vc->size = newsize;     return VECTOR_OK; } 

避免重大錯誤

使用未初始化的變量,C語言要求所有變量在使用之前要初始化,使用未初始化的變量會造成為定義的行為,這和C++不同,C++保證所有變量在使用之前都得到初始化,Java儘量保證變量使用前的得到初始化,如類基本數據成員會被初始化為默認值。

free錯誤對空指針調用 free,對不是由 malloc family 函數分配的指針調用 free,或者對已經調用 free 的指針再次調用 free。一開始初始化指針為NULL可以減少錯誤,GCC和Clang編譯器有-Wuninitialized 選項來對未初始化的變量顯示警告信息,另外不要將同一個指針用於靜態變量和動態變量。

char *ptr = NULL; void nullfree(void **pptr) { void *ptr = *pptr; assert(ptr != NULL) free(ptr); *pptr = NULL; } 

3.對空指針解引用,數組越界訪問

對NULL指針或者free』d內存解引用,數組越界訪問,是很明顯的錯誤,為了消除這種錯誤,一般的做法就是增加數組越界檢查的功能,比如Java裡的array就有下標檢查的功能,但是這樣會帶來嚴重的性能代價,我們要修改ABI(application binary interface),讓每個指針都跟隨著它的範圍信息,在數值計算中cost is terrible。

4.違反類型規則

把int×指針cast成float×,然後對它解引用,在C裡面會引發undefined behavior,C規定這種類型的轉換需要使用memset,C++裡面有個reinterpret_cast函數用於無關類型之間的轉換,reinterpret_cast (expression)

防止內存洩漏

內存洩漏發生在程序不再使用的動態內存沒有得到釋放,這需要我們掌握動態分配對象的作用域,尤其是什麼時候該調用free來釋放內存,常用的集中方法如下:

在程序啟動的時候分配在程序啟動的時候分配需要的heap memory,程序退出時把釋放的任務交給作業系統,這種方法一般適用於程序運行後馬上退出的那種。 使用變長數組(VLA)如果你需要一塊變長大小的空間並且作用域在函數中,變長數組可以幫到你,但是也有一個限制,一個函數中的變長數組內存大小一般不超過幾百字節,這個數字C標準沒有明確的定義,最好是把內存分配到棧上,在棧上允許分配的最大VLA內存是SIZE_MAX,掌握目標平臺的棧大小可以有效的防止棧溢出。 使用引用計數引用計數是一個很好的管理內存的方法,特別是當你不希望自己定義的對象被複製時,每一次賦值把引用計數加1,每次失去引用就把引用計數減1,當引用計數等於0時,以為的對象已經不再需要了,我們需要釋放對象佔用的內存,由於C不提供自動的析構函數,我們必須手動釋放內存,看一個例子:

#include <stdlib.h> #include <stdint.h> #define MAX_REF_OBJ 100  #define RC_ERROR -1 struct mem_obj_t{ void *ptr; uint16_t count; };  static struct mem_obj_t references[MAX_REF_OBJ]; static uint16_t  reference_count = 0; /* create memory object and return handle */  uint16_t create(size_t size){ if (reference_count >= MAX_REF_OBJ)  return RC_ERROR; if (size){ void *ptr = calloc(1, size); if (ptr !=  NULL){ references[reference_count].ptr = ptr;  references[reference_count].count = 0; return reference_count++; } }  return RC_ERROR; } 

/* get memory object and increment reference counter */ void* retain(uint16_t handle){  if(handle < reference_count && handle >= 0){     references[handle].count++;     return references[handle].ptr;     } else {         return NULL;     } }  /* decrement reference counter */ void release(uint16_t handle){ printf("release\n");  if(handle < reference_count && handle >= 0){     struct mem_obj_t *object = &references[handle];      if (object->count <= 1){         printf("released\n");     free(object->ptr);     reference_count } else {     printf("decremented\n");     object->count         }      } } 

C++標準庫有個auto_ptr智能指針,能夠自動釋放指針所指對象的內存,C++ boost庫有個boost::shared_ptr智能指針,內置引用計數,支持拷貝和賦值,看下面這個例子:

「Objects of shared_ptr types have the ability of taking ownership of a pointer and share that ownership: once they take ownership, the group of owners of a pointer become responsible for its deletion when the last one of them releases that ownership.」

#include <boost/smart_ptr.hpp> #include <iostream> int main() {     // Basic useage     boost::shared_ptr<int> p1(new int(10));     std::cout << "ref count of p1: " << p1.use_count() << std::endl;     boost::shared_ptr<int> p2(p1); // or p2 = p1;     std::cout << "ref count of p1: " << p1.use_count() << std::endl;     *p1 = 999;     std::cout << "*p2: " << *p2 << std::endl;     p2.reset();     std::cout << "ref count of p1: " << p1.use_count() << std::endl;     return 0; } 

4.內存池,有利於減少內存碎片,看下面這個例子:

#include <stdlib.h> #include <stdint.h>  struct mem_pool_t{ void* ptr;//指向內存池起始地址 size_t size;//內存池大小 size_t used;//已用內存大小 };  //create memory pool struct mem_pool_t* create_pool(size_t size){ mem_pool_t* pool=calloc(1,sizeof(struct men_pool_t)); if(pool!=NULL){ void* mem=calloc(1,size); if(mem!=NULL){ pool->ptr=mem; pool->size=size; pool->used=0; return pool;         }     } return NULL; }  //allocate memory from pool void* pool_alloc(mem_pool_t* pool,size_t size){ if(pool=NULL)     return NULL; size_t bytes_left=pool->size-pool->used; if(size&&size<=bytes_left){     void* mem=pool->ptr+pool->used;     pool->used+=size;     return mem;     } return NULL; }  //release memory of the pool void pool_free(mem_pool_t* pool){ if(pool!=NULL){ free(pool->ptr); free(pool);  } } 

5.垃圾回收機制引用計數採用的方法是當內存不再需要時得到手動釋放,垃圾回收發生在內存分配失敗或者內存到達一定的水位(watermarks),實現垃圾回收最簡單的一個算法是MARK AND SWEEP算法,該算法的思路是遍歷所有動態分配對象的內存,標記那些還能繼續使用的,回收那些沒有被標記的內存。Java採用的垃圾回收機制就更複雜了,思路也是回收那些不再使用的內存,JAVA的垃圾回收和C++的析構函數又不一樣,C++保證對象在使用之前得到初始化,對象超出作用域之後內存得到釋放,而JAVA不能保證對象一定被析構。

指針和數組

我們一般的概念裡指針和數組名是可互換的,但是在編譯器裡他們被不同的對待,當我們說一個對象或者表達式具有某種類型的時候我們一般是說這個對象是個左值(lvalue),當對象不是const的時候,左值是可以修改的,比如對象是複製操作符的左參數,而數組名是一個const左值,指向地一個元素的const指針,所以你不能給數組名賦值或者意圖改變數組名,如果表達式是數組類型,數組名通常轉換成指向地一個元素的指針。

但是也有例外,什麼情況下數組名不是一個指針呢?1.當它是sizeof操作符的操作數時,返回數組佔的內存字節數2.當它是取地址操作&的操作數時,返回一個數組的地址

看下面這個例子:

short a[] = {1,2,3}; short *pa; short (*px)[];  void init(){     pa = a;     px = &a;      printf("a:%p; pa:%p; px:%p\n", a, pa, px);      printf("a[1]:%i; pa[1]:%i (*px)[1]:%i\n", a[1], pa[1],(*px)[1]); } 

a是一個short類型數組,pa是一個指向short類型的指針,px呢?px是一個指向數組類型的指針,在a被賦值給pa之前,他的值被轉換成一個指向數組第一個元素的指針,下面那個a卻沒有轉換,因為遇到的是&操作符。數組下標a[1]等價於(a+1),和p[1]一樣,也指向(p+1),但是兩者還是有區別的,a是一個數組,它實際上存儲的是第一個元素的地址,所以數組a是用來定位第一個元素的,而pa不一樣,它就是一個指針,不是用來定位的。再比如:

int a[10]; int b[10]; int *a; c=&a[0];//c是指向數組a地一個元素的指針 c=a;//a自動轉換成指向第一個元素的指針,實際上是指針拷貝 b=a;//非法的,你不能用賦值符把一個數組的所有元素賦給另一個數組 a=c;//非法的,你不能修改const指針的值 

【編輯推薦】

【責任編輯:

未麗燕

TEL:(010)68476606】

點讚 0

相關焦點

  • 剖析c語言結構體的高級用法(二)
    ,所謂複雜數據就是裡面聚集各種基本數據類型,比如int ,  float   char  等;不過這裡這個現象我之前有網友討論過,以為是對的,但是之後又有網友指出,在vc++6.0裡面,這樣定義卻佔用一個字節大小的,看到這裡我只能說,c的不同標準要求不一樣或者編譯器不同吧,為此我特地下了一個vc++6.0編譯器來做了一下試驗,結果還真是一個字節,不過你要注意的是它是c++後綴名,但是你在vc++6.0
  • c語言中sscanf函數的高級用法
    常見用法和scanf類似,用%s,%d等獲取字符串和整數。但在%號後可以支持更多的格式,甚至是正則表達式,這樣一來sscanf的功能就比較強大了。(也就是不把此數據讀入參數中)2、{a|b|c}表示a,b,c中選一,[d],表示可以有d也可以沒有d。3、width表示讀取寬度。
  • C語言strcmp和strcpy的用法
    一、c語言strcmp()用法原型:int strcmp(const char *s1, const char *s2);頭文件:#include
  • 【C語言】天哪!break居然有這麼多用法
    當然了,除了對技能類興趣類的分享外,我們還會不定期更新一些與讀者心與心交流的文字,例如夜聊、夜聽、最終達到的效果是:一起進步,一起勉勵,在代碼的世界中茁壯成長!在現實世界中活出精彩!本期分享的乾貨主要是c語言的break知識點。
  • 你所不知道的C語言經典九大編程實例思想
    獲取更多精彩文章請關注云主宰蒼穹引言:對於學習計算機程式語言而言,一門程式語言的經典思想是十分重要的。這是一門計算機程式語言的特色優點,是其解決問題的經典思維。你所了解的C語言,有哪些經典的編程實例思想,歡迎下方留言交流!
  • 嵌入式系統高級C語言編程
    內容簡介  《嵌入式系統高級C語言編程》將主要介紹針對嵌入式系統的基於C語言的軟體項目開發的流程,較為複雜的c語言編程知識和技巧,編程風格和調試習慣
  • 單片機的C語言中數組的用法
    數組在C51語言的地位舉足輕重,因此深入地了解數組是很有必要的。下面就對數組進行詳細的介紹。(1)一維數組本文引用地址:http://www.eepw.com.cn/article/201611/320327.htm一維數組是最簡單的數組,用來存放類型相同的數據。數據的存放是線性連續的。
  • 你不知道的「高級」英語閱讀和英語「高級」閱讀
    你所不知道的「高級」原版英語閱讀和原版英語「高級」閱讀「英語閱讀」及其訓練分兩種:1) 把英語閱讀全部「讀成」中文的英語閱讀。這叫「高級」原版英語閱讀。因為它的實質卻是把所有這些「原版高級」英語讀成了中文,所以只是是「變相式」中文閱讀:用中文分析和閱讀其中的語法,用中文把其中的長難句翻譯成中文理解,用中文擴大詞彙量,講解語法詞法的用法等等。這種所謂的「高級原版」英語閱讀實質是只是一種英語的「學習」行為,也就是英語「只學不用」(「用」中文啦!),英語「學」「用」脫節。
  • 掌握C語言的必知要點
    溫故而知新,可以為師矣,初學一門語言的時候,我們會躍躍欲試,並沒有真正深入的理解,經過一段時間的實踐,會產生困惑,學而不思則殆,這時回過頭來看書,會有意想不到的收穫,會豁然開朗,會讓你在以後的實踐中更加運用自如
  • C語言,C++,C ,Java之間的關係
    C語言,C++,C#,Java,這幾種語言,應該說是當前最流行,也是最基礎的計算機語言。是不是有些人看著會頭大,大腦會不叫混亂,一個計算機怎麼會有那麼的的語言呢?看著就頭大。現在,小編先來給大家說下計算機語言的發張,一臺計算機最本質的語言是機器語言,由01010101的代碼組成,CPU處理的也是由由010101的代碼組成的數據。但是,這種語言太簡單了,不好理解。就來個數字組成的語言,可以用來表達一句話,一個數字,圖像,字母......也許只有計算機可以理解,反正小編是不知道什麼意思。
  • Python裝飾器以及高級用法
    一個簡單的繞道:返回函數的函數Python是一種非常特殊的語言,因為函數是第一類對象。這意味著一旦函數在作用域中被定義,它就可以傳遞給函數,賦值給變量,甚至從函數返回。這個簡單的事實是使python裝飾器成為可能的原因。查看下面的代碼,看看你是否可以猜出標記為A,B,C和D的行會發生什麼。
  • c語言——基本語法
    c語言由Dennis MacAlistair Ritchie創始,是普適性最強的一種電腦程式編輯語言,它不僅可以發揮出高級程式語言的功用,還具有彙編語言的優點。本期將簡潔地介紹c的基本語法。目錄1.輸入輸出2.選擇結構3.循環結構4.數組5.結構體6.子函數一、printf輸出格式:printf(格式控制,輸出表列);例如:printf("%d, %c\n", i, c);
  • 專升本c語言和二級c語言哪個難?
    c語言是計算機專業必考的科目,很多同學不知道專升本c語言和二級c語言哪個難?專升本c語言備考技巧有哪些?請看下文的介紹。專升本c語言和二級c語言哪個難?二級c語言要難一些,專升本c語言只考編程題,而且考的題目也不難,二級c語言考的比較系統,題型也更豐富,而且有時考的很細,要求知道更準確的c語言語法。零基礎,如何學c語言?
  • C語言中的變量存儲類型static老手都這樣用
    ,你還記得嗎?2、 Static關鍵字用法C語言中,無論是變量還是函數都可以用static關鍵字來修飾。具體用法我們分別來看。例如,你在file1.c文件中定義了多個函數,如果你不允許函數名為fun2的函數被其它文件的函數調用,只需要將其聲明為static即可,這樣fun2函數隻允許被file1.c中的其它函數調用,其它源文件中的函數無法調用fun2,起到了隱藏的作用。
  • C語言中「c=a+b」,這種結構合理嗎?
    int a = 5, b = 7, c; c = a+++b; 這個代碼確實不咋符合習慣的寫法,但是不管你相不相信,上面的例子是完全合乎語法的。問題是編譯器如何處理它?
  • C語言怎麼樣?今天聊聊C語言的發展史!
    C語言發展史的點點滴滴。 任何一種新事物的出現都不是來自於偶然,而是時代所驅使的必然結果。 如果你問我:C語言有多偉大。那麼,我可能會想一下,說:多偉大我不知道,但是我知道很偉大。
  • C語言的一些高級議題
    指針是C語言的靈魂,我們經常聽到這樣的說法,當我們初學C語言的時候,似乎覺得也沒有什麼,但是當你越來越深入的了解它,你就會發現C語言的強大有時甚至超乎你的想像。C語言作為一種相對較為底層的語言,在某些方面有著不可替代的優勢。因此,要學好C語言,要深入,要精通。
  • 成都嵌入式學習:C語言常用函數用法大全
    華清遠見成都中心高端IT就業培訓專家C語言是當中最廣泛的計算機程式語言,是所有計算機程式語言的祖先,其他計算機程式語言包括當前流行的Java語言,都是用C語言實現的,C語言是編程效率最高的計算機語言,既能完成上層應用開發,也能完成底層硬體驅動編程,在電腦程式設計當中,特別是在底層硬體驅動開發當中,具有不可替代的作用。
  • 【C語言】02.第一個C語言程序
    不過呢,開發工具屏蔽了很多操作細節和語法細節,不利於初學者直觀、系統地學習一門語言。因此,在這裡,我們暫時使用文本編輯工具UltraEdit來寫C語言代碼。 2.寫代碼1> C程序由函數構成寫代碼之前,你首先要知道:任何一個C語言程序都是由一個或者多個程序段(小程序)構成的,每個程序段都有自己的功能,我們一般稱這些程序段為「函數」。
  • C語言基礎知識學習(二)
    函數是C語言的基本單位;3. C語言程序開始於主函數,結束於主函數;4. C語言中沒有輸入輸出語句但有輸入輸出函數.為什麼要指定指針的數據類型,不都是地址嗎?因為不同類型的變量,所佔的空間大小不同。指針變量存放的是變量的首地址,那麼在取出數據的時候要知道地址的長度(所以一個數據有首地址編號(該類型的指針變量)和長度)例如int型指針變量加1意味著地址移動sizeof(int)個字節.