刻意实验:C++ 对象
Posted Debroon
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了刻意实验:C++ 对象相关的知识,希望对你有一定的参考价值。
对象实验
类对象所占空间
#include<iostream>
class A{};
int main(){
std::cout << "空类对象占用空间:" << sizeof(A);
// 输出结果:1
}
奇怪,类里明明没有变量和方法,完全是空的,应该是 0 才对呀。
因为空类在内存中有起始地址(通过调试可以看到地址),那必然最少是有一个字节。
就像房子,只有你真的有房子,不管房子多小,都是存在的。
#include<iostream>
class A{
public:
int print1(){ cout<<"This is A"<<endl; }
int print2(){ cout<<"This is A"<<endl; }
int print3(){ cout<<"This is A"<<endl; }
};
int main(){
std::cout << "类对象占用空间:" << sizeof(A);
// 输出:1
}
这次类里添加了几个成员函数,发现依然是 1,说明类的成员函数不占用类对象的内存空间。
结论:
- 类里面不管有多少个函数,这个类的对象只占1个字节的内存。(成员变量是占用对象字节的,和结构体一样)
这个字节的内存的内容是什么?是指针吗?指针不是占4个字节吗?
- 当类中类有定义任何变量的时候,类的对象都是1个字节的,当类中没有任何变量的时候,这个类里没有任何真正的成员变量,所以大小应该是0,但0大小不好在内存中定位一个地址,所以,就规定它大小为0的对象要占一字节空间,以便让它拥有一个合法的地址。如果是有子类的,还有考虑到内存对齐的问题的。
对象结构的发展和演化
#include<iostream>
class A{
public:
static int a; // 静态成员变量保存在对象外面
static void b(){} // 类的成员函数、静态成员函数都不占用类对象的内存空间
};
int main(){
std::cout << "类对象占用空间:" << sizeof(A);
// 输出:1
}
为什么还是1呢?其实静态成员变量、静态成员函数都保存在对象外面,所以A还是一个空壳。
#include<iostream>
class A{
public:
virtual void b(){}
};
int main(){
std::cout << "类对象占用空间:" << sizeof(A);
// 输出:8
}
再看一个。
#include<iostream>
class A{
public:
virtual void a(){}
virtual void b(){}
};
int main(){
std::cout << "类对象占用空间:" << sizeof(A);
// 输出:8
}
发现无论是1个虚函数,还是2个虚函数,对象大小都是8,32位系统指针大小为4字节,64位系统指针大小为8字节。
这因为虚函数和其他函数不同,虚函数存放在虚函数表里。
系统往对象中添加了一个指针vptr,对象所占空间就是指针所占的内存空间。
this指针的调整
#include<iostream>
using namespace std;
class A{
public:
int a;
A(){ printf("A() -> this 地址 = %p\\n", this); }
void funA(){ printf("funA() -> this 地址 = %p\\n", this); }
};
class B{
public:
int b;
B(){ printf("B() -> this 地址 = %p\\n", this); }
void funB(){ printf("funB() -> this 地址 = %p\\n", this); }
};
class C: public A, public B{
public:
int c;
C(){ printf("C() -> this 地址 = %p\\n", this); }
void funC(){ printf("funC() -> this 地址 = %p\\n", this); }
};
class D: public B, public A{
public:
int d;
D(){ printf("D() -> this 地址 = %p\\n", this); }
void funD(){ printf("funD() -> this 地址 = %p\\n", this); }
};
int main(){
cout << sizeof(A) << endl << sizeof(B) << endl;
cout << sizeof(C) << endl << sizeof(D) << endl;
cout << endl;
C c;
c.funA();
c.funB();
c.funC();
cout << endl;
D d;
d.funB();
d.funA();
d.funD();
}
A、B不用说了,关键是C、D。
C、D的区别,仅在于继承A、B顺序不同:
- C:先A后B
- D:先B后A
输出结果:
A:4
B:4
C:12
D:12
A() -> this 地址 = 0x7ffeee09a7a0
B() -> this 地址 = 0x7ffeee09a7a4
C() -> this 地址 = 0x7ffeee09a7a0
funA() -> this 地址 = 0x7ffeee09a7a0
funB() -> this 地址 = 0x7ffeee09a7a4
funC() -> this 地址 = 0x7ffeee09a7a0
B() -> this 地址 = 0x7ffeee09a790
A() -> this 地址 = 0x7ffeee09a794
D() -> this 地址 = 0x7ffeee09a790
funB() -> this 地址 = 0x7ffeee09a790
funA() -> this 地址 = 0x7ffeee09a794
funD() -> this 地址 = 0x7ffeee09a790
发现 C 和 A 的首地址相同,D 和 B 的首地址相同,子类与第一个继承的父类相同。如图所示(地址变了,但都是差4个字节):
子类对象(C),它包含父类子对象(A、B)。
如果子类只从一个父类继承,那这个子类对象的地址和父类子对象的地址相同。
如果子类对象同时继承多个父类:
- 第一个父类子对象的开始地址和子类对象的开始地址相同 — A 和 C 相同的首地址相同。
- 后续的父类子对象(B)的开始地址,和子类对象的开始地址相差地址多少呢?那就得把前边那些父类子对象所占用的内存空间干掉。
this也在调整,您调用哪个子类的成员函数,this指针就会被编译器自动调整到对应子类的起始地址。
在上面的程式中也可发现,如我们在B类中调用this指针,this就会调整到B中,我们在A类中调用this指针,this指针就会调整。
分析obj(目标文件),构造函数语义
书上说,如果我们没有定义任何构造函数,那编译器会隐式的自动定义一个默认构造函数。
每个.cpp文件都会生成一个目标文件(.obj、.o),我们就在目标文件里查看这一过程。
#include<iostream>
using namespace std;
class MATX{};
int main(){
MATX A;
}
现在生成目标文件:
- Linux/Mac:g++ -c file_name,会看到目标文件(.o)
- Windows:在VS中生成解决方案,鼠标右击标题,打开所在文件,点击Debug文件夹,会看到目标文件(.obj,记得显示文件扩展名)
程序转化语义
程序转化语义:编译器对我们写的代码进行转化,变成编译器偏好的代码。
- 程序员视角看代码(现代编译器)
- 编译器视角看代码(原始编译器)
#include <iostream>
using namespace std;
class X{
public:
int i;
X(){
i = 0;
cout << "构造函数被调用" << endl;
}
X(const X & tmp_x){ // 拷贝构造函数
i = tmp_x.i;
cout << "拷贝函数被调用" << endl;
}
};
int main(){
X a;
a.i = 15;
X b = a;
X c(a);
X d = (a);
}
输出结果:
构造函数被调用
拷贝函数被调用
拷贝函数被调用
拷贝函数被调用
定义时初始化对象:
// 程序员视角
X d = (a); // 定义一个对象,且调用构造函数
// 编译器视角
X d; // 定义一个对象,为对象分配内存,但没有调用构造函数
d.X::X(a); // 调用对象的拷贝构造函数
#include <iostream>
using namespace std;
class X{
public:
int i;
X(){
i = 0;
cout << "构造函数被调用" << endl;
}
X(const X & tmp_x){ // 拷贝构造函数
i = tmp_x.i;
cout << "拷贝函数被调用" << endl;
}
~X(){
cout << "析构函数被调用" << endl;
}
};
void func(X tmp_x){};
int main(){
X a;
func(a);
}
输出结果:
构造函数被调用
拷贝函数被调用
析构函数被调用
析构函数被调用
参数初始化:
void func(X tmp_x){}; // 程序员角度func
void func(X &tmp_x){}; // 编译器角度func,加了引用
// 程序员视角
X a;
func(a);
// 老编译器视角
X tmp_obj; // 产生一个临时变量
tmp_obj.X::X(a); // 调用拷贝构造函数
func(tmp_obj); // 用临时变量调用func
tmp_obj.X::~X(); // 调用析构
返回值初始化:
#include <iostream>
using namespace std;
class X{
public:
int i;
X(){
i = 0;
cout << "构造函数被调用" << endl;
}
X(const X & tmp_x){ // 拷贝构造函数
i = tmp_x.i;
cout << "拷贝函数被调用" << endl;
}
~X(){
cout << "析构函数被调用" << endl;
}
};
X func(){
X a;
return a;
}
int main(){
X f = func();
}
输出结果:
构造函数被调用
析构函数被调用
// 程序员视角
X func(){ // 返回 X 对象
X a;
return a;
}
X f = func();
// 编译器视角
void func(X &extra){ // 返回 void 类型
X x0; // 不会调用构造函数
extra.X::X(x0); // 调用拷贝构造函数
return;
}
X f; // 不会调用构造函数
func(f);
加一个成员函数。
#include <iostream>
using namespace std;
class X{
public:
int i;
X(){
i = 0;
cout << "构造函数被调用" << endl;
}
X(const X & tmp_x){ // 拷贝构造函数
i = tmp_x.i;
cout << "拷贝函数被调用" << endl;
}
~X(){
cout << "析构函数被调用" << endl;
}
void functest(){
cout << "functest()被调用" << endl;
}
};
X func(){
X a;
return a;
}
int main(){
// 程序员视角
func().functest();
// 编译器视角
X f;
(func(f), f).functest(); // 逗号表达式:先计算表达式1,再计算表达式2,整个逗号表达式的值是表达式
}
输出结果:
构造函数被调用
functest()被调用
析构函数被调用
// 程序员视角
X(*pf)();
pf = func()
pf().functest();
// 程序员视角
X f;
X(*pf)(X &);
pf = func();
pf(f);
f.functest();
我们看了编译器的视角下,定义初始化对象、参数初始化、返回值初始化等。
程序的优化
程序的优化,从开发者层面和编译器层面。
#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;
// 方案二:
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(){
// 开发者层面的优化(开发者视角)
CTempValue ts1(10, 20);
Double(ts1);
}
那我们要怎么从编译器优化?
要看不要求如 GCC。
成员初始化列表
Student::Student(char *name, int age, float score): _name(name){
_age = age;
_score = score;
}
何时必须用成员初始化列表
- 如果这个成员是个引用
class Student {
char *_name;
int _age;
float &_score; // 成员是引用
}
Student(float &val):_score(val) { // 成员是引用,必须初始化列表
_age = 0;
_score = 0; // 使用初始化列表后,也可以赋值,但有点重复
}
- 如果是个const类型成员
class Student {
char *_name;
int _age;
const float _score; // 成员是const
};
Student(float &val):_score(val) { // 成员是引用,必须初始化列表
_age = 0;
}
- 如果你这个类是继承一个基类,并且基类中有构造函数,这个构造函数里边还有参数。
class A{
int a1;
int a2;
A(int tmpa1, int tmpa2){} // 带参数的构造函数
};
class B: public A{
int b;
B(int tmpvale):A(tmpvalue, tmpvalue){}
}
- 如果你的成员变量类型是某个类类型,而这个类的构造函数带参数时;
class A{
int a1;
int a2;
A(int tmpa1, int tmpa2){} // 带参数的构造函数
};
class B{
int b;
A a; // 类类型
B(int tmpvale):a(tmpvalue, tmpvalue){}
}
使用初始化列表的优势
除了必须用初始化列表的场合,我们用初始化列表还有什么其他目的? 有,就是提高程序运行效率。
对于类类型成员变量放到初始化列表中能够比较明显的看到效率的提升。
但是如果是个简单类型的成员变量 比如 int m_test,其实放在初始化列表或者放在函数体里效率差别不大。
class X
{
public:
int m_i;
X(int value = 0) :m_i(value) //类型转换构造函数
{
printf("this = %p", this);
cout << "X(int)构造函数被调用" << endl;
}
X(const X 实验1