地址和指针的概念:
内存区的每一个字节有一个编号,这就是“地址”。如果在程序中定义了一个变量,对程序进行编译时,系统就会给这个变量分配内存单元。
在C语言中,对变量的访问有两种方式:
1)直接访问 例如:a = 6;
2)间接访问 例如:scanf("%d", &a);
调用函数时,把变量a的地址传给函数scanf,函数首先把该地址保存到一个单元中,然后把从键盘接收的数据通过所存储的地址保存到a变量中。
1 /* 2 *:取值操作符; 3 &:取址操作符 4 */ 5 int i = 66; 6 int *pointer; // 注意:此处*,不是取值,而是定义指针变量!! 7 pointer = &i; // &取址操作符 8 printf("%d\n", *pointer); // *pointer 取值操作符
指针与指针变量
知道了一个变量的地址,就可以通过这个地址来访问这个变量,因此,又把变量的地址称为该变量的“指针”。
C语言中可以定义一类特殊的变量,这些变量专门用来存放变量的地址,称为指针变量。
在定义指针变量是要注意两点:
1)指针变量前面的*,表示该变量的类型为指针型变量,其一般形式为:类型说明符 *变量名;
其中,*表示这个一个指针变量,变量名即为定义的指针变量名,类型说明符表示该指针变量所指向的变量的数据类型。
2)定义指针变量是“必须”指定基类型。注意:只有整型变量的地址才能放到指向整型变量的指针变量中;例如:
1 // 错误!! 2 float a; 3 int *pointer_1; 4 pointer_1 = &a;
指针变量只能存放地址!!
1 // 通过指针变量访问整型变量 2 int a = 88, b = 66; 3 int *pointer_1, *pointer_2; 4 pointer_1 = &a; 5 pointer_2 = &b; 6 7 printf("%d %d\n", a, b); 8 printf("%d %d\n", *pointer_1, *pointer_2); 9 /* 10 输出结果: 11 88 66 12 88 66 13 */
关于“&” “*”说明:‘&’‘*’两个运算符的优先级别相同,但按自右向左方向结合;
定义一个指针变量 int *p; p = &a[i]
引用一个数组元素,可以用:
1)下标法:如 a[i]
2)指针法:如*(a + i) 或 *(p + i)
注意:数组名即“翻译成数组的第一个元素的地址”(编译器会把数组名编译为地址,数组第一个元素的地址)
对”&“和”*“运算符再做些说明:
1 int a = 100; 2 int *pointer_1; 3 pointer_1 = &a;
1)&*pointer_1 的含义是什么呢 ?
解析:‘&’和‘*’两个运算符的优先级别相同,但它俩都是右结合性(自右向左方向结合),因此先进行 *pointer_1的运算,即:*pointer_1 = a;在执行&运算,即:&a,为变量a的地址。
2)*&a 的含义是什么?
先进行 &a,为变量a的地址;在*运算,即取值运算,最终结果就是变量a;
3)(*pointer_1) ++ 相当于 a ++; 括号是必要的,因为’++‘ 和 ’*‘ 同一优先级别,而且也是右结合性。
数组,一个数组包含若干个元素,每个数组元素在内存中都占用存储单元,他们都有相应的地址,地址是连续的。
用数组名作为函数参数
1 // 编译时,是将arr按指针变量处理的,相当于将函数首部写成 2 // input(int *arr, 10); 3 // void input(int arr[], int n) 等价于 void input(int *arr, int n) 4 void input(int arr[], int n) { 5 printf("do something"); 6 }
需要说明的是:C语言调用函数时,虚实结合的方法都是采用”值传递“方式,当用变量名作为函数参数时传递的是变量的值;当用数组名作为函数参数时,由于数组名代表的是数组首元素地址,因此传值的值是地址,所以要求形参为指针变量。
关于数组的一个常见错误:
1 char *c_strcpy (char destination[],const char source[]); 2 int main(void) { 3 char str1[] = "this is a very long string"; 4 char str2[] = "this is a short string"; 5 strcpy(str2, str1); 6 puts(str2); 7 return 0; 8 } 9 10 char *c_strcpy (char destination[], const char source[]) { 11 int size_of_array = sizeof source / sizeof source[0]; 12 for (int i = 0; i < size_of_array; i++) { 13 destination[i] = source[i]; 14 } 15 return destination; 16 }
出现错误的是第 11 行代码。我们知道,数组作为函数的参数,在本函数中 source 只是一个指针(地址,系统在本函数运行时,是不知道a所表示的地址有多大的数据存储空间,这里只是告诉函数:一个数据存储空间首地址),所以,sizeof source 的结果是指针变量 source 占内存的大小。一般在32位机上是4个字节。source[0] 是 int 类型,sizeof source 也是4个字节,所以,结果永远是1。
结论:获得数组长度,只能在数组定义所在的代码区中获取数组长度,才能获取正确的数组长度。
正确代码:
1 char *c_strcpy (char destination[], const char source[], int len); 2 int main(void) { 3 char str1[] = "this is a very long string"; 4 char str2[] = "this is a short string"; 5 int len = sizeof(str1) / sizeof(str1[0]); 6 c_strcpy(str2, str1, len); 7 puts(str2); 8 return 0; 9 } 10 11 char *c_strcpy (char destination[], const char source[], int len) { 12 for (int i = 0; i < len; i++) { 13 destination[i] = source[i]; 14 } 15 return destination; 16 }
倒序输出数组中的值:
1 int main(int argc, const char * argv[]) { 2 int a[] = {34, 56, 89, 87, 1, 34, 67}; 3 int count = sizeof(a)/sizeof(int); 4 int i; 5 reverse_pointer(a, count); 6 // reverse_arr(a, count); 7 for (i = 0; i < count; i ++) { 8 printf("%d\n", a[i]); 9 } 10 return 0; 11 } 12 13 // 方法一:倒序输出数组中的值(指针作为参数) 14 void reverse_pointer(int *x, int n) { 15 int *p, *i, *j, temp, m; 16 m = (n - 1) / 2; 17 i = x; // i指向数组的第一个元素 18 j = x - 1 + n; // j指向数组的最后一个元素 19 p = x + m; // p指向数组的中间一个元素,配对 20 for (; i < p; i ++ , j --) { 21 temp = *i; 22 *i = *j; 23 *j = temp; 24 } 25 } 26 27 // 方法二:倒序输出数组中的值(数组作为参数) 28 void reverse_arr(int arr[], int len) { // 形参arr是数组名,在此处仅是指针 29 int temp, i, j, m; 30 m = (len -1 ) / 2; 31 for (i = 0; i < m; i ++) { 32 j = len - 1 - i; 33 temp = arr[i]; 34 arr[i] = arr[j]; 35 arr[j] = temp; 36 } 37 }
1 // 找出数组中的最大值和最小值 2 void max_min_value(int arr[], int n) { 3 int *p, *array_end; 4 array_end = arr + n; 5 int max, min; 6 max = min = *arr; 7 for (p = arr + 1; p < array_end; p ++) { 8 if (*p > max) { 9 max = *p; 10 } else if (*p < min) { 11 min = *p; 12 } 13 } 14 }
归纳起来,如果有一个实参数组,想在函数中改变此数组中元素的值,实参与形参的对应关系有以下4种情况:
1) 形参和实参都用数组名,如
1 void func(int x[], int n) { 2 printf("do something"); 3 } 4 5 void main(void) { 6 int a[10]; 7 func(a, 10); 8 }
2) 实参用数组名,形参用指针变量,如
1 void func(int *p, int n) { 2 printf("do something"); 3 } 4 5 void main(void) { 6 int a[10]; 7 func(a, 10); 8 }
3) 实参形参都采用指针变量,如
1 void func(int *p, int n) { 2 printf("do something"); 3 } 4 5 void main(void) { 6 int a[10], *p; 7 func(p, 10); 8 }
4) 实参为指针变量,形参为数组名,如
1 void func(int x[], int n) { 2 printf("do something"); 3 } 4 5 void main(void) { 6 int a[10], *p; 7 func(p, 10); 8 }
多维数组与指针:
多维数组元素的地址,定义一个二维数组
1 int a[3][4] = {{1, 3, 5, 7}, {9, 11, 13, 15}, {17, 19, 21, 23}};
设二维数组的首行的首地址为2000,则:
一维数组里面存放的是二维数组元素的地址,二维数组里面存放的是二维数组元素的值!
1 // 定义二维数组,三行四列 2 int a[3][4] = {0, 1, 2, 3 , 4, 5, 6, 7, 8, 9, 10, 11}; 3 4 // 打印 a[0][0] 首元素地址 5 printf("a=%p\n", a); 6 printf("*a=%p\n", *a); 7 printf("a[0]=%p\n", a[0]); 8 printf("&a[0]=%p\n", &a[0]); 9 printf("&a[0][0]=%p\n", &a[0][0]); 10 11 printf("\n"); 12 13 // 打印 a[1][0] 的地址,第二行 14 printf("a+1=%p\n", a + 1); 15 printf("*(a+1)=%p\n", *(a + 1)); 16 17 printf("\n"); 18 19 // 打印 a[1][1] 的值 20 printf("*(a[1]+1)=%d\n", *(a[1]+1)); 21 printf("*(*(a+1)+1)=%d\n", *(*(a+1)+1)); 22 23 // 把二维数组 a 分解为一维数组 a[0],a[1],a[2]之后,设 p 为指向二维数组的指针变量,可定义为: 24 int (*p)[4]; 25 // 它表示 p 是一个指针变量,它指向包含4个元素的一维数组。若指向第一个一维数组 a[0],其值等于 a,a[0] 或 &a[0][0] 26 27 // 而 p + i 则指向一维数组 a[i] 28 29 // 结论: *(p + i) + j 是二维数组 i 行 j 列的元素的地址 30 // 而 *(*(p + i) + j) 是 i 行 j 列元素的值 31 32 // 二维数组指针变量说明的一般形式: 33 类型说明符 (*指针变量名)[长度] 34 35 // 其中“类型说明符”为所指数组的数据类型。 36 // “*”表示其后的变量是指指针类型。 37 // “长度”表示二维数组分别为多个一维数组时,一维数组的长度,也就是二维数组的列数
1 // 用指针输出二维数组的值 2 int a[3][4] = {0, 1, 2, 3 , 4, 5, 6, 7, 8, 9, 10, 11}; 3 int (*p)[4]; 4 int i, j; 5 p = a; 6 for (i = 0; i < 3; i ++) { 7 for (j = 0; j < 4; j ++) { 8 printf("%2d", *(*(p + i) + j)); 9 } 10 }