chap08:指针
chap08:指针
通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。也有一种说法,C语言的精华就在指针。
所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。不过,人们往往不会区分两者的概念,而是混淆在一起使用,在必要的情况下,大家也要注意区分。
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
下图是 4G 内存中每个字节的编号(以十六进制表示):
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。
#include <stdio.h>
int main(){
int a = 100;
char str[20] = "Hello world.";
printf("%#X, %#X\n",&a,str);
//控制符%p,专门用来以十六进制形式输出地址,不过%p的输出格式并不统一
printf("%p, %p\n",&a,str);
return 0;
}
虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
一、指针变量的定义和使用
1、指针变量的定义
指针变量声明的一般形式为:
type *var_name;
*
表示这是一个指针变量,datatype
表示该指针变量所指向的数据的类型 。例如:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
2、指针变量的使用
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
*pointer
这里的*
称为指针运算符,用来取得某个地址上的数据
#include <stdio.h>
int main(){
int a = 15;
int *p = &a;
printf("%d, %d\n", a, *p); // 两种方式都可以输出a的值
// *p等价于a,也可以修改内存上的数据
*p = 20;
printf("%d, %d\n", a, *p);
return 0;
}
使用指针是间接访问数据,使用变量名是直接访问数据,前者比后者的代价要高。
3、指针变量的运算(加法、减法和比较运算)
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等(不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义)。
加法、减法:可以理解为移动指针。比较运算是比较指针的前后关系。
请看下面的代码:
#include <stdio.h>
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%d, &b=%d, &c=%d\n", &a, &b, &c);
printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc);
//加法运算
printf("指针加1,注意不同类型指针地址值的变化\n");
pa++; pb++; pc++;
printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc);
//减法运算
printf("指针减1,注意地址又回到原先的值\n");
pa--; pb--; pc--;
printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc);
//比较运算
printf("两个指针相比较\n");
if(pa == paa){
printf("pa与paa指向同一个地方\n");
}else{
printf("pa与paa指向不同一个地方\n");
}
return 0;
}
注意: 不同类型指针,移动的距离不一样!
对于指向普通变量的指针,我们往往不进行加减运算,虽然编译器并不会报错,但这样做没有意义,因为不知道它变化后指向的是什么数据。这里只是为了理解不同类型指针加减移动的距离不同。
二、数组指针(指向数组的指针)
- 数组中的所有元素在内存中是连续排列的,正好指针的加减运算可以为访问数组元素提供另外的方式。
- 数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。是一个常量。
遍历数组元素:
#include <stdio.h>
int main(){
int arr[5] = { 99, 15, 100, 888, 252 };
int *p = arr;
int i;
printf("第一种访问方式:\n");
for(i=0; i<5; i++){
printf("%d ", arr[i]);
}
printf("\n");
printf("第二种访问方式:\n");
for(i=0; i<5; i++){
printf("%d ", p[i]);
}
printf("\n");
printf("第三种访问方式:\n");
for(i=0; i<5; i++){
printf("%d ", *(arr+i));
}
printf("\n");
printf("第四种访问方式:\n");
for(i=0; i<5; i++){
printf("%d ", *(p+i));
}
printf("\n");
printf("第五种访问方式:\n");
for(p=arr; p<arr+5; p++){
// 注意循环结束指针p所指的位置
printf("%d ", *p);
}
return 0;
}
三、字符串指针
前面学过,C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中。
字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组。
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "Hello world.";
char *pstr = str;
printf("字符数组的长度为:%d\n",sizeof(str));
printf("字符串的长度为:%d\n",strlen(str));
puts(str);
puts(pstr);
return 0;
}
除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
#include <stdio.h>
#include <string.h>
int main(){
char *pstr = "Hello world.";
printf("字符数组的长度为:%d\n",sizeof(pstr));
printf("字符串的长度为:%d\n",strlen(pstr));
puts(pstr);
return 0;
}
它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
四、指针变量作为函数参数
在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
比较下面两个程序,理解(值传递与地址传递的不同):
#include <stdio.h>
// 交换两个变量的值
void swap(int a, int b){ // 传递的是值
int temp; //临时变量
temp = a;
a = b;
b = temp;
}
int main(){
int a = 66, b = 99;
swap(a, b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
#include <stdio.h>
void swap(int *p1, int *p2){
int temp; //临时变量
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(){
int a = 66, b = 99;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
五、用数组作函数参数
#include <stdio.h>
int max(int intArr[6], int n){
int i, maxValue = intArr[0]; //假设第0个元素是最大值
for(i=1; i<n; i++){
if(maxValue < intArr[i]){
maxValue = intArr[i];
}
}
return maxValue;
}
int main(){
int nums[6], i;
int len = sizeof(nums)/sizeof(int);
//读取用户输入的数据并赋值给数组元素
for(i=0; i<len; i++){
scanf("%d", nums+i);
}
printf("Max value is %d!\n", max(nums, len));
return 0;
}
用数组做函数参数时,实际上传递的是数组的首地址,仅仅是一个指针,而不是真正的数组。
所以:int max(int intArr[6], int n)
中的数组长度写了也是无意义的,等价于:
int max(int intArr[], int n)
等价于:
int max(int *intArr, int n){
int i, maxValue = intArr[0]; //假设第0个元素是最大值
for(i=1; i<n; i++){
if(maxValue < intArr[i]){
maxValue = intArr[i];
}
}
return maxValue;
}
在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。
六、二维数组指针(指向二维数组的指针)
C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]
、a[0][1]
、a[0][2]
、a[0][3]
。
假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:
我们先来定义一个指向 a 的指针变量 p:
int (*p)[4] = a;
括号中的*
表明 p 是一个指针,它指向一个数组,数组的类型为int [4]
,这正是 a 所包含的每个一维数组的类型。
[ ]
的优先级高于*
,( )
是必须要加的,如果赤裸裸地写作int *p[4]
,那么应该理解为int *(p[4])
,p 就成了一个指针数组,而不是二维数组指针。
对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4]
,那么p+1
就前进 4×4 = 16 个字节,p-1
就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1
会使得指针指向二维数组的下一行,p-1
会使得指针指向数组的上一行。
数组名 a 在表达式中也会被转换为和 p 等价的指针!
下面我们就来探索一下如何使用指针 p 来访问二维数组中的每个元素。按照上面的定义:
-
p
指向数组 a 的开头,也即第 0 行;p+1
前进一行,指向第 1 行。 -
*(p+1)
表示取地址上的数据,也就是整个第 1 行数据。注意是一行数据,是多个数据,不是第 1 行中的第 0 个元素,下面的运行结果有力地证明了这一点:
#include <stdio.h>
int main(){
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;
printf("%d\n", sizeof(*(p+1)));
return 0;
}
运行结果:
16
*(p+1)+1
表示第 1 行第 1 个元素的地址。如何理解呢?
*(p+1)
单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
*(*(p+1)+1)
表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。
根据上面的结论,可以很容易推出以下的等价关系:
a+i == p+i // 行地址
a[i] == p[i] == *(a+i) == *(p+i) // 列元素地址
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j) // 数组元素
【实例】使用指针遍历二维数组。
#include <stdio.h>
int main(){
int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
int(*p)[4];
int i,j;
p=a;
for(i=0; i<3; i++){
for(j=0; j<4; j++) printf("%2d ",*(*(p+i)+j)); // 思考一下有几种方式
printf("\n");
}
return 0;
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!