取地址运算

使用取地址符号 & 来获取一个变量的地址。

1
2
3
int a;
printf("%x",&a);
// 结果输出 60fe9c 表示变量a的地址

指针的声明

在声明指针变量的时候,变量名前加 * 表示为指针变量,可以写多个 * 来表示多重指针。

对指针变量进行赋值的时候,是给指针变量一个地址,所以需要使用 & 操作符。

1
2
3
int a,*p1,**p2;
p1=&a;
P2=&p1;

上面代码表示,声明一个整形变量 a,声明一个整形指针变量 p1,声明一个二重整形指针指针。

指针的套娃要严格地按照有多少个星号,一个多重指针只能接受来自比他少一个星号的指针的地址。

指针变量的引用

指针变量使用 * 来访问其保存的内存地址里面的数据,修改 *p 会直接修改内存里面的数据。

1
2
3
4
5
6
int a,*p1,**p2;
p1=&a;
p2=&p1;
**p2=3;
printf("%d",a);
// 输出结果是 3

malloc内存分配

首先使用之前需要声明库:stdlib.h

malloc() 函数的原型是:

1
void* malloc (size_t size);

它在执行时,需要输入一个申请空间大小(为字节大小),然后返回一个内存地址,这个内存地址为 void * 类型,也就是不确定的指针类型,所以我们一般在给指针用 malloc() 函数分配一个地址的时候一般会强制类型转换为对应的指针类型,比如:

1
int *P=(int *)malloc(sizeof(int));

这样就在内存中申请了一个整形大小的空间,然后将返回的内存地址强制转化为整形指针类型,然后给指针 P 初始化。

指针变量作为函数参数

函数参数里面也可以放置指针参数,实际上传参的时候,传入的是一个地址:

1
2
3
4
5
6
7
8
9
10
11
12
void Swap(int *x,int *y)
{
int temp=x;
x=y;
y=temp;
}
int *a,*b;
int c=1,d=2;
a=&c;
b=&d;
Swap(a,b);
Swap(&c,&d);

上面代码里面,两次交换都真正交换了a和b的值,只不过是两种不同的方法。函数的形参声明只能声明为指针类型,这样才能接受实参传过来的地址。

一维数组和指针

数组名是一个常量指针(无法被修改的指针),里面保存的值是这个数组第一个元素的地址,可以通过地址的计算来访问数组中的其他元素。

下面用代码展示,数组本质上是通过指针实现的。对于指针进行的加减运算,是对内存地址每次一个指针空间大小的加减,比如下面的 p+2 实际上地址是加了两个 int 的大小,也就是 8 位。

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

printf("%x %x\n",a,p);

printf("%d %d\n",a[2],*(a+2));
printf("%d %d\n",p[2],*(p+2));

/*输出结果
60fe74 60fe74
2 2
2 2
*/

二维数组和指针

对于二维数组,同样也是指针,不过不同的是二维数组是由一个指针数组实现的。像 a[10][10] 这样的二维数组,a[i] 就是一个指针,保存着第 i 行数组的第一个元素的内存地址。我们可以通过下面这样来使用指针来访问数组元素:

1
2
3
int a[2][5]={0,1,2,3,4,5,6,7,8,9};
printf("%x %x %d %d",*(a+0),*(a+1),*(*(a+0)+0),*((a+1)+0));
# 结果: 60fe78 60fe8c 0 5

说明了和一维数组不同的是,二维数组的数组名,是一个二重指针。

那么如何声明一个指向二维数组的指针呢,方法就是:int (*P)[5] ,这样这个 p 指针就可以指向二维数组 a[2][5] 了。

在进行指针运算的时候会涉及到行运算,也就是一次性运算跳跃 j*sizeof(int) 位的地址。所以在声明指针的时候,必须告诉编译器这个二维数组的列数,不然无法计算对应的内存地址。

要注意的是声明的时候不要写成 int *p[5] ,因为数组下表运算符 [] 的优先级高于指针运算符 *,所以这个会被定义为一个指针数组,里面可以装 5 个内存地址数据。

字符串与指针

C语言中有两种方式来保存字符串,一种方式是通过字符数组,另外一个是通过字符指针。

字符指针可以直接接受一个字符串常量或者接受一个字符数组首地址来赋值。

1
2
3
4
char string[20]="Xorex";
char *P=string;
//或者这样:
char *P="Xorex";

字符串输出的时候,printf 函数自带了特殊的参数 %s ,这个参数接受的实参是一个内存地址,然后它会将这个内存地址里面以及其后面的所有字符都输出出来,直到遇到字符串的结尾:\0 结束输出。

结构体与指针

可以声明一个结构体指针,来指向一个结构体。定义方法和其他类型一样,前面加上一个星号就可以了。

结构体指针调用结构体里面的元素也很方便,有两个种方法,一种是 (*p).point 一种是 p->point ,区别就是否使用指针运算,如果使用就是和普通的结构体调用一样了。

当然,像是套娃,什么的也是可以做到的:

1
2
3
4
5
6
7
8
9
struct Edge
{
int this;
struct Edge *Next;
}a,c,*b;

b=&a;
(*b).Next=&c;
(*((*b).Next)).this=1;

用指针对结构体数组的操作和对普通的数组操作是相同的,直接对内存地址进行运算就可以了

指针数组和指向指针的指针

指针数组前面已经提过了,和普通数组唯一不同的就是这个数组每个元素保存的都是内存地址,声明方法是 *p[n] 因为 [] 优先级高于 * 所以会先声明一个数组,然后声明这个数组是指针类型的。

然后就是指向指针的指针了,这个东西就是个套娃,声明的时候想要套几层娃就加上几个星号,然后套娃的时候只能套星数小一级的。

可以利用指针套娃来制造多维数组使用。

指向函数的指针

程序在运行的时候,函数也是放在内存里面的,每个函数都有一个入口地址,即函数的首地址。而函数名就像是数组名一样,保存着这个地址。

那么既然是保存的是内存地址,那么就轮到最擅长处理地址的指针上场了。像其他指针的一样,指向的内存地址类型需要和指针的类型照应,char 类型变量只能由 char * 类型指针指着。那么怎么声明能指向函数的指针呢?

举个例子:

1
2
3
4
5
6
int max(int x,int y)
{return x>y?x:y;}

int (*P)(int x,int y)=max;
printf("%d %d",P(1,2),(*P)(1,2));
// 输出 2 2

下面的就是声明了一个函数指针,因为指针和函数拥有相同的返回值,有用相同的形参,所以这个声明的指针就可以指向上面的 max 函数。

有意思的地方是,printf 两次输出的时候,调用的方法不一样,一次使用的指针运算符,一次没有,但是两者执行效果是一模一样的。这是因为函数名称本质上就是一个指针,它所指向的内存地址就是变量的内存地址,所以调用函数只需要给一个正确的地址就可以了,下面的输出可以表明函数名称本质上就是指向自己内存地址的指针。

1
2
3
4
5
6
printf("%x %x %x",max,&max,*max);
printf("%d %d %d\n",max(1,2),(&max)(1,2),(*max)(1,2));
/* 输出:
401560 401560 401560
2 2 2
*/

表明只需要函数所在内存地址加参数就可以调用函数了。而且函数名就是一个指向自己内存地址的指针。

最后

指针是非常强大的一种工具,这篇文章也不过是站在一个初学者角度上的基础用法总结。作为C语言最强大的利器,以后还会再彻彻底底的分析它。