对于我这个非计算机专业的人来说,指针真的很让我头疼,该如何理解指针、如何使用指针是我的痛点,但是在嵌入式中又会经常用的到,所以本文将介绍该如何求使用指针。
一、指针的概念
什么是指针?指针就是编程语言中的一个对象,利用地址,他的值直接指向存在存储器中的另一个值。由于通过地址就能找到所需的变量单元,我们就将这个地址形象化为指针,意思就是通过他能找到以他为地址的内存单元。简单来说,指针用于存放内存地址、允许程序通过地址访问数据以及修改数据。
那么为什么要有指针?我们访问数据以及修改数据直接用它所在的变量直接修改不可以吗?为什么要多此一举来通过指针作为媒介呢?其实还真不可以,其一便是指针的使用使得不同区域的代码可以轻易的共享内存数据。其二是有些操作必须使用到指针,下边会有介绍。当然还有其他原因,总之,指针是非常重要切不可替代的!
二、指针的定义与使用
有关指针的运算符号有:
& (取地址运算符):&a就表示取变量a的地址。
* (指针运算符) :*p表示取p所指向的变量的内容。
2.1 通过指针修改变量的值
int a = 1; //定义一个整型变量a并赋值为1
int *p = &a; //p为int*类型,即整型指针,并将a地址赋给p
printf("a = %d\n",a); //打印出a的值
*p = 100; //将a的值变为100,*p就是a
printf("a = %d\n",a); //打印出a的值
可以发现a的值已经改变,这是指针中最简单的用法了。
2.2 指针步长的介绍
在64位系统中,指针占8字节(不管是int *、char *等),那么既然都是8字节,那是不是定义指针可以随便定义呢?例如:
char ch = 'a';
int *q = &ch;
char b = *q;
printf("b = %c\n",b);
就会产生警告,第一是因为我们要保存的是字符型的地址,不能用int *,编译也会有类型不兼容警告。第二便是步长(单位)不一样,对于这点下面我做出详细介绍:
虽然指针变量的大小都一样,但是不同类型的指针变量,取指针指向的内容宽度是与类型有关系的。假如是char *的指针,在取内容时它只能取出一个字节的内容,而如果是int*类型的指针,去内容时只能取出4个字节的内容。于是乎,不同类型的指针宽度是不同的,这也就产生了步长的概念。
所谓步长就是不同类型的指针变量,取指针所指向的内容宽度。在代码中,步长就是指针变量+1所跨过的字节大小,步长的大小往往等于该指针变量所存储的类型的地址对应的那个类型的大小。
例如int a=1;占四个字节,(假设四个字节地址为0x100 0x101 0x102 0x103);char *q的地址为0x200;在C语言中有规定,无论变量占多少字节,取地址时一定取的最前边字节的地址,故用0x100表示整个变量a的地址(数组也是同理)。这里再定义一个int *p = &a;那么p++后地址为0x104,q++后地址为0x201。这也是步长不一样导致的,简单的理解为:p为指针,指向的是int a这个整体,当p+1相当于指向下一个整型了,而一个整型跨四个字节,故p+1加四个字节。q也类似,q指向的为字符,字符占一个字节,故q+1加一个字节。
2.3 指针作为函数参数
假设有两个变量a,b要在某一个函数进行处理:void name(int a,int b);由于形参不会改变实参的值,函数外定义的a、b为实参,而函数体的a、b为形参,如下:
#include<stdio.h>
void name(int x,int y)
{
x=8;
y=9;
}
void main()
{
int a=1,b = 1;
name(a,b);
printf("a=%d\n,b=%d\n",a,b);
}
输出值为:a=1,b=1;当然即使你将a和b定义为全局变量也不能改变其值,如果是一个变量我们可以将这个处理后的值返回出来,如下:
int a=1,b = 1;
int name(int x)
{
x=8; return x;
}
void main()
{
a = name(a);
printf("a=%d,b=%d\n",a,b);
}
但如果涉及到多个变量就手足无措了,因为返回值只有一个,我该返回a还是b的值呢?保大还是保小?难道就不能都保吗?当然可以,这里就要用到指针了,如下:
int a=1,b = 1;
void name(int *x,int *y)
{
*x=8;
*y=9;
}
void main()
{
name(&a,&b);
printf("a=%d\n,b=%d\n",a,b);
}
此时a和b的值分别为8和9,为什么指针作为形参就能改变呢?首先我们将a和b的地址作为形参(对a和b取地址),然后在函数中我们分别将a和b的地址传给x、y,而x、y为指针,*x就是a,*y就是b,所以能改变a和b的值。
2.4 指针的运算
相信通过上面的介绍,对指针有了初步理解,这里再介绍一下指针的运算。
int a=1,b,*p,*q;
void main()
{
p = &a;
q = p; printf("q=%d\n",*q);
b = ++*p; printf("b=%d ",b); printf("p Address:%d\n",p);
b = *p++; printf("b=%d ",b); printf("p=%d ",*p); printf("p Address:%d\n",p);
b = (*p)++; printf("b=%d ",b); printf("p=%d ",*p); printf("p Address:%d\n",p);
}
注:
对于指针p带“ * ”和不带“ * ”是有区别的,p表示的为地址,而*p表示此地址的值,所以我们第一行不能写成*p = &a,*p就是a,不能将一个地址赋给一个变量。
而指针和指针间是可以直接赋值的,如第二行,结果就是q也存储的是a的地址,也就是*q就是a,所以*q输出值为1。
++*p的意思是先将*p自增,也就是a加1得到二,然后将a的值赋给b,得到b的值为2,此时p所指向的地址为4206608。
*p++是先将*p的值赋给b,所以b的值还是2,然后再将p所指向的地址增加一个步长,由于是int型变量,步长为4,所以p的地址变为了4206612,而这个地址里的值为0,所以*p是的值为0。
(*p)++是将*p的值先赋给b,然后将*p的值加1,经过上一行代码,此时p并未指向a,而是a的下一个int型的地址,值为0,故b的值为0,而*p加1的值为1,故*p的值为1,p的地址不变还是4206612。
三、指针与数组
3.1 通过指针访问数组
#include<stdio.h>
int a[5]={1,2,3,4,5};
void main()
{
int *p = a;
int *q = &a[0];
for(unsigned char i = 0;i<5;i++)
printf("%d ",*(p+i));
printf("\n");
for(unsigned char i = 0;i<5;i++)
printf("%d ",*(q+i));
}
其中第一句和第二句是等价的,指针会指向数组的第一个元素,也就是a[0],其输出结果还是一样的:
有没有发现,这次将指针指向数组时并没有用取地址符号“ & ”,这里又涉及到了一个知识点,想让一个指针变量指向一个数组或字符串的首地址,就不需要使用&,因为数组名或字符串字面量本身就代表了他们的首地址。
3.2 通过数组访问指针
void main()
{
char *str = "Hello World";
printf("%c\n",*str);
for(unsigned char i = 0;i<11;i++)
printf("%c ",str[i]);
}
第一行定义一个指针变量指向"Hello World"这个字符串常量,c语言中字符串常量是按照字符数组来处理的,只是这个字符串数组没有名字,所以引用这个字符串只能通过指针变量来引用(即可以理解为没有名字,通过访问地址来引用)。
由于是字符数组,所以指针指向的为数组的第一个元素,也就是H,所以第二行打印出来的值为H。
由于char型的步长为1个字节,所以指针自增会指向下一个字符,指针和数组在使用角度上是可以替换的,指针可以用数组表示数组也可以用指针表示。所以输出结果如下:
当然,假如改为int型指针,输出结果就会为H o r 。
3.3 指针和数组的区别
上面介绍了数组可以表示指针,指针也可以表示数组,但指针与数组完全等价吗?其实不然,我们在定义一个数组时,系统会分配一块连续可用的内存给数组,其地址是不能改变的,但是其值可以修改。例如:
可以发现在第10行报错了,虽然一维数组的数组名代表第一个元素的地址,但是是相当于一个常指针(地址常量),其不能修改。而p虽然指向的是一个常量字符串,但是它本身为指针变量,其值可以改变,但其指向的数据内容"Hello World" 确是不能修改的,为一个字符串常量放在静态存储区。还有一点就是两者所占空间内存大小也不同,str所占空间为32,而p所占空间是8字节,所以说两者是有类似的地方但并不是完全互等。
3.4 指针数组
指针数组是数组!一个数组的元素值为指针,则该数组是指针数组。指针数组的所有元素都必须是具有相同存储类型和指向相同数据类型的指针变量。
指针数组说明的一般形式为:
类型说明符 *数组名[数组长度] 例:int *p[5];
表示p为一个指针数组,有五个元素,每个元素值都为一个指针,指向整型变量。
int main()
{
char *string[] = {"Hello","World"};
printf("%s",*string);
return 0;
}
string就是指针数组,根据优先级string会优先和[ ]结合,故string首先为数组,再与*结合变为指针,也就产生了指针数组。string有两个元素都为指针,分别指向"Hello"、"World",故string占16个字节,注:其中"Hello","World"分别都存到了内存的只读数据区里面,为字符串常量。那么这个输出值为多少呢?毫无疑问为"Hello",string等于数组首个元素的地址,也就是"Hello"。如果想输出另一个字符串可以用*(string++)来输出,或者使用以下代码就能输出完整的Hello World
int main()
{
char *string[] = {"Hello","World"};
printf("%s %s",string[0],string[1]);
return 0;
3.5 数组指针
数组指针是一个指针!该指针指向(一维或二维)数组。注意:指针是指向整个数组,而不是数组的第一个元素!!
数组指针说明的一般形式为:
类型说明符 (*指针名)[数组长度] 例:int (*p)[5];
表示p为一个指针指向一个数组,该数组有五个元素,每个元素值都为一个整型。
如何去区分指针数组以及数组指针?
1. 数组指针”和“指针数组”,只要在名词中间加上“的”字,就知道中心了——
数组的指针:是一个指针,什么样的指针呢?指向数组的指针。
指针的数组:是一个数组,什么样的数组呢?装着指针的数组。
2. 由于优先级顺序:()>[]>*,所以“带括号的为数组指针,不带括号的为指针数组”。(*p)[n]:根据优先级,先看括号内,则p是一个指针,向右看为中括号,所以这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针;
*p[n]:根据优先级,先看[],则p是一个数组,再结合*,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组。
void main()
{
int a[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6}};
int i,j;
/* for(i=0;i<3;i++)
{
for(j = 0;j<4;j++)
printf("%d ",a[i][j]);
printf("\n");
}
*/
int (*p)[4] = a;
for(i=0;i<3;i++)
{
for(j = 0;j<4;j++)
printf("%d ",(*p)[j]);
*p++;
printf("\n");
}
}
为了方便大家理解数组指针,这里定义了一个二维数组a,有三行四列:
我们定义一个一维数组指针,其中有四个值,指向二维数组首行地址。也就是*p相当于a[0],我们利用for(j = 0;j<4;j++) printf("%d ",(*p)[j]);就能将第一行数据遍历出来。然后让p指向下一行地址,执行*p++,就能将p的地址变为a的第二行地址,就这样就将二维数组遍历出来了。
四、函数与指针
5.1 函数指针
原型: 返回类型(*指针变量名)(参数类型列表)
返回类型:为函数返回的数据类型
(*指针变量名):这里的*表示这是一个指针,而括号是必须的,以区分函数指针和函数返回指针的声明。
参数类型列表:这是函数函数的类型列表,逗号分隔。
函数指针实际上是一个指针,指向的对象是一个函数。
void Hello(void)
{
printf("Hello");
}
void main()
{
void (*p)();
p = Hello;
p();
}
首先定义了一个名为Hello的函数打印Hello字符串。
main函数第一行定义了一个函数指针,其返回类型为void,函数名为p,没有形参。
第二行是将函数赋值给p,使p指向函数Hello。
第三行就是通过函数指针p去调用这个函数,等价于Hello(); 注意不要写成了p=f1();这样的意思是将f1函数的返回值赋值给p。
最后打印出来的值就是Hello
如果有形参有返回值的函数如何定义呢?如下,首相定义了一个相加函数,在main函数中定义了一个函数指针p,第一行注释掉的代码等价于下边两行,最后打印出来的结果为3。
int Add(int x,int y)
{
return x+y;
}
void main()
{
//int (*p)(int,int) = Add;
int (*p)(int,int);
p = Add;
printf("%d\n",p(1,2));
}
函数指针还有一种定义方式:
#include<stdio.h>
int Add(int x,int y)
{
return x+y;
}
typedef int (*T)(int,int);
void main()
{
//int (*p)(int,int) = Add;
int (*p)(int,int);
p = Add;
printf("%d ",p(1,2));
T q1 = Add;
T q2 = Add;
printf("%d ",q1(2,2));
printf("%d ",q2(3,2));
}
其中typedef int (*T)(int,int);是声明一个新的类型T,T表示函数指针类型,咋一看和结构体很像是不是?其输出结果为:
是不是感觉很鸡肋?函数名一般是用它功能命名的,这样写既复杂又难懂,直接调用函数不来的方便一些且更号理解一些?看见Add函数名就猜到了是相加,但是一个q指针函数你一下能理解其意思?太鸡肋了吧?哈哈哈哈,别急,其实函数指针还是有很大用处的,下边听我细细道来。回调函数听过吧?在STM32的HALL库中可谓是很常见了,这便是函数指针的用处了。
5.2 函数指针的用法之——回调函数
上边提到了Add函数是将两个数字相加,那么要将两个数字相减呢?要么是再建立一个相减函数、要么就是在Add函数中再加一个标志位,成立执行相加,不成立则执行相减。但这两种方法实现的代码会更长,这里只是一个简单的加减功能,如果更复杂的功能那代码不是很长吗?如何在一个函数中实现加减乘除功能呢?该如何做呢?
#include<stdio.h>
int Add(int x,int y)
{
return x+y;
}
int minus(int x,int y)
{
return x-y;
}
int multiply(int x,int y)
{
return x*y;
}
int mdivide(int x,int y)
{
return x/y;
}
int calculation(int x,int y,int (*p)(int,int))
{
return p(x,y);
}
void main()
{
int end;
end = calculation(10,2,Add);
printf("ADD=%d ",end);
end = calculation(10,2,minus);
printf("minus=%d ",end);
end = calculation(10,2,multiply);
printf("multiply=%d ",end);
end = calculation(10,2,mdivide);
printf("mdivide=%d ",end);
}
这里通过函数指针的形式使在一个函数中实现了多种功能,我们在不修改calculation函数的情况下,我们就实现了加减乘除的算法。那么回调函数的意义是什么?回调函数可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
5.3 指针函数
原型: 类型说明符 *函数名(参数类型列表)
类型说明符:为函数返回的指针的数据类型
*函数名:指针函数的名字。
参数类型列表:这是函数函数的类型列表,逗号分隔。
指针函数实际上是一个函数,但其返回值为一个指针。C语言中允许一个函数的返回值为一个指针(地址),这种返回指针的函数称为指针函数,注意:不能返回局部变量的地址!!
这里不要把指针函数和函数指针弄混淆了:
指针函数是函数,返回值为指针
函数指针是指针,指向的对象是函数
int(*p)() 和 int *p() 区别?
int(*p)():定义的是一个变量,表示p是一个指向函数入口的指针变量,该函数的返回值是整型,(*p)两边的括号不能少。
int *p(): 不是变量而是函数声明,说明p是一个指针型函数,其返回值是一个指向整型量的指针,*p两边没有括号。
五、指针与结构体
通过指向结构体变量的指针变量输出结构体变量中成员的信息。
struct student
{
char name[4];
char age;
}*P;
//或者写成
struct student
{
char name[4];
char age;
};
student *p;
如何使用指针访问结构体成员呢?
方法一: (*p).name = "ABC";
方法二: p->name = "ABC";
六、指针的指针
void main()
{
int a = 0;
int *p = &a;
int **q = &p;
printf("a=%d ",a);
*p = 1; printf("a=%d ",a);
**q = 5; printf("a=%d ",a);
}
我们定义了一个整型变量 a赋值为0,在定义一个整型指针指向a,即*p就是a。再定义了一个指针的指针指向p,即*q就是p,而**q就是a了。如果我们想修改a的值可以通过这两个指针去修改它,打印的结果为:
那么可不可以让**q = &a呢?显而易见是不可以的,应为q是int **类型,只能指向int *型的变量,而p为int *型的变量,a为int型变量,所以不可以。
七、判断c语言复杂类型声明——右左法则
首先右左法则并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。
右左法则:首先从最里面的圆括号看起,然后往右看,再往左看,每当遇到圆括号时,就应该调转阅读方向。下边来举几个例子来帮助大家理解左右法则:
例1:int (*(**fun)[N])(int)
int (*(**fun)[N])(int)
1. 首先看圆括号里面,为指针的指针,fun指向一个指针,至于是什么样的指针,我们向右看。
2. 右看为[N],很明显为数组,所以fun指向的是一个数组,为数组指针,所以fun是指向数组指针的指针。
3. 再往左看为*,表示这个数组里的每个元素都是指针,什么样的指针呢?我们再向右看:
4. 右看为括号,括号除了表示优先级外,还可以作为函数调用运算符,所以这个数组里面的指针指向函数,而这个函数的参数是一个int,(*(**fun)[N])(int)是指向函数指针的数组指针的指针。
5. 再往左看为int,即这个函数的返回值为int,所以这个指针指向的是一个参数为int、返回值为int的函数。
例2:int *(*(*fp)(int))[10];
int *(*(*fp)(int))[10];
1. 找到了变量名fp,向右看为右括号,再向左看为*号,所以fp为指针。然后我们继续向又看:
2. 为左括号,所以fp为指向函数的指针,函数参数为int型。然后向左看:
3. 左看为*号,所以可以确定为为指针函数,其返回值为指针(没有括号,即不是*((*fp),那么它指向的是什么呢?我们再向右看:
4. 右看为中括号,所以说明为数组,所以该指针是指向数组,其数组有10个元素。再往左看:
5. 只剩下了int *,表示数组中的每个元素都是整型指针,
总结:fp为指针,指向函数,被函数的参数为整型,其返回值为指向数组的指针。数组有10个元素,每个元素都是整型指针。