2022-05-08更新:针对新的渲染器优化了显示

对于大部分C语言初学者,指针是最大的一块骨头 ——沃兹基·硕德

本节内容:

  • 指针的简单引入

    1. [What] 指针是什么

    2. [Why] 为什么要用指针

    3. [How] 指针怎么玩(声明,使用,运算,数组指针,结构体指针,函数指针)

  • 函数传参的几种方式

    1. 值传递

    2. 地址传递

    3. 引用传递

指针的简单引入


指针是什么

首先,我们要清楚指针是什么,下面是指针的原始定义

系统在内存中,为变量(本人按:这里应加上函数)分配存储空间的首个字节单元的地址,称之为该变量的地址。地址用来标识每一个存储单元,方便用户对存储单元中的数据进行正确的访问。在高级语言中地址形象地称为指针。

指针变量就是保存指针的变量,但很多人 将「指针变量」简称为「指针」,故本文的指针都是指针变量的意思

下面要认两个重要概念:指针的值和类型

「指针的值」 内存地址一般用十六进制数表示,故 指针的值就是一个十六进制数

「指针的类型」 指针不仅是一个地址那么简单,对象不同则类型不同,如指向 int类型 的指针就称这个指针是 int 型的,int 型指针就只能存 int 类型变量 的地址,这种要求可以一定程度上避免混乱。指针的类型用于推断对象的长度,以便进行指针运算(后面会讲)

注意:「泛型对象指针」或称「void*指针」可以指向任何对象类型,但不能提供对象的长度,故无法直接运算与引用

PS:实际上在 C 中是可以隐式(或者说自动)转换的, 但是在 C++ 中不能 ,必须显式(或者说手动)地转换,我建议还是保留显式转换的习惯,这是一个好码风

已经声明的 数组结构体函数 等本身实际上都能看作指针(准确说是常量)

一切标识符皆为指针(笑)


为什么要用指针

a. 使用指针保存 变量 的地址,这样就可以通过指向某变量的指针来 间接传递或操纵 某变量

b. 使用指针保存 函数 的地址,这样就可以通过指向某变量的指针来 间接使用 某函数,不要以为这是没事找事,我们可以通过这个模拟C++中的类

c. 我们可以使用指针 构建某些数据结构 ,如链表,二叉树等


指针怎么玩

先认识两个字符

  • & 取地址运算符(用于 取 某变量的地址)

  • * 引用运算符(用于通过地址 使用/操纵 某变量)

  • * 也充当 类型标识符(声明的时候告诉编译器这个变量是指针)

对于 & ,想必大家都用过,在 scanf 函数 中,我们使用它向函数提供变量的地址,这样 scanf 就能知道将数据写入到哪个位置

对于 * , 引用运算符 和 类型标识符 统称为 指针运算符

1
2
int a;
scanf("%d",&a);// 给 a 的地址让scanf写入

指针的声明和初始化

作为一种变量,指针的声明和初始化和普通变量形式类似

公式:类型名 *指针名

加个 * 表示「指针名」是个指针,它是「类型名」类型的指针

1
2
int *p;        //声明 p 是一个指针,它指向一个int,称 p 为int类型的指针,或 p 是 int* 类型的变量
char *p //声明 p 是一个指针,它指向一个char,称 p 为char类型的指针,或 p 是 char* 类型的变量

类型标识符可以紧跟在变量名前面,也可以接在类型后面,也可以放中间,都是等效的

1
2
3
int* p;
int *p; //我一般用这种
int * p;

混合声明是合法的,但我不觉得是好的码风

1
2
3
int* p,q;      //声明了一个指针和一个int类型变量
int *p,q; //声明了一个指针和一个int类型变量
int * p,q; //声明了一个指针和一个int类型变量

是变量,那当然也能构成数组,但 指针数组 和 数组指针(后面会讲) 不是一个东西

1
2
int *arr[10]   // 声明一个指针数组,该数组有10个元素,每个元素都是int类型的指针 
int (*arr)[10] // 声明一个数组指针,该指针指向一个int类型的一维数组

同一般变量一样,函数外声明后内容为空(空地址叫nil),函数内声明内容不确定(指向地址不确定)
应尽快初始化,其实最好声明时就初始化

1
2
3
int a;
int *p;
p = &a; //或int *p = &a;一步到位

不允许把一个地址直接赋予指针变量,必须转换为指针类型,但是允许你直接赋成NULL(空指针)

1
2
3
int *q = 0x000000000061FDF0;     //编译错误
int *q = (int*)0x000000000061FDF0; //类型必须相同
int *q = NULL //空指针不等于未初始化的指针

指针可以套娃,结合结构体我们可以弄一些好玩的东西

1
2
3
4
int a=10;
int *p=&a;
int **q=&p;
printf("%d",**q);//输出为10

指针的使用

当某指针保存了某变量的地址后,加上引用运算符就直接等价于原变量

例如,指针 p 保存了 a 的地址,那么 *pa 直接等同

1
2
3
4
5
6
7
int a=1;
int *p=&a; //p 保存了 a 的地址
printf("%p\n",p); //用 %p 输出指针的值,这里输出 a 的地址:000000000061FE14
printf("%d\n",*p); //*p 与 a 等价,输出 1
*p=3;
printf("%d\n",a); //输出 3
printf("%d\n",*p); //输出 3

但这句话不是完全正确的,有时,你会遇到如下的错误

1
2
3
4
*p++;					 //这看似应该等同于a++
printf("%d\n", a); //输出 3,没有加?为什么?
printf("%d\n", *p); //输出 6422040,这是啥?
printf("%d\n", *p == a); //输出 0,这是啥?

这个的具体的原因是什么 ,下面会解释
但你发现如果为 *p 加了个括号的话,就不会有问题

1
2
3
4
(*p)++;               //加一个括号试试
printf("%d\n",a); //输出 4
printf("%d\n",*p); //输出 4
printf("%d\n",*p==a); //输出 1,一切正常

所以请你记住,为了保险,建议在使用指针时加个括号,并且现在可以得出下面这个结论(极其重要)

p 指向 a 时,(*p)a 完全等效(连读三遍)

上面的例子中有 *p++ ,那这到底是什么意义呢?这牵扯就到指针的运算

指针的运算

指针的运算包括「指针 ± 整型」和「指针 - 指针」

「指针 ± 整型」

指针加/减 i ,指针的值 前进/后退 i 个 指针类型 长度

1
2
3
printf("%p\n",p);     //输出 000000000061FE14
p++; //因为 p 是int型指针,1个int占4字节,故前进4
printf("%p\n",p); //输出 000000000061FE18

对于 *p++ 通过查表得知 ++* (指针运算符)优先级相同,根据右结合性先运行 p++ ,再引用变量。

此时 *p 会根据当前的地址取一个 int 长度(4 字节)并据此返回一个 int ,虽然 p++p 指向的不一定是一个恰当的地址,但指针不管这些,我们应避免这种情况发生,除非你能确定指针加减后也能指向一个 int(如数组)。也就是说,你应当只在数组中使用指针的运算

1
2
3
4
5
printf("%p\n",p);     //输出 000000000061FE14
printf("%d\n",*p); //输出 1
*p++; //等同于 *(p++)
printf("%p\n",p); //输出 000000000061FE18
printf("%d\n",*p); //输出 6422040(指针通过地址返回一个int,不管其他事)

其他的举一反三

「指针 - 指针」(没有加)

只有指向数组中的元素的指针才能相减,相减返回两者的距离(类型长度为单位),是一个整型

1
2
3
int a[10];
int *p1 = &a[2],*p2 = &a[8];
printf("%d\n",p2-p1); //输出 6

数组指针

既然提到了数组,那么就先来说说数组的指针特性

先声明一个数组

1
int a[10]={0,1,2,3,4,5,6,7,8,9};

还记得开头说的数组可以看作指针吗?(准确说是指针常量)

1
2
3
4
printf("%p\n",a);           //输出 000000000061FDF0
printf("%p\n",&a[0]); //输出 000000000061FDF0
printf("%p\n",a+2); //输出 000000000061FDF8
printf("%p\n",&a[2]); //输出 000000000061FDF8

可见 a 可以看成一个 int 类型的指针,它的值为第一个元素(即 a[0] )的地址

既然是指针,我们可以用另一种方式使用数组

1
2
printf("%d\n",a[2]);        //输出 2
printf("%d\n",*(a+2)); //输出 2

也就是说,*(a + i) 等同于 a[ i ]

PS:若想在调试时查看数组却突然以指针的格式显示(常见于把数组传递到其他函数时),可以在“查看”添加下面的表达式

1
*(int(*)[20])a //20是数组大小

既然数组是指针,我们可以直接把数组赋给指针

1
2
int *p=a;
printf("%p\n",p); //输出 000000000061FDF0

这样,我们就可以通过 p 引用 a 数组了

1
2
3
4
printf("%d\n",a[4]);        //输出 4
printf("%d\n",*(p+4)); //输出 4
*(p+4)=5; //等同于 a[4]=5
printf("%d\n",a[4]); //输出 5

下面我们来看看数组指针,但是我们先区分下 「指针数组」「数组指针」

1
2
int *p[3];    //声明了一个指针数组,该数组有3个元素,其中每个元素都是一个指向int类型的指针
int (*p)[3]; //声明了一个数组指针,指向一个大小为3的int数组,或称声明了一个类型为int[3]的指针

数组指针就是指向数组的指针,对比普通指针,区别在由于对象的不同,故数组指针在进行加减法时的单位长度不同,就是说这里 p + 1 会让 p 的值增加 4*3=12 ,数组指针一般用于配合二(多)维数组

先声明一个二维数组

1
int a[2][3]={{1,2,3},{4,5,6}};

再把它的值赋给数组指针 p

1
2
3
4
5
6
p=a;          
printf("%p\n",p); //输出 000000000061FE0
printf("%d\n",(*p)[0]);//输出了 a[0][0] 即 1
p++; //p 跳过了3个 int 长度,指向 a[1]
printf("%p\n",p); //输出 000000000061FE0C,步长为4*3=12
printf("%d\n",(*p)[2]);//输出了 a[1][2] 即 6

结构指针

先声明一个结构体

1
2
3
4
5
6
struct student
{
char name[10];
int mark;
};
struct student a;

按照指针的声明法,声明一个 struct student 类型的指针

1
struct student *p=&a;

这样,(*p) 就与 a 等价了,所以下面两句话是等价的

1
2
a.mark=100;
(*p).mark=100;

还有一种更简单的写法,使用 「指向结构体成员运算符」 ->

1
p->name=100;

综上所述,下面三种形式是等价的

  • 结构体变量.成员名
  • (*指针变量).成员名
  • 指针变量->成员名

函数指针

指向函数的指针叫函数指针

先声明一个函数

1
2
3
4
5
int Max(int a,int b)
{
if(a>b)return a;
else return b;
}

函数指针的定义法: 返回值类型 (* 指针变量名) ( [形参列表] );

注意括号不要忘

1
2
3
4
int (*p)(int,int);   //声明 p 为一个函数指针,它能指向某个输入参数为两个int,返回参数为一个int的函数
p=Max; //这样 p 就和 Max 等价了
int x=1,y=2;
printf("%d",p(x,y)); //输出 2

函数指针是一个指针,是指针就能作为变量传给函数,所以我们可以这么玩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
int Max(int a,int b)
{
if(a>b)return a;
else return b;
}
int Min(int a,int b)
{
if(a<b)return a;
else return b;
}
int (*p)(int,int)=Max;
int (*q)(int,int)=Min;
/*定义work函数,它的第三个参数是一个函数指针*/
int work(int a,int b,int (*f)(int,int))
{
return f(a,b);
}
int main()
{
int x,y;
scanf("%d %d",&x,&y);
printf("Max= %d\n",work(x,y,p));
printf("Min= %d\n",work(x,y,q));
}

这段代码能输入两个数,并输出最大值和最小值

关于指针的两个函数

严格来说,这两个是关于动态分配的,我在这也一并说了吧

malloc 函数

malloc 函数可用于分配若干字节的内存空间。若系统不能提供足够的内存单元,函数返回空指针 NULL,否则返回分配到的内存空间的起始地址。该函数对于的头文件为 stdlib.h,原型如下:

1
void *malloc(unsigned int size);

size 表示申请空间的大小(单位为字节),返回一个 void* 指针,void* 指针在上面讲过了,这里不再提

使用举例:这里声明了一个大小为 n 的数组:

1
2
3
int *a;
a = (int *)malloc(n * sizeof(int)); //在 C 中,可以省略(int *),但不建议这么做
//上面两行可以不严格地等价于 int a[n];

这里将 malloc 返回的空指针转换成 int 型指针,再赋给 a
sizeof(int) 返回系统中 int 类型所占的字节数,n * sizeof(int) 表示 nint 的空间大小

有申请就有释放的,下面的 free 函数就是 malloc 的反操作

free 函数

free 用于释放申请的空间,原型为:

1
void free(void *p);

p 为申请空间的起始地址,执行本函数就将申请的空间返还给系统

PS:指针还可以用来保存字符串常量

具体用法是在初始化时给指针赋上一个字符串,该字符串会存到常量区的一个字符数组中,这个指针会指向存放这个字符数组的首地址

1
2
3
char *s;
s="123";
printf("%s",s); // 123

函数传参的几种方式

虎头蛇尾…这边基本直接复制了,以后有啥再慢慢加哈

值传递

1
2
3
4
5
6
7
8
9
void exchange1(int x,int y)
{
int temp;
temp=x;
x=y;
y=temp;
}
int a=4,b=5;
exchange1(a,b);

值的确是传进去了,但 这个是没有用的,值传递只传递值,交换x和y不改变原来的a和b的值

地址传递

1
2
3
4
5
6
7
8
void exchange2(int *px,int *py)
{
int temp=*px;
*px=*py;
*py=temp;
}
int a=4,b=5;
exchange2(&a,&b);

将ab的地址传递给函数,对*px,*py的操作即是对a,b变量本身的操作。可以实现a,b的值交换

注意: 数组的传递不管想不想传地址,你实际上传的都是地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void add(int *a,int n)//或 void add(int a[],int n)
{
for(int i=1;i<=n;i++)a[i]++;
return;
}
int main()
{
int a[100],n;
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
add(a,n);
for(int i=1;i<=n;i++)printf("%d ",a[i]);
}

引用传递(仅限C++)

引用是变量的一个别名,调用这个别名和调用这个变量是完全一样的,如:

1
2
int* c = &a;//c是指向a的指针
int& d = b;//d是b的引用,alias of b = d
1
2
3
4
5
6
7
8
9
void exchange3(int &x,int &y)
{
int temp=x;
x=y;
y=temp;
}

int a=3,b=4;
exchange3(a,b);

仅形式参数的格式与值传递不同,内部定义域调用与值传递完全相同,可以实现ab值得对调

因为在x,y 前有一个取地址符号&,在调用exchang3(a,b)时会用a,b替换x,y,称xy引用了变量ab,在函数内部便是对实参ab进行操作了,函数 内部可以直接修改a,b的值

来源:

C语言–指针详解 - tongye - 博客园

C语言函数传递数组和传递地址的区别你知道吗_C 语言_脚本之家

函数参数传递三种方式(传值方式,地址传递,引用传递) - long_ago - 博客园


思考题:试解释下面的现象并总结 *p-=1; 的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main()
{
int a=1;
int *p=&a; //p 保存了 a 的地址
printf("%p\n",p); //输出 000000000061FE14
p++;
printf("%p\n",p); //输出 000000000061FE18
*p-=1;
printf("%p\n",p); //输出 000000000061FE17 减了1
p-=1;
printf("%p\n",p); //输出 000000000061FE13
*p-=1;
printf("%p\n",p); //输出 000000000061FE13 没减
}

参考答案