坚持学习/暴露问题/不断提升

c++/设计模式/算法结构/系统
posts - 2, comments - 20, trackbacks - 0, articles - 0

2011年8月10日

本篇摘要

  交换两个变量是非常古老的话题了,然而本文绝对保证给你新鲜的感觉!本文涉及到最简单的“不用临时变量交换两个整数”还涉及到如果利用异或来实现两个指针、两个浮点数的交换,要知道指针的浮点数是不允许直接异或运算的哦;同时本文还阐述了如何交换用户自定义类型及其指针。

本文完全是个人自由发挥之作,欢迎广大砖家来拍砖,我个人感觉文中必然有很多不足,甚至错误之处,这并非我谦虚,事实上我写本文的目的就是希望在挨砖板中成长!新人或许看不懂很多东西,如果你看不懂,那么就不要随便膜拜,因为看不懂的或许原本就是错的,高手看完后如果本文写得还可以,那么请留下好评,以供新手参考本文是否有阅读本文的必要,如果觉得本文完全是垃圾,那么请不要客气,您可以赤裸地,露骨地指出本文的错误和不足,让本人在批评中进步,但请不要进行人身攻击,谢谢!

准备工作

  由于本文涉及到交换两个用户自定义类型的变量,为了举例方便,本文定义如下的Person类(其中省略了拷贝构造函数的重写,因为本文不用到它):

class Person
{
public:
         Person(
int age ,const char* name ):m_Age(age)
         {
                   
int len = strlen(name);
                   
this->m_Name = new char[len+1];
                   strcpy(
this->m_Name,name);
         }
         Person()
         {
                   
this->m_Age = -1;
                   
this->m_Name = 0;
         }
         
void PrintSelf()
         {
                   cout
<<this->m_Name<<":"<<this->m_Age<<endl;
         }
         Person
& operator= (const Person& other)
         {
                   
if (this == &other)
                   {
                            
return *this;
                   }
                   
else
                   {
                            
this->m_Age = other.m_Age;
                            delete 
this->m_Name;
                            
int len = strlen(other.m_Name);
                            
this->m_Name = new char[len+1];
                            strcpy(
this->m_Name,other.m_Name);
                            
return *this;
                   }
         }
         
~Person()
         {
                   delete 
this->m_Name;
         }
private:
         
int m_Age;
         
char* m_Name;
};

  为了后文表述方便,这里再定义Person类的两个对象和两个指针,定义如下:

Person youngMan(18,” young man”);

Person oldMan(81,” old man”);

Person* pYoungMan = &youngMan;

Person* pOldMan = &oldMan;

最常见的交换两个对象的方法:GeneralSwap

通常,我们为了交换两个变量都采取下面的方法来实现,它需要一个临时变量:

template<class T>
void GeneralSwap(T& a,T& b)
{
         T temp;
         temp 
= a;
         a 
= b;
         b 
= temp;
}

    显然人人都知道这个写法,但是我仍然觉得有必要重点申明几点:1、注意函数的参数是引用(也可指针),为什么我就不解释了;2、这个交换函数基本上是最简单、最通用的,简单到人人都会写,通用到它几乎可适用于任何数据类型:char , int , long , float, double等各种系统自定义数学类型(无符号的,带符号的),用户自定义数据类型(需要有默认构造函数,否则语句T temp;会报错),以及各种指针(系统自定义类型的指针,和用户自定义类型的指针)。当然用户自定义类型中如果包含了指针数据成员,那么需要重载赋值运算符,事实上这样的用户自定义类,你都应该自己重写赋值运算符、拷贝构造函数,否则不但不能使用GeneralSwap,其他涉及到拷贝和赋值的操作都可能导致出错!

利用GeneralSwap交换两个用户自定义对象

    下面深入探讨一下关于用户自定义对象的交换问题:针对准备工作中的Person类的两个对象youngMan和oldMan语句GeneralSwap(youngMan,oldMan);能实现他们的交换。短短一行代码就能实现将一个18岁的花季少男跟一个81岁的老头子掉包,这像不像是耍魔术啊,呵呵。要注意了,该交换代码虽短,但涉及到默认构造函数的调用(GeneralSwap中的T temp;语句)和赋值运算符重载函数的调用(GeneralSwap中的三个赋值语句)。

或许您很少这么用吧,事实上在我写本文之前,我都没真正交换过两个自定义的对象,通常我们都不愿意这么交换两个自定义对象。原因是效率太低!或许你要问,万一有的应用就是需要交换两个自定义的对象怎么办?好办,用指针啊!对,指针的好处就是效率高,为什么C++比java效率高,原因之一就是java取消了指针。下面的第一行代码就是交换两个Person类的指针:

GeneralSwap(pYoungMan,pOldMan);

//GeneralSwap(*pYoungMan,* pOldMan);     //效率低

    为什么使用指针就效率高了呢?原因是指针就是地址,地址就是整数,于是问题等价于交换两个整数,因此它不调用赋值运算符重载函数!只要你在应用程序中始终通过指向对象的指针来访问对象,那么交换两个指针就能达到交换对象的目的。注意被注释掉的第二行代码,它是正确的,但是它又回到了交换两个实际对象,其效率低,最好不要这么用!

对于这个最常见、最简单的GeneralSwap我都废话了一大堆,而且还扯出了一个没多少用的关于用户自定义对象的交换问题,这实属个人思维散射,请砖家们狠狠地拍。

在进行下一个方法之前,再次强调一点,这个方法的特点是简单、通用!后面的方法都将与之做比较。

利用加减法实现两个数的交换

    几乎人人都知道还可以利用加减法来实现两个数的交换,其代码也异常简单:

template<class T>
void Add_Sub_Swap_1(T& a, T& b)
{
        a 
= a+b;
        b 
= a-b;
         a 
= a-b;
}

    Add_Sub_Swap_1可以用于交换两个整数,但由于涉及加减法,因此有数据溢出的危险;也可以用于交换浮点数,但是有可能由于舍入误差导致结果不准确。

Add_Sub_Swap_1不能用于交换两个用户自定义的对象,下面的语句编译就通过不,编译器告诉你Person类没有定义operator +等符号:

Add_Sub_Swap_1(youngMan,oldMan);//编译通不过!

    Add_Sub_Swap_1不能用于交换两个指针,语句Add_Sub_Swap_1(pYoungMan,pOldMan);编译时将报错:error C2110: cannot add two pointers,是的,两个指针不能直接做加法运算(减法是可以的)。那么是不是就不能利用加减法实现两个指针的交换呢?答案是:“可以!”,接下来我将阐述如何实现。

利用加减法交换两个指针

    Add_Sub_Swap_1不能用于交换两个指针,前面我说可以用加减法来实现两个指针的交换,这是有根据的:指针仍然是变量,只不过它是存储普通变量的地址的变量。只要我们把指针“看作”变量,那么就能实现加法。那么如何把指针“看作”变量呢?答案是:“通过强制类型转换”!指针表示变量的地址,在32位平台上它是一个无符号的整数,因此可以将指针强制转换为无符号类型的整数。我对上面的Add_Sub_Swap_1进行了改进:

template<class T>
void Add_Sub_Swap_2(T& a, T& b)
{
         
*(( unsigned*)(&a)) = *(( unsigned*)(&a)) + *(( unsigned*)(&b));
         
*(( unsigned*)(&b)) = *(( unsigned*)(&a)) - *(( unsigned*)(&b));
         
*(( unsigned*)(&a)) = *(( unsigned*)(&a)) - *(( unsigned*)(&b));
}

    利用Add_Sub_Swap_2既可以交换两个普通的整数、浮点数同时它可以交换两个任意类型的指针(包含系统预定义类型和用户自定义类型的指针,其实本质上所有指针都属于同一种类型:32位无符号整数类型)。不信您试试Add_Sub_Swap_2(pYoungMan,pOldMan);它能得到正确答案。

虽然Add_Sub_Swap_2解决了Add_Sub_Swap_1无法交换两个指针的问题,但是它仍然无法交换两个用户自定义类型的变量,原因是用户自定义类型没有加减法运算。看来要想用加减法实现两个用户定义类型的交换是不可能的了(除非用户自定义的operator+和operator-能满足交换两个对象的目的,这很难,除非是非常简单的用户自定义类型,比如你不使用系统类型int非要定义一个MyInt类)。

利用异或实现两个整数的交换

    同样地,几乎人人都知道利用异或来交换两个数,其实现也非常简单:

template <class T>
void Xor_Swap_1(T& a,T& b)
{
         a 
= a^b;
         b 
= a^b;
         a 
= a^b;
}

    上面的函数的实用性非常有限,它只能交换两个整数(包含char,int,long),要想交换两个浮点数是不行的,因为浮点数不能参与位运算,要想交换两个指针也是不行的,编译器不允许你把两个指针拿来做位运算,要想交换两个用户自定义对象也是不行的,因为它仍然不能参与位运算。那么是不是利用异或交换两个变量就没法用于浮点数、指针和用户自定义的对象了呢?答案是“能”!后面几节我将阐述这些问题。

利用异或实现两个float和指针的交换

    前面的Xor_Swap_1无法实现两个浮点数和指针的交换,其原因是浮点数和指针均不直接支持位运算。那么如何才能利用异或来交换两个浮点数和指针呢?方法仍然是“强制类型转换”!因为浮点数在内存中仍然是用一串二进制bit来表示的嘛,只要把浮点数看作(强制类型转换)二进制bit构成的整数,那么就能进行位运算了,至于指针嘛,处理方法完全相同。具体如何做呢,其实现大概是这样的:

template <class T>
void Xor_Swap_2(T& a,T& b)
{
         
*((unsigned*)(&a)) = *((unsigned*)(&a)) ^ *((unsigned*)(&b));
         
*((unsigned*)(&b)) = *((unsigned*)(&a)) ^ *((unsigned*)(&b));
         
*((unsigned*)(&a)) = *((unsigned*)(&a)) ^ *((unsigned*)(&b));
}

    利用这个函数可以交换两个float类型的变量,也可以交换任意类型的指针!非常值得注意的是:用它交换两个double类型数据或者两个Person类的对象(youngMan,oldMan)均能编译通过,但是其结果却是错的。至于为什么,以及如何解决,这将是我下一节要阐述的内容。

利用异或实现两个double类型变量和用户自定义变量的交换

     Xor_Swap_2解决了利用异或不能交换两个float数据和指针的问题,然而它却不能正确地交换两个double数据和两个Person类对象。这是为什么呢?原因是函数内部是把参数强制类型转换成unsigned类型的,而sizeof(float)和sizeof(pointor)的值都等于sizeof(unsigned),但是sizeof(double)却不等于sizeof(unsigned),也就是说把double强制转换成unsigned类型时,发生了“位截断”(在概念是区别与数据截断),那么得到的结果肯定就不对了。至于无法交换两个Person类对象,其原因也相同。

这里我要深入分析一下强制类型转换是如何发生位截断的,首先看看以下测试的输出结果,注意代码中的注释,为了节约篇幅,我把值得注意的地方都放在注释中了:

Double a = 1.0,b=2.0;

Xor_Swap_2(a,b);//交换两个double数据

Cout<<a<<b;//输出仍然是1.0和2.0,a,b的值并未改变

Xor_Swap_2(youngMan,oldMan);//交换两个用户自定义对象

youngMan.PrintSelf();//输出young man:81

oldMan.PrintSelf();//输出old man:18

    可以看出两个double数据并没被交换,而两个Person对象在交换之后发生了怪异现象:产生了81岁的年轻人和18岁的老年人!这一点正好说明强制类型转换时发生了位截断,由于Person类的第一个数据成员m_Age正好是int型,在Xor_Swap_2内部做强制类型转换时正好取得了两个对象的m_Age成员,于是出现了两个对象被部分交换的情况,那么又如何解释两个double数据没有变法呢?事实上两个double数据仍然发生了部分交换,因为这里的两个double数(a,b)的前4个字节正好相同,因此看不出部分交换。

既然我们知道了Xor_Swap_2为什么不能用于交换两个double类型的数据和两个用户自定义的数据,那么就有办法对它进行改进。具体改进的思想就是把参数按照一个byte一个byte地分别异或,按照这个思路我实现了如下的函数:

template <class T>
void Xor_Swap_3(T& a,T& b)
{
         
int size = sizeof(T);
         
for (int i = 0;i<size;i++)
         {
                   
*((unsigned char*)(&a)+i) = (*((unsigned char*)(&a)+i)) ^ (*((unsigned char*)(&b)+i));
                   
*((unsigned char*)(&b)+i) = (*((unsigned char*)(&a)+i)) ^ (*((unsigned char*)(&b)+i));
                   
*((unsigned char*)(&a)+i) = (*((unsigned char*)(&a)+i)) ^ (*((unsigned char*)(&b)+i));
         }
}

     这个版本的函数不仅能交换两个整数、任何指针、float数和double数,更牛逼的是它能交换两个用户定义类型的变量!事实上它基本上是在内存一级上操作数据,而任何类型的数据对象最终都表现为内存对象。这其实就是通过内存拷贝实现两个对象的交换的一个版本吧,当然还有利用memcpy等手段进行内存拷贝来实现两个变量的交换的,这里我就不赘述了。

结束语

    本篇到此写完了,有种不好的感觉,因为文中大量使用“强制类型转换”而这个东西是C++中容易出错的地方,而我再写本文时,并没有去复习关于强制类型转换的相关知识,因此担心很多地方有潜在的出错可能,还请各位砖家指正!

@import url(http://www.cppblog.com/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);

posted @ 2011-08-10 11:29 二狗子_五哥 阅读(3452) | 评论 (4)编辑 收藏

2011年8月9日

摘要:

Sizeof的作用非常简单:求对象或者类型的大小。然而sizeof又非常复杂,它涉及到很多特殊情况,本篇把这些情况分门别类,总结出了sizeof的10个特性:

(0)sizeof是运算符,不是函数;

(1)sizeof不能求得void类型的长度;

(2)sizeof能求得void类型的指针的长度;

(3)sizeof能求得静态分配内存的数组的长度!

(4)sizeof不能求得动态分配的内存的大小!

(5)sizeof不能对不完整的数组求长度;

(6)当表达式作为sizeof的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值!

(7)sizeof可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体!

(8)sizeof求得的结构体(及其对象)的大小并不等于各个数据成员对象的大小之和!

(9)sizeof不能用于求结构体的位域成员的大小,但是可以求得包含位域成员的结构体的大小!

概述:

Sizeof是C/C++中的关键字,它是一个运算符,其作用是取得一个对象(数据类型或者数据对象)的长度(即占用内存的大小,byte为单位)。其中类型包含基本数据类型(不包括void)、用户自定义类型(结构体、类)、函数类型。数据对象是指用前面提到的类型定义的普通变量和指针变量(包含void指针)。不同类型的数据的大小在不同的平台下有所区别,但是c标准规定所有编译平台都应该保证sizeof(char)等于1。关于sizeof的更多概述你可以在msdn总输入sizeof进行查询。

看了上面这些,或许你看了没有多少感觉。没关系,下面我将详细列出sizeof的诸多特性,这些特性是造成sizeof是一个较刁钻的关键字的原因:

十大特性:

特性0:sizeof是运算符,不是函数

这个特性是sizeof的最基本特性,后面的很多特性都是受到这个特性的影响,正因为sizeof不是函数,因此我们不把它所要求得长度的对象叫做参数,我本人习惯上叫做操作数(这不严谨,但是有助于我记住sizeof是个操作符)。

特性1:sizeof不能求得void类型的长度

是的,你不能用sizeof(void),这将导致编译错误:illegal sizeof operand。事实上你根本就无法声明void类型的变量,不信你就试试void a;这样的语句,编译器同样会报错:illegal use of type 'void'。或许你要问为什么,很好,学东西不能只知其然,还要知其所以然。我们知道声明变量的一个重要作用就是告诉编译器该变量需要多少存储空间。然而,void是“空类型”,什么是空类型呢,你可以理解成不知道存储空间大小的类型。既然编译器无法确定void类型的变量的存储大小,那么它自然不让你声明这样的变量。当然了,声明void类型的指针是可以的!这就是特性2的内容。

特性2:sizeof能求得void类型的指针的长度

在特性1中说过,可以申明void类型的指针,也就是说编译器可以确定void类型的指针所占用的存储空间。事实上确实如此,目前,几乎所有平台上的所有版本的编译器都把指针的大小看做4byte,不信你试试sizeof(int*);sizeof(void*);sizeof(double*);sizeof(Person*);等等,它们都等于4!为什么呢?问得好,我将尽全力对此作出解释:其实指针也是变量,只不过这个变量很特殊,它是存放其他变量的地址的变量。又由于目前32位计算机平台上的程序段的寻址范围都是4GB,寻址的最小单元是byte,4GB等于232Byte,这么多的内存其地址如果编码呢,只需要用32个bit就行了,而32bit = 32/8 = 4byte,也就是说只需要4byte就能存储这些内存的地址了。因此对任何类型的指针变量进行sizeof运算其结果就是4!

特性3:sizeof能求得静态分配内存的数组的长度!

Int a[10];int n = sizeof(a);假设sizeof(int)等于4,则n= 10*4=40;特别要注意:char ch[]=”abc”;sizeof(ch);结果为4,注意字符串数组末尾有’\0’!通常我们可以利用sizeof来计算数组中包含的元素个数,其做法是:int n = sizeof(a)/sizeof(a[0]);

非常需要注意的是对函数的形参数组使用sizeof的情况。举例来说,假设有如下的函数:

void fun(int array[10])

{

         int n = sizeof(array);

}

你会觉得在fun内,n的值为多少呢?如果你回答40的话,那么我很遗憾的告诉你,你又错了。这里n等于4,事实上,不管形参是int的型数组,还是float型数组,或者其他任何用户自定义类型的数组,也不管数组包含多少个元素,这里的n都是4!为什么呢?原因是在函数参数传递时,数组被转化成指针了,或许你要问为什么要转化成指针,原因可以在很多书上找到,我简单说一下:假如直接传递整个数组的话,那么必然涉及到数组元素的拷贝(实参到形参的拷贝),当数组非常大时,这会导致函数执行效率极低!而只传递数组的地址(即指针)那么只需要拷贝4byte

特性4:sizeof不能求得动态分配的内存的大小!

假如有如下语句:int* a = new int[10];int n = sizeof(a);那么n的值是多少呢?是40吗?答案是否定的!其实n等于4,因为a是指针,在特性2中讲过:在32位平台下,所有指针的大小都是4byte!切记,这里的a与特性3中的a并不一样!很多人(甚至一些老师)都认为数组名就是指针,其实不然,二者有很多区别的,要知详情,请看《c专家编程》。通过特性3和特性4,我们看到了数组和指针有着千丝万缕的关系,这些关系也是导致程序潜在错误的一大因素,关于指针与数组的关系问题我将在《C/C++刁钻问题各个击破之指针与数组的秘密》一文中进行详细介绍。

特性3指出sizeof能求静态分配的数组的大小,而特性4说明sizeof不能求的动态分配的内存的大小。于是有人认为sizeof是编译时进行求值的,并给出理由:语句int array[sizeof(int)*10];能编译通过,而很多书上都说过数组大小是编译时就确定下来的,既然前面的语句能编译通过,所以认为sizeof是编译时进行求值的。经过进一步测试我发现这个结论有些武断!至少是有些不严谨!因为在实现了c99标准的编译器(如DEV C++)中可以定义动态数组,即:语句:int num;cin>>num; int arrary[num];是对的(注意在vc6.0中是错的)。因此我就在DEV C++中对刚才的array利用语句int n =sizeof(array);cout<<n<<endl来求大小,结果编译通过,运行时输入num的值10之后,输出n等于40!在这里很明显num的值是运行时才输入的,因此sizeof不可能在编译时就求得array的大小!这样一来sizeof又变成是运行时求值的了。

那么到底sizeof是编译时求值还是运行时求值呢?最开初c标准规定sizeof只能编译时求值,后来c99又补充规定sizeof可以运行时求值。但值得注意的是,即便是在实现了c99标准的DEV C++中仍然不能用sizeof求得动态分配的内存的大小!

特性5:sizeof不能对不完整的数组求长度!

在阐述该特性之前,我们假设有两个源文件:file1.cpp和file2.cpp,其中file1.cpp中有如下的定义:

int arrayA[10] = {1,2,3,4,5,6,7,8,9,10};

int arrayB[10] = {11,12,13,14,15,16,17,18,19,20};

file2.cpp包含如下几个语句:

         extern arrayA[];

         extern arrayB[10];

         cout<<sizeof(arrayA)<<endl;            //编译出错!!

         cout<<sizeof(arrayB)<<endl;

在file2.cpp中第三条语句编译出错,而第条语句正确,并且能输出40!为什么呢?原因就是sizeof(arrayA)试图求不完整数组的大小。这里的不完整的数组是指数组大小没有确定的数组!sizeof运算符的功能就是求某种对象的大小,然而声明:extern int arrayA[]只是告诉编译器arrayA是一个整型数组,但是并没告诉编译器它包含多少个元素,因此对file2.cpp中的sizeof来说它无法求出arrayA的大小,所以编译器干脆不让你通过编译。

那为什么sizeof(arrayB)又可以得到arraryB的大小呢?关键就在于在file2.cpp中其声明时使用extern int arrayB[10]明确地告诉编译器arrayB是一个包含10个元素的整型数组,因此大小是确定的。

到此本特性讲解差不多要结束了。其实本问题还能引申出连接和编译等知识点,但是目前我暂时还没自信对这两个知识点进行详细的,彻底的讲解,因此不便在此班门弄斧,不久的将来我会在本系列中加上相关问题的阐述。

特性6:当表达式作为sizeof的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值!

为了说明这个问题,我们来看如下的程序语句:

char ch = 1;

         int num=1;

         int n1 = sizeof(ch+num);

         int n2 = sizeof(ch = ch+num);

假设char占用1byte,int占用4byte,那么执行上面的程序之后,n1,n2,ch的值是多少呢?我相信有不少人会认为n1与n2相等,也有不少人认为ch等于2,事实这些人都错了。事实上n1等于4,n2等于1,ch等于1,为什么呢?请看分析:

由于默认类型转换的原因,表达式ch+num的计算结果的类型是int,因此n1的值为4!而表达式ch=ch+num;的结果的类型是char,记住虽然在计算ch+num时,结果为int,但是当把结果赋值给ch时又进行了类型转换,因此表达式的最终类型还是char,所以n2等于1。n1,n2的值分别为4和1,其原因正是因为sizeof返回的是表达式计算结果的类型大小,而不是表达式中占用最大内存的变量的类型大小!

对于n2=sizeof(ch =ch+num);乍一看该程序貌似实现了让ch加上num并赋值给ch的功能,事实并非如此!由于sizeof只关心类型大小,所以它自然不应该对表达式求值,否则有画蛇添足之嫌了。但是,在支持变长数组定义的(即实现了c99标准的)编译器(dev C++)中执行了int len = 3;cout<<sizeof(int [++len])<<”,”<<len;输出是多少呢?答案是16,4!这里的++len却执行了!很不可理喻吧?这到底是为什么呢?我翻阅了《The New C Standard》一书,这主要是由于可变长度的数组的长度需要在其长度表达式求值之后才能确定大小,因此上述情况下,sizeof中的++len执行了。

正是因为sizeof的操作数中的某些表达式会被执行,而有些表达式不会被执行,这里告诫各位,尽量不要在sizeof中直接对表达式求大小,以免出现错误,你可以将sizeof(ch = ch+num);改写成 ch = ch +num;sizeof(ch);虽然多了一条语句,看似冗余了,其实好处多多:首先更加清晰明了,其次不会出现ch等于1这样的错误(假设程序的逻辑本身就是要执行ch = ch +num;)。

特性7:sizeof可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体!

假设有如下函数(是一个写得很不好的函数,但是能很好的说明需要阐述的问题):

int fun(int& num,const int& inc)

{

         float div = 2.0;

         double ret =0;

         num = num+inc;

         ret = num/div;

         return ret;

}那么语句:

int a = 3;

         int b = 5;

         cout<<sizeof(fun(a,b))<<endl;

         cout<<a<<endl;输出多少呢?不同的人会给出不同的答案,我将对sizeof(fun(a,b))的值和a的值分别进行讨论:

首先sizeof(fun(a,b))的值:其正确是4,因为用sizeof求函数调用的大小时,它得到的是函数返回类型的大小,而fun(a,b)的返回类型是int,sizeof(int)等于4。很多人把函数的返回类型返回值的类型弄混淆了,认为sizeof(fun(a,b))的值是8,因为函数返回值是ret,而ret被定义成double,sizeof(doube)等于8。注意,虽然函数返回值类型是double,但是在函数返回时,将该值进行了类型转换(这里的转换不安全)。也有人错误的认为sizeof(fun(a,b))的值是12,它们的理由是:fun内部定义了两个局部变量,一个是float一个是double,而sizeof(float)+sizeof(doube)= 4+8=12。这样的答案看似很合理,其实他们是错误地认为这里的sizeof是在求函数内部的变量的大小了。这当然是错误的。

接下来看a的值:其正确答案是3!还记得特性6吗?这里很类似,sizeof的操作对象是函数调用时,它不执行函数体!为此,建议大家不要把函数体放在sizeof后面的括号里,这样容易让人误以为函数执行了,其实它根本没执行。

既然对函数条用使用sizeof得到的是函数返回类型的大小,那么很自然能得出这样的结论:不能对返回类型为void的函数使用sizeof求其大小!原因请参考特性1。同理,对返回类型是任何类型的指针的函数调用使用sizeof求得的大小都为4,原因请参考特性2。

最后我们来看看这样的语句:cout<<sizeof(fun);其答案是多少呢?其实它得不到答案,原因是编译就通不过!最开始,我以为能输出答案4,因为我认为fun是函数名,而我知道函数名就是函数的地址,地址就是指针,于是我认为sizeof(fun)其实就是对一个指针求大小,根据特性2,任何指针的大小都是4。可是当我去验证时,编译器根本不让我通过!这个是为什么呢?我一时半会想不到,所以还请朋友们补充!

特性8:sizeof求得的结构体(及其对象)的大小并不等于各个数据成员对象的大小之和!

结构体的大小跟结构体成员对齐有密切关系,而并非简单地等于各个成员的大小之和!比如对如下结构体两个结构体A、B使用sizeof的结果分别是:16,24。可以看出sizeof(B)并不等于sizeof(int)+sizeof(double)+sizeof(int)=16。

struct A{

         int num1;

         int num2;

         double num3;

};

struct B{

         int num1;

         double num3;

         int num2;

};

如果您不了解结构体的成员对齐,你会感到非常惊讶:结构体A和B中包含的成员都一样,只不过顺序不同而已,为什么其大小不一样呢?要解释这个问题,就要了解结构体成员对齐的规则,由于结构体成员对齐非常复杂,我将用专题——C/C++刁钻问题各个击破之位域和成员对齐——进行讲解,这里我只简单地介绍其规则:

1、 结构体的大小等于结构体内最大成员大小的整数倍

2、 结构体内的成员的首地址相对于结构体首地址的偏移量是其类型大小的整数倍,比如说double型成员相对于结构体的首地址的地址偏移量应该是8的倍数。

3、 为了满足规则1和2编译器会在结构体成员之后进行字节填充!

 

基于上面三个规则我们来看看为什么sizeof(B)等于24:首先假设结构体的首地址为0,第一个成员num1的首地址是0(满足规则2,前面无须字节填充,事实上结构体绝对不会在第一个数据成员前面进行字节填充),它的类型是int,因此它占用地址空间0——3。第二个成员num3是double类型,它占用8个字节,由于之前的num1只占用了4个字节,为了满足规则2,需要使用规则3在num1后面填充4个字节(4——7),使得num3的起始地址偏移量为8,因此num3占用的地址空间是:8——15。第三个成员num2是int型,其大小为4,由于num1和num3一共占用了16个字节,此时无须任何填充就能满足规则2。因此num2占用的地址空间是16——19。那么是不是结构体的总大小就是0——19共20个字节呢?请注意,别忘了规则1!由于结构体内最大成员是double占用8个字节,因此最后还需要在num2后面填充4个字节,使得结构体总体大小为24。

按照上面的三个规则和分析过程,你可以很容易地知道为什么sizeof(A)等于16。特别需要说明的是,我这里给出了三个结论性的规则,而没有阐述为什么要这样。你或许有很多疑问:为什么要结构体成员对齐,为什么要定义规则1等。如果你有这样的疑问,并尝试去弄清楚的话,那么我敢断言,不久的将来你必定会有大成就,至少在学习c++上是这样。前面说过,我会再写一篇专题:C/C++刁钻问题各个击破之位域和成员对齐来详细回答这些问题,如果你急于要弄明白,那么你可以参考其他资料,比如说《高质量c++程序设计指南》。

最后再提醒一点,在进行设计时,最好仔细安排结构体中各个成员的顺序,因为你已经看到了上面的结构体B与结构体A包含的成员相同,只不过顺序略有差异,最终就导致了B比A多消耗了50%的空间,假如在工程中需要定义该结构体的数组,多消耗的空降将是巨大的。即使将来内存降价为白菜价格,你也不要忽视这个问题,勤俭节约是中国人民的优良传统,我们应该继承和保持!

特性9:sizeof不能用于求结构体的位域成员的大小,但是可以求得包含位域成员的结构体的大小!

首先解释一下什么是位域:类型的大小都是以字节(byte)为基本单位的,比如sizeof(char)为1byte,sizeof(int)为4byte等。我们知道某个类型的大小确定了该类型所能定义的变量的范围,比如sizeof(char)为1byte,而1byte等于8bit,所以char类型的变量范围是-128——127,或者0——255(unsigned char),总之它只能定义28=256个数!然而,要命的是bool类型只取值true和false,按理所只用1bit(即1/8byte)就够了,但事实上sizeof(bool)等于1。因此我们可以认为bool变量浪费了87.5%的存储空间!这在某些存储空间有限的设备(比如嵌入式设备)上是不合适的,为此需要提供一种能对变量的存储空间精打细算的机制,这就是位域。简单来说,在结构体的成员变量后面跟上的一个冒号+一个整数,就代表位域,请看如下的结构体:

Struct A

{

         Bool b:1;

         char ch1:4;

         char ch2:4;

}item; 其中b,ch1,ch2都是位域成员,而i是普通成员。该结构体的试图让bool类型的变量b只占用1个bit,让ch1和ch2分别只占用4个bit,以此来达到对内存精打细算的功能(事实上使用位域对内存精打细算有时候能成功,有时候却未必,我将《C/C++刁钻问题各个击破之位域和成员对齐》进行论述)。另外需要特别注意的是:c语言规定位域只能用于int,signed int或者unsigned int类型,C++又补充了char和long类型!你不能这样使用位域:float f:8;这是不能通过编译的。并且位域变量不能在函数或者全局区定义,只能在结构体,自定义类,联合(union)中使用!

基于上面的结构体,语句sizeof(item.b)和sizeof(item.ch1)等对位域成员求大小的语句均不能通过编译。其原因能再本篇的概论中找到:sizeof以byte为单位返回操作数的大小!

那么爱学好问的你可能要问,sizeof(A)能否通过编译呢?如何能,其结果又是多少呢?这是两给非常好的问题,事实上我之前没有看到任何关于这方面的论述(可能是我看的资料不足),我正是在看到sizeof(item.b)不能通过编译时想到了这两个问题,然后通过验证得出了后面的结论:对包含位域的结构体是可以使用sizeof求其大小的,但其求值规则比较复杂,不仅涉及到成员对齐,还与具体编译环境有关!在这里你只需要知道可以对包含位域的结构体使用sizeof求其大小,对于sizeof是根据什么规则来求这个大小的问题,我将会在专题:《C/C++刁钻问题各个击破之位域和成员对齐》中进行详细阐述

后记:

至此,本专题差不多该结束了,需要说明的是,这里并没有包含所有关于sizeof的知识点,但是也几乎包含了所有的容易出错的特性。为了完成该文,我花了断断续续3天半时间,想想效率实在是底下。由于是本系列的第一个专题,我格外慎重,深怕讲错了误导大家。即便如此,也难免错误或不妥之处,还请各位朋友指正!

另外,我有几句话要对大学生朋友们说:教科书通常只是教授很基础的知识,要想深入学习,还需要翻阅其他资料,比如论文、网络资料、论坛博文,最重要的一点是要在学习时经常总结、记录、归纳,积少成多,这样坚持下来一定受益匪浅。 @import url(http://www.cppblog.com/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);

posted @ 2011-08-09 10:31 二狗子_五哥 阅读(5996) | 评论 (16)编辑 收藏