C語言中指標, 陣列和字串(Pointer, Array and String in C Programming Language)

cocoonyang發表於2018-01-03

指標

在C語言中,指標是一種衍生型別(derived type).  一個指標就是一個儲存某個物件或函式的地址的變數("A pointer is a variable that contains the address of a variable")[10](p93).  例如:

int* pa;

其中pa是一個指向整型數的指標,整型數是pa的基礎型別(referenced type) . 常量指標的宣告格式如下所示:

float * const const_pointer_a

const_pointer_a是一個指向整型數的常量指標, 其中的const是修飾const_pointer_a的。下面這行程式碼

const float * pointer_const_a

pointer_const_a是一個指向常量整型數的指標, 其中的const是修飾float的. 又如:

struct tag (*[5])(float)

是一種包含5個函式指標的陣列,其中函式指標的基礎型別是一種以一個float做引數,返回值為名為struct tag的資料結構.


 陣列

在C語言中,宣告一個陣列:

int a[10]; 

其相應的彙編程式碼(VC2015)是:  

COMM	_a:DWORD:0aH

其中“0aH”即陣列長度10(16進製為0a)

有以下含義:

1. 宣告瞭一個名為a的陣列;  (a是一個陣列的名字)
2. 為陣列a在記憶體空間申請一塊連續的記憶體塊; 
3. 這個記憶體塊能儲存10個int型變數, 這些int型變數名分別為a[0], a[1], ... , a[9] ;
4. 陣列名a不是變數(不能當作l-value被賦值),它代表的是包含多個變數的一個陣列。(在計算機機器程式碼實現C語言中定義的陣列結構時,不能將儲存陣列的記憶體塊像整型數那樣當作一個整體在計算機儲存空間和CPU之間複製過來,拷貝過去。因此在具體實現中儲存陣列中第一個變數的地址儲存給陣列名a)


如果在陣列宣告中沒有指明陣列長度,會引發編譯錯誤(在VC2015中, 直接將這行程式碼忽略掉)。例如:

int array[]; //這行程式碼直接被編譯器優化掉了,如果程式後面的程式碼中使用變數array,會引發編譯錯誤。

C語言中, 陣列和指標的關係十分密切。 C語言程式執行時, 任何使用陣列下標實現的操作都可以通過指標實現,而且通常使用指標耗時更少[10]p97 ("Any operation that can be achieved by array subscripting can also be done with pointers. The pointer version will in general be faster (at least to the uninitiated)" )。 


字串

C語言的基本資料型別中沒有字串。 C語言中使用字元陣列儲存字串,null('\0')字元表示字串的結束。也就是說在C語言中字串是以一個以null('\0')字元結尾的字元陣列。例如:

char label[] = "Single";
在記憶體中的儲存形式如下[1]:
------------------------------
| S | i | n | g | l | e | \0 |
------------------------------

其相應的彙編程式碼(VC2015)是:  

_label	DB	'Single', 00H

null('\0')是C語言內定的字串結尾標識, 因此字串是不應包含null('\0')字元。

C語言中提供字串常量,例如:

char str[] = "Single";
char *message = "Single";
其相應的彙編程式碼(VC2015)是:  
PUBLIC	_message
_DATA	SEGMENT
_str	DB	'Single', 00H
	ORG $+1
_message DD	FLAT:$SG4519
$SG4519	DB	'Single', 00H
_DATA	ENDS
使用字串常量會產生一個指向字串的常量指標,如上述程式碼中的mesage實際上是一個指標(就是$SG4519)。C語言的發明人Brian W. Kernighan 和 Dennis M. Ritchie[10] 沒有提及字串常量中的字元是否可以被修改. C語言標準(ANSI C)則明確宣告修改字串常量的效果是未定義的。 


指標、陣列和字串之間的關係

指標、陣列和字串之間的關係如下圖所示:陣列儲存在計算機中一塊連續的記憶體段中;字串是一個以null('\0')字元結尾的字元陣列;對陣列元素的操作是通過指標實現的。



陣列->指標


當陣列名作為引數傳遞給某一函式時,由於C語言是值傳遞,因此實際傳遞給函式的是陣列第一個元素的地址。在C語言中,函式的引數在呼叫函式時是作為區域性變數使用,陣列名引數實際上就當作一個儲存某變數地址的區域性變數(也就是指標)使用[10]p99。因此在函式中,使用操作符sizeof()作用於陣列名函式引數得不到其陣列長度, 實際上得到的是指標變數長度

例如:

void foo( int pVar[] )
{
    int tmp = sizeof(pVar);
    return;
}
其相應的彙編程式碼(VC2015)是:  
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.00.23506.0 
;
; ...

_TEXT	SEGMENT
_tmp$ = -4						; size = 4
_pVar$ = 8						; size = 4
_foo	PROC
; Line  
	push	ebp
	mov	ebp, esp
	push	ecx
; Line  
	mov	DWORD PTR _tmp$[ebp], 4
; Line  
	mov	esp, ebp
	pop	ebp
	ret	0
_foo	ENDP
_TEXT	ENDS


sizeof和strlen

sizeof 在程式碼中看著像一個函式,但實際上在C語言和C++語言中sizeof是一個操作符。 sizeof() 在編譯期間就直接處理了。例如:

int array[2];

void main()
{
    int len_1   = 8;
    int len_2 = sizeof(array);
    return;
}

將上述程式碼儲存到檔案foo.c中。在VC2015編譯環境中,啟動Developer Command Prompt for VS2015,執行

cl.exe /FA foo.c
得到彙編程式碼檔案foo.asm。 開啟foo.asm(使用記事本即可),其中有兩行彙編程式碼是
; Line 5
	mov	DWORD PTR _len_1$[ebp], 8
; Line 6
	mov	DWORD PTR _len_2$[ebp], 8

可以看到  sizeof()在編譯期間就直接轉換相應的整數。

C語言strlen(s)方法是利用字串結尾字元null('\0')來計算字串的長度。例如:
char label[10] = "Single";

其相應的彙編程式碼(VC2015)是:  

_label	DB	'Single', 00H
	ORG $+3
	ORG $+2

其含義是:

1. 宣告瞭一個變數 label; 
2. 為變數label在記憶體空間申請一塊能容納10個字元變數的連續記憶體塊, 變數名分別為label[0], label[1], ... , label[9] ; 
3. 將記憶體塊的首地址賦值給變數label; 
4. 將S,i,n,g,l,e依次複製到變數label[0],...label[5]。
5. 將"\0"複製到變數label[6]

在記憶體中的儲存形式如下[1]:

------------------------------------------
| S | i | n | g | l | e | \0 |   |   |   |
------------------------------------------
最後3個位元組沒有用上,仍屬於字元陣列,但是卻不屬於字串。
	char label[10] = "Single";

	printf("len = %d\n", strlen(label) );
	printf("size = %d\n", sizeof(label) );

strlen(label) 在統計字串label的長度時是從label[0]開始,依次遍歷每一個字元,直到遇到"\0"為止。 strlen(label)的值是6。

sizeof(label) 在程式碼編譯期間就直接替換成 10。因此程式碼執行結果是: 

len = 6 
size = 10

對於字串,

	char* label = "Single";

	printf("len = %d\n", strlen(label) );
	printf("size = %d\n", sizeof(label) );

其中 指標變數宣告語句相應的彙編程式碼是

_label	DD	FLAT:$SG4518
$SG4518	DB	'Single', 00H
執行結果是:
len = 6 
size = 7

初始化

字元陣列和字串差別不僅僅只有以上幾點,它們不同初始化方法, 所分配的記憶體位於程式記憶體空間的不同區域. 例如:

	char *c = "abc";
	c[1] = 'a';

在VC2010編譯環境中能通過編譯,程式執行時卻崩潰了。在VC2015編譯環境中,不能通過編譯。

下面的程式碼一切正常。

	char c[]="abc";
	c[1] = 'a';

在多工作業系統中的每一個程式都執行在一個屬於它自己的記憶體沙盤中。這個沙盤就是虛擬地址空間(virtual address space),在32位模式下它總是一個4GB的記憶體地址塊。這些虛擬地址通過頁表(page table)對映到實體記憶體,頁表由作業系統維護並被處理器引用[2][3]。下圖是一個Linux程式的標準的記憶體段佈局[2]:



程式程式使用的記憶體一般分為 程式碼段(Code or Text),只讀資料段(RO Data),已初始化資料段(RW Data),未初始化資料段(RSS),堆(heap)和棧(stack). 程式碼段、只讀資料段、讀寫資料段、未初始化資料段屬於靜態區域,而堆和棧屬於動態區域[4]。 字串和字元陣列的不同初始化方法,它們所分配的記憶體位於程式記憶體空間的不同區域。 例如[4]:

const char ro[] = { "this is read only data" }; //只讀資料區
static char rw_1[] = { "this is global read write data" }; //已初始化讀寫資料段
char BSS_1[100]; //未初始化資料段
const char *ptrconst = "constant data"; //字串放在只讀取資料段

int main()
{
	short b; //在棧上,佔用2個位元組

	char a[100]; //在棧上開闢100個位元組, 它的值是其首地址

	char s[] = "abcdefg"; //s在棧上,佔用4個位元組,"abcdefg"本身放置在只讀資料儲存區,佔8個位元組

	char *p1; //p1在棧上,佔用4個位元組

	char *p2 = "123456"; //p2 在棧上,p2指向的內容不能改,“123456”在只讀資料區

	static char rw_2[] = { "this is local read write data" }; //區域性已初始化讀寫資料段

	static char BSS_2[100]; //區域性未初始化資料段

	static int c = 0; //全域性(靜態)初始化區

	p1 = (char *) malloc(10 * sizeof(char)); //分配記憶體區域在堆區

	strcpy(p1, "xxxx"); //“XXXX”放在只讀資料區,佔5個位元組

	free(p1); //使用free釋放p1所指向的記憶體

	return 0;
}



棧區(stack)記憶體—由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。 棧區記憶體可用於儲存 函式內部的動態變數,函式的引數和函式的返回值。在函式呼叫時,第一個進棧的是主函式中後的下一條指令(函式呼叫語句的下一條可執行語句)的地址,然後是函式的各個引數,在大多數的C編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的[5]。


堆區(heap)記憶體的分配和釋放是由程式設計師所控制的,程式結束時由作業系統回收。 使用方法:C中是malloc函式,C++中是new識別符號[5].


由於程式記憶體空間各儲存區域功能的差別,不同記憶體區域所允許的操作不同。在程式設計時,如不注意它們的差別,會引發編譯或執行錯誤,例如:

int main(){
    char *pa = "Hello, world."; 
    return 1;
}
文字字串"Hello, world."儲存在程式碼區,不可修改[7]。字元指標pa儲存在棧區,可以修改。但是如果試圖通過pa修改字串內容,程式碼編譯正常,但程式執行時會引發異常[6], 例如:
int main(){
    char *pa;
    pa = "Hello, world.";
    pa[2]='a';
    return 1;
}
為了避免這類執行異常,可把pa看成指向常量的指標,將程式碼改寫成:

const char *pa  = "Hello, world.";
這樣如果試圖修改pa的內容,在編譯時即可報錯:
you cannot assign to a variable that is const

如果使用字元陣列,例如:

char c[] = "abc";
是在棧頂分配4個位元組,分別在這四個位元組存放'a','b','c','\0'。 棧區記憶體允許修改,因此上述修改字元陣列的內容的程式碼編譯執行正常。


初始化:

如果在初始化char array時字串的長度大於字元陣列宣告的長度,例如
	char label[10] = "SingleSingle123213213121";

	printf("len = %d\n", strlen(label) );
	printf("size = %d\n", sizeof(label) );
編譯時會觸發"array bounds overflow"錯誤


char array在單獨宣告時必須要使用常量指定陣列長度,例如:

const int len = 10;
        //...
	char label_array[len];
如果不指定陣列長度
char label_array[];
編譯時會觸發"unkonw size" 錯誤


如果使用變數,
	int len = 10;
	char label_array[len];
編譯時會觸發 "error C2131: expression did not evaluate to a constant note: failure was caused by non-constant arguments or reference to a non-constant symbol"



下面是用於測試字元陣列和字串之間差別的程式碼
//
//  Char* operation
//

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


/*從字串的左邊擷取n個字元*/
char * left(char *dst,char *src, int n)
{
    char *p = src;
    char *q = dst;
    int len = strlen(src);
    if(n>len) n = len;
    /*p += (len-n);*/   /*從右邊第n個字元開始*/
    while(n--) *(q++) = *(p++);
    *(q++)='\0'; /*有必要嗎?很有必要*/
    return dst;
}


/*從字串的中間擷取n個字元*/
char * mid(char *dst,char *src, int n,int m) /*n為長度,m為位置*/
{
    char *p = src;
    char *q = dst;
    int len = strlen(src);
    if(n>len) n = len-m;    /*從第m個到最後*/
    if(m<0) m=0;    /*從第一個開始*/
    if(m>len) return NULL;
    p += m;
    while(n--) *(q++) = *(p++);
    *(q++)='\0'; /*有必要嗎?很有必要*/
    return dst;
}


struct Card{
	char*  _pData = NULL;
	int _fieldNumber;
	int _fieldSize;
	bool _alive;

	void show()
	{
		if (NULL != _pData)
		{
			printf("data is: %s \n", _pData);
		}else{
			printf("data pointer is NULL. \n" );
		}
		return;
	}

	char* get( int index ){
//		char result[9];
//		int startLocation = (index*_fieldSize + 8) - 8;
//		mid(result, _pData, _fieldSize, startLocation );
//		return result;
		//memset(buffer,9,sizeof(buffer))

		if (NULL == _pData)
		{
			printf("data pointer is NULL. \n" );
			return NULL;
		}

		char* result =  (char *)malloc(_fieldSize+1);
		int startLocation = (index*_fieldSize + 8) - 8;
		mid(result, _pData, _fieldSize, startLocation );
		return result;
	}

	void pushEnetry( char* entryData )
	{
		 int entryLen = strlen(entryData);
		 int len = 0;

		if (NULL != _pData)
		{
			len = strlen(_pData);
		}

		 printf("pushEnetry: len = %d \n", len);

		 char *theEntry = (char *)malloc((_fieldSize)* sizeof(char));
		 {

		 }

		 int newLen = len + entryLen;

		 char* newpData =  (char *)malloc((newLen+1)* sizeof(char));

		char *p = entryData;
		char *q = newpData;
		char* oldData = _pData;
		 while( len-- )
		 {
			 *(q++) = *(oldData++);
		 }

		 while( entryLen-- )
		 {
			 *(q++) = *(p++);
		 }
		 *(q++)='\0';

		 printf("newpData = %s \n", newpData);



//		 memcpy(newpData, _pData, len * sizeof(char));
//
//		 //memcpy(&newpData[len], entryData, entryLen * sizeof(char));
//
//
//		 newpData += len;
//
//
//		  newpData[newLen]='\0';

		if (NULL != _pData)
		{
			free(_pData);
		}

		 _pData = newpData;

	}
};


void test_1()
{
	char* text = "1234567890abcdefghijklmn";
	printf("string is: %s \n", text );

    char* result_1 =  (char *)malloc(8);   // = new char[8];
    mid(result_1, text, 7, 5 );
	printf("string is: %s\n", result_1 );
}

void test_2()
{
	Card myCard;

	char* text = "1234567890abcdefghijklmn";
	//myCard._pData = text;
	myCard._fieldSize = 8;
	myCard.show();


	char* result;
	result = myCard.get(0);
	if (NULL != result)
	{
		printf("string is: %s\n", result);
	}

	char* foo = "foo";
	myCard.pushEnetry(foo);

	// myCard.show();

	result = myCard.get(0);
	if (NULL != result)
	{
		printf("string is: %s\n", result);
	}

	return;

}


void test_3(char pa[])
{
	printf("test_3: len = %d\n", strlen(pa) );
	printf("test_3: size = %d\n", sizeof(pa) );
	return;
}



void printArray(int data[], int length)
{
//    for(int i(0); i < length; ++i)
//    {
//        std::cout << data[i] << ' ';
//    }
//    std::cout << std::endl;
}


const int len = 10;
int main()
{
	test_1();
	test_2();
	return 0;
}


[8]對字元陣列和字串的差別做了較為詳細的解釋:

"Okay, I'm going to have to assume that you mean SIGSEGV (segmentation fault) is firing in malloc. This is usually caused by heap corruption. Heap corruption, that itself does not cause a segmentation fault, is usually the result of an array access outside of the array's bounds. This is usually nowhere near the point where you call malloc."


"malloc stores a small header of information "in front of" the memory block that it returns to you. This information usually contains the size of the block and a pointer to the next block. Needless to say, changing either of these will cause problems. Usually, the next-block pointer is changed to an invalid address, and the next time malloc is called, it eventually dereferences the bad pointer and segmentation faults. Or it doesn't and starts interpreting random memory as part of the heap. Eventually its luck runs out."[8]

"Note that free can have the same thing happen, if the block being released or the free block list is messed up."[8]

"How you catch this kind of error depends entirely on how you access the memory that malloc returns. A malloc of a single struct usually isn't a problem; it's malloc of arrays that usually gets you. Using a negative (-1 or -2) index will usually give you the block header for your current block, and indexing past the array end can give you the header of the next block. Both are valid memory locations, so there will be no segmentation fault."[8]

"So the first thing to try is range checking. You mention that this appeared at the customer's site; maybe it's because the data set they are working with is much larger, or that the input data is corrupt (e.g. it says to allocate 100 elements and then initializes 101), or they are performing things in a different order (which hides the bug in your in-house testing), or doing something you haven't tested. It's hard to say without more specifics. You should consider writing something to sanity check your input data."[8]

 

References:

[1] https://www.cs.bu.edu/teaching/cpp/string/array-vs-ptr/
[2] http://duartes.org/gustavo/blog/comments/anatomy.html
[3]http://www.cnblogs.com/lancidie/archive/2011/06/26/2090547.html
[4] http://jingyan.baidu.com/article/4665065864601ff549e5f8a9.html
[5] http://blog.csdn.net/codingkid/article/details/6858395
[6] http://www.cnblogs.com/nzbbody/p/3553222.html
[7] http://www.cnblogs.com/dejavu/archive/2012/08/13/2627498.html   
[8] http://stackoverflow.com/questions/7480655/how-to-troubleshoot-crashes-in-malloc
[9] http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/
[10] Brian W. Kernighan, Dennis M. Ritchie. C 程式設計語言(2nd version). 清華大學出版社, Prentice Hall, 1997年. 
[11] International standard of programming language C. April 12, 2011.




相關文章