4

指针是C语言中非常重要的数据类型,如果你说C语言中除了指针,其他你都学得很好,那你干脆说没学过C语言。究竟什么是指针呢?我们先来看一个概念。

直接引用

  1. 回想一下,之前我们是如何更改某个变量的值?
    我们之前是通过变量名来直接引用变量,然后进行赋值:

char a;
a = 10;
  1. 看上去是很简单,其实程序内部是怎么操作的呢?
    其实,程序对变量的读写操作,实际上是对变量所在的存储空间进行写入或取出数据。就上面的代码而言,系统会自动将变量名a转换为变量的存储地址,根据地址找到变量a的存储空间,然后再将数据10以2进制的形式放入变量a的存储空间中。

clipboard.png

  1. 通过变量名引用变量,由系统自动完成变量名和其存储地址之间的转换,称为变量的"直接引用"方式

一、什么是指针?

1.我们已经知道,"直接引用"是直接通过变量名来读写变量

2.C语言中还有一种"间接引用"的方式(以变量a为例):首先将变量a的地址存放在另一个变量中,比如存放在变量b中,然后通过变量b来间接引用变量a,间接读写变量a的值。这就是"间接引用"。

clipboard.png

如果程序通过"间接引用"的方式来修改a的值,可以这样做:先根据 变量名b 获取 变量b 的地址ffc2,取出变量b中存储的内容ffc1,也就是变量a的地址,再根据变量a的地址ffc1找到a的存储空间,然后修改里面的数据。

3.总结一句:用来存放变量地址的变量,就称为"指针变量"。在上面的情况下,变量b就是个"指针变量",我们可以说指针变量b指向变量a。

二、指针的定义

一般形式:类名标识符 *指针变量名;

int *p;

float *q;

"*"是一个说明符,用来说明这个变量是个指针变量,是不能省略的,但它不属于变量名的一部分
前面的类型标识符表示指针变量所指向的变量的类型,而且只能指向这种类型的变量

三、指针的初始化

1.先定义后初始化

1 // 定义int类型的变量a
2 int a = 10;
3 
4 // 定义一个指针变量p
5 int *p;
6 
7 // 将变量a的地址赋值给指针变量p,所以指针变量p指向变量a
8 p = &a;

注意第8行,赋值给p的是变量a的地址&a

2.在定义的同时初始化

// 定义int类型的变量a
int a = 10;

// 定义一个指针变量p
// 并将变量a的地址赋值给指针变量p,所以指针变量p指向变量a
int *p = &a;

3.初始化的注意

指针变量是用来存放变量地址的,不要给它随意赋值一个常数。下面的写法是错误

int *p; 
p = 200; // 这是错误的

四、指针运算符

1.给指针指向的变量赋值

 1 char a = 10;
 2 printf("修改前,a的值:%d\n", a);
 3 
 4 // 指针变量p指向变量a
 5 char *p = &a;
 6 
 7 // 通过指针变量p间接修改变量a的值
 8 *p = 9;
 9 
10 printf("修改后,a的值:%d", a);

当程序刚执行完第5行代码时,内存中大概的分布情况是这样的

clipboard.png

a值是10,p值就是变量a的地址ffc3。

注意下第5、第8行,都有个"*",它们的含义是不一样的:

(1) 第5行的"*"只是用来说明p是个指针变量

(2) 第8行的"*"是一个指针运算符,这里的*p代表根据p值ffc3这个地址访问对应的存储空间,也就是变量a的存储空间,然后将右边的数值9写入到这个存储空间,相当于 a = 9;,于是内存中就变成这样了

clipboard.png

输出结果为:
clipboard.png
,可以发现,我们通过变量p间接修改了变量a的值。

2.取出指针所指向变量的值

指针运算符除了可以赋值之外,还可以用于取值

1 char a = 10;
2  
3 char *p;
4 p = &a;
5 
6 char value = *p;
7 printf("取出a的值:%d", value); 

输出结果:
clipboard.png
,第6行中的*p的意思是:根据p值(即变量a的地址)访问对应的存储空间,并取出存储的内容(即取出变量a的值),赋值给value

3.使用注意

在指针变量没有指向确定地址之前,不要对它所指的内容赋值。下面的写法是错误

int *p; 
*p = 10; //这是错误的

应该在指针变量指向一个确定的变量后再进行赋值。下面的写法才是正确

// 定义2个int型变量
int a = 6, b;

// 定义一个指向变量b的指针变量p
int *p;
p = &b;

// 将a的值赋值给变量b
*p = a;

五、指针的用途举例

1.例子1

前面我们通过指针变量p间接访问了变量a,在有些人看来,觉得指针变量好傻B,直接用变量名a访问变量a不就好了么,干嘛搞这么麻烦。别着急,接下来举个例子,让大家看看指针还能做什么事情。

现在有个要求:写一个函数swap,接收2个整型参数,功能是互换两个实参的值。

1> 如果没学过指针,你可能会这样写

void swap(char v1, char v2) {
    printf("更换前:v1=%d, v2=%d\n", v1, v2);
    
    // 定义一个中间变量
    char temp;
    
    // 交换v1和v2的值
    temp = v1;
    v1 = v2;
    v2 = temp;
    
    printf("更换后:v1=%d, v2=%d\n", v1, v2);
}

int main()
{
    char a = 10, b = 9;
    printf("更换前:a=%d, b=%d\n", a, b);
    
    swap(a, b);
    
    printf("更换后:a=%d, b=%d", a, b);
    return 0;
}

输出结果:clipboard.png

虽然v1和v2的值被交换了,但是变量a和b的值根本就没有换过来。因为基本数据类型作为函数实参时,只是纯粹地将值传递给形参,形参的改变并不影响实参。

我们可以简要分析一下这个过程:

  • 在第20行中,将变量a、b的值分别传递给了swap函数的两个形参v1、v2

2> 如果学了指针,就应该这样写

1 void swap(char *v1, char *v2) {
 2     // 中间变量
 3     char temp;
 4     
 5     // 取出v1指向的变量的值
 6     temp = *v1;
 7     
 8     // 取出v2指向的变量的值,然后赋值给v1指向的变量
 9     *v1 = *v2;
10     
11     // 赋值给v2指向的变量
12     *v2 = temp;
13 }
14 
15 int main()
16 {
17     char a = 10, b = 9;
18     printf("更换前:a=%d, b=%d\n", a, b);
19     
20     swap(&a, &b);
21     
22     printf("更换后:a=%d, b=%d", a, b);
23     return 0;
24 }

先看看输出结果:

clipboard.png
变量a和b的值终于换过来了。

解释一下:

(在16位编译器环境下,一个指针变量占用2个字节)

先注意第20行,传递是变量的地址。因此swap函数的形参v1指向了变量a,v2指向了变量b

clipboard.png

第6行代码是取出v1指向的变量的值,也就是变量a的值:10,然后赋值给变量temp

clipboard.png

第9行代码是取出v2指向的变量(变量b)的值,然后赋值给v1指向的变量(变量a)

clipboard.png

第12行代码是将temp变量的值赋值给v2指向的变量(变量b)

clipboard.png

相信你已经感受到指针的强大了,如果没有指针,在一个函数的内部根本改变不了外部的实参。

2.例子2

接下来再举一个指针的实用例子。默认情况下,一个函数只能有一个返回值,有了指针,我们可以实现函数有"多返回值"。

现在有个要求:写一个函数sumAndMinus,可以同时计算2个整型的和与差,函数执行完毕后,返回和与差(注意了,这里要返回2个值)

// 计算2个整型的和与差
int sumAndMinus(int v1, int v2, int *minus) {
    // 计算差,并赋值给指针指向的变量
    *minus = v1 - v2;
    
    // 计算和,并返回和
    return v1 + v2;
}

int main()
{
    // 定义2个int型变量
    int a = 6, b = 2;

    // 定义2个变量来分别接收和与差
    int sum, minus;

    // 调用函数
    sum = sumAndMinus(a, b, &minus);
    
    // 打印和
    printf("%d+%d=%d\n", a, b, sum);
    
    // 打印差
    printf("%d-%d=%d\n", a, b, minus);
    return 0;
}

输出结果:
clipboard.png
,和与差都由同一个函数计算并返回出来。和是函数的直接返回值,差是通过函数的第3个指针参数间接返回。

因此有了指针,我们可以让函数有"无限个"返回值。

六、关于指针的疑问

刚学完指针,都可能有一大堆的疑惑,这里我列出几个常见的疑惑吧。

1.一个指针变量占用多少个字节的内存空间?占用的空间是否会跟随所指向变量的类型而改变?

在同一种编译器环境下,一个指针变量所占用的内存空间是固定的。比如,在16位编译器环境下,任何一个指针变量都只占用2个字节,并不会随所指向变量的类型而改变。

clipboard.png

2.既然每个指针变量所占用的内存空间是一样的,而且存储的都是地址,为何指针变量还要分类型?而且只能指向一种类型的变量?比如指向int类型的指针、指向char类型的指针。

其实,我觉得这个问题跟"数组为什么要分类型"是一样的。

看下面的代码,利用指针p读取变量c的值

int i = 2;
char c = 1;

// 定义一个指向char类型的指针
char *p = &c;

// 取出
printf("%d", *p);

这个输出结果应该难不倒大家:1,是可以成功读取的。

如果我改一下第5行的代码,用一个本应该指向int类型变量的指针p指向char类型的变量c

int *p = &c;

clipboard.png

我们再来看一下输出:513,c的原值是1,现在取出来却是513,怎么回事呢?这个要根据内存来分析

根据变量的定义顺序,这些变量在内存中大致如下图排布:

其中,指针变量p和int类型变量i各占2个字节,char类型的c占一个字节,p指向c,因此p值就是c的地址

clipboard.png

1> 最初的时候,我们用char p指向变量c。当利用p来获取变量c的值时,由于指针p知道变量c是char类型的,所以会从ffc3这个地址开始读取1个字节的数据:0000 0001,转为10进制就是1

2> 后来,我们用int p指向变量c。当利用p获取变量c的值时,由于指针p认为变量c是int类型的,所以会从ffc3这个地址开始读取2个字节的数据:0000 0010 0000 0001,转为10进制就是513

可见,给指针分类是多么重要的一件事,而且一种指针最好只指向一种类型的变量,那是最安全的。

七、指向一维数组元素的指针

1.用指针指向一维数组的元素

// 定义一个int类型的数组
int a[2];

// 定义一个int类型的指针
int *p;

// 让指针指向数组的第0个元素
p = &a[0];

// 修改所指向元素的值
*p = 10;

// 打印第一个元素的值
printf("a[0] = %d", a[0]);

输出结果:a[0] = 10,说明已经通过指针间接修改了数组元素的值,跟指向一个普通int类型变量是一样的。

由于数组名代表着数组的首地址,即 a == &a[0],因此第8行代码等价于:

// 让指针指向数组的第0个元素
p = a;

内存分析图如下,一个指针变量占用2个字节,一个int类型的数组元素占用2个字节

clipboard.png

2.用指针遍历数组元素

2.1 最普通的遍历方式是用数组下标来遍历元素

// 定义一个int类型的数组
int a[4] = {1, 2, 3, 4};

int i;
for (i = 0; i < 4; i++) {
    printf("a[%d] = %d \n", i, a[i]);
}

输出结果:
clipboard.png

2.2 用指针来遍历数组元素

先定义一个指针,指向数组的第一个元素

// 定义一个int类型的数组
int a[4] = {1, 2, 3, 4};

// 定义一个int类型的指针,并指向数组的第0个元素
int *p = a;

clipboard.png

p的值是a[0]的地址,因此,现在我们利用指针p只能访问数组的第0个元素a[0],用*p就可取出a[0]的值1。要想访问其他元素,就必须拿到元素的地址,可以发现每个元素的地址差值为2,因为在16位编译器环境下,一个int类型的变量占用2个字节。现在只是知道a[0]的地址值为p,怎么根据a[0]的地址获取其他元素的地址呢?其实非常简单,p+1就是a[1]的地址。注意了,这里的p+1代表着p的值加2,并不是p的值加1,比如p的值为ffc3,p+1则为ffc5,而非ffc4。依次类推,p+2就是a[2]的地址ffc7,p+3就是a[3]的地址ffc9。

我先解释一下,为什么p+1代表p的值加2,而不是加1呢?

其实,p+1不一定代表p的值加2,也可能是加1、加4或者加8。究竟加多少,这跟指针的类型有关。下图是在16位编译器环境下的情况。

clipboard.png

聪明的你可能已经找到规律了,因为char类型的变量要占用1字节,所以p+1代表p的值加1;float类型的变量占用4字节,所以p+1代表p的值加4。从这一点,也可以很好地说明为什么指针一定要分类型,不同类型的指针,p+1的含义是不一样的。

上述代码中的p指向了int类型的数组元素a[0],所以p+1代表p的值加2。知道怎么获取其他元素的地址了,那么就可以利用指针p遍历数组元素了。

 1 // 定义一个int类型的数组
 2 int a[4] = {1, 2, 3, 4};
 3 
 4 // 定义一个int类型的指针,并指向数组的第0个元素
 5 int *p = a;
 6 
 7 int i;
 8 for (i = 0; i < 4; i++) {
 9     // 利用指针运算符*取出数组元素的值
10     int value = *(p+i);
11     
12     printf("a[%d] = %d \n", i, value);
13 }

注意第10行的代码,*(p+i)代表根据p+i的值(其实就是第i个数组元素的地址)访问对应的存储空间,并取出存储的内容(也就是取出第i个数组元素的值),赋值给左边的value。

最后的输出效果是一样的:
clipboard.png

注意的是:遍历完毕后,指针变量p还是指向a[0],因为p值一直没有变过,一直都是a[0]的地址ffc3

补充一下,其实第10行改成下面的代码也是可以的:

int value = *(a+i);

大家都知道,a值代表数组的首地址,也就是a[0]的地址ffc3。a+1则代表a的值加2,即a[1]的地址ffc5,也就是说,a+i代表着元素a[i]的地址。相信大家也能猜出来了,a+1不一定代表着a值加2,究竟加多少,取决于数组的类型。a+i的计算方法与p+i相同

利用上面的方法遍历完数组元素后,p一直指向元素a[0]。其实我们也可以直接修改p的值来访问数组元素,只需要改一下第10行的代码即可

// 利用指针运算符*取出数组元素的值
int value = *(p++);

p++其实就是相当于p = p + 1,直接修改了p值,而且每次是加2。因此,每执行一次p++,指针p就会指向下一个数组元素。

输出结果肯定是一样的:
clipboard.png

但是,遍历完毕后,指针变量p没有指向任何数组元素,因为一共执行了4次p++,最后p值为ffcb。当然,可以重新让p指向a[0]:p = &a[0];或者p = a;

注意,这里的写法是错误

int value = *(a++);

a++相当于a=a+1,数组名a是个常量!不能进行赋值运算!

3、指针与数组的总结

p是指针,a是一个数组

1> 如果p指向了一个数组元素,则p+1表示指向数组该元素的下一个元素。比如,假设p = &a[0],则p+1表示a[1]的地址

2> 对于不同类型的数组元素,p值的改变是不同的。如果数组元素为int类型,p+1代表着p的值加上2(16位编译器环境下)

3> 如果p的初值是&a[0],那么

  • p+i和a+i都可以表示元素a[i]的地址,它们都指向数组的第i个元素。a代表数组首地址,a+i也是地址,它的计算方法与p+i相同

  • *(p+i)和*(a+i)都表示数组元素a[i]

  • 虽然p+i和a+i都指向数组的第i个元素,但二者使用时还是有区别的。因为作为指针变量的p可以改变自身值,如p++,使p的值自增。而数组名a是一个代表数组首地址的常量,它的值是不能改变的,即a++是不合法的

4> 引用一个数组元素可以有两种方法:

  • 下标法: 如a[i]

  • 指针法: 如(p+i) 或 (a+i)

4、数组、指针与函数参数

1.用数组名作为函数实参时,是把实参数组的首地址传递给形参数组,两个数组共同占用同一段内存空间,这样形参数组中的元素值发生变化就会使实参数组的元素值也同时变化

void change(int b[]) {
    b[0] = 10;
}

int main()
{
    // 定义一个int类型的数组
    int a[4] = {1, 2, 3, 4};

    // 将数组名a传入change函数中
    change(a);
    
    // 查看a[0]
    printf("a[0]=%d", a[0]);
    
    return 0;
}

change函数的形参是数组类型的,在第11行调用change函数时,将数组名a,也就是数组的地址传给了数组b。因此数组a和b占用着同一块内存空间。

输出结果:clipboard.png

2.这种地址的传递也可以用指针来实现。函数的实参和形参都可以分别使用数组或指针。这样就有4种情况:

clipboard.png

也就是说,如果一个函数的形参类型是一个数组,调用函数时,你可以传入数组名或者指针变量;

void change(int b[]) {
    b[0] = 10;
}

int main()
{
    // 定义一个int类型的数组
    int a[4] = {1, 2, 3, 4};
    
    int *p = a;

    // 将数组名a传入change函数中
    change(p);
    
    // 查看a[0]
    printf("a[0]=%d", a[0]);
    
    return 0;
}

注意第1行的形参类型是个数组int b[],第10行定义了指针变量p,第13行将p当做实参传入函数

如果一个函数的形参类型是一个指针变量,调用函数时,你可以传入数组名或者指针变量。

1 void change(int *b) {
 2     b[0] = 10;
 3     // 或者*b = 10;
 4 }
 5 
 6 int main()
 7 {
 8     // 定义一个int类型的数组
 9     int a[4] = {1, 2, 3, 4};
10 
11     // 将数组名a传入change函数中
12     change(a);
13     
14     // 查看a[0]
15     printf("a[0]=%d", a[0]);
16     
17     return 0;
18 }

注意第1行的形参类型是个指针变量int *b,第12行将数组名a当做实参传入函数。

由第2行可以看出,在很多情况下,指针和数组是可以相互切换使用的。但是,并不能说指针就等于数组。

八、指针和字符串

指针和字符串 详情

九、返回指针的函数与指向函数的指针

返回指针的函数与指向函数的指针


注:本文转自李明杰老师的博文


Corwien
6.3k 声望1.6k 粉丝

为者常成,行者常至。