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 ...
随机推荐
- 前端总结·基础篇·JS(三)arguments、callee、call、apply、bind及函数封装和构造函数
前端总结系列 前端总结·基础篇·CSS(一)布局 前端总结·基础篇·CSS(二)视觉 前端总结·基础篇·CSS(三)补充 前端总结·基础篇·JS(一)原型.原型链.构造函数和字符串(String) 前 ...
- 解决此问题:Oracle 删除用户时报 “必须指定 CASCADE 以删除 'SE'”,
这说明你要删除的oracle 用户"SE" 下面还有数据库对象,如 table, view 等,这样你删除用户时必须加选项 cascade:drop user se cascade ...
- CCS内存数据转成图片
在嵌入式DSP图像处理开发过程中,经常需要将DSP内存中的图像数据保存下来,作为数据集.CCS5.4或者CCS3.3都只支持保存内存原始数据而不支持将内存数据直接存储为一张图片,为了能将CCS保存的. ...
- rsync (windows 服务端,linux客户端)将windows上的数据同步到linux服务器,反之也可
一:总体概述. 1.windows上面首先装CW_rsync_Server.4.1.0_installer,安装时要输入的用户名密码要记住哦!接下来就是找到rsyncd.conf进入配置细节 2.li ...
- 常见端口、端口查询及TCP状态
查看电脑端口的开放情况命令:cmd——netstat -a -n -a:显示所有连接和监听端口:-n:以数字形式显示地址和端口号 “本地地址”指本地IP地址及其正在使用的端口号,“外部地址”指连接某端 ...
- 日新进用户200W+,解密《龙之谷》手游背后的压测故事
2017年3月,腾讯正式于全平台上线了<龙之谷>手游,次日冲到了App Store畅销排行第二的位置,并维持到了现在.上线当日百度指数超过40万,微信游戏平台数据显示预约数780多万,而据 ...
- ng2响应式表单-翻译与概括官网REACTIVE FORMS页面
本文将半翻译半总结的讲讲ng2官网的另一个未翻译高级教程页面. 原文地址. 文章目的是使用ng2提供的响应式表单技术快速搭出功能完善丰富的界面表单组件. 响应式表单是一项响应式风格的ng2技术,本文将 ...
- git clone操作到开发机的错误记录
在开发机上,执行操作 $ git clone https://github.com/xxx/rank.git 返回错误: error: The requested URL returned error ...
- SQL Server 备份所有数据库代码
今天让我备份一下网上所有数据库,猛地一看,几百个呢, 坑爹呢,只好网上找找有没有简便的,没想到还真有 记下来,以后好用,哈哈... use master declare @DbName varchar ...
- 将ROS中的/sensor_msgs/NavSatFix数据导入google earth显示轨迹
将ros中的gps_msg数据导入google earth显示轨迹 [TOC] 1. 获取GPS数据 将ros中发布的gps topic输出到文本中 rostopic echo -p /gpsData ...