一、内存对齐
1、什么是字节对齐,为什么要对齐?
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。显然在读取效率上下降很多。
2、请看下面的结构:
struct MyStruct
{
double dda1;
char dda;
int type;
};
对结构MyStruct采用sizeof会出现什么结果呢?sizeof(MyStruct)为多少呢?也许你会这样求:
sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13
但是当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。你知道为什么在VC中会得出这样一个结果吗?
其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式(vc6.0,32位系统)。
类型
对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char
偏移量必须为sizeof(char)即1的倍数
int
偏移量必须为sizeof(int)即4的倍数
float
偏移量必须为sizeof(float)即4的倍数
double
偏移量必须为sizeof(double)即8的倍数
Short
偏移量必须为sizeof(short)即2的倍数
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
下面用前面的例子来说明VC到底怎么样来存放结构的。
struct MyStruct
{
double dda1;
char dda;
int type
};
为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式,先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用 sizeof(char)=1个字节;接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof (int)=4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int)=4的倍数,所以把type存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。所以整个结构的大小为:sizeof(MyStruct)=8+1+ 3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。
下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况:
struct MyStruct
{
char dda;
double dda1;
int type
};
这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(MyStruc)为24。结合上面提到的分配空间的一些原则,分析下VC怎么样为上面的结构分配空间的。(简单说明)
struct MyStruct
{
char dda; //偏移量为0,满足对齐方式,dda占用1个字节;
double dda1;//下一个可用的地址的偏移量为1,不是sizeof(double)=8
//的倍数,需要补足7个字节才能使偏移量变为8(满足对齐
//方式),因此VC自动填充7个字节,dda1存放在偏移量为8
//的地址上,它占用8个字节。
int type; //下一个可用的地址的偏移量为16,是sizeof(int)=4的倍
//数,满足int的对齐方式,所以不需要VC自动填充,type存
//放在偏移量为16的地址上,它占用4个字节。
};//所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构
//的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof
//(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为
//sizeof(double)=8的倍数。
所以该结构总的大小为:sizeof(MyStruc)为1+7+8+4+4=24。其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。
VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。
VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;
否则必须为n的倍数。下面举例说明其用法。
#pragma pack(push) //保存对齐状态
#pragma pack(4)//设定为4字节对齐
struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop)//恢复对齐状态
以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为 m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragma pack(4)改为#pragma pack(16),那么我们可以得到结构的大小为24。(请读者自己分析)
3、再看下面这个例子
#pragma pack(8)
struct S1{
char a;
long b;
};
struct S2 {
char c;
struct S1 d;
long long e;
};
#pragma pack()
sizeof(S2)结果为24.
成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐.
也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.
S1中,成员a是1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;
S2 中,c和S1中的a一样,按1字节对齐,而d 是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是按4字节对齐.成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e.这时,长度为24,已经可以被8(成员e按8字节对齐)整除.这样,一共使用了24个字节.
a b
S1的内存布局:11**,1111,
c S1.a S1.b d
S2的内存布局:1***,11**,1111,****11111111
这里有三点很重要:
1.每个成员分别按自己的方式对齐,并能最小化长度。
2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。
Win32平台下的微软 编译器(cl.exe for 80×86)的对齐策略:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。
内存对齐内容参考自:http://hi.baidu.com/asmsky/item/a8b94d4a918acb0b6cc2f076
二、内联函数
内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看编译器对该函数定义的具体处理。
1、内联函数的作用
消除函数调用时的时间开销。
2、内联函数与宏
功能与预处理宏功能相似。
函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。而宏只是在预处理的地方把代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个函数更有效率。
宏不能访问对象的私有成员。宏容易产生二义性(参考本文第四部分)。
3、注意事项
内联函数要有效果必须定义函数体。
inline func(void); 无效
inline func(void) { return 0; } 有效
在C++,类的内部定义的函数默认为内联函数,不管有无inline关键字。
内联函数如果函数体过大,编译器会放弃内联方式,而采用普通函数的方式调用。
三、const用法
1、声明常量
const int a=1;
a=2; (错)
2、初始化
const int a;(错,必须初始化)
3、const修饰指针
const int* a=&[1]
int const *a=&[2]
int* const a=&[3]
const int* const a=&[4]
如果const位于*号的左侧,即指针指向为常量;
如果const位于*号的右侧,则指针为常量;
[1]和[2]情况相同,这种情况下不允许对内容进行更改操作,如*a=3是错的;
[3]指针为常量,所以不能对指针本身进行更改操作,如++a是错误的;
[4]指针和内容均为常量,不能对内容和指针本身进行更改操作。
4、const作为函数返回值
则函数不能作为左值
5、修饰类成员函数
不能修改数据成员,一般用来读取某个数据成员的值;若调用非const成员函数亦会出错。
6、修饰函数参数
函数体中不能修改参数的值
7、void func(void);
void func(void) const;
算是重载函数,这两个函数不属于相同函数。
四、sizeof 与 const对比
1、数组
char arr[10]="what";
strlen(arr) 值为:4
sizeof(arr) 值为:10
2、指针
char *str="0123456789"
strlen(arr) 值为:10
sizeof(arr) 值为:4(4为指针所占大小)
3、注意情况
char *str=new char[10[;
strlen(str)值为:不固定
sizeof(str)值为:4
sizeof(*str)值为:1
五、define浅析
关于define调用请参考第一部分第2小点。
在C或C++中可以用#define来定义宏,在编译预处理时会进行宏替换。
1、注意点
宏展开时是直接进行替换操作,以下可能产生误解:
(1)#define SQUARE(a) a*a
(2)#define SQUARE(a) (a*a)
(3)#define SQUARE(a) (a)*(a)
(4)#define SQUARE(a) ((a)*(a))
int main() { printf("square:%d\n",SQUARE(3+5)*2); return 0; }
第一种情况是:square:28
第二种情况是:square:46
第三种情况是:square:128
第四种情况是:square:128
显然第一、二种是错误的。
而第三种只是这种情况下刚好成立,在很多其他的情况下这种定义也是错误的。
第四种才是正确的宏定义。
2、define宏 与 typedef 区别
宏定义只是简单的字符串替换,是在预处理完成的;
而typedef则是在编译时处理的,它不是作简单的替换,而是对类型说明符重新命名;被命名的标识符具有类型定义说明功能。
#define PIN1 int*
typedef (int*) PIN2;
从形式上看,这两者类似;但在实际应用中却不相同。
下面用PIN1,PIN2说明变量时就可以看出它们的区别:
PIN1 a,b;
在宏替换后变成:
int *a,b;
表示a是指向整形的指针变量,而b是整型变量。
然后:
PIN2 a,b;
表示a,b都是指向整型的指针变量;因为PIN2是一个类型说明符
五、一些类型修饰符
1、static用法
static内容参考此处:http://blog.csdn.net/xiaocai0001/article/details/662921
static关键字是C、C++中都存在的关键字,它主要有三种使用方式:
1)局部静态变量
2)外部静态变量/函数
3)静态数据成员/成员函数(针对于C++)
(1) 局部静态变量
在C/C++中, 局部变量按照存储形式可分为三种auto, static, register。
与auto类型(普通)局部变量相比, static局部变量有三点不同:
1)存储空间分配不同
auto类型分配在栈上,函数调用结束后自动释放;而static分配在静态存储区,在程序整个运行期间都不会释放直到程序结束。
2)static局部变量在所处模块在初次运行时进行初始化工作,且只操作一次
3)对于局部静态变量,如果不赋初值,编译期会自动初值0或空字符,而auto类型则是不确定的。
特点: static局部变量的“记忆性”与生存期的“全局性”
所谓“记忆性”是指在两次函数调用时,在第二次调用进入时,能保持第一次调用退出时的值。
#include <stdio.h> void staticLocalVar() { static int a = 0; // 运行期时初始化一次, 下次再调用时, 不进行初始化工作 printf("a=%d\n",a); ++a; } int main() { staticLocalVar(); // 第一次调用, 输出a=0 staticLocalVar(); // 第二次调用, 记忆了第一次退出时的值, 输出a=1 return 0; }
(2) 外部静态成员/函数
在C中static有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。但为了限制全局变量/函数的作用域,函数或变量前加static使得函数成为静态函数。但此处"static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。注意此时,对于外部(全局)变量,不论是否有static限制,它的存储区域都是在静态存储区,生存期都是全局的。此时的static只是起作用域限制作用,限定作用域在本模块(文件)内部。
使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。
示例:
//file1.cpp
static int varA;
int varB;
extern void funA(){...}
static void funB() {...}
//file2.cpp
extern int varB; // 使用file1.cpp中定义的全局变量
extern int varA; // 错误! varA是static类型, 无法在其他文件中使用
extern vod funA(); // 使用file1.cpp中定义的函数
extern void funB(); // 错误! 无法使用file1.cpp文件中static函数
(3)静态数据成员/成员函数(C++特有)
C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数. 这是与普通成员函数的最大区别, 也是其应用所在, 比如在对某一个类的对象进行计数时, 计数生成多少个类的实例, 就可以用到静态数据成员. 在这里面, static既不是限定作用域的, 也不是扩展生存期的作用, 而是指示变量/函数在此类中的唯一性. 这也是”属于一个类而不是属于此类的任何特定对象的变量和函数”的含义. 因为它是对整个类来说是唯一的, 因此不可能属于某一个实例对象的. (针对静态数据成员而言, 成员函数不管是否是static, 在内存中只有一个副本, 普通成员函数调用时, 需要传入this指针, static成员函数调用时, 没有this指针. )
请看示例程序四(<effective c++ (2nd)>(影印版)第59页)
class EnemyTarget{ public: EnemyTarget() { ++numTargets; } EnemyTarget(const EnemyTarget&) { ++numTargets; } ~EnemyTarget() { --numTargets; } static size_t numberOfTargets() { return numTargets; } bool destroy(); // returns success of attempt to destroy EnemyTarget object private: static size_t numTargets; // object counter }; // class statics must be defined outside the class; // initialization is to 0 by default size_t EnemyTarget::numTargets;
在这个例子中, 静态数据成员numTargets就是用来计数产生的对象个数的。
另外,在设计类的多线程操作时, 由于POSIX库下的线程函数pthread_create()要求是全局的, 普通成员函数无法直接做为线程函数, 可以考虑用Static成员函数做线程函数。
2、extern用法
extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。另外,extern也可用来进行链接指定。
具体用法参考static用法的第2部分。
3、volatile
volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。
推荐一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面是volatile变量的几个例子:
1)并行设备的硬件寄存器(如:状态寄存器)
2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3)多线程应用中被几个任务共享的变量
六、函数指针
首先,函数指针是什么意思?
假定一个指针指向一个int变量,它保存着这个int变量在内存中在存储的地址。同样,函数也有地址,这是因为函数的机器语言实现是由载入到内存的代码组成。指向函数的指针中保存着函数代码起始处的地址。
其实,当声明一个数据指针时,必须声明它指向的数据的类型。当声明一个函数指针时,必须声明它指向的函数类型。要指定函数类型,就要指出函数的返回类型以及函数的参量类型。例如,考虑以下原型:
void ToUpper (char* );
函数ToUpper()的类型就是:具有char*类型的参量,返回类型是void的函数。
要声明指向这种类型的函数的指针pf,可以这样做:
void (*pf) (char* );
由于有运算符优先级的规则,所以第一个圆括号是必需的。省略掉圆括号会导致完全不同的情况:
void *pf (char* ); //pf是返回一个指针的函数
void ToUpper (char* ); void ToLower (char* ); int round (double); void (*pf) (char* ); char mis[] = "morphling.huang"; pf = ToUpper; //合法,ToUpper是函数ToUpper()的地址 pf = ToLower; //合法,ToUpper是函数ToLower()的地址 pf = round; //无效,round是错误类型的函数 pf = ToLower(); //无效,ToLower()是是地址 pf = ToUpper; (*pf)(mis); //把ToUpper作用于mis(语法1) pf = ToLower; pf(mis); //把ToLower作用于mis(语法2)
语法1与语法2写法都是合法的。
历史上,贝尔实验室的C和UNIX的开发者采用第一种观点,而Berkeley的UNIX的扩展者采用第二种观点。K&R C不允许第二种形式。
但是为了保持与现有代码的兼容性,ANSI C把这二者作为等价形式全部接受。
#include "stdio.h" int add1(int a1,int b1); int add2(int a2,int b2); int main(int argc,char* argv[]) { int numa1=1,numb1=2; int numa2=2,numb2=3; int (*op[2])(int a,int b); int (*fun)(int a,int b); op[0]=add1; op[1]=add2; printf("%d %d\n",(*op[0])(numa1,numb1),(*op[1])(numa2,numb2)); //输出结果:3 5 printf("%d %d\n",op[0](numa1,numb1),op[1](numa2,numb2)); //输出结果:3 5 fun=add1; printf("%d\n",fun(numa1,numb1)); //输出结果:3 fun=add2; printf("%d\n",(*fun)(numa2,numb2)); //输出结果:5 } int add1(int a1,int b1){ return a1+b1; } int add2(int a2,int b2){ return a2+b2; }