作为C++的核心单元,对象模型在编译器眼中是如何实现的?本文从几个基本理论模型出发,剖析实际。 深度探索C++对象模型 ---- 简单对象模型 对象存放若干slots,由slot指向实际成员。 表驱动对象模型 这个模型的function部分可以看做在上面的简单对象模型基础上再增加了一层间接性,因此被称作双表格模型。IBM的系统对象模型SOM也依赖于这种模型。 例如,如果Derived继承自Base1和Base2基类,temp为已知Derived指针。 在派生类指针赋值给基类指针时编译器需要调整 Base2 * pbase2 =temp ? ---- C++对象模型 上述模型的Extension部分其实已经涵盖了部分对象模型的静态结构,而对象模型的生成与维护则更多见原书中的一系列章节。 Bjarne Stroustrup设计的C++对象模型从简单对象模型派生而来,对内存空间和存取时间做了优化。
Markdown 画图工具 Processon 1,关于对象 从这篇博客开始真正介绍C++对象模型,前边BB了那么多没用的,终于开始了C++对模型的分析。 关于C++对象模型的介绍,我将根据《深度探索C++对象模型》这本书,其书中的每一章,对应一篇博客,博客内容为自己对这本书的理解和补充吧。 pd.init(&pd); } 1.2 class 需要指出的是,C++类的非static的成员函数都有一个隐式的参数,即this(class object *const this)指针(对象的首地址) delete p; } class point的对象对应的内存布局 class point2d的对象对应的内存布局 通过对比point和point2d的对象内存布局,可知,如果父类中定义了虚函数,并且在子类中进行了重写 ,则在子类的对象模型中,用子类重写的函数的地址将父类的虚函数地址替换掉,否则不进行替换。
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10)
Bear"; } private: int cell; }; int main() { Bear b(1, 2) ; ZooAnimal z = b; z.print(); } 以上代码对应的汇编如下(g++ -S -m32 p27.cc): _ZN9ZooAnimalC2ERKS_ pushl $1 leal -24(%ebp), %eax //eax=ebp-24, b对象首地址 pushl %eax //b的首地址压栈 ,即b.this压栈 call _ZN4BearC1Eii //Bear::Bear(b.this, 1, 2) addl $16, %esp 图示 3.参考 《深度探索C++对象模型》
成员函数和成员变量分开存储 只有非静态成员变量才属于对象上。 每个空对象占用的内存空间为:1。c++编译器会给每个空对象也分配一个内存空间,是为了区分空对象占内存的位置。 每个空对象应该有一个独一无二的空间。 函数也不会占用对象空间,所有对象共享一个函数实例。 sizeof(p1) << endl; } int main() { test(); system("pause"); return 0; } 此时输出: 1 4 4 说明了:空对象也是有 静态成员变量并不属于特定的某一个对象,同理,静态成员函数也不属于某一个对象。进一步来说:所有对象共享一个成员函数实例。
跟着箭头走是第一个虚表,存放这个类中有几个虚函数,就像Shape类有两个虚函数,所以就存放这两个虚函数的调用地址;后面接着箭头走的就是经过Shape对象调用的虚函数 再看第二个类,它继承了Shape的全部再加上它自身的数据类型 直接调用此类中的即可 //如果不是指针调用 quadShape b; Shape a = (Shape)b; a.draw(); //输出的也必然是shape里的draw,因为是静态绑定,什么对象调用 mm.push_back(ss1); ss->draw(); ss1->draw(); cout << mm.size() << endl; //对于ss指针对象 以下例子说明this的作用 //把上述Shape类里面的vfunc2()函数改为 void vfunc2(){ cout << "设计模式Template Method" << endl 所以其实qs.vfunc2()等价于Shape::vfunc2(&quadShape); 在vfunc2()函数里,draw()其实也就是this->draw();(this就是quadShape的指针对象
// 如果testl的结果非0,jmp .L2 ,jne means jmp if not equal movl -12(%ebp), %eax //eax=[ebp-12], 即 eax=bra.pnext testl %eax, %eax je .L4 //如果testl的结果为0, jmp .L4 .L2: movl subl $12, %esp leal -20(%ebp), %eax //eax=bar的首地址 pushl %eax //bar对象首地址压栈 addl $4, %eax //eax=eax+4,即foo的首地址 subl $12, %esp pushl %eax //foo对象的 -_ZN3BarC2Ev .weak _ZN3BarC1Ev .set _ZN3BarC1Ev,_ZN3BarC2Ev .text
eax), %eax //eax=vptr addl $8, %eax //eax=vptr+8 movl (%eax), %eax //eax=vptr[2] virtual机制 subl $12, %esp pushl -12(%ebp) //b压栈 call *%eax //call vptr[2] */ jmp .L37 subl $12, %esp pushl %eax //压栈 call _ZN1XC2Ev //X:X() /* _ZN1XC2Ev对应的代码如下: _ZN1XC2Ev: //X::X() .LFB1044: .cfi_startproc pushl //eax=this subl $8, %esp pushl %edx pushl %eax call _ZN1AC2Ev
1.何为C++对象模型? 引用《深度探索C++对象模型》这本书中的话: 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分。 对于各种支持的底层实现机制。 对象模型概述:介绍简单对象模型、表格驱动对象模型,以及非继承情况下的C++对象模型。 继承下的C++对象模型。 4.3.非继承下的C++对象模型 概述:在此模型下,nonstatic 数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。 这与上述的C++对象模型相符合。 这个结果与我们的C++对象模型图完全符合。
的说明 (1)我们通过自己的打印输出也可以看出来,这个时候是不能打印输出age,address的,因为他们不像public里面的name,他们一个是被保护的,一个是私有的,因此我们无法直接进行访问; 2. 变量是私有的,这个可以提高我们的代码的安全性;这个里面我们还自定义了一个新的函数对年龄进行加加操作,主函数里面没有调用这个函数之前age是20,调用这个函数以后就变成了21; 3.this指针指向语句里面的当前对象 ,这个时候name进行了接收,这个时候因为我们使用了this指针,所以编译器就可以正常识别,知道是把后面的参数赋值给前面的变量; 4.成员函数仅仅处理一个成员变量,处理多个成员变量需要独立的外部函数,对象可以作为函数的参数 (1)这段代码分别求了个人3门科目的平均分,以及3个人同一门科目的平均分; (2)getaverage是求单个人3门成绩的平均分。 (1)这个就是使用对象数组作为函数的参数;传递的是数组名,我们依然使用数组进行接收,这个和C语言里是类似的; (2)就相当于使用数组表示原来的s1,s2,s3,s4; (3)这个里面调用setstudent
1.类的默认成员函数 在 C++ 中,如果一个类没有显式定义某些成员函数,编译器会自动为该类生成默认的成员函数。 2.构造函数 2.1 构造函数的定义 在 C++ 中,构造函数是一种特殊的成员函数,用于初始化对象的数据成员。构造函数的名称必须与类名相同,并且没有返回类型,即使是 void 也不行。 2. 无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此) 3. 对象实例化时系统会自动调用对应的构造函数。 4. 构造函数可以重载。 2. 无参数无返回值。 (这里跟构造类似,也不需要加void) 3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。 4. 对象生命周期结束时,系统会自动调用析构函数。 一个局部域的多个对象,C++规定后定义的先析构。
接着上一章,这一次我将继续介绍C++类与对象的知识: 1.类的默认成员函数 用户没有显式实现,编译器自动生成的成员函数,即默认成员函数。 C++规定:对象在销毁时会自动调用析构函数,完成对象中资源(其实就是手动申请的空间资源,需要动态开辟空间,比如malloc之类的)的清理释放工作。 我们来说说第7个特点:⼀个局部域的多个对象,C++规定后定义的先析构 int main() { Date d1(2025,10,30); Date d2(2025,11,9); 1.4赋值运算符重载 1.4.1运算符重载 定义: 运算符重载是 C++ 面向对象特性的重要组成部分。 访问并且调用了这个函数,以实现d1 - d2; 第二种则是直接相减,d1 - d2 ,编译器会把这句表达式,按照C++标准规定的语法逻辑 翻译成 d1.operator-(d2) ,也就是显式调用,
一、六大默认成员函数 C++为了弥补C语言的不足,设置了6个默认成员函数 二、构造函数 2.1 概念 在我们学习数据结构的时候,我们总是要在使用一个对象前进行初始化,这似乎已经成为了一件无法改变的事情 对象生命周期结束时,C++编译系统系统自动调用析构函数 typedef int DataType; class Stack { public: Stack(size_t capacity = 3) 4.3 使用场景 1、使用已存在对象创建新对象 2、函数参数类型为类类型对象 3、函数返回值类型为类类型对象 为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 但是C++中的*this指针是隐含的参数,我们没办法直接加,C++为了解决此类问题,规定当我们将const修饰放在成员函数后面的时候,默认就是将该成员函数隐藏的*this进行const修饰 将const 所以+=可以直接就操作,+却要重新创建一个类对象然后进行拷贝,再拷贝的对象上进行操作 注意事项: 1、+=和-=都是引用返回、不能用const修饰(改变原对象),返回值是类(支持连续操作) 2、+和-都是传值返回
构造函数的特点: 函数名就是类名 无返回值(返回值什么都不需要给,也不需要写void,C++规定) 类实例化对象时会自动调用对应的构造函数 如果类中没有显示的定义构造函数,则C++编译器会自动生成一个无参的默认构造函数 C++规定,对象销毁时自动调用析构函数,完成对对象中资源的清理释放工作。 C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返 回都会调用拷贝构造完成。 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。 d1); // Date d2 = d1; // 可以利用指针完成拷贝,但是不是拷贝构造,只是⼀个普通的构造 Date d5(&d1); // 传值传参 C++规定自定义类型对象进行拷贝行为必须调用拷贝构造 5.1 运算符重载 运算符重载的特点: 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。
2.构造函数 构造函数是特殊的成员函数。虽然名叫构造函数,但是它的主任务并不是开空间创建对象,而是对象实例化时初始化对象。 相当于Stack中的Init函数. ,C++中规定对象在销毁时会自动调用析构函数,完成对象中资源的清理和释放工作。 (3)C++规定自定义类型对象进行传值传参时必须调用拷贝构造,所以自定义类型进行传值传参和传值返回都会调用拷贝构造来完成。 (4)如果未显式写拷贝构造函数,编译器会自动生成拷贝构造函数。 +规定自定义类型对象进行拷贝行为必须调用拷贝构造,这里传值传参要调用拷贝构造 Date d2(d1); Date d2 = d1;//这两种写法都是可以的,也都是拷贝构造 return 0; } 5.赋值运算符重载 5.1 运算符重载 当运算符被用到类类型对象时,C++允许我们用运算符进行重载的形式指定新的定义。
这篇文章主要介绍了详解C++对象模型和this指针,是C++入门学习中的基础知识,需要的朋友可以参考下,希望能够给你带来帮助对象模型成员变量和成员函数分开存储一、只有非静态成员变量才属于类的对象上空对象占用字节为 this 指针指向被调用成员函数所属的对象特点:1. this指针是隐含每一个非静态成员函数内的一种指针2.this 指针不需要定义,直接使用即可。 用途:1.当形参和成员变量同名时,可用this指针来区分2.在类的非静态成员变量中返回对象本身,可使用return *this一、class Person{public:Person(int age)/ 但如果函数,不使用引用方法,返回的是一个值,就会创建新的对象Person PersonAddAge(Person &p)//不使用引用方法{this->age += p.age;// this是指向p2 所以每一次Person PersonAddAge()后,都是一个新的对象,所以最后输出结果p2 是不变的20。疑问:至于为什么不是p2 为 10 。 以值方式返回局部对象会调用拷贝构造函数。
本文参考深度探索C++对象模型 编译器为未定义构造函数的类合成默认构造和拷贝构造函数 如果你已经开始点头了,那么你和我一样,陷入了深深的误解。 当我看到书中作者的这句话时,几乎是一身冷汗。 在C++中,class和struct在某些实现中是转换等同的。那么,为什么还需要合成构造函数呢?按C的做法来不就好了么。 同理,在C++中,完全也可以进行这样的处理,而根本不需要合成一个构造函数! 这样的情形,我们称之为trivial. ---- 那么,什么时候才叫nontrivial呢。
本文参考深度探索C++对象模型/ISO文档 析构函数必须为虚,构造函数不能为虚,因为在对象完全构造之前是没有类型的,也不存在虚表,所以虚构造函数也就不可能发生。 但是,我从短暂的人生当中学到一件事......越是玩弄C++,就越会发现人类的能力是有极限的......除非超越人类。CC,我不做人了! 在C++中,这种idiom又被称为“虚构造函数“,是基于语法的拓展。 这里的虚构造函数,能够按照指针指向的实际对象给出多态式的拷贝与默认构造。 尽管知道指向的是Derived类 Derived* pb2 =pb1->clone(); //允许!
C++兼容了C语言,当然也支持这种编程范式。 但C++更主要的特点在支持基于对象(object-based, OB)和面向对象(object-oriented, OO),OB和OO的基础是对象封装,所谓封装就是将数据和数据的操作(函数)组织在一起, 在C++中,即使是空对象也会占用一定的空间,通常是1个字节。这个字节用来确保每个对象都有唯一的地址,以便在程序中进行操作。 结果表明,在这种情况下,C++的对象的内存布局跟C语言的结构的内存布局是一样的,并不会比C语言多占用一些内存空间。 静态数据成员 C++的类也支持在类里面定义静态数据成员,那么定义了静态数据成员之后类对象的内存布局是怎么样的呢?
原因分析 深度探索C++对象模型 1.3 章节 https://github.com/wangcy6/weekly/blob/master/reading-notes/object-model/ 译成中文就是,编译器必须要确保如果一个对象有一个或多个vptr,这些vptr不是由原对象来初始化或改变的。 也就是说:当使用赋值的方式或拷贝构造的方式创建一个对象时,这个对象的vptr与源对象无关。 +对象模型》的4.2节能够找到完美答案,具体摘抄如下: “表格中的virtual functions地址是如何被建构起来的? 在C++中,virtual functions(可经由其class object被调用)可以在编译时期获知。此外,这一组地址是固定不变的,执行期不可能新增或替换之。 +对象模型-构造函数语义学 补充内容 错误理解1 :如果类没有定义任何构造函数,编译器一定会自动生成默认的构造函数 注意:这种说法是错误的(编译器太懒了) 正确的说法: 惟有默认构造函数”被需要“的时候编译器才会合成默认构造函数