关于指针和堆栈

转载:http://blog.qdac.cc/?p=2804

【码神】[长春]swish(109867294) 21:17:40

这块要明白一个东西,我们程序所能操作的数据,从CPU的角度来看,只是在寄存器中的那几个东西。剩下的内存中的东西,磁盘上的东西,实际上对于CPU核心来说,都是外部的东西。

【码神】[长春]swish(109867294) 21:19:07
就象对一个人来说,你大脑所能操纵的是你自己的身体和四肢,通过你的身躯和语言去操纵身边的人和物

【码帝】【宁波】空(9534557) 21:19:09
变量是地址的别称啊

【码神】[长春]swish(109867294) 21:19:32
对的,对于CPU里说,内存也不是它能直接操纵的

【码神】[长春]swish(109867294) 21:20:24
所以,这就存在一个问题,我们要操纵一块内存怎么办?

【码神】[长春]swish(109867294) 21:21:54
我们要使唤一个人,首先就要知道这个人在那儿,你连这个人都找不到,你想操作他,那就是属于白日做梦了。同理你操纵一块内存时,当然也要知道这块内存在那儿,这样子才有机会去操纵它。

【码神】[深圳]音儿小白(2514718952) 21:22:08
@[长春]swish 所以我们看反汇编, 有一堆的 mov eax, [xxxx]

【码神】[深圳]音儿小白(2514718952) 21:22:44
内存地址都必须放到寄存器后,才能进行运算

【码神】[长春]swish(109867294) 21:23:33
我们用来标明内存块的位置,就是地址。用来存放这个地址的变量,在编程语言里就叫做指针。

【码神】[长春]swish(109867294) 21:24:17
这样子说大家应该明白了,指针就是在编程语言中用于存贮一块内存的地址的变量。

【码神】[长春]swish(109867294) 21:25:11
那么问题来了,这个变量本身它也需要占用内存,所以,它也会有自己的地址。这个地址它又存在那里?

【码神】[长春]swish(109867294) 21:27:15
我们声明一个指针类型的变量,99%的情况是存在栈里,而剩下的1%是存在堆

【码神】[长春]swish(109867294) 21:27:52
比如:
var
p:Pointer;

【码神】[长春]swish(109867294) 21:28:15

这个p本身的地址是存在栈里的,而不是在堆里。

【码神】[长春]swish(109867294) 21:29:26
但是好玩的是指针里存的地址,99%却是堆里的

【码神】[长春]swish(109867294) 21:31:16
咱们以一个简单的例子来说明指针这个事:
var
S:UnicodeString;
begin
SetLength(S,0);
SetLength(S,100);
S:=’abc‘;
S:=’def‘;
end;

【码神】[长春]swish(109867294) 21:31:47
这个例子很简单,UnicodeString 是 XE 以后默认的 String 对应的类型

【码神】[长春]swish(109867294) 21:32:24
这里注意一个问题,String 并不是一个简单类型

【码妖】[青岛] 阿木(345148965) 21:33:05
string是一个自己生命周期的特殊对象

【码神】[长春]swish(109867294) 21:33:11
为了了解 String 这个类型的本质,我们将其对应的 C++ 定义挪过来

【码神】[长春]swish(109867294) 21:34:34
我们捡核心的来
【码神】[长春]swish(109867294) 21:34:35
#pragma pack(push,1)
struct StrRec {
#ifdef _WIN64
int _Padding;
#endif /* _WIN64 */
unsigned short codePage;
unsigned short elemSize;
int refCnt;
int length;
};
#pragma pack(pop)

const StrRec& GetRec() const;
StrRec& GetRec();

private:
WideChar *Data;
【码神】[长春]swish(109867294) 21:35:12
这是它的定义,可以看到它是一个 StrRec 记录+一个数据指针的形式

【码神】[长春]swish(109867294) 21:35:27
现在回到例子:
var
S:UnicodeString;

【码神】[长春]swish(109867294) 21:35:39
我们讨论下这发生了什么?

【码神】[深圳]音儿小白(2514718952) 21:36:47
在栈上建立了一个StrRec

【码神】[长春]swish(109867294) 21:36:52
首先,由于在栈上分配内存,所以 S 的地址就是当前栈顶的地址,然后将这个地址就会当成是变量S的地址,我们 @S 时得到的就是这个地址。

【码神】[长春]swish(109867294) 21:37:19
音儿落下了Data 成员

【码神】[长春]swish(109867294) 21:37:58
是 ESP 的值增加了至少 StrRec+Data 这个值

【码神】[深圳]音儿小白(2514718952) 21:38:08

【码神】[长春]swish(109867294) 21:38:46
@S 这个得到的值如果我们赋给一个变量,这个变量就是一个指针类型的变量

【码神】[长春]swish(109867294) 21:39:09
接着再往下走,看看 SetLength(S,0) 会发生什么?

【码神】[长春]swish(109867294) 21:39:43
实际上,UnicodeString 做为 Delphi 的内部类型,享受了许多高级待遇。

【码神】[长春]swish(109867294) 21:40:07
1、它不象普通的记录,它会被初始化
2、它的值会自动清理;
3、它基于引用计数来管理 Data 成员的释放;

【码神】[长春]swish(109867294) 21:41:58
现在实际上在 SetLength 之前,它已经象音儿说的,初始化原来的ESP地址到新的 ESP 地址之间的栈上的内存块的内容为0。

【码神】[长春]swish(109867294) 21:42:53
然后调用 SetLength 时,它会比较StrRec.Length与你需求的Length的长度,如果一样就啥也不干

【码神】[长春]swish(109867294) 21:45:13
SetLength(S,100)
这个做的事情就是比较长度,然后用 GetMem 分配100字节的内存,并把它的地址保存到 Data 成员上,这个Data 就是指向这块新内存的指针了,原来是指向空地址的,现在有新地址了。

【码神】[长春]swish(109867294) 21:46:41
我们跑到下一步:
S:=’abc‘;
这个发生什么?

【码神】[长春]swish(109867294) 21:49:22
abc 首先是一个字符串常量,这个东西会首先会在内存的某个不起眼的角落里呆着

【码神】[长春]swish(109867294) 21:49:57
这个实际上发生了好几件事

【码神】[长春]swish(109867294) 21:53:23
第一句是将 S 的地址存到eax里,第二步是将’abc’这个常量在内存中的地址给放到edx里

然后调用UStrLAsg 这个函数来完成实际的赋值过程

【码神】[长春]swish(109867294) 21:54:12
procedure _UStrLAsg(var Dest: UnicodeString; const Source: UnicodeString); // locals
var
P: Pointer;
begin
if Pointer(Source) <> nil then
_UStrAddRef(Pointer(Source));
P := Pointer(Dest);
Pointer(Dest) := Pointer(Source);
_UStrClr(P);
end;

【码神】[长春]swish(109867294) 21:54:29
好了,这个函数的源码咱们是可以看到的

【码神】[长春]swish(109867294) 21:56:23

翻译上面的代码就是:如果源不为空,则将源的引用计数+1,然后将源的地址保存到目标地址上去

【码神】[长春]swish(109867294) 21:57:36
但你注意到它的参数了吧,’abc’已经是一个UnicodeString了,所以实际上你定义的字符串常量已经在启动时变成UnicodeString了

【码神】[长春]swish(109867294) 21:57:50

注意一点,WideString 没有引用计数

【码神】[长春]swish(109867294) 21:59:40
所以实际上做的事情咱们完整说下来是:
1、将 abc 通过空所说的Move方式保存起来,此时其引用计数为1;
2、将这个字符串中数据的地址赋给目标变量s,同时增加引用计数;
3、清理掉原来的内存块;

【码神】[长春]swish(109867294) 22:01:01
好了,现在咱们再扯回指针。

【码神】[长春]swish(109867294) 22:01:41
看看都发生了啥变化,都有什么东西游来游去。

【码神】[长春]swish(109867294) 22:02:11
在整个过程中,S 这个变量所在的栈的位置是不变的,所以 @S 的值就是固定的

【码神】[长春]swish(109867294) 22:02:43
无论你对 S 赋值还是删除元素,@S 永远和你函数刚进来时一样。

【码神】[长春]swish(109867294) 22:04:02
同样,由于@S 这个地址到 @S+SizeOf(S) 这段地址中各个元素的地址也是不变的,也就是说,Data 这个指针的自身的地址也是不变的,变的只是 Data 这个地址存的内容

【码神】[长春]swish(109867294) 22:07:26
咱们简化下,假设 S 在栈上的地址是0,Data 本身的地址咱不算了,随便指定一个,假设是16,初始化完成后,这个内存块是这样子的:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
此时,Data 的内容从16-19这四个字节的内容是 0 0 0 0
当我们用SetLength或者赋值等等方式,Data 保存了新的地址,假设这个地址是 1,上面的内容就会变为
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0

【码神】[长春]swish(109867294) 22:07:55
之所以1在前面是因Windows 是LE 编码,小头在前,这个简单说一下

【码神】[长春]swish(109867294) 22:08:28
此时,@Data 的地址不变,Data 的内容变了,变成了1

【码神】[长春]swish(109867294) 22:09:51
现在继续,既然指针指向的是一个地址,那么,根据一个萝卜一个坑的原则,通过指针,你访问数据时,永远只能直接访问的是当前元素

【码神】[长春]swish(109867294) 22:10:36
但拨萝卜可以一个个的拨,所以,地址可以一个个的往下跳,也可以往前跳

【码神】[长春]swish(109867294) 22:12:56
说到这里,我们就要了解跳坑的规则

【码妖】[青岛] 阿木(345148965) 22:13:00
那data的内容是在堆还是栈?

【码帝】【宁波】空(9534557) 22:13:11
@[青岛] 阿木 堆

【码神】[长春]swish(109867294) 22:13:55
指针类型的跳坑原则就是只能整数倍跳,不能半个半个跳

【码妖】[浙江]空号WaxBerray(286195153) 22:14:30
堆最大能分配多大?

【码神】[长春]swish(109867294) 22:15:13
如果不考虑32位和64位程序的区分,堆能分配的大小受物理可用内存限制

【码神】[长春]swish(109867294) 22:15:34
这个咱们以前讨论过

【码妖】[浙江]空号WaxBerray(286195153) 22:15:34
那栈呢?

【码神】[长春]swish(109867294) 22:15:45
栈默认是1MB

【码神】[长春]swish(109867294) 22:15:57
可以在项目设置里修改

【码神】[长春]swish(109867294) 22:16:14
咱先继续,一会再讨论堆和栈的问题

【码神】[长春]swish(109867294) 22:17:16
比如一个32整数类型的指针 P,当你加1时,它跳的是4个字节,当你减1时,它跳的是-4个字节,而不会是其它值

【码神】[长春]swish(109867294) 22:17:31
同样,一个PByte类型的指针,就是一个一个字节的跳了

【码神】[长春]swish(109867294) 22:18:18
它等价于:
P:=Integer(P)+SizeOf(TypeOfP);

【码神】[长春]swish(109867294) 22:20:26
正是因为指针类型跳的长度是固定的,而指针指向地址对应的内存块是连续的(99.9%的情况),所以你可以一次跳多个坑,当然跳多了越过界了,掉下去就是你自己的事了。

【码神】[长春]swish(109867294) 22:20:36
这也是指针使用的风险所在

【码妖】[浙江]空号WaxBerray(286195153) 22:21:39
那怎么避免这个风险呢?

【码神】[长春]swish(109867294) 22:21:50
因为指针可以随便的跳来跳去,在 C++ 中,将它归为了随机迭代器。关于随机迭代器的概念,咱也不拓展了,知道这么个事就好。

【码神】[长春]swish(109867294) 22:22:11
将来有兴趣学C++ 的STL 的时候,会了解到这些。

【码神】[深圳]音儿小白(2514718952) 22:22:26
判断是不是超过上限

【码神】[深圳]音儿小白(2514718952) 22:22:31
P + Len

【码神】[长春]swish(109867294) 22:22:39
这个风险就是靠程序员来控制的,编译器无法替你控制

【码神】[长春]swish(109867294) 22:23:54
越界的话,最典型的是AV错误

【码神】[长春]swish(109867294) 22:24:39
但注意不是100%出,不能说越界就一定出

【码神】[长春]swish(109867294) 22:24:59
现在咱们的指针的说明基本也差不多了

【码神】[长春]swish(109867294) 22:25:10
回来再把堆和栈是怎么回事简单说下

【码神】[长春]swish(109867294) 22:26:23
栈上的内存是预分配的,不管你用不用,它都在那儿占着

【码妖】[浙江]空号WaxBerray(286195153) 22:26:58
为什么要这么设计栈?

【码神】[长春]swish(109867294) 22:27:09
堆上的内存是按需分配,只有用到时,你才会去申请,操作系统给你分配,用完,你需要负责将内存还回去的

【码神】[长春]swish(109867294) 22:27:44
为什么这么设计又是一个长篇大论了,简而言之的话,是为了方便和性能

【码神】[长春]swish(109867294) 22:28:57
因为是连续预分配的,所以你声明一个新的变量时,几乎是没有什么额外的开销,所唯一要做的,就是调整下ESP的值,内存分配就算完事了,释放的时候,再调整下ESP的值,内存释放也就算完事了,当然这是说简单类型。

【码神】[长春]swish(109867294) 22:29:36
而堆的话,内存分配因为是随用随申请,所以它的大小理论上仅受物理内存中的连续内存块大小限制。

【码神】[长春]swish(109867294) 22:30:09
堆上的内存申请理论上的上限是你物理内存中连续的最大块内存的大小。

【码帝】【宁波】空(9534557) 22:30:36
@[深圳]音儿小白 这个就是为什么这个月上了XE7之后我苦难的地方.纠结很久.

【码神】[长春]swish(109867294) 22:30:54
由于堆上的内存在不停的重复的申请和释放,也就引申出了内存碎片的概念,但不在咱们讨论的范围了。

【码神】[长春]swish(109867294) 22:31:10
先说这么些吧,有什么问题咱们再讨论

问题:UnicodeString 的 Length 是否就是字符数?

【码神】[长春]swish(109867294) 22:34:12
UnicodeString 的字符数和Length并不一定相等

【码神】[深圳]音儿小白(2514718952) 22:34:19
那是什么?

【码帝】【宁波】空(9534557) 22:34:40
不是的. 字符计数不是这样的. 这个我也不确定.因为UNICODE的概念 计数是字数,1个字或许会有2个WIDERCHAR

【码妖】[浙江]空号WaxBerray(286195153) 22:34:46
一直以为是字符数量

【码神】[长春]swish(109867294) 22:35:06
Unicode 有所谓的扩展区的概念

【码神】[长春]swish(109867294) 22:35:15
扩展区的字符1个占4个字节

【码神】[深圳]音儿小白(2514718952) 22:35:20
唉,咱们中文,英文,数字都没错的, 就是字符数

【码神】[长春]swish(109867294) 22:35:24
$DB00~$DFFF

【码神】[深圳]音儿小白(2514718952) 22:35:40
好像别的语言会可能不一样

【码神】[长春]swish(109867294) 22:35:41
音儿,就是中文的时候出的问题

【码神】[长春]swish(109867294) 22:36:02
比如说这个字

【码神】[深圳]音儿小白(2514718952) 22:36:05
中文在unicodestring中, 一个字符会有不是两字节的情况?

【码神】[深圳]音儿小白(2514718952) 22:36:50
群主说说例外?

【码神】[长春]swish(109867294) 22:37:00
这个字估计很多人不认识:

【码神】[长春]swish(109867294) 22:46:23
还有一个提示吧,GBK 里,汉字也是2 或4个字节

【码神】[长春]swish(109867294) 22:46:38
GB2312算做 GBK 的子集

问题2:什么编码最省空间?

最省空间的是GBK编码,这个昨晚因为太晚没有做解释。许多人认为 UTF8 编码最省空间,但实际上不是的,UTF8 和 GBK 处理英文时都是 1 个字节,但处理中文时, GBK 编码要么是2个字节,要么是4个字节(极少数),而 UTF8 编码普遍是 3-4 个字节,综合下来,GBK 编码实际上占用的空间还是要略小。但 GBK 编码的劣势就是它是基于 GB2312 发展过来的,转换为 Unicode 编码时,实际上经过了一个查表转换的过程,通用性也稍差一点。

时间: 2024-07-31 10:25:26

关于指针和堆栈的相关文章

LeetCode OJ 143. Reorder List(两种方法,快慢指针,堆栈)

Given a singly linked list L: L0→L1→-→Ln-1→Ln,reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→- You must do this in-place without altering the nodes' values. For example,Given {1,2,3,4}, reorder it to {1,4,2,3}. Subscribe to see which companies asked this quest

深入探讨this指针

深入探讨this指针   为了写这篇文章,准备了好长时间,翻遍了箱底的书籍.可是如今还是不敢放开手来写,战战兢兢.不是操心自己写错,而是唯恐自己错误误导别人.同一时候也希望这篇文章能给你一点收获.既然是深入探讨this指针,所以建议刚開始学习的人,最好具有一定编译基础,调试基础.假设大家觉得这片文章有不满的地方,就给我发信批评一下,以便及时修正. 关于this指针的描写叙述我们一般从语言层次上讲: this指针作为一个隐含參数传递给非静态成员函数,用以指向该成员函数所属类所定义的对象.当不同的对

对堆栈的认识

什么是堆和栈,它们在哪儿? 问题描述 编程语言书籍中经常解释值类型被创建在栈上,引用类型被创建在堆上,但是并没有本质上解释这堆和栈是什么.我仅有高级语言编程经验,没有看过对此更清晰的解释.我的意思是我理解什么是栈,但是它们到底是什么,在哪儿呢(站在实际的计算机物理内存的角度上看)? 1.在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗? 2.它们的作用范围是什么? 3.它们的大小由什么决定? 4.哪个更快? 答案一 栈是为执行线程留出的内存空间.当函数被调用的时候,栈顶为局部

linux平台学x86汇编(七):堆栈的使用

[版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet,文章仅供学习交流,请勿用于商业用途] 堆栈是内存中用于存放数据的专门保留的区域,该区域的数据存放和删除方式比较特殊.一般内存区域数据元素按照连续的方式存放到数据段,在数据段中最低内存开始存放,然后向更高的内存位置依次存放.而堆栈保留在内存区域的末尾位置,并且在当数据存放在堆栈中时,它向下增长.程序运行时使用的任何命令行参数都被送入堆栈中,并且堆栈指针被设置为指向数据元素的底部. 当每个数据被添加到堆栈数据区域中时

C语言指针与数组易混淆知识点(一)

一指针与数组 二指针与函数 三指针数组数组指针指向指针的指针 四程序陷阱 一.指针与数组 指针:指针本身也是一个变量,它的内容是指向的内容的地址.指针同样有类型的区分,char 的指针只能指向char型数据,int 指针指向int型数据.但是指针所占内存单元的大小(即其内容)是跟操作系统的地址位数有关,比如32位地址的系统,那么指针所占的内存单元就是4个字节,16位就是2个字节,因此,指针的类型只是限定它所指向的变量的类型,其内容所占的单元大小是与操作系统的地址位数相关,与变量类型无关. 在32

堆栈在哪?

什么是堆和栈,它们在哪儿? 原文链接:http://www.kuqin.com/shuoit/20140815/341692.html 问题描述 编程语言书籍中经常解释值类型被创建在栈上,引用类型被创建在堆上,但是并没有本质上解释这堆和栈是什么.我仅有高级语言编程经验,没有看过对此更清晰的解释.我的意思是我理解什么是栈,但是它们到底是什么,在哪儿呢(站在实际的计算机物理内存的角度上看)? 在通常情况下由操作系统(OS)和语言的运行时(runtime)控制吗? 它们的作用范围是什么? 它们的大小由

Linux内核堆栈调用实现分析

1 内核线程 内核为每个线程分配8K的栈空间, 在每个堆栈的顶部放着struct thread_info 结构体,用来保存线程相关信息. 其中有几个重要变量: Preempt_count : 此变量分为四部分 0-7bit :当前进程是否能抢占的标志 8-15bit:softirq  使能标志 16-23bit :hardirq 使能标志 24bit:PREEMPT_ACTIVE标志位(原子上下文标志位??) Task:  进程相关的结构,包含更加丰富的信息 Cpu_context :cpu 寄

[Js-c++]c++中的指针、引用和数组名

指针和引用学了好久,还是偶尔会弄混淆,简单记录分析一下区别吧,有记起来新的区别就不断更新添加进去 1.指针可以多次重新分配,引用在无法重新分配,而且必须在声明的时候被初始化 2.指针可以指向空(即被定义为nullptr),但是引用必须指向一个东西 3.不存在"引用计算"这种东西(但是你用 &d+5 这种的确不会报错,也能算出来东西) 4.一个指针在堆栈上有自己的内存地址和大小(x86上有4个字节),而引用共享相同的内存地址(与原始变量),但是也占用堆栈上的一些空间.由于引用具有

指针沉思录

一.指针与内存 指针本身是内存的集合空间的索引: 内存的集合空间包含变量.命令等等: 定义一个指针,是内存空间(栈区)添加了元素: 给指针赋值,是将指针索引指向内存空间的具体地址: 指针使用,是将索引空间类型化,然后使用这个类型: 内存.地址与位置:指针与堆栈 指针本身的内存: 指针捕获(引用)的内存: 指针操作的内存: 二.指针与函数: 指针作为函数参量,指针本身是赋值传递: 只是将一个地址量传入函数: 三.指针与类型 目标地址的内存空间: 目标地址的内存空间的类型绑定的操作: 类型绑定的操作