C语言指针理解到深入(1)

本文同步发布在程序猿网斋的小菜博客 http://www.w2cxy.com/blog-2-23.html

为叙述方便,统一一下地址和指针的说法,本文中地址定义为内存位置,指针定义为带存储结构描述的地址,没有特别说明指针指的是一级指针,指针变量定义为存放指针的变量!在32位系统中,内存寻址范围为0x0000_0000到0xFFFF_FFFF,而一个程序的内容(变量、常量、函数...)会存放到内存中的某个位置,具体位置与编译器、系统都有关,当然一些C语言编译器支持程序员指定绝对存储位置。下面将从多方面探讨C语言的指针问题。

1、变量、内存、地址
声明一个变量 
int a = 0x12344321;
则这个变量会由编译器或者运行时分配内存,假设首地址是0xF000_0001则变量在内存存储情况如下:
0xF000_0001   0x12
0xF000_0002   0x24
0xF000_0003   0x43
0xF000_0004   0x21

2、指针的引出
在程序中访问上面1的a变量是通过变量名a,但是我们自然会想到是不是可通过地址去访问这个变量,显然只要知道了a的首地和它的类型(知道了类型就知道了存储结构、占用多少个字节)就可去访问这个变量了。C语言支持这种访问方式!比如上面a如果通过地址访问:
int b = *((int *)0x0xF0000001);/*b = a = 10*/
上面的语句虽然比较晦涩,但是它确实完全符合C语言语法标准,而且确实出现了变量首地址和类型两种变量访问条件,(int *)0x0xF0000001这时可以称为指针或者整型指针了。期间两个地方出现了一个*符号,在这种场合它就不是乘法的意思了。第二个*和int组合表示了一种新的数据存储类型,即整型指针!地址0x0xF0000001(整型数)强转为了int *类型。
其他存储类型的指针类型的都在原来的类型名后加*就是指针类型了,表中列出了一下对应关系:
char   long   float double...
char * long * float doubel *...
*在操作(读、赋值等)时出现即第一个*是个操作,称为间接运算符(注意*和存储类型一起出现时是为了组合出指针类型,不是运算符)。表示取指针(int *)0x0xF0000001所指向的内存存放的值,也就是变量a的值。 上面通过绝对地址强转为指针类型访问内存中存放的内容,称为绝对访问!可惜大部分情况下我们无法直接获取存放内容的地址,就像a的地址一样,只是个假设值,但是显然编译器或者运行环境是知道的,所以获取指针的事情往往不是由程序员做的(底层开发的程序员比如嵌入式程序员除外)!在C语言中有获取指针的语法即&符号,&在指针操作中表示获取对象的指针,而不是与的意思,也是个运算符称为寻址运算符(个人觉得叫取指运算符更贴切,因为&获得的值是带类型描述的)。看一个&的例子:
int c = *(&a); /*c = a = 10*/
上面的&a等同于(int *)0x0xF0000001,但是(int *)0x0xF0000001是个假设值没有实际意义,&a却是由编译器或者运行时完成获取a的指针的操作!int c = *(&a)和int c = a的c结果是一致的,这样操作好像没有意义,而且前者经历&*=三步运算,后者只要=一步运算,前者效率较低!但是这里运用指针的效率低是个特例,指针偏偏是在为很多解决效率低问题的场合登场的!

3、指针变量与指针指向值的操作
操作(或者说访问)即读操作与写操作。  
3.1 指针变量的声明与操作
上面的例子int c = *(&a);得到指针又获取指针指向的内容之后指针就没用了,只看到他的一个中间作用,那么有没有办法把这个指针存储下来,以便后面的程序继续使用呢?指针也是个值,分配内存用来存放某个指针的值就是声明指针变量了的过程(当然也可以是指针数组存放多个指针),他的声明方法如下:
int *p1; /*未初始化*/
int *p2 = &a; /*声明的同事初始化*/
int *p3 = (int *)0x0xF0000001; /*没有实际意义 但是符合语法*/
上面p1到p3都是合法的指针变量声明,比如p2存放的值就是&a,那么上面提到的
int c = *(&a);
这样的语句也可以改为
int c = *p2; /*c = a = 10*/
当然指针变量和其他变量一样,也可以在声明后修改所存放的指针,比如
int e = 10
p2 = &e; /*原来p2存的是&a*/
指针也可以给指针赋值,赋值,比如
p1 = p2; /*赋值后p1和p2存放的指针一样,也就是他们指向了相同的内存空间*/
3.2 指针内容的访问
指针内容的访问即通过指针访问指针指向的内存内容,当然访问指向内容是个间接操作 ,需要用间接操作符*,访问前必须先访问指针本身,比如下面程序片段:
int f = 123;
int *p4;
p4 = &f; /*写f的指针到p4中*/
*p4 = 100; /*p4中获取f的指针后,通过f的指针访问指针的内存即f的值*/
printf("f=%d,*p4=&d", f, *p4); /*输出结果:f=100,*p4=100*/
可以抽象地理解*p4是个整体,即为p4指向的值等同于f,但是实际上*是个操作,*p4即【间接操作(*)】【指针(p4)的内容】,而没有*号直接写p4就是【操作】【指针变量本身】。

4、数组与指针的相似性
声明一个数组
unsigned char array[3] = {1,2,3}
数组在内存中一种假设情况存储情况如下
0xF000_0001   0x1
0xF000_0002   0x2
0xF000_0003   0x3
也就是说他们的地址是紧挨着的,显然知道了第一个元素的指针,就可以推导出后面元素的指针了。a除了代表数组名之外也代表着第一个元素的指针,即a等同于&a[0],是个unsigned char *类型指针,所以数组可以用数组名为指针的方式操作数组内容,比如
char tmp1 = *a; /*char tmp = a[0])*/
char tmp2 = *(a+1); /*char tmp2 = a[1]*/
*(a+2) = 3; /*a[2] = 3*/
当然也可以声明一个变量存放数组的第一个(不是是第一个元素也是可以的)元素的指针,然后操作数组,比如
unsigned char * pArray = a;
char tmp1 = *pArray;
数组指针的指向还可以动态地改变,所以处理使用偏移量(类型这样的操作*(pArray+1)其中1就是偏移量,pArray作为基地址保持不变)访问数组其他元素,还可以通过修改变量的值,比如:
int i;
for(i=0; a<sizeof(a); i++){
    printf("%d", pArray++);
}
当然指针也可以像数组名一样操作,比如
char tmp1 = pArray[0];
但是数组名和指针变量不完全等同,如果是数组名a不可用出现++、--这种修改自身值的操作,所以用指针变量替代数组名操作数组会灵活很多!导致这种区别的原因在于数组名其实是个只读指针变量,而不是指针变量,如果把
unsigned char * pArray = a;
修改为
unsigned char * const pArray = a;
pArray 和数组名的限制情况就一致了,一旦声明,只读指针变量本身就不可以修改了,但是它指向的内容是可以写的!

5、char *和字符串指针
char *和int *的用法一样,比如
char ch = ‘a’;
char *pCh = &ch;
之所以char *要单独拿出来理解,是因为char *还可以表示字符串,比如
char *str = "HelloWord!";
这时str是一个指向"HelloWord!"第一个字符即'H'的指针,str的类型是char*,只知道str对应的是一个字符而不是字符串,所以从str中不知道字符串有多长,判断字符串结束时是通过字符串是否出现了'\0',所以"HelloWord!"字符串的内容为HelloWord!'\0',隐藏了一个不可见的结束符,结束符也栈一个字节。因为字符串常量是不可修改的,所以尝试通过str修改字符串内容是个错误的操作。位了避免错误去写字符串常量,更好的做法是明确用const修饰str指向的内容,比如把
char *str = "HelloWord!";
修改为
const char *str =  "HelloWord!";
实际上C语言没有字符串类型,C语言的字符串只能以数组的形式在内存中存储,所以同样可以用数组的形式声明字符串,但是用数组声明的字符串和用指针声明的会产生一些区别,对比下面的几个声明
char str1[] = "HelloWord!";
const char str2[] =  "HelloWord!";
char str3[11] =  "HelloWord!";
char str4[12] =  "HelloWord!";
char str5[10] =  "HelloWord!";
str1、3、4、5的内容是可改变的(原因是初始化数组时编译器或运行时是把字符串中的值拷贝的数组中,而不是让数组指向字符串常量),但是指针str指向的字符串是不可变的(不管有没有const修饰,因为初始化指针时编译器或运行时是把字符串常量的指针存到指针变量中);str2完全等同于str了,str3和str1也是完全等同的,str4的实际可访问的内存是12字节
,而str、str2、str3可访问的内存是11个字节,虽然多了一个可以访问字节,但是如果把str4当字符串处理,字符串的长度依然不变;str5严格来说不能当作字符串,因为在初始化的时候str5只能存放下字符串的前10字节,结束符'\0'被丢弃了,没有结束符的字符集实际上不符合字符串的概念!重要的是调用没有结束符的的字符集是非常危险的,比如
printf(str5);
printf通过判断字符结束符而退出函数,所以printf输出没有结束符的字符集产生的结果完全是不可预料的!

6、指针数组(下面举的例子和上面的代码片段不再关联)
指针数组(数组的元素是指针),比如
char a,b,c;
char *array[3] = {&a, &b, &c}; /*该数组的元素类型是char **/
char *strArray[] = {"abc","abcd","abcdef"};/*该数组放着几个字符串常量的指针,而不是字符串全部内容*/

7、数组指针(下面举的例子和上面的代码片段不再关联)
数组名的复用功能非常多,他代表着数组,代表第一个元素的指针(注意不能代表整个数组的指针),同时它也支持指针间接运算符。有数组声明
int array[3];
&array代表的是整个数组的指针,他的指针类型不再是char *,而是有的怪异的 int (*)[3]指针类型。如果企图存储
int *pArray = &array; 
编译器会报如下错误
a value of type "int (*)[3]" cannot be used to initialize an entity of type "int *"
如下存储依然错误
int *p[3]pArray =  &array;
正确的语法是是
int (*pArray)[3] =  &array;
这里的pArray( &array或者)就叫数组指针,它代表指向整个数组,做个打印测试
printf("%p, %p\n", array, pArray);
对比array和&array的值是一样的,可见一个数组的数组指针和数组第一元素的指针的值都是数组首地址。
array和pArray和区别在于一个是int *数据存储类型一个是int (*)[3]数据存储类型,数据存储类型决定指针操作内存的差异,对比下面的操作
int * pArray1= array;
int (*pArray2)[3] =  &array;
pArray1++; /*pArray1地址偏移量加4*8就是sizeof(int)字节*/
pArray2++;/*pArray2地址偏移量加4*8*3就是一个数组的长度也是sizeof(int [3])个字节*/
数组指针可以方便地访问二维数组的元素,比如下面的代码片段
int towArray[100][100];
int(*pTmp)[100] = towArray;
unsigned char i,j;
for (i = 0; i < 100; i++){
   **pTmp = i;
   pTmp++;
}
上面的例子虽然没有什么意义,但是要看懂也并不是那么容易,towArray是个二维数组,它有着100个int [100]型数组,&int [100]的指针类型正是int (*)[100],而towArray代表第一个一维数组的指针,等同于&towArray[0],所以int(*pTmp)[100] = towArray是把pTmp指向了第一个一维数组。再看第一次循环*pTMP代表间接操作第一个一维数组,**pTMP代表间接操作第一个一维数组的第一个元素,纠结哦!其实等同于pTmptowArray[0][0]。 pTmp++将pTmp指针指向下一个二维数组的元素(注意他的地址偏移值不是1而是100)。

8、二级指针
所谓二级指针指的是,它本身是个指针,它指向的内容也是个指针。
下面还是从简单代码片段入手,比如
int a1;
int p1 = &a;
int pp1 = &p1; 
a1 = 100;
*p1 = 200;
**pp1 = 300;
上面pp1就是一个二级指针变量,他指向的是p1,而p1指向的是a1, a1、*p1、**p1可以操作相同的内存。
其实上面6的 **pTmp = i已经是一个二级指针操作了。回到上面6的代码片段中,为何指针pTMP明明指向的是数组,还出现**pTmp的二级指针的操作?原因在于数组操作也可以用指针操作!towArray的元素放的为int (*)[100]类型指针,towArray也可理解为指针,那不是二维数组名可等同于二级指针对待!

未完待续:下一篇将将讨论指针的应用,即指针和函数的情缘!
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dz2015

哈哈 菜鸟来混一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值