C語言的指針和數組

指針和內存

指針變量也是個變量,不過保存的是另外一個變量的地址。另外編譯器還會記住指針所指向變量的類型,從而在指針運算時根據變量類型採起不一樣操做。html

例如,char * a 定義了char 類型的指針變量 a,經過 *a 讀取數據時,每次只會讀一個字節(char 類型變量的長度)。而int * i 定義了 int 類型的指針變量 i,經過 *i 讀取數據時,每次會讀兩個或四個字節(int 類型變量的長度跟編譯器平臺有關)。web

#include <stdio.h>

int main()
{
	int a = 666;
	char c = 'a';
	int * p1 = &a; // 至關於(int *) p1,表示 p1 是執行 int 類型的指針
	char * p2;	 // 至關於(char *) p2,表示 p2 是執行 char 類型的指針
	p2 = &c;	// & 符號用於取變量的地址
	printf("address of a is %#x, value of a is %d\n", p1, *p1);
	printf("address of c is %#x, value of c is %c\n", p2, *p2);
}

輸出:編程

address of a is 0x107900bc, value of a is 666
address of c is 0x107900bb, value of c is a

char 型指針顯示多個字節的問題

#include <stdio.h>

int main()
{
	float a = 1.2;
	char * p = (char *)&a; // 這裏的 p 指向有符號字符型變量,符號位爲1時會打印4個字節
	printf("%#x\n", *p);
}

輸出:數組

0xffffff9a

要解決這個問題,把上面的 char * p = &a; 變成 unsigned char * p = &a; 便可。bash

指針類型轉換

有時咱們須要用 char * 按照字節大小讀取數據,可是非 char 類型的指針當作 char 指針處理時會報錯或警告。這時須要強制類型轉換:網絡

#include <stdio.h>

int main()
{
	int a = 0x77777777;
	char * p = (char *)&a;
	printf("%#x\n", *p);
}

段錯誤

指針操做若有不慎,會常常看到 Segmentation Fault 段錯誤。這是由於指針指向了非法的內存,例以下面的代碼執行的內存地址,操做系統是不容許訪問的:svg

#include <stdio.h>

int main()
{
	int a = 0x12345678;
	int * p = &a;
	p = 0x00000001;
	printf("address of a is %#x, value of a is %d\n", p1, *p1);
}

內存除了用於存放程序運行時的數據外,還有一部份內存用於操做硬件。例如內存的某一段連續空間用於映射顯存、I2C、USB 設備等。函數

大端存儲、小端存儲

對於單字節的 char 類型變量不存在這個問題。但多字節的變量,高字節存儲在內存的高地址仍是低地址,決定了採用哪一種存儲方式。優化

  • 大端模式 Big-Endian:低地址存放高位,相似於把數據看成字符串順序處理:地址由小向大增長,而數據從高位往低位放;和閱讀習慣一致。
  • 小端模式 Little-Endian:低地址存放低位,將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低。
#include <stdio.h>

int main()
{
	int a = 0x12345678;
	unsigned char * p1 = &a;
	printf("address of a is %#x, value of a is %#x\n", p1, *p1);
}

上面代碼在 32 位平臺上運行時,經過 char 類型的指針只讀取第一個字節,若是輸出 78 表示是小端存儲(低地址存放低位),不然是大端存儲。輸出:ui

address of a is 0xbb483e84, value of a is 0x78

目前Intel的80x86系列芯片是惟一還在堅持使用小端的芯片,ARM芯片默認採用小端,但能夠切換爲大端。另外,對於大小端的處理也和編譯器的實現有關,在C語言中,默認是小端(但在一些對於單片機的實現中倒是基於大端,好比Keil 51C),Java是平臺無關的,默認是大端。在網絡上傳輸數據廣泛採用的都是大端。

指針的修飾符

const

C 語言的 const 比較弱,很容易繞過去,例如經過指針。const 修飾的變量仍然存儲在讀寫區,而非只讀區。

  • 指針能夠指向任意變量,可是不可經過指針修改變量值。兩種寫法:
const char * p; // 推薦使用,至關於 const ((char *) p)
char const * p; // 不推薦
  • 指針只能指向指定的變量,可是變量值能夠任意修改。兩種寫法:
char * const p; // 推薦使用,至關於 (char *) (const p)
char * p const; // 不推薦
  • 指針只能指向指定的變量,且不可經過指針修改變量值
const char * const p; // 至關於 const ((char *) (const p))

綜合示例:

#include <stdio.h>

int main()
{
	char * str1 = "hello\n"; // C 語言中字符串不可修改
	char str2 [] = {"hello\n"};// 數組能夠修改

	//str1[0] = 'a'; // str1 修改會致使 segmentation fault
	str2[0] = 'a';
	printf("%s\n", str2);
}

上面代碼中,字符串是不可修改的,因此能夠用 const 限制,若是有代碼則會在編譯時報錯:

int main()
{
	char * str1 = "hello\n"; // C 語言中字符串不可修改
	const char str2 [] = {"hello\n"};// 數組能夠修改

	str1[0] = 'a'; // str1 修改會致使 segmentation fault
	str2[0] = 'a';
	printf("%s\n", str2);
}

編譯報錯:

/code/main.c: In function ‘main’:
/code/main.c:9:2: error: assignment of read-only location ‘str2[0]’
 str2[0] = 'a';

const 變量的繞過(越界)

#include <stdio.h>

int main()
{
	int a = 0x66667777;
	int b = 0x11111111;
	int *p = &b;
	*(p+1) = 0xffffffff;
	printf("%#x\n", a);
}

volatile

編譯器默認的優化是開啓的。但有時候咱們操做的內存是映射到硬件的,此時可能須要關閉優化。

volatile char * p = 0x20;
while (*p == 0x20) ...

typedef

指針能夠指向任意類型的資源,例如 int、char、數組、函數。指定簡明易讀的別名能夠提升代碼可讀性。

char * name_t;
typedef char * name_t;
name_t myVar;

指針的運算符

++、–、+、-

指針的加減操做,跟指針指向變量的具體類型有關。指針指向的變量佔幾個字節,指針每次加減一就是加減幾個字節,確保恰好能夠指向下一個同類型元素。

#include <stdio.h>

int main()
{
	const char *p = {"hello\n"};
	int *s = p;
	printf("%c, %c, %c, %c, %#x\n", *p, *(p+1), *(p+2), *(p+3), *s);
}

[]

在數組中,保存的是相同類型的元素。經過下標能夠訪問到每個元素,不須要咱們在編程的時候關係元素佔幾個字節。這跟指針的加減運算是同樣的。p[0] 等價於 *p,p[1] 等價於 *(p+1),以此類推:

#include <stdio.h>

int main()
{
	const char *p = {"hello\n"};
	printf("%c, %c, %c, %c\n", *p, p[0], *(p+1), p[1]);
}

指針的邏輯運算

指針能夠進行比較,>= 、<= 、== 、!= 四種。

  • 跟特殊值 0x0 或 NULL 這個無效地址進行比較,相等則表示結束。
  • 必須是同類型的指針,比較時纔有意義。

多級指針

經常使用的是二維指針,二維以上基本上不用。

當在內存中有多個離散的變量時,爲了放在一個變量中統一訪問,就須要把這個用做訪問入口的統一變量設計爲數組,數組中的每一個元素都是指針,執行原始變量。
二維指針
語法的簡單示例:
int 變量int a;
← int 變量的指針int * p = &a;
← int 變量的指針的指針int **p2 = &p;

bash 終端能夠在命令後面帶參數,編譯器會把全部參數彙總到 main 函數的參數中:

#include <stdio.h>

int main(int argc, char ** argv)
{
	int i;
	for (i = 0; i < argc; i++) {
		printf("argv[%d] is: %s\n", i, argv[i]);
	}
	
	i = 0;
	while(argv[i] != NULL) {
		printf("argv[%d] is: %s\n", i, argv[i++]);
	}
	return 0;
}
# ./build 666 hello world !
argv[0] is: ./build
argv[1] is: 666
argv[2] is: hello
argv[3] is: world
argv[4] is: !
argv[1] is: ./build
argv[2] is: 666
argv[3] is: hello
argv[4] is: world
argv[5] is: !

數組

數組的內存操做

數組是地址操做的一種形式,使用的時候跟指針幾乎同樣。經過數組分配的內存空間的特性以下:

  • 大小:在定義的時候指定,能夠經過 malloc 分配,也能夠經過元素的類型及個數 int[10] 這種形式分配
  • 讀取方式:經過數組中的元素類型肯定。例如 char 類型的數組,每次讀取 1 個字節
int a[10]; // 分配 4*10Byte 的內存,a 是指向這個內存的標籤,不可變,不是指針

C 語言只有指針的概念,並無真正意義的數組,因此在用指針操做數組時,須要注意:不要越界。

#include <stdio.h>

int main(int argc, char ** argv)
{
	char a[] = {"hello\n"};
	char * p = {"hello\n"};
	printf("a is: %s\n", a);
	printf("p is: %s\n", p);
	//a = "hello"; // a 是標籤,數組不可變,不然編譯報錯
	p = "world\n";			// p 是指針,能夠變
	printf("a is: %s\n", a);
	printf("p is: %s\n", p);
}

字符空間和非字符空間

關於char、unsigned char 和 signed char 三種類型直接的差異,能夠參考:http://bbs.chinaunix.net/thread-889260-1-1.html

內存中的數據空間能夠分爲兩類:

  • 字符空間:存儲的數據是可讀的字符串,以 \0 結束。用 char 來表示,例如 char a[10];。用 strcpy 複製數據,複製時以 \0 結束,或者用 strncpy 複製。
  • 非字符空間:存儲的是二進制數據,不可讀。用 unsigned char 來表示,例如 unsigned char b[10]。用 memcpy 複製數據,複製時須要指定字節個數
int buf[10];
int source[1000];

memcpy(buf, source, 10*sizeof(int));

數組的初始化

注意:C 語言中只有字符串常量。由於 C 語言沒有字符串變量的概念,若是想修改字符串的值,必須將字符串存儲爲字符數組。全部字符串都以 \0 結尾。

  • 聲明數組時,同時賦值一個內存空間:
    C 語言自己不支持空間賦值,一般是編譯器自動對這種賦值轉換爲逐個元素賦值,能夠反彙編查看一下。
char a[] = "hello\n"; // C 編譯器看到雙引號時,自動在末尾加 \0
char b[10] = {'h', 'e', 'l', 'l', 'o', '\n', '\0'}; // 未賦值的元素默認是0
char c[] = {"hello\n"}; // 由於雙引號和大括號都用來劃分存儲空間,可省略大括號
int i[] = {12, 23, 666};
  • 聲明數組後,逐個元素賦值:
char a[10];
a[0] = 'h';
a[1] = 'e';
...

字符串數組和字符串指針的差別

字符串是 C 語言中須要特別注意的地方。字符串常量賦值到數組時,實際上會先建立一個數組變量,而後依次把每一個字符拷貝到這個數組中,數組指向的變量跟字符串常量無關,能夠修改。但字符串賦值到指針時,指針指向的就是這個字符串常量,此時指針指向的值不可修改。

char a[10] = {"hello"}; // 內存中分配了一個字符串常量空間和一個字符串變量空間,變量 a 指向這個變量空間,能夠修改空間中的元素
a[2] = 'w'; // OK

char *p = "hello"; // 內存中只有一個字符串常量空間和一個指向該常量的指針變量,指針變量 p 指向常量,不可修改
p[2] = 'w'; // 報錯 segmentation fault

數組名是個標籤,不可賦值

C 語言中,數組中的每一個元素能夠修改,可是不可直接對數組名進行賦值。若是想再次賦值,只能逐個元素賦值。

int a[] = {2, 5, 6};
a = {3, 5}; // 編譯報錯,數組名相似函數名,是個常量標籤,不可賦值

內存空間拷貝函數

內存空間逐一賦值操做很常見,因此 C 語言將其封裝爲字符串拷貝函數。能夠在 Linux 下經過 man 3 strcpy 之類的命令查看函數定義。

strcpy 函數

strcpy 函數碰到 0 就中止拷貝。若是源字符串太長,strcpy 可能致使內存泄漏,通常不用。函數原型以下:
char *strcpy(char *dest, const char *src);

char a[] = "666";
strcpy(a, "hello world");

strncpy 函數

strncpy 函數能夠限制拷貝的數量,防止發生越界。
char *strcpy(char *dest, const char *src, size_t n);

指針數組

數組中存在指針,構成指針數組。指針數組就是二級指針。

int *a[10]; // 開闢 10 個空間存放數組 a,a 中放 (int *) 類型的指針
int **a; // ((int *) *) a

將數組名保存爲指針

C 語言中,一維數組的數組名變量中放的就是數組首元素的地址,能夠直接賦值給指針,並用這個指針訪問數組中的元素。但二維數組跟二維指針沒有任何關係。

下面例子會報錯,p2 指向指針數組,但 b 指向兩個連續的內存塊,每塊內存由 5 個 int 類型變量組成

#include <stdio.h>

int main()
{
	int a[10]; // a 是數組標籤,表示一塊由 10 個 int 元素組成的空間
	int b[2][5]; // b 是數組標籤,表示兩塊空間,各由 5 個 int 元素組成

	int *p1 = a;
	int **p2 = b; // 這一行會報錯
	int *p4 [5] = b; // 這一行會報錯,這裏 p4 是數組,其中的每個元素都是 int 類型的指針
	int (*p3)[5] = b; // 正常編譯,這裏 p3 是指針,指向一塊由 5 個 int 元素組成的空間
	
	printf("%d\n", a[5]);
	printf("%d\n", b[1][1]);
	printf("%d\n", p3[1][1]);
}

對於三維數組 int a[2][3][4];,能夠用指針表示:

int (*p) [3][4];
相關文章
相關標籤/搜索