文章目录
- 一、类对象所占用的空间
- 二、对象结构的发展和演化
- 三、this 指针调整
- 四、构造函数语义
- 五、拷贝构造函数语义
- 六、程序转化语义
- 七、程序的优化
- 八、程序优化续、拷贝构造续,深浅拷贝
- 九、成员初始化列表
- 十、虚函数表指针位置分析
- 十一、继承关系作用下虚函数的手工调用
- 十二、虚函数表分析
- 十三、多重继承虚函数表分析
- 十四、辅助工具,vptr、vtbl创建时机
- 十五、类不纯时引发的虚函数调用问题
- 十六、数据成员绑定时机
- 十七、进程内存空间布局
- 十八、数据成员布局
- 十九、数据成员存取
- 二十、单一继承下的数据成员布局
- 二十一、单类单继承虚函数下的数据成员布局
- 二十二、多重继承数据布局与this调整深谈
- 二十三、虚基类问题的提出和初探
- 二十四、两层结构时虚基类表内容分析
- 二十五、三层结构时虚基类表内容分析
- 二十六、成员变量地址、偏移、指针等重申
- 二十七、普通成员函数调用方式
- 二十八、虚成员函数、静态成员函数调用方式
- 二十九、静动态类型、绑定,坑点,多态体现深谈
- 三十、单继承虚函数趣味性测试和回顾
- 三十一、多继承虚函数深释
- 三十二、多继承第二基类虚函数支持、虚继承带虚函数
- 三十三、RTTI运行时类型识别回顾与存储位置介绍
- 三十四、函数调用、继承关系性能说
- 三十五、指向成员函数的指针及vcall进一步谈
- 三十六、inline函数回顾和扩展细节说
- 三十七、继承体系下的对象构造步骤
- 三十八、对象复制语义学、析构函数语义学
- 三十九、局部对象、全局对象的构造和析构
- 四十、局部静态对象、对象数组构造析构和内存分配
- 四十一、临时性对象的详细探讨
- 四十二、模板及其实例化详细分析
- 四十三、邪门歪道
一、类对象所占用的空间
知识点归纳:
- 一个类对象至少占用1个字节的内存空间;
- 成员变量占用对象的内存空间(见演示代码);
- 类的成员函数不占用类对象的内存空间,每个类只诞生一个成员函数(跟着类走),而不管用这个类产生了多少个该类的对象。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
class A {
public:
void func() {}; // 成员函数
void func1() {}; // 成员函数
void func2() {}; // 成员函数
// char ab; // 成员变量,char类型占一个字节内存
int ab; // int类型占4个字节
};
// 类对象所占用的空间
int main() {
A obja, objb;
int ilen = sizeof(obja); // sizeof(A) = 1
cout << ilen << endl;
obja.ab = 12;
objb.ab = 24;
return 1;
}
二、对象结构的发展和演化
知识点归纳:
- 非静态的成员变量(普通成员变量)跟着类对象走(存在对象内部),也就是每个类对象都有自己的成员变量;
- 静态成员变量跟对象没有什么关系,所以肯定不会保存在对象内部,是保存在对象外面(表示所占用的内存空间和类对象无关)的;
- 成员函数,不管静态的还是非静态,全部都保存在类对象之外。所以不管几个成员函数,不管是否是静态的成员函数,对象的
sizeof()
的大小都是不增加的; - 虚函数,不管几个虚函数,
sizeof()
都是多了4个字节。(1)类里只要有一个虚函数(或者说至少有一个虚函数),这个类会产生一个指向虚函数的指针,有两个虚函数,那么这个类就会产生两个指向虚函数的指针,类本身指向虚函数的指针(一个或者一堆)要有地方存放,存放在一个表格里,这个表格就称为“虚函数表(virtual table[vtbl])”,这个虚函数表一般是保存在可执行文件中的,在程序执行的时候载入到内存中来,虚函数表是基于类的,跟着类走的;(2)因为有了虚函数的存在,导致系统往类对象中添加了一个指针,类对象增加的四个字节,其实是因为虚函数的存在,这个指针正好指向这个虚函数表,很多资料上把这个指针叫vptr,这个vptr的值由系统在适当的时机(比如构造函数中通过增加额外的代码来给值)。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
class myobject {
public:
myobject(){}; // 构造函数
virtual ~myobject(){}; // 析构函数
float getvalue() const { // 普通成员函数
return m_value;
}
static int s_getcount() { // 静态成员函数
return ms_scount;
}
virtual void vfrandfunc(){}; // 虚函数
protected:
float m_value; // 普通成员变量 4字节
static int ms_scount; // 静态成员变量
};
int main() {
int ilen = sizeof(aobj);
cout << ilen << endl;
//int ilen2 = sizeof(char *);
//int ilen3 = sizeof(int *);
myobject obj;
int ilen = sizeof(obj);
cout << ilen << endl; // 8字节,成员变量m_value占4字节,虚函数表指针占4字节。
return 1;
}
对于类中,(1)静态数据成员不计算在类对象sizeof()内;(2)普通成员函数和静态成员函数不计算在类对象的sizeof()内;(3)虚函数不计算在类对象的sizeof()内,但是虚函数会让类对象的sizeof()增加4个字节以容纳虚函数表指针;(4)虚函数表[vtbl]是基于类的(跟着类走的,跟对象没关系,不是基于对象的);(5)如果有多个数据成员,那么为了提高访问速度,某些编译器可能会将数据成员之间的内存占用比例进行调整(内存字节对齐);(6)不管什么类型指针,比如char *p
或int *q
,该指针占用的内存大小是固定的。类对象大小的组成:(1)非静态成员变量所占的内存总量以及这些成员变量之间字节对齐所额外占用的内存;(2)若有虚函数,则会产生虚函数表指针(vptr)。当然,如果类之间是多重继承关系。并且每个父类都有虚函数,情况不同,后续探讨。
三、this 指针调整
知识点归纳:
- 派生类对象,它是包含基类子对象的;
- 如果派生类只从一个基类继承的话,那么这个派生类对象的地址和基类子对象的地址相同;
- 但如果派生类对象同时继承多个基类,那么就要注意:第一个基类子对象的开始地址和派生类对象的开始地址相同,后续这些基类子对象的开始地址和派生类对象的开始地址相差多少呢?那就得把前边那些基类子对象所占用的内存空间干掉。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
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); }
void funcB() { printf("C::funcB()的this指针是:%p!\n", this); }
};
int main() {
// this指针调整:多重继承
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
C myc;
myc.funcA();
myc.funcB(); // 已经被子类C覆盖了
myc.B::funcB();
myc.funcC();
return 1;
}
总结:调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中对应该子类对象的起始地址那去。
四、构造函数语义
知识点归纳:
- 默认构造函数(缺省构造函数):没有参数的构造函数,传统认识认为如果我们自己没定义任何构造函数,那么编译器就会为我们隐式自动定义一个默认的构造函数,这种构造函数称为:“合成的默认构造函数”;
- “合成的默认构造函数”,只有在必要的时候,编译器才会为我们合成出来,而不是必然或者必须为我们合成出来。必要的时候是什么时候?(1)该类MBTX没有任何构造函数,但包含一个类类型的成员ma,而该对象ma所属于的类MATX有一个缺省的构造函数,这个时候,编译器就会为该类MBTX生成一个 “合成默认的构造函数”,合成的目的是为了调用MATX里的默认构造函数,换句话说就是编译器合成了默认的MBTX构造函数,并且在其中安插代码,调用MATX的缺省构造函数;
#include "pch.h"
#include <iostream>
using namespace std;
class M0TX {
public:
M0TX() { // 默认构造函数
cout << "wodeceshi" << endl;
}
};
class MATX {
public:
MATX() { // 默认构造函数
cout << "goodHAHAHAHA" << endl;
}
};
class MBTX {
public:
int m_i;
int m_j;
M0TX m0; // 类类型成员变量
MATX ma; // 类类型成员变量
void funct() { cout << "IAmVeryGood" << endl; }
};
int main() {
MBTX myb;
return 1;
}
(2)父类带缺省构造函数,子类没有任何构造函数,那因为父类这个缺省的构造函数要被调用,所以编译器会为这个子类合成出一个默认构造函数,合成的目的是为了调用这个父类的构造函数。换句话说,编译器合成了默认的构造函数,并在其中安插代码,调用其父类的缺省构造函数;(3)如果一个类含有虚函数,但没有任何构造函数时,因为虚函数的存在,编译器会给我们生成一个基于该类的虚函数表vftable,编译器给我们合成了一个构造函数,并且在其中安插代码,把类的虚函数表地址赋给类对象的虚函数表指针(赋值语句/代码),可以把虚函数表指针看成是表面上看不见的一个类的成员函数,因为虚函数的调用存在一个多态问题,所以需要用到虚函数表指针;(4)编译器给我们往MBTX缺省构造函数中增加了代码:(a)生成了类MBTX的虚函数表;(b)调用了父类的构造函数;(c)因为虚函数的存在,把类的虚函数表地址赋给对象的虚函数表指针;(5)当我们有自己的默认构造函数时,编译器会根据需要扩充我们自己写的构造函数代码,比如调用父类构造函数,给对象的虚函数表指针赋值。编译器干了很多事,没默认构造函数时必要情况下帮助我们合成默认构造函数,如果我们有默认构造函数,编译器会根据需要扩充默认构造函数里边的代码;(6)如果一个类带有虚基类,编译器也会为它合成一个默认构造函数。虚基类:通过两个直接基类继承同一个简介基类,所以一般是三层 ,有爷爷Grand,有两个爹A和A2,有孙子C。虚基类结构,编译器为子类和父类都产生了“合成的默认构造函数”:
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
class Grand { // 爷爷类
public:
};
class A : virtual public Grand {
public:
};
class A2 : virtual public Grand {
public:
};
class C : public A, public A2 { // 这里不需要virtual
public:
C() {
int aa;
aa = 1;
}
};
int main() {
C cc;
return 1;
}
五、拷贝构造函数语义
知识点归纳:
- 传统上大家认为,如果我们没有定义一个自己的拷贝构造函数,编译器会帮助我们合成一个拷贝构造函数。其实这个合成的拷贝构造函数,也是在必要的时候才会被编译器合成出来;
- 某些情况下,如果我们不写自己的拷贝构造函数,编译器就会帮助我们合成出拷贝构造函数来。(1)如果一个类A没有拷贝构造函数,但是含有一个类类型CTB的成员变量m_ctb,该类型CTB含有拷贝构造函数,那么,当代码中有涉及到类A的拷贝构造时,编译器就会为类A合成一个拷贝构造函数。编译器合成的拷贝构造函数往往都是干一些特殊的事情。如果只是一些类成员变量值的拷贝这些事,编译器是不用专门合成出拷贝构造函数来干的,编译器内部就干了;(2)如果一个类CTBSon没有拷贝构造函数,但是它有一个父类CTB,父类有拷贝构造函数,当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,调用父类的拷贝构造函数;(3)如果一个类CTBSon没有拷贝构造函数,但是该类声明了或者继承了虚函数,当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数,往这个拷贝构造函数里插入语句:这个语句的含义是设定类对象myctbson2的虚函数表指针值;(4)如果一个类没有拷贝构造函数, 但是该类含有虚基类,当代码中有涉及到类的拷贝构造时,编译器会为该类合成一个拷贝构造函数。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
//
// class CTB {
// public:
// /*CTB(const CTB&) {
// cout << "CTB()的拷贝构造函数执行了" << endl;
// }
// CTB() {
// }*/
// virtual void mvirfunc() {}
//};
//
// class CTBSon :public CTB {
// public:
// //virtual void mvirfunc() {}
//};
//
// class ASon {
// public:
// int m_testson;
//};
//
// class A {
// public:
// int m_test;
// ASon asubobj;
// CTB m_ctb;
//};
class Grand { // 爷爷类
public:
};
class A : virtual public Grand {
public:
};
class A2 : virtual public Grand {
public:
};
class C : public A, public A2 {
public:
};
int main() {
// A mya1;
// mya1.m_test = 15;
// mya1.asubobj.m_testson = 120;
// A mya2 = mya1; //调用拷贝构造函数。 这个mya2.m_test = 15,这个其实是编译器内部的一个手法:
// 成员变量初始化手法,比如int这种简单类型,直接就按值就拷贝过去,编译器不需要合成拷贝构造函数的情况下就帮助我们把这个事情办了;
// a) A mya2 = mya1; 是拷贝构造一个对象。
// b)我们自己也没有写类A的拷贝构造函数,编译器也没有帮助我们生成拷贝构造函数。
// c)我们却发现mya1对象的一些成员变量值确实被拷贝到mya2中去。这是编译器内部的一些直接拷贝数据的实现手法,
// 比如类A中有类类型ASon成员变量asubobj,也会递归是的去拷贝类ASon的每个成员变量。
//CTBSon myctbson1;
//CTBSon myctbson2 = myctbson1;
C cc;
C cc2 = cc; // 当代码中有涉及到类的拷贝构造时
return 1;
}
六、程序转化语义
知识点归纳:
- 我们写的代码,编译器会对代码进行拆分,拆分成编译器更容易理解和实现的代码。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
using namespace std;
class X {
public:
int m_i;
X(const X &tmpx) {
m_i = tmpx.m_i;
cout << "拷贝构造函数被调用" << endl;
}
X() {
m_i = 0;
cout << "构造函数被调用" << endl;
}
~X() { cout << "析构函数被调用" << endl; }
void functest() { cout << "functest()被调用" << endl; }
};
// void func(X tmpx) {
// return;
//}
// 老编译器看func(老编译器角度)
// void func(X &tmpx) {
// return;
//}
// 人类视角
// X func() {
// X x0;
// //....
// return x0; // 系统产生临时对象并把x0的内容拷贝构造给了临时对象。
//}
// 编译器角度的func
void func(X &extra) {
X x0; // 从编译器角度,这行不调用X的构造函数
//...
//...
extra.X::X(x0);
return;
}
int main() {
// 看一看编译器是如何解析这些代码的。
// 站在程序员角度/站在编译器角度
// 程序员看代码视角和编译器看代码视角之间不断切换。
(1)定义时初始化对象(程序员视角)
// X x0;
// x0.m_i = 15;
// X x1 = x0; // 定义的时候初始化
// X x2(x0);
// X x3 = (x0);
切换到编译器角度,编译器会拆分成两个步骤(编译器视角)
// cout << "---------------" << endl;
// X x100; // 步骤一:定义一个对象,为对象分配内存。从编译器视角来看,这句是不调用X类的构造函数。
// x100.X::X(x0); // 步骤二:直接调用对象的拷贝构造函数去了;
(2)参数的初始化(程序员视角/现代编译器)
// X x0;
func(x0);
老编译器视角
// X tmpobj; // 编译器产生一个临时对象
// tmpobj.X::X(x0); // 调用拷贝构造函数
// func(tmpobj); // 用临时对象调用func
// tmpobj.X::~X(); // func()被调用完成后,本析构被调用。
// (3)返回值初始化(程序员角度)
// X my = func();
// 编译器对上述代码的理解(编译器角度)
// X my; // 不会调用X的构造函数
// func(my);
// 人类视角
// func().functest();
// 切换到编译器视角
// X my; // 不会调用X的构造函数
// (func(my), my).functest(); // 逗号表达式:先计算表达式1,再计算表达式2,整个逗号表达式的结果是表达式2的值;
// 程序员视角
// X(*pf)(); // 定义个函数指针
// pf = func;
// pf().functest();
// 编译器视角
X my; // 不调用构造函数
void (*pf)(X &);
pf = func;
pf(my);
my.functest();
return 1;
}
七、程序的优化
知识点归纳:
- 优化说明:(1)编译器是否真优化了,不好说,你要做各种测试才知道;(2)如果你的代码很复杂,编译器可能放弃不优化;(3)不要过度优化;(4)优化可能使你犯错误。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
using namespace std;
class CTempValue {
public:
int val1;
int val2;
public:
CTempValue(int v1 = 0, int v2 = 0) : val1(v1), val2(v2) { // 构造函数
//cout << "调用了构造函数!" << endl;
//cout << "val1 = " << val1 << endl;
//cout << "val2 = " << val2 << endl;
}
CTempValue(const CTempValue &t) : val1(t.val1), val2(t.val2) { // 拷贝构造函数
// cout << "调用了拷贝构造函数!" << endl;
}
virtual ~CTempValue() {
// cout << "调用了析构函数!" << endl;
}
};
// 函数(开发者视角)
CTempValue Double(CTempValue &ts) {
// CTempValue tmpm; // 消耗一个构造函数,一个析构函数
// tmpm.val1 = ts.val1 * 2;
// tmpm.val2 = ts.val2 * 2;
// return tmpm; // 生成一个临时对象,然后调用拷贝构造函数把tmpm的内容拷贝构造到这个临时对象中去,然后返回临时对象。
// // 这个临时对象也消耗了一个拷贝构造函数 ,消耗了一个析构函数;
//
return CTempValue(ts.val1 * 2, ts.val2 * 2); // 生成一个临时对象。
}
// double(编译器视角)
void Double(CTempValue &tmpobj, CTempValue &ts) { // 编译器会插入第一个参数
tmpobj.CTempValue::CTempValue(ts.val1 * 2, ts.val2 * 2);
return;
}
int main() {
// 从开发者层面
// 从编译器层面
// (1)开发者层面的优化(开发者视角)
CTempValue ts1(10, 20);
// CTempValue ts2 = Double(ts1);
// Double(ts1);
CTempValue tmpobj;
clock_t start, end;
start = clock(); // 程序开始到本行执行时所用的毫秒数
cout << "start = " << start << endl;
for (int i = 0; i < 1000000; i++) {
// Double(ts1);
Double(tmpobj, ts1);
}
end = clock();
cout << "end = " << end << endl;
cout << end - start << endl;
// 编译器视角
//CTempValue ts1;
//ts1.CTempValue::CTempValue(10, 20);
//CTempValue tmpobj;
//Double(tmpobj, ts1);
// (2)从编译器层面的优化
// linux编译器g++优化,针对 与返回临时对象这种情况。 NRV优化(Named Return Value)。
// RVO(Return Value Optimization);
// g++ -fno-elide-constructors 2_8.cpp -o 2_8
return 1;
}
八、程序优化续、拷贝构造续,深浅拷贝
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
using namespace std;
class X {
public:
int m_i;
int* p_mi;
X(const X& tmpx) {
p_mi = new int(100); // 我们自己创建内存
memcpy(p_mi, tmpx.p_mi, sizeof(int)); // 把目标对象的内存内容拷贝过来,叫深拷贝。
m_i = tmpx.m_i;
cout << "拷贝构造函数被调用" << endl;
}
X() { // 缺省构造函数
p_mi = new int(100);
m_i = 0;
cout << "构造函数被调用" << endl;
}
~X() {
delete p_mi;
cout << "析构函数被调用" << endl;
}
// explicit X(int value) :m_i(value) // 类型转换构造函数
X(int value) : m_i(value) { // 类型转换构造函数
p_mi = new int(100);
cout << "X(int)构造函数被调用" << endl;
}
};
int main() {
// cout << "--------begin-----------" << endl;
// X x10(1000);
// cout << "-------------------" << endl;
// X x11 = 1000; //隐式类型转换
// cout << "-------------------" << endl;
// X x12 = X(1000);
// cout << "-------------------" << endl;
// X x13 = (X)1000;
// cout << "--------end-----------" << endl;
从编译器视角(不优化)
第一行
// X x10;
// x10.X::X(1000);
后边三行
// X _tmp0;
//_tmp0.X::X(1000); // 带一个参数的构造函数被调用
// X x12;
// x12.X::X(_tmp0); // 拷贝构造函数被调用
//_tmp0.X::~X(); // 调用析构。
// 总结:当编译器面临用一个类对象作为另外一个类对象初值的情况,各个编译器表现不同。但是所有编译器都为了提高效率而努力。
// 我们也没有办法确定我们自己使用的编译器是否一定会调用拷贝构造函数。
// 拷贝构造函数是否必须有? 不一定,视情况而定。
// 如果你只有一些简单的成员变量类型,int,double,你会发现你根本不需要拷贝构造函数;编译器内部本身就支持成员变量的
// bitwise(按位) copy 按位拷贝
X x0;
x0.m_i = 150;
X x1(x0); // 有自己的拷贝构造函数编译器是必然会调用的。
cout << x1.m_i << endl; // 编译器支持bitwise拷贝,所以x1.m_i = 150;
// 当需要处理很复杂的成员变量类型的时候。
// 因为我们增加了自己的拷贝构造函数,导致编译器本身的bitwise拷贝能力失效,所以结论:
// 如果你增加了自己的拷贝构造函数后,就要对各个成员变量的值的初始化负责了;
// 深浅拷贝问题;
return 1;
}
九、成员初始化列表
知识点归纳:
- 何时必须用成员初始化列表?(a)如果这个成员是个引用;(b)如果是个const类型成员;(c)如果你这个类是继承一个基类,并且基类中有构造函数,这个构造函数里边还有参数;(d)如果你的成员变量类型是某个类类型,而这个类的构造函数带参数时;
- 使用初始化列表的优势(提高效率):(a)除了必须用初始化列表的场合,我们用初始化列表还有什么其他目的?有,就是提高程序运行效率;(b)对于类类型成员变量xobj放到初始化列表中能够比较明显的看到效率的提升;(c)但是如果是个简单类型的成员变量,比如
int m_test
,其实放在初始化列表或者放在函数体里效率差别不大;(d)提醒:成员变量初始化尽量放在初始化列表里,显得高端大气上档次,考官对这个感兴趣; - 初始化列表细节探究,说明:(a)初始化列表中的代码可以看作是被编译器安插到构造函数体中的,只是这些代码有些特殊;(b)这些代码是在任何用户自己的构造函数体代码之前被执行的,所以大家要与构造函数中的代码区分开;(c)用户代码和编译器插入的初始化所属的代码;(d)这些列表中变量的初始化顺序是定义顺序,而不是在初始化列表中的顺序;(e)不建议在初始化列表中,进行两个都在初始化列表中出现的成员之间的初始化。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
using namespace std;
// class Base {
// public:
// int ba;
// int bb;
// Base(int tmpa, int tmpb) {}
// };
// class CSub {
// public:
// CSub(int tmpv) {}
// };
// class A : public Base {
// public:
// int m_x;
// int m_y;
// int &m_yy;
// const int m_myc;
// CSub cmysub;
// // A() :m_x(0), m_y(0)
// A(int &tmpvalue) : m_yy(tmpvalue), m_myc(tmpvalue), Base(tmpvalue, tmpvalue), cmysub(tmpvalue) {
// // m_myc = tmpvalue;
// // m_yy = tmpvalue;
// m_x = 0;
// m_y = 0;
// m_yy = 180;
// }
// };
class X {
public:
int m_i;
X(int value = 0) : m_i(value) { // 类型转换构造函数
printf("this = %p", this);
cout << "X(int)构造函数被调用" << endl;
}
X(const X &tmpv) {
printf("this = %p", this);
cout << "X拷贝构造函数被调用" << endl;
}
X &operator=(const X &tmp) {
printf("this = %p", this);
cout << "X拷贝赋值运算符被调用" << endl;
return *this;
}
~X() {
printf("this = %p", this);
cout << "X析构函数被调用" << endl;
}
};
class A {
public:
X xobj; // 类类型对象
int m_test;
int m_test2;
// 构造函数
// A(int tmpvalue) // 这里却构造了xobj,耗费了一次调用构造函数的机会
// 站在编译器视角
//X xobj;
//xobj.X::X();
// 大家把初始化列表中的代码看成是函数体内代码的一部分;
// A(int tmpvalue) :xobj(1000), m_test2(100)/*,m_test(m_test2)*/ //这种代码是错误的
A(int tmpvalue)
: xobj(1000),
m_test2(m_test),
m_test(100)
// 站在编译器视角
//X xobj;
//xobj.X::X(1000)
{
// m_test = m_test2;
cout << "mtest = " << m_test << endl;
cout << "mtest2 = " << m_test2 << endl;
// m_test = 100;
// 站在编译器视角
// X tmpobj; // 生成一个临时对象
// tmpobj.X::X(1000); // 临时对象调用构造函数
// xobj.X::operator=(tmpobj); // 调用的是xobj的拷贝赋值运算符。
// tmpobj.X::~X(); // 调用析构函数回收对象
// xobj = 1000; // 构造一个临时对象,把临时对象内容给了xobj,释放掉临时对象
// m_test = 500;
}
};
int main() {
//int abc = 1;
//A a(abc);
//cout << abc << endl;
A myaobj(1000);
return 1;
}
十、虚函数表指针位置分析
知识点归纳:
- 一个类如果有虚函数,这个类会产生一个虚函数表,相应的类对象有一个指针,指针(vptr)会指向这个虚函数表的开始地址。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
class A {
public:
int i; // 4字节
virtual void testfunc() {} // 虚函数,vptr4字节。
};
int main() {
// 虚函数表指针位置分析
// 类:有虚函数,这个类会产生一个虚函数表。
// 类对象,有一个指针,指针(vptr)会指向这个虚函数表的开始地址。
A aobj;
int ilen = sizeof(aobj);
cout << ilen << endl; // 8字节
char *p1 = reinterpret_cast<char *>(&aobj); // 类型转换,硬转 &aobj这是对象aobj的首地址。
char *p2 = reinterpret_cast<char *>(&(aobj.i));
if (p1 == p2) { // 说明aobj.i和aobj的位置相同,说明i在对象aobj内存布局的上边。虚函数表指针vptr在下边
cout << "虚函数表指针位于对象内存的末尾" << endl;
} else {
cout << "虚函数表指针位于对象内存的开头" << endl;
}
return 1;
}
上述代码运行输出:“虚函数表指针位于对象内存的开头”,说明虚函数表指针位于对象内存的开头。
十一、继承关系作用下虚函数的手工调用
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
// 父类
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
};
class Derive : public Base {
virtual void g() { cout << "Derive::g()" << endl; }
};
int main() {
// 继承关系作用下虚函数的手工调用
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
Derive *d = new Derive(); // 派生类指针。
long *pvptr = (long *)d; // 指向对象的指针d转成了long *类型。
long *vptr = (long *)(*pvptr); // (*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。
// vptr 就是d对象的虚函数表指针
for (int i = 0; i <= 4; i++) { // 循环5次;其实只有3个虚函数,循环3次即可
printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
}
typedef void (*Func)(void); // 定义一个函数指针类型
Func f = (Func)vptr[0]; // f就是函数指针变量。 vptr[0]是指向第一个虚函数的。
Func g = (Func)vptr[1];
Func h = (Func)vptr[2];
/*Func i = (Func)vptr[3];*/
/*Func j = (Func)vptr[4];*/
f();
g();
h();
// i();
Base *dpar = new Base();
long *pvptrpar = (long *)dpar;
long *vptrpar = (long *)(*pvptrpar);
for (int i = 0; i <= 4; i++) { // 循环5次;
printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]);
}
Func fpar = (Func)vptrpar[0];
Func gpar = (Func)vptrpar[1];
Func hpar = (Func)vptrpar[2];
cout << "--------------------" << endl;
fpar();
gpar();
hpar();
return 1;
}
十二、虚函数表分析
知识点归纳:
- 一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同;
- 父类中有虚函数就等于子类中有虚函数,换句话来说,父类中有虚函数表,则子类中肯定有虚函数表。因为子类是继承父类的;也有人认为,如果子类中把父类的虚函数的virtual去掉,是不是这些函数就不再是虚函数了?事实上,只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。但不管是父类还是子类,都只会有一个虚函数表,不能认为子类中有两个虚函数表。子类中是否可能会有多个虚函数表呢?后续我们讲解这个事;
- 如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同,但仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),则该表项所执行的该函数的地址应该相同。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
// 父类
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
};
class Derive : public Base {
virtual void g() { cout << "Derive::g()" << endl; }
//void f() { cout << "Derive::f()" << endl; }
//void g() { cout << "Derive::g()" << endl; }
//void h() { cout << "Derive::h()" << endl; }
};
int main() {
继承关系作用下虚函数的手工调用
// cout << sizeof(Base) << endl;
// cout << sizeof(Derive) << endl;
// Derive *d = new Derive(); // 派生类指针。
// Derive *d2 = new Derive(); // 派生类指针。
// long *pvptr = (long *)d; // 指向对象的指针d转成了long *类型。
// long *vptr = (long *)(*pvptr); // (*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。
// long *pvptr2 = (long *)d2; // 同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针)
// long *vptr2 = (long *)(*pvptr2);
// for (int i = 0; i <= 4; i++) // 循环5次;
//{
// printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
//}
// typedef void(*Func)(void); // 定义一个函数指针类型
// Func f = (Func)vptr[0]; // f就是函数指针变量。 vptr[0]是指向第一个虚函数的。
// Func g = (Func)vptr[1];
// Func h = (Func)vptr[2];
///*Func i = (Func)vptr[3];
// Func j = (Func)vptr[4];*/
// f();
// g();
// h();
i();
// Base *dpar = new Base();
// long *pvptrpar = (long *)dpar;
// long *vptrpar = (long *)(*pvptrpar);
// for (int i = 0; i <= 4; i++) // 循环5次;
//{
// printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]);
//}
// Func fpar = (Func)vptrpar[0];
// Func gpar = (Func)vptrpar[1];
// Func hpar = (Func)vptrpar[2];
// cout << "--------------------" << endl;
// fpar();
// gpar();
// hpar();
typedef void (*Func)(void); // 定义一个函数指针类型
Derive derive;
long *pvptrderive = (long *)(&derive);
long *vptrderive = (long *)(*pvptrderive); // 0x00b09b6c {project100.exe!void(* Derive::`vftable'[4])()} {11538847}
Func f1 = (Func)vptrderive[0]; // 0x00b0119f {project100.exe!Base::f(void)}
Func f2 = (Func)vptrderive[1]; // 0x00b0150f {project100.exe!Derive::g(void)}
Func f3 = (Func)vptrderive[2]; // 0x00b01325 {project100.exe!Base::h(void)}
Func f4 = (Func)vptrderive[3]; // 0x69726544
Func f5 = (Func)vptrderive[4]; // 0x3a3a6576
Derive derive2 = derive; // 调用拷贝构造函数
long *pvptrderive2 = (long *)(&derive2);
long *vptrderive2 = (long *)(*pvptrderive2);
Base base = derive; // 直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。
// 所以Base base = derive;实际干了两个事情:
// 第一个事情:生成一个base对象
// 第二个事情:用derive来初始化base对象的值。
// 这里编译器给咱们做了一个选择,显然derive初始化base对象的时候,
// derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,编译器帮我们做到了这点;
long *pvptrbase = (long *)(&base);
long *vptrbase = (long *)(*pvptrbase); // 0x00b09b34 {project100.exe!void(* Base::`vftable'[4])()} {11538847}
Func fb1 = (Func)vptrbase[0]; // 0x00b0119f {project100.exe!Base::f(void)}
Func fb2 = (Func)vptrbase[1]; // 0x00b01177 {project100.exe!Base::g(void)}
Func fb3 = (Func)vptrbase[2]; // 0x00b01325 {project100.exe!Base::h(void)}
Func fb4 = (Func)vptrbase[3]; // 0x00000000
Func fb5 = (Func)vptrbase[4]; // 0x65736142
return 1;
}
OO(面向对象)和OB(基于对象)概念:c++通过类的指针和引用来支持多态,这是一种程序设计风格,也就是我们常说的面向对象(object-oriented model);OB(object-based),也叫ADT(abstract datatype model,抽象数据模型)。如果不支持多态,执行速度更快,因为函数调用的解析不需要运行时决定(没有多态),而是在编译期间就解析完成,内存空间紧凑程度上更紧凑,因为没有虚函数指针和虚函数表这些概念,但显然,OB的设计灵活性就差。c++既支持面向对象程序设计(继承,多态),也支持基于对象程序设计。
十三、多重继承虚函数表分析
知识点归纳:
- 一个对象,如果它的类有多个基类则有多个虚函数表指针(注意是两个虚函数表指针,而不是两个虚函数表)。在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr);
- 如下图,子类对象ins有两个虚函数表指针,vptr1和vptr2;类Derived有两个虚函数表,因为它继承自两个基类;子类和第一个基类公用一个vptr(因为vptr指向一个虚函数表,所以也可以说子类和第一个基类共用一个虚函数表vtbl),我们注意到了类Derived的虚函数表1里边的5个函数,而
g()
正好是base1里边的函数;子类中的虚函数覆盖了父类中的同名虚函数,比如derived::f()
,derived::i()
。
演示代码:
#include "pch.h"
#include <iostream>
using namespace std;
// 基类1
class Base1 {
public:
virtual void f() { cout << "base1::f()" << endl; }
virtual void g() { cout << "base1::g()" << endl; }
};
// 基类2
class Base2 {
public:
virtual void h() { cout << "base2::h()" << endl; }
virtual void i() { cout << "base2::i()" << endl; }
};
// 子类
class Derived : public Base1, public Base2 {
public:
virtual void f() { // 覆盖父类1的虚函数
cout << "derived::f()" << endl;
}
virtual void i() { // 覆盖父类2的虚函数
cout << "derived::i()" << endl;
}
// 如下三个我们自己的虚函数
virtual void mh() { cout << "derived::mh()" << endl; }
virtual void mi() { cout << "derived::mi()" << endl; }
virtual void mj() { cout << "derived::mj()" << endl; }
};
int main() {
// 多重继承虚函数表分析
// 多重继承
cout << sizeof(Base1) << endl;
cout << sizeof(Base2) << endl;
cout << sizeof(Derived) << endl;
Derived ins;
Base1 &b1 = ins; // 多态
Base2 &b2 = ins;
Derived &d = ins;
typedef void (*Func)(void);
long *pderived1 = (long *)(&ins);
long *vptr1 = (long *)(*pderived1); // 取第一个虚函数表指针。
long *pderived2 = pderived1 + 1; // 跳过4字节。
long *vptr2 = (long *)(*pderived2); // 取第二个虚函数表指针。
Func f1 = (Func)vptr1[0]; // 0x00ab15d7 {project100.exe!Derived::f(void)}
Func f2 = (Func)vptr1[1]; // 0x00ab15f0 {project100.exe!Base1::g(void)}
Func f3 = (Func)vptr1[2]; // 0x00ab15cd {project100.exe!Derived::mh(void)}
Func f4 = (Func)vptr1[3]; // 0x00ab15ff {project100.exe!Derived::mi(void)}
Func f5 = (Func)vptr1[4]; // 0x00ab15eb {project100.exe!Derived::mj(void)}
Func f6 = (Func)vptr1[5]; // 非正常
Func f7 = (Func)vptr1[6];
Func f8 = (Func)vptr1[7];
Func f11 = (Func)vptr2[0]; // 0x00ab15af {project100.exe!Base2::h(void)}
Func f22 = (Func)vptr2[1]; // 0x00ab15b9 {project100.exe!Derived::i(void)}
Func f33 = (Func)vptr2[2]; // 非正常
Func f44 = (Func)vptr2[3];
b1.f(); // b1是ins的引用,所以调用的是derived::f()
// 如果是Base1 b1 = ins,则调用的是base1::f()
b2.i();
d.f();
d.i();
d.mh();
d.g();
//----------------
cout << "-----------------" << endl;
f1();
f2();
f3();
f4();
f5();
cout << "-------------" << endl;
f11();
f22();
return 1;
}
左边为Linux运行结果,右边为Windows运行结果:
十四、辅助工具,vptr、vtbl创建时机
cl.exe是编译链接工具,打开vs开发人员命令提示符,cd
进入 .cpp 文件的路径,windows 下输入:cl /d1 reportSingleClassLayoutDerived example.cpp
,linux 下输入:
g++ -fdump-class-hierarchy -fsyntax-only example.cpp
。
可看到类的结构信息:
vptr(虚函数表指针)跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来。对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加为vptr赋值的代码,这是在编译期间编译器为构造函数增加的。当程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了。对于vtbl(虚函数表),编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容,然后也是在编译器期间在相应的类构造函数中添加给vptr赋值的代码,这样程序运行的时候,当运行到生成类对象的代码时,会调用类的构造函数,执行到类的构造函数中给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了。
十五、类不纯时引发的虚函数调用问题
单纯的类是指比较简单的类,尤其是没有虚函数和虚基类的类,不纯的类则反之,先看一段代码:
#include "pch.h"
#include <iostream>
#include <time.h >
using namespace std;
class X {
public:
int x;
int y;
int z;
// X() :x(0), y(0), z(0)
X() {
// 编译器角度 伪码;
// vptr = vtbl; // 下边的memset会把vptr(虚函数表指针)清0
memset(this, 0, sizeof(X));
cout << "构造函数被执行" << endl;
}
// X(const X &tm) :x(tm.x), y(tm.y), z(tm.z)
X(const X &tm) {
memcpy(this, &tm, sizeof(X));
cout << "拷贝构造函数被执行" << endl;
}
virtual ~X() { cout << "析构函数被执行" << endl; }
virtual void virfunc() { cout << "虚函数virfunc()被执行" << endl; }
void ptfunc() { cout << "普通函数ptfunc()被执行" << endl; }
};
int main() {
// X x0; // 调用构造函数
///*x0.x = 100;
// x0.y = 200;
// x0.z = 300;*/
// x0.virfunc(); // 虚函数表指针为null居然可以成功调用虚函数;
// X x1(x0); // 调用拷贝构造函数
// cout << "x1.x=" << x1.x << " x1.y=" << x1.y << " x1.z=" << x1.z << endl;
// X *px0 = new X();
// px0->ptfunc(); // 正常调用
// px0->virfunc(); // 无法正常调用
// delete px0; // 无法正常调用
// new出来的对象,虚函数变得无法正常执行了;
int i = 9;
printf("i的地址 = %p\n", &i);
X x0;
printf("ptfunc()的地址=%p\n", &X::ptfunc); // 打印正常的成员函数地址。
// long *pvptrpar = (long *)(&x0);
// long *vptrpar = (long *)(*pvptrpar);
// printf("virfunc的地址 = %p\n", vptrpar[1]); // 虚函数virfunc地址
x0.ptfunc();
x0.virfunc(); // 不叫多态,属于静态联编
// 推断:函数ptfunc()和virfunc()函数,是在编译的时候就确定好的;
X *pX0 = new X();
pX0->ptfunc();
pX0->virfunc(); // 通过虚函数表指针,找虚函数表,然后从虚函数表中找到virfunc虚函数的地址并调用。
// 更明白:虚函数,多态,这种概念专门给指针或者引用用的;
X &xy = *pX0;
xy.virfunc();
X &xy2 = x0;
xy2.virfunc();
return 1;
}
如果类并不单纯,那么在构造函数中使用如上所示的memset
或者拷贝构造函数中使用如上所示的memcpy
方法,就会出现程序崩溃的情形。因为某些情况下,编译器会往类内部增加一些看不见但真实存在的成员变量(隐藏成员变量),有了这种变量的类,就不单纯了;同时,这种隐藏的成员变量的增加(使用)或者赋值的时机,往往都是在执行构造函数或者拷贝构造函数的函数体之前进行,所以如果使用memset
、memcpy
,很可能把编译器给隐藏变量的值清空或者覆盖了,比如类中增加了虚函数,系统默认往类对象中增加虚函数表指针,这个虚函数表指针就是隐藏的成员变量。虚函数主要解决的问题是父类指针指向子类对象这种情况,如果只有虚函数,没有继承,那么此时虚函数和普通函数可以认为没什么实际区别。再说一下静态联编和动态联编,静态联编是在编译的时候就能确定调用哪个函数,把调用语句和被调用函数绑定到一起,动态联编是在程序运行时,根据实际情况,动态地把调用语句和被调用函数绑定到一起,动态联编一般在有多态和虚函数情况下才存在。从上述代码明白,虚函数、多态这种概念专门给指针(new出来的对象)或者引用用的。
十六、数据成员绑定时机
如下代码,编译器是对成员函数myfunc
的解析,是整个A类定义完毕后才开始的,所以对这个myvar
的解析和绑定,是在这个类定义完成后发生的。因为只有整个类A定义完毕后,编译器才能看到类A中的myvar
,才能根据时机的需要把出现myvar
的场合做适当的解释(成员函数中解析成类中的myvar
,全局函数中解析成全局的myvar
)。对于成员函数参数,是在编译器第一次遇到整个类型mytype
的时候被决定的,所以mytype
第一次遇到的时候,编译器只看到了typedef string mytype
,没有看到类中的typedef in mytype
。为了在类中尽早的看到类型mytype
,这种类型定义语句typedef
,一定要挪到类的最开头定义,这样后边的成员函数第一次遇到这个类型mytype
的时候,它就本着最近碰到的类型的原则来应用最近碰到的类型。
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
// string myvar= "I Love China!"; // 全局量,字符串型
typedef string mytype;
//定义一个类
class A {
typedef int mytype;
public:
// int myfunc();
///*{
// return myvar;
//}*/
// void myfunc(mytype tmpvalue) { //mytype = string
// m_value = tmpvalue; // 出错,是把一个string类型给一个整型
//}
void myfunc(mytype tmpvalue); // string
private:
// int myvar; // 同全局变量名相同,但类型不同。
mytype m_value; // int
};
void A::myfunc(mytype tmpvalue) { // int
m_value = tmpvalue;
}
void myfunc(mytype tmpvalue) { // string
string mvalue = tmpvalue;
}
// int A::myfunc() { // 成员函数
// cout << myvar << endl; // myvar是类内定义的
// cout << ::myvar.c_str() << endl; // myvar是全局的
// return myvar; // 这里还是A::myvar
//}
// int myfunc() {
// return myvar; // 这里的myvar是全局的,是string类型,所以这里报错;
//}
int main() {
//A aobj;
//aobj.myvar = 15;
//aobj.myfunc();
return 1;
}
十七、进程内存空间布局
不同的数据在内存中会有不同的保存时机和保存位置,当运行一个可执行文件时,操作系统就会把这个可执行文件加载到内存,此时进程有一个虚拟的地址空间(内存空间)。linux有个nm
命令,能够列出可执行文件中的全局变量存放的地址。看一段代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
int *ptest = new int(120);
int g1;
int g2;
int g3 = 12;
int g4 = 32;
int g5;
int g6 = 0;
static int g7;
static int g8 = 0;
static int g9 = 10;
void mygfunc() { return; }
// 定义一个类
class MYACLS {
public:
int m_i;
static int m_si; // 声明不是定义,不会分配空间
int m_j;
static int m_sj;
int m_k;
static int m_sk;
// static void myclsfunc() {}
};
int MYACLS::m_sj = 0; // 这才是定义;会分配空间
int main() {
int i = 7;
printf("i地址=%p\n", &i);
printf("ptest地址=%p\n", &ptest);
printf("g1地址=%p\n", &g1);
printf("g2地址=%p\n", &g2);
printf("g3地址=%p\n", &g3);
printf("g4地址=%p\n", &g4);
printf("g5地址=%p\n", &g5);
printf("g6地址=%p\n", &g6);
printf("g7地址=%p\n", &g7);
printf("g8地址=%p\n", &g8);
printf("g9地址=%p\n", &g9);
printf("MYACLS::m_sj地址=%p\n", &(MYACLS::m_sj));
printf("mygfunc()地址=%p\n", mygfunc);
printf("main()地址=%p\n", main);
cout << (void *)mygfunc << endl;
return 1;
}
运行结果:
内存布局:
未初始化或赋值为0的全局变量和静态变量存放在BSS段,已初始化的全局变量和静态变量存放在数据段。
十八、数据成员布局
知识点归纳:
- 普通成员变量的存储顺序是按照在类中的定义顺序从上到下来的,比较晚出现的成员变量在内存中有更高的地址。类定义中
pubic
、private
、protected
的数量,不影响类对象的sizeof
; - 某些因素会导致成员变量之间排列不连续,就是边界调整(字节对齐),调整的目的是提高效率,是编译器的自动调整,编译器会往成员之间填补一些字节,使用类对象的
sizoef
字节数凑成一个4的整数倍、8的整数倍;再引入一个概念叫一字节对齐(不对齐),详见代码; - 有虚函数时,编译器会往类定义中增加vptr虚函数表指针,它属于内部的数据成员;
- 成员变量偏移值,就是这个成员变量的地址离对象首地址偏移多少。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
#define GET(A, m) (int)(&((A *)0)->m) // 也可以调用这个获取成员变量的偏移值
// 定义一个类
#pragma pack(1) // 对齐方式设置为1字节对齐(不对齐)
class MYACLS {
public:
int m_i;
static int m_si; // 声明不是定义
int m_j;
static int m_sj;
int m_k;
static int m_sk;
char m_c; // 1字节
int m_n; // 4字节
private:
int m_pria;
int m_prib;
public:
void printMemPoint() {
cout << "打印成员变量偏移值------------" << endl;
printf("MYACLS::m_i = %d\n", &MYACLS::m_i);
printf("MYACLS::m_j = %d\n", &MYACLS::m_j);
printf("MYACLS::m_k = %d\n", &MYACLS::m_k);
printf("MYACLS::m_c = %d\n", &MYACLS::m_c);
printf("MYACLS::m_n = %d\n", &MYACLS::m_n);
printf("MYACLS::m_pria = %d\n", &MYACLS::m_pria);
printf("MYACLS::m_prib = %d\n", &MYACLS::m_prib);
cout << "-------------------------" << endl;
cout << GET(MYACLS, m_prib) << endl;
}
public:
virtual void myfv() {}
};
#pragma pack() // 取消指定对齐,恢复缺省对齐;
int MYACLS::m_sj = 0; // 这才是定义;
int main() {
MYACLS myobj;
cout << sizeof(myobj) << endl;
myobj.m_i = 2;
myobj.m_k = 8;
myobj.m_j = 5;
printf("myobj.m_i = %p\n", &myobj.m_i);
printf("myobj.m_j = %p\n", &myobj.m_j);
printf("myobj.m_k = %p\n", &myobj.m_k);
printf("myobj.m_c = %p\n", &myobj.m_c);
printf("myobj.m_n = %p\n", &myobj.m_n);
MYACLS *pmyobj = new MYACLS();
printf("pmyobj->m_i = %p\n", &pmyobj->m_i);
printf("pmyobj->m_j = %p\n", &pmyobj->m_j);
printf("pmyobj->m_k = %p\n", &pmyobj->m_k);
printf("pmyobj->m_c = %p\n", &pmyobj->m_c);
printf("pmyobj->m_n = %p\n", &pmyobj->m_n);
pmyobj->printMemPoint();
// 成员变量指针
int MYACLS::*mypoint = &MYACLS::m_n;
printf("pmyobj->m_n偏移值 = %d\n", mypoint);
return 1;
}
运行结果:
无虚函数和有虚函数时的打印对比:
十九、数据成员存取
知识点归纳:
- 静态成员变量,可以当做一个全局量,但是它只在类的空间内可见,引用时用 “类名::静态成员变量名”;
- 非静态成员变量的存取(普通的成员变量),存放在类的对象中,存取通过类对象(类对象指针)。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
class FAC {
public:
int m_fai;
int m_faj;
};
class MYACLS : public FAC { // 继承父类,那么父类成员会添加在子类成员的前面
public:
int m_i;
static int m_si; // 声明而不是定义
int m_j;
void myfunc() {
m_i = 5;
m_j = 6;
}
};
int MYACLS::m_si = 10; // 这个是定义
int main() {
MYACLS myobj;
MYACLS *pmyobj = new MYACLS();
//cout << MYACLS::m_si << endl;
//cout << myobj.m_si << endl;
//cout << pmyobj->m_si << endl;
MYACLS::m_si = 1;
myobj.m_si = 2;
pmyobj->m_si = 3;
printf("myobj.m_i = %p\n", &myobj.m_i);
printf("pmyobj->m_i = %p\n", &pmyobj->m_i);
printf("MYACLS::m_si = %p\n", &MYACLS::m_si);
printf("myobj.m_si = %p\n", &myobj.m_si);
printf("pmyobj->m_si = %p\n", &pmyobj->m_si);
// pmyobj->myfunc();
// 编译器角度:MYACLS::myfunc(pmyobj)
// MYACLS::myfunc(MYACLS *const this)
//{
// this->m_i = 5;
// this->m_j = 5;
//}
// 对于普通成员的访问,编译器是把类对象的首地址加上成员变量的偏移值;
// &myobj + 4 = &myobj.m_j
printf("MYACLS::m_i = %d\n", &MYACLS::m_i);
printf("MYACLS::m_j = %d\n", &MYACLS::m_j);
pmyobj->myfunc();
pmyobj->m_faj = 7;
// 虚基类;
myobj.m_i = 15;
pmyobj->m_i;
return 1;
}
运行两次:
发现普通成员变量的地址每次都会变,静态成员的地址不会改变。在Linux下用 nm
+ 可执行文件,查看这个可执行文件:
发现静态变量的地址是在可执行文件中已经写好的。结论就是,静态成员变量只有一个实体,保存在可执行文件的数据段。
二十、单一继承下的数据成员布局
先看一段演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
// class FAC { // 父类
// public:
// int m_fai;
// int m_faj;
// };
// class MYACLS : public FAC { // 子类
// public:
// int m_i;
// int m_j;
// };
// class Base { // sizeof = 8字节;
// public:
// int m_i1;
// char m_c1;
// char m_c2;
// char m_c3;
// };
class Base1 {
public:
int m_i1;
char m_c1;
};
class Base2 : public Base1 {
public:
char m_c2;
};
class Base3 : public Base2 {
public:
char m_c3;
};
int main() {
// printf("FAC::m_fai = %d\n", &FAC::m_fai);
// printf("FAC::m_faj = %d\n", &FAC::m_faj);
// printf("MYACLS::m_fai = %d\n", &MYACLS::m_fai);
// printf("MYACLS::m_faj = %d\n", &MYACLS::m_faj);
//
// printf("MYACLS::m_i = %d\n", &MYACLS::m_i);
// printf("MYACLS::m_j = %d\n", &MYACLS::m_j);
// FAC facobj;
// MYACLS myaclobj; //子类对象中实际上是包含着父类子对象的
// cout << sizeof(Base) << endl; //8字节,数据布局(内存排列上)紧凑;
// printf("Base::m_i1 = %d\n", &Base::m_i1);
// printf("Base::m_c1 = %d\n", &Base::m_c1);
// printf("Base::m_c2 = %d\n", &Base::m_c2);
// printf("Base::m_c3 = %d\n", &Base::m_c3);
cout << sizeof(Base1) << endl; // 8
cout << sizeof(Base2) << endl; // 12
cout << sizeof(Base3) << endl; // 16
printf("Base3::m_mi1 = %d\n", &Base3::m_i1);
printf("Base3::m_mc1 = %d\n", &Base3::m_c1);
printf("Base3::m_mc2 = %d\n", &Base3::m_c2);
printf("Base3::m_mc3 = %d\n", &Base3::m_c3);
Base2 mybase2obj;
Base3 mybase3obj;
// 不能用memcpy内存拷贝把Base2内容直接往Base3里拷贝,可能会把Base3的内容覆盖
return 1;
}
运行结果:
知识点归纳:
- 一个子类对象所包含的内容,是它自己的成员加上它父类的成员的总和;
- 从偏移值看,父类成员先出现,然后才是孩子类成员。
内存对齐:
引入继承关系后,可能会带来内存空间的额外增加,在Windows上:
在Linux上:
Linux上和Windows上数据布局不一样,说明:(a)编译器在不断的进步和优化;(b)不同厂商编译器,实现细节也不一样;(c)内存拷贝要谨慎,比如上述代码,不能用memcpy
内存拷贝把Base2内容直接往Base3里拷贝,可能会把Base3的内容覆盖。
二十一、单类单继承虚函数下的数据成员布局
知识点归纳:
- 类中引入虚函数时,会有额外的成本付出:(a)编译的时候,编译器会产生虚函数表;(b)对象中会产生虚函数表指针vptr,用以指向虚函数表;(c)增加或者扩展构造函数时,会增加给虚函数表指针vptr赋值的代码,让vptr指向虚函数表;(d)如果多重继承,比如继承了2个父类,每个父类都有虚函数的话,每个父类都会有vptr,那继承时,子类就会把这两根vptr都继承过来,如果子类还有自己额外的虚函数的话,子类与第一个基类共用一个vptr;(f)析构函数中也被扩展增加了虚函数表指针vptr相关的赋值代码,这个赋值代码似乎和构造函数中代码相同。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
class Base {
public:
int m_bi;
// virtual void mybvirfunc() {}
};
class MYACLS : public Base {
public:
int m_i;
int m_j;
virtual void myvirfunc() {} // 虚函数
MYACLS() {
int abc = 1; // 方便加断点
}
~MYACLS() {
int def = 0; // 方便加断点
}
};
int main() {
cout << sizeof(MYACLS) << endl;
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi);
printf("MYACLS::m_i = %d\n", &MYACLS::m_i);
printf("MYACLS::m_j = %d\n", &MYACLS::m_j);
MYACLS myobj;
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;
return 1;
}
单个类带虚函数的数据成员布局和单一继承父类带虚函数的数据成员布局,子类与基类共用一个vptr:
如果父类不带虚函数,子类带虚函数,即单一继承父类不带虚函数的数据成员布局(子类虚函数指针位于父类成员的前面):
二十二、多重继承数据布局与this调整深谈
先看一段代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
using namespace std;
class Base1 {
public:
int m_bi;
virtual void mybvirfunc() {}
Base1() { printf("Base1::Base1()的this指针是:%p!\n", this); }
};
class Base2 {
public:
int m_b2i;
virtual void mybvirfunc2() {}
Base2() { printf("Base2::Base2()的this指针是:%p!\n", this); }
};
class MYACLS : public Base1, public Base2 {
public:
int m_i;
int m_j;
virtual void myvirfunc() {} // 虚函数
MYACLS() {
int abc = 1; // 方便加断点
printf("MYACLS::MYACLS()的this指针是:%p!\n", this);
}
~MYACLS() {
int def = 0; // 方便加断点
}
};
int main() {
cout << sizeof(MYACLS) << endl;
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi);
printf("MYACLS::m_b2i = %d\n", &MYACLS::m_b2i);
printf("MYACLS::m_i = %d\n", &MYACLS::m_i);
printf("MYACLS::m_j = %d\n", &MYACLS::m_j);
MYACLS myobj;
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;
myobj.m_b2i = 12;
MYACLS *pmyobj = new MYACLS();
pmyobj->m_i = 3;
pmyobj->m_j = 6;
pmyobj->m_bi = 9;
pmyobj->m_b2i = 12;
// Base2 *pbase2 = &myobj; // this指针调整导致pbase2实际是向前走8个字节的内存位置的
// //myobj = 0x0093fad0,经过本语句以后,pbase2 = 0x0093fad8
站在编译器视角,把上边这行语句进行了调整
Base2 *pbase2 = (Base2 *)(((char *)&myobj) + sizeof(Base1));
// Base1 *pbase1 = &myobj;
// Base2 *pbase2 = new MYACLS(); //父类指针new子类对象 ,这里new出来的是24字节
// MYACLS *psubobj = (MYACLS *)pbase2; //比上边地址少了8字节(偏移)
delete pbase2; //报异常。所以我们认为pbase2里边返回的地址不是分配的首地址,而是偏移后地址。
// //而真正分配的首地址应该是在psubobj里边的这个地址
// delete psubobj;
return 1;
}
单一继承的this偏移:
父类没有虚函数,而子类有虚函数时,父类的this指针会调整,比子类的this指针高4个字节,父类的成员变量m_bi
的偏移值是0(偏移值是以对象的this指针为准,这个变量是父类的变量,所以以父类的this指针为准)。要访问一个成员,只要将this指针加上成员的偏移值就可以访问到。
当父类也有虚函数时,父类和子类的this指针都不用调整,都指向vptr,此时父类的成员变量m_bi
的偏移值是4(要跳过vptr)。
多重继承且父类都带虚函数的数据成员布局:
要访问一个类对象中的成员,是通过 this指针(编译器会自动调整)以及该成员的偏移值,这两个因素来定位成员的位置,比如m_b2i
= this 指针 + 偏移值。this指针偏移的调整需要编译器介入来处理完成。更复杂的继承关系内存分布图:
二十三、虚基类问题的提出和初探
传统多重继承造成的空间问题、效率问题、二义性问题:
c1会继承两个m_grand
。为解决传统多重继承出现的问题,下面看一下虚基类,包括虚基类表vbtable(virtual base table)和虚基类表指针vbptr(virtual base table pointer)相关的内容:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Grand { // 爷爷类
public:
int m_grand;
};
class A1 : virtual public Grand {
public:
int m_a1;
};
class A2 : virtual public Grand {
public:
int m_a2;
};
class C1 : public A1, public A2 {
public:
int m_c1;
};
int main() {
cout << sizeof(Grand) << endl;
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(C1) << endl;
// C1 c1;
// c1.m_grand = 12; // 访问不明确,名字冲突,二义性;
// // 引入虚基类之后,就不会出现访问不明确的问题了
// c1.A1::m_grand = 13;
// c1.A2::m_grand = 15;
// 空类sizeof(Grand) = 1;
// virtual虚继承之后,A1,A2里就会被编译器插入一个虚基类表指针,这个指针,有点成员变量的感觉
// A1,A2里因为有了虚基类表指针,因此占用了4个字节
A1 a1;
A2 a2;
// 虚基类表指针,用来指向虚基类表(后续谈)。
return 1;
}
虚基类,让Grand类只被继承一次。A1的内存布局:
c1 的内存布局:
二十四、两层结构时虚基类表内容分析
分析一下虚基类表5~8字节之间的内容。虚基类表一般是8字节,四个字节为一个单位,每多一个虚基类,虚基类表会多加4个字节。编译器因为有虚基类,会给A1、A2类增加默认的构造函数,并且这个默认构造函数里会被编译器增加进去代码,给vbptr虚基类表指针赋值。
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Grand { // 爷爷类
public:
int m_grand;
};
class Grand2 { // 爷爷类
public:
int m_grand2;
// int m_grand2_1;
};
class A1 : virtual public Grand, virtual public Grand2 {
public:
int m_a1;
};
class A2 : virtual public Grand { //, virtual public Grand2
public:
int m_a2;
};
class C1 : public A1, public A2 {
public:
int m_c1;
};
int main() {
cout << sizeof(Grand) << endl;
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(C1) << endl;
A1 a1obj;
a1obj.m_grand = 2;
a1obj.m_grand2 = 6;
// a1obj.m_grand2_1 = 7;
a1obj.m_a1 = 5;
return 1;
}
当A1只虚继承Grand时,A1虚基类表指针指向一个虚基表,这个虚基表中的5~8字节内存记录着从虚基表指针到虚基类对象的偏移值,如下图这个偏移值为8字节:
虚基类表指针的地址 + 这个偏移量就等于虚基类对象首地址,就能够访问到虚基类对象。下图是虚函数表的内容(0x01057B30为虚函数表指针指向的地址,即虚函数表的地址),可以看到5~8字节的内容为8:
继续观察各种形色的继承。当A1虚继承Grand,同时实继承了Grand2:
class A1 : virtual public Grand, public Grand2 {
public:
int m_a1;
};
内存布局为:
虚基类对象在最下面。下图是虚函数表的内容,可以看到5~8字节的内容为8,1~4字节的内容为fffffffc(其实就是-4):
当A1实继承Grand,同时虚继承了Grand2:
class A1 : public Grand, virtual public Grand2 {
public:
int m_a1;
};
内存布局:
下图是虚函数表的内容,可以看到5~8字节的内容为8,1~4字节的内容为fffffffc:
当A1虚继承了Grand,同时虚继承了Grand2:
class A1 : virtual public Grand, virtual public Grand2 {
public:
int m_a1;
};
内存布局:
下图是虚函数表的内容,可以看到5~8字节的内容为8,1~4字节的内容为0,而且因为多了一个虚继承,所以虚函数表多了4个字节,9~2字节的内容为0000000c(虚基表指针到Grand2对象的偏移量):
(a)虚基类表现在是3项,+4,+8都是通过取得虚基类表中的偏移值来赋值的;(b)虚基类表中的偏移量是按照继承顺序来存放的;(c)虚基类子对象一直放在最下边。再分析一下虚基类表的1~4字节的内容,1~4字节内容是:虚基类表指针的首地址和本对象A1首地址之间的偏移量,也就是虚基类表指针的首地址 - A1对象的首地址(比如上述例子中的fffffffc,其实就是-4)。只有对虚基类成员进行处理比如赋值的时候,才会用到虚基类表,取其中的偏移,参与地址的计算。
二十五、三层结构时虚基类表内容分析
A1和A2都是虚继承Grand,所以都有虚函数表指针,虚基类表是在编译的时候已经生成好的,地址是固定的:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Grand { // 爷爷类
public:
int m_grand;
};
class A1 : virtual public Grand {
public:
int m_a1;
};
class A2 : virtual public Grand {
public:
int m_a2;
};
class C1 : public A1, public A2 {
public:
int m_c1;
};
int main() {
cout << sizeof(Grand) << endl;
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(C1) << endl;
// A1 a1obj;
// a1obj.m_grand = 2;
// a1obj.m_grand2 = 6;
a1obj.m_grand2_1 = 7;
// a1obj.m_a1 = 5;
C1 c1obj;
c1obj.m_grand = 2;
c1obj.m_a1 = 5;
c1obj.m_a2 = 6;
c1obj.m_c1 = 8;
// C1 c2obj;
// A2 *pobja2 = new C1();
A2 *pa2 = &c1obj;
pa2->m_grand = 8;
pa2->m_a2 = 9;
return 1;
}
内存布局如下:
c1类同时实继承A1和A2,所以c1有两个分别继承于A1、A2的虚函数表指针。当直接使用C1类对象时,发现没有用到vbptr2,只用到了vbptr1:
C1 c1obj;
c1obj.m_grand = 2;
c1obj.m_a1 = 5;
c1obj.m_a2 = 6;
c1obj.m_c1 = 8;
当使用A2类的指针指向c1对象时,就会用到vbptr2:
A2 *pa2 = &c1obj;
pa2->m_grand = 8;
pa2->m_a2 = 9;
二十六、成员变量地址、偏移、指针等重申
先看一段代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class MYACLS {
public:
int m_i;
int m_j;
int m_k;
};
void myfunc(int MYACLS::*mempoint, MYACLS &obj) {
obj.*mempoint = 260; // 注意写法
}
int main() {
// 对象成员变量内存地址及其指针
MYACLS myobj;
myobj.m_i = myobj.m_j = myobj.m_k = 0;
printf("myobj.m_i = %p\n", &myobj.m_i); // 对象的成员变量是有真正的内存地址的;
MYACLS *pmyobj = new MYACLS();
printf("pmyobj->m_i = %p\n", &pmyobj->m_i);
printf("pmyobj->m_j = %p\n", &pmyobj->m_j);
int *p1 = &myobj.m_i;
int *p2 = &pmyobj->m_j;
*p1 = 15;
*p2 = 30;
printf("p1地址=%p,p1值=%d\n", p1, *p1);
printf("p2地址=%p,p2值=%d\n", p2, *p2);
// 成员变量的偏移值及其指针(和具体对象是没有关系的)
cout << "打印成员变量偏移值----------------" << endl;
printf("MYACLS::m_i偏移值 = %d\n", &MYACLS::m_i); // 打印偏移值,这里用的%d
printf("MYACLS::m_j偏移值 = %d\n", &MYACLS::m_j);
// 用成员变量指针来打印偏移值也可以,看写法
// 大家要知道,成员变量指针里边保存的 实际上是个偏移值(不是个实际内存地址)。
int MYACLS::*mypoint = &MYACLS::m_j;
printf("MYACLS::m_j偏移地址 = %d\n", mypoint);
mypoint = &MYACLS::m_i; // 这里注意,单独使用时直接用名字,定义时才需要加MYACLS::
printf("MYACLS::m_i偏移地址 = %d\n", mypoint);
// 没有指向任何数据成员变量的指针
// 通过 一个对象名或者对象指针后边跟成员变量指针来访问某个对象的成员变量:
myobj.m_i = 13;
myobj.*mypoint = 22;
pmyobj->*mypoint = 19;
myfunc(mypoint, myobj);
myfunc(mypoint, *pmyobj);
cout << "sizeof(mypoint) =" << sizeof(mypoint) << endl; // 也是个4字节;
int *ptest = 0; // 0x00000000
int MYACLS::*mypoint2;
mypoint2 = 0; // 成员变量指针 0xffffffff 也就是-1 和普通指针NULL=0不相同
mypoint2 = NULL; // 0xffffffff 也就是-1
printf("mypoint2 = %d\n", mypoint2);
// if(mypoint2 == mypoint) // 不成立
int MYACLS::*mypoint10 = &MYACLS::m_i;
if (mypoint == mypoint10) { // 成立的
cout << "成立" << endl;
}
// 不让加
// mypoint2 += 1;
// mypoint2++;
// mypoint2 = ((&MYACLS::m_i) + 1);
return 1;
}
运行结果:
二十七、普通成员函数调用方式
知识点归纳:
- c++语言设计的时候有一个要求,就是要求对普通成员函数的调用不应该比全局函数效率差;
- 基于这种设计要求,编译器内部实际上是将对成员函数
myfunc()
的调用转换成了对全局函数的调用; - 成员函数有独立的内存地址,是跟着类走的,并且成员函数的地址是在编译的时候就确定好的。
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class MYACLS {
public:
int m_i;
void myfunc(int abc) { m_i += abc; }
};
// 调用成员函数时 编译器视角
// a)编译器额外增加了一个叫this的形参,是个指针,指向的其实就是生成的对象;
// b)常规成员变量的存取,都通过this形参来进行,比如上述this->m_i + abc;
// void _ZN6MYACLS6myfuncEi(MYACLS *const this, int abc) // 编译器会额外安插一个this指针,一般会扔到参数的开头
//{
// this->m_i + abc;
//}
// 全局函数
void gmyfunc(MYACLS *ptmp, int abc) { ptmp->m_i += abc; }
int main() {
MYACLS myacls;
myacls.myfunc(18); // 调用成员函数
//_ZN6MYACLS6myfuncEi(&myacls,18)
gmyfunc(&myacls, 18); // 调用全局函数
printf("MYACLS::myfunc的地址=%p\n", &MYACLS::myfunc);
return 1;
}
二十八、虚成员函数、静态成员函数调用方式
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class MYACLS {
public:
int m_i;
void myfunc(int abc) {
// m_i += abc; // 这里需要用到this指针,如果this指针为空,则会报告异常
mystfunc();
}
virtual void myvirfunc() {
printf("myvirfunc()被调用,this = %p\n", this);
// myvirfunc2(); // 这行走虚函数表指针调用
MYACLS::myvirfunc2(); // 直接调用虚函数,效率更高。这种写法压制了虚拟机制,不再通过查询虚函数表来调用
// 这种用类名::虚函数名()明确调用虚函数的方式等价于直接调用一个普通函数;
}
virtual void myvirfunc2() { printf("myvirfunc2()被调用,this = %p\n", this); }
// 静态成员函数
// static int m_si;
static void mystfunc() { // 不需要this参数
printf("mystfunc()被调用\n");
// m_si = 1;
}
};
int main() {
// 虚成员函数(虚函数)调用方式
MYACLS myacls;
// myacls.myvirfunc(); // 用对象调用虚函数,就像调用普通成员函数一样,不需要通过虚函数表
MYACLS *pmyacls = new MYACLS();
// pmyacls->myvirfunc(); // 要通过虚函数表指针查找虚函数表,通过虚函数表
// 找到虚函数的入口地址,完成对虚函数的调用
// 从编译器视角
//(*pmyacls->vptr[0])(pmyacls);
// a)vptr,编译器给生成的虚函数表指针,指向虚函数表
// b)[0] 虚函数表中第一项。代表myvirfunc()地址
// c)传递一个参数进去,就是this,也是编译器给加的
// d)*就得到了虚函数的地址;
// printf("MYACLS::myvirfunc2虚函数的地址为%p", &MYACLS::myvirfunc2);
// 静态成员函数调用方式
myacls.mystfunc();
pmyacls->mystfunc();
MYACLS::mystfunc();
// 这三行在编译器眼中,跟调用全局函数一样:
//_ZN6MYACLS8mystfuncEv();
//_ZN6MYACLS8mystfuncEv();
//_ZN6MYACLS8mystfuncEv();
// 空对象
((MYACLS *)0)->mystfunc(); // 能够正常调用静态成员函数
((MYACLS *)0)->myfunc(12); // 如果myfunc需要用到this,则不能这样调用,反之则可以
// 空对象作用:有些成员函数希望支持独立于类对象之外的存取操作;
myacls.myfunc(12);
pmyacls->myfunc(12);
// 静态成员函数特性
// a)静态成员函数没有this指针,这点最重要
// b)无法直接存取类中普通的非静态成员变量;
// c)静态成员函数不能在最后使用const,也不能设置为virtual
// d)可以用类对象调用,但不非一定要用类对象调用。
// e)静态成员函数等同于非成员函数,有的需要提供回调函数的这种场合,可以将静态成员函数作为回调函数;
printf("MYACLS::mystfunc()地址 = %p\n", MYACLS::mystfunc);
delete pmyacls;
return 1;
}
二十九、静动态类型、绑定,坑点,多态体现深谈
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Base {
public:
void myfunc() { // 普通成员函数
cout << "Base::myfunc()" << endl;
}
virtual void myvirfunc(int value = 1) { cout << "Base::myvirfunc(),value = " << value << endl; }
};
class Derive : public Base {
public:
void myfunc() { // 普通成员函数
cout << "Derive::myfunc()" << endl;
}
virtual void myvirfunc(int value = 2) { cout << "Derive::myvirfunc(),value = " << value << endl; }
};
// class Derive2 :public Base {
// public:
//};
class A {
public:
virtual void myvirfunc() {}
};
int main() {
// 静态类型和动态类型
// 静态类型:对象定义时的类型,编译期间就确定好的
// Base base; // base的静态类型是Base,没有动态类型,因为不是指针也不是引用
// Derive derive; // derive的静态类型是Derive,没有动态类型,因为不是指针也不是引用
// Base *pbase; // pbase的静态类型是Base *,至少目前没有动态类型,因为它没有指向任何对象
// Base *pbase2 = new Derive(); // pbase2的静态类型是Base * ,动态类型是Derive
// Base *pbase3 = new Derive2(); // pbase3的静态类型是Base *,动态类型是Derive2
// 动态类型:对象目前所指向的类型(运行的时候才决定的类型);
// 一般只有指针或者引用才有动态类型的说法。而且一般都是指父类的指针或者引用
// 另外,动态类型在执行过程中可以改变,比如
// pbase = pbase2; // pbase的动态类型Derive
// pbase = pbase3; // pbase的动态类型改变为Derive2
// 静态绑定和动态绑定
// 静态绑定:绑定的是静态类型,所对应的函数或者属性依赖于对象的静态类型,发生在编译期
// 动态绑定:绑定的是动态类型,所对应的函数或者属性依赖于对象的动态类型,发生在运行期
// a)普通成员函数是静态绑定,而虚函数是动态绑定;
// b)缺省参数一般是静态绑定
// 下面有示例。
// 继承的非虚函数坑
Derive derive;
Derive *pderive = &derive;
pderive->myfunc(); // Derive::myfunc()
Base *pbase = &derive;
pbase->myfunc(); // Base::myfunc()
// 普通成员函数是静态绑定,换句话说,myfunc() 是普通成员函数。
// 这里到底调用父类的myfunc还是子类的myfunc取决于调用者的静态类型;
// 因为这里pderive的静态类型是Derive *,所以调用的是Derive的myfunc();
// pbase的静态类型是Base,所以调用的是Base里的myfunc();
// 结论:不应该在子类中重新定义一个继承来的非虚函数。否则当用父类指针指向子类对象,再调用
// 普通成员函数,会调用到父类的成员函数。
// 虚函数的动态绑定
cout << "----------------" << endl;
Base base;
pderive->myvirfunc(); // 执行Derive的myvirfunc(); --- Derive::myvirfunc()
pbase->myvirfunc(); // pbase动态类型是Derive,而虚函数是动态绑定,参照是它的动态类型;--- Derive::myvirfunc()
// 理论上是打印:Derive::myvirfunc(),value = 2
// 但实际上是:Derive::myvirfunc(),value = 1
// 因为函数参数缺省值的静态绑定,所以缺省参数绑定到了父类函数的缺省参数上去了
// 所以不要重新定义虚函数的缺省参数的值
Base &yinbase = derive;
cout << "----------------begin" << endl;
yinbase.myvirfunc(); // Derive::myvirfunc(),value = 1
cout << "----------------end" << endl;
pbase = &base;
pbase->myvirfunc(); //----Base::myvirfunc();
// 虚函数是动态绑定;换句话说,myvirfunc()是虚函数,执行哪个myvivfunc()取决于调用者的动态类型,
// 这里pbase的动态类型分别Derive,Base,所以调用的也分别是Derive和Base的myvirfunc(),
// pderive的动态类型是Derive,所以调用的是Derive的myvirfunc();
// c++ 中的多态性的体现
// c++ 中“多态性”这个概念,分两方面谈:
// a)从代码实现上
// b)从表现形式上
//有一个观点是肯定的:多态,必须是存在虚函数,没有虚函数,绝不可能存在多态,有虚函数并且调用虚函数;
// (a)从代码实现上
// 当我们调用一个虚函数时,走的是不是通过查询虚函数表来找到虚函数入口地址,
// 然后去执行虚函数,如果走的是这个途径,那就是多态,如果不走这个途径,它就不是多态;
A *pa = new A();
pa->myvirfunc(); // 是多态
A a;
a.myvirfunc(); // 不是多态
A *pa1 = &a;
pa1->myvirfunc(); // 是多态
// b)从表现形式上(通过代码来体现)
// (1)有继承关系,有父类有子类,父类中必须有虚函数(这意味着子类中一定有虚函数),子类重写父类的虚函数;
// (2)父类指针或者引用指向子类对象
// (3)当以父类指针或者引用调用子类中重写了的虚函数时,就能看出来多态的表现了,因为调用的是子类的虚函数;
return 1;
}
三十、单继承虚函数趣味性测试和回顾
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Base {
public:
// virtual void f() { cout << "Base::f()" << endl; }
// virtual void g() { cout << "Base::g()" << endl; }
// virtual void h() { cout << "Base::h()" << endl; }
virtual void pvfunc() = 0;
};
class Derive : public Base {
public:
// virtual void i() { cout << "Derive::i()" << endl; }
// virtual void g() { cout << "Derive::g()" << endl; }
// void myselffunc() {} // 只属于Derive的函数
};
int main() {
// 单继承下的虚函数
// Derive myderive;
// Derive *pmyderive = &myderive;
// pmyderive->f();
// pmyderive->g();
// pmyderive->h();
// pmyderive->i();
// Base *pb = new Derive(); // 基类指针 指向一个子类对象
// pb->g();
// 编译器视角
// (*pb->vptr[1])(pb);
// Derive myderive;
// Base &yb = myderive; // 基类引用 引用一个子类对象
// yb.g();
// 唯一需要在执行期间知道的东西就是通过哪个虚函数表来调用虚函数,(父类的还是子类的);
// 回顾和一些小试验
// 虚函数地址:编译期间知道,写在了可执行文件中,编译期间已经构建出来。
// vptr编译期间产生。编译器在构造函数中插入了给vptr赋值的代码;
// 当创建对象时,因为要执行对象的构造函数,此时vptr就被赋值;
// Derive a1;
// Derive a2;
// Derive *pa3 = new Derive();
// Base b1;
cout << sizeof(Base) << endl;
return 1;
}
子类和父类的虚函数表示例图:
子类自己增加了一个虚函数virtual void i()
,示意图(在虚函数表最下面):
三十一、多继承虚函数深释
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
virtual ~Base() {
int abc;
abc = 1;
}
};
class Base2 {
public:
virtual void hBase2() { cout << "Base2::hBase2()" << endl; }
virtual ~Base2() {
int abc;
abc = 1;
}
};
class Derive : public Base, public Base2 {
public:
virtual void i() { cout << "Derive::i()" << endl; }
virtual void g() { cout << "Derive::g()" << endl; }
void myselffunc() {} // 只属于Derive的函数
virtual ~Derive() {
int abc;
abc = 1;
}
};
int main() {
// 多继承下的虚函数
// 多重继承复杂性体现在后边这个基类上;
Base2 *pb2 = new Derive();
// 编译器视角
// Derive *temp = new Derive();
// Base2 *pb2 = temp + sizeof(Base); // 跳过Base
// Base2 *pb2 = (Base2 *)((char *)temp + sizeof(Base)); // 上一句代码实际上应该是这样
delete pb2;
return 1;
}
代码中 pb2 指向的结构如下图:
如何成功删除用第二基类指针new出来的继承类对象?(a)要删除的实际是整个Derive()对象;(b)要能够保证Derive()对象的析构函数被正常调用;(c)编译器会调用Base2的析构函数,还是调用Derive的析构函数呢?要调用Derive的析构函数才能正常释放内存;(d)执行delete pb2
时,系统的动作会是?如果Base2里没有析构函数,编译器会直接删除以pb2开头的这段内存,一定报异常,因为这段内存压根就不是new起始的内存(没有调用Derive的析构函数);如果Base2里有一个析构函数,但这个析构函数是个普通析构函数(非虚析构函数),那么当delete pb2
时,这个析构函数就会被系统调用,但是delete的仍旧是pb2开头这段内存,所以一定报异常,因为这段内存压根就不是new起始的内存;析构函数如果不是虚函数,编译器会实施静态绑定,静态绑定意味着delete Base2
指针时,删除的内存开始地址就是pb2的当前位置,所以肯定是错误的(没有调用Derive的析构函数);如果Base2里是一个虚析构函数,则会依次调用~Dervice()
、~Base2()
、~Base()
;Derive里就算没有虚析构函数,因为Base2里有虚析构函数,编译器也会为此给Derive生成虚析构函数,为了调用~Base2()
和~Base()
虚析构函数;(e)结论就是,凡是涉及到继承的,所有类都要求写虚析构函数。
delete pb2; // 下面是 delete pb2 的反汇编代码
00F230BD call edx
00F210AA jmp Derive::`vector deleting destructor' (0F22878h)
00F22878 sub ecx,4 调整this指针,指向了Derive对象首地址 **********************
00F2287B jmp Derive::`vector deleting destructor' (0F210D2h)
00F210D2 jmp Derive::`scalar deleting destructor' (0F22970h)
00F22996 call Derive::~Derive (0F2141Ah) // 调用~Derive
00F2141A jmp Derive::~Derive (0F22580h)
00F225EC call Base2::~Base2 (0F212DFh) // 调用~Base2
00F225F4 call Base::~Base (0F212E9h) // 调用~Base
00F229A9 call operator delete (0F212EEh)
下面代码为了画出虚函数表:
Base *pbm = new Base();
Base2 *pb222 = new Base2();
Derive *p11212 = new Derive();
p11212->g();
p11212->i(); // 走虚函数表
Derive dddd;
dddd.i(); // 直接调用虚函数
Derive类的第二个虚函数表中发现了thunk字样:它一般用在多重继承中(从第二个虚函数表开始可能就会有);用于this指针调整。它其实是一段汇编代码,这段代码干两个事情:(1)调整this指针;(2)调用Derive析构函数。如下图:
三十二、多继承第二基类虚函数支持、虚继承带虚函数
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
//
// class Base {
// public:
// virtual void f() { cout << "Base::f()" << endl; }
// virtual void g() { cout << "Base::g()" << endl; }
// virtual void h() { cout << "Base::h()" << endl; }
//
// virtual ~Base() {
//
// }
//
// virtual Base *clone() const {
// return new Base();
// }
//
//};
//
// class Base2 {
// public:
// virtual void hBase2() {
//
// cout << "Base2::hBase2()" << endl;
// }
//
// virtual ~Base2() {
//
// }
//
// virtual Base2 *clone() const {
// return new Base2();
// }
//};
//
// class Derive :public Base,public Base2 {
// public:
// virtual void i() { cout << "Derive::i()" << endl; }
// virtual void g() { cout << "Derive::g()" << endl; }
// void myselffunc() {} //只属于Derive的函数
//
// virtual ~Derive() {
//
// }
// virtual Derive *clone() const {
// return new Derive();
// }
//};
class Base {
public:
virtual void f() {}
virtual ~Base() {}
int m_basei;
};
class Derive : public virtual Base {
public:
virtual ~Derive() {}
int m_derivei;
};
int main() {
// 多重继承第二基类对虚函数支持的影响(this指针调整作用)
// 子类继承了几个父类,子类就有几个虚函数表
// 多重继承下,有几种情况,第二个或者后续的基类会对虚函数的支持产生影响
// this指针调整的目的就是让对象指针正确的指向对象首地址,从而能正确的调用对象的成员函数或者说正确确定数据成员的存储位置。
// a)通过指向第二个基类的指针调用继承类的虚函数;
// Base2 *pb2 = new Derive();
// delete pb2; // 调用继承类的虚析构函数
// b)一个指向派生类的指针,调用第二个基类中的虚函数
// Derive *pd2 = new Derive();
// pd2->hBase2();
// c)允许虚函数的返回值类型有所变化
// Base2 *pb1 = new Derive(); // pb1指向的是Base2子对象的首地址
// Base2 *pb2 = pb1->clone(); // pb1->clone()调用的是 Derive::clone();
// 执行clone()时,pb1首先会调整回指向Derivce对象的首地址,这样调用的是Derive版本的clone()
// 虚继承下的虚函数
// cout << sizeof(Derive) << endl; // 16
// Derive dobj;
// dobj.m_basei = 2;
// dobj.m_derivei = 5;
// Derive *pdobj = new Derive();
// pdobj->f();
// Base *pbase = new Derive();
// pbase->m_basei = 6;
Derive *pderive = new Derive();
Base *pbase2 = (Base *)pderive;
pbase2->m_basei = 7;
return 1;
}
pbase2 结构:(父类指针指向子类对象)
三十三、RTTI运行时类型识别回顾与存储位置介绍
演示代码:
#include "pch.h"
#include <iostream>
#include <time.h >
#include <stdio.h>
#include <vector>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
virtual ~Base() {}
};
class Derive : public Base {
public:
virtual void g() { cout << "Derive::g()" << endl; }
void myselffunc() {} // 只属于Derive的函数
virtual ~Derive() {}
};
int main() {
// RTTI(运行时类型识别)简单回顾
Base *pb = new Derive();
pb->g(); // Derive::g()
Derive myderive;
Base &yb = myderive;
yb.g(); // Derive::g()
// c++运行时类型识别RTTI,要求父类中必须至少有一个虚函数;如果父类中没有虚函数,那么得到RTTI就不准确;
// RTTI可以在执行期间查询一个多态指针,或者多态引用的信息;
// RTTI能力靠typeid和dynamic_cast运算符来体现;
cout << typeid(*pb).name() << endl; // 输出 class Derive
cout << typeid(yb).name() << endl; // 输出 class Derive
Derive *pderive = dynamic_cast<Derive *>(pb);
if (pderive != NULL) {
cout << "pb实际是一个Derive类型" << endl;
pderive->myselffunc(); // 调用自己专属函数
}
// RTTI实现原理
// typeid返回的是一个常量对象的引用,这个常量对象的类型一般是type_info(类);
const std::type_info &tp = typeid(*pb);
Base *pb2 = new Derive();
Base *pb3 = new Derive();
const std::type_info &tp2 = typeid(*pb2);
const std::type_info &tp3 = typeid(*pb3);
cout << typeid(int).name(); // 输出 int
if (tp == tp2) {
cout << "很好,类型相同" << endl;
}
//其他用法,静态类型;不属于多态类型
// cout << typeid(int).name() << endl;
// cout << typeid(Base).name() << endl;
// cout << typeid(Derive).name() << endl;
// Derive *pa3 = new Derive();
// cout << typeid(pa3).name();
// Base *pb = new Derive();
// Derive myderive;
// Base &yb = myderive;
// cout << typeid(*pb).name() << endl; // class Derive
// cout << typeid(yb).name() << endl; // class Derive
// Base *pb2 = new Derive();
// const std::type_info &tp2 = typeid(*pb2);
// RTTI的测试能力跟基类中是否存在虚函数表有关系,如果基类中没有虚函数,
// 也就不存在基类的虚函数表,RTTI就无法得到正确结果;
printf("tp2地址为:%p\n", &tp2);
long *pvptr = (long *)pb2;
long *vptr = (long *)(*pvptr);
printf("虚函数表首地址为:%p\n", vptr);
printf("虚函数表首地址之前一个地址为:%p\n", vptr - 1); //这里的-1实际上是往上走了4个字节
long *prttiinfo = (long *)(*(vptr - 1));
prttiinfo += 3; // 跳过12字节 (3*4 = 12字节)
long *ptypeinfoaddr = (long *)(*prttiinfo);
const std::type_info *ptypeinfoaddrreal = (const std::type_info *)ptypeinfoaddr;
printf("ptypeinfoaddrreal地址为:%p\n", ptypeinfoaddrreal);
cout << ptypeinfoaddrreal->name() << endl;
// vptr,vtbl,rtti的type_info信息 构造时机
// rtti的type_info信息:编译后就存在了,写到了可执行文件中
//总结:各个编译器实现有一定差异,但总体都是以虚函数表开始地址为突破口
return 1;
}
输出如下:
type_info信息结构:
有些编译器的实现可能如下:
三十四、函数调用、继承关系性能说
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
using namespace std;
namespace _nmsp1 { // 命名空间
// 函数调用中编译器的循环代码优化
// debug,release
//(1)优化循环,把循环优化成1条语句;
//(2)在编译期间,编译器也具有运算能力,有些运算编译器在编译期间就能搞定;
__int64 mytest(int mv) {
__int64 icout = 0;
for (int i = 1; i < 1000000; i++) {
icout += 1;
if (i == 10000 && mv == 999) {
printf("------\n");
}
}
// icout += 循环多少次之后的和值
return icout;
}
void func() {
clock_t start, end;
__int64 mycout = 1;
start = clock();
for (int i = 0; i <= 1000; i++) {
// mycout += mytest(i); //给固定参数时,编译器将这种参数固定的函数调用视为一种不变的表达式
mycout += mytest(6);
}
// mycout += 循环1000次的和值
end = clock();
cout << " 用时(毫秒):" << end - start << endl;
cout << " mycout:" << mycout << endl;
}
} // namespace _nmsp1
namespace _nmsp2 {
// 继承关系深度增加,开销一般也会增加
//很多情况下,锁着继承深度的增加,开销或者说执行时间也会增加;
//(2.1)多重继承一般也会导致开销增加
class A {
public:
A() { cout << "A::A()" << endl; }
};
class A1 {
public:
A1() { cout << "A1::A1()" << endl; }
};
class B : public A, public A1 {
public:
};
class C : public B {
public:
C() { cout << "C::C()" << endl; }
};
void func() { C cobj; }
} // namespace _nmsp2
namespace _nmsp3 {
// 继承关系深度增加,虚函数导致的开销增加
class A {
public:
// A() {
// cout << "A::A()" << endl;
//}
virtual void myvirfunc() {}
};
class B : public A {
public:
};
class C : public B {
public:
C() { cout << "C::C()" << endl; }
};
void func() { C *pc = new C(); }
} // namespace _nmsp3
int main() {
//_nmsp1::func();
//_nmsp2::func();
_nmsp3::func();
return 1;
}
三十五、指向成员函数的指针及vcall进一步谈
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
using namespace std;
namespace _nmsp1 { // 命名空间
// 指向成员函数的指针
// 成员函数地址,编译时就确定好的。但是,调用成员函数,是需要通过对象来调用的;
// 所有常规(非静态)成员函数,要想调用,都需要一个对象来调用它;
class A {
public:
void myfunc1(int tempvalue1) { cout << "tempvalue1 = " << tempvalue1 << endl; }
void myfunc2(int tempvalue2) { cout << "tempvalue2 = " << tempvalue2 << endl; }
static void mysfunc(int tempvalue) { cout << "A::mysfunc()静态成员函数--tempvalue = " << tempvalue << endl; }
};
void func() {
A mya;
void (A::*pmypoint)(int tempvalue) = &A::myfunc1; // 定义一个成员函数指针并给初值
pmypoint = &A::myfunc2; // 给成员函数指针赋值
(mya.*pmypoint)(15); // 通过成员函数指针来调用成员函数,必须要通过对象的介入才能调用
A *pmya = new A();
(pmya->*pmypoint)(20); // 用对象指针介入来使用成员函数指针来调用成员函数
// 编译器视角
// pmypoint(&mya, 15);
// pmypoint(pmya, 20);
void (*pmyspoint)(int tempvalue) = &A::mysfunc; //一个普通的函数指针,而不是成员函数指针
pmyspoint(80);
// 通过成员函数指针对常规的成员函数调用的成本,和通
// 过普通的函数指针来调用静态成员函数,成本上差不多;
}
} // namespace _nmsp1
namespace _nmsp2 {
// 指向虚成员函数的指针及vcall进一步谈
// vcall (vcall trunk) = virtual call:虚调用
// 它代表一段要执行的代码的地址,这段代码引导我们去执行正确的虚函数,
// 或者我们直接把vcall看成虚函数表,如果这么看待的话,那么vcall{0}代表的就是虚函数表里的第一个函数,
// vcall{4}就代表虚函数表里的第二个虚函数
// 完善理解:&A::myvirfunc:打印出来的是一个地址,这个地址中有一段代码,这段代码中记录的是
// 该虚函数在虚函数表中的一个偏移值,有了这个偏移值,再有了具体的对象指针,我们就能够知道调用
// 的是哪个虚函数表里边的哪个虚函数了;
// 成员函数指针里,保存的可能是一个vcall(vcall trunk)地址(虚函数),要么也可能是一个真正的
// 成员函数地址,如果是一个vcall地址,那vcall能够引导编译器找出正确的虚函数表中的虚函数地址进行调用;
class A {
public:
void myfunc1(int tempvalue1) { cout << "tempvalue1 = " << tempvalue1 << endl; }
void myfunc2(int tempvalue2) { cout << "tempvalue2 = " << tempvalue2 << endl; }
static void mysfunc(int tempvalue) { cout << "A::mysfunc()静态成员函数--tempvalue = " << tempvalue << endl; }
virtual void myvirfuncPrev(int tempvalue) { cout << "A::myvirfuncPrev()虚成员函数--tempvalue = " << tempvalue << endl; }
virtual void myvirfunc(int tempvalue) { cout << "A::myvirfunc()虚成员函数--tempvalue = " << tempvalue << endl; }
};
void func() {
// 成员函数指针 -- vcall(vcall trunk)地址(虚函数)
void (A::*pmyvirfunc)(int tempvalue) = &A::myvirfunc;
A *pvaobj = new A;
pvaobj->myvirfunc(190);
(pvaobj->*pmyvirfunc)(190);
printf("%p\n", &A::myvirfunc);
pmyvirfunc = &A::myfunc2; //真正的成员函数地址
(pvaobj->*pmyvirfunc)(33);
delete pvaobj;
}
} // namespace _nmsp2
namespace _nmsp3 {
// vcall在继承关系中的体现
class A {
public:
// void myfunc1(int tempvalue1) {
// cout << "tempvalue1 = " << tempvalue1 << endl;
//}
// void myfunc2(int tempvalue2) {
// cout << "tempvalue2 = " << tempvalue2 << endl;
//}
// static void mysfunc(int tempvalue) {
// cout << "A::mysfunc()静态成员函数--tempvalue = " << tempvalue << endl;
//}
// virtual void myvirfuncPrev(int tempvalue) {
// cout << "A::myvirfuncPrev()虚成员函数--tempvalue = " << tempvalue << endl;
//}
virtual void myvirfunc(int tempvalue) { cout << "A::myvirfunc()虚成员函数--tempvalue = " << tempvalue << endl; }
virtual ~A() {}
};
class B : public A {
public:
virtual void myvirfunc(int tempvalue) { cout << "B::myvirfunc()虚成员函数--tempvalue = " << tempvalue << endl; }
virtual ~B() {}
};
void func() {
B *pmyb = new B(); // pmyb:对象指针
void (B::*pmyvirfunc)(int tempvalue) = &B::myvirfunc; // 成员函数指针
(pmyb->*pmyvirfunc)(190);
printf("%p\n", &A::myvirfunc); // vcall地址 和下个vcall地址不一样
printf("%p\n", &B::myvirfunc);
}
} // namespace _nmsp3
int main() {
//_nmsp1::func();
//_nmsp2::func();
_nmsp3::func();
return 1;
}
三十六、inline函数回顾和扩展细节说
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
using namespace std;
namespace _nmsp1 { // 命名空间
// inline函数回顾
// 使用inline之后,编译器内部会有一个比较复杂的测试算法来评估这个inline函数的复杂度;
// 可能会统计这个inline函数中,赋值次数,内部函数调用,虚函数调用等次数 ---权重
//(1)开发者写inline只是对编译器的一个建议,但如果编译器评估这个inline函数复杂度过高,
// 这个inline建议就被编译器忽略;
//(2)如果inline被编译器采纳,那么inline函数的扩展,就要在调用这个inline函数的那个点上进行,
// 那么可能带来额外的问题比如参数求值,可能产生临时对象的生成和管理;
// inline扩展细节
//(2.1)形参被对应实参取代
//(2.2)局部变量的引入(局部变量能少用尽量少用,能不用尽量不用)
//(2.3)inline失败情形
inline int myfunc(int testv) {
// int tempvalue = testv * (5 + 4) * testv;
// return testv * (5 + 4) * testv;
// return tempvalue;
if (testv > 10) {
testv--;
myfunc(testv);
}
return testv;
}
// int rtnvalue()
//{
// return 5;
//}
void func() {
// int i = myfunc(12 + 15); //编译器会先求值,然后用实参再替换形参
// int a = 80;
// int i = myfunc(a + 15); //编译器会先计算a和15的和值,然后再替换掉形参
// int i = myfunc(rtnvalue() + 15); //编译器会先调用rtnvalue()得到返回值,返回值和15做加法,然后再替换掉形参
int i = myfunc(12);
cout << i << endl;
}
} // namespace _nmsp1
int main() {
_nmsp1::func();
return 1;
}
三十七、继承体系下的对象构造步骤
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
namespace _nmsp1 { // 命名空间
class A {
public:
A() {
printf("A this = %p\n", this);
cout << "A::A()" << endl;
}
virtual ~A() {}
virtual void myvirfunc() {}
virtual void myvirfunc2() {}
};
class B : public A {
public:
B() {
printf("B this = %p\n", this);
cout << "B::B()" << endl;
}
virtual ~B() {}
virtual void myvirfunc() {}
virtual void myvirfunc2() {}
};
class C : public B {
public:
C() : m_c(11) {
// 不要在构造函数中使用诸如memcpy或者直接操作等手段,
// 来修改虚函数表指针的值,否则调用虚函数时就可能造成系统崩溃
// memcpy(this,)
// 某个类的构造函数中调用一个虚函数,
// 那么走的不是虚函数表,而是直接调用
myvirfunc(); // 构造函数中,这里没有走虚函数表,而是直接通过虚函数地址,
// 直接调用这个虚函数(静态方式调用)
printf("C this = %p\n", this);
cout << "C::C()" << endl;
}
virtual ~C() {}
virtual void myvirfunc() { myvirfunc2(); }
virtual void myvirfunc2() {}
int m_c;
};
//---------------------
void func() {
// C cobj;
C *mycobj = new C();
mycobj->myvirfunc(); // 代码实现上的多态
}
} // namespace _nmsp1
int main() {
_nmsp1::func();
return 1;
}
对象的构造顺序:
C cobj;
C::C() //末端类
B::B()
A::A() //根源类
vptr = A::vftable; //编译器插入的代码 34 9b 2f 01
cout << "A::A()" << endl; //我们自己的代码
vptr = B::vftable; //编译器插入的代码 54 9b 2f 01
cout << "B::B()" << endl; //我们自己的代码
vptr = C::vftable; //编译器插入的代码 3c 9b 2f 01
//....不要在这里动虚函数表指针
//memset(this,0,sizeof(C));
m_c = 11; //初始化列表中基本类型变量m_c的赋值时机
cout << "C::C()" << endl; //我们自己的代码
三十八、对象复制语义学、析构函数语义学
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
namespace _nmsp1 { // 命名空间
// 对象的默认复制行为
// 如果不写自己的拷贝构造函数和拷贝赋值运算符,编译器也会有默认的对象拷贝和对象赋值行为;
// 拷贝赋值运算符,拷贝构造函数
// 当我们提供自己的拷贝赋值运算符和拷贝构造函数时,我们就接管了系统默认的拷贝行为,
// 此时,我们有责任在拷贝赋值运算符和拷贝构造函数中写适当的代码,来完成对象的拷贝或者赋值的任务;
// 如何禁止对象的拷贝构造和赋值:把拷贝构造函数和拷贝赋值运算符私有起来,只声明,不需要写函数体;
class A {
public:
int m_i, m_j;
A() {} // 缺省构造函数
private:
A& operator=(const A& tmp); // 拷贝赋值运算符
//{
// m_i = tmp.m_i;
// m_j = tmp.m_j;
// return *this;
//}
A(const A& tmp); // 拷贝构造函数
//{
// m_i = tmp.m_i;
// m_j = tmp.m_j;
//}
};
void func() {
A aobj;
aobj.m_i = 15;
aobj.m_j = 20;
// A aobj2 = aobj; // 执行拷贝构造函数(如果你写了拷贝构造函数)
A aobj2;
A aobj3;
aobj3.m_i = 13;
aobj3.m_j = 16;
// aobj2 = aobj3; // 执行拷贝赋值运算符(如果你写了拷贝赋值运算符)
}
} // namespace _nmsp1
namespace _nmsp2 {
// 析构函数语义
// 1、析构函数被合成
// 什么情况下编译器会给我们生成一个析构函数?
// a)如果继承一个基类,基类中带析构函数,那么编译器就会给A合成出一个析构函数来调用基类JI中的析构函数
// b)如果类成员是一个类类型成员,并且这个成员带析构函数,编译器也会合成出一个析构函数,
// 这个析构函数存在的意义是要调用m_j这个类类型成员所在类的析构函数;
// 2、析构函数被扩展
// 如果我们有自己的析构函数,那么编译器就会在适当的情况下扩展我们的析构函数代码;
// a)如果类成员m_j是一个类类型JI成员,并且这个成员m_j带析构函数~JI(),编译器扩展类A的析构函数~A()代码
// 先执行了类A的析构函数代码,再执行JI的析构函数代码
// b)如果继承一个基类,基类中带析构函数,那么编译器就会扩展类A的析构函数来调用基类JI中的析构函数
class JI {
public:
JI() { cout << "JI::JI()" << endl; }
virtual ~JI() { cout << "JI::~JI()" << endl; }
};
class A : public JI {
public:
// JI m_j; // 类类型成员变量
A() { cout << "A::A()" << endl; }
virtual ~A() { cout << "A::~A()" << endl; }
};
void func() { A aobj; }
} // namespace _nmsp2
int main() {
//_nmsp1::func();
_nmsp2::func();
return 1;
}
三十九、局部对象、全局对象的构造和析构
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
namespace _nmsp1 { // 命名空间
// 局部对象的构造和析构
// 只要出了对象的作用域,编译器总会在适当的地方插入调用对象析构函数的代码;
class A {
public:
A() { cout << "A::A()" << endl; }
~A() { cout << "A::~A()" << endl; }
int m_i;
};
void func() {
int i;
int j;
//......
// if (1) {
// return;
//}
A obja; // 最好现用现定义 ,对于局部对象,里边的m_i是随机值;
obja.m_i = 10;
int mytest = 1;
if (mytest == 0) { // swtich case return;
return;
}
int b = 0;
b = 10;
return;
}
} // namespace _nmsp1
namespace _nmsp2 {
// 全局对象的构造和析构
// 全局变量是放在数据段里的
// 全局对象,在不给初值的情况下,编译器默认会把全局对象所在内存全部清0;
// 全局变量在编译阶段就会把空间分配出来(全局变量的地址在编译期间就确定好的)。
// 全局对象构造和析构的步骤:
// a)全局对象g_aobj获得地址(编译时确定好的,内存也是编译时分配好的,内存时运行期间一直存在)
// b)把全局对象g_aobj的内存内容清0的能力(也叫静态初始化)
// c)调用全局对象g_aobj所对应的类A的构造函数
//.....
// d)main() {
// //......
//}
//....
// e)调用全局对象g_aobj所对应类A的析构函数
class A {
public:
A() { cout << "A::A()" << endl; }
~A() { cout << "A::~A()" << endl; }
int m_i;
};
A g_aobj; // 全局对象,该全局对象在main函数执行之前就被构造完毕,可以在main函数中直接使用了
// 在main函数执行完毕后,才被析构掉的
void func() { printf("g_aobj全局对象的地址=%p\n", &g_aobj); }
} // namespace _nmsp2
// main函数开始之前要干很多事
int main() {
_nmsp2::g_aobj.m_i = 6;
//_nmsp1::func();
_nmsp2::func();
return 1;
}
// main函数结束之后要干很多事
四十、局部静态对象、对象数组构造析构和内存分配
知识点归纳:
- 静态局部变量和静态全局变量的区别:从作用域看,静态局部变量具有局部作用域,静态全局变量具有全局作用域;从分配内存空间看,静态局部变量、静态全局变量都在静态存储区分配空间。
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
namespace _nmsp1 {
// 局部静态对象的构造和析构
// a)如果不调用myfunc()函数,那么根本不会触发A的构造函数;
// b)局部静态对象,内存地址是在编译期间就确定好的;
// c)静态局部变量刚开始也被初始化为0;
// d)局部静态对象的析构,是在main函数执行结束后才被调用的。(前提是这个静态局部对象被构造过)
// 局部静态对象只会被构造一次,在调用的时候构造;在main函数执行完毕后析构
class A {
public:
A() { cout << "A::A()" << endl; }
~A() { cout << "A::~A()" << endl; }
int m_i;
};
// void myfunc()
const A &myfunc() {
// 局部静态对象
static A s_aobj1; // 不管myfunc()函数被调用几次,s_aobj1这种静态局部对象只会被构造1次(只调用一次构造函数)
// static A s_aobj2;
printf("s_aobj1的地址是%p\n", &s_aobj1);
// printf("s_aobj2的地址是%p\n", &s_aobj2);
return s_aobj1;
}
void func() {
myfunc();
myfunc();
}
} // namespace _nmsp1
namespace _nmsp2 {
// 局部静态对象数组的内存分配
class A {
public:
A() {
// cout << "A::A()" << endl;
}
~A() {
// cout << "A::~A()" << endl;
}
int m_i;
};
void myfunc() {
static A s_aobj[1000'0000]; // 数组内存应该是连续的
// for (int i = 0; i < 10000000; i++) { // 让编译器的分配内存优化能力失效,如果申请的内存没用,那么编译器实际上可能不会申请那么多内存
// s_aobj[i].m_i = i;
//}
printf("s_aobj数组的首地址是%p\n", s_aobj);
}
void func() {
myfunc();
myfunc();
}
} // namespace _nmsp2
int main() {
//_nmsp1::func();
_nmsp2::func();
while (1) {}
return 1;
}
四十一、临时性对象的详细探讨
演示代码:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
namespace _nmsp1 {
// 拷贝构造函数相关的临时性对象
class A {
public:
A() { cout << "A::A()构造函数被执行" << endl; }
A(const A& tmpobj) { cout << "A::A()拷贝构造函数被执行" << endl; }
~A() { cout << "A::~A()析构函数被执行" << endl; }
};
A operator+(const A& obj1, const A& obj2) {
A tmpobj;
printf("tmpobj的地址为%p\n", &tmpobj);
printf("---------------------\n");
//.....
return tmpobj; // 编译器产生临时对象,把tmpobj对象的内容通过调用拷贝构造函数,把
// tmpobj的内容拷贝构造给这个临时对象,然后返回的是这个临时对象;
}
void func() {
A myobj1;
printf("myobj1的地址为%p\n", &myobj1);
A myobj2;
printf("myobj2的地址为%p\n", &myobj2);
A resultobj = myobj1 + myobj2; // 这个从operator +里返回的临时对象直接构造到了resultobj里;
printf("resultobj的地址为%p\n", &resultobj);
return;
}
} // namespace _nmsp1
namespace _nmsp2 {
// 拷贝赋值运算符相关的临时性对象
class A {
public:
A() { cout << "A::A()构造函数被执行" << endl; }
A(const A& tmpobj) { cout << "A::A()拷贝构造函数被执行" << endl; }
A& operator=(const A& tmpaobj) {
cout << "A::operator()拷贝赋值运算符被执行" << endl;
printf("拷贝赋值运算符中tmpaobj的地址为%p\n", &tmpaobj);
return *this;
}
~A() { cout << "A::~A()析构函数被执行" << endl; }
};
A operator+(const A& obj1, const A& obj2) {
A tmpobj;
printf("tmpobj的地址为%p\n", &tmpobj);
// printf("---------------------\n");
//.....
return tmpobj; // 编译器产生临时对象,把tmpobj对象的内容通过调用拷贝构造函数 把
// tmpobj的内容拷贝构造给这个临时对象,然后返回的是这个临时对象;
}
void func() {
A myobj1;
printf("myobj1的地址为%p\n", &myobj1);
A myobj2;
printf("myobj2的地址为%p\n", &myobj2);
A resultobj;
resultobj = myobj1 + myobj2; // 拷贝赋值运算符
// A resultobj = myobj1 + myobj2; // 拷贝构造函数
printf("resultobj的地址为%p\n", &resultobj);
return;
}
} // namespace _nmsp2
namespace _nmsp3 {
// 直接运算产生的临时性对象
// 1)临时对象被摧毁
// 2)临时对象因绑定到引用而被保留
class A {
public:
A() { cout << "A::A()构造函数被执行" << endl; }
A(const A& tmpobj) {
cout << "A::A()拷贝构造函数被执行" << endl;
m_i = tmpobj.m_i;
}
A& operator=(const A& tmpaobj) {
cout << "A::operator()拷贝赋值运算符被执行" << endl;
return *this;
}
~A() { cout << "A::~A()析构函数被执行" << endl; }
int m_i;
};
A operator+(const A& obj1, const A& obj2) {
A tmpobj;
tmpobj.m_i = obj1.m_i + obj2.m_i;
return tmpobj; // 编译器产生临时对象,把tmpobj对象的内容通过调用拷贝构造函数 把
// tmpobj的内容拷贝构造给这个临时对象,然后返回的是这个临时对象;
}
void func() {
//A myobj1;
//myobj1.m_i = 1;
//A myobj2;
//myobj2.m_i = 2;
// A resultobj = myobj1 +myobj2;
// myobj1 + myobj2; // 产生了临时对象,然后该临时对象立即被析构;
// 临时对象的析构是整行语句的最后一步,这样就能保证printf打印出来一个有效值;
// printf("(myobj1 + myobj2).m_i = %d\n", (myobj1 + myobj2).m_i);
// 编译器要往必要的地方,帮助我们插入代码,来产生临时对象供编译器完成我们程序开发者代码要实现的意图;
// A tmpobja1 = (myobj1 + myobj1);
//if ((myobj1 + myobj1).m_i > 1 || (myobj1 + myobj2).m_i > 5) {
// int abc;
// abc = 4;
//}
// 这一行有问题,因为临时对象过了这行就被摧毁;
// const char *p = (string("123") + string("456")).c_str();
// string aaa = (string("123") + string("456"));
// const char *q = aaa.c_str(); // 这个应该OK
// printf("p = %s\n", p);
// printf("q = %s\n", q);
const string& aaa = string("123") + string("456");
printf("aaa = %s\n", aaa.c_str());
return;
}
} // namespace _nmsp3
int main() {
//_nmsp1::func();
//_nmsp2::func();
_nmsp3::func();
return 1;
}
四十二、模板及其实例化详细分析
演示代码:
// mytemplate.h
#ifndef __MYTEMPLATE__
#define __MYTEMPLATE__
#include <iostream>
using namespace std;
template <class T>
class ATPL {
public:
T m_i, m_j;
ATPL(T tmpi = 0, T tmpj = 0) { // 构造函数
m_i = tmpi;
m_j = tmpj;
}
enum ECURRSTATUS {
E_CS_Busy,
E_CS_Free,
};
static int m_sti;
static T m_sti2;
void func1() const { cout << "func1()" << endl; }
void func2() const { cout << "func2()" << endl; }
virtual void virfunc1() { cout << "virfunc1()" << endl; }
virtual void virfunc2() { cout << "virfunc2()" << endl; }
};
template <class T>
int ATPL<T>::m_sti = 10;
template <class T>
T ATPL<T>::m_sti2 = 11;
#endif
// myfunc.cpp
#include "pch.h"
#include <iostream>
#include "mytemplate.h"
using namespace std;
int ftest() {
ATPL<int> myobj;
myobj.m_sti2 = 21;
cout << myobj.m_sti2 << endl;
return 15;
}
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
#include "mytemplate.h"
using namespace std;
// template class ATPL<int>; // 显式实例化语法,这种语法会把模板类的所有内容都实例化出来;
template void ATPL<int>::func2() const; // 显式实例化单独的成员函数,并没有实例化出类ATPL<int>本身;
// namespace _nmsp1 {
// // 函数模板
// // 编译器编译时,根据针对funcadd()的调用来确定T的类型。
// // 如果我们并没有针对funcadd()的调用代码,那么编译器不会为我们产生任何和funcadd有关的代码,就好像函数模板funcadd()从来没存在过一样;
//
//
// template <class T> // 针对T的类型推断,是编译器在编译的时候,根据针对funcadd()的调用来确定的
// T funcadd(const T&a, const T& b) {
// T addhe = a + b;
// return addhe;
// }
// void func() {
// cout << funcadd(12, 14) << endl;
//
//
// return;
// }
//}
// namespace _nmsp2 {
// // 类模板的实例化分析
// // 如果程序代码中没有用到ATPL,那么编译器对ATPL类模板视而不见,就好像从来没存在过一样;
// // 1)模板中的枚举类型,和类模板本身关系不大
// // 2)类模板中的静态成员变量
// // 3)类模板的实例化
// // 4)成员函数的实例化
//
// template<class T>
// class ATPL {
// public:
// enum ECURRSTATUS {
// E_CS_Busy,
// E_CS_Free
// };
// public:
// T m_i, m_j;
// ATPL(T tmpi = 0, T tmpj = 0) { // 构造函数
// m_i = tmpi;
// m_j = tmpj;
// }
//
// void func1() const { cout << "func1()" << endl; } // 在不被调用的情况下不会被实例化
// void func2() const { cout << "func2()" << endl; }
//
//
// static int m_sti;
// static T m_sti2;
//
// };
// template <class T> int ATPL<T>::m_sti = 10;
// template <class T> T ATPL<T>::m_sti2 = 15; // 这的值15是否合法取决于将来T的类型
//
// void func() {
// ATPL<int>::ECURRSTATUS myenum; // 注意这里加<int>
// myenum = ATPL<int>::E_CS_Busy;
// ATPL<int>::ECURRSTATUS myenum2;
// myenum2 = ATPL<int>::E_CS_Free;
//
// ATPL<double>::ECURRSTATUS myenum3;
// myenum3 = ATPL<double>::E_CS_Free;
//
//
// //ATPL<int>::m_sti = 18;
// //ATPL<float>::m_sti = 21;
//
// //cout << ATPL<int>::m_sti << endl;
// //cout << ATPL<float>::m_sti << endl;
//
// //ATPL<int>::m_sti2 = 18;
// //ATPL<float>::m_sti2 = 21;
// //cout << ATPL<int>::m_sti2 << endl;
// //cout << ATPL<float>::m_sti2 << endl;
//
// //ATPL<int> *pobj = NULL; // 此种写法不会实例化出具体的类
// const ATPL<int> &yobj = 0; // 实例化了类模板
// yobj.func1();
//
// //ATPL<int> tmpobj(0);
// //const ATPL<int> &yobj = tmpobj;
//
//
//
//
// }
//}
namespace _nmsp3 {
// 多个源文件中使用类模板
// 在多个obj文件中可能产生多个重复的类模板对应的具体的实例化类,但链接的时候只会保留一个ATPL<int>类的实体,其余的会被忽略掉;
// 1)虚函数的实例化
// 虚函数即使没有被调用,但也会被实例化出来,为什么?因为有虚函数,编译器就会产生虚函数表。虚函数表里是各个虚函数的地址,既然需要各个虚函数的地址,那么必须要实例化每个虚函数出来。
// 2)显式实例化
void myfunc() {
//ATPL<int> myobj;
//myobj.m_sti2 = 18;
//cout << myobj.m_sti2 << endl;
}
void func() {}
} // namespace _nmsp3
int main() {
//_nmsp1::func();
//_nmsp2::func();
_nmsp3::func();
return 1;
}
四十三、邪门歪道
代码演示:
#include "pch.h"
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
namespace _nmsp1 {
// 不能被继承的类B
// c++11 final关键字;
// 友元函数 + 虚继承 = 不能被继承的类
// 副作用:友元,破坏封装性;虚继承,消耗比较大;
class A {
private:
A() {
int abc;
abc = 12;
}
friend class B; // 我们希望类B能够成为一个不能被继承的类
};
// class B :public A
class B : virtual public A {
public:
int m_b;
};
// 希望类B不能被继承
class C : public B {
public:
int m_c;
};
void func() {
// A objba; // 构造函数私有,所以生成A对象不可以
B myobjb;
myobjb.m_b = 15;
//C myobjc;
//myobjc.m_c = 20;
return;
}
} // namespace _nmsp1
namespace _nmsp2 {
// 类外调用私有虚成员函数
class A {
private:
virtual void virfunc() { myfunc(); }
void myfunc() { cout << "myfunc() run---------" << endl; }
};
void func() {
A obj;
//(reinterpret_cast<void(*)()>(**(int **)(&obj)))(); // 调用虚函数virfunc();
//无非就是走虚函数表来调用虚函数
long *pvptr = (long *)&obj; // 把对象obj的地址拿到
long *vptr = (long *)(*pvptr); // 拿到虚函数表指针
typedef void (*Func)(void); // 定义个函数指针类型
Func f = (Func)vptr[0]; // 给虚函数表指针f赋值为第一个虚函数地址virfunc
f(); // 调用虚函数
}
} // namespace _nmsp2
int main() {
//_nmsp1::func();
_nmsp2::func();
return 1;
}