欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C/C++----sizeof关键字

程序员文章站 2024-03-23 15:54:58
...

你真的了解sizeof()吗?

(所列举代码均在VS2013上编译运行,编译器不同可能产生结果不同)

首先看几个问题:
1.sizeof是函数吗?
2.sizeof中的表达式进行计算吗?
3.sizeof对内置类型,对指针,对数组,对结构体(包括含位域结构体,含柔性数组结构体),对枚举,对函数,对联合体(下面会贴出大小端判断方式),对类,对继承类,对抽象类,对继承类的字节计算你又清楚吗?
如果以上三个问题回答不上的话,那么说明你对sizeof并不是深刻的了解。

首先sizeof是函数吗?
我们知道C/C++中函数的调用必不可少的就是函数名后的形参括号来代表我们是再调用函数(例如:rand()),当我们把一个函数后的括号去掉,那编译器肯定就会报错啦~。那么我们就第一个问题来用这种方法检测一下sizeof就知道了。

#include<stdio.h>
#include<time.h>

int main()
{
    int a = 10;
    printf("%d", sizeof a);
    return 0;
}

执行结果
C/C++----sizeof关键字
可以发现完全没问题,说明一个问题它不是一个函数!,那么它到底是什么呢?其实它是一个操作符(类似C++中的++,- -),简单的说其作用就是返回一个对象或者类型所占的内存字节数,它返回的是一个size_t(unsigned int)。

sizeof括号中的表达式会进行计算吗?
当我们写出下面的代码的时候:

int main()
{
    int a = 10;
    printf("%d\n", sizeof a++); //4
    printf("%d\n", a);          //10
    return 0;
}

发现a++压根就没运行嘛。实际上,sizeof在编译阶段会将后面的变量或者表达式都替换成类型,然后求字节数,也就是说a++在编译阶段其实已经替换成了int.所以不会就运行。点击查看sizeof汇编处理

sizeof对各种类型的运算
1.内置类型
char (1) short(2) int(4) long(4) float(4) double(8)
longlong(8) 在32位下类型对应的字节数。64位下long会变成8个字节。

2.指针
任何类型的指针在32位都是4个字节,在64位都是8个字节

3.数组

void fun(char arr[10])
{
    printf("%d\n", sizeof(arr));
}
int main()
{
    int arr[10];
    char str1[] = "nihao";
    char *str2 = "nihao";
    printf("%d\n", sizeof(arr));     //40
    fun(arr);                         //4
    printf("%d\n", sizeof(arr[0]));  //4
    printf("%d\n", sizeof(str1));     //6
    printf("%d\n", sizeof(str2));     //4
    return 0;
}

第一个我们对数组名sizeof代表的是整个数组所占字节数。
我们先跳过第二个。第三个是通过下标访问了元素,里面是int类型数据所以是4。
第四个对字符数组名求sizeof还是一样,但是要注意的就是后面还有一个’\0’终止符,所以是6。
第五个是前面所说的任何类型指针都是4个字节。
第二个原因是因为形参中的数组都退化成了指针。所以我们其实是在对一个指针求sizeof也就是4

4.结构体

typedef struct
{
    int a;
    char b;
}A;
typedef struct
{
    int a;
    char b;
}B;
int main()
{
    A a;
    B b;
    printf("%d\n", sizeof(a));       //8
    printf("%d\n", sizeof(b));       //8
    return 0;
}

是不是发现和自己想的完全不一样,按我们平时理解不应该都是5吗,怎么会是8呢?那是因为结构体内存要进行字节对齐!
那么为何要字节对齐呢?
CPU 的访问粒度不仅仅是大小限制,地址上也有限制。也就是说,CPU 只能访问对齐地址上的固定长度的数据。
以四字节对齐为例,就是只能访问 0x0 - 0x3,0x4 - 0x7, 0x8 - 0xc 这样的(闭)区间,不能跨区间访问。
如果一个数据跨越了两个这样的区间,那么就只能将这个数据的操作拆分成两部分,分别执行,效率当然就低了。
因此强制地址对齐规则,就能够让CPU提高访问效率。
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
说简单一点呢就是:
1.结构体的每个变量起始字节必须是这个变量的整数倍,如果前面的起始字节不够,那么就会补充直到满足条件。
2.结构体的大小是占字节最大类型的整数倍。

二者同时要满足,那么上面的结果就很好理解了。

在这里很重要的一点也是很容易错的一点就是上面的内容是看编译器规定的,而在gcc上,默认对齐不再是最大数据类型字节数而是4个字节!!比如下面这个题在gcc上运行:

3 typedef struct
  4 {
  5     double a;
  6     short b;
  7     int c;
  8     char d;
  9 }A;
 10 
 11 int main()
 12 {
 13     A x;
 14     printf(" %d",sizeof(x));             //20
 15     return 0;
 16 }

按照我们前面的想法,它应该是8+2+2+4+1+7=24.但是结果是20,原因就是上面所说的gcc是按照4字节来对齐而不是最大类型

typedef struct
{
    char a : 1;    
    char b : 2;
    char c : 6;
    int d;
}A;

int main()
{
    A a;
    printf("%d\n", sizeof(a));      //8
    return 0;
}

上面就是含有位域的结构体,前面a,b,c就是位域。
1) 如果相邻位域字段的类型相同,且其位宽之和小于该类型字节数,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于该类型字节数,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍。
所以前三个都是char类型,一个字节8个位,a+b=3<8所以是一个字节,但是a+b+c>8这时的c就不能够放到这个字节中了,所以c就要另起一个字节空间存放。所以a,b,c总共占了2个字节,然后就和满足前面提到的结构体大小的规则,d要在整数倍位置存放,也就是在4才可以存放,所以2+2=4,然后放入d,4+4=8.就是8个字节了。

typedef struct A
{
    int i;
    int arr[];               //柔性数组
}A;


int main()
{   

    printf("%d", sizeof(A));    //4
    return 0;
}

柔性数组就是未知大小数组,它在结构体中担当成员的时候是不占结构体的内存布局的。

在这里要注意的是空结构体在VS上无法编译通过,但是在gcc可以,并且求sizeof结果是0。

5.枚举

enum A
{
    NUM_1,
    NUM_2,
    NUM_3
}A;

int main()
{   

    printf("%d", sizeof(A));            //4
    return 0;
}

enum只是定义了一个常量集合,里面没有“元素”,而枚举类型是当做int来存储的,所以枚举类型的sizeof值都为4

6.函数

int show()
{
    ;
}
/*void show()
{
    ;
}
*/                   error!!!
int main()
{
    printf("%d\n", sizeof(show()));   //4
    return 0;
}

对函数求sizeof其实就是看其函数返回值,但是要注意的是当是void的时候,VS是直接报错的,在gcc上则是1.

7.联合体(共用体)

typedef union
{
    char a;
    int b;
    int c;
    double d;
}A;

int main()
{
    A a;
    printf("%d\n", sizeof(a));    //8
    return 0;
}

联合体内的变量都共用的是一块内存,只以最大类型字节为主(超出的也不算)。

8.C++中的类,继承类,抽象类,虚基类

class A
{
protected:
    char a;
    char b;
    char c;
    char e;
};

class B:public A
{
public:
    void fun1(){}
    int fun2(){ return 0; }
private:
    char a;
    int b;
};

class C
{
public:
    virtual void fun1()=0private:
    char a;
    int b;
};

class D
{
};

class E:virtual public A
{
public:
    void fun1(){}
    int fun2(){ return 0; }
private:
    char a;
    int b;
};

int main()
{
    cout << sizeof(A) << endl;               //4
    cout << sizeof(B) << endl;               //12
    cout << sizeof(C) << endl;               //12
    cout << sizeof(D) << endl;               //1
    cout << sizeof(E) << endl;               //16
    return 0;
}

在C++ 中struct 和class都是,它们的唯一区别就是strcut 是默认公有(public)限定class默认私有(private)限定。

在内存布局方面还是和C语言结构体一样,因此A的大小是4个字节。

B共有继承了A,当B继承了A后,内存布局不仅仅是只有自己B内的数据,还有就是A中继承来的数据,并且类的普通成员方法不占空间,所以是4+1+3+4=12.(注意内存对齐)。

C有着和B一样的成员变量,但是却没有继承任何基类,但是大小却和继承后的B一样大,原因是因为在成员方法前加了virtual后,该成员方法会变成虚函数如果只是声明它(就是代码中的virtual void fun1()=0;)而没有定义的话,那么它就是纯虚函数,这时C也叫做抽象类。在虚函数和纯虚函数中,会生成一个指向虚函数表的虚函数指针(vfptr)。该指针位于该类内存布局的最上方,所以C的大小就是4+1+3+4=12.

D在C++中空类占一个字节。原因是因为,类定义对象要进行构造,而构造函数中需要用this指针去接收内存地址,因为是空类,所以就给了最小的寻址单位一个字节。(这与上面C语言空结构体大小是0的区别,就是因为结构体定义变量,类定义对象)。

E和B继承A的方式的区别就是加了virtual关键字,变成了虚继承,进行虚继承后会产生vbptr指针。因此比B多了4个指针。

关于C++类,继承与多态后面总结中会写出

额外内容

通常我们可以利用联合体的特性来判断大小端。

typedef union
{
    char a;
    int b;
}A;

int main()
{
    A x;
    x.b = 1;
    if (1==x.a)
    {
        printf("这是小端\n");
    }
    else
    {
        printf("这是大端\n");
    }
    return 0;
}

第二篇复习博客,因为是C语言还是很简单的,后面会复习总结其他知识。