[c++][内存管理]从sizeof看C++对象模型

从sizeof看C++对象模型

$KaTeX parse error: Expected '}', got 'EOF' at end of input: …度: 代码难度: 代码量: $}

最近在学习C++,于是打算整理一些专题知识,梳理自己的学习成果。一来是回忆和整合,加深理解,再一个是希望大佬指点。

今天说的是 sizeof 的话题。

插一句话,我认为不管是否写的是专业相关,我的博客(或者说笔记)的特点是,充满思考和分析,结合自己对生活的观察,以一个普通的人视角来写,而非干巴巴的搬运别人的东西。

目录

背景

实习和学习需要。

内存模型是一个经常会被问到的问题,加上语言本身比较复杂,经常日常使用往往不会在意,所以特此探究一下。

sizeof 是一个与内存相关的东西,今天早上翻起一本书,无意看到sizeof的一些知识,感觉脑子一下清楚了。

本文大概会讲几个东西,(1)运算符的概念(2)sizeof (3)字符串的问题

正文

运算符的概念

最大名鼎鼎的运算符,我举几个例子,+、-、*,、,*(解除指针),&取地址符,new(分配内存),今天来说说sizeof。

书上有抛出一个概念,sizeof 是一个运算符, 所以运算符不用带括号。比如

int a=10;
int b = sizeof a;
int c = sizeof(a); // 提个问题:sizeof返回的是unsigned int, 赋给int 没有问题吧!!!,这其实一个类型转换的问题. 只要不溢出就可以了

在Java语言中,instanceOf 这个关键字也曾经一度令我迷惑,为啥不带括号呢?做成一个函数多好。

一个那么长的单词,你配叫运算符吗?但是,sizeof 偏偏就是。运算符和函数大概最大的区别就是,函数调用要用栈,而运算符不用,成本极小。

sizeof

接着上面说,sizeof是运算符,但是我还是习惯以函数的形式使用它,另外有的时候必须用函数形式,比如

sizeof(int);
sizeof(double); // 加了括号是不是就按函数处理了呢? 这个有待考证.
sizeof(MyClass);// 自定义类型

sizeof 可以反映一个变量的真实物理空间。

一个大家都知道的概念是,数组在传入sizeof的时候,会返回数组的实际长度,但是数组被当做参数传入函数的时候,其形参就会退化成一个指针。这应该是传参的机制,和 sizeof 本身无关。
补充一下, 如果你给一个函数的形参形成void f1(int arr[], int len);的形式, 编译器还是会把 arr[]看做一个指针的!!

另外再说一个概念,内存对齐。使用sizeof 涉及到内存对齐的概念,虽然这个知识点对于编程没啥用(纠正一下,对于初学者没啥用,但是对于成为高手(写编译器的高手)有用),但是知道总是好的。

举个例子:
struct my_struct{
	int a; //4Byte	
	char b;	 //1Byte
	short c; // 2Byte
} test;
int siz = sizeof(test); // 返回8

有人就纳闷了,为什么是8而不是7呢?因为方便计算偏移量,编译器规定结构体大小是最宽的成员变量的大小的整数倍,所以要给 属性 b后面填充字符。 4+1+1 (填充字节) + 2 = 8

为什么要方便计算偏移量呢?因为为了方便读取。这和体系架构相关,有的CPU读取地址(这个也是道听途书,真的咱也不知道),从偶数位开始读,读奇数位不方便。(这是我看来的,想想底层实现,应该也是这个理)。

另外呢,对齐,确实方便计算属性的个数,方便管理。所以要有字节对齐。

问题来了,那么类的实例在内存中加载时里面是否有类似的特点呢?C++里面也是一样的. class关键字和strut关键字其实是一样的.

另外补充一点,sizeof 可以是在编译器运行的,所以它可以放在函数外面用作静态持续性变量的赋值。

再提出一个有趣的问题, 以下代码,调整类型的次序,打印的结果不一样。

void test_sizeof(){
    struct my_struct{
        int a; //4Byte
        char b;	 //1Byte
        short c; // 2Byte
    } test;

    int siz = sizeof(test); // 返回8
    cout << siz << endl;

    struct my_struct2{
        short c; // 2Byte
        int a; //4Byte
        char b;	 //1Byte

    } test2;

    int siz2 = sizeof(test2); 
    cout << siz2 << endl; // 返回12
    
    
    struct my_struct3{
        short c; // 2Byte
        char b;	 //1Byte
        int a; //4Byte

    } test3;

    int siz3 = sizeof(test3);
    cout << siz3 << endl; // 返回8
}

根据上面所说,编译器规定结构体大小是最宽的成员变量的大小的整数倍。

我们看第二种情况,为什么test2的长度是12byte呢?按上面的规定,字节对齐的规则是,结构体的大小必须是最宽的成员的整数倍,12和8都是4的整数倍,只因为一个区别,最宽的成员位置不同,因而大小也就不同。

如果把最宽的放到中间,那么前后比较窄的成员都不能利用彼此剩余的空间,必须重新开辟一个最宽变量大小的内存,如果把最宽的放到最前面或者最后面,比较窄的成员变量,short和char类型的就可以挤一挤,只用补全一个字节,就可以对齐。

我们进一步提出,结构体和类的对齐方式是一致的。而且这种对齐原则,应该是适用于内置类型,自定义类型可能未必遵循这个原则。(这个有待确认)

sizeof遇到字符串

先来说默认字符串常量的问题。

我们都知道有个类叫做string, 还有C风格的字符串数组。看下面的例子:

class A{
public:
	A(char* a); 
	A(string a);
	A(string& a);
}
int main(){
	A a("abcdefg");// 问题来了,请问那个函数会被调用呢?
}

问题来了,请问那个函数会被调用呢?经过我验证,是第一个函数,因为默认的字符串常量在C++中被当做字符串数组!

C++的哲学是:速度,速度,还他妈的是速度。不要用到可能不会用到的特质,因为都是有代价的。

string是一个类,成本比字符串数组高。

那么问题来了,sizeof 遇到字符串数组怎么办?

char * a = "abcd";
int siz = sizeof(a); 

siz的值是多少呢?是5。因为他后面有个\0,这个特性是区分字符串终止的关键点。但是如果用 sizeof,就每次都要减去1来求得真正有意义的字符的长度。而且,数组的恼人特征是,如果当做参数传进去,长度信息会被丢弃,启动退化成一个指针,那么每次都必须带一个长度信息进去了。(用作参数的时候,真麻烦!!)

而有一个函数strlen,就很方便。把字符串数组传进参数的时候,它可以自行执行向后遍历,拿到字符串的长度,而且会只计算真正有意义的字符串的长度。

继承之下的sizeof

class Base2{ //测试sizeof 与 继承
    int a;
    int a2;
};

class Derived2: public Base2{
public:
    int b;
    void f1(){
        cout << "Derived f1" << endl;
    }
};

void main_f9(){
    Derived2 d;
    cout << sizeof(d) << endl; //12 ,3个int
    int * ptr=NULL;
    cout << sizeof(ptr) << endl; // 8, 我的电脑是64位的
}

在Java中,一个对象是有头信息的。也就是说,我们可以根据一个对象头知道其实际类的位置,在C++中,应该是不具有这样的内容的,一个实例的类型信息是单独被保存在其他位置的。

令人费解的是

class Base2{ //测试sizeof 与 继承
    int a;
    int a2;
public:
    //int pa; //4
    virtual void f1(){ //虚表8个字节
        cout << "Base f1" << endl;
    }
};

class Derived2: public Base2{
public:
    int b;
    void f1(){
        cout << "Derived f1" << endl;
    }
};


void main_f9(){
    Base2 base2;
    cout << sizeof(base2) << endl; // 16个字节, 8(虚表) + 4(int) + 4
    Derived2 d;
    cout << sizeof(d) << endl; 
    // 24个字节,3个整数,12个字节,又因为子类有虚表(8个字节),其中的元素最宽是8个字节, 这个是我不能理解的,这涉及到C++的对象模型
    int * ptr=NULL;
    cout << sizeof(ptr) << endl; // 8
}

类的实例的对齐方式是怎么样的?多层的类会导致类的实例的体积膨胀。

以下是一个例子:

#include <iostream>
#include <vector>
 
using namespace std;
 
class Foo {
public:
    int val;
    char bit1, bit2, bit3;
};

class A { // size 8,大小是最宽的元素的整数倍
public:
    int val;
    char bit1;
};

class B : public A { // size 12
public:
    char bit2;
};

class C : public B { // size 12
public:
    char bit3;
};

int main()
{
    cout << "size Foo = " << sizeof(Foo) << endl; // 8
    cout << "size A   = " << sizeof(A) << endl;  // 12
    cout << "size B  = " << sizeof(B) << endl;  // 12
    cout << "size C   = " << sizeof(C) << endl;  // 12
    return 0;
}

继续补充,多态下的sizeof

继承之下的sizeof

sizeof 是一个编译时就确定结果的运算符,所以sizeof不会动态检查一个对象的运行时类型,如果一个父类引用指向一个子类对象,尽管子类对象可能有额外属性,sizeof 也只会返回父类引用类型的大小。

所以,在强调一次,sizeof 只支持静态运算。

其他问题

如果一个类没有属性,没有继承其他类,那么请问它的大小是?它的内容是?
Q1:对于Java来说,他们的祖先类Object有很多方法,在C++里是如何做模拟的?比如 clone函数?equals函数
A:clone函数可以用赋值运算符来代替。equals函数可以用 operator== 重载吧(这个还要确定一下TODO)。

征求互动

如果你觉得读完有帮助,请点个赞。
如果你觉得读完有打动你,欢迎和我留言互动。
如果你觉得我哪里写的不好,请留下你的看法。
如果觉得有改进的地方,也请不吝赐教。

下次会继续写面向对象中的内存模型,类和对象在内存中的分布,面向对象。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值