`
haierboos
  • 浏览: 438207 次
文章分类
社区版块
存档分类
最新评论

C语言深度解剖读书笔记(4.指针的故事)

 
阅读更多

指针这一节是本书中最难的一节,尤其是二级指针和二维数组直接的关系。

本节知识点:

1.指针基础,一张图说明什么是指针:

2.跨过指针,直接去访问一块内存:
只要你能保证这个地址是有效的 ,就可以这样去访问一个地址的内存*((unsigned int *)(0x0022ff4c))=10; 但是前提是 0x0022ff4c是有效地址。对于不同的编译器这样的用法还不一样,一些严格的编译器,当你定义一个指针,把这个指针赋值为一个这样的地址的时候,当检测到地址无效,编译的时候就会报错!!!如果一些不太严格的编译器,不管地址有效无效都会编译通过,但是对于无效地址,当你访问这块地址的时候,程序就会运行停止!
3.a &a &a[0]三者的区别:
首先说三者的值是一样的,但是意义是不一样的。(这里仅仅粗略的说说,详细见文章<c语言中数组名a和&a>)
&a[0]:这个是数组首元素的地址
a : 的第一个意义是 数组首元素的地址,此时与&a[0]完全相同
第二个意义是 数组名 sizeof(a) 为整体数组有多少个字节
&a :这个是数组的地址 。跟a的区别就是,a是一个 int* 的指针(在第一种意义的时候) ,而&a是一个 int (*p)[5]类型的数组指针,指针运算的结果不一样。(此处的int* 仅仅是为了举例子,具体应该视情况而定)
4.指针运算(本节最重要的知识点,但并不是最难的,所以的问题都来源于这儿):
对于指针的运算,首先要清楚的是指针类型(在C语言中,数据的类型决定数据的行为),然后对于加减其实就是对这个指针的大小加上或者减去,n*sizeof(这个指针指向的数据的类型)。即:一个类型为T的指针的移动,是以sizeof(T)为单位移动的。如:int* p; p+1就是p这个指针的值加上sizeof(int)*1,即:(unsigned int)p + sizeof(int)*1。对于什么typedef的,struct的,数组的都是一样的。
这个有一个例子,代码如下:
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) 
{
/*	int a[20]={1,2,4};
	printf("%d\n",sizeof(a));
	printf("%p\n",a);
	printf("%p\n",&a);
	printf("%p\n",&a[0]);	
*/


/*	int a[5]={1,2,3,4,5};
	int (*p)[5]=&a;
	printf("%d\n",*((int *)(p+1)-1));
*/	
	
	int a[5]={1,2,3,4,5};
	int* p=(int *)(&a+1);
//	int *p=&a+1;  //这个条语句是  把&a这个数组指针 进行了指针运算后  的那个地址  强制类型转换成了 int *指针 
	printf("%d\n",*(p-1));
	return 0;
	
}
5.访问指针和访问数组的两种方式:
分别是以下标方式访问和以指针的方式访问,我觉得没有任何区别,*(p+4)和p[4]是一样的 ,其实都可以理解成指针运算。如果非要说出区别,我觉得指针的方式会快些,但是在当前的硬件和编译器角度看,不会太明显。同样下标的方式可读性可能会高些。
6.切记数组不是指针:
数组是数组,指针是指针,根本就是两个完全不一样的东西。当然要是在宏观的内存角度看,那一段相同类型的连续空间,可以说的上是数组。但是你可以尝试下,定义一个指针,在其他地方把他声明成数组,看看编译器会不会把两者混为一谈,反过来也不会。
但是为什么我们会经常弄混呢?第一,我们常常利用指针的方式去访问数组。第二,数组作为函数参数的时候,编译器会把它退化成为指针,因为函数的参数是拷贝,如果是一个很大的数组,拷贝是很浪费内存的,所以数组会被退化成指针(这里一定要理解好,退化的是数组成员的类型指针,不一定是数组指针的哈)。
7.弄清数组的类型:
数组类型是由数组元素类型数组长度两个因素决定的,这一点在数组中体现的不明显,在数组指针的使用中体现的很好。
  char a[5]={'a','b','c','d','e'};
  char (*p)[3]=&a;
上面的代码是错误的,为什么?因为数组指针和数组不是一个类型,数组指针是指向一个数组元素为char 长度为3的类型的数组的,而这个数组的类型是数组元素是char长度是5,类型不匹配,所以是错的。
8.字符串问题:
a.C语言中没有真正的字符串,是用字符数组模拟的,即:字符串就是以'\0'结束的字符数组。
b.要注意下strlen,strcmp等这个几个函数的返回值,是有符号的还是无符号的,这里很容易忽略返回值类型,造成操作错误。
c.使用一条语句实现strlen,代码如下(此处注意assert函数的使用,安全性检测很重要):
#include <stdio.h>
#include <assert.h>

int strlen(const char* s)
{
    return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );
}

int main()
{
    printf("%d\n", strlen( NULL));
    
    return 0;
}
d.自己动手实现strcpy,代码如下:
#include <stdio.h>
#include <assert.h>

char* strcpy(char* dst, const char* src)
{
    char* ret = dst;
    
    assert(dst && src);
    
    while( (*dst++ = *src++) != '\0' );
    
    return ret;
}

int main()
{
    char dst[20];

    printf("%s\n", strcpy(dst, "hello!"));
    
    return 0;
}
e.推荐使用strncpy、strncat、strncmp这类长度受限的函数(这些函数还能在字符串后面自动补充'\0'),不太推荐使用strcpy、strcmpy、strcat等长度不受限仅仅依赖于'\0'进行操作的一系列函数,安全性较低。
f.补充问题,为什么对于字符串char a[256] = "hello";,在printf和scanf函数中,使用a行,使用&a也行?代码如下:
#include <stdio.h>
int main()
{
	char* p ="phello";
	char a[256] = "aworld";
	char b[25] = {'b','b','c','d'};
	char (*q)[256]=&a;
	
	printf("%p\n",a);  //0022fe48
	//printf("%p\n",&a);
	//printf("%p\n",&a[0]);
	
	
	printf("tian %s\n",(0x22fe48)); 
	printf("%s\n",q);    //q就是&a 
	printf("%s\n",*q);   //q就是a 
	
	printf("%s\n",p);
	
	printf("%s\n",a);
	printf("%s\n",&a);
	printf("%s\n",&a[0]);
	
	printf("%s\n",b);
	printf("%s\n",&b);
	printf("%s\n",&b[0]);	
}
对于上面的代码:中的0x22fe48是根据打印a的值获得的。
printf("tian %s\n",(0x22fe48));这条语句,可以看出来printf真的是不区分类型啊,完全是根据%s来判断类型。后面只需要一个值,就是字符串的首地址。a、&a、&a[0]三者的值还恰巧相等,所以说三个都行,因为printf根本就不判断指针类型。虽然都行但是我觉得要写有意义的代码,所以最好使用a和*p。还有一个问题就是,char* p = "hello"这是一个char*指针指向hello字符串。所以对于这种方式只能使用p。因为*p是hello字符串的第一个元素,即:‘h’,&p是char* 指针的地址,只有p是保存的hello字符串的首地址,所以只有p可以,其他都不可以。scanf同理,因为&a和a的值相同,且都是数组地址。
9.二维数组(本节最重要的知识点):
a.对于二维数组来说,二维数组就是一个一维数组 数组,每一个数组成员还是一个数组,比如int a[3][3],可以看做3个一维数组,数组名分别是a[0] a[1] a[2] sizeof(a[0])就是一维数组的大小 ,*a[0]是一维数组首元素的值,&a[0]是一维数组的数组指针。
b.也可以通过另一个角度看这个问题。a是二维数组的数组名,数组元素分别是数组名为a[0]、a[1]、a[2]的三个一维数组。对a[0]这个数组来说,它的数组元素分别是a[0][0] a[0][1]、 a[0][2]三个元素。a和a[0]都是数组名,但是是两个级别的,a作为数组首元素地址的时候等价于&a[0](最容易出问题的地方在这里,这里一定要弄清此时的a[0]是什么,此时的a[0]是数组名,不是数组首元素的地址,不可以继续等价下去了,千万不能这样想 a是&a[0]a[0]是&a[0][0] a就是&&a[0][0]然后再弄个2级指针出来,自己就蒙了!!!这是一个典型的错误,首先&&a[0][0]就没有任何意义,跟2级指针一点关系都没有,然后a[0]此时不代表数组首元素地址,所以这个等价是不成立的。Ps:一定要搞清概念,很重要!!!),a[0]作为数组首元素地址的时候等价于&a[0][0]。但是二维数组的数组头有很多讲究,就是a(二维数组名)、&a(二维数组的数组地址)、&a[0](二维数组首元素地址即a[0]一维数组的数组地址a有的时候也表示这个意思)、a[0](二维数组的第一个元素 即a[0]一维数组的数组名)、&a[0][0](a[0]一维数组的数组首元素的地址a[0]有的时候也表示这个意思),这些值都是相等,但是他们类型不相同,行为也就不相同,意义也不相同。分析他们一定要先搞清,他们分别代表什么。
下面是一个,二维数组中指针运算的练习(指针运算的规则不变,类型决定行为):
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main(int argc, char *argv[]) 
{
	int a[3][3]={1,2,3,4,5,6,7,8,9};
	printf("%d\n",sizeof(a[0]));
	printf("%d\n",*a[2]);
	printf("%d\n",*(a[0]+1));
	
	printf("%p\n",a[0]);
	printf("%p\n",a[1]);
	printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一样  指针类型不一样   &a[0]+1这个是数组指针  a[1]是&a[1][0] 是int*指针 
	
	printf("%d\n",*((int *)(&a[0]+1)));
	
	printf("%d\n",*(a[1]+1));
	
	printf("%p\n",a);
	printf("%p\n",&a);
	printf("%p\n",&a[0]);
	
	printf("%d\n",sizeof(a));   //这是a当作数组名的时候
	
	printf("%d\n",*((int *)(a+1))); //此时 a是数组首元素的地址  数组首元素是a[0]  
				 //首元素地址是&a[0]  恰巧a[0]是数组名 &a[0]就变成了数组指针 
	return 0;
}
总结:对于a和a[0]、a[1]等这些即当作数组名,又当作数组首元素地址,有时候还当作数组元素(即使当作数组元素,也无非就是当数组名,当数组首元素地址两种),这种特殊的变量,一定要先搞清它现在是当作什么用的
c.二维数组中一定要注意,大括号,还是小括号,意义不一样的。
10.二维数组和二级指针:
很多人看到二维数组,都回想到二级指针,首先我要说二级指针跟二维数组毫无关系,真的是一点关系都没有。通过指针类型的分析,就可以看出来两者毫无关系。不要在这个问题上纠结。二级指针只跟指针数组有关系,如果这个二维数组是一个二维的指针数组,那自然就跟二级指针有关系了,其他类型的数组则毫无关系。切记!!!还有就是二级指针与数组指针也毫无关系!!
11.二维数组的访问:
二维数组有以下的几种访问方式:
int a[3][3];对于一个这样的二位数组
a.方式一:printf("%d\n",a[2][2]);
b.方式二:printf("%d\n",*(a[1]+1));
c.方式三:printf("%d\n",*(*(a+1)+1));
d.方式四:其实二维数组在内存中也是连续的,这么看也是一个一维数组,所以就可以使用这个方式,利用数组成员类型的指针。
    int *q;
    q = (int *)a;
    printf("%d\n",*(q+6));
e.方式五:二维数组中是由多个一维数组组成的,所以就可以利用数组指针来访问二维数组。
    int (*p)[3];
    p = a;
    printf("%d\n",*(*(p+1)+1));
给一个整体的程序代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
	int a[3][3]={1,2,3,4,5,6,7,8,9};
	int (*p)[3];
	int *q; 
	printf("%d\n",*(*(a+1)+1));   //a        *(&a[0]+1)
	p = a;
	q = (int *)a;
	printf("%d\n",*(*(p+1)+1));
	printf("%d\n",*(a[1]+1));
	printf("%d\n",a[1][1]);
	printf("%d\n",*(q+6));
}
总结:对于二位数组int a[3][3] 要想定义一个指针指向这个二维数组的数组元素(即a[0]等一维数组),就要使用数组指针,这个数组指针要跟数组类型相同。a[0]等数组类型是元素类型是int,长度是3,所以数组指针就要定义成int (*p)[3]。后面的这个维度一定要匹配上,不然的话类型是不相同的。
这里有一个程序,要记得在c编译器中编译,这个程序能看出类型相同的重要性:
#include <stdio.h>

int main()
{
    int a[5][5];
    int(*p)[4];
    
    p = a;
    
    printf("%d\n", &p[4][2] - &a[4][2]);
}
12.二级指针:
a.因为指针同样存在传值调用和传址调用,并且还有指针数组这个东西的存在,所以二级指针还是有它的存在价值的。
b.常使用二级指针的地方:
(1)函数中想要改变指针指向的情况,其实也就是函数中指针的传址调用,如:重置动态空间大小,代码如下:
#include <stdio.h>
#include <malloc.h>

int reset(char**p, int size, int new_size)
{
    int ret = 1;
    int i = 0;
    int len = 0;
    char* pt = NULL;
    char* tmp = NULL;
    char* pp = *p;
    
    if( (p != NULL) && (new_size > 0) )
    {
        pt = (char*)malloc(new_size);
        
        tmp = pt;
        
        len = (size < new_size) ? size : new_size;
        
        for(i=0; i<len; i++)
        {
            *tmp++ = *pp++;      
        }
        
        free(*p);
        *p = pt;
    }
    else
    {
        ret = 0;
    }
    
    return ret;
}

int main()
{
    char* p = (char*)malloc(5);
    
    printf("%0X\n", p);
    
    if( reset(&p, 5, 3) )
    {
        printf("%0X\n", p);
    }
    
    return 0;
}

(2)函数中传递指针数组的时候,实参(指针数组)要退化成形参(二级指针)。
(3)定义一个指针指向指针数组的元素的时候,要使用二级指针。
c.指针数组:char* p[4]={"afje","bab","ewrw"}; 这是一个指针数组,数组中有4个char*型的指针,分别保存的是"afje"、"bab"、"ewrw"3个字符串的地址。p是数组首元素的地址即保存"afje"字符串char*指针的地址。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main(int argc, char *argv[]) 
{	
	char* p[4]={"afje","bab","ewrw"};
	char* *d=p; 
	printf("%s\n",*(p+1));  
	printf("%s\n",*(d+1));  //d  &p[0] p[0]是"afje"的地址,所以&p[0]是保存"afje"字符串的char*指针的地址	
	return 0;
}

d.子函数malloc,主函数free,这是可以的(有两种办法,第一种是利用return 把malloc的地址返回。第二种是利用二级指针,传递一个指针的地址,然后把malloc的地址保存出来)。记住不管函数参数是,指针还是数组, 当改变了指针的指向的时候,就会出问题,因为子函数中的指针就跟主函数的指针不一样了,他只是一个复制品,但可以改变指针指向的内容。这个知识点可以看<在某培训机构的听课笔记>这篇文章。

13.数组作为函数参数:数组作为函数的实参的时候,往往会退化成数组元素类型的指针。如:int a[5],会退化成int* ;指针数组会退化成二级指针;二维数组会退化成一维数组指针;三维数组会退化成二维数组指针(三维数组的这个是我猜得,如果说错了,希望大家帮我指出来,谢谢)。如图:

二维数组作为实参的例子:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int fun(int (*b)[3])  //此时的b为  &a[0] 
{
	printf("%d\n",*(*(b+1)+0));
	printf("%d\n",b[2][2]);// b[2][2] 就是  (*(*(b+2)+2))
	printf("%d\n",*(b[1]+2));
}

int main(int argc, char *argv[]) 
{
	int a[3][3]={1,2,3,4,5,6,7,8,9};
	 fun(a);//与下句话等价
	 fun(&a[0]);	
	return 0;
}

数组当作实参的时候,会退化成指针。指针当做实参的时候,就是单纯的拷贝了!

14.函数指针与指针函数:
a.对于函数名来说,它是函数的入口,其实函数的入口就是一个地址,这个函数名也就是这个地址。这一点用汇编语言的思想很容易理解。下面一段代码说明函数名其实就是一个地址,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

void abc()
{
	printf("hello fun\n");
}
int main(int argc, char *argv[]) 
{
	void (*d)();
	void (*p)();
	p = abc;
	abc();
	printf("%p\n",abc);
	printf("%p\n",&abc);//函数abc的地址0x40138c
	p();
	(*p)(); 	
  	d = ((unsigned int*)0x40138c);  //其实就算d= 0x40138c这么给赋值也没问题 
  	d();
	return 0;
}   
 

可见函数名就是一个地址,所以函数名abc与&abc没有区别,所以p和*p也没有区别。

b.我觉得函数指针最重要的是它的应用环境,如回调函数(其实就是利用函数指针,把函数当作参数进行传递)代码如下,还有中断处理函数(同理)详细见<

ok6410学习笔记(16.按键中断控制led)>中的 中断注册函数,request_irq。还有就是函数指针数组,第一次见到函数指针数组是在zigbee协议栈中。

回调函数原理代码:

#include <stdio.h>

typedef int(*FUNCTION)(int);

int g(int n, FUNCTION f)
{
    int i = 0;
    int ret = 0;
    
    for(i=1; i<=n; i++)
    {
        ret += i*f(i);
    }
    
    return ret;
}

int f1(int x)
{
    return x + 1;
}

int f2(int x)
{
    return 2*x - 1;
}

int f3(int x)
{
    return -x;
}

int main()
{
    printf("x * f1(x): %d\n", g(3, f1));
    printf("x * f2(x): %d\n", g(3, &f2));
    printf("x * f3(x): %d\n", g(3, f3));
}

注意:可以使用函数名f2,函数名取地址&f2都可以,但是不能有括号。

c.所谓指针函数其实真的没什么好说的,就是一个返回值为指针的函数而已。

15.赋值指针的阅读:

a.char* (*p[3])(char* d); 这是定义一个函数指针数组,一个数组,数组元素都是指针,这个指针是指向函数的,什么样的函数参数为char* 返回值为char*的函数。

分析过程:char (*p)[3] 这是一个数组指针、char* p[3] 这是一个指针数组 char* 是数组元素类型、char* p(char* d) 这个是一个函数返回值类型是char* 、char (*p)(char* d)这个是一个函数指针。可见char* (*p[3])(char* d)是一个数组 数组中元素类型是 指向函数的指针,char* (* )(char* d) 这是函数指针类型,char* (* )(char* d) p[3] 函数指针数组 这个不好看 就放里面了。(PS:这个看看就好了~~~当娱乐吧)

b.函数指针数组的指针:char* (*(*pf)[3])(char* p) //这个就看看吧 我觉得意义也不大 因为这个逻辑要是一直下去 就递归循环了。

分析过程:char* (* )(char *p) 函数指针类型,char* (*)(char *p) (*p)[3] 函数指针 数组指针 也不好看 就放里面了。





分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics