C++对象模型探索--02对象
对象结构的发展和演化
类对象所占用的空间
- 类的成员函数不占用类的内存空间
- 一个类对象至少占用1个字节的内存空间
- 成员变量占用对象的内存空间
总结:成员变量是包含在每个对象中的,是占用对象字节的。而成员函数虽然也写在类的定义中,但成员函数不占对象字节数的(不占用内存空间)。
c++对象模型逐步建立起来
- 非静态的成员变量(普通成员变量)跟踪类对象走(存在对象内部),也就是每个类对象都有自己的成员变量
- 静态成员变量跟对象没有什么关系,所以肯定不会保存在对象内部,是保存在对象外面的
- 成员函数:不管静态的还是非静态的,全部保存在类对象之外。所以不管有几个成员函数,不管是否是静态的成员函数,对象的sizeof的大小都是不增加的
- 虚函数:不管几个虚函数,sizeof()都是多了4个字节
- 类里只要有一个虚函数(或者说至少有一个虚函数),这个类会产生一个指向虚函数的指针
- 有两个虚函数,那么这个类就会产生两个指向虚函数的指针
- 类本身指向虚函数的指针(一个或者一堆)要有地方存放,存放在一个表格里,这个表格我们称为”虚函数表(virtual table[vtbl])“,这个虚函数表一般保存在可执行文件中的,在程序执行的时候载入到内存中来
- 虚函数表是基于类的,跟着类走的
- 说说类对象,这4个字节的增加,其实是因为虚函数的存在,因为有了虚函数的存在,导致系统往类对象中添加了一个指针,这个指针正好指向这个虚函数表(vptr)。这个vptr的值有系统在适当的时机(比如构造函数中通过增加额外的代码)
总结:对于类中
- 静态数据成员不计算在sizeof内
- 普通成员函数和静态成员函数不计算在类对象的sizeof()内
- 虚函数不计算在类对象的sizeof()内,但是虚函数会让类对象的sizeof()增加4个字节以容纳虚函数表指针
- 虚函数表(vtbl)是基于类的(跟着类走,跟对象没关系,不是基于对象的)
- 如果有多个数据成员,那么为了提高访问速度,某些编译器可能会将数据成员之间的内存占用比例进行调整。(内存字节对齐)
- 不管什么类型的指针(char *p, int *q)该指针占用内存的大小是固定的
this指针调整
this指针调整:发生在多重继承中
派生类对象是包含基类子对象的。如果派生类只从一个基类继承,那么这个派生类对象的地址(首地址)和基类的首地址相同,如果派生类对象同时继承多个基类,那么第一个基类子对象的开始地址和派生对象的开始地址相同,后续这些基类子对象的开始地址和派生类对象的开始地址相差前边那些基类子对象所占用的内存空间
总结:调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中对应该子类对象的起始地址那去。
#include <iostream>
#include <stdio.h>class A
{public:int a;A(){printf("A::A()的this指针是:%p\n", this);}void funcA(){printf("A::funcA()的this指针是:%p\n", this);}
};class B
{public:int b;B(){printf("B::B()的this指针是:%p\n", this);}void funcB(){printf("B::funcB()的this指针是:%p\n", this);}
};class C : public A, public B
{public:int c;C(){printf("C::C()的this指针是:%p\n", this);}void funcC(){printf("C::funcC()的this指针是:%p\n", this);}
};int main(int argc, char **argv)
{std::cout << "sizeof(A) :" << sizeof(A) << std::endl;std::cout << "sizeof(B) :" << sizeof(B) << std::endl;std::cout << "sizeof(C) :" << sizeof(C) << std::endl;std::cout << std::endl;C objc;std::cout << std::endl;objc.funcA();objc.funcB();objc.funcC();return 0;
}
输出结果为:
sizeof(A) :4
sizeof(B) :4
sizeof(C) :12A::A()的this指针是:0x7ffd7c617dfc
B::B()的this指针是:0x7ffd7c617e00
C::C()的this指针是:0x7ffd7c617dfcA::funcA()的this指针是:0x7ffd7c617dfc
B::funcB()的this指针是:0x7ffd7c617e00
C::funcC()的this指针是:0x7ffd7c617dfc
obj构造函数语义
构造函数
默认构造函数(缺省构造函数):没有参数的构造函数。传统认识认为如果我们自己没定义任何构造函数,
那么编辑器就会为我们隐式自动定义一个默认的构造函数,我们称这种构造函数为:合成的默认构造函数,
合成的默认构造函数只有在必要的时候编译器才会为我们合成出来,而不是必然或者必须为我们合成出来。父类带缺省构造函数,子类没有任何构造函数,那因为父类这个缺省的构造函数要被调用,所以编译器会为这个子类合成出一个默认构造函数。
合成的目的是为了调用父类的构造函数。换句话说,编译器合成了默认的构造函数,并在其中安插代码调用其父类的缺省构造函数。如果一个类含虚函数,但没有任何构造函数时,因为虚函数的存在,编译器会给我们生成一个基于该类的虚函数表vftable。编译器给我们合成一个构造函数,在其中安插代码,并把类的虚函数表地址赋给类对象的虚函数表指针,
我们可以把虚函数表指针看成是我们表面上看不见的一个类的成员函数。当我们有自己的默认构造函数时,编译器会根据需要扩充我们自己写的构造函数代码,比如调用父类构造函数,给对象的虚函数表指针赋值。没有默认构造函数时必要情况下编译器帮我们
合成默认构造函数,如果有默认构造函数时,编译器会根据需要扩充默认构造函数。如果一个类带有虚基类(通过2个直接基类继承一个间接基类,所以虚基类一般是三层结构),编译器也会为它合成一个默认构造函数。虚基类结构编译器为子类和父类都产生了合成的默认构造函数。
class A {public:};class B1: virtual public A {public:};class B2: virtual public A {public:};class C : public B1, public B2 {public: };
g++ -fdump-class-hierarchy -c main.cpp:可以输出详细的虚函数表内容
拷贝构造函数语义
传统上大家认为如果我们没有定义一个自己的拷贝构造函数,编译器会帮助我们合成一个拷贝构造函数。这个合成的拷贝构造函数也是在必要的时候才会被编译器合成出来。
#include <iostream>
#include <stdio.h>class B{
public:int m_b;
};class A{public:int m_a;B b;
};
int main(int argc, char **argv)
{A obja0;obja0.m_a = 10;obja0.b.m_b = 10;A obja1 = obja0;// 调用拷贝构造函数,这个obja1.m_a=10;是编译器内部一个手法(成员变量初始化)// 直接按值拷贝过去,编译器不需要合成拷贝构造函数// obja1 = obja0; 是拷贝构造一个对象// 我们没有写类A的拷贝构造函数,编译器也没有帮助我们生成拷贝构造函数。// 我们却发现obja0对象的一些成员变量值确实被拷贝到obja1中去,这是编译器内部的一些直接拷贝数据的手法// 比如类A中有类型B成员变量b,也会递归的去拷贝类B的每个成员变量。 return 0;
}
如果我们不写自己的拷贝构造函数,在以下情况下编译器会帮助我们合成出拷贝构造函数来。
如果一个类A没有拷贝构造函数,但是含有一个类类型B的成员变量,该类型B有拷贝构造函数,那么当代码中有涉及到类A的拷贝构造时,编译器会为A合成一个拷贝构造函数。
编译器合成的拷贝构造函数往往都是干一些特殊的事情,如果只是一些类成员变量的值拷贝这些事,编译器是不用专门合成出拷贝构造函数来干的。#include <iostream> #include <stdio.h>class B{ public:B(const B&) {std::cout << "B copy construct done" << std::endl;}B(){}int m_b; };class A{public:int m_a;B b; // 含有一个类类型B的成员变量b,且B类型含有拷贝构造函数 };int main(int argc, char **argv) {A obja0;obja0.m_a = 10;obja0.b.m_b = 10;A obja1 = obja0; // 调用拷贝构造函数return 0; }
如果一个类C没有拷贝构造函数,但是它有一个父类B,父类有拷贝构造函数,当代码中有涉及到C的拷贝构造时,编译器会为C合成一个拷贝构造函数,调用父类的拷贝构造函数。
#include <iostream> #include <stdio.h>class B{ public:B(const B&) {std::cout << "B copy construct done" << std::endl;}B(){}int m_b; };class C : public B{public:int m_c; };int main(int argc, char **argv) {C objc0;C objc1 = objc0;return 0; }
如果一个类C没有拷贝构造函数,但是该类声明了或者继承了虚函数,当代码中有涉及到C的拷贝构造函数时,编译器会为C合成一个拷贝构造函数,往这个拷贝构造函数里插入语句:这个
语句的含义是设定类对象的虚函数表指针值。
声明虚函数#include <iostream> #include <stdio.h>class C {public:int m_c;virtual void vfunc() {std::cout << "virtual function call" << std::endl;} };int main(int argc, char **argv) {C objc0;C objc1 = objc0;return 0; }
继承虚函数
#include <iostream> #include <stdio.h>class B{ public:B(const B&) {std::cout << "B copy construct done" << std::endl;}B(){}virtual void vfunc() {std::cout << "virtual function call" << std::endl;}int m_b; };class C : public B{public:int m_c; };int main(int argc, char **argv) {C objc0;C objc1 = objc0;return 0; }
如果一个类C没有拷贝构造函数,但是该类含有虚基类,当代码中有涉及到类C的拷贝构造时,编译器会为该类生成一个拷贝构造函数。
#include <iostream> #include <stdio.h>class A{ public:}; // 虚继承 class B1: virtual public A { public: };class B2: virtual public A { public: };class C: public B1, public B2 { public: };int main(int argc, char **argv) {C objc0;C objc1 = objc0;return 0; }
成员初始化列表
下列情况必须用成员初始化列表
- 如果这个类的成员变量是个引用
class A {public:int &m_val0; // 成员是引用A(int &val):m_val0(val) };
- 如果这个类的成员变量是个const类型成员
class A {public:int &m_val0;const int m_val1; // 成员是const 类型A(int &val):m_val0(val),m_val1(val) };
- 如果这个类是个继承一个基类,并且基类中有构造函数,这个构造函数里边含有参数
class B{public:int m_a;int m_b;B(int a, int b); };class A : public B{public:int &m_val0;const int m_val1; A(int &val):m_val0(val),m_val1(val),B(val,val) };
- 如果类的成员变量类型是某个类类型,而这个类的构造函数带参数时
class C{public:int m_a;C(int a); };class A{public:int &m_val0;const int m_val1; C c_c;A(int &val):m_val0(val),m_val1(val),c_c(val) };
使用初始化列表的优势
使用初始化列表能够提高程序的运行效率。
不使用初始化列表调用方式
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <iostream>class C{public:int m_a;C(int val=0):m_a(val){printf("this=%p ", this);printf("C(int)构造函数被调用\n");}C(const C &val){printf("this=%p ", this);printf("C拷贝构造函数被调用\n");}C &operator=(const C &val){printf("this=%p ", this);printf("C拷贝赋值运算符被调用\n");return *this;}~C(){printf("this=%p ", this);printf("C析构函数被调用\n");}};class A{public:C c_c;int m_val;A(int val) // 这里构造了c_c,耗费一次构造函数调用{c_c = 1000; // 构造一个临时对像,把临时对下内容赋值给c_c,释放掉临时对象m_val = val;}
};int main(int argc, char **argv)
{A obj(1000);return 0;
}
输出结果为:
this=0x7ffff2950d10 C(int)构造函数被调用this=0x7ffff2950cd4 C(int)构造函数被调用
this=0x7ffff2950d10 C拷贝赋值运算符被调用
this=0x7ffff2950cd4 C析构函数被调用this=0x7ffff2950d10 C析构函数被调用
gdb调试现象如下:
(gdb) b main.cpp:49
Breakpoint 1 at 0x120c: file main.cpp, line 49.
(gdb) r
Starting program: /home/xiaxin/workspace/cplus/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Breakpoint 1, main (argc=1, argv=0x7fffffffdd48) at main.cpp:49
49 A obj(1000);
(gdb) s
A::A (this=0x7fffffffdc10, val=1000) at main.cpp:40
40 A(int val)
(gdb) n
41 {(gdb) n
this=0x7fffffffdc10 C(int)构造函数被调用
42 c_c = 1000;
(gdb) n
this=0x7fffffffdbd4 C(int)构造函数被调用
this=0x7fffffffdc10 C拷贝赋值运算符被调用
this=0x7fffffffdbd4 C析构函数被调用
43 m_val = val;
(gdb)
使用初始化列表调用方式
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <iostream>class C{public:int m_a;C(int val=0):m_a(val){printf("this=%p ", this);printf("C(int)构造函数被调用\n");}C(const C &val){printf("this=%p ", this);printf("C拷贝构造函数被调用\n");}C &operator=(const C &val){printf("this=%p ", this);printf("C拷贝赋值运算符被调用\n");return *this;}~C(){printf("this=%p ", this);printf("C析构函数被调用\n");}};class A{public:C c_c;int m_val;A(int val):c_c(1000) // 初始化列表调用{m_val = val;}
};int main(int argc, char **argv)
{A obj(1000);return 0;
}
输出结果为:
this=0x7ffe14386950 C(int)构造函数被调用
this=0x7ffe14386950 C析构函数被调用
C++对象模型探索--02对象相关推荐
- Cpp 对象模型探索 / new 对象时加括号和不加括号时的差别
一.结论 CTest *pt1 = new CTest(); CTest* pt2 = new CTest; 1.若类 CTest 是空类,则二者 new 的结果没有区别. 2.若类存在显示声明的缺省 ...
- 深入探索C++对象模型之C++对象(vs,gcc,clang测试)
日期 更新内容 2020.12.17上午 之前虚继承部分有问题,重新进行了测试,更新了结构图 2020.12.17下午 添加了gcc测试 2020.12.17晚上 添加了clang测试 1 C++对象 ...
- Cpp 对象模型探索 / 静态局部对象只构造一次的原因和执行析构的方法
代码 class A { public:A() {}~A() {} };void func() {static A a; }int main() {func();return 0; } 问题 1.函数 ...
- Cpp 对象模型探索 / 编译器为对象创建缺省析构函数的条件
1.基类中含有析构函数的子类,编译器为子类创建析构函数. 2.类成员变量是类对象,该类对象含有析构函数,则编译器为子类创建析构函数. 代码 class Parent { public:~Parent( ...
- Cpp 对象模型探索 / 多重继承下基类指针释放子类对象的原理说明(虚析构函数的作用)
源码 #include <iostream>class Base1 { public:virtual void func_1_1(){ std::cout << "B ...
- Cpp 对象模型探索 / 对象访问成员变量的原理
一.栗子 1.源码 #include <iostream> #include <stdio.h>class Base { public:Base() { std::cout & ...
- Cpp 对象模型探索 / 对象的虚函数表指针的位置
一.源码 #include <iostream>class A { public:virtual void func(){};public:int count_ = 0; };int ma ...
- Cpp 对象模型探索 / 编译器为对象创建缺省构造函数的条件
零.前言 书本上常说,编译器会给没有任何构造函数的类自动创建一个缺省的构造函数(没有形参的构造函数).但是事实上不是这样么?栗子: class A { public:int i; };int main ...
- C++对象模型探索 / 普通类对象占用的空间
一.空类的大小 #include <iostream>class A{};int main() {A obja;std::cout << "obja 的地址:&quo ...
最新文章
- Wireshark数据包分析之DHCP协议包解读
- 查找_排序_思维导图
- 搭建一个基于http的yum服务器
- C - Mr. Panda and Strips Gym - 101194C(思维//尺取//2016 icpc china final)
- 推荐算法--推荐系统冷启动问题(03)
- BasKet Note Pads-利用软件作条记
- python watchdog的使用_python watchdog监控文件修改
- NHL明星与美国冰球协会联手发起NFT拍卖
- 电脑如何查看x86与arm_电脑关联程序更改 如何更改电脑查看图片的方式
- C64x+中断控制器
- linux打开caj文件,在Deepin、UOS、Linux下打开caj格式文件的软件
- 数据结构课程设计 # 论文查重分析系统 (C/C++版和python版)
- KALI--入门教程--kali下载(vm),更新国内源,更换中文界面
- 任务调度+资源调度整合(学习笔记)
- 手机兼容性测试--testin云测流程
- 大商创x支持mysql版本_【大商创安装】大商创X宝塔面板安装配置简述
- matlab数学实验报告西安交通大学微分方程模型高为16米,数学实验第二次作业——常微分方程数值求解...
- 201571030314/201571030316《小学四则运算软件软件需求说明》结对项目报告
- MySQL基础之查询语句
- B75对应的服务器芯片组,两代主力 编辑带你看B75和H61相差多少