深入浅出C指针
http://bbs.9ria.com/blog-164422-18039.html
初学者在学习C语言时,通常会遇到两个瓶颈,一个是“递归”,一个是“指针”。大学老师在讲述这两个知识点时通常都是照本宣科,而没有站在一个初学者的角 度来审视问题,更没有剖析其内部机理。本人在此将发表一系列技术文章,希望能将C语言中“指针”这一概念讲述清楚,希望初学者能从中收益。在此笔者也极力 推荐Kenneth A.Reek写的《Pointers On C》这本书。
1.内存和地址
初学者面对内存一词时总是有一种既陌生又熟悉的感觉。首先,在日常生活中大家总是会讨论某某设备内存有多大,是不是该加一个内存条等等。但是,内存究竟是 什么?在此,笔者并不想深入探讨内存的本质,以及内存在不同操作系统上的结构有什么区别等等这一系列问题。这里只希望初学者知道以下三个问题即可:
(1)内存是计算机在运行过程中存储数据的地方
(2)内存被分割成了“无数”个小区域,每个区域的大小在不同的环境下有所不同,可能是1个字节,2个字节,或者多个字节。
(3)每个小区域都有一个独一无二的标识,即我们后面所说的地址(指针)。
(4)每个小区中都有包含一个值,可以是整形,浮点型,字符型等等。
我们可以形象的用下图表示内存的结构:
100 104 108 112 116
| | | | |
------------------------------------------------------------------------
(542) (3.14) (‘A’) (12323) (-12)
如图所示,上方表示内存的地址,下方括号中的内容表示该地址下内存中的值。我们想访问这些内存中的值,只需要知道内存地址即可。但是,我们怎么知道我们要 访问的内容其内存地址是多少?的确,要记住这些内存地址几乎是不可能的,所以,编译器可以让我们用变量的形式访问他们,于是这张图可以变成如下所示:
total_count Pi m_ch money number
| | | | |
------------------------------------------------------------------------
(542) (3.14) (‘A’) (12323) (-12)
我们将内存地址变成了我们更容易记住的变量,这样我们在编写程序的时候就可以方便的访问内存中的数据。但是,请记住,这只是编译器帮助我们进行了优化,而 真正编译后的机器码则是通过真正的内存地址来访问内存中的数据,即寻址,关于计算机的寻址过程,有兴趣的读者可以参考计算机组成原理或者汇编语言等书。
2.值和类型
我们首先来看一下下面这几个表达式:
int total_count = 542;
float Pi = 3.14;
int *p1 = &total_count;
char *p2 = &m_ch;
前两个表达式很好理解,我们申明了一个整形和一个浮点型变量,并分别赋值为542和3.14。那后面两个表达式是什么意思呢?
我们姑且可以简单记住:在申请变量的表达式中,如果类型的后面出现了*号,那么这个变量就叫做指针,或者指针变量。指针变量分为很多种类型,例如整形指针,浮点型指针,字符型指针等等。
好了,我们知道了指针的概念,那么,指针到底是什么?
指针也是变量,即指针变量,它和其他的变量在本质上是没有区别的。但是指针变量只能保存一种值,就是地址。
也许你会问了,既然指针保存的是地址,那么为什么要将指针分为那么多的种类,整形值要有整形指针,字符型值要有字符型指针?为什么不能用一种指针就把所有的地址都包含了呢?难道不同类型变量的地址也不同吗?
这个问题,本人没有查阅过官方的解释,首先可以肯定,内存地址在理论上是没有任何区别的,无论是用来保存什么类型变量的内存,其本质都是01Bit构成的 区域,内存地址当然也不会有任何区别,从纯技术技术的角度上讲,笔者认为编译器完全可以建立一种制度,用统一的指针类型保存不同变量的地址,这完全不会影 响程序的运行。但是编译器没有这样做的原因,笔者分析主要是出于安全性的考虑。当程序员有意或无意的将两种不同类型的指针所指向的内存内容进行赋值时,如 果编译器事先不能做出检查,那么也许会在程序运行过程中出现异常,例如产生非常严重的缓冲区溢出错误。
3.指针的间接访问符
好了,我们将问题回到这几个表达式上来。现在的问题是p1和p2这两个变量中保存的是什么。拿p1为例,也许你可以这样理解,因为p1是指针变量,所以他 的值应该和他指向的内存中的值一样,所以p1 = 542. 这样理解看似非常符合逻辑,但却是一个大的错误。虽然p1很特殊,但是指针变量也是变量,它不会聪明到自动去完成一个非常复杂的自动间接访问操作。p1的 值实际是100,即变量total_count的地址。但是读者请注意,100并不是传统意义上整形100,例如 int *p1 = 100;这是一个错误的表达式,因为不能直接将整形值赋值给指针变量,作如下变化即可 int *p1 = (int *)100;
也就是说,如果我们输出p1的值,打印出来将会是一串莫名其妙的数字。当然,我们通常对这些莫名其妙的地址不感兴趣,我们更感兴趣的是这些地址背后影藏这怎样的信息。所以我们可以这样来访问int count = *p1;
等等...读者读到这里也许有些糊涂了,
int *p1 = &total_count; int count = *p1; 这么多的*号和&号也许读者有些搞不清了。我们来理清一下思路。
在int *p1 = &total_count;中:
*p1只是一个标识,代表p1是一个指针,以后访问这个指针时直接p1即可,访问得到的值是一个地址编码。
&total_count代表一个地址,在任意一个变量前(包括指针)加上&符号,都将代表这个变量所在内存的地址。&p1则代 表指针所在内存的地址,即指针的指针(稍后详细介绍)其实这么有什么可疑问的,因为指针本身也是变量,所以和其他变量本质上没有任何区别。
在 int count = *p1;中,p1和前面说的一样,访问它的值是一个地址,而*号与之前的*号有些不同,之前的*号只是一个标识,表示当前申请的变量是一个指针,而这里 的*号我们给它一个新的名字,叫指针的间接访问符。听起来有些别扭,简单地理解就是,在一个指针变量前加上*号则可以访问该指针所所表示内存的实际值。
这里可以教初学者一个小技巧,当利理解一个和指针有关的语句是,“指针”和“地址”这两个词可以互换,初学者姑且可以认为“指针”就是“地址”,“地址”就是“指针”,这样理解不确切,但却很实用。
好了,下面让我们看一个更复杂的例子:
int *p1 = &total_count;
int count2 = *(*(&p1));
请读者在10秒钟之内告诉我count2的值是多少。哈哈,好吧,让我们来一点点分析。
从括号最里面开始:
p1是一个指针,他的值是total_count的地址;
&p1是一个地址,即指针p1的地址,我们用刚才的小技巧来看看,即地址p1地址,即指针p1的指针。
*(&p1)是一个值,这个值是&p1表示的地址所在内存的值实际上就是p1的值,&对p1进行了一次引用操作,*对其再解引用,实际上没有变化。此时*(&p1)的值为total_count的地址。
*(*(&p1))为total_count的值,也就是542.
到此,该表达式的分析结束,不过在实际编程中,没有人会用如此复杂的表达式进行编码。
4.指针的指针
指针的指针,即地址的地址。第一个地址是狭义上的地址,该地址实际上已经被“值”化,第二个地址是我们传统意义上的地址。这句话理解起来有些困难,我们先来看看下面的表达式;
int *p1 = &total_count;
int **p2 = &p1;
第一个表达式的意义我们已经清楚了,我们申请了一个指针变量,或者说是地址变量,它保存的是total_count的地址。
第二个表达式我们来一步步分析,p1是一个指针,即一个地址,&为地址符,加起来就是地址的地址,或者说指针的指针。而int **p2 我们可以这样理解 int *X,我们定义了一个指针变量X ,X为*p2,即X也是一个指针变量,加起来的意思就是我们申请了一个p2指针变量,这个指针变量指向的也是一个指针变量。
同理: int ***p3 = &p2;
int ****p4 = &p3;
......都是成立的表达式,不过在实际编码中,遇到的最多情况是指针和指针的指针这两种情况。
深入浅出C指针的更多相关文章
- 【C++深入浅出】智能指针之auto_ptr学习
起: C++98标准加入auto_ptr,即智能指针,C++11加入shared_ptr和weak_ptr两种智能指针,先从auto_ptr的定义学习一下auto_ptr的用法. template& ...
- 深入浅出c++之---this指针
前言:C语言中的数组指针和指针数组 数组指针,是指向数组的指针的缩写:指针数组,是存放指针的数组的缩写.其实很多时候,往往因为简写和缩写带给我们很多困惑.我曾想过不用简称去学习,但在很多时候,我们查询 ...
- 深入浅出剖析C语言函数指针与回调函数(一)【转】
本文转载自:http://blog.csdn.net/morixinguan/article/details/65494239 关于静态库和动态库的使用和制作方法. http://blog.csdn. ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- VC++动态链接库(DLL)编程深入浅出(zz)
VC++动态链接库(DLL)编程深入浅出(zz) 1.概论 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用 ...
- 索引深入浅出(4/10):非聚集索引的B树结构在聚集表
一个表只能有一个聚集索引,数据行以此聚集索引的顺序进行存储,一个表却能有多个非聚集索引.我们已经讨论了聚集索引的结构,这篇我们会看下非聚集索引结构. 非聚集索引的逻辑呈现 简单来说,非聚集索引是表的子 ...
- 索引深入浅出(5/10):非聚集索引的B树结构在堆表
在“索引深入浅出:非聚集索引的B树结构在聚集表”里,我们讨论了在聚集表上的非聚集索引,这篇文章我们讨论下在堆表上的非聚集索引. 非聚集索引可以在聚集表或堆表上创建.当我们在聚集表上创建非聚集索引时,聚 ...
- 《深入浅出Windows Phone 8.1 应用开发》基于Runtime框架全新升级版
<深入浅出Windows Phone 8.1 应用开发>使用WP8.1 Runtime框架最新的API重写了上一本<深入浅出Windows Phone 8应用开发>大部分的的内 ...
- [深入浅出WP8.1(Runtime)]生成图片和存储生成的图片文件
7.2.3 使用RenderTargetBitmap类生成图片 RenderTargetBitmap类可以将可视化对象转换为位图,也就是说它可以将任意的UIElement以位图的形式呈现.那么我们在实 ...
随机推荐
- 委托delegate 泛型委托action<> 返回值泛型委托Func<> 匿名方法 lambda表达式 的理解
1.使用简单委托 namespace 简单委托 { class Program { //委托方法签名 delegate void MyBookDel(int a); //定义委托 static MyB ...
- linux_api之进程环境(二)
本篇索引: 1.引言 2.终端登录 3.进程组 4.会话期 1.引言 通过上一篇的学习,我们已经知道了如何控制一个进程,fork函数从父进程中复制出子进程,我们可以通过exec函数让子进程运行新的 ...
- java中HashMap的keySet()和values()
我们通常说,keySet()返回所有的键,values()返回所有的值,其实是不太对的,因为无论是keySet()和values(),其实都没有实质的内容,且容我慢慢说来. 他们前者返回了一个Set, ...
- pat09-散列3. Hashing - Hard Version (30)
09-散列3. Hashing - Hard Version (30) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 HE, Qin ...
- [Matlab] figure
figure只能设置序号 不能设置title 而stem和plot可以设置title
- Java学习第十八天
1:Map(掌握) (1)将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值. (2)Map和Collection的区别? A:Map 存储的是键值对形式的元素,键唯一,值可以重 ...
- oracle dblink简介
database link概述 database link是定义一个数据库到另一个数据库的路径的对象,database link允许你查询远程表及执行远程程序.在任何分布式环境里,database都是 ...
- 深入理解JavaScript系列(9):根本没有“JSON对象”这回事!
前言 写这篇文章的目的是经常看到开发人员说:把字符串转化为JSON对象,把JSON对象转化成字符串等类似的话题,所以把之前收藏的一篇老外的文章整理翻译了一下,供大家讨论,如有错误,请大家指出,多谢. ...
- C# 使用cookie实现登录
首先,我们需要做的是什么? 我们成功登录之后,跳转到主界面,然后主界面的登录按钮变成头像啥的.下一次打开网页就要判断有没有登录过,有cookie就不需要登录,直接显示头像 1.成功登录后,客户端请求服 ...
- 循环结构 while
while 循环语句可以根据某些条件重复执行一条t-sql 语句或一个语句块 语法: while (条件) begin 语句或语句块 end 程序调试 alt+f5 启动调试 f9 切换断点 f10 ...