从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)。
征求互动
如果你觉得读完有帮助,请点个赞。
如果你觉得读完有打动你,欢迎和我留言互动。
如果你觉得我哪里写的不好,请留下你的看法。
如果觉得有改进的地方,也请不吝赐教。
下次会继续写面向对象中的内存模型,类和对象在内存中的分布,面向对象。
参考
无