1
.结构体对齐的具体含义(
#pragma pack
的用法)
朋友帖了如下一段代码:
#pragma pack(4)
class TestB
{
public:
int aa;
char a;
short b;
char c;
};
int nSize = sizeof(TestB);
这里
nSize
结果为
12
,在预料之中。
现在去掉第一个成员变量为如下代码:
#pragma pack(4)
class TestC
{
public:
char a;
short b;
char c;
};
int nSize = sizeof(TestC);
按照正常的填充方式
nSize
的结果应该是
8
,为什么结果显示
nSize
为
6
呢?
事实上,很多人对
#pragma pack
的理解是错误的。
关于
struct
的使用方法
struct
是一种复合数据类型,其构成元素既可以是基本数据类型(如
int
、
long
、
float
等)的变量,也可以是一些复合数据类型(如
array
、
struct
、
union
等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(
natural alignment
)条件
分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界是指按结构体的成员中
size
最大的成员对齐。
#pragma pack
规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为
0
的地方,以后每个数据成员的对齐,按照
#pragma pack
指定的数值和结构体的自然对齐长度中比较小的那个进行。
也就是说,当
#pragma pack
的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
结构体的对齐,按照结构体中
size
最大的数据成员和
#pragma pack
指定值之间,较小的那个进行。
具体解释
#pragma pack(4)
class TestB
{
public:
int aa; //
第一个成员,放在
[0,3]
偏移的位置,
char a; //
第二个成员,自身长为
1
,
#pragma pack(4),
取小值,也就是
1
,所以这个成员按一字节对齐,放在偏移
[4]
的位置。
short b; //
第三个成员,自身长
2
,
#pragma pack(4)
,取
2
,按
2
字节对齐,所以放在偏移
[6,7]
的位置。
char c; //
第四个,自身长为
1
,放在
[8]
的位置。
};
这个类实际占据的内存空间是
9
字节
类之间的对齐,是按照类内部最大的成员的长度,和
#pragma pack
规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是
min(sizeof(int),4)
,也就是
4
。
9
按照
4
字节圆整的结果是
12
,所以
sizeof(TestB)
是
12
。
如果
#pragma pack(2)
class TestB
{
public:
int aa; //
第一个成员,放在
[0,3]
偏移的位置,
char a; //
第二个成员,自身长为
1
,
#pragma pack(4),
取小值,也就是
1
,所以这个成员按一字节对齐,放在偏移
[4]
的位置。
short b; //
第三个成员,自身长
2
,
#pragma pack(4)
,取
2
,按
2
字节对齐,所以放在偏移
[6,7]
的位置。
char c; //
第四个,自身长为
1
,放在
[8]
的位置。
};
//
可以看出,上面的位置完全没有变化,只是类之间改为按
2
字节对齐,
9
按
2
圆整的结果是
10
。
//
所以
sizeof(TestB)
是
10
。
最后看原贴:
现在去掉第一个成员变量为如下代码:
#pragma pack(4)
class TestC
{
public:
char a;//
第一个成员,放在
[0]
偏移的位置,
short b;//
第二个成员,自身长
2
,
#pragma pack(4)
,取
2
,按
2
字节对齐,所以放在偏移
[2,3]
的位置。
char c;//
第三个,自身长为
1
,放在
[4]
的位置。
};
//
整个类的大小是
5
字节,按照
min(sizeof(short),4)
字节对齐,也就是
2
字节对齐,结果是
6
//
所以
sizeof(TestC)
是
6
。
在
Linux
下面就是
#define __PACKED_ATTR __attribute__ ((__packed__))
typedef struct {
char p[3] __PACKED_ATTR;
long i __PACKED_ATTR;
} test ;
typedef struct {
char p[3];
long i;
} test1;
gcc test.c
编译后,它们的大小就是
7,8
了
windows
下面默认的是
#pragma pack
(
8
)
因为编译器在编译时会对程序进行优化,以便加快访问速度,所以一般都会按照
2
的倍数进行字节对齐。用这个宏就是为了防止编译器对结构的定义进行对齐。
#pragma(push,n)
用来设置警告消息的等级
2
.
堆和堆栈的区别
一个由
c/C++
编译的程序占用的内存分为以下几个部分
1
、栈区(
stack
)
—
由编译器自动分配释放
,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2
、堆区(
heap
)
—
一般由程序员分配释放,
若程序员不释放,程序结束时可能由
OS
回收
。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3
、全局区(静态区)(
static
)
—
,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,
未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
-
程序结束后由有系统释放
4
、文字常量区
—
常量字符串就是放在这里的。
程序结束后由系统释放
5
、程序代码区
—
存放函数体的二进制代码。
二、堆和栈的理论知识
2.1
申请方式
stack:
由系统自动分配。
例如,声明在函数中一个局部变量
int b;
系统自动在栈中为
b
开辟空间
heap:
需要程序员自己申请,并指明大小,在
c
中
malloc
函数
如
p1 = (char *)malloc(10);
在
C++
中用
new
运算符
如
p2 = (char *)malloc(10);
但是注意
p1
、
p2
本身是在栈中的。
2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的
delete
语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3
申请大小的限制
栈:在
Windows
下
,
栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在
WINDOWS
下,栈的大小是
2M
(也有的说是
1M
,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示
overflow
。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4
申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由
new
分配的内存,一般速度比较慢,而且容易产生内存碎片
,
不过用起来最方便
.
另外,在
WINDOWS
下,最好的方式是用
VirtualAlloc
分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
2.5
堆和栈中的存储内容
栈:
在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的
C
编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
(
经典!
)
3
.关于静态函数调用非静态成员的情况的总结
class a
{
a(){}
~a(){}
public:
static a* makeA(){return new a();}
}
请问以上代码是否成立
一个类的静态成员函数不能像非静态成员函数那样
“
默认调用
”
它的非静态成员函数
(
因为静态成员函数没有隐含的
this
参数)。
在一个类的静态成员函数中,只要通过某种方式得到了一个指向本类型的对象的指针,并
且有合适的
access level,
就可以对此对象调用其非静态成员函数
我认为这段代码能够成立的原因不在于
new
操作符的某个特性,因为事实上你把
makeA
函数换成
static a fA(){ return a(); }
也是可以
pass
的。
class a
{
a(){}
~a(){}
public:
static a* MakeA(){ return new a(); }
static a fA(){ return a(); }
};
我对于这个问题的非专业理解是这样的:
static
函数没有隐含的
this
指针输入这一因素导
致
static
函数不能访问
“
需要
this
指针的函数
”
。构造函数虽然看上去不是静态函数,但
是这是个特殊函数
——this
指针并不出现在它的参数列表里面,因为
this
指针是在构造函
数调用之后、成员变量初始化之前生成的。因此,构造函数也是一个
“
不需要
this
指针的
函数
”
。这一调用就可以通过编译并且无误运行。
比较构造函数和析构函数的调用,很容易发现如下语句的正误:
a(); //
对的
~a(); //
错的,需要一个对象,或者说是
this
指针
this->a(); //
错的,不需要
this
指针
this->~a(); //
对的
其实操作符
new()
和
delete()
都是类的静态成员,他们被自动转化为静态成员,
而无需程序员显式的声明。
具体细节可见
c++ primer
有两类
new
操作符
,
不要混淆了:
1) void ::operator new(size_t)
是一个全局函数;
2)
如果一个类重载了
opertor new(),
它重载的这个
opertor new()
就自动成为类的静态成
员函数(即使不显式写出
static
关键字)。
4
.使用句柄捆绑一个对象的时候涉及到值语义还是指针语义问题
当句柄
1
捆绑一个对象的时候,通过句柄
1
的操作当然会影响到捆绑对象的内容,但是如果将句柄
1
赋给句柄
2
,修改句柄
2
,是否影响句柄
1
所捆绑的对象的内容?
这需要看设计者的想法
设计句柄拷贝构造函数的时候是直接将源的数据对象指针直接赋给了目的句柄的对象指针避免了对象复制带来的开销,但是会增加相应的引用计数加
1
;
在涉及到试图修改对象
(
数据成员
)
的接口的时候,
基于指针语义的方法是,直接用该数据对象指针修改这样,一个对象被多个句柄捆绑的时候,修改任何一个都会改变该对象的值;具有较好的时间效率
基于值语义的方法是,禁止本句柄捆绑的对象被其它句柄所引用,即保证本句柄的对象的引用计数的保持不变为
1
。在修改对象的接口函数中,可以先判断对象的引用计数值,当不是
1
的时候
(
发生了句柄的赋值
)
要单独创建新的对象同时将计数值减
1
,然后再在这个新的对象上面做修改。这种方法即为
写时复制
由于不可改变的对象
(
比如一些临时的副本
)
的操作类似于值,写时复制技术时针对可变对象的一种优化技巧,它引入了复制的开销,但是获得了直观的操作方法:向一个句柄赋值就是抛弃旧对象,绑定新对象
5
.
重载与覆盖
,
隐藏
(zt)
重载与覆盖
成员函数被重载的特征:
(
1
)相同的范围(在同一个类中);
(
2
)函数名字相同;
(
3
)参数不同;
(
4
)
virtual
关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
(
1
)不同的范围(分别位于派生类与基类);
(
2
)函数名字相同;
(
3
)参数相同;
(
4
)基类函数必须有
virtual
关键字。
“
隐藏
”
是指派生类的函数屏蔽了与其同名的基类函数,
规则如下:
(
1
)如果派生类的函数与基类的函数同名,但是参数不同。
此时,不论有无
virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。
(
2
)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有
virtual
关键字。
此时,基类的函数被隐藏(注意别与覆盖混淆)。
如下示例程序中:
(
1
)函数
Derived::f(float)
覆盖了
Base::f(float)
。
(
2
)函数
Derived::g(int)
隐藏了
Base::g(float)
,而不是重载。
(
3
)函数
Derived::h(float)
隐藏了
Base::h(float)
,而不是覆盖。
#include<iostream.h>
class Base{
public:
virtual void f(floatx){cout<<"Base::f(float)"<<x<<endl;}
void g(floatx){cout<<"Base::g(float)"<<x<<endl;
void h(floatx){cout<<"Base::h(float)"<<x<<endl;}
};
class Derived:publicBase{
public:
virtual void f(floatx){cout<<"Derived::f(float)"<<x<<endl;}
void g(intx){cout<<"Derived::g(int)"<<x<<endl;}
void h(floatx){cout<<"Derived::h(float)"<<x<<endl;}
};
void main(void){
Derived d;
Base *pb=&d;
Derived *pd=&d;
//Good:behavior depends solely on type of the object
pb->f(3.14f); //Derived::f(float)3.14
pd->f(3.14f); //Derived::f(float)3.14
//Bad:behavior depends on type of the pointer
pb->g(3.14f); //Base::g(float)3.14
pd->g(3.14f); //Derived::g(int)3(surprise!)
//Bad:behavior depends on type of the pointer
pb->h(3.14f); //Base::h(float)3.14(surprise!)
pd->h(3.14f); //Derived::h(float)3.14
6
.
描述内存分配方式以及它们的区别。
(Autodesk , Microsoft)
答案:
(1
)
从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,
static
变量。
(
2
)
在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
(
3
)
从堆上分配,亦称动态内存分配。程序在运行的时候用
malloc
或
new
申请任意多少的内存,程序员自己负责在何时用
free
或
delete
释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
数组只能定义在静态存取区域比如全局数组,或者定义在栈上,但是指针可以指向任意的内存区域。
修改内容上的差别
char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; //
注意
p
指向常量字符串
p[0] = ‘X’; //
编译器不能发现该错误,运行时错误
用运算符
sizeof
可以计算出数组的容量(字节数)。
sizeof(p),p
为指针得到的是一个指针变量的字节数,而不是
p
所指的内存容量。
C++/C
语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12
字节
cout<< sizeof(p) << endl; // 4
字节
计算数组和指针的内存容量
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4
字节而不是
100
字节
}
描述一下
C++
的多态
(
microsoft
)
答案:
C++
的多态表现在两个部分
,一个是静态连编下的函数重载,运算符重载;
动态连编下的虚函数、纯虚函数(抽象类)
如何打印出当前源文件的文件名以及源文件的当前行号?
答案:
cout << __FILE__ ;
cout<<__LINE__ ;
__FILE__
和
__LINE__
是系统预定义宏,这种宏并不是在某个文件中定义的,而是由编译器定义的。
main
主函数执行完毕后,是否可能会再执行一段代码,给出说明?
答案:可以,可以用
_onexit
注册一个函数,它会在
main
之后执行
int fn1(void), fn2(void), fn3(void), fn4 (void);
void main( void )
{
String str("zhanglin");
_onexit( fn1 );
_onexit( fn2 );
_onexit( fn3 );
_onexit( fn4 );
printf( "This is executed first.\n" );
}
int fn1()
{
printf( "next.\n" );
return 0;
}
int fn2()
{
printf( "executed " );
return 0;
}
int fn3()
{
printf( "is " );
return 0;
}
int fn4()
{
printf( "This " );
return 0;
}
如何判断一段程序是由
C
编译程序还是由
C++
编译程序编译的?
答案
:
#ifdef __cplusplus
cout<<"c++";
#else
cout<<"c";
#endif
文件中有一组整数
,
要求排序后输出到另一个文件中
答案:
void Order(vector<int> &data) //
起泡排序
{
int count = data.size() ;
int tag = false ;
for ( int i = 0 ; i < count ; i++)
{
for ( int j = 0 ; j < count - i - 1 ; j++)
{
if ( data[j] > data[j+1])
{
tag = true ;
int temp = data[j] ;
data[j] = data[j+1] ;
data[j+1] = temp ;
}
}
if ( !tag )
break ;
}
}
void main( void )
{
vector<int>data;
ifstream in("c:\\data.txt");
if ( !in)
{
cout<<"file error!";
exit(1);
}
int temp;
while (!in.eof())
{
in>>temp;
data.push_back(temp);
}
in.close();
Order(data);
ofstream out("c:\\result.txt");
if ( !out)
{
cout<<"file error!";
exit(1);
}
for ( i = 0 ; i < data.size() ; i++)
out<<data[i]<<" ";
out.close();
}
7.
栈对象,堆对象和静态对象的区别
栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用
operator new
操作,
operator new
会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的
,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是
1MB
~
2MB
,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。
堆对象,其产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。
C
语言里面的
static
变量的作用
(1)
用于全局变量:外部静态变量,只能在本源文件中被引用,不能被其它源文件所引用。
(2)
用于局部变量:局部静态变量,在函数返回后存储单元不释放;下一次调用该函数时,该变量为上次函数返回时的值。
(3)
用于函数:内部函数,只能被本源文件中的函数所调用,不能被其它源文件调用。
接下来看看
C++
里面的
static
对象的作用。
首先是全局静态对象。全局对象为类间通信和函数间通信提供了一种最简单的方式,虽然这种方式并不优雅。一般而言,在完全的面向对象语言中,是不存在全局对象的,比如
C#
,因为全局对象意味着不安全和高耦合,在程序中过多地使用全局对象将大大降低程序的健壮性、稳定性、可维护性和可复用性。
C++
也完全可以剔除全局对象,但是最终没有,我想原因之一是为了兼容
C
。
其次是类的静态成员,上面已经提到,基类及其派生类的所有对象都共享这个静态成员对象,所以当需要在这些
class
之间或这些
class objects
之间进行数据共享或通信时,这样的静态成员无疑是很好的选择。
接着是局部静态对象,主要可用于保存该对象所在函数被屡次调用期间的中间状态,其中一个最显著的例子就是递归函数,我们都知道递归函数是自己调用自己的函数,如果在递归函数中定义一个
nonstatic
局部对象,那么当递归次数相当大时,所产生的开销也是巨大的。这是因为
nonstatic
局部对象是栈对象,每递归调用一次,就会产生一个这样的对象,每返回一次,就会释放这个对象,而且,这样的对象只局限于当前调用层,对于更深入的嵌套层和更浅露的外层,都是不可见的。每个层都有自己的局部对象和参数。
在递归函数设计中,可以使用
static
对象替代
nonstatic
局部对象(即栈对象),
这不仅可以减少每次递归调用和返回时产生和释放
nonstatic
对象的开销
,而且
static
对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
调用约定(
Calling Convention
)是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。
在
C++
中,为了允许操作符重载和函数重载,
C++
编译器往往按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于
C
的链接器。这项技术通常被称为名称改编(
Name Mangling
)或者名称修饰(
Name Decoration
)。许多
C++
编译器厂商选择了自己的名称修饰方案。
因此,为了使其它语言编写的模块(如
Visual Basic
应用程序、
Pascal
或
Fortran
的应用程序等)可以调用
C/C++
编写的
DLL
的函数,必须使用正确的调用约定来导出函数,并且不要让编译器对要导出的函数进行任何名称修饰。
1
.调用约定(
Calling Convention
)
调用约定用来处理决定函数参数传送时入栈和出栈的顺序(由调用者还是被调用者把参数弹出栈),以及编译器用来识别函数名称的名称修饰约定等问题。在
Microsoft VC++ 6.0
中定义了下面几种调用约定,我们将结合汇编语言来一一分析它们:
1
、
__cdecl
__cdecl
是
C/C++
和
MFC
程序默认使用的调用约定,也可以在函数声明时加上
__cdecl
关键字来手工指定。采用
__cdecl
约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用
__cdecl
约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。
__cdecl
可以写成
_cdecl
。
下面将通过一个具体实例来分析
__cdecl
约定:
在
VC++
中新建一个
Win32 Console
工程,命名为
cdecl
。其代码如下:
int __cdecl Add(int a, int b); //
函数声明
void main()
{
Add(1,2); //
函数调用
}
int __cdecl Add(int a, int b) //
函数实现
{
return (a + b);
}
函数调用处反汇编代码如下:
;Add(1,2);
push 2 ;
参数从右到左入栈,先压入
2
push 1 ;
压入
1
call @ILT+0(Add) (00401005) ;
调用函数实现
add esp,8 ;
由函数调用清栈
2
、
__stdcall
__stdcall
调用约定用于调用
Win32 API
函数。采用
__stdcal
约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条
ret n
指令直接清理传递参数的堆栈。
__stdcall
可以写成
_stdcall
。
还是那个例子,将
__cdecl
约定换成
__stdcall
:
int __stdcall Add(int a, int b)
{
return (a + b);
}
函数调用处反汇编代码:
; Add(1,2);
push 2 ;
参数从右到左入栈,先压入
2
push 1 ;
压入
1
call @ILT+10(Add) (0040100f) ;
调用函数实现
函数实现部分的反汇编代码:
;int __stdcall Add(int a, int b)
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
;return (a + b);
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;
清栈
3
、
__fastcall
__fastcall
约定用于对性能要求非常高的场合。
__fastcall
约定将函数的从左边开始的两个大小不大于
4
个字节(
DWORD
)的参数分别放在
ECX
和
EDX
寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。
__fastcall
可以写成
_fastcall
。
依旧是相类似的例子,此时函数调用约定为
__fastcall
,函数参数个数增加
2
个:
int __fastcall Add(int a, double b, int c, int d)
{
return (a + b + c + d);
}
函数调用部分的汇编代码:
;Add(1, 2, 3, 4);
push 4 ;
后两个参数从右到左入栈,先压入
4
mov edx,3 ;
将
int
类型的
3
放入
edx
push 40000000h ;
压入
double
类型的
2
push 0
mov ecx,1 ;
将
int
类型的
1
放入
ecx
call @ILT+0(Add) (00401005) ;
调用函数实现
函数实现部分的反汇编代码:
; int __fastcall Add(int a, double b, int c, int d)
push ebp
mov ebp,esp
sub esp,48h
push ebx
push esi
push edi
push ecx
lea edi,[ebp-48h]
mov ecx,12h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-8],edx
mov dword ptr [ebp-4],ecx
;return (a + b + c + d);
fild dword ptr [ebp-4]
fadd qword ptr [ebp+8]
fiadd dword ptr [ebp-8]
fiadd dword ptr [ebp+10h]
call __ftol (004011b8)
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 0Ch ;
清栈
关键字
__cdecl
、
__stdcall
和
__fastcall
可以直接加在要输出的函数前,也可以在编译环境的
Setting...->C/C++->Code Generation
项选择。它们对应的命令行参数分别为
/Gd
、
/Gz
和
/Gr
。缺省状态为
/Gd
,即
__cdecl
。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。
4
、
thiscall
thiscall
调用约定是
C++
中的非静态类成员函数的默认调用约定。
thiscall
只能被编译器使用,没有相应的关键字,因此不能被程序员指定。采用
thiscall
约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,只是另外通过
ECX
寄存器传送一个额外的参数:
this
指针。
这次的例子中将定义一个类,并在类中定义一个成员函数,代码如下:
class CSum
{
public:
int Add(int a, int b)
{
return (a + b);
}
};
void main()
{
CSum sum;
sum.Add(1, 2);
}
函数调用部分汇编代码:
;CSum sum;
;sum.Add(1, 2);
push 2 ;
参数从右到左入栈,先压入
2
push 1 ;
压入
1
lea ecx,[ebp-4] ;ecx
存放了
this
指针
call @ILT+5(CSum::Add) (0040100a) ;
调用函数实现
函数实现部分汇编代码:
;int Add(int a, int b)
push ebp
mov ebp,esp
sub esp,44h ;
多用了一个
4bytes
的空间用于存放
this
指针
push ebx
push esi
push edi
push ecx
lea edi,[ebp-44h]
mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
pop ecx
mov dword ptr [ebp-4],ecx
;return (a + b);
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8 ;
清栈
5
、
naked
属性
采用上面所述的四种调用约定的函数在进入函数时,编译器会产生代码来保存
ESI
、
EDI
、
EBX
、
EBP
寄存器中的值,退出函数时则产生代码恢复这些寄存器的内容。对于定义了
naked
属性的函数,编译器不会自动产生这样的代码,需要你手工使用内嵌汇编来控制函数实现中的堆栈管理。由于
naked
属性并不是类型修饰符,故必须和
__declspec
共同使用。下面的这段代码定义了一个使用了
naked
属性的函数及其实现:
__declspec ( naked ) func()
{
int i;
int j;
_asm
{
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
}
_asm
{
mov esp, ebp
pop ebp
ret
}
}
naked
属性与本节关系不大,具体请参考
MSDN
。
6
、
WINAPI
还有一个值得一提的是
WINAPI
宏,它可以被翻译成适当的调用约定以供函数使用。该宏定义于
windef.h
之中。下面是在
windef.h
中的部分内容:
#define CDECL _cdecl
#define WINAPI CDECL
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define APIENTRY WINAPI
由此可见,
WINAPI
、
CALLBACK
、
APIENTRY
等宏的作用。
9.
引用和指针的区别
1.
引用必须被初始化为指向一个对象
,
并不能在指向其他对象
.
指针可以指向一系列不同的对象
,
也可以什么都不指向
.
2.
指针可以指向空对象
,
但是增加了安全隐患
.
但是没有指向空对象的引用
3.
当我们不一定要指向有效对象时只能使用指针
.
4.
引用比指针有更好的操作性,即使用起来更加直观
.
如
: class ywls{....};
ywls operator+(ywls &obj1,ywls &obj2)
{ywls result;....return result}
此处的参数传递最适合用引用
,
即快又方便
.
10
.
hash_table Or binary_search_tree
hash_table
和二叉搜索树都经常被用来构建符号表(或者字典)以及相关的结构,并且他们都表现出了很高的效率。最近也在不同的程序中使用了这两种数据结构,实现完毕后思考一下,对两者做了一个简单的比较:
1
)
二叉搜索树是基于
比较
的原则实现,而
hash_table
则采用
关键字索引
的原则实现的。
2
)
hash_table
的索引结构使得对元素的
插入和查找
独立于表结构大小,是一个常量时间,具有
非常高
的效率。但是二叉搜索树的结构也使得插入和查找的
效率很高
。
3
)
由
2
)看似乎
hash_table
是符号表的最佳构建方式,但是
hash_table
也有自己的
局限性
。实际上
hash_table
是利用
空间在换取时间
,
hash_table
中可能使得一些空间被浪费掉(没有
hash
到该元素),并且
表结构大小一开始就是固定的
。另外对于
hash
函数的选择可能使得出现较多的冲突,影响了效率。不过,
hash_table
在
时间和空间的平衡
上还是比较突出的。
4
)
hash_table
的另外一个局限性就是,不利于支持扩展的操作,例如
排序
。二叉搜索树中的中序遍历就可以得到全部元素的有序序列,而
hash_table
则不是很容易支持这种操作。再比如
选择
操作,在二叉搜索树中寻找第
k
大元素很容易实现,但在
hash_table
中则要求重新排序得到。