C语言精要总结-指针系列(一)
考虑到指针内容繁多,这里将指针作为一个系列,从简入繁,带着没有研究过指针的朋友,一点一点深挖并掌握这C语言的精华。初步计划如下

此文为指针系列第一篇:
内存与地址
我们可以把内存看做一排连续的房间,每个房间(字节空间)都有一个房间号,房间号就是这个房间的地址,而且每个房间里都有八个位。

为了存储不同大小的值,多数时候我们要用连续几个房间来存储一个值,这时我们会用其中一个房间号来表示这一片连续的房间,至于这个房间号是第一个房间的房间号,还是最后一个房间的房间号,不同的机器有不同的规定。文中我们假设这个房间号是左起第一个房间的房间号,这样房间号(指针)其实就有了类型之分。

通过地址,计算机就可以操纵内存单元的内容,虽然在代码中,类似于*0x0048f93d = 'a';这样的表达式是合法的,但为了让代码看起来更友好,显然不能在代码里全部写数字地址。因此高级语言的编译器为我们实现了直接通过变量名来访问内存位置,当然硬件单元依然通过寻址来访问内存位置。

指针变量
指针变量本身也是变量,也需要在内存中占用一定的存储单元,只是其存储的值是其他变量的内存地址,分配的位置也可以是跟基本类型变量是连续的。(下图中一个长方形并不代表一个字节),如图:

用代码来描述即是
short shortVar = ;
int intVar = ;
short * p1 = & shortVar;
int * p2 = & intVar;
上面的语句给出了指针定义(type *)和初始化(= &var)的方法。这里定义了一个名叫p1指向short类型的指针变量,并用shortVar的地址来初始化;定义了一个名叫p2指向int类型的指针变量,并用intVar的地址来初始化。当然,像下面这样直接用一个地址值来初始化指针也是合法的
int *p = (int *)0x0048f93d;
虽然这在我们看来是个地址值,但在编译器眼里,这是一个int类型的值,所以需要强制转换。这种写法,除非很明确这个地址时用来做什么的,否则不要这么做。
如果在定义一个指针变量时,还不确定用什么地址来初始化,则一定要初始化为NULL,这是一个空指针值,也是一个值为0的宏,它代表指针不指向任何位置。如果不给一个局部指针变量做任何初始化,它存储的将是一个不可预知的值,指向一个不可预知的位置,如果对这样一个指针变量进行操作,很容易引起异常中断。而对于全局变量,编译器会自动初始化为0。
解引用操作
解引用,又叫间接访问,即通过一个指针变量访问它所指向的地址的过程。这个解引用操作符便是单目操作符*。但注意对一个指针进行进行解引用,不一定是取值,也可能是写值,这取决于解引用表达式是作为左值(赋值符号左边)还是右值(赋值符号右边)。
例如对上述指针p1,p2进行解引用操作,如下代码
printf("%d\n",* p1); //
printf("%d\n",* p2); //
*p1 = ;
*p2 = ;
printf("%d\n",* p1); //
printf("%d\n",* p2); //
那么像下面这个表达式做了什么呢?
*&shortVar = 1;
很显然,这是将1赋值给变量shortVar,根据右结合性,取地址(&)之后立即解引用(*),这其实多此一举,如果编译器不对这样的代码做优化,那将生成一些无意义的操作代码。
二级指针
二级指针,也叫指针的指针,也就是一个指向指针变量的指针。按照指针变量的定义方法(type * pVar),我们要定义一个指向整型指针变量的指针,应该像下面这样定义
int * * p2p = & p2;
没错,这就是定义一个指向整型指针的指针的定义方式。在内存中结构(假设分配的恰好是连续的)就如下图所示

很显然,二级指针变量依然也是一个指针变量,哪怕后面还有三级、四级指针变量,都始终是一个指针变量,对它进行解引用或者取地址,原理跟一级指针是一样的。比如对二级指针p2p进行一次解引用,将得到p2这个指针变量,再进行一次解引用将得到intVar这个变量,正如上图所示。
printf("%d\n",**p2p); //
二级指针跟二维数组名是有很大区别的,这会在后续的文章中指出。另外,如果对一个二级指针取地址,将得到一个三级地址,依次类推。
指针的大小
我们知道指针是用来存储地址值的,而分配给指针变量的空间,只用来存储地址值,而不会记录变量类型等信息,这跟普通变量是一样,它们被记录在编译器的符号表中。
既然指针变量自身的空间只存地址,那么不管什么类型的指针,它们占用的空间大小应该是一样的,那究竟应该分配多大的空间?这取决于CPU最大寻址地址的大小。为了保证指针变量能存下最大的寻址地址,应该给指针变量分配足以存储最大寻址地址的大小。
在32位CPU上,CPU最大寻址空间为2的32次方(4G),因此要存下最大的32位的地址值,需要为指针变量分配4个字节的空间,而在64位CPU中,为了能寻到2的64次方的内存空间,需要为指针变量分配8个字节的空间。
因此,编译器也充分考虑了这个问题。它可以控制分配的指针变量的空间大小。用VS在写Console Application时,默认编译的是32位 Console Application,这是为了保证程序的兼容性,以保证程序一定可以在32位和64位机器上运行,此时,vs编译器默为指针变量分配4个字节的空间。但是本人的笔记本是支持64位寻址的CPU,因此,本人用gcc version 5.1.0 (tdm64-1)编译出来的程序,指针变量分配了8个字节的空间。
后续文章中,如不明确指出,我们认为指针变量占4个字节的空间。
指针类型强制转换
在看怎么定义二级指针时,有读者可能考虑,为什么不能这样定义
(int *) * p2p = & p2;
乍一看可能没什么不对,但实际上,这个表达式并不是定义一个变量,而是在执行一个非法的赋值操作。假如前面已经定义过p2p这样的一个二级指针,这个表达式还真会做一些事情:
- 解引用p2p得到一个一级指针tmp
- 将一级指针tmp强制转换为一个指向int 类型的指针
- 取p2指针的地址(一个二级指针)赋值给一级指针tmp(注意是赋值给一级指针本身,而不是一级指针指向的变量)
显然这是不能执行成功的。但是它却告诉我们,指针是可以强制转换的。
但对指针类型强制转换,和普通数据类型会有些不一样:对指针类型强制转换,不会改变指针变量本身空间的大小及空间内存储的地址值,而只会修改符号表中的指针类型及其指向类型占用空间的大小值(为指针运算做准备)。
一起来图解一下下面这段代码
// int a = 0x12345678;
// return *(char*)(&a) == (char) a;
int a = 0x12345678;
int * pa = &a ;
char * pch = (char *) pa;
char ch = (char) a;
printf("%x\n",*pch);
printf("%x\n",ch);
假如程序出现的变量按如下方式分配

对指针强制转换之后,pch存储的地址值跟pa存储的地址值时一样的,但是他们在编译器符号表中的类型是不一致的,因此指向的空间大小是不一样的,pa指向整个变量a,而pch指向变量a的第一个低字节。ch变量毫无疑问存储的将是78,因为对一个基本数据强制转换,只会取数据的低位。
很显然,如果按照图中所示,程序的第7行第8行将输出12和78。但实际上在本人的笔记本上,两次都是输出78。
这其实就是很经典的大端存储和小端存储的判别。如果按照图中所示,其实变量a是按大端模式存储(即低地址存高位)。而如果按照小端模式存储,则应该低地址存低数据位,如下图。

而上面那段代码,就是用来检测计算机是按大端存储还是按小端存储的,很显然,本人的笔记本按小端存储。
用这个程序想说明的是,对一个指针进行调整级别的强制转换再解引用,可能会引起一些兼容性问题,因为这取决于系统实现。
另外在程序中我们会经常看到void * 类型的指针,这样的指针主要是为了写通用的代码,你可以将任意类型的指针强制转换为void* 类型的指针,在之后要解引用的时候,再强制转换回正确的指针类型进行解引用。例如我们常见的c语言库函数qsort中的:int comparator ( const void * elem1, const void * elem2 );。
C语言精要总结-指针系列(一)的更多相关文章
- C语言精要总结-指针系列(二)
此文为指针系列第二篇: C语言精要总结-指针系列(一) C语言精要总结-指针系列(二) 指针运算 前面提到过指针的解引用运算,除此之外,指针还能进行部分算数运算.关系运算 指针能进行的有意义的算术运算 ...
- 【C++自我精讲】基础系列一 指针与引用
[C++自我精讲]基础系列一 指针与引用 一 前言 指针.引用.指针与引用区别. 二 指针 变量:代码中常常通过定义变量来申请并命名存储空间,并通过变量的名字来使用这段存储空间. //变量 ...
- 【C++自我精讲】基础系列二 const
[C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...
- 【C++自我精讲】基础系列四 static
[C++自我精讲]基础系列四 static 0 前言 变量的存储类型:存储类型按变量的生存期划分,分动态存储方式和静态存储方式. 1)动态存储方式的变量,生存期为变量所在的作用域.即程序运行到此变量时 ...
- 【C++自我精讲】基础系列六 PIMPL模式
[C++自我精讲]基础系列六 PIMPL模式 0 前言 很实用的一种基础模式. 1 PIMPL解释 PIMPL(Private Implementation 或 Pointer to Implemen ...
- Atitit java方法引用(Method References) 与c#委托与脚本语言js的函数指针
Atitit java方法引用(Method References) 与c#委托与脚本语言js的函数指针 1.1. java方法引用(Method References) 与c#委托与脚本语言js ...
- 【转载】C/C++语言void及void指针深层探索
C/C++语言void及void指针深层探索 1.概述许多初学者对C/C++语言中的void及void指针类型不甚理解,因此在使用上出现了一些错误.本文将对void关键字的深刻含义进行解说,并详述vo ...
- 函数指针玩得不熟,就不要自称为C语言高手(函数指针是解耦对象关系的最佳利器,还有signal)
记得刚开始工作时,一位高手告诉我说,longjmp和setjmp玩得不熟,就不要自称为C语言高手.当时我半信半疑,为了让自己向高手方向迈进,还是花了一点时间去学习longjmp和setjmp的用法.后 ...
- C语言中的函数指针
C语言中的函数指针 函数指针的概念: 函数指针是一个指向位于代码段的函数代码的指针. 函数指针的使用: #include<stdio.h> typedef struct (*fun_t ...
随机推荐
- checkSelfPermission 找不到 Android 动态权限问题
checkSelfPermission 找不到 Android 动态权限问题 最近写了一个Demo,以前好好地.后来手机更新了新系统以后,不能用总是闪退.而且我的小伙伴的是android 7.0系统 ...
- 《连载 | 物联网框架ServerSuperIO教程》- 16.OPC Server的使用步骤。附:3.3 发布与版本更新说明。
1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...
- java学习(一)静态代码块 构造代码块 构造方法的执行顺序及注意问题
今天我总结了一下java中静态代码块 构造代码块 构造方法的执行顺序及其注意问题 首先要知道静态代码块是随着类的加载而加载,而构造代码块和构造方法都是随着对象的创建而加载 当时做了这么一个小案例(想必 ...
- Angular4.0.0正式版发布
来源于angular4.0.0发布时的公告,译者:niithub 原文发布时间:Thursday, March 23, 2017 翻译时间:2017年3月24日 angular4.0.0正式版现在可以 ...
- rgba()和opacity的使用
rgba()表示 红 绿 蓝 alpha ,W3C指在原有的rgb颜色模型之后增加了 “alpha”参数,“可以让制定的颜色透明化”(rgb()上扩展的,其只可以设置颜色,而不能使设置的颜色透明化) ...
- MYSQL数据库-约束
约束是一种限制,它通过对表的行或列的数据做出限制,来确保表的数据的完整性.唯一性. MYSQL中,常用的几种约束: 约束类型: 主键 默认值 唯一 外键 非空 关键字: PRIMARY KEY DEF ...
- shell 处理 文件名本身带星号的情况
获取到的所有文件名放到数组中时必须加上引号,不然 for 循环时会被解析成通配符,或者使用 shell 字典,同样也需要引号. shell 字典示例 #!/bin/bash echo "sh ...
- Android Studio项目构建常见问题解决
1. 创建或导入项目后编译时一直在等待 问题: 原因:AS连网去下载gradle了,但是网络不好或不通 解决:禁用网络,AS就会立即自动终止下载进入到主界面了.此时再去指定离线的gradle版本进行编 ...
- Azure Messaging-ServiceBus Messaging消息队列技术系列7-消息事务
上篇博文中我们介绍了Azure Messaging-ServiceBus Messaging消息回执机制. Azure Messaging-ServiceBus Messaging消息回执机制 本文中 ...
- IOS开发创建开发证书及发布App应用(五)——编译应用
5.编译应用 最近升级ios7,一直没有时间写,终于搞完了,完成之前没有完成的工作 由于适配ios7,所以Xcode也升级到5了,所以下面截图基本在Xcode5上,以前的版本基本也差不多的 打开项目的 ...