深入理解C语言 - 指针详解

时间:2023-03-09 04:35:41
深入理解C语言 - 指针详解

一、什么是指针

C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。也就是说:指针是一种保存变量地址的变量。

前面已经提到内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从 0 开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:

深入理解C语言 - 指针详解

这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。

二、为什么要使用指针

在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:

1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;

2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;

3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。

三、如何声明一个指针

指针其实就是一个变量,指针的声明方式与一般的变量声明方式没太大区别:

int *p; //声明一个返回整型数据的指针 

int *p[3]; //因为[]的优先级比*高,所以P是一个数组,因此P是一个由返回整型数据的指针所组成的数组

int (*p)[3]; //首先P是一个指针,然后再与[]结合,说明指针所指向的内容是一个数组,所以P是一个指向由整型数据组成的数组的指针

int (*p)(int); //首先P是一个指针,然后与()结合,说明指针指向的是一个函数,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针  

int *(*p(int))[3]; //P是一个参数为一个整数且返回一个指向由整型指针变量组成的数组的指针变量的函数

指针的声明比普通变量的声明多了一个一元运算符 “*”。运算符 “*” 是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针所指向的对象。在上述的声明中: p 是一个指针,保存着一个地址,该地址指向内存中的一个变量; *p 则会访问这个地址所指向的变量。

声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:或是使他指向现有的内存,或者给他动态分配内存,否则我们并不知道指针指向哪儿,这将是一个很严重的问题。初始化操作如下:

/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x;  //指针p被初始化,指向变量x ,其中取地址符&用于产生操作数内存地址 /* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10); //malloc函数用于动态分配内存
free(p);    // free函数用于释放一块已经分配的内存,常与malloc函数一起使用,要使用这两个函数需要头文件stdlib.h
p = NULL;

指针的初始化实际上就是给指针一个合法的地址,让程序能够清楚地知道指针指向哪儿。

四、细说指针

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存大小。

先声明几个指针作为例子:

int*ptr;
char*ptr;
int**ptr;
int(*ptr)[3];

1.指针的类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针本身的类型。让我们看看上面例子中各个指针的类型:

int*ptr; //指针的类型是int*
char*ptr; //指针的类型是char*
int**ptr; //指针的类型是int**
int(*ptr)[3]; //指针的类型是int(*)[3]

2.指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:

int*ptr; //指针所指向的类型是int
char*ptr; //指针所指向的的类型是char
int**ptr; //指针所指向的的类型是int*
int(*ptr)[3]; //指针所指向的的类型是int()[3]

3.指针的值或者叫指针所指向的内存区

指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。

指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。

4.指针本身所占据的内存大小

指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度。

五、指针的算术运算

C 指针的算术运算只限于两种形式:

1) 指针 +/- 整数 :

可以对指针变量 p 进行 p++、p--、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于 p 所指的内存地址前进或者后退了 i 个操作数。用一张图来说明一下:

深入理解C语言 - 指针详解

在上图中,10000000等是内存地址的十六进制表示(数值是假定的),p 是一个 int 类型的指针,指向内存地址 0x10000008 处。则 p++ 将指向与 p 相邻的下一个内存地址,由于 int 型数据占 4 个字节,因此 p++ 所指的内存地址为 1000000b。其余类推。不过要注意的是,这种运算并不会改变指针变量 p 自身的地址,只是改变了它所指向的地址。举个例子:

#include "stdio.h"

int main()
{
char arr[20]="the_example";
int *ptr=(int *)arr;
ptr++;
printf("%c",*ptr); //输出: arr[4] - 'e' return 0;
}

在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向数组arr。接下来指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr 所指向的地址由原来的数组arr的地址向高地址方向增加了4个字节。由于char 类型的长度是一个字节,所以,原来ptr是指向数组arr的第0个字节开始的四个字节,此时指向了数组a中从第4个字节开始的四个字节。

2)指针 +/- 指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。两个指针相减的结果的类型是 ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。举个例子:

#include "stdio.h"

int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,0};
int sub;
int *p1 = &a[2];
int *p2 = &a[8]; sub = p2-p1;
printf("%d\n",sub);    // 输出结果为 6 return 0;
}

六、指针与数组的关系

数组的数组名其实可以看作一个指针,一个通过数组和下标实现的表达式可以等价地通过指针及其偏移量来实现,这就是数组和指针的互通之处。但有一点要明确的是,数组和指针并不是完全等价,指针是一个变量,而数组名不是变量,它数组中第 1 个元素的地址,数组可以看做是一个用于保存变量的容器。看下例:

#include "stdio.h"

int main()
{
int x[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = x;
printf("x的地址为:%p\n",x);
printf("x[0]的地址为:%p\n",&x[0]);
printf("p的地址为:%p\n",&p); //打印指针p的地址,并不是指针所指向的地方的地址 p += 2;
printf("*(p+2)的值为:%d\n",*p); //输出结果为3,*(p+2)指向了x[2] return 0;
}

结果如下:

深入理解C语言 - 指针详解

可以看到, x 的值与 x[0] 的地址是一样的,也就是说数组名即为数组中第 1 个元素的地址。实际上,打印 &x 后发现,x 的地址也是这个值。而 x 的地址与指针变量 p 的地址是不一样的。故而数组和指针并不能完全等价。

七、指针与结构的关系

结构指针是指向结构的指针,C语言中使用 -> 操作符来访问结构指针的成员,举个例子:

#include "stdio.h"

typedef struct
{
char name[10];
int age;
int score;
}message; int main()
{
message mess = {"tongye",23,83};
message *p = &mess; printf("%s\n",p->mess);      // 输出结果为:tongye
printf("%d\n",p->score); // 输出结果为:83 return 0;
}

八、指针和函数的关系

C语言的所有参数均是以“传值调用”的方式进行传递的,这意味着函数将获得参数值的一份拷贝。这样,函数可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。

1.指针作为函数的参数

传值调用的好处是是被调函数不会改变调用函数传过来的值,可以放心修改。但是有时候需要被调函数回传一个值给调用函数,这样的话,传值调用就无法做到。为了解决这个问题,可以使用传指针调用。指针参数使得被调函数能够访问和修改主调函数中对象的值。用一个例子来说明:

#include "stdio.h"

//值传递
void swap1(int a,int b) //参数为普通的int变量
{
  int temp;
  temp = a;
  a = b;
  b = temp;
} //指针传递
void swap2(int *a,int *b) //参数为指针,接受调用函数传递过来的变量地址作为参数,对所指地址处的内容进行操作
{
  int temp; //最终结果是,地址本身并没有改变,但是这一地址所对应的内存段中的内容发生了变化,即x,y的值发生了变化
  temp = *a;
  *a = *b;
  *b = temp;
} int main()
{
  int x = 1,y = 2;
  swap1(x,y); //将x,y的值本身作为参数传递给了被调函数
  printf("%d %5d\n",x,y); //输出结果为:1 2   swap(&x,&y); //将x,y的地址作为参数传递给了被调函数,传递过去的也是一个值,与传值调用不冲突
  printf("%d %5d\n",x,y); //输出结果为:2 1
  
  return 0;
}

2.指向函数的指针

在C语言中,函数本身不是变量,但是可以定义指向函数的指针,也称作函数指针,函数指针指向函数的入口地址。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等。 声明一个函数指针的方法如下:

返回值类型 (* 指针变量名)([形参列表]);

int (*pointer)(int *,int *);        // 声明一个函数指针

上述代码声明了一个函数指针 pointer ,该指针指向一个函数,函数具有两个 int * 类型的参数,且返回值类型为 int。下面的代码演示了函数指针的用法:

#include "stdio.h"
#include "string.h" int str_comp(const char *m,const char *n)
{
//库函数 strcmp 用于比较两个字符串,其原型是:int strcmp(const char *s1,const char *s2);
if(strcmp(m,n) == 0)
return 0;
else
return 1;
} /* 函数 comp 接受一个函数指针作为它的第三个参数 */
void comp(char *a,char *b,int (*prr)(const char *,const char*))
{
if((*prr)(a,b) == 0)
printf("str1 = str2\n");
else
printf("str1 != str2\n");
} // 声明一个函数 comp ,注意该函数的第三个参数,是一个函数指针 int main()
{
char str1[20]; //声明一个字符数组
char str2[20];
int (*p)(const char *,const char *) = str_comp; //声明并初始化一个函数指针 gets(str1); //使用 gets() 函数从 I/O 读取一行字符串
gets(str2);
comp(str1,str2,p); //函数指针 p 作为参数传给 comp 函数 return 0;
}

这段代码的功能是从键盘读取两行字符串(长度不超过20),判断二者是否相等。

2.使用typedef定义函数指针

示例如下:

void MyFun(int x)
{
printf("%d\n",x);
} typedef void (*funP)(int); //定义函数指针类型 int main(int argc, char* argv[])
{
funP p;
p=&MyFun; //将MyFun函数的地址赋给FunP变量
(*p)(20); //这是通过函数指针变量FunP来调用MyFun函数的。 return 0;
}

typedef的功能是定义新的类型。第一句就是定义了一种funP的类型,并定义这种类型为指向某种函数指针,这种函数以一个int为参数并返回void类型。后面就可以像使用int,char一样使用funP了。

参考:

C语言--指针详解

C语言指针详解(经典,非常详细)