日历
网志分类
· 所有网志 (91)
· 程序员面试题收集 (11)
· 算法 (25)
· Paper (3)
· c++,java (37)
· bbs search project (2)
· career (7)
· 设计模式 (0)
· 未分类 (6)
最新的评论
· 06/05 新书推荐!清华...
站内搜索
友情链接
· 我的歪酷 非非共享界
· 涛声依旧blog,也有很多题收集
· jammy 生活就是映像
· Aleph mm's
· onebird(一鸣)
· lucene.com.cn
· Goolge黑板报--没事看看
· jjing

订阅 RSS

0034090

歪酷博客

kua's

海明威说--
“这个世界是美好的,值得我们为之奋斗”

我相信后半句...

                                                             kuapig@gmail.com


kua @ 2009-01-05 15:21

一, 为什么会有字节对齐问题?
字节对齐问题之主观所以存在,我想是源于程序员和CPU对内存数据访问的理解稍有不同。在我们看来,程序对内存数据的访问总是按照其大小,一个字节接着一个字节进行的,比如读取一个char型变量,系统就读去一个字节的内容,读取一个int型变量,系统就读取四个字节的内容:
0|1|2|3|4|5|6|7……
但是事实并非如此。并非所有CPU都是一个一个字节地来访问内存的(现在可能很少有CPU这么做,效率太低了!)。比如,CPU可以按照1,2,4,6, 8,16,32……的粒度来访问内存数据。这被称之为memory access granularity(内存访问粒度/间隔)[1],例如按照4字节来访问内存:
0123|4567|8……
显然,内存访问效率和数据对象的大小以及CPU的内存访问粒度相关。比如我们在32位系统上访问一个4字节int型数据对象,假设这个对象a的起始地址为0×00:
0×00 0×01 0×02 0×03
这时候,a的内存位置处于效率最佳情况下:
访问粒度 CPU访问次数
1字节 4次
2字节 2次
4字节 1次
我们当然希望每次运行程序,支持4字节访问的CPU都可以一次就读/写这个int型数据。但是事实确未必一定是这样。如果a的起始地址为0×01,则:
访问粒度 CPU访问次数
1字节 4次
2字节 3次
4字节 2次
按照2字节访问需要三次:第一次访问0×00~0×01,第二次访问0×02~0×03,第三次访问0×04~0×05,然后把三部分数据拼凑起来。
同理,按照4字节访问需要两次:第一次访问0×00~0×03,第二次访问0×04~0×07,然后把两部分数据拼凑起来。
显然,a的地址不符合一定的要求,就会导致效率损失。
二,X86的字节对齐规则
Kang Su Gatlin在其文章[2]中指出了X86上对齐的原则。32位X86CPU支持1,2,4,8字节的访问。访问L字节(2的整数倍)数据,其地址A满足A MOD L = 0。
这样对基本的数据类型就有如下规则:
char —— 单字节对齐,A MOD 1 = 0;可为任意的奇/偶值
short —— 双字节对齐,A MOD 2 = 0;偶地址
int —— 四字节对齐,A MOD 4 = 0;地址最后一位为:4,8,C
double —— 八字节对齐, A MOD 8 = 0;地址最后一位为:0,8
三,堆栈的对齐布局
先用VC6运行一个例子:
#include
int main(int argc, char* argv[])
{
char c0;
char c1;
char c2;
printf(”%p\n%p\n%p\n”,&c0,&c1,&c2);
return 0;
}
在Debug模式下,得到类似如下的结果(具体地址值可能不同):
0012FF7C
0012FF78
0012FF74
OK,这个结果的每个地址,都符合1字节对齐,MOD 1 = 0。不过,等等,也符合四字节对齐阿!MOD 4 = 0。堆栈理论上只需要3个字节,这里却用了12个字节。
因为VC6编译器默认对堆栈采用了4字节对齐原则。所以在不进行任何优化的情况下,一个char对象也需要占据4个字节空间。这样做的好处呢?—— 显然,无论如何,1字节和2字节对� B68 ��都是符合对齐原则的。
用RELEASE模式,把编译选项调整到MAXSPEED或者MAXSIZE,可以得到如下的输出:
0012FF7D
0012FF7E
0012FF7F
这次堆栈中就没有冗余的字节了。访问速度也不会因为字节排列而下降。
对于32位总线而言,我认为8字节对齐可能没有什么太大意义。因为无论如何,8字节数据对象,例如double,都需要两个时钟周期的。这也就是为什么 VC6在32位平台上总是默认4字节对齐的原因:4字节对齐总是兼容单/双字节对齐方式的。关于32位总线上8字节对齐的性能影响,我么在后面的例子中可以看到。
我们可以修改上一个例子:
#include
int main(int argc, char* argv[])
{
char c0;
char c1;
double d0;
char c2;
printf(”%p\n%p\n%p\n%p\n”,&c0,&d0,&c1,&c2);
return 0;
}
Debug模式下:
0012FF7C
0012FF6C
0012FF78
0012FF74
VC6将堆栈变量的位置做了偏移。double被放在了栈顶。这样做的好处是什么暂时不太清楚。
四,结构体/联合体的布局
Kang Su Gatlin指出了struct/union的布局规则,对于inter-struction/inter-union,对齐规则很简单,即 struct/union中最大的那个alignment,而这个max(alignment),又取决于编译器的设置(用参数/Zpn或者代码中用 #pragma pack(n)),即自身的对齐要求和编译器设置对齐要求二者中的最小值。
4.1 简单的情况
例如:
#include “stdio.h”
struct SA
{
char g;
double k;
};

int main(int argc, char* argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}

输出结果如下:
Size=16
Addr=0012FF70
VC6默认采用8字节对齐,double的对齐要求也是8字节。因此这里输出大小为16,地址按照规则需符合8字节对齐,最后四位为0000。
我们可以加上pack 限制
#include “stdio.h”
#pragma pack(4)
struct SA
{
char g;
double k;
};

int main(int argc, char* argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}
则输出为:
Size=12
Addr=0012FF74
将默认的对齐边界设置为4,则成员g仅占用4个字节,因为k在4字节边界上对齐。由于SA的成员最大对齐值为4,所以整个结构体也4字节对齐。所以地址的最后4位可以被4整除。
4.2 嵌套的结构体成员
看一个例子:
#include “stdio.h”
struct SB
{
int c;
double k;
short b;
};

struct SA
{
char g;
SB b;
char k;
};

int main(int argc, char* 1498 argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}
输出为:
Size=40
Addr=0012FF58
从这个例子可以看出嵌套结构体的规则其实也很简单。首先找到SA中成员的最大对齐边界,由于b是结构体类型,因此b的对齐边界取决于SB中的成员最大对齐边界:8。因此,SA中的成员8字节对齐。由于SB中的成员也是8字节对齐的,所以大小为8*5=40。而整个变量a的地址也要符合8字节对齐要求。
再看看用了pack(4)的情况:
#include “stdio.h”
#pragma pack(4)
struct SB
{
int c;
double k;
short b;
};

struct SA
{
char g;
SB b;
char k;
};

int main(int argc, char* argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}
按照分析,SB的最大对齐边界:4,所以SA的对齐边界也是4,这个结果应该输出:
Size=24
Addr=0012FF68
注意pack(n)的限制,作用域从起始位置开始。例如:
#include “stdio.h”
struct SB
{
int c;
double k;
short b;
};

#pragma pack(4)
struct SA
{
char g;
SB b;
char k;
};

int main(int argc, char* argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}
SB按照8字节对齐,SA按照4字节对齐,其成员b整体上满足4字节对齐即可。因此大小是8*3(SB大小)+8(SA的成员g和k的大小):
Size=32
Addr=0012FF60
再看更复杂的例子:
#include “stdio.h”
#pragma pack(4)
struct SB
{
int c;
double k;
short b;
};

#pragma pack(2)
struct SA
{
char g;
SB b;
char k;
};

int main(int argc, char* argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}
SB的对齐边界为4,因此大小为16,SA的对齐边界为2(最小值),因此大小为16+4=20:
Size=20
Addr=0012FF6C
再来看看成员变量位置的颠倒会给struct带来什么结果?
#include “stdio.h”
struct SB
{
int c;
short b;
double k;
};
struct SA
{
char g;
SB b;
char k;
};
int main(int argc, char* argv[])
{
SA a;
printf(”Size=%d\nAddr=%p\n”,sizeof(a),&a);
return 0;
}
运行结果如下:
Size=32
Addr=0012FF60
变量a的大小变成了32,因为SB中的double成员k的声明被放在了最后。而int成员c和short成员b各占4字节,就可以保证k处于8字节对齐状态。因此,SB的大小为16。再加上SA的另外两个成员:g和k分别占用8个字节,所以得到的Size就为32。同样,地址还是符合8字节对齐边界的要求。
从这个例子也可以看出,通过合理安排结构体成员的声明顺序,可以减少其占用内存的大小。
五,性能
我使用了如下的程序来测试自己对齐给性能带来的影响:
// Test.cpp : Test the performance of data alignment of 1, 2, 4, 8
#include “stdafx.h”
#include
#include
#include
#include

LARGE_INTEGER operator -(LARGE_INTEGER a, LARGE_INTEGER b)
{
LARGE_INTEGER reVal;
if (a.LowPart
class CCountTest
{
public:
CCountTest()
{
dest = new T[128];
origsource = new byte[10000];
}
~CCountTest()
{
delete[] origsource;
delete[] dest;
}

void CountTest(int offset)
{
UINT iters = 99999;
T* source;
LARGE_INTEGER startCount, endCount, freq;

byte* pByte = GetAlign(origsource, offset);
source = (T *) (pByte);
printf(”dest = %p source = %p\n”, dest, source);

QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&startCount);
{
for (UINT x = 0; x CountTest;
CountTest.CountTest(1);
CountTest.CountTest(2);
CountTest.CountTest(4);
CountTest.CountTest(8);
return 0;
}
这里我们用double类型进行测试,从一个数组中向另一个数组复制数据,测试环境:
Intel P4 2.8G + 512 DDR + Windows2000
输出结果如下:
dest = 00342080 source = 00342489
elapsed time = 0;111243
dest = 00342080 source = 0034248A
elapsed time = 0;111481
dest = 00342080 source = 0034248C
elapsed time = 0;74270
dest = 00342080 source = 00342488
elapsed time = 0;69388

可以看到,1字节和2字节对齐是,对double数据的存取远比4或者8字节对齐来的低效!但是4字节和8字节对齐似乎没有对double的存取造成多大的效率差异。实事上,实际运行中最后两个时间值得大小关系并不唯一确定,这里只是一次运行的结果。这可能说明,在32位平台上,double类型数据对象的 8字节对齐,其实并没有比4字 1768 节对齐来的效率更高。毕竟32位总线不可能一次传输完8个字节的数据。这也符合前面我们提到的栈字节对齐中为何double 型变量并不一定被系统安排为8字节对齐的情况。

六,尾巴
个人觉得,其实大多数情况下,在Windows上开发,我们都不用关心堆栈和结构体的成员声明顺序,以及是否通过填充冗余字节来达到地址对齐。因为编译器通过优化选项可以自动进行处理。即使浪费了一些内存,在大多数应用中,恐怕也无关痛痒。只是不要相当然的用一个常数来替代sizeof函数就好。
另外就是程序移植到其他的平台上时,可能目标平台并不支持非对齐式的数据访问。编译器也不会自动纠正对齐问题。这样一来,如果想编写移植性好的程序,就要非常小心了。

参考文章:

1,Jonathan Rentzsch : Data alignment: Straighten up and fly right (http://www-128.ibm.com/developerworks/library/pa-dalign/ )

2,Kang Su Gatlin :Windows Data Alignment on IPF, x86, and x64 (http://msdn.microsoft.com/library/en-us/dv_vstechart/html/vcconwindowsdataalignmentonipfx86×86-64.asp?frame=true)

续:

MSN的C++ Group里看到一些讨论,关于栈变量放置的问题:

我们可以修改上一个例子:
#include
int main(int argc, char* argv[])
{
char c0;
char c1;
double d0;
char c2;
printf(”%p\n%p\n%p\n%p\n”,&c0,&d0,&c1,&c2);
return 0;
}
Debug模式下:
0012FF7C
0012FF6C
0012FF78
0012FF74
VC6将堆栈变量的位置做了偏移。double被放在了栈顶。这样做的好处是什么暂时不太清楚。
——–double放在栈顶,因为有些编译器可以将同一类型的变量连续放置(好处仍然不清楚)。如果要强行安排这些变量的放置顺序,可以用一个struct:

struct A{

char c0;
char c1;
double d0;
char c2;

}

这样就OK了,当然前提是不能让编译器优化struct内部的放置顺序。




 
kua @ 2008-12-24 14:34

Please look at the code below, any problem you find?

 

p += 1;

 

int* p = new int[5];delete [] p; //or delete p;

When you apply for a memory space, the system will record this space, it will have a head and tail to tag this space.
The only way to free this space is to delete or free the head. Otherwise you will meet the expression:_CrtIsValidHeapPointer(pUserData)  error.


 
kua @ 2007-11-16 19:11

当你需要处理XML文档时,你的首要选择是使用DOM(文档对象模型)还是使用SAX(用于XML的简单API),即当前使用的两个主要的XML API。你可以使用任何一种(或者在同一时间使用两种)来处理XML文档,然而DOM将文档载入到内存中处理,而SAX则相反,它可以检测一个即将到来的 XML流,由此并不需要所有的XML代码同时载入到内存中。
选择DOM与SAX,与在一个数据库中的表单与视图之前选择一样:选择适合于当前实际情况的方法。如果你只是想简单地查看XML文档而不处理它,那么请选择使用SAX。


       SAX与DOM之间的区别


SAX与DOM之间有一些显著区别,包括:
         DOM是复杂对象处理的首选,比如当XML比较复杂的时候,或者当你需要随机处理文档中数据的时候。SAX从文档的开始通过每一节点移动,以定位一个特定的节点。
DOM为载入到内存的文档节点建立类型描述。最终,这些描述呈现了可容易横向移动、潜在巨大、树型结构。如果XML很冗长,DOM就会显示出无法控制的胀大。例如,一个300KB的XML文档可以导致RAM或者虚拟内存中的3,000,000KB的DOM树型结构。通过比较就会发现,一个SAX文档根本就没有被解构,它也没有隐藏在内存空间中(当然当XML流被读入时,会有部分文档暂时隐藏在内存中)。SAX就是一种“更轻巧的”技术──它可以给你的系统带来更轻的负担。SAX相当于观看一场马拉松比赛,而DOM就好比邀请所有的比赛选手到家里参加晚餐。
所以,你如何选择SAX和DOM?如果你处理复杂的东西,比如高级XSLT转换,或者Xpath过滤,请选择使用DOM。如果你建立或者更改XML文档,你也可以选择DOM。
相反,你可以使用SAX来查询或者阅读XML文档。SAX可以快速扫描一个大型的XML文档,当它找到查询标准时就会立即停止,然后再处理之。
在某些情况下,在一个方案中,最佳的选择是使用DOM和SAX处理不同的部分。例如,你可以使用DOM将XML载入到内存并改变它,然后通过从DOM树中发送一个SAX流而转移最后的结果。


SAX概念

        SAX是Simple API for XML的缩写,它并不是由W3C官方所提出的标准,可以说是“民间”的事实标准。实际上,它是一种社区性质的讨论产物。虽然如此,在XML中对SAX的应用丝毫不比DOM少,几乎所有的XML解析器都会支持它。

与DOM比较而言,SAX是一种轻量型的方法。我们知道,在处理DOM的时候,我们需要读入整个的XML文档,然后在内存中创建DOM树,生成DOM树上的每个Node对象。当文档比较小的时候,这不会造成什么问题,但是一旦文档大起来,处理DOM就会变得相当费时费力。特别是其对于内存的需求,也将是成倍的增长,以至于在某些应用中使用DOM是一件很不划算的事(比如在applet中)。这时候,一个较好的替代解决方法就是SAX。

        SAX在概念上与DOM完全不同。首先,不同于DOM的文档驱动,它是事件驱动的,也就是说,它并不需要读入整个文档,而文档的读入过程也就是SAX的解析过程。所谓事件驱动,是指一种基于回调(callback)机制的程序运行方法。(如果你对Java新的代理事件模型比较清楚的话,就会很容易理解这种机制了)

        在XMLReader接受XML文档,在读入XML文档的过程中就进行解析,也就是说读入文档的过程和解析的过程是同时进行的,这和DOM区别很大。解析开始之前,需要向XMLReader注册一个ContentHandler,也就是相当于一个事件监听器,在ContentHandler中定义了很多方法,比如startDocument(),它定制了当在解析过程中,遇到文档开始时应该处理的事情。当XMLReader读到合适的内容,就会抛出相应的事件,并把这个事件的处理权代理给ContentHandler,调用其相应的方法进行响应




 
kua @ 2007-11-10 18:26




什么是析构函数
------------------
class A
{
public:
A();
~A(); // 析构函数 : 在类对象生命周期结束时被自动调用,来释放一些资源;
};

另外:基类析构函数声明为virtual是很重要的~

比如:有一class B 继承自 上边的 class A;
如果我们这样 A pClassA = new B(pClassA这里是个指针?); 即new 一个派生类(B)的对象,并且有 基类的指针来指向它~这样没问题,但是 如果我们要释放刚才的空间~ delete pClassA 这个就会有麻烦,因为C++标准中对这一行为没有明确定义,即通过基类指针来销毁派生类对象这个行为的结果是无法预期的,其中可能直接导致,delete pClassA 这个行为 根本不能使 派生类的(B)的析构函数被调用到,如果B的析构函数中有一些 必须要做的事 (比如释放资源什么的)那么将无法进行了~;

如果将 基类(A)的析构函数声明为virtual就不会这样了,这是因为虚函数会由类的虚函数表(vtalbe)来维护,每当通过指针或引用来调用虚函数,就会到这个vtable中去找相应的虚函数指针;

每个有virtual函数的类都有一份vtable,派生类将自动继承基类的vtable,vtable中实际存储的是virtual函数的指针~
每当派生类改写了基类虚函数,vtable的相应指针项就会更改了~而指向新的(派生类override的)虚函数地址~

这样 我们上边的delete pClassA这个行为 就会引发 B类的析构函数了,



上面介绍的有关虚函数表的概念 也真是 C++ 类的多态行为的内幕原理~


 
kua @ 2007-10-31 17:58

函数内无任何判断,内存分配以及2个指针指向的内存不交叠判断都由用户判断
char* mystrcpy(char* dest, const char* src)
{
 char* p = des;
 while(*p++ = *src++);
  return dest;
}


 
kua @ 2007-10-31 12:48

Select  [Distinct()] From 
Where
Group by  Having
Order by 


Select  count(1), date From
Where
Group by date Having count(*) > 3
Order by date

group by
分组
通用数据库具有基于表的特定列对数据进行分析的能力。
可按照在 GROUP BY 子句中定义的组对行进行分组。以其最简单的形式,组由称为分组列的列组成。 SELECT 子句中的列名必须为分组列或列函数。列函数对于 GROUP BY 子句定义的每个组各返回一个结果。下列示例产生一个列出每个部门编号的最高薪水的结果:
SELECT DEPT, MAX(SALARY) AS MAXIMUM
FROM STAFF
GROUP BY DEPT


 
kua @ 2007-10-29 15:37

以下都是我个人的理解,估计有问题,看的时候请慎重考虑,仔细判断
1.被虚继承的类就是虚基类
2.何为虚继承: 先说普通继承
普通继承: class B: public A, 
B从A继承,B会将A做一份copy,作为自身的一部分,然后在加自己独有的一部分。
加入A是一个有虚函数的类,考虑A和B的结构
sizeof(A) = sizeof(A的数据成员) + sizeof(vptr),这个vptr指向A类的 vtable
sizeof(B) = sizof(A的数据成员) + sizeof(B数据成员) + sizeof(vptr),这个vptr指向B类的vtable

虚继承: 虚继承与普通继承不同点在于,子类不实际的copy父类的一部分,而是生成一个引用,指向父类对象
但是在用sizeof统计大小时,依然将父类的成员变量统计在内,此时B的结构如下:
sizeof(B) = sizof(A的数据成员) + sizeof(B数据成员) + sizeof(vptr) + sizeof(pointer to A), vptr指向B类的vtable, 并且多了一个引用指向A对象

虚继承的这个特性在C++的多重继承应用中可以起到很好的作用:
一个没有虚继承的多重继承:
class A
{
 void f() ;
};
class B: public A
{
};
class C:public A
{
}
class D: public B, public C
{
}
void main()
{
 D d;
d.f();
}
这种情况下编译器会报错:D::f is ambiguous,could be the 'f' in base 'A' of base 'B' of class 'D',or the 'f' in base 'A' of base 'C' of class 'D'
很明显看出:d.f()是有歧义的,因为D中有2个A的copy,也就有2个A::f()函数,编译器不知道应该调用哪一个。

这个问题应用虚继承可以得到很好的解决,类定义重写如下:
class A
{
 void f() ;
};
class B: public virtual A
{
};
class C:public virtual A
{
}
class D: public B, public C
{
}
void main()
{
 D d;
d.f();
}
此时D中只指向一个A对象,调用d.f(),就是调用那个唯一的A的f()函数,没有歧义,顺利执行。因为此时B和C都是指向同一个A对象。
此时D的结构如下图:

需要注意的是B和C都必须是从A虚继承,只有一个是虚继承仍然会有歧义



 
kua @ 2007-10-29 13:11

C++中是不允许virtual 构造函数的,如果在构造函数前加virtual
以vc6为例,编译器报错为:'inline' is the only legal storage class for constructors
虽然不明白这个跟inline有什么关系,但是错误是肯定的

构造函数和析构函数都可以是private的,这种情况下编译通过,但是该类无法被实例化,即使继承也不行
例如:

class A       
{
private:
 A();
 virtual ~A();

};

class B : public A 
{
public:
 B();
 virtual ~B();

};
B b;
以上代码是编译不通过的,因为B() , ~B()分别需要调用A(),~A(),而它们都是private的 所以无法调用

解决方法只有friend
包括friend class和 friend function
例如可以将A的定义修改如下,就可以实现

class A 
{
private:
 A();
 virtual ~A();
 friend class B;

};

或者friend函数也可以在内部生成A的变量,因为它可以调用A的私有构造函数和析构函数




 
kua @ 2007-10-27 12:52

恐怕比较一下volatile和synchronized的不同是最容易解释清楚的。volatile是变量修饰符,而synchronized则作用于一段代码或方法;看如下三句get代码:

  1. int i1;              int geti1() {return i1;}
  2. volatile int i2;  int geti2() {return i2;}
  3. int i3;              synchronized int geti3() {return i3;}

  geti1()得到存储在当前线程中i1的数值。多个线程有多个i1变量拷贝,而且这些i1之间可以互不相同。换句话说,另一个线程可能已经改变了它线程内的i1值,而这个值可以和当前线程中的i1值不相同。事实上,Java有个思想叫“主”内存区域,这里存放了变量目前的“准确值”。每个线程可以有它自己的变量拷贝,而这个变量拷贝值可以和“主”内存区域里存放的不同。因此实际上存在一种可能:“主”内存区域里的i1值是1,线程1里的i1值是2,线程2里的i1值是3——这在线程1和线程2都改变了它们各自的i1值,而且这个改变还没来得及传递给“主”内存区域或其他线程时就会发生。
  而geti2()得到的是“主”内存区域的i2数值。用volatile修饰后的变量不允许有不同于“主”内存区域的变量拷贝。换句话说,一个变量经volatile修饰后在所有线程中必须是同步的;任何线程中改变了它的值,所有其他线程立即获取到了相同的值。理所当然的,volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。
  既然volatile关键字已经实现了线程间数据同步,又要synchronized干什么呢?呵呵,它们之间有两点不同。首先,synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“主”内存区域同步整个线程的内存。因此,执行geti3()方法做了如下几步:
1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放)
2. 线程内存的数据被消除,从“主”内存区域中读入(Java虚拟机能优化此步。。。[后面的不知道怎么表达,汗])
3. 代码块被执行
4. 对于变量的任何改变现在可以安全地写到“主”内存区域中(不过geti3()方法不会改变变量值)
5. 线程释放监视this对象的对象锁
  因此volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。




 
kua @ 2007-10-26 12:50

reinterpret_cast是一个非类型安全的转换符,可以将任意类型强制转换为其他类型,且实现结果编译器相关
例如:

int main() { // from [2]
    struct dat { short a; short b;};
    long value = 0xA224B118;
    dat * pd = reinterpret_cast<dat *> (&value);
    cout << pd->a;   // display first 2 bytes of value
    return 0;
}
该例中将long型转换成struct,但此代码是不可移植的,在IBM兼容机和Mac机上的运行结果完全不一样,因为他们存储字节的方式不一样。
reinterpret_cast的使用要非常的谨慎,例如:

int n = 10;
// double d = reinterptret_cast<double>(n)  编译出错,这种情况不应该使用reinterpret_cast转型,应该使用static_cast
double d = reinterptret_cast<double&>(n)
cout << d;
结果未定义,但绝对不会是10,因为reinterpret_cast只是简单执行位copy,而不管类型结构
这个与(double)int是完全不一样的,(double)int会意识到是要转型成double,因而会做一些结构的调整,例如字节的存放方式

同理:下面的例子也是未定义的
 int b[2] = {5, 6};
 double a[2] = {2.2, 1.45};
 int* p = b;
 double* d = reinterpret_cast<double*>(p);
 cout << *d;

d虽然指向了b数组的头部,但是d的类型是double*,它每次取值都是取8个字节,而b是一个int数组,因而
cout << *d, 结果是个未知数







 
kua @ 2007-10-26 12:47

C++风格的转型运算符小结:
(可参考《More Effective C++》Item M2,“尽量使用C++风格的转型)

为了改正C中丑陋的转型操作,C++中引入了四个新的转型操作符,分别是:
dynamic_cast
const_cast
static_cast
reinterpret_cast

1.dynamic_cast
这个转型操作符主要用在安全的向下转型(safe downcasting)中,也就是从基类指针/引用向派生类指针/引用的转型
当转型的两个类型明显没有继承关系时,dynamic_cast仍然可以编译通过,但编译器会给出警告信息,且执行结果未定义。
或者当明显的由父类向子类转型时,dynamic_cast仍然可以编译通过,但编译器会给出警告信息,且执行结果未定义。

当你将dynamic_cast用在指针上时,如果成功,就传回一个转型目标的指针,如果失败,则传回null指针。所以用到dynamic_cast的时候必然会导致if-then-else的程序风格,其中else就是用来检测转型失败的情况
当对引用使用dynamic_cast时,失败是引发一个异常
2.const_cast
此转型操作符用来将对象或指针的常量性(sonstness)转型掉。例如:

const A * a1;
A * a2 = const_cast<A *> (a1);

需要注意的是,const_cast转型并不总是成功的,当遇到转型的对象本身就是const的时候,那么将其常量性转型,结果未定义。3.static_cast
此转型操作符用于内建数据类型之间的转型。它与C的转型操作最接近。当没有其他适当的转型操作符可用时,就使用它。它可以将整型转换为枚举型,双精度型转换为整型,浮点型转换为长整型等等。例如:

上面的例子中,我们将一个变量从 int 转换到 double. 这些类型的二进制表达式是不同的。要将整数 9 转换到 双精度整数 9,static_cast需要正确地为双精度整数 d 补足比特位。其结果为 9.0 。

4.reinterpret_cast
此转型操作符的结果取决于编译器,用于修改操作数类型,非类型安全的转换符。这次,与3的结果有所不同。在进行计算以后,d 包含无用值。这是因为 reinterpret_cast 仅仅是复制 n 的比特位到 d,没有进行必要的分析。
reinterpret_cast最常用的领域是对函数指针的转型



 
kua @ 2007-10-26 12:34

int* p = (int*) 20;
cout << *p++;
结果应该是未定义(即运行错误)

int* p = (int*)20; 
这句语句: 将p指向一个内存地址,这个地址是20,而这个地址里面的东西未定义
cout << *p++,其实是取一个非法地址的值,运行时报错

注意:* 操作符优先级大于++
cout << *p++;的意思是先打出*p,然后p++