| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 827 人关注过本帖
标题:[转载]关于C++中的常量与变量对C++中的变量与常量的理解
只看楼主 加入收藏
sevenchina
Rank: 1
等 级:新手上路
帖 子:18
专家分:0
注 册:2007-7-20
收藏
 问题点数:0 回复次数:5 
[转载]关于C++中的常量与变量对C++中的变量与常量的理解

[转载]关于C++中的常量与变量对C++中的变量与常量的理解

程序运行时,所用的数据首先要被放在内存。内存有两个最基本的属性,一个是它的地址(编号),另一个就是它存储的数据。就如一堆小箱子,编号用来区分到底是用到哪个箱子,数值就如箱子里面放着的东西。

数据放在内存,我们给它一个名字,名字只不过是个符号,符号本身都是没有什么意义的,符号代表的东西才有意义。取了名字之后可以根据名字来方便取回我的数就行了。名字到最后都会影射到地址。可以说,名字是只是给人看的,那个人最可能是你自己,所以为了自己, 也为了别人幸福,请花点心思去取个好名字。

数据放在内存之后,可以分为变量与常量,常者,不变也;量者,数值也。前面已经说了,内存有地址和存储的数据两个最基本的属性,因此常量与变量当然也有两个最基本的属性了,一是它分配到的内存地址,另一个就是地址所指内存里面的数值。常量与变量就是从地址所标内存里面的数值可否变化来区分的。程序执行时数值可变为变量,不可变为常量。常量的数值在程序执行之前已经确定下来的了。当然变量与常量还有其它的要素,比如名字和类型。名字最终会影射到地址,类型可以决定它们的大小和行为。类型有其自身的意义。

从内存里面的数值是否可变可以区分变量与常量。那么从分配的内存地址来看呢,又可以将变量与常量分为 静态(static),动态(dynamic),自动(auto)三种不相同的状态。C++中,关键字const含义为不变(常), 关键字static含义为固定(静)。照我理解(注意,是我自己的理解),常和静,变和动,都是一个意思,指变化和不变化,只不过常和变是对于内存存储的数而言,静和动是对于内存地址而言。

好啦。现在看看静态,动态,和自动到底是什么含义,怎么去区分? 当程序刚启动,系统会分配些栈啊,程序控制块啊,各个段啊之类,这可以算一个准备阶段,在这个准备阶段,程序代码还没有正式执行,常量与常量所需的内存已经分配好了,地址已经确定下来,这就为静态。当程序代码已经执行,才去分配内存,内存地址还没有确定,为动态或者自动。要是代码正在执行,需要内存分配,这一个分配行为由系统全部完成,不用你去操心,就为自动;要是需要程序员自己决定分配的时机,显式调用malloc,new 之类的分配函数,就为动态。概括的说,程序执行之前已经分配好内存,决定好内存地址,为静态; 程序执行之时再分配,分两种情况,1)系统自动完成,为自动类型。2)需要显式调用分配函数,自己决定时机,为动态。

将属性关联到数据的过程,叫做绑定(binding)。如果在程序执行之前,变量与常量的属性已经确定了,就叫静态绑定;要是要等到程序执行之时,属性才被关联,被确定,就叫动态绑定。看看C++的书籍,静态,动态,绑定的概念会老是出现的, 到这里应该有大概的理解了,主要是以属性确立的时机来区分。其实不单是变量,常量,有时候调用什么函数(也就是函数的地址)也需要在程序执行之时才能确定。这时候可以先将函数地址先存起来,或者做成一个表。可能有人问,怎么函数地址也可以放起来的吗? 当然了,函数也需要内存来放,既然放在内存,为了找回它,就一定要得回它的位置,也就地址。对于计算机来说,所有东西都是101010之类的数值,什么都已经没有区别了。既然如此,函数,代码,浮点,对象,跟int之类的整型没有什么两样。int可以先放着,函数为什么不可以?

===================================================================
C++中,const是个很重要的关键字,施加了一种约束。有约束其实不是件坏事情,无穷的权利意味着无穷的灾难。应用了const之后,就不可以改变变量的数值了,要是一不小心改变了编译器就会报错,你就容易找到错误的地方。不要害怕编译器报错,正如不要害怕朋友指出你的缺点,编译器是写程序人的朋友,编译时期找到的错误越多,隐藏着的错误就会越少。所以,只要你觉得有不变的地方,就用const修饰,用得越多越好。比如你想求圆的周长,需要用到Pi, Pi不会变的,就加const,const double Pi = 3.1415926; 比如你需要在函数中传引用,只读,不会变的,前面加const; 比如函数有个返回值,返回值是个引用,只读,不会变的,前面加const; 比如类中有个private数据,外界要以函数方式读取,不会变的,加const, 这个时候就是加在函数定义末尾,加在末尾只不过是个语法问题。其实语法问题不用太过注重,语法只不过是末节,记不住了,翻翻书就可以了,接触多了,自然记得,主要是一些概念难以理解。你想想,const加在前面修饰函数返回值,这时候const不放在末尾就没有什么地方放了。

不过const修饰指针就需要注意一下了。要是修饰的类型不是指针,比如int之类,const放在int之前和int之后是一样的,比如
const int a = 2;
int const a = 2;
有着同样的效果。我自己偏向于第一种写法,其实想想,第二种写法更为合理,表示修饰变量a本身,所以a的值不可变。
当类型为指针时,以星号*为界, const加在左右两边,有不同的意思。
1) const int* pa = &a; (可以写成 int const* pa = &a; 注意是以星号为界)
2) int* const pa = &a;
写法1)表示pa所指向的变量,也就是a的值不可变。写法2)表示pa的指向,也就是pa本身的值不可以变,不可以现在指向a, 跟着指向b.
=======================================
int a = 2;
int b = 3;
const int* pt = &a;
//*pa = 1; Error
pa = &b; OK

=====================================
int a = 2;
int b = 3;
int* const pt = &a;
*pa = 1; OK
//pa = &b; Error

==================================
前面已经说过了,const用来指示内存中的数值不会变。指针本质上是一个地址(编号), 这个编号也需要放在内存。所以pa这个变量放在内存,数值是一个地址。当const在*右边,const直接修饰pa, 表示pa的数值不会变,所以也就不可以改变指向。当const在*左边,就修饰指向的变量,故*pa的值不能变。要是const int* const pa = &a; a的值不可变,pa的指向也不可变。请仔细想一想。这个问题困惑了我很久的了。

搜索更多相关主题的帖子: 常量 变量 
2007-07-31 16:42
sevenchina
Rank: 1
等 级:新手上路
帖 子:18
专家分:0
注 册:2007-7-20
收藏
得分:0 

[转]比较基础的帖,希望对大家有帮助

前面已经说过了, const这个关键字可以施加一种约束, 使你不会错改内存的值。不过要是你真的想改变那个值,还是可以的, 麻烦一点而已。比如
================================
int a = 2;
const int* pa = &a;
//*pa = 100; Error
*const_cast<int*>(pa) = 100; //OK
===============================
*pa = 100 错误是因为我们用const施加一种约束。而const_cast是个转型符,用来将const约束取消掉,故*const_cast<int*>(pa) = 100可以通过。const_cast只能用在指针和引用类型。

为什么C++会设计这样一个转型符呢?是为了给程序员更大的自由。C++的一个设计理念是信任程序员,它假设程序员会知道自己在干什么。当你写出*pa = 100的时候,你可能是不小心。但是当你写出*const_cast<int*>(pa) = 100的时候就代表你清楚自己想做什么了。C++是自由的,但是为了享受这份自由,你会经历很大磨难。而其它很多语言,当自己是保姆,当程序员是小孩子,将很多东西都包装好,这个不准那个也不准。以学自行车为例,C++会放开双手,让你自己去骑,开头当然会跌得很惨,之后你可以骑车到你想去的任何地方;另外一些语言就不同了,在车子后面安装两个小轮子,还不放心,再在前面安装两个,没错是很安全,不过骑不快,遇到窄点的地方就进不去了。C++的自由是争议很大的地方,各人都有不同的观点。

(注: 想了解多点转型符,可以参考More Effective C++条款2, 本文主要讲些基本概念,不会讲太多语法)

那么到底那些常量变量,静态动态,是怎么在内存是怎么布置的呢?

我们知道,数据有不同的性质,有些可读不可写,有些可读可写,有些只可以给系统读写......等等。这样不同性质的数据就分开放到内存不同的位置,这叫分段或者叫分区。分段之后最大的好处是容易实现保护,可以指定从这里开始,到那里结束,这个范围的内存空间的性质,假设是只读的,要是程序执行的时候意图修改,就会触发错误处理。我们经常会将空指针赋值为0, 低端地址通常为系统保留,不可被访问,当想引用地址0中的数据,就会出错。原则上将空指针赋值为不可访问的内存地址就可以了,不一定是0。
============================================
高地址 +-----------+
| | (未初始化)变量
+-----------+
| | 静态变量
+-----------+
| | 只读
+-----------+
| | 常量(不可访问)
+-----------+
| | 代码
+-----------+
| | 堆
+-----------+
| | 栈
+-----------+
| | 系统保留
低地址 +-----------+
windows程序执行时的典型内存分区
==========================================
段是按照数据的性质而不是按用途来分的,常量与代码就可以合为一段。为了更快速地将程序装入内存,通常可执行文件也会有一定的结构,内部也可以分区,称为节区。VC中有个小工具dumpbin, 你可以随便编译一个程序,比如main.exe, 敲入命令, dumpbin main.exe, 会出现类似的字样
========================================
Summary
4000 .data
1000 .rdata
5000 .text
===================================
这些.data .rdata .text是节区的名称。程序定义的一些常量,比如字符串,多数会放到.data里面。当程序被执行时,这些内容会被装到内存的相应位置。想看看节区里面的内容,可以敲入 dumpbin main.exe -section:.data -rawdata:bytes > data.txt 之类的命令。如果学过汇编的朋友,这些应该会很清楚,有那些.data .code .const之类的语句。

下面,介绍一下栈(stack)。学过数据结构的都应该知道,stack是先进后出的。比如叠起来的碟子,只可以在最上面放和最方面取。当放上一个碟子,就会变高。当取下一个碟子,就会变低。最先放的碟子会在最后取出,最后放的会最先取出。放碟子,使碟子升高,这动作叫做push, 取碟子,使碟子降低,这动作叫做pop。

stack这结构用得极其频繁,8086系列的计算机就有相应的机械指令push, pop和相应的寄存器(e)sp (e)bp来支持硬件上的栈。

为得到数据,需要得到它的地址。通常地址都不会直接给出的,会使用基址+偏移的形式。这种形式可以表述为 从某某地方开始,向下或者向上数多少格。所谓基址是一个参考点,偏移是相对参考点而言的。比如 基址为1000, 偏移为5,就得到1005; 基址为1000, 偏移为-5, 就得到995。基址+偏移的形式也用的很广泛,平时接触得最多的算是数组了。

寄存器ebp就表示基址, 通常我们叫它栈底寄存器,其实不用理它叫什么,知道是个参考点就行了。esp就是一个偏移, 相对ebp而言, 指示栈的顶端。push指令会使esp减少(减少意味着距离ebp更远,就是栈顶升高), 跟着在那个地方放数据。pop指令将eip所指的数取出,更着eip增加(增加意味着距离ebp近了,就是栈顶降低)。至于升高或降低多少,就看你的数据有多大了。
=============================================
push eax

EAX | ..... | EAX | ..... | EAX | ..... |
12345678H +---------+ 12345678H +---------+ 12345678H +---------+
ESP 0 | 02h | ESP -4 | 02h | ESP -4 | 02h |
+---------+ +---------+ +---------+
| 0Dh | ESP->| 0Dh | ESP->| 78h |
+---------+ +---------+ +---------+
| 10h | | 10h | | 56h |
+---------+ +---------+ +---------+
| F0h | | F0h | | 34h |
+---------+ +---------+ +---------+
| 06h | | 06h | | 12h |
+---------+ +---------+ +---------+
EBP(ESP)->| 78h | EBP->| 78h | EBP->| 78h |
+---------+ +---------+ +---------+
| ...... | | ...... | | ...... |
| | | | | |
开始 中间过程 之后
=========================================
pop ebx

EBX | ..... | EBX | ..... | EBX | ..... |
00000000H +---------+ 12345678H +---------+ 12345678H +---------+
ESP -4 | 02h | ESP -4 | 02h | ESP 0 | 02h |
+---------+ +---------+ +---------+
ESP->| 78h | ESP->| 78h | | 78h |
+---------+ +---------+ +---------+
| 56h | | 56h | | 56h |
+---------+ +---------+ +---------+
| 34h | | 34h | | 34h |
+---------+ +---------+ +---------+
| 12h | | 12h | | 12h |
+---------+ +---------+ +---------+
EBP->| 78h | EBP->| 78h | EBP(ESP)->| 78h |
+---------+ +---------+ +---------+
| ...... | | ...... | | ...... |
| | | | | |
开始 中间过程 之后
===========================================
(注1:发觉自己真的很罗嗦,希望不会使大家很烦。这帖子是针对初学者的,所以有些很简单的东西也写进去了,不是看不起大家哦)
(注2:写程序时用的地址只是线性地址,从线性地址到物理地址还有个令人抓狂的复杂过程)

2007-07-31 16:43
sevenchina
Rank: 1
等 级:新手上路
帖 子:18
专家分:0
注 册:2007-7-20
收藏
得分:0 

为解决前面的遗留问题,需要分析代码编译后的汇编输出, (1)(2)其实都是为了分析打基础的。所用的编译器为VC++6.0, 假设我们的文件为main.cpp。敲入编译命令 cl -GX -nologo -FAs main.cpp。-FAs选项可以输出汇编代码,之后目录处会多了main.asm。编译时不作优化处理, 以免使分析更复杂。也可以建立个控制台工程,Project->setting调出设置对话框, C/C++标签, Category选择Listing Files, Listing file type选择Assembly with Source Code。其实刚学C++的时候,拿个文本编辑器写代码,再敲命令编译会更方便,文本编辑器可以考虑一下vim。假如这系列帖子可以写下去,很可能会有很多汇编分析的,要有点心理准备,其实也不会很难的,来来去去都是那几条指令, push, pop用得很多,之前也已经解释过了。也可以选择自己喜欢的编译器, 不过那样汇编输出会有些不同。我知道VC++6.0不是很合标准,不过还没有牵涉到template就没有多大关系了,VC++.net之类大笨重了,我的机器烂,会很慢。gcc的汇编输出跟学汇编时养成的习惯正好相反,看着别扭。所以选择6.0。

开始的汇编分析会讲到比较详细,后面会较简略。看这个例子
=========================
#include <stdio.h>

const int a = 3;
int b = 1024 * a;

int main()
{
const int a = 2;
int b = 1024 * a;

const int* pa_g = &::a;
const int* pa_l = &a;

//*(const_cast<int*>(pa_g)) = 100;
printf("%d\n", *pa_g);
printf("%d\n", ::a);
printf("%d\n", ::b);

*(const_cast<int*>(pa_l)) = 100;
printf("%d\n", *pa_l);
printf("%d\n", a);
printf("%d\n", b);

return 0;
}
========================================
先不运行,猜一猜结果会是什么。运行结果为3 3 3072 100 2 2048。猜对没有? 我猜的是 3 3 3072 100 100 2048。结果令我很吃惊,我怎么也想不明白printf("%d\n", *pa_l)和printf("%d\n", a)为什么会不同。还有*(const_cast<int*>(pa_g)) = 100;编译的时候是没有问题的,运行时就会出错。看汇编输出之后,一切都明白了。(假设已经得到汇编输出)

========================================
CONST SEGMENT
_a DD 03H
CONST ENDS
_DATA SEGMENT
?b@@3HA DD 0c00H
_DATA ENDS

这就是全局变量a和b的内存分配方式。那些SEGMENT ENDS指定数据分到什么节区,还记得之前说的.data .rdata .text吗? const中的数据会放到.rdata, 装入内存就是只读的数据。_DATA中的数据会放到.data 装入内存就是成了全局数据了,原则上static定义的也是全局数据。那为什么b会变成?b@@3HA这古怪的样子呢? 这主要是为了解决重名问题。程序中可以重名,不过编译连接的时候就不可以,名字要唯一的。这样就需要有一套规则来将程序中的名字转换到一个唯一的名字。每个编译器都有自己的规则。以前我无聊的时候看过每个符号代表什么意思,不过很多都搞不明白,没有记错的话, H是代表int类型, 内置的内型都是自己的代表字母,不过这些不用深究。要是用gcc, 还有个工具叫c++filt, 你敲这些转换后符号进去,会输出程序定义时的符号。这时候可以看出,全局变量a放到CONST段,只读;全局变量b放到_DATA段,可以读写。

现在可以解释*(const_cast<int*>(pa_g)) = 100; 运行时候为什么会出错了? pa_g指向::a(::a代表全局变量a, 为了和main函数里面的a作区别), 而::a是只读的,意图修改::a的值,当然会出错了,还记得之前说的段保护吗?

再看看::a,可以看到它的值为03H, 也就是3, ::b,值为0c00H, 也就是3072。编译成可执行文件之后,::a和::b会在文件的节区中存储着,跟着再装入内存,所以::a和::b在程序还没有执行之前就有了初始值。::b的初始值为3075,说明编译的时候已经计算了1024 * a。要是程序中定义了常量,常量出现的地方会直接被它的数值所替代。那么式子就变成1024 * 3,完全可以在编译时期确定下来。式子中有常量,编译时候直接计算了结果,这叫做常量折叠。很复杂的式子在编译时期也可以直接计算出来,比如(a*a*a)+(a/a)+1024-a; 所以写程序的时候,不要那么笨自己去手工算了,没有意义的,还使代码更难明白。比如想分配9k内存,直接写成malloc(9*1024)就可以了。

=========================================
_DATA SEGMENT
$SG532 DB '%d', 0aH, 00H
$SG533 DB '%d', 0aH, 00H
$SG534 DB '%d', 0aH, 00H
$SG536 DB '%d', 0aH, 00H
$SG537 DB '%d', 0aH, 00H
$SG538 DB '%d', 0aH, 00H
_DATA ENDS

可以看出,程序用到的字符串也放到_DATA中,0aH是ASCII码,表示回车符,字符串以'\0'结尾。'\0'起来就是00h。这是没有优化的结果,优化之后,相同内容的字符串很可能会合并为一个。
=======================================

下面转入_TEXT段,通常这里会放代码。看看main函数里面a,b,pa_g,pa_l的定义
_a$ = -4
_b$ = -12
_pa_g$ = -16
_pa_l$ = -8
为什么会这样的呢? 不要着急,慢慢看下去。

===========================
push ebp
mov ebp, esp
sub esp, 16
这几句是进入函数时候做的准备,是调整堆栈,给局部变量分配空间。sub esp 16 给a,b,pa_g,pa_l分配空间,占用16个字节。这问题先放开。

============================
mov DWORD PTR _a$[ebp], 2

mov DWORD PTR _b$[ebp], 2048

mov DWORD PTR _pa_g$[ebp], OFFSET FLAT:_a

lea eax, DWORD PTR _a$[ebp]
mov DWORD PTR _pa_l$[ebp], eax

看看a,b,pa_g,pa_l的赋值。上面已经知道 _a$ = -4, 这其实就是个偏移,ebp为参考点,向上数4个基本单元,这个内存范围就分给了变量a, ebp-4就是a的起始地址。之前说过类型的意义,其中一个就是可以确定变量的大小,同样dword ptr就表示这是一个dword类型,dword是双字,也就是占用4个字节。Masm汇编中用中括号表示地址里面的值,所以wrod ptr [ebp-4]就表示变量a的值。[ebp-4]可以写成-4[bep], 而 _a$ = -4, 因此就知道mov dword ptr _a$[ebp], 2 表示将2赋给a, 就是a=2。

同样mov DWORD PTR _b$[ebp], 2048, 是给b赋值。2048是编译的时候已经计算了a*1024,为常量折叠。

可以看出
const int a = 2;
int b = a * 1024;
a有没有const来修饰,从汇编输出是看不出来的。const只不过是编译时候定的约束,真正编译出来的代码其实是没有分别的。

OFFSET FLAT:_a是全局变量a的偏移, 将这偏移放到pa_g中,pa_g就指向了全局的a, pa_g就为一指针。所谓指针,放的就是地址,更准确的是一个偏移, 这个偏移是以数据段起始地址为参考点,这里偏移当成数据的地址就可以了。

lea eax, DWORD PTR _a$[ebp]是取得局部a的地址,跟着放入pa_l中,所以pa_l指向局部的a, pa_l也为一指针。

那为什么取全局变量a的地址用offset, 而取局部变量a的地址用lea的呢? 这是因为全局a在程序运行时候,地址就已经确定了。局部a在栈中分配内存,地址在程序执行的时候才确定。offset只可以取静态的地址,lea却可以取动态的地址。这是offset 和lea的分别。学汇编的时候,这个问题也很多人问的。

=================================
mov ecx, DWORD PTR _pa_g$[ebp]
mov edx, DWORD PTR [ecx]
push edx
push OFFSET FLAT:$SG532
call _printf
add esp, 8

这是语句printf("%d\n", *pa_g);的汇编输出,首先将pa_g的值取出,放到ecx中,pa_g的值是个地址。再根据ecx这个地址,取出地址所指内存的数据,放到edx, 这个就是*pa_g的含义。先从pa_g得到地址,再根据地址取数据。push edx将取到的数据压栈,再将"%d\n"的地址压栈,再调用函数printf。之后add esp 8语句将栈恢复平衡。函数的调用和返回,以后有机会再写篇帖子,现在不多说。

=================================
之后的函数调用过程都差不多。再看
mov ecx, DWORD PTR _pa_l$[ebp]
mov DWORD PTR [ecx], 100
先将pa_l的值取出,放到ecx, 注意这是个地址,下一个语句就这地址所值内存的值,也就是局部变量a的值变为100。这个时候a=100。从这里可以看出,用const修饰不一定就不能改变。不过改变的时候用麻烦一点,需要指针或者引用,间接的改变。

===========================
好啦,因为a的值已经改变,自然printf("%d\n", *pa_l);会输出100。那么为什么printf("%d\n", a); 却还是输出2的呢? 那不是矛盾了吗?
前面我们说过, 编译时候遇到常量,就用常量的数值来代替。因为a定义为常量, 所以printf("%d\n", a);直接变为printf("%d\n", 2);看看编译结果
push 2
push OFFSET FLAT:$SG537
call _printf
add esp, 8
直接传递参数2的,所以这个时候a变不变,跟printf("%d\n", a)都没有关系了。再看看printf("%d\n", b);b不是常量,被编译成
mov ecx, DWORD PTR _b$[ebp]
push ecx
push OFFSET FLAT:$SG538
call _printf
add esp, 8
需要先取出b的值,再传递参数。这是常量和变量很大的不同,不过这个不同发生在编译时候。当执行的时候,已经没有什么区别了,从编译出来的代码基本上不能区分变量和常量了。而局部的常量和全局常量的不同,在于全局常量被分到只读段,局部常量还是在栈段。局部常量的所谓不可变,是编译器在编译的时候保证的,而不是在程序执行的时候。

提一下EXTRN _printf:NEAR是引入外部符号。好好看看这个例子,希望大家有所收获

2007-07-31 16:44
sevenchina
Rank: 1
等 级:新手上路
帖 子:18
专家分:0
注 册:2007-7-20
收藏
得分:0 

好好学习,天天向上~~~自己顶

2007-07-31 16:47
Arcticanimal
Rank: 3Rank: 3
等 级:论坛游民
威 望:7
帖 子:341
专家分:20
注 册:2007-3-17
收藏
得分:0 
楼主真是努力...

try new catch
2007-07-31 20:17
ph53543
Rank: 1
等 级:新手上路
帖 子:21
专家分:0
注 册:2007-7-4
收藏
得分:0 
还没看 先顶
2007-07-31 20:38
快速回复:[转载]关于C++中的常量与变量对C++中的变量与常量的理解
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.019995 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved