c语言中的指针应该怎么理解?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c语言中的指针应该怎么理解?相关的知识,希望对你有一定的参考价值。

这是我收录的指针讲义

第一章。指针的概念

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的 类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让 我们分别说明。
先声明几个指针放着做例子:
例一:
(1)int *ptr;
(2)char *ptr;
(3)int **ptr;
(4)int (*ptr)[3];
(5)int *(*ptr)[4];

1。 指针的类型。
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的 类型:
(1)int *ptr; //指针的类型是int *
(2)char *ptr; //指针的类型是char *
(3)int **ptr; //指针的类型是 int **
(4)int (*ptr)[3]; //指针的类型是 int(*)[3]
(5)int *(*ptr)[4]; //指针的类型是 int *(*)[4]
怎么样?找出指针的类型的方法是不是很简单?

2。指针所指向的类型。
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译 器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符 *去掉,剩下的就是指针所指向的类型。例如:
(1)int *ptr; //指针所指向的类型是int
(2)char *ptr; //指针所指向的的类型是char
(3)int **ptr; //指针所指向的的类型是 int *
(4)int (*ptr)[3]; //指针所指向的的类型是 int()[3]
(5)int *(*ptr)[4]; //指针所指向的的类型是 int *()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越 来越熟悉时,你会发现,把与指针搅和在一起的"类型"这个概念分成"指针的 类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。我看了不 少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。

3。 指针的值,或者叫指针所指向的内存区或地址。
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。
指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块
内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里?
4。 指针本身所占据的内存区。
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。
指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。

第二章。指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:
例二:
1。 char a[20];
2。 int *ptr=a;
...
...
3。 ptr++;
在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整 形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。
由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。
我们可以用一个指针和一个循环来遍历一个数组,看例子:
例三:
int array[20];
int *ptr=array;
...
//此处略去为整型数组赋值的代码。
...
for(i=0;i<20;i++)

(*ptr)++;
ptr++;

这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所
以每次循环都能访问数组的下一个单元。
再看例子:
例四:
1。 char a[20];
2。 int *ptr=a;
...
...
3。 ptr+=5;
在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5 乘sizeof(int),在32位程序中就是加上了5乘4=20。由于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。
如果上例中,ptr是被减去5,那么处理过程大同小异,只不过ptr的值是被减去5乘sizeof(int),新的ptr指向的地址将比原来的ptr所指向的地址向低地址方向移动了20个字节。

总结一下,一个指针ptrold加上一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值增加了n乘sizeof(ptrold所指向的类型)个字节。就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向高地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。
一个指针ptrold减去一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值减少了n乘sizeof(ptrold所指向的类型)个字节,就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。

第三章。运算?amp;和*

这里&是取地址运算符,*是...书上叫做"间接运算符"。
&a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型是a的类型,指针所指向的地址嘛,那就是a的地址。
*p的运算结果就五花八门了。总之*p的结果是p所指向的东西,这个东西有这些特点:它的类型是p指向的类型,它所占用的地址是p所指向的地址。
例五:
int a=12;
int b;
int *p;
int **ptr;
p=&a;//&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址 是a的地址。
*p=24;//*p的结果,在这里它的类型是int,它所占用的地址是p所指向的地 址,显然,*p就是变量a。
ptr=&p;//&p的结果是个指针,该指针的类型是p的类型加个*,在这里是int
**。该指针所指向的类型是p的类型,这里是int*。该指针所指向的地址就是指针p自己的地址。
*ptr=&b;//*ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所 指向的类型是一样的,所以用&b来给*ptr赋值就是毫无问题的了。
**ptr=34;//*ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指
针再做一次*运算,结果就是一个int类型的变量。

第四章。指针表达式。

一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表达式。
下面是一些指针表达式的例子:
例六:
int a,b;
int array[10];
int *pa;
pa=&a;//&a是一个指针表达式。
int **ptr=&pa;//&pa也是一个指针表达式。
*ptr=&b;//*ptr和&b都是指针表达式。
pa=array;
pa++;//这也是指针表达式。
例七:
char *arr[20];
char **parr=arr;//如果把arr看作指针的话,arr也是指针表达式
char *str;
str=*parr;//*parr是指针表达式
str=*(parr+1);//*(parr+1)是指针表达式
str=*(parr+2);//*(parr+2)是指针表达式

由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。
好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。
在例七中,&a不是一个左值,因为它还没有占据明确的内存。*ptr是一个左值,因为*ptr这个指针已经占据了内存,其实*ptr就是指针pa,既然pa已经在内存中有了自己的位置,那么*ptr当然也有了自己的位置。

第五章。数组和指针的关系

如果对声明数组的语句不太明白的话,请参阅我前段时间贴出的文?lt;>。
数组的数组名其实可以看作一个指针。看下例:
例八:
int array[10]=0,1,2,3,4,5,6,7,8,9,value;
...
...
value=array[0];//也可写成:value=*array;
value=array[3];//也可写成:value=*(array+3);
value=array[4];//也可写成:value=*(array+4);
上例中,一般而言数组名array代表数组本身,类型是int [10],但如果把a
rray看做指针的话,它指向数组的第0个单元,类型是int *,所指向的类型是数组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个指向数组第3个单元的指针,所以*(array+3)等于3。其它依此类推。
例九:
char *str[3]=
"Hello,this is a sample!",
"Hi,good morning.",
"Hello world"
;
char s[80];
strcpy(s,str[0]);//也可写成strcpy(s,*str);
strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));
strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));
上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char**,它指向的类型是char *。
*str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串"Hello,this is a sample!"的第一个字符的地址,即’H’的地址。
str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向的类型是char *。
*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,good morning."的第一个字符’H’,等等。

下面总结一下数组的数组名的问题。声明了一个数组TYPE array[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE [n];第二,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。
在不同的表达式中数组名array可以扮演不同的角色。
在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数
测出的是整个数组的大小。
在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第0号单元的值。sizeof(*array)测出的是数组单元的大小。
表达式array+n(其中n=0,1,2,....。)中,array扮演的是指针,故arr
ay+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。
例十:
int array[10];
int (*ptr)[10];
ptr=&array;
上例中ptr是一个指针,它的类型是int (*)[10],他指向的类型是int [10]
,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。

本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究
竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:

int (*ptr)[10];
则在32位程序中,有:
sizeof(int(*)[10])==4
sizeof(int [10])==40
sizeof(ptr)==4
实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。

第六章。指针和结构类型的关系

可以声明一个指向结构类型对象的指针。
例十一:
struct MyStruct

int a;
int b;
int c;

MyStruct ss=20,30,40;//声明了结构对象ss,并把ss的三个成员初始
化为20,30和40。
MyStruct *ptr=&ss;//声明了一个指向结构对象ss的指针。它的类型是
MyStruct*,它指向的类型是MyStruct。
int *pstr=(int*)&ss;//声明了一个指向结构对象ss的指针。但是它的
类型和它指向的类型和ptr是不同的。

请问怎样通过指针ptr来访问ss的三个成员变量?
答案:
ptr->a;
ptr->b;
ptr->c;
又请问怎样通过指针pstr来访问ss的三个成员变量?
答案:
*pstr;//访问了ss的成员a。
*(pstr+1);//访问了ss的成员b。
*(pstr+2)//访问了ss的成员c。
呵呵,虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用p
str来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指
针来访问数组的各个单元:
例十二:
int array[3]=35,56,37;
int *pa=array;
通过指针pa访问数组array的三个单元的方法是:
*pa;//访问了第0号单元
*(pa+1);//访问了第1号单元
*(pa+2);//访问了第2号单元
从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。
所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干?quot;填充字节",这就导致各个成员之间可能会有若干个字节的空隙。
所以,在例十二中,即使*pstr访问到了结构对象ss的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,
嘿,这倒是个不错的方法。
通过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。

第七章。指针和函数的关系

可以把一个指针声明成为一个指向函数的指针。
int fun1(char*,int);
int (*pfun1)(char*,int);
pfun1=fun1;
....
....
int a=(*pfun1)("abcdefg",7);//通过函数指针调用函数。
可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为
实参。
例十三:
int fun(char*);
int a;
char str[]="abcdefghijklmn";
a=fun(str);
...
...
int fun(char*s)

int num=0;
for(int i=0;i

num+=*s;s++;

return num;
)
这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s后,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是str和s各自占用各自的存储空间。在函数体内对s进行自加1运算,并不意味着同时对str进行了自加1运算。

第八章。指针类型转换

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。
例十四:
1。 float f=12.3;
2。 float *fptr=&f;
3。 int *p;
在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的语句吗?
p=&f;
不对。因为指针p的类型是int*,它指向的类型是int。表达式&f的结果是一
个指针,指针的类型是float*,它指向的类型是float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行"强制类型转换":
p=(int*)&f; 如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*和TYPE,
那么语法格式是:
(TYPE*)p;
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。

一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,也会发生指针类型的转换。
例十五:
void fun(char*);
int a=125,b;
fun((char*)&a);
...
...
void fun(char*s)

char c;
c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;


注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a的结果是一个指针,它的类型是int *,它指向的类型是int。形参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指针 char*temp,然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指向的地址就是a的首地址。

我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:
unsigned int a;
TYPE *ptr;//TYPE是int,char或结构类型等等类型。
...
...
a=20345686;
ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制

ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制)
编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法:
unsigned int a;
TYPE *ptr;//TYPE是int,char或结构类型等等类型。
...
...
a=某个数,这个数必须代表一个合法的地址;
ptr=(TYPE*)a;//呵呵,这就可以了。
严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYPE*)的意思是把无符号整数a的值当作一个地址来看待。
上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候,就会出现非法操作错误。

想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:
例十六:
int a=123,b;
int *ptr=&a;
char *str;
b=(int)ptr;//把指针ptr的值当作一个整数取出来。
str=(char*)b;//把这个整数的值当作一个地址赋给指针str。

好了,现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。

第九章。指针的安全问题

看下面的例子:
例十七:
char s=’a’;
int *ptr;
ptr=(int*)&s;
*ptr=1298;
指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。
让我们再来看一例:
例十八:
1。 char a;
2。 int *ptr=&a;
...
...
3。 ptr++;
4。 *ptr=115;
该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。
在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。
在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大
于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明白的。

如果对你有所帮助,请记得采纳最佳答案,谢谢
参考技术A 我在想,描述指针P时,可不可以换其他的词语来辅助理解“指针”这个词语,因为“指针”这个词语确实不太好理解。你想啊,在描述整型变量、字符变量的时候,大家都很好理解,因为“整型”、“字符”型、“字符串”型这样的词语大家都很好理解,你说“指针”这个词语大家都不多见,这怎么理解呢?难不成还要拿个词语大全先去查下“指针”的汉语词义吗?在讲解指针P时,可以说P是一个地址变量,此时先不要说“指针”这个词语,而是说“地址”这个词语,也就是地址变量P怎么怎么。那么定义指针变量P时的写法:nt *P 也可以暂时写为 address P。这样初学者学习到指针P时会不会一目了然:哦,address这个单词我小学就学过了,是地址的意思,那么变量P的类型就是地址类型咯。 参考技术B 我觉得要理解指针必须先了解程序存储原理,也就是代码和数据在内存中的组织情况。了解了这些后,指针其实就是内存地址而已。C对指针理解比较纠结一些,你也可以先去看看汇编,汇编可以理解为无类型,使用各种寻址方式来实现数据存取。C中的指针操作也就这么几种,比如强制类型转换,可以简单的理解为立即数的赋值,值的内容就是内存地址。再如指针的加减,始终记住加减的是该指针指向数据类型长度的多少倍,int16 * Dataptr =xxx; Dataptr++;这里的加加相当于地址直接加2。还有一个要记住:指针的指针还是指针。 参考技术C 通俗的说指针就是内存地址的标识。比如说p是一个指针,它可以指向一个内存地址。在C语言中用*来定义指针,如int *p;就定义了一个整形的指针p,也用*来引用指针指向的值,如*p=12;就是给p指针指向的内存赋值为12。而&是用来取地址的,如scanf(&a,12);p指针本身是存一个地址,占4个字节,一般情况我使用的是它存的地址所在的内容。 参考技术D 指针实际是一个整数数值,这个数字代表操作系统分配的内存的一个位置(通常成为“地址”)。举个排队的例子,比如一个门口有很多人依次排队,我们可以从排队的第一个人开始,依次给每个人一个唯一的号码,我们通过号码,就能快速找到对应的排队的人。相似的,当我们通过指针得到内存地址后,再通过指针类型(char、int、long等),可以提取从这个内存位置开始的指定bit的二进制数据,这个数据可以被程序执行来使用。

带你一步一步理解C语言指针!

一直觉得C语言较其他语言最伟大的地方就是C语言中的指针,有些人认为指针很简单,而有些人认为指针很难,当然这里的对简单和难并不是等价于对指针的理解程度。为此作者在这里对C语言中的指针进行全面的总结,从底层的内存分析,彻底让读者明白指针的本质。

小编认为C指针应该和C语言中的变量放在一起,因为C指针本质上还是一个变量,但现在大部分教材将其单独拿出来讲解,这也使得很多初学者认为指针是一个和变量毫无相关的概念。

一、指针变量

首先读者要明白指针是一个变量,为此作者写了如下代码来验证之:

#include "stdio.h"

int main(int argc, char **argv)

    unsigned int a = 10;
    unsigned int *p = NULL;
    p = &a;
    printf("a=%d\\n",a);
    printf("&a=%d\\n",&a);
    *p = 20;
    printf("a=%d\\n",a);
    return 0;

运行后可以看到a的值被更改了,上面的例子可以清楚的明白指针实质上是一个放置变量地址的特殊变量,其本质仍然是变量。

既然指针是变量,那必然会有变量类型,因此这里必须对变量类型做解释。在C语言中,所有的变量都有变量类型,整型、浮现型、字符型、指针类型、结构体、联合体、枚举等,这些都是变量类型。变量类型的出现是内存管理的必然结果,相信读者知道,所有的变量都是保存在计算机的内存中,既然是放到计算机的内存中,那必然会占用一定的空间,问题来了,一个变量会占用多少空间呢,或者说应该分出多少内存空间来放置该变量呢?

为了规定这个,类型由此诞生了,对于32位编译器来说,int类型占用4个字节,即32位,long类型占用8字节,即64位。这里简单说了类型主要是为后面引出指针这个特殊性,在计算机中,将要运行的程序都保存在内存中,所有的程序中的变量其实就是对内存的操作。计算机的内存结构较为简单,这里不详细谈论内存的物理结构,只谈论内存模型。将计算机的内存可以想象为一个房子,房子里面居住着人,每一个房间对应着计算机的内存地址,内存中的数据就相当于房子里的人。

既然指针也是一个变量,那个指针也应该被存放在内存中,对于32位编译器来说,其寻址空间为2^32=4GB,为了能够都操作所有内存(实际上普通用户不可能操作所有内存),指针变量存放也要用32位数即4个字节。这样就有指针的地址&p,指针和变量的关系可以用如下图表示:

从上图可以看到&p是指针的地址,用来存放指针p,而指针p来存放变量a的地址,也就是&a,还有一个*p在C语言中是解引,意思是告诉编译器取出该地址存放的内容。

上面提到过关于指针类型的问题,针对32位编译器而言,既然任何指针都只占用4个字节,那为何还需要引入指针类型呢?仅仅是为了约束相同类型的变量么?实际上这里不得不提到指针操作,先思考如下两个操作:

上面两个操作的意思是不同的,先说下第一种:p+1操作,如下图所示:

对于不同类型指针而言,其p+1所指向的地址不同,这个递增取决于指针类型所占的内存大小,而对于((unsigned int)p)+1,该意思是将地址p所指向的地址的值直接转换为数字,然后+1,这样无论p是何种类型的指针,其结果都是指针所指的地址后一个地址。

从上述可以看到,指针的存在使得程序员可以相当轻松的操作内存,这也使得当前有些人认为指针相当危险,这一观点表现在C#和Java语言中,然而实际上用好指针可以极大的提高效率。下面深入一点来通过指针对内存进行操作,现在我们需要对内存6422216中填入一个数据125,我们可以如下操作:

unsigned int *p=(unsigned int*)(6422216);
*p=125;

当然,上面的代码使用了一个指针,实际上C语言中可以直接利用解引操作对内存进行更方便的赋值,下面说下解引操作*

二、解引用

所谓解引操作,实际上是对一个地址操作,比如现在想将变量a进行赋值,一般操作是a=125,现在我们用解引操作来完成,操作如下:

*(&a)=125;

上面可以看到解引操作符为*,这个操作符对于指针有两个不同的意义,当在申明的时候是申明一个指针,而当在使用p指针时是解引操作,解引操作右边是一个地址,这样解引操作的意思就是该地址内存中的数据。这样我们对内存6422216中填入一个数据125就可以使用如下操作

*(unsigned int*)(6422216)=125;

上面需要将6422216数值强制转换为一个地址,这个是告诉编译器该数值是一个地址。值得注意的是上面的所有内存地址不能随便指定,必须是计算机已经分配的内存,否则计算机会认为指针越界而被操作系统杀死即程序提前终止。

三、结构体指针

结构体指针和普通变量指针一样,结构体指针只占4个字节(32位编译器),只不过结构体指针可以很容易的访问结构体类型中的任何成员,这就是指针的成员运算符->。

上图中p是一个结构体指针,p指向的是一个结构体的首地址,而p->a可以用来访问结构体中的成员a,当然p->a*(p)是相同的。

四、强制类型转换

为何要在这里提强制类型转换呢,上面的测试代码可以看到编译器会报很多警告,意思是告诉程序员数据类型不匹配,虽然并不影响程序的正确运行,但是很多警告总会让人感到难受。因此为了告诉编译器代码这里没有问题,程序员可以使用强制类型转换来将一段内存转换为需要的数据类型,例如下面有一个数组a,现在将其强制转换为一个结构体类型stu:

#include <stdio.h>

typedef struct STUDENT

    int      name;
    int    gender;
stu;

int a[100]=10,20,30,40,50;

int main(int argc, char **argv)

    stu *student;
    student=(stu*)a;
    printf("student->name=%d\\n",student->name);
    printf("student->gender=%d\\n",student->gender);
    return 0;

上面的程序运行结果如下:

可以看到a[100]被强制转换为stu结构体类型,当然不使用强制类型转换也是可以的,只是编译器会报警报。

上图为程序的示意图,图中数组a[100]的前12个字节被强制转换为了一个struct stu类型,上面仅对数组进行了说明,其它数据类型也是一样的,本质上都是一段内存空间。

五、void指针

为何在这里单独提到空指针类型呢?,主要是因为该指针类型很特殊。void类型很容易让人想到是空的意思,但对于指针而言,其并不是指空,而是指不确定。在很多时候指针在申明的时候可能并不知道是什么类型或者该指针指向的数据类型有多种再或者程序员仅仅是想通过一个指针来操作一段内存空间。

这个时候可以将指针申明为void类型。但是问题来了,由于void类型原因,对于确定的数据类型解引时,编译器会根据类型所占的空间来解引相应的数据,例如int *p,那么*p就会被编译器解引为p指针的地址的4个字节的空间大小。但对于空指针类型来说,编译器如何知道其要解引的内存大小呢?先看一段代码:

#include <stdio.h>

int main(int argc, char **argv)

    int a=10;
    void *p;
    p=&a;
    printf("p=%d\\n",*p);
    return 0;

编译上面的程序会发现,编译器报错,无法正常编译。

这说明编译器确实是在解引时无法确定*p的大小,因此这里必须告诉编译器p的类型或者*p的大小,如何告诉呢?很简单,用强制类型转换即可,如下:

*(int*)p

这样上面的程序就可以写为如下:

#include <stdio.h>

int main(int argc, char **argv)

    int a=10;
    void *p;
    p=&a;
    printf("p=%d\\n",*(int*)p);
    return 0;

编译运行后:

可以看到结果确实是正确的,也和预期的想法一致。由于void指针没有空间大小属性,因此void指针也没有++操作。

六、函数指针

函数指针使用

函数指针在Linux内核中用的非常多,而且在设计操作系统的时候也会用到,因此这里将详细讲解函数指针。既然函数指针也是指针,那函数指针也占用4个字节(32位编译器)。下面以一个简单的例子说明:

#include <stdio.h>

int  add(int a,int b)

    return a+b;


int main(int argc, char **argv)

    int (*p)(int,int);
    p=add;
    printf("add(10,20)=%d\\n",(*p)(10,20));
    return 0;

程序运行结果如下:

可以看到,函数指针的申明为:

函数指针的解引操作与普通的指针有点不一样,对于普通的指针而言,解引只需要根据类型来取出数据即可,但函数指针是要调用一个函数,其解引不可能是将数据取出,实际上函数指针的解引本质上是执行函数的过程,只是这个执行函数是使用的call指令并不是之前的函数,而是函数指针的值,即函数的地址。其实执行函数的过程本质上也是利用call指令来调用函数的地址,因此函数指针本质上就是保存函数执行过程的首地址。函数指针的调用如下:

为了确认函数指针本质上是传递给call指令一个函数的地址,下面用一个简单例子说明:

上面是编译后的汇编指令,可以看到,使用函数指针来调用函数时,其汇编指令多了如下:

0x4015e3    mov    DWORD PTR [esp+0xc],0x4015c0
0x4015eb    mov    eax,DWORD PTR [esp+0xc]
0x4015ef    call   eax

分析:第一行mov指令将立即数0x4015c0赋值给寄存器esp+0xc的地址内存中,然后将寄存器esp+0xc地址的值赋值给寄存器eax(累加器),然后调用call指令,此时pc指针将会指向add函数,而0x4015c0正好是函数add的首地址,这样就完成了函数的调用。

细心的读者是否发现一个有趣的现象,上述过程中函数指针的值和参数一样是被放在栈帧中,这样看起来就是一个参数传递的过程,因此可以看到,函数指针最终还是以参数传递的形式传递给被调用的函数,而这个传递的值正好是函数的首地址。

从上面可以看到函数指针并不是和一般的指针一样可以操作内存,因此作者觉得函数指针可以看作是函数的引用申明。

函数指针应用

在linux驱动面向对象编程思想中用的最多,利用函数指针来实现封装,下面以一个简单的例子说明:

#include <stdio.h>

typedef struct TFT_DISPLAY

    int   pix_width;
    int   pix_height;
    int   color_width;
    void (*init)(void);
    void (*fill_screen)(int color);
    void (*tft_test)(void);

tft_display;

static void init(void)

    printf("the display is initialed\\n");


static void fill_screen(int color)

    printf("the display screen set 0x%x\\n",color);



tft_display mydisplay=

    .pix_width=320,
    .pix_height=240,
    .color_width=24,
    .init=init,
    .fill_screen=fill_screen,
;

int main(int argc, char **argv)


    mydisplay.init();
    mydisplay.fill_screen(0xfff);
    return 0;

上面的例子将一个tft_display封装成一个对象,上面的结构体成员中最后一个没有初始化,这在Linux中用的非常多,最常见的是file_operations结构体,该结构体一般来说只需要初始化常见的函数,不需要全部初始化。上面代码中采用的结构体初始化方式也是在Linux中最常用的一种方式,这种方式的好处在于无需按照结构体的顺序一对一。

回调函数

有时候会遇到这样一种情况,当上层人员将一个功能交给下层程序员完成时,上层程序员和下层程序员同步工作,这个时候该功能函数并未完成,这个时候上层程序员可以定义一个API来交给下层程序员,而上层程序员只要关心该API就可以了而无需关心具体实现,具体实现交给下层程序员完成即可(这里的上层和下层程序员不指等级关系,而是项目的分工关系)。

这种情况下就会用到回调函数(Callback Function),现在假设程序员A需要一个FFT算法,这个时候程序员A将FFT算法交给程序员B来完成,现在来让实现这个过程:

#include <stdio.h>

int  InputData[100]=0;
int OutputData[100]=0;

void FFT_Function(int *inputData,int *outputData,int num)

    while(num--)
    

    


void TaskA_CallBack(void (*fft)(int*,int*,int))


    (*fft)(InputData,OutputData,100);


int main(int argc, char **argv)


    TaskA_CallBack(FFT_Function);
    return 0;

上面的代码中TaskA_CallBack是回调函数,该函数的形参为一个函数指针,而FFT_Function是一个被调用函数。可以看到回调函数中申明的函数指针必须和被调用函数的类型完全相同。

原文:李文山的博客 https://lishanwen.cn/

仅供大家学习参考与知识传播,版权归原作者所有,如有侵权,麻烦联系进行删除,感谢~

以上是关于c语言中的指针应该怎么理解?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确的理解指针和结构体指针?

如何正确的理解指针和结构体指针?

c语言指针怎么理解,啥时候会用到指针,要注意哪些方面

请问c语言中引用怎样理解?

C 语言行指针与列指针详解

C语言中二维数组行指针是啥