对象结构的发展和演化
类对象所占用的空间
- 类的成员函数不占用类的内存空间
- 一个类对象至少占用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) :12
A::A()的this指针是:0x7ffd7c617dfc
B::B()的this指针是:0x7ffd7c617e00
C::C()的this指针是:0x7ffd7c617dfc
A::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析构函数被调用