[讨论] 2015-8-13日 主题:关于指针和堆栈

【码神】[长春]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 编码时,实际上经过了一个查表转换的过程,通用性也稍差一点。
分享到: