C語言基礎知識

1、關鍵字volatile的使用

      volatile關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統、硬件或者其它線程等。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。

      volatile應該解釋爲“直接存取原始內存地址”比較合適,“易變的”這種解釋簡直有點誤導人;
“易變”是因爲外在因素引起的,象多線程,中斷等,並不是因爲用volatile修飾了的變量就是“易變”了,假如沒有外因,即使用volatile定義,它也不會變化;而用volatile定義之後,其實這個變量就不會因外因而變化了,可以放心使用了;大家看看前面那種解釋(易變的)是不是在誤導人。


一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile
2、多任務環境下各任務間共享的標誌應該加volatile
3、存儲器映射的硬件寄存器通常也要加volatile說明,因爲每次對它的讀寫都可能有不同意義;

易錯例子:

1) 一個參數既可以是const還可以是volatile嗎?解釋爲什麼。
2) 一個指針可以是volatile 嗎?解釋爲什麼。
3) 下面的函數有什麼錯誤:
int square(volatile int *ptr)
{
   return *ptr * *ptr;
}


下面是答案:
1) 是的。一個例子是隻讀的狀態寄存器。它是volatile因爲它可能被意想不到地改變。它是const因爲程序不應該試圖去修改它。
2) 是的。儘管這並不很常見。一個例子是當一箇中斷服務子程序修改指向一個buffer的指針時。
3) 這段代碼裏有個惡作劇。這段代碼的目的是用來返指針*ptr指向值的平方,但是,由於*ptr指向一個volatile型參數,編譯器將產生類似下面的代碼:
int square(volatile int *ptr)
{
  int a,b;
  a = *ptr;
  b = *ptr;
  return a * b;
}
由於*ptr的值可能被意想不到地該變,因此a和b可能是不同的。結果,這段代碼可能返回值不是你所期望的平方值!正確的代碼如下:
long square(volatile int *ptr)
{
  int a;
  a = *ptr;
  return a * a;
}

典型的例子:
for ( int i=0; i<100000; i++);
這個語句用來測試空循環的速度的;但是編譯器肯定要把它優化掉,根本就不執行
如果你寫成:
for ( volatile int i=0; i<100000; i++);
它就會執行了

 

2、典型的C程序存儲空間佈局

一個典型的C程序存儲空間佈局由以下幾個部分組成:
    正文段:CPU執行的指令部分,也就是主要的程序代碼編譯出來的結果,只讀,通常可以共享。
    初始化數據段:通常稱之爲數據段,包含了程序中需要明確賦值的變量,譬如一些初始化的全局變量等,如 int a = 10,變量名和值都存放在這個段中。
    未初始化數據段:通常稱之爲BSS(Block Started by Symbol)段,包含了程序中沒有進行賦值的變量,譬如一些未初始化的全局變量,如 int a,在程序執行之前,內核會把這部分全部置爲0(NULL),
    棧:自動變量(指在函數內部定義使用的變量)以及每次函數調用時所需保存的信息放在此段中。如函數調用時要保存返回地址等。棧是從上向下分配的。
    堆:通常在堆中進行動態存儲分配,如malloc, calloc, realloc等都從這裏面分配。堆是從下向上分配的。
    通常堆頂和棧底之間的虛擬地址空間是很大的。
    對X86處理器上的Linux,正文段從0x08048000開始,棧底則從0xC0000000之下開始。
    下圖是一個典型的C程序存儲空間的邏輯佈局:

 

//main.cpp 
int a = 0; 全局初始化區 
char *p1; 全局未初始化區 
main() 
{ 
     int b; 棧 
      char s[] = "abc"; 棧 
      char *p2; 棧 
      char *p3 = "123456"; 123456/0在常量區,p3在棧上。 
      static int c =0; 全局(靜態)初始化區 
      p1 = (char *)malloc(10); 
     p2 = (char *)malloc(20); 分配得來得10和20字節的區域就在堆區。 
      strcpy(p1, "123456"); 123456/0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。 
}


 

3、memset、memcpy、strcpy 的作用和區別 

(1)、memset

  原型:    extern void *memset(void *buffer, int c, int count);             

  用法:    #include <string.h>          

  功能:   把buffer所指內存區域的前count個字節設置成字符 c。          

  說明:    返回指向buffer的指針。用來對一段內存空間全部設置爲某個字符

  例子:    memset(st, 0, sizeof(struct _test)*10);  //清空方法 

//memset 源碼的實現 C語言
#include <mem.h> 
void* memset(void* s, int c, size_t n)
{
     unsigned char* p = (unsigned char*) s;
     while (n > 0) {
           *p++ = (unsigned char) c;
            --n;
     }
     return s;
}

(2)memcpy

   原型:extern void *memcpy(void*dest,void*src,unsignedintcount);             

  用法: #include <string.h>             

   功能: 由src所指內存區域複製count個字節到dest所指內存區域。            

   說明: src和dest所指內存區域不能重疊,函數返回指向dest的指針.可以拿它拷貝任何數據類型的對象。   

  例如:  char a[10],b[5];  memcpy(b, a, sizeof(b));             /*注意如果用sizeof(a),會造成b的內存地址溢出*/

 

(3) Strcpy  

   原型: extern char *strcpy(char *dest,char *src);                  

   用法: #include <string.h>                   

   功能: 把src所指由NULL結束的字符串複製到dest所指的數組中。              

   說明:src和dest所指內存區域不可以重疊且dest必須有足夠的空間來容納 src的字符串.返回指向dest的指針。                   

   例如:   char a[100],b[50];

             strcpy(a,b);如用   strcpy(b,a);要注意a中的字符串長度(第一個‘/0’之前)是否超過50位,如超過,則會造成b的內存地址溢出。

//來看看 strcpy的 源代碼實現:
char   *strcpy(char   *strDest,const   char   *strSrc)   
{   
    assert((strDest!=NULL)&&(strSrc   !=NULL)) //判斷指針是否合法,即分配內存,指向某塊確定區域
     char   *address   =   strDest;            //記住目標地址的起始值
     while((*strDest++   =   *strSrc++)!='\0') //先拷貝,後判斷,這樣就不用在拷貝完了後,再加一句
          NULL;                                // *strDest = '/0'; -->即加一個結束符.因爲字符串結束已拷貝了.
    return   address;                         //返回目標首地址的值。             
}

(4) 三者區別                    

    memset   主要應用是初始化某個內存空間。                 

    memcpy   是用於copy源空間的數據到目的空間中。                 

    strcpy   用於字符串copy,遇到‘\0’,將結束。 

 

4、sizeof與strlen的區別

1.sizeof操作符的結果類型是size_t,它在頭文件中typedef爲unsigned int類型。該類型保證能容納實現所建立的最大對象的字節大小。
2.sizeof是算符,strlen是函數。

3.sizeof可以用類型做參數,strlen只能用char*做參數,且必須是以''/0''結尾的。
     sizeof還可以用函數做參數,比如:
    short f();
    printf("%d/n", sizeof( f() ));
   輸出的結果是sizeof(short),即2。

4.數組做sizeof的參數不退化,傳遞給strlen就退化爲指針了。

5.大部分編譯程序 在編譯的時候就把sizeof計算過了,是類型或是變量的長度。這就是sizeof(x)可以用來定義數組維數的原因
char str[20]="0123456789";
int a=strlen(str); //a=10;
int b=sizeof(str); //而b=20;

6.strlen的結果要在運行的時候才能計算出來,用來計算字符串的長度,不是類型佔內存的大小。

7.sizeof後如果是類型必須加括號,如果是變量名可以不加括弧。這是因爲sizeof是個操作符不是個函數。
 
8.當使用了於一個結構類型或變量時, sizeof 返回實際的大小,
 當使用一靜態地空間數組, sizeof 返回全部數組的尺寸。
 sizeof 操作符不能返回被動態分配的數組或外部的數組的尺寸

 

5、不用第三個變量,交換兩個數據的值

方法一:
 a = a+b;
 b = a-b;
 a = a-b;
 
方法二:
 a = a+b - (b=a);
 
方法三:
 i ^= j;
 j ^= i;
 i ^= j;
 
比較:方法一,方法二 相加減可能越界。最好採用方法三。

 

6、static與final的區別

static關鍵字
     static 關鍵字可以用來修飾類的變量,方法和內部類。static 是靜態的意思,也是全局的意思它定義的東西,屬於全局與類相關,不與具體實例相關。就是說它調用的時候,只是 ClassName.method(),而不是 new ClassName().method()。new ClassName()不就是一個對象了嗎?static 的變量和方法不可以這樣調用的。它不與具體的實例有關。

 

final關鍵字
final 關鍵字有三個東西可以修飾的。修飾類,方法,變量。  詳細解釋一下:
 
在類的聲明中使用 final 
使用了 final 的類不能再派生子類,就是說不可以被繼承了。有些 java 的面試題裏面,問 String 可不可以被繼承。答案是不可以,因爲 java.lang.String是一個 final 類。這可以保證 String 對象方法的調用確實運行的是 String 類的方法,而不是經其子類重寫後的 方法。
 
在方法聲明中使用 final 
被定義爲 final 的方法不能被重寫了,如果定義類爲 final 的話,是所有的方法都不能重寫。而我們只需要類中的某幾個方法,不可以被重寫,就在方法前加 final 了。而且定義爲 final 的方法執行效率要高的啊。
 
在變量聲明中使用 final 
這樣的變量就是常量了,在程序中這樣的變量不可以被修改的。修改的話編譯器會抱錯的。而且執行效率也是比普通的變量要高。final 的變量如果沒有賦予初值的話,其他方法就必需給他賦值,但只能賦值一次。

 

    
注意:子類不能重寫父類的靜態方法哦,也不能把父類不是靜態的重寫成靜態的方法。想隱藏父類的靜態方法的話,在子類中聲明和父類相同的方法就行了。

 

7、排序法

 (1)、冒泡法

#include <stdio.h>
#include <stdlib.h>

static void bub_sort(char *buf, int num)
{
	int i,j;
	
	for (i=1; i<num; i++)
		for (j=0; j<num-i; j++)
			if (buf[j]>buf[j+1]){
				
				buf[j]   ^= buf[j+1];
				buf[j+1] ^= buf[j];
				buf[j]   ^= buf[j+1];
			}
}

int main(void)
{
	char buff[10]={0,15,26,74,11,19,59,95,23,10};
	int i;

	bub_sort(buff, 10);
	printf("\nsort:");
	for (i=0; i<10; i++)
		printf("%d ", buff[i]);	
	
	return 0;
}


(2)、二分法

//如果找到,返回buff的下標 
static int dich_sort(const char *buf, int len,int data)
{
	int i;
	int mid;
	int low=0,high=len;

	while (low <= high)
	{
		mid = (low + high)/2;
		if (buf[mid] > data)
			high = mid-1;
		else if (buf[mid] < data)
			low = mid+1;
		else
			return mid;	
	}			
	
	return -1;
}

 

6、位域


含位域結構體的sizeof:
前面已經說過,位域成員不能單獨被取sizeof值,我們這裏要討論的是含有位域的結構體的sizeof,只是考慮到其特殊性而將其專門列了出來。
C99規定int、unsigned int和bool可以作爲位域類型,但編譯器幾乎都對此作了擴展,允許其它類型類型的存在。
使用位域的主要目的是壓縮存儲,其大致規則爲:
1) 如果相鄰位域字段的類型相同,且其位寬之和小於類型的sizeof大小,則後面的字
段將緊鄰前一個字段存儲,直到不能容納爲止;
2) 如果相鄰位域字段的類型相同,但其位寬之和大於類型的sizeof大小,則後面的字
段將從新的存儲單元開始,其偏移量爲其類型大小的整數倍;
3) 如果相鄰的位域字段的類型不同,則各編譯器的具體實現有差異,VC6採取不壓縮方
式,Dev-C++採取壓縮方式;
4) 如果位域字段之間穿插着非位域字段,則不進行壓縮;
5) 整個結構體的總大小爲最寬基本類型成員大小的整數倍。
還是讓我們來看看例子。
示例1
struct BF1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
其內存佈局爲:
|__f1___|____f2___ |__|____f3______|______|
|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|

位域類型爲char,第1個字節僅能容納下f1f2,所以f2被壓縮到第1個字節中,而f3
能從下一個字節開始。因此sizeof(BF1)的結果爲2
示例2
struct BF2
{
char f1 : 3;
short f2 : 4;
char f3 : 5;
};
由於相鄰位域類型不同,在VC6中其sizeof6,在Dev-C++中爲2
示例3
struct BF3
{
char f1 : 3;
char f2;
char f3 : 5;
};
非位域字段穿插在其中,不會產生壓縮,在VC6Dev-C++中得到的大小均爲3



運算符:

優先級:   ! > 算術運算符 > 關係運算符 > && > || > 賦值運算符


寫出下列程序在X86上的運行結果。

struct mybitfields
{
unsigned short a : 4;
unsigned short b : 5;
unsigned short c : 7;
}test;

void main(void)
{
int i;
test.a=2;
test.b=3;
test.c=0;
i=*((short *)&test);
printf("%d ",i);
}

這個題的爲難之處呢,就在於前面定義結構體裏面用到的冒號,如果你能理解這個符號的含義,那麼問題就很好解決了。這裏的冒號相當於分配幾位空間,也即在定義結構體的時候,分配的成員a 4位的空間, b 5位,c 7位,一共是16位,正好兩個字節。下面畫一個簡單的示意:
變量名 位數
test 15 14 13 12 11 10 9 |8 7 6 5 4 |3 2 1 0
test.a | |0 0 1 0
test.b |0 0 0 1 1 |
test.c 0 0 0 0 0 0 0 | |
在執行i=*((short *)&test); 時,取從地址&test開始兩個字節(short佔兩個字節)的內容轉化爲short型數據,即爲0x0032,再轉爲int型爲0x00000032,即50。輸出的結果就是50。當然,這裏還涉及到字節及位的存儲順序問題,後面再說。

前面定義的結構體被稱爲位結構體。所謂位結構體,是一種特殊的結構體,在需要按位訪問字節或字的一個或多個位時,位結構體比按位操作要更方便一些。
位結構體的定義方式如下:
struct [位結構體名]{
數據類型 變量名:整數常數;
...
}位結構變量;
說明:
1)這裏的數據類型只能爲int型(包括signed和unsigned);
2)整數常數必須爲0~15之間的整數,當該常數爲1時,數據類型爲unsigned(顯然嘛,只有一位,咋表示signed?光一符號?沒意義呀);
3)按數據類型變量名:整數常數;方式定義的結構成員稱爲位結構成員,好像也叫位域,在一個位結構體中,可以同時包含位結構成員及普通的結構成員;
4)位結構成員不能是指針或數據,但結構變量可以是指針或數據;
5)位結構體所佔用的位數由各個位結構成員的位數總各決定。如在前面定義的結構體中,一共佔用4+5+7=16位,兩個字節。另外我們看到,在定義位結構成員時,必須指定數據類型,這個數據類型在位結構體佔用多少內存時也起到不少的作用。舉個例子:
struct mybitfieldA{
char a:4;
char b:3;
}testA;

struct mybitfieldB{
short a:4;
short b:3;
}testB;
這裏,testA佔用一個字節,而testB佔用兩個字節。知道原因了吧。在testA中,是以char來定義位域的,char是一個字節的,因此,位域佔用的單位也按字節做單位,也即,如果不滿一個字節的話按一個字節算(未定義的位按零處理)。而在testB中,short爲兩個字節,所以了,不滿兩個字節的都按兩個字節算(未定義位按零處理)

(判斷大端小端模式) 試題1:請寫一個C函數,若處理器是Big_endian的,則返回0;若是Little_endian的,則返回1

解答:

int checkCPU( )

{

    {

           union w

           { 

                  int a;

                  char b;

           } c;

           c.a = 1;

           return(c.b ==1);

    }

}

Big-Endian    一個Word中的高位的Byte放在內存中這個Word區域的低地址處。
Little-Endian  一個Word中的低位的Byte放在內存中這個Word區域的低地址處。

 

3、在C++程序中調用被 C編譯器編譯後的函數,爲什麼要加 extern “C”? (5分)

答:C++語言支持函數重載,C語言不支持函數重載。函數被C++編譯後在庫中的名字與C語言的不同。假設某個函數的原型爲: void foo(int x, int y);

該函數被C編譯器編譯後在庫中的名字爲_foo,而C++編譯器則會產生像_foo_int_int之類的名字。

C++提供了C連接交換指定符號extern“C”來解決名字匹配問題。

 

 

 

 

 

 

C語言易錯題

1、

        int arr[] = {6,7,8,9,10};
        int *ptr=arr;
        *(ptr++) += 123;
        printf("%d,%d\n",*ptr,*(++ptr));
從右到左的順序

     結果爲: 8,8

2、

一語句實現是不否爲2的若干次冪的判斷

      return a&(a-1)?:1:0;  //返回0,則是2的若干次方

3、

There are two int variables: a and b, don’t use “if”, “? :”,“switch”or other judgement statements, find out the bigger one of the two

    參考答案:(a + b + abs(a - b)) / 2

4、

寫出程序的運行結果:
#include <stdio.h>
void main()
{
int *pa = NULL;
int *pb = pa + 15;
printf("%x\n", pb);

}

結果:因爲pa的長度爲4 byte ,所以15 的單位爲4 byte ,pb 的值爲pa 加上 15 * 4 =0x3C。

5、

#include <stdio.h>
int main(void)
{
int i = 3;
int j; 
j = sizeof(++i + ++i);
printf("%d%d\n", i, j); return 0;
}

結果:輸出是3和4,sizeof不是函數,是操作符,有其特殊性。sizeof後面的表達式(++i + ++i)是不會被真正執行的,sizeof的結果在編譯時就確定,所以該表達式的副作用不生效,i 還是原來的值

運算符:

優先級:   ! > 算術運算符 > 關係運算符 > && > || > 賦值運算符

發佈了41 篇原創文章 · 獲贊 36 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章