目录
一、前言
作为一个不断进步的程序员,在我们会使用一项技能后,不能只知其然不知其所以然,针对非科班或非系统成才的野生程序员,更应该在熟练使用后进行深入剖析和理解。要秉承看懂,会用,理解,复现的步骤进行学习,在使用这项技能后别人问到为什么要这么使用或者面试问到深层次的问题时不至于无所应对。有种“我知道怎么用,但是我不知道为什么”的无力感。
二、什么是类
1、类的组成
一个类由类名,类成员函数,类成员变量以及其他(静态变量,友元等)组成,以下面的 Demo 类为例来介绍。
class Demo
{
public:
Demo()
{
n1 = 0;
n2 = 0;
n3 = 0;
}
~Demo(){}
void fun1()
{
fun2();
}
inline void infun()
{
std::cout<<"n1 ="<<n1<<std::endl
<<"n2 ="<<n2<<std::endl
<<"n3 ="<<n3<<std::endl;
}
protected:
void fun2()
{
fun3();
}
private:
void fun3()
{
n1 = 0;
n2 = 1;
n3 = 2;
}
public:
int n1;
protected:
int n2;
private:
int n3;
};
上面代码段中使用 [关键字] Class 声明了 Demo 类,Demo 类由构造函数,析构函数,类成员函数 fun1、fun2、fun3 ,内联函数 infun,和类成员变量 n1、n2、n3 组成(其中默认)。
其中 [关键字] public, [关键字] private, [关键字] protected 定义了该类类成员函数和类成员变量的访问权限。
- public:公有访问权限,实例化类后可直接调用。
- protected:保护访问权限,仅供类内成员(函数,变量)和友元类型(函数,类)访问。(在类继承与 private 有区别)
- private:私有访问权限,仅供类内成员(函数,变量)和友元类型(函数,类)访问。(在类继承与 protected 有区别)
Demo() 类的构造函数,在类的实例化时调用。例如:Demo test; [语句] 调用时。(可有多种类型的构造函数(含参,不含参数),根据需求进行配置。)
~Demo() 类的析构函数,在类即将消亡时调用。例如:new 出来的 Demo 类在 delete 时,普通实例化的 Demo 类在函数 return 后,该函数栈出栈时。(全类仅限一个)
inline void infun() 类的内联函数,内联函数执行速度比普通类成员函数执行速度要快,但一般适用于代码量较少的情况下使用(十行甚至更少)。
*拓展:拷贝构造函数,移动构造函数,类函数重载,类操作符重载,友元等。
2、类的继承
继承的字面意思是继续前人遗留下来的事业,把前人的知识等接过来。类继承可以理解为把基类(原有类)的结构接过来,在基类的基础上继续开发类的事业,在一个良好的基类上进行派生能够很方便的进行代码维护,且达到了代码复用的目的。继承类型分为:公有继承(public)、保护继承(protected),私有继承(private)。
class Base
{
public:
Base(){}
~Base(){}
void DisplayName()
{
std::cout<<"This is Base"<<std::endl;
}
};
class ChildA:public Base
{
public:
ChildA(){}
~ChildA(){}
void DisplayName()
{
std::cout<<"This is ChildA"<<std::endl;
}
void DisplayValue()
{
std::cout<<"Value is :"<<nBase<<std::endl;
//std::cout<<"Value is :"<<npBase<<std::endl; 父类私有成员不可访问
}
};
class ChildAA:public ChildA
{
public:
ChildAA(){}
~ChildAA(){}
void DisplayName()
{
std::cout<<"This is ChildAA"<<std::endl;
}
void DisplayValue()
{
std::cout<<"Value is :"<<nBase<<std::endl;
//std::cout<<"Value is :"<<npBase<<std::endl; 父类私有成员不可访问
}
};
class ChildB:protected Base
{
public:
ChildB(){}
~ChildB(){}
void DisplayName()
{
std::cout<<"This is ChildB"<<std::endl;
}
void DisplayValue()
{
std::cout<<"Value is :"<<nBase<<std::endl;
//std::cout<<"Value is :"<<npBase<<std::endl; 父类私有成员不可访问
}
};
class ChildBB:protected ChildB
{
public:
ChildBB(){}
~ChildBB(){}
void DisplayName()
{
std::cout<<"This is ChildBB"<<std::endl;
}
void DisplayValue()
{
std::cout<<"Value is :"<<nBase<<std::endl;
//std::cout<<"Value is :"<<npBase<<std::endl; 父类私有成员不可访问
}
};
class ChildC:private Base
{
public:
ChildC(){}
~ChildC(){}
void DisplayName()
{
std::cout<<"This is ChildC"<<std::endl;
}
void DisplayValue()
{
std::cout<<"Value is :"<<nBase<<std::endl;
//std::cout<<"Value is :"<<npBase<<std::endl; 父类私有成员不可访问
}
};
class ChildCC:private ChildC
{
public:
ChildCC(){}
~ChildCC(){}
void DisplayName()
{
std::cout<<"This is ChildCC"<<std::endl;
}
void DisplayValue()
{
//std::cout<<"Value is :"<<nBase<<std::endl; ChildC私有继承了基类Base,基类中除private之外的所有成员变量都转为private类型继承至ChildC中,ChildCC无法访问父类ChildC的私有成员
//std::cout<<"Value is :"<<npBase<<std::endl; 基类Base的私有成员无法访问
}
};
- 公有继承:派生类可见的继承基类的公有成员,保护成员,不可见的继承私有成员(无法访问)。公有继承后,派生类继承的公有成员和保护成员保持基类原有的访问状态。
- 保护继承:派生类可见的继承基类的公有成员,保护成员,不可见的继承私有成员(无法访问)。保护继承后,派生类继承的公有成员和保护成员变为保护的访问状态,只对派生后的子类和友元可见(能够访问)。
- 私有继承: 派生类可见的继承基类的公有成员,保护成员,不可见的继承私有成员(无法调用)。私有继承后,派生类继承的公有成员和保护成员变为私有的访问状态,对派生后的子类不可见(无法访问)。
3、虚的概念
虚函数
虚函数:基类中使用 [关键字] virtual 声明的能够在一个或多个派生类中被重新定义的函数。
示例:virtual [class name]::virtualfun();
纯虚函数:基类中使用 [关键字] virtual 声明的且不进行实现的能够在一个或多个派生类中必须被重新定义的函数。
示例:virtual [class name]::virtualfun() = 0;
虚函数表:使用虚函数的类其自己维护的一份虚函数指针的指针数组,这个数组通常被称为虚函数表。
虚继承
C++这类具有多重继承的语言,实现类的继承时会遇到以下场景:
类D公有继承于类B和类C,类B和类C公有继承于类A,若类D调用类A的属性或方法,在多重继承的前提下会导致二义性。示例如下:
class A:
{
public:
void DisplayClass() { qDebug("this is A"); }
};
class B:public A
{
};
class C:public A
{
};
class D:public B,public C
{
};
int main()
{
D d;
d.DisplayClass();
return 0;
}
错误信息 :
解决方法1:指定由某个继承类调用
class A:
{
public:
void DisplayClass() { qDebug("this is A"); }
};
class B:public A
{
};
class C:public A
{
};
class D:public B,public C
{
};
int main()
{
D d;
d.B::DisplayClass();
return 0;
}
解决方法2:使用虚继承
class A:
{
public:
void DisplayClass() { qDebug("this is A"); }
};
class B:virtual public A
{
};
class C:virtual public A
{
};
class D:public B,public C
{
};
int main()
{
D d;
d.DisplayClass();
return 0;
}
三、类的内存结构
1、类成员变量
测试类如下:
#include <QDebug>
class TEST
{
public:
int a;
char b;
double c;
};
int main()
{
qDebug()<<"size of class TEST is:"<<sizeof(TEST);
}
根据 double c; 所处的位置不同,类占用的空间也不同,遵循按最大数据结构的变量字节补齐的策略:
- 默认排序:首先找到类内最大占用空间的变量 double,占用 8 字节,若变量占用小于 8 字节时应进行字节补齐。例如本类,int 占用 4 字节,后续补齐 4 字节,此时占用 8 字节,紧跟的 char 占用 1 字节,补齐的 4 字节可以容纳,不新增字节,此时占用 8 字节,最后 double 占用 8字节,输出应为 “size of class TEST is:16”。
- char 在第一位,int 在第二位,double 在第三位,char 占用 1 字节,后续补齐 7 字节,此时占用 8 字节,紧跟的 int 占用 4 字节,补齐的 7 字节可以容纳,不新增字节,此时占用 8 字节,最后 double 占用 8 字节,输出应为 “size of class TEST is:16”。
- char 在第一位,double 在第二位,int 在第三位(int 位置与 char 位置互换时也一样),char 占用 1 字节,后续补齐 7 字节,此时占用 8 字节,紧跟的 double 占用 8 字节,补齐的 7 字节不可以容纳,新增 8 字节,此时占用 16 字节,最后 int占用 4 字节,后续补齐 4 字节,此时占用 24 字节,输出应为 “size of class TEST is:24”。
由上可知,在构建一个类时,合理分配补齐的内存能够压缩该类在实际使用中占用的内存。
2、类函数
情况一:不含虚函数,不存在虚继承
测试类如下:
#include <QDebug>
class TEST
{
public:
TEST() {}
~TEST(){}
int getval() {return 1;}
QString getstr() {return "1";}
};
int main()
{
qDebug()<<"size of class TEST is:"<<sizeof(TEST);
}
若一个类中,只有成员函数(包括构造,析构等函数) ,则这个类应占用空间为 0 字节,实际占用空间为 1 字节(由于 0 字节不好在内存去定位它的地址,就规定了占用 0 字节的类实际要象征性的占用 1 个字节)。
情况二:存在虚继承,不存在虚函数
#include <QDebug>
class TEST
{
public:
TEST() {}
~TEST(){}
int getval() {return 1;}
QString getstr() {return "1";}
};
class X:virtual public TEST
{
public:
X() {}
~X() {}
int getval() {return 2;}
};
int main()
{
qDebug()<<"size of class TEST is:"<<sizeof(TEST);
qDebug()<<"size of class X is:"<<sizeof(X);
}
在 Linux64 位操作系统上,输出为:
- “size of class TEST is:1”。
- “size of class X is:8”。
由此可知,虚继承后的子类内部的虚基类指针大小为 8 字节。(由机器平台以及操作系统位数而定,非固定)
情况三:存在虚函数,不存在虚继承
#include <QDebug>
class TEST
{
public:
TEST() {}
~TEST(){}
int getval() {return 1;}
QString getstr() {return "1";}
virtual int setval() = 0;
virtual QString setstr() { return "1";}
};
class X1: public TEST
{
public:
X1() {}
~X1() {}
virtual int setval() override { return 3;}
virtual QString setstr() override {return "3";}
};
int main()
{
qDebug()<<"size of class TEST is:"<<sizeof(TEST);
qDebug()<<"size of class X1 is:"<<sizeof(X1);
}
在 Linux64 位操作系统上,输出为:
- “size of class TEST is:8”。
- “size of class X1 is:8”。
由此可知,虚函数的虚表指针大小为 8 字节,且子类继承后重写的虚函数或纯虚函数共用一个虚函数的虚表指针。(由机器平台以及操作系统位数而定,非固定)
情况四:既存在虚函数,又存在虚继承
#include <QDebug>
class TEST
{
public:
TEST() {}
~TEST(){}
int getval() {return 1;}
QString getstr() {return "1";}
virtual int setval() = 0;
virtual QString setstr() { return "1";}
};
class X:virtual public TEST
{
public:
X() {}
~X() {}
int getval() {return 2;}
virtual int setval() override { return 2;}
};
class X1: public TEST
{
public:
X1() {}
~X1() {}
virtual int setval() override { return 3;}
virtual QString setstr() override {return "3";}
};
int main()
{
qDebug()<<"size of class TEST is:"<<sizeof(TEST);
qDebug()<<"size of class X is:"<<sizeof(X);
qDebug()<<"size of class X1 is:"<<sizeof(X1);
}
在 Linux64 位操作系统上,输出为:
- “size of class TEST is:8”。
- “size of class X is:8”。
- “size of class X1 is:8”。
由此可知,虚函数的虚表指针和虚基类指针共用一个指针,大小为 8 字节。(由机器平台以及操作系统位数而定,非固定)