关于C语言指针的一些新认识(1)
前言
指针是C语言的精华,但我对它一直有种敬而远之的感觉,因为一个不小心就可能让你的程序陷入莫名其妙的麻烦之中。所以,在处理字符串时,我总是能用数组就尽量用数组。但是,数组有个缺点:不能动态地分配内存的大小。
终于下定决定克服这个心里障碍,探一探指针的究竟,却发现了很多自己之前没有认识到的,甚至是认识有错误的地方。这里,把这两天学到的新知识做了一个简单的整理,并记录下来。因为水平有限,若有疏漏之处,还希望大家及时指正,以免祸害他人。
基础知识 & Questions
指针
约等于是一个地址(这个地址是某个对象的存储空间的首地址),它是一个以当前系统寻址范围为取值范围的无符号整数(unsigned int),在32位系统中长度为32bits。
指针变量
首先它是一个变量(它存储的内容是可以改变的),其次这个变量是用来存放某个对象存储空间的首地址(指针)的。它的类型便是它指向的对象的类型,并且允许强制类型转换。它指向的对象可以是:空(NULL)、通用类型(void)、简单类型(int,char…)、结构体(struct)、类(Class)甚至是函数(function)!
其实对于指针和指针变量这俩名词我总是用着用着就搞混了,刚刚捋顺清楚,没一下午的功夫就又糊涂了…不过我觉得心里明白其中的含义就好,会用就好,不必在名词使用的正误上追究太多。
不过,要是非得打个比方来说,感觉指针就像是一个整数,指针变量则是一个整型的变量,指针只不过是一个指针变量某一时刻的取值。
指针符号『*』和地址符号『&』
& : 取对象的地址(即:取得该对象存储空间对于的首地址)
* : 取对应首地址存放的对象的内容(即:值)
相对地址 内容 0xf1110000 00 0xf1110002 00 0xf1110003 00 0xf1110004 15 0xf1110005 … ……….
0xf1110100 … 假设粗体字中表示整型变量 num 的存储空间,那么num的值是:21(=16 + 5)
1: int * pNum = #2: * pNum = 22;在上述代码中,&取出的是num的首地址:0xf1110000,而 * 取出的却是num对应的整个内存。
这里我想问一个问题,也是一直困扰我很久的一个问题:(Q1)指针pNum只占用了4B的存储空间,并没有额外的存储空间存储它指向对象的类型信息,它是怎么知道num占用了4B的存储空间的呢?这个问题我们稍后解答。
注:本例中采用小端序(Little End)。
声明:
1: //pattern: type * pv1, * pv2, v3, ... ;
2: //attention: v3 is not a pointer!
3: char * ptr;
初始化:
1: //Initialization when declaration
2: char * pointer = NULL;
3: char Msg[] = "I Love U";
4: char * pMsg = Msg; //init by array
5: char * ptr = "I'm not a good boy"; //init by C string
6: int age = 21;
7: int * pAge = &age; //init by refference
- Q2:code 4中看不出数组和指针有什么区别,是不是数组跟指针是一样的呢?
- Q3:code 5中是否可以通过ptr[0] = 'l' ;的方式修改字符串呢?
赋值:
1: //assignment
2: struct Woman{
3: int age;
4: int weight;
5: int height;
6: };
7: struct Woman laura, * pWoman;
8: int * pAge;
9: pWoman = &laura;
10: pAge = &pWoman->age;
用指针访问结构体或者类的成员时,一定要使用:“->”,用“.”是不合法的。
探索
好吧,说到这我又把C语言指针的最基本的用法回顾了一遍,同时,也提出了3个简单的问题。接下来,我就本着探索的精神来解决了一下这里的疑惑,另外还会在下一篇博客中陆续提出几个新的问题。
探索工具
代码面前没有秘密可言,要窥探程序的运行机制,当然非汇编代码莫属了。下面我们将从汇编的角度来解决上面遇到的3个问题。
首先,我们要知道如何产生高级语言对应的汇编代码。GNU gcc的编译指令是:gcc –masm=intel –S source.c –o source.s,这里 -masm是产生intel风格的汇编代码,如果要是不加这句话,则产生AT&T风格的汇编代码。另外,Window平台下的编译器也同样提供了产生中间汇编代码的编译选项。(参考:AT&T与Intel汇编语言的比较)
另外,补充一点儿汇编的知识:(更多的汇编知识请点击这里)
ebp | 基址寄存器 base pointer R |
esp | 堆栈寄存器 stack pointer R |
eax, ebx, ecx, edx | 数据寄存器 |
esi | source index register |
edi | destination index register |
Q1
我写了一个简单的测试程序:
1: #include <stdio.h>
2:
3: int main()
4: {
5: int num = 21;
6: int * pNum = #
7: printf("num: %d pNum: %d\n", num, *pNum);
8:
9: * pNum = 22;
10: printf("num: %d pNum: %d\n", num, *pNum);
11:
12: return 0;
13: }
程序的输出为:
输出它的汇编代码为:(这里我只保留了code 9对应的汇编代码)
1: mov eax, DWORD PTR [esp+28]
2: mov DWORD PTR [eax], 22
看到这,我突然明白:原来指针的类型信息仅仅是保存在了我们自己写的源代码里,在进行编译时,编译器已经将类型信息转换成对应的操作了。比如:“* pNum = 22;”这句代码,因为pNum是int型的指针变量,
所以先:mov eax, DWORD PTR [esp+28],从堆栈中取出pNum中存储的地址;
然后再:mov DWORD PTR [eax], 22,将立即数22赋值给eax寄存器存储的地址后连续的双字内存空间,即将22复制到num的内存空间。
如果num是short int型,pNum是short int型的指针,那么这句就应该会被编译成:
mov WORD PTR [eax], 22,即将22复制到一个单字内存空间。
Q2
很长一段时间以来,我一直认为数组和指针是一回事儿。确实也是因为他们太像了,很多时候可以混着用。但是看看下面这段程序输出的结果,我便陷入了迷惑之中…
1: #include <;stdio.h>
2:
3: int main()
4: {
5: char Msg[] = "I Love U";
6: char * ptr = "I'm not a good boy";
7: printf("%s\n%s\n", Msg, ptr);
8: printf("%d %d\n", sizeof(Msg), sizeof(ptr));
9: return 0;
10: }
输出结果:
Msg中共有8个字符,加上字符串尾部的 '\0'刚刚好9个字符,这没问题。但是为什么ptr指向的字符串就值输出了4呢?这输出的不可能是它指向字符串的长度,而很可能是它自身占用的内存大小。
下面来看汇编代码,希望能从这里找到答案:
.file "test.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC1:
.ascii "I'm not a good boy\0"
LC2:
.ascii "%s\12%s\12\0"
LC3:
.ascii "%d %d\12\0"
LC0:
.ascii "I Love U\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB6:
; 汇编和连接信息
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset
.cfi_offset , -
movl %esp, %ebp
.cfi_def_cfa_register
pushl %edi
pushl %esi
pushl %ebx
andl $-, %esp
subl $, %esp
.cfi_offset , -
.cfi_offset , -
.cfi_offset , -
call ___main
; 将字符串常量复制到Msg的存储空间中
leal (%esp), %edx ; &Msg -> edx,将Msg的地址复制给edx
movl $LC0, %ebx ; &LC0 -> ebx,将标号LC0的地址复制给ebx
movl $, %eax ; $9 -> eax,将立即数9传给eax
movl %edx, %edi ; 将Msg的地址传给目的寄存器
movl %ebx, %esi ; 将LC0的地址传给源索引寄存器(source index)
movl %eax, %ecx ; 将eax中的9传给ecx
rep movsb ; 开始在esi和edi之间传输数据,每传一个eci减一,直到eci为0
; 将Msg和ptr对应字符串的地址当做参数传给printf(),并打印
movl $LC1, (%esp) ; Msg占据9B,从28开始是ptr的地址
movl (%esp), %eax ; 将LC1的地址赋给eax
movl %eax, (%esp) ; 将eax中存放的地址(LC1)赋给8(偏移地址)
leal (%esp), %eax ; 将Msg的地址赋给eax
movl %eax, (%esp) ; 将eax中存放的地址(Msg)赋给4(偏移地址)
movl $LC2, (%esp)
call _printf
; 将sizeof()计算出的结果当立即数传给printf()的参数,并打印
movl $, (%esp)
movl $, (%esp)
movl $LC3, (%esp)
call _printf
; 程序退出
movl $, %eax
leal -(%ebp), %esp
popl %ebx
.cfi_restore
popl %esi
.cfi_restore
popl %edi
.cfi_restore
popl %ebp
.cfi_def_cfa ,
.cfi_restore
ret
.cfi_endproc
LFE6:
.def _printf; .scl 2; .type 32; .endef
从上面的汇编代码中可以看出,程序将 "I Love U\0" 这9个字符从 .section .rdata (只读数据段)中拷贝到了运行时的堆栈中,并且可以断定 28(%esp)既是字符数组Msg的首地址。但可怜的字符串”I'm not a good boy\0“就没那么幸运了,它之后依然呆在数据段中(常量,不可被修改)。
看到这,我想我明白了数组和指针最本质的差别。在编译器进行编译时,并没有刻意地开辟4B的内存保存数组的初始地址,而是将数组名直接翻译成了数组对应的存储空间的首地址,它是一个彻彻底底的地址常量!就像一个代表地址的立即数一样!上例中,第34行汇编代码:leal 9(%esp), %edx, 在堆栈中,9~27这9B的内存都是数组的内存空间,所以在对数组初始化时,直接使用了其首地址。注意:这里用的是leal(将源操作数的地址赋给目的操作数),而不是movl(将源操作数的内容赋给目的操作数)。
但是指针变量ptr却拥有自己的在堆栈中拥有属于自己的内存空间:28~31。
从之前对指针的定义来看,数组名也可以算作是指针常量(它本身标记的就是一个地址而已),记住:数组名不是指针变量,它只能用作右值!
Q3
解决了Q2,Q3的答案也便一目了然了。因为char * ptr = "I'm not a good boy";在被编译时,编译器将字符串当做常量存放在了data段中(只读),而指针变量ptr只是在自己的存储空间中存放了这个字符串常量的首地址。常量当然不允许被修改啦~
未完,待续…
啰啰嗦嗦的写了这么多,这篇博客就先写到这里吧,希望这种分析方式可以为大家解决问题提供一个新的视角,下一篇我将写一写在使用指针过程中遇到的细节问题
关于C语言指针的一些新认识(1)的更多相关文章
- 关于C语言指针的一些新认识(2)
在使用C语言编程的过程中,遇到了很多关于指针使用的小问题,这里总结一下就当做是编程的小技巧啦 Q1. 如何用printf( )输出指针 这个问题相当于如何用printf( )输出地址,答案是:用"%p ...
- C语言指针学习
C语言学过好久了,对于其中的指针却没有非常明确的认识,趁着有机会来好好学习一下,总结一下学过的知识,知识来自C语言指针详解一文 一:指针的概念 指针是一个特殊的变量,里面存储的数值是内存里的一个地址. ...
- C语言指针总结
C语言中的精华是什么,答曰指针,这也是C语言中唯一的难点. C是对底层操作非常方便的语言,而底层操作中用到最多的就是指针,以后从事嵌入式开发的朋友们,指针将陪伴我们终身. 本文将从八个常见的方面来透视 ...
- C语言指针操作
欢迎访问我的新博客:http://www.milkcu.com/blog/ 原文地址:http://www.milkcu.com/blog/archives/pointer-manipulation. ...
- C语言指针的陷阱
C语言指针的陷阱 分类: C/Cpp 转自:http://blog.csdn.net/porscheyin/article/details/3461670 “C语言诡异离奇,陷阱重重,却获得了巨大 ...
- 2-Linux C语言指针与内存-学习笔记
Linux C语言指针与内存 前面我们对于: c语言的基本用法 makeFile文件的使用 main函数的详解 标准输入输出流以及错误流管道 工具与原理 指针与内存都是c语言中的要点与难点 指针 数组 ...
- C语言指针与数组
C语言指针与数组 数组的下标应该从0还是1开始? 我提议的妥协方案是0.5,可惜他们未予认真考虑便一口回绝 -- Stan Kelly-Bootle 1. 数组并非指针 为什么很多人会认为指 ...
- C语言指针学习总结
上学的时候学习C语言,最烦的就是里面指针,可是指针也恰恰是C语言的灵魂. 最近在重温数据结构的内容,因为大多数据结构的教材都是用C语言描述的,而数据结构中也大量的用到了指针的内容,所以我就在这篇笔记中 ...
- 深入理解C语言 - 指针使用的常见错误
在C语言中,指针的重要性不言而喻,但在很多时候指针又被认为是一把双刃剑.一方面,指针是构建数据结构和操作内存的精确而高效的工具.另一方面,它们又很容易误用,从而产生不可预知的软件bug.下面总结一下指 ...
随机推荐
- 读《实战GUI自动化测试》之:第三步,如何提高测试结果分析的效率
转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting3/ 所谓自动化测试,就是“自动化”+“测试”.自动化本身显然不 ...
- Android屏幕尺寸与度量单位(px,dp,sp)简介
MarkdownPad Document *:first-child { margin-top: 0 !important; } body>*:last-child { margin-botto ...
- Linux 学习(二)
Linux相关命令 命令 说明 startx 当前用户界面切换至图形界面 init5 切换至另一用户的图形化界面 init3 从图形界面切换回文本界面 pwd 显示当前用户路径 logout 注销 s ...
- Probabilistic locking in SQLite
In SQLite, a reader/writer lock mechanism is required to control the multi-process concurrent access ...
- handlesocket.md
[介绍](http://www.uml.org.cn/sjjm/201211093.asp ) * 查看启动参数 `service mariadb status > st.txt` ...
- 洛谷——P2659 美丽的序列
P2659 美丽的序列 单调栈维护区间最小值,单调递增栈维护区间最小值, 考虑当前数对答案的贡献,不断加入数,如果加入的数$>$栈顶,说明栈顶的元素对当前数所在区间是有贡献的,同时加入当前的数. ...
- Linux iostat-监视系统输入输出设备和CPU的使用情况
推荐:更多linux 性能监测与优化 关注:linux命令大全 iostat命令被用于监视系统输入输出设备和CPU的使用情况.它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况.同vmsta ...
- JDBC在Java Web中的应用
JDBC在Java Web中的应用 制作人:全心全意 在Java Web开发中,JDBC的应用十分广泛.通常情况下,Web程序操作数据库都是通过JDBC实现,即使目前数据库方面的开源框架层出不穷,但其 ...
- 窥探原理:实现一个简单的前端代码打包器 Roid
roid roid 是一个极其简单的打包软件,使用 node.js 开发而成,看完本文,你可以实现一个非常简单的,但是又有实际用途的前端代码打包工具. 如果不想看教程,直接看代码的(全部注释):点击地 ...
- python字符串方法replace()简介
今天写replace方法的时候的代码如下: message = "I really like dogs" message.replace('dog','cat') print(me ...