文章目录
?前言? 类的实例化?类对象模型 ? 如何计算类对象的大小?类对象的存储方式猜想?猜想一:对象中包含类的各个成员?猜想二:代码只保存一份,在对象中保存存放代码的地址?猜想三:只保存成员变量,成员函数存放在公共的代码段 ? 类中什么都没有---空类? 类中仅有成员函数? 类中既有成员变量,又有成员函数?总结
?前言
上回我们学习了类的定义,初步了解了什么是类?类的定义,以及类的三个访问限定符:public
,private
,protected
,本小节将讲解类的实例化,类对象模型的猜想存储,及三种简单类的计算。
? 类的实例化
在 C++ 中,类的实例化是指创建一个类的对象。当我们定义了一个类之后,就可以根据这个类创建出多个对象。这个过程就称为类的实例化。
实例化一个类的语法是:ClassName objectName;
首先,假设我们定义了一个 Person
类:
Person
类,包含了两个数据成员 name
和 age
,以及一个成员函数 introduce()
。在类的声明阶段,并没有为 Person
类分配任何内存空间。 // 类的声明#include <iostream>using namespace std;class Person {public: std::string name; int age; void introduce() ;};
类的定义:
我们还需要定义introduce()
函数的具体实现: void Person::introduce() { std::cout << "My name is " << name << " and I'm " << age << " years old." << std::endl;}
在类的定义阶段,同样没有为 Person
类分配任何内存空间。 类的实例化:
现在我们可以创建Person
类的对象了: int main() { Person p1; p1.name = "Asen"; p1.age = 18; p1.introduce(); return 0;}
当我们创建 Person
对象 p1
时,系统会为 p1
分配内存空间,用于存储它的数据成员 name
和 age
。此时,我们可以访问和修改 p1
对象的成员变量和成员函数。
通过这个例子,我们可以看到,类的声明和定义只是描述了类的结构,而类的实例化person p1
这一步才是真正创建了类的对象并分配了内存空间。每个实例化的对象都有自己独立的内存空间,可以访问和修改自己的数据成员。
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。
谜语:“年纪不大,胡子一把,主人来了,就喊妈妈” 谜底:山羊
当然,一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
int main(){ Person._age = 100; // 编译失败:error C2059: 语法错误:“.” return 0;}
注意:Person
类是没有空间的,只有Person
类实例化出的对象才有具体的年龄。
类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
刚才谈到:类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。那我不太信,那我算算看看:
#include <iostream>using namespace std;// 类的声明class Person {public: std::string name; int age; void introduce();};//类的函数定义void Person::introduce(){ std::cout << "My name is " << name << " and I'm " << age << " years old." << std::endl;}int main() { cout << sizeof(Person) << endl; return 0;}
利用sizeof(person)
我们可以看到,即使你没有直接实例化一个 Person
对象,但是 sizeof(Person)
仍然算出一个32
字节的值,这是为啥?
这是因为:
类的内存布局:当你定义一个类时,编译器会为这个类分配一定的内存空间,用于存储类的数据成员。
即使你没有创建任何对象,编译器也需要知道这个类的内存布局,以便在需要创建对象时正确地分配内存。编译时内存分配:
在编译时,编译器会计算出类的总大小,包括所有数据成员的大小。这个总大小就是 sizeof(Person) 的结果。
?类对象模型
? 如何计算类对象的大小
不同以往的C语言结构体,问题是C++类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
让我们来看一个生动容易简单的例子:
class calculate_one{public:void Init(char c1, char c2, int i){_c1 = c1;_c2 = c2;_i = i;}void Print(){cout << _c1 << "-" << _c2 << "-" << _i << endl;}char _c1;char _c2;int _i;};int main(){cout << sizeof(calculate_one) << endl;}
这个类的大小是多少个字节呢?
首先我们想想结构体内存对齐规则:
0
的地址处。其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。 VS 中默认的值为 8
linux
中gcc
没有默认对齐数,对齐数就是成员自身的大小 结构体总大小为最大对齐数(结构体中的每一个成员都有一个对齐数,所有对齐数中的)的整数>倍。如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。 当我们去掉两个函数,计算出字节大小是8,这里不太理解结构体的计算,可以点击这里查找:
【C语言】自定义类型:结构体深入解析(二)结构体内存对齐&&宏offsetof计算偏移量&&结构体传参
当不去掉函数,发现,结果依然不改变。这里就涉及到了类各个成员的存储结构关系:
class calculate{public:void Init(char c1, char c2, int i){_c1 = c1;_c2 = c2;_i = i;}void Print(){cout << _c1 << "-" << _c2 << "-" << _i << endl;}char _c1;char _c2;int _i;};int main(){char a = 'A';char b = 'B';calculate cule1;cule1._i;calculate cule2;cule2._i;cule1.Init(a, b, 1)cule2.Init(a, b, 2);cout << sizeof(calculate) << endl;}
当我们实例化两个对象时,编译器此时就会分配所需空间给两个对象
calculate cule1;cule1._i;calculate cule2;cule2._i;
当我们再去,调用这两个对象里的函数时,会怎样?它怎么存储,看看汇编:
cule1.Init(a, b, 1)cule2.Init(a, b, 2);
我们在我的C++奇迹之旅相遇:支持函数重载的原理也是提到call(函数的地址),call里的括号里的地址就是函数的地址。
可以看出函数的地址是一样的,难道他们都在同一个地方存储函数,或者说在一个固定的公共区存储函数定义,需要时通过地址来查找,因此类对象的存储方式到底是什么样的?
?类对象的存储方式猜想
?猜想一:对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
?猜想二:代码只保存一份,在对象中保存存放代码的地址
节省内存空间,因为成员函数代码只保存一份,不需要为每个对象都保存一份函数代码,提高执行效率。因为只需要加载一份函数代码,不需要重复加载。支持多态,因为不同的对象可以有不同的函数指针,指向不同的函数实现,从而实现多态。
?猜想三:只保存成员变量,成员函数存放在公共的代码段
类的成员函数代码只保存一份,存放在程序的公共代码段中。每个类对象中只保存成员变量的实际数据。对象中不保存任何指向成员函数的指针。
当通过对象调用成员函数时,编译器会根据成员函数的名称和类型,找到对应的函数代码地址,并传入对象自身的this
指针,来完成函数的调用
总结:由于每个成员或者对象的函数地址都是一样的,因此猜想三更合理:只保存成员变量,成员函数存放在公共的代码段
我们再通过对下面的不同对象分别获取大小来分析看下
? 类中什么都没有—空类
class A3{};
声明一个空的类 A3
,编译器仍然需要为该类的实例分配内存空间。即使这个类没有任何成员变量或成员函数,每个对象也需要在内存中占据至少一个字节的空间。这是因为在C++中,每个对象都必须具有唯一的内存地址,以便程序能够准确地引用它们。
这个额外的字节通常被称为“空对象占位符”或“填充字节”,它确保每个对象都有独特的地址。这个字节不会存储任何数据,但是确保了对象在内存中的唯一性,使得程序能够正确地对其进行操作。
这种行为在C++标准中没有明确规定,而是由具体的编译器实现来决定。通常情况下,编译器会为了内存对齐的需要而分配这个额外的字节,以确保对象在内存中的布局符合特定的对齐要求。
所以,即使类 A3
是空的,它的大小也会被编译器分配为至少1字节,以确保每个对象都具有唯一的内存地址。
? 类中仅有成员函数
class A2 {public:void f2() {}};
即使类中仅有成员函数而没有任何成员变量,C++
编译器仍然会为该类的实例分配至少一
个字节的内存空间。这是因为每个对象都必须具有唯一的内存地址,以便程序能够准确地引用它们。
? 类中既有成员变量,又有成员函数
class A1 {public:void f1() {}private:int _a;};
在这个示例中,类 A1
中有一个私有成员变量 _a
和一个公有成员函数 f1()
。根据C++的规则,成员函数不会影响类的大小,因为它们不会被存储在每个对象中。所以,f1()
不会影响 sizeof(A1)
的值。
然而,类 A1
中包含一个 int
类型的私有成员变量 _a
。在32位系统上,int
类型通常占用4个字节的内存空间。因此sizeof(A1)
的大小是4个字节,这个大小正是 _a
的大小。