C语言指针详解

指针的引入

为什么需要指针?

指针解决了一些编程中基本的问题。

第一,指针的使用使得不同区域的代码可以轻易的共享内存数据。当然你也可以通过数据的复制达到相同的效果,但是这样往往效率不太好,因为诸如结构体等大型数据,占用的字节数多,复制很消耗性能。但使用指针就可以很好的避免这个问题,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或者其他可能)。

第二,指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表,链式二叉树等等。

第三,有些操作必须使用指针。如操作申请的堆内存。还有:C语言中的一切函数调用中,值传递都是“按值传递”的,如果我们要在函数中修改被传递过来的对象,就必须通过这个对象的指针来完成。

指针是什么?

我们指知道:C语言中的数组是指一类类型,数组具体区分为 int 类型数组,double类型数组,char数组等等。同样指针这个概念也泛指一类数据类型,int指针类型,double指针类型,char指针类型等等。

通常,我们用int类型保存一些整型的数据,如 int num = 97 , 我们也会用char来存储字符: char ch = ‘a’。

我们也必须知道:任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

因此:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。在32位系统中,占4个字节。

指针变量声明的一般形式为:

type *var_name; 

//星号*的位置也可以靠近类型,如type* var_name;但一般不推荐

int *a;

int* a;

两者意思相同且后者看上去更为清楚,a被声明为类型为 int* 的指针。 但是,这并不是一个好技巧,原因如下:

int* b, c, d;

人们很自然地以为这条语句把所有三个变量声明为指向整形的指针, 但事实上并非如此。我们被它的形式愚弄了。星号实际上是表达式 *b 的一部分,只对这个标识符有用。b 是一个指针, 但其余两个变量只是普通的整形。要声明三个指针, 正确的语句如下:

int *b, *c, *d;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。

所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

指针的相关符号

星号*
C语言中*可以表示乘号,也可以表示指针符号。这两个用法是毫无关联的,只是恰好用了同一个符号而已。
星号在用于指针相关功能的时候有2种用法:第一种是指针定义时,*结合前面的类型用于表明要定义的指针的类型;第二种功能是指针解引用,解引用时*p表示p指向的变量本身。

*是作用在指针上,整体相当于一个变量。

取地址符&
取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址。和*是逆操作。

指针定义并初始化、与指针定义然后赋值的区别
(1)指针定义时可以初始化,指针的初始化其实就是给指针变量赋初值(跟普通变量的初始化没有任何本质区别)。
(2)指针变量定义同时初始化的格式是:int a = 32; int *p = &a;
(2)不初始化时指针变量先定义再赋值:int a = 32; int *p; p = &a;        正确的
                                                                                         *p = &a;       错误的

如何使用指针?

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。

1、当我们int *p定义一个指针变量p时,如果p是局部变量,所以也遵循C语言局部变量的一般规律(定义局部变量并且未初始化,则值是随机的),所以此时p变量中存储的是一个随机的数字。
2、此时如果我们解引用p,则相当于我们访问了这个随机数字为地址的内存空间。那这个空间到底能不能访问不知道(也许行也许不行),所以如果直接定义指针变量未绑定有效地址就去解引用几乎必死无疑。
3、定义一个指针变量,不经绑定有效地址就去解引用,就好象拿一个上了镗的枪随意转了几圈然后开了一枪。
4、指针绑定的意义就在于:让指针指向一个可以访问、应该访问的地方(就好象拿着枪瞄准目标的过程一样),指针的解引用是为了间接访问目标变量(就好象开枪是为了打中目标一样)

补充和总结:

1、指针的实质就是个变量,它跟普通变量没有任何本质区别。指针完整的名字应该叫指针变量,简称为指针。

2、指针的出现是为了实现间接访问。在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。

3、间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的C语言也必须实现间接寻址。
4、高级语言如Java、C#等没有指针,那他们怎么实现间接访问?答案是语言本身帮我们封装了。

指针的运算

指针与++ --符号进行运算

指针本身也是一种变量,因此也可以进行运算。但是因为指针变量本身存的是某个其他变量的地址值,因此该值进行* / %等运算是无意义的。两个指针变量相加本身也无意义,相减有意义。指针变量+1,-1是有意义的。+1就代表指针所指向的格子向后挪一格,-1代表指针所指向的格子向前挪一格。

*p++就相当于*(p++),p先与++结合,然后p++整体再与*结合。

*p++解析:++先跟p结合,但是因为++后置的时候,本身含义就是先运算后增加1(运算指的是p++整体与前面的*进行运算;增加1指的是p+1),所以实际上*p++符号整体对外表现的值是*p的值,运算完成后p再加1.
所以*p++等同于:*p;   p += 1;

*++p等同于 p += 1;    *p;

(*p)++,使用()强制将*与p结合,只能先计算*p,然后对*p整体的值++。

++(*p),先*p取值,再前置++,该值+1后作为整个表达式的值。

总结:++符号和指针结合,总共有以上4种情况。--与++的情况很类似。

野指针

神马是野指针?哪里来的?有什么危害?
野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)


野指针很可能触发运行时段错误(Segmentation fault)
因为指针变量在定义时如果未初始化,值也是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,所以意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的。


野指针因为指向地址是不可预知的,所以有3种情况:

第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;

第二种是指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;

第三种情况就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。


指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律(栈反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值),就决定了栈的使用多少会影响这个默认值。因此野指针的值是有一定规律不是完全随机,但是这个值的规律对我们没意义。因为不管落在上面野指针3种情况的哪一种,都不是我们想看到的。

怎么避免野指针?
野指针的错误来源就是指针定义了以后没有初始化,也没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。
知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指针指向一个绝对可用的空间。
常规的做法是:
第一点:定义指针时,同时初始化为NULL
第二点:在指针解引用之前,先去判断这个指针是不是NULL
第三点:指针使用完之后,将其赋值为NULL
第四点:在指针使用之前,将其赋值绑定给一个可用地址空间
野指针的防治方案4点绝对可行,但是略显麻烦。很多人懒得这么做,那实践中怎么处理?在中小型程序中,自己水平可以把握的情况下,不必严格参照这个标准;但是在大型程序,或者自己水平感觉不好把握时,建议严格参照这个方法。

NULL到底是个啥?

在C语言中,int *p;你可以p = (int *)0;但是不可以p = 0;因为类型不相同。
所以NULL的实质其实就是0,然后我们给指针赋初值为NULL,其实就是让指针指向0地址处。为什么指向0地址处?2个原因。第一层原因是0地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化,就表示是野指针);第二层原因是这个地址0地址在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误,这种已经是最好的结果了。


一般在判断指针是否野指针时,都写成if (NULL != p)而不是写成if (p != NULL)
原因是:如果NULL写在后面,当中间是==号的时候,有时候容易忘记写成了=,这时候其实程序已经错误,但是编译器不会报错。这个错误(对新手)很难检查出来;如果习惯了把NULL写在前面,当错误的把==写成了=时,编译器会报错,程序员会发现这个错误。


NULL不是C语言/C++关键字,本质上是一个宏定义

NULL的标准定义:

#ifdef _cplusplus            // 条件编译
#define NULL 0
#else
#define NULL (void *)0        // 这里对应C语言的情况
#endif

解释:C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。


NULL的本质解析:NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000,代表内存的0地址。(void *)0这个整体表达式表示一个指针,这个指针变量本身占4字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际是0地址开始的一段内存)。

从指针角度理解NULL的本质
1、int *p;        // p是一个函数内的局部变量,则p的值是随机的,也就是说p是一个野指针。

2、int *p = NULL;    // p是一个局部变量,分配在栈上的地址是由编译器决定的,我们不必关心,但是p的值是(void *)0,实际就是0,意思是指针p指向内存的0地址处。这时候p就不是野指针了。
3、为什么要让一个野指针指向内存地址0处?主要是因为在大部分的CPU中,内存的0地址处都不是可以随便访问的(一般都是操作系统严密管控区域,所以应用程序不能随便访问)。所以野指针指向了这个区域可以保证野指针不会造成误伤。如果程序无意识的解引用指向0地址处的野指针则会触发段错误。这样就可以提示你帮助你找到程序中的错误。

为什么需要NULL?
第一个作用就是让野指针指向0地址处安全。
第二个作用就是一个特殊标记。按照标准的指针使用步骤是:

int *p = NULL;        // 定义p时立即初始化为NULL
p = xx;
if (NULL != p)
{
    *p                 // 在确认p不等于NULL的情况下才去解引用p
}
p = NULL            // 用完之后p再次等于NULL

注意:一般比较一个指针和NULL是否相等不写成if (p == NULL),而写成if (NULL == p)。原因是第一种写法中如果不小心把==写成了=,则编译器不会报错,但是程序的意思完全不一样了;而第二种写法如果不小心把==写成了=则编译器会发现并报错。

注意不要混用NULL与'\0'
(1)'\0' 和 '0' 和 0  和 NULL几个区分开
(2)'\0'是一个转义字符,他对应的ASCII编码值是0,本质就是0
(3)'0'是一个字符,他对应的ASCII编码值是48,本质是48
(4)0是一个数字,他就是0,本质就是0
(4)NULL是一个表达式,是强制类型转换为void *类型的0,本质是0

总结:'\0'用法是C语言字符串的结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头;'0'是字符0,对应0这个字符的ASCII编码,一般用来获取0的ASCII码值;0是数字,一般用来比较一个int类型的数字是否等于0;NULL是一个表达式,一般用来比较指针是否是一个野指针。

补充:

如果企图通过一个空指针来访问一个存储单元,将会得到一个出错信息。

#include <stdio.h>

int main()
{
	char *a = NULL;
	
	printf("Result is: %d", *a);
	
    return 0;
}

//3 Segmentation fault      (core dumped) ./a.out

另外要注意,p = 0;是合法的。

#include <stdio.h>

int main()
{
	int *p1, *p2, *p3;
	p1 = NULL;
	p2 = 0;
	p3 = '\0';
	
	printf("Result is: %x\n", p1);
	printf("Result is: %x\n", p2);
	printf("Result is: %x\n", p3);
	
    return 0;
}

/*
    Result is: 0
    Result is: 0
    Result is: 0
*/

注意:如果p是个指针,那么

p = NULL;
p = 0;
p = '\0'

三者是等效的。

不要弄混了,这三者是等效的,和对0地址进行访问会出错不冲突。

指针的强制类型转换

指针的本质是:变量,指针就是指针变量
一个指针涉及2个变量:一个是指针变量自己本身,一个是指针变量指向的那个变量

int *p;定义指针变量时,p(指针变量本身)是int *类型,*p(指针指向的那个变量)是int类型的。
int *类型说白了就是指针类型,只要是指针类型就都是占4字节,解析方式都是按照地址的方式来解析(意思是里面存的32个二进制加起来表示一个内存地址)。

结论就是:所有的指针类型(不管是int * 还是char * 还是double *)的解析方式是相同的,都是地址。

对于指针所指向的那个变量来说,指针的类型就很重要了。指针所指向的那个变量的类型(它所对应的内存空间的解析方法)要取决于指针类型。譬如指针是int *的,那么指针所指向的变量就是int类型的。

指针数据类型转换实例分析1(int * -> char *)
int和char类型都是整形,类型兼容的。所以互转的时候有时候错有时候对。
int和char的不同在于char只有1个字节而int有4个字节,所以int的范围比char大。在char所表示的范围之内int和char是可以互转的不会出错;但是超过了char的范围后char转成int不会错(向大方向转就不会错,就好比拿小瓶子的水往大瓶子倒不会漏掉不会丢掉),而从int到char转就会出错(就好象拿大瓶子水往小瓶子倒一样)

指针数据类型转换实例分析2(int * -> float *)
以前分析过:int和float的解析方式是不兼容的,所以int *转成float *再去访问绝对会出错。

注意1:

我们看到的地址通常都是这种形式,0x12345678,于是我们会根据思维定势以为这就是一个地址指针,然后想着对它解引用,*0x12345678 = 12;这是错误的。因为此时的0x12345678根本就不是一个指针,只是一个普通的数字,只不过用十六进制的形式来表示而已。

可以通过强制类型转换来实现上述操作,比如:* ((int *)0x12345678) = 12;

指针和const

const和指针结合,共有4种形式:
const int *p;    p是一个指针,指针指向一个int型数据。p所指向的是个常量。        
int const *p;    p是一个指针,指针指向一个int型数据。p所指向的是个常量。    
int *const p;    p是一个指针,指针指向一个int型数据。p本身是常量,p所指向的是个变量
const int *const p;    p是一个指针,指针指向一个int型数据。p本身是常量,指向的也是常量

结论和记忆方法:
const在*前面,就表示const作用于p所指向的量。所以这时候p所指向的是个常量。
const在*后面,表示p本身是常量,但是p指向的不一定是常量。

const型指针有什么用?
比如:char *strcpy(char *dst, const char *src);
字符串处理函数strcpy,它的函数功能是把src指向的字符串,拷贝到dst中。这里的const指示src是个输入型参数,不要改变它所指向的内容。

指针和数组

指针与数组的初步结合

数组名:做右值时,或者单独输出时,数组名表示数组的首元素首地址,因此可以直接赋值给指针。
如果有 int a[5];
则 a和&a[0]都表示数组首元素a[0]的首地址。
而&a则表示数组的首地址。

注意:数组首元素的首地址和数组的首地址是不同的。前者是数组元素的地址,而后者是数组整体的地址。两个东西的含义不同,但是数值上是相同的。

数组的两种访问方式:

根据以上,我们知道可以用一个指针指向数组的第一个元素,这样就可以用间接访问的方式去逐个访问数组中各个元素。这样访问数组就有了两种方式。
有 int a[5];  int *p; p = a;
数组的方式依次访问:a[0]       a[1]            a[2]            a[3]            a[4]
指针的方式依次访问:*p        *(p+1)        *(p+2)        *(p+3)         *(p+4)

数组下标方式和指针方式均可以访问数组元素,两者的实质其实是一样的。在编译器内部都是用指针方式来访问数组元素的,数组下标方式只是编译器提供给编程者一种壳(语法糖)而已。所以用指针方式来访问数组才是本质的做法。

从内存角度理解指针访问数组的实质:
数组的特点就是:数组中各个元素的地址是依次相连的,而且数组还有一个很大的特点(其实也是数组的一个限制)就是数组中各个元素的类型相同。类型相同就决定了每个数组元素占几个字节是相同的(譬如int数组每个元素都占4字节,没有例外)。
数组中的元素其实就是地址相连接、占地大小相同的一串内存空间。这两个特点就决定了只要知道数组中一个元素的地址,就可以很容易推算出其他元素的地址。

指针和数组类型的匹配问题
(1)int *p; int a[5];    p = a;        // 类型匹配
(1)int *p; int a[5];    p = &a;        // 类型不匹配。p是int *,&a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配
(2)&a、a、&a[0]从数值上来看是完全相等的,但是意义来看就不同了。从意义上来看,a和&a[0]是数组首元素首地址,而&a是整个数组的首地址;从类型来看,a和&a[0]是元素的指针,也就是int *类型;而&a是数组指针,是int (*)[5];类型。

总结:指针类型决定了指针如何参与运算
1、指针参与运算时,因为指针变量本身存储的数值是表示地址的,所以运算也是地址的运算。

2、在数组中,指针参与运算的特点是,指针变量+1,并不是真的加1,而是加1*sizeof(数组变量类型);如果是int *指针,则+1就实际表示地址+4,如果是char *指针,则+1就表示地址+1;如果是double *指针,则+1就表示地址+8.

注意区分指针+1和地址+1的区别,地址+1的时候,是移下下一个字节地址。

补充:

对于char *str[]来说,str不是数组指针,而是个指针数组,更具体来说,是个字符串数组。

这里要注意,虽然str不是数组指针,但是因为数组的特殊性,这里的str表示首元素的首地址,依然可以进行解指针操作。

char str[][]也是个字符串数组。

指针数组和数组指针

字面意思来理解指针数组与数组指针
指针数组的实质是一个数组,这个数组中存储的内容全部是指针变量。
数组指针的实质是一个指针,这个指针指向的是一个数组。

分析指针数组与数组指针的表达式

int *p[5];

int (*p)[5];

int *(p[5]);

一般规律:

int *p;(p是一个指针); int p[5];(p是一个数组)
我们在定义一个符号时,关键在于:首先要搞清楚你定义的符号是谁(第一步:找核心);其次再来看谁跟核心最近、谁跟核心结合(第二步:找结合);以后继续向外扩展(第三步:继续向外结合直到整个符号完)。
如果核心和*结合,表示核心是指针;如果核心和[]结合,表示核心是数组;如果核心和()结合,表示核心是函数。


用一般规律来分析3个符号:
第一个:int *p[5];

核心是p,p是一个数组,数组有5个元素,数组中的元素都是指针,指针指向的元素类型是int类型的;整个符号是一个指针数组。


第二个,int (*p)[5];

核心是p,p是一个指针,指针指向一个数组,数组有5个元素,数组中存的元素是int类型; 总结一下整个符号的意义就是数组指针。


第三个,int *(p[5]); 
解析方法和结论和第一个相同,()在这里是可有可无的。

注意:符号的优先级到底有什么用?其实是决定当2个符号一起作用的时候决定哪个符号先运算,哪个符号后运算。
遇到优先级问题怎么办?第一,查优先级表;第二,自己记住(记住[] . ->这几个优先级即可)。

总结1:优先级和结合性是分析符号意义的关键
在分析C语言问题时不要胡乱去猜测规律,不要总觉得c语言无从捉摸,从已知的规律出发按照既定的规则去做即可。
总结2:学会逐层剥离的分析方法
找到核心后从内到外逐层的进行结合,结合之后可以把已经结合的部分当成一个整体,再去和整体外面的继续进行结合。
总结3:基础理论和原则是关键,没有无缘无故的规则

针对是数组还是指针,我的判断是,如果有括号将星号和变量括起来,就是指针,否则,就是数组。

补充:指针数组和数组指针如何引用元素?

指针数组就是一个数组,按照数组的方式去使用即可,前面的星号,只表明数组里存的是指针而已。比如int *a[5],直接a[i]引用数组元素即可,只是此时a[i]是个指针。

示例如下:

#include <stdio.h>

int main()
{
	int a = 1, b = 5;
	int *c[2];

	c[0] = &a;
	c[1] = &b;

	printf("Hello, World! %d\n", *c[0]); //Hello, World! 1
	printf("Hello, World! %d\n", *c[1]); //Hello, World! 5
   
    return 0;
}

数组指针,是一个指向数组的指针,而不是指向数组元素的指针,那么如果引用数组元素呢?

一开始,我以为对数组指针进行解引用,就变成了个数组变量,接着结合下标就可以引用,但是经过实践发现,这样是错的。

由此可知,无法通过数组指针直接引用数组元素。(对比:结构体指针可以直接通过->引用结构体元素。)

那么,如果我有个数组的指针,怎么才能引用到元素呢?

我想了想, 可以利用数组指针和数组首元素指针在数值上相同这一特性来间接引用。

示例如下:

#include <stdio.h>

int main()
{
	int a[2] = {1, 3};
	int (*p)[]; //数组指针类型声明时可以不写大小,因为只是说明一种类型
	
	p = &a;

	//printf("Hello, World! %d\n", *p[0]); //error
	printf("Hello, World! %d\n", *((int *)p + 1)); //Hello, World! 3
   
    return 0;
}

如果想要接收&a,那么就需要定义数组指针(用处不大)。

如果想要接收数组的数组名,我们需要定义的指针类型是数组元素的类型,而不是定义一个数组指针。

上述案例演示的是普通数组,那么如果这个数组是个指针数组呢?如何定义才能接收&a?

分析:

普通情况下,要想接收int类型数组的地址,就要有一个int类型数组的指针。

如果数组是个int指针数组,那么对应的指针类型应该也是int指针数组类型。

#include <stdio.h>

int main()
{
	int a[2];
	int *p1 = a;
	int (*p2)[] = &a;

	int *b[2];
	int **p3 = b;
	int *(*p4)[] = &b;
   
    return 0;
}

其实,可以不用过多关注&arr。

重点关注数组名arr,int *b[2]里面放的是指针,b是首元素的指针,也就是指针的指针,即双重指针。解双重指针,就解两次引用。

#include <stdio.h>

int main()
{
	int i = 1, j = 5;

	int *b[2] = {&i, &j};
	int **p = b;

    /* 我的第一个 C 程序 */
    printf("Hello, World! %d\n", **(p + 1));
   
    return 0;
}

函数指针

函数指针的实质还是指针,还是指针变量。本身占4字节(在32位系统中,所有的指针都是4字节)

函数指针、数组指针、普通指针之间并没有本质区别,区别在于指针指向的东西是个什么玩意。

函数的实质是一段代码,这一段代码在内存中是连续分布的(一个函数的大括号括起来的所有语句将来编译出来生成的可执行程序是连续的),所以对于函数来说很关键的就是函数中的第一句代码的地址,这个地址就是所谓的函数地址,在C语言中用函数名这个符号来表示。

结合函数的实质,函数指针其实就是一个普通变量,这个普通变量的类型是函数指针变量类型,它的值就是某个函数的地址(也就是它的函数名这个符号在编译器中对应的值)

函数指针的书写和分析方法
C语言本身是强类型语言(每一个变量都有自己的变量类型),编译器可以帮我们做严格的类型检查。
所有的指针变量类型其实本质都是一样的,但是为什么在C语言中要去区分它们,写法不一样呢(譬如int类型指针就写作int *p; 数组指针就写作int (*p)[5],函数指针就得写得更复杂)
假设我们有个函数是:void func(void); 对应的函数指针:void (*p)(void); 类型是:void (*)(void);
函数名和数组名最大的区别就是:函数名做右值时加不加&效果和意义都是一样的;但是数组名做右值时加不加&意义就不一样。

函数名或者&函数名,都表示函数的地址:

#include <stdio.h>

int main()
{
    int fun(int i, int j)
	{
		return (i + j);
	}
	
	printf("Hello, World! %p\n", fun);
	printf("Hello, World! %p\n", &fun);
   
    return 0;
}

上述两个输出语句的结果是一样的。

写一个复杂的函数指针的实例:譬如函数是strcpy函数

char *strcpy(char *dest, const char *src);

对应的函数指针是:char *(*pFunc)(char *dest, const char *src);

通过指针调用时,有两种调用方式:

1、pFunc(参数1,参数2),前面不用加星号*

2、(*pFunc)(参数1,参数2),此时*和变量名要用括号括起来

附上一道题,弄明白。

这个题里有几点要注意:

1、a()和(*a)()调用,是一样的,所以B是对的;

2、int *b(),这里是一个函数声明,不是变量定义,题目中特意给混在一起造成迷惑。只是声明,并没有定义,无法调用。只有在定义之后,函数名才可以表示该函数的指针。

补充1

看到一段代码:

int (*p1)(int), (*p2)(int), (*t)(int), y1, y2;

有点蒙,这啥意思?

看起来像函数的声明定义。但是如果是声明,不应该是这样的嘛:

int *p1, *p2, *t, y1, y2;

每个指针后面还加个(int),是啥意思?

有点像强制类型转换,但如果是强制类型转换,(int)也应该放到前面呀?

想了半天没想明白,以为是C++里的语法。

后面看评论才知道,原来这是声明函数指针。。。。。。。。。。。。

函数指针也和其他类型定义一样,可以共用一个数据类型符号。

各种类型指针总结

指针作为一种范类型,和数组一样,有很多类型的指针,应该说,有多少种数据类型,就有多少种类型的指针。

数组,就有各种数组,整形数组、浮点型数组、数组的数组、结构体数组、指针数组等等。

指针,就有基本的整形指针、浮点型指针、数组指针、结构体指针,另外还有函数指针。

对于基本的指针,都比较简单,char *p1;short *p2;int *p3;long *p4;float *p5;double *p6;对于这些基本的指针,原理是一样的,以int *p为例,这是一个int类型的指针,指针本身是一个4字节的16进制数地址,但是指向的是一个存放int类型数的地址空间。此时p的类型是int *,表示是个int类型的指针。

结构体指针,和基本类型指针一样,只不过指向的是个结构体,而且,在定义形式上,和基本类型一样,比如:

struct Student
{
    char *name;
    int age;
};

此时,定义一个结构体指针变量就是:struct Student *zhangsan;这里,zhagnsan是一个结构体指针变量,该变量的类型是struct Student *,可以看到,和int *是一样的形式。

相对来说,数组和函数的指针就稍微麻烦一些,不是说原理上麻烦,而是定义的形式上。

先说数组指针,如果要定义一个指向含有5个int元素的数组的指针变量,需要怎么写呢?

int (*p)[5];

注意,这里的*p一定要用括号括起来,要不然就不是指针了,而是变成了指针数组。

int *p[5]; //这是指针数组

int (*p)[5]; //这是数组指针

定义之后,就可以给其赋值了。

int num[5];

int (*p)[5] = &num; //是赋数组指针,不是直接num,num表示数组首元素首地址

那么,这里的数组指针的类型是什么呢?

和int *p;比较下,其类型就是不含变量名的部分int *,同理,数组指针的类型是:

int (*)[5]; //关键要指明所指向的数组所含数据类型及其长度

对于函数指针来说,道理是一样的。

定义函数指针变量:

int (*p)(int a); 
// 关键要指明函数的返回值和参数列表,参数列表中可不写变量名,重要的是类型

同样的,其类型为:

int (*)(int a); //此处参数列表中可不写变量名,重要的是类型

函数是用来调用的,那么,怎么通过指针来调用呢?

我们知道,函数名就是其调用地址。我将函数名赋值给一个函数指针,之后,调用函数指针就相当于调用了函数,如下所示:

int print(int a)
{
    //statement;
}

int (*out)(int a) = print; //此处参数列表中可不写变量名,重要的是类型
out(5);

此时,只要函数有相同的返回值和参数列表,就可以赋值给该函数指针,然后通过指针名去调用函数。

如果类型不匹配,就会出错:

initialization of ‘int (*)(int)’ from incompatible pointer type ‘int (*)(int,  int)’

注意1:

要区分指针和强制类型转换,强制类型转换的符号是()

int *p;这是定义指针,(int *)p这是强制类型转换;int (*p)[5]这是数组指针;int (*p)(int)这是函数指针;要注意区分这几点。

注意2:

关于main函数中的参数

int main(int argc, const char *argv[]){}

这里的const char *argv[],这不是一个指针,而是一个指针数组,数组里放的是指针,什么指针呢?char *类型的数组,放的是char *类型的元素,char *是个字符型指针,也是一个字符串,所以,这里其实就是个字符串数组。

二重指针

二重指针与普通一重指针的区别
本质上来说,二重指针和一重指针的本质都是指针变量,指针变量的本质就是变量。
一重指针变量和二重指针变量本身都占4字节内存空间,

二重指针的本质
二重指针本质上也是指针变量,和普通指针的差别就是它指向的变量类型必须是个一重指针。二重指针其实也是一种数据类型,编译器在编译时会根据二重指针的数据类型来做静态类型检查,一旦发现运算时数据类型不匹配编译器就会报错。
C语言中如果没有二重指针行不行?其实是可以的。一重指针完全可以做二重指针做的事情,之所以要发明二重指针(函数指针、数组指针),就是为了让编译器了解这个指针被定义时定义它的程序员希望这个指针被用来指向什么东西(定义指针时用数据类型来标记,譬如int *p,就表示p要指向int型数据),编译器知道指针类型之后可以帮我们做静态类型检查。编译器的这种静态类型检查可以辅助程序员发现一些隐含性的编程错误,这是C语言给程序员提供的一种编译时的查错机制。
为什么C语言需要发明二重指针?原因和发明函数指针、数组指针、结构体指针等一样的。

指向指针的指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。

一个指向指针的指针变量必须如下声明,即在变量名前放置两个星号。例如,下面声明了一个指向 int 类型指针的指针:

int **var;

当一个目标值被一个指针间接指向到另一个指针时,访问这个值需要使用两个星号运算符,如下面实例所示:

二重指针的用法
二重指针指向一重指针的地址
二重指针指向指针数组

实践编程中二重指针用的比较少,大部分时候就是和指针数组纠结起来用的。
实践编程中有时在函数传参时为了通过函数内部改变外部的一个指针变量,会传这个指针变量的地址(也就是二重指针)进去

比如:

void find_max_and_min(int **pmax,int **pmin, int arr[]) {
	*pmax = *pmin = arr;
 
	int i;
	
	for(i=0;i<10;i++) {
		if(**pmax < arr[i]) {
			*pmax = arr+i;
		}
		if(**pmin > arr[i]) {
			*pmin = arr+i;
		}
	}
 
}

二重指针、二重数组、指针,指针数组、数组指针之间的联系

64位机器

在64位机器上,地址是8个字节。

指向函数的指针数组

数组名和指针变量等效吗

看这道题:

数组名作为指针是个常量,是不能++操作的,而指针则是变量。

最明显的就是sizeof结果不一样 

关于多重指针的解法

附上一道题:

这种题,最好的做法就是画图。要不容易大脑混乱。

有几个要注意的点:

第一点:

char *p = "abcd";这种定义中,p是个指针,这个指针可以用来表示字符串,其实质是指向字符串的第一个字符,也就是字符'a'的地址。上例中,对s+2进行解引用,就会得到一个指针,也就是得到一个字符串,也就是首字母p的地址。

要注意字符串类型数组和非字符串类型数组的区别,如果是普通的数组,比如int a[2] = {5, 9};这里a表示首元素的地址,也就是5的地址;但是对于char *s[] = {"black", "white", "pink", "violet"};来说,s此时就是个二重指针,这里s也是表示首元素的地址,但是因为其首元素是个字符串,而字符串本身也是个指针,所以上面的a是指针,而s就是二重指针。对s进行解引用,得到的是个指针,这个指针是个字符串,也是字符串首字母的地址,也就是字符b的地址。这个区别是字符串的特殊定义导致的,应当十分注意。

第二点:

这里为什么要把ptr赋值给三重指针p,而不直接使用ptr?这是因为,ptr是个常量,无法进行自增自减,所以需要将其赋值给一个变量,再进行自增操作。

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: "C语言指针详解.pdf" 是一份详细介绍C语言指针概念和使用的PDF文档。C语言中,指针是一种特殊的变量类型,用于存储其他变量的内存地址。 该PDF文档首先详细介绍了指针的定义和声明。指针的声明需要指定指针变量的类型和名称,并使用星号(*)来表示该变量是一个指针指针变量名的前面加上一个星号,可以获取所指向的变量的值,这被称为"解引用"。 文档还介绍了指针的运算。指针可以进行自增和自减运算,指针之间可以进行相减操作,返回的结果表示它们之间的距离或者偏移量。此外,还可以将指针赋值给另一个指针,或者将指针赋值给一个变量,反之亦然。 除了基本的指针概念,文档还详细介绍了指针的常见应用场景。这包括指针作为函数参数,用于在函数内部对传入的变量进行修改。还有通过指针来实现动态内存分配和释放,以及使用指针实现数据结构(如链表和树)等。 此外,该文档还包含一些常见的指针错误和问题的解决方案。这些错误包括空指针引用、野指针引用以及内存泄漏等。文档指出了这些错误的影响以及如何避免它们。 总的来说,"C语言指针详解.pdf" 是一份详细介绍C语言指针概念、使用和常见问题解决方案的文档,对于学习和理解C语言指针的人们是一份宝贵的资料。 ### 回答2: 《C语言指针详解.pdf》是一本关于C语言指针的详细解析的电子书。在这本书中,作者详细介绍了C语言指针的概念、用途和基本语法。 首先,指针C语言中非常重要的概念,它是一种数据类型,用于存储和操作内存地址。指针可以指向各种数据类型,如整数、字符、数组和结构体等。 在《C语言指针详解.pdf》中,作者详细讲解了指针的声明和初始化,以及如何通过指针来访问和修改变量的值。作者还介绍了指针数组的关系,以及指针和函数之间的关联。 此外,书中还涵盖了指针的高级应用,如指针的算术运算、指向指针指针指针数组等。作者通过丰富的例子和代码来帮助读者理解这些概念和技巧。 《C语言指针详解.pdf》不仅适合C语言初学者,也适合有一定编程基础的读者。通过阅读此书,读者将能够更深入地理解C语言指针的功能和用法,掌握指针在编程中的灵活运用。 总之,《C语言指针详解.pdf》是一本内容详尽且易于理解的C语言指针教程。读者通过阅读此书,可以提高自己在C语言编程中的指针应用能力,从而更好地实现程序的设计和开发。 ### 回答3: 《C语言指针详解.pdf》是一本介绍C语言指针概念和使用方法的详细手册。C语言中的指针是一种非常重要和特殊的数据类型,它提供了直接访问内存地址的能力,使得C语言具有了更高的灵活性和效率。 这本手册首先会介绍指针的基本概念,包括指针变量的定义和声明、指针的初始化和赋值。它会详细讲解指针和变量之间的关系,以及指针的运算规则和使用方法。读者可以学习到如何通过指针操作变量的值和地址,以及如何利用指针实现函数的参数传递和返回值。 接下来,手册会介绍指针数组之间的关系。C语言中,数组名本质上是一个指向数组首元素的常量指针,因此可以通过指针来操作数组。手册将详细讲解指针数组指针算术运算,以及指针和多维数组的关系。 此外,手册还会介绍指针字符串之间的关系。C语言中,字符串本质上是以空字符结尾的字符数组,可以通过指针来操作字符串。手册将详细讲解指针字符串的操作,包括字符串的输入输出、字符串的比较和拷贝。 最后,手册还会介绍指针和结构体之间的关系。C语言中,结构体是用户自定义的复合数据类型,可以通过指针来操作结构体。手册将详细讲解指针和结构体的操作,包括结构体指针的定义和使用,以及结构体指针作为函数参数的传递方式。 总之,《C语言指针详解.pdf》是一本深入浅出的指针教程,对于想更深入理解C语言指针的读者来说,是一本非常实用的参考书。无论是初学者还是有一定基础的读者,都可以从中获得很多宝贵的知识和技巧。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值