C指標與記憶體
指標是C / C++ 中重要的構造型別,指標賦予了C / C++程式直接訪問和修改記憶體的能力。C / C++的許多重要應用,如編譯、OS、嵌入式開發都依賴於這種能力。
馮諾依曼體系的計算機記憶體儲存指令和資料,我們可以將其抽象為指令區和資料區(當然實際情況要複雜得多)。資料區中包含棧(stack)和堆(heap)區,棧區的資料由編譯器管理而堆區資料則由程式設計師管理。
由於指令同樣存在於記憶體中,那麼函式在記憶體中也會擁有地址(指標)。 函式指標相對於函式名來說 可以在執行期動態地選擇呼叫的函式,這一特性是C++實現動態多型性的重要基礎。
一、 指標的基本用法
指標是一種構造型別,所有的基本型別和struct等都有自己的指標。指標包含首地址(即指標的值)和指向物件的型別兩部分資訊,所以指標並不嚴格等價於地址。
不要使用未初始化的指標否則將導致嚴重的執行時錯誤,所幸作業系統的記憶體管理可以保證作業系統自身和其它程式的穩定,但你的程式肯定會異常退出了。指標的型別是重要的,指向struct的指標初始化後可以使用ptr -> member表示式來訪問某一個成員,但是未經初始化或void 指標將導致錯誤或警告。
在《C程式設計基礎》中提到,方括號([]),指標(*)這些符號在宣告語句和執行語句中的含義有所不同,但依舊具有運算子的一些特性。
即指標(*),方括號([])以及函式呼叫的(參數列)在宣告語句中 可以認為是將一個識別符號標記為特殊型別的標誌,當同一個宣告語句中存在多個標誌時,它們按照表示式求解的順序決定宣告的型別:
(1)基本宣告:
int *p; 宣告指標p(注意不是*p),*p表示其指向的資料。
int p[M]; 宣告長為M的一維陣列p。
int p[M][M]; 宣告M×M二維陣列p。
(2)複合宣告:
int **p 宣告p為二重指標(不是**p),則 *p為一重指標,**p為資料物件。
int *p[M] 宣告指標陣列,初等運算子從右向左結合首先宣告一個陣列,然後宣告陣列元素型別為指標。
int *fun(...) 宣告返回指標的函式,從右向左首先宣告fun為函式,然後宣告返回值型別為指標。
int (*ptr) (...) 宣告ptr為函式指標,從右向左首先宣告這是一個函式,(*ptr)宣告這是一個函式指標。使用函式指標時只需以(*ptr)代替函式名即可,例:
qsort庫函式使用函式指標作謂詞函式
#include<stdio.h> #include<stdlib.h> int tell(const void *a,const void *b) { int x, y; x = *(int *)a; y = *(int *)b; return x - y; } int main(void) { int i, m, a[256] = { 0 }; int (*tp) (const void *a, void *b); tp = tell; scanf("%d", &m); for (i = 0; i < m; i++) { scanf("%d", &a[i]); } qsort (a, m,sizeof(int ),tp); for (i=0;i<m;i++) { printf("%d ",a[i]); } printf("\n"); return 0; }
指標作為函式引數則可以實現傳址呼叫,下面給出一個最簡單的交換兩個數的程式:
#include<stdio.h> int swap(int *pa, int *pb) { int t; t = *pa; // *pa is "a" itself *pa = *pb; *pb = t; return 0; } int main(void) { int a,b; scanf("%d %d",&a,&b); swap(&a,&b); // using &a as a ptr to "a" printf("%d %d\n",a,b); return 0; }
·常指標
const int * ptr; int const * ptr; 指向常物件的指標
int * const ptr = &a; 指向不能改變的指標,必須初始化
const int * const ptr = &a; int const * const ptr;
指向與物件均不能改變的指標;
以*為標誌,靠近基本型別的為指向常物件,靠近指標名為指向不變。
二、 陣列及其儲存
1.一維陣列
上文已經說明陣列的宣告方式:
(1)宣告一個長度為8的int陣列arr int arr[8];
(2)陣列的長度必須為常正整數,不能是const物件【const int n = 8;int arr[n];】;可以是列舉元素但不能是列舉物件,通常使用字面值或巨集來作為長度 #define N 10 ;
(3)陣列下標從0開始(不是1);
(4)方括號([])可以訪問陣列的某個成員, arr[i];arr[0]; 。
在函式內定義的陣列不會自動初始化,在函式外定義的陣列將自動初始化為0(或等價值)。在函式內定義陣列同時初始化時,未初始化變數將自動初始為0值 int arr[8] = {1,2,3}; 常用這個特性將陣列全部初始化為0值, int arr[8] = {0}; 。將所有元素初始化時不需要指定陣列長度,即 int arr[] = {1,2}; 與 int arr[2] = {1,2} 等價。
2.高維陣列
我們可以將高維陣列理解為陣列的陣列,即高維陣列的元素是低一維的陣列。因為陣列就名就是其首地址,那麼,高維陣列的元素是低維陣列的首地址。以二維陣列為例,利用a[i]<=>*(a+i)逐級展開
a[i][j] <=> *(a[i]+j) <=> *(*(a+i)+j)
i的單位為一維陣列的長度,j的單位為基本型別的長度。推導時,將[]展開即可,加一個&就是少一個*。
*(a+i)+j <=> a[i]+j <=> &a[i][j]
高維陣列的初始化有兩種形式。首先將其元素理解為陣列,每一個陣列的初值用一個花括號括起,把它們當做一個元素用另一個大括號括起。高維陣列在記憶體中是線性儲存的,可以根據它在記憶體中的儲存順序當做一維陣列進行初始化。這兩種方式下,沒有被賦值的元素均被自動賦0。在對所有元素賦值時,第一維的長度可以省略由編譯系統求出,其它維數不可省略。
3.
陣列是一段連續的記憶體空間,陣列名可以認為是常指標(不允許更改指向)。從陣列實現來看,下標從0開始是非常自然的。
C對下標的處理方法的處理方法就是將它轉化為地址,a[i]與*(a+i)無條件等價。
三、動態記憶體管理
1.分配記憶體(memory alloc):
void *malloc(unsigned int size); (常用)
(1) 在記憶體中分配一個大小為size的連續空間,引數size為無符號整型(不允許為負數)。函式的返回值是所分配區域第一個位元組的地址。如果函式不能成功地執行(如記憶體空間不足)則返回空指標(NULL)。
(2) void指標型別不能理解為指向任何型別,而應理解為指向不確定型別的資料的指標。應用指派運算子(強制轉換)將void指標變為具體的型別指標,size引數應用sizeof()運算子求得以避免錯誤,如結構體的大小不嚴格等於所有成員大小之和並提高程式可移植性。例如:
int * ptr = (int *)malloc( sizeof(int)* N );
宣告ptr指向一段長度為N連續int型空間(實際上是一個長度為N的int陣列)。
2.分配連續空間:
void *calloc(unsigned n,unsigned size);
在動態儲存區分配n個長度為size的連續空間,可以用來儲存一個陣列。
3.釋放記憶體(free):
void free(void *p);
釋放指標變數p所指向的動態空間,無返回值。例如: free(ptr);
4.重新分配記憶體(realloc):
void *realloc(void *p,unsigned int size);
將p指向的動態空間大小改變為size。