众所周知,类是C++的核心,而对于类的生存周期的长短是受到很多因素影响,在这里我从以下几个方面来分析:
为了体现各种情况下类的生存周期,我们写一个测试用例来直观地感受。
class CGoods//实现三种构造方式 一个拷贝构造函数 一个赋值运算符重载
{
public:
CGoods(char* name, int amount, float price)
{
std::cout << this << " :CGoods::CGoods(char*,int,float)" << std::endl;
mname = new char[strlen(name) + 1];
strcpy(mname, name);
mamount = amount;
mprice = price;
}
CGoods()
{
std::cout << this << " :CGoods::CGoods()" << std::endl;
mname = new char[1];
}
CGoods(float price)
{
std::cout << this << " :CGoods::CGoods(float)" << std::endl;
mname = new char[1];
mprice = price;
}
CGoods(const CGoods& rhs)
{
std::cout << this << " :CGoods::CGoods(const CGoods& )" << std::endl;
mname = new char[strlen(rhs.mname) + 1];
strcpy(mname, rhs.mname);
mamount = rhs.mamount;
mprice = rhs.mprice;
}
/*
const
1.防止实参被修改
2.接收隐式生成的临时量
*/
CGoods& operator=(const CGoods& rhs)
{
std::cout << this << " :CGoods::operator=(const CGoods&) <<==" << &rhs << std::endl;
if (this != &rhs)
{
delete[] mname;
mname = new char[strlen(rhs.mname) + 1];
strcpy(mname, rhs.mname);
mamount = rhs.mamount;
mprice = rhs.mprice;
}
return *this;
}
~CGoods()
{
std::cout << this << " :CGoods::~CGoods()" << std::endl;
delete[] mname;
mname = NULL;
}
private:
char* mname;
int mamount;
float mprice;
};
/*
隐式
系统推演对象类型 生成对象
显式
指出对象类型 生成对象
临时量
内置类型生成的临时量 ==> 常量
自定义类型生成的临时量 ==> 变量
隐式生成的临时量 ==> 常量
临时对象的优化
条件
临时对象的生成是为了生成新对象
方式
以生成临时对象的方式生成新对象
引用能提升临时对象的生存周期
把临时对象提升成和引用变量相同的生存周期
*/
CGoods good1("good1", 1, 1.1);//.data
int main()
{
CGoods good3;//前两次是全局变量,第三次调用CGood();
CGoods good4(17.5f);//CGood(float)
CGoods good5("good5", 5, 5.5);//
static CGoods good6("good6", 6, 6.6);
good3 = 17.5f;
good3 = CGoods(17.5f);
good3 = (CGoods)("good3", 3, 3.3);
CGoods good7 = 17.5f;//隐式调用构造
CGoods good8 = CGoods("good8", 8, 8.8);//显示调用构造
CGoods* pgood9 = new CGoods("good9", 9, 9.9);//堆区
CGoods* pgood10 = new CGoods[2];//堆区
CGoods* pgood11 = &CGoods("good11", 11, 11.11);//临时量
CGoods& rgood12 = CGoods("good12", 12, 12.12);
const CGoods& rgood13 = 17.5f;
//const int & a = 10;
delete[] pgood10;
delete pgood9;
return 0;
}
CGoods good2("good2", 2, 2.2);//.data
在这个测试用例中,尽可能的将类在各种情况下出现的可能性都进行实现
运行后的结果如下:
如结果所示,内存位置在010段的三个类属于.data段生成的,他们的地址和堆栈所处的地址位置有很大的不同,这个涉及到C语言的4G虚拟内存对于.data段,.text段,.bss段和堆区栈区的划分,这个在之后的博文中会有提及。目前我们先看.data段生成的类的生存周期。
可以看出010段地址的三个类虽然生成时不在一起,但是在析构时,也就是在生命周期结束时是一起的,而且是在文件结束时才会进行析构,至于为什么不是一起生成的,是因为这三个类中有两个类是全局类,声明在main函数外,虽然good2是在main函数的后面,但是程序中编译连接时会进行地址重定位,保证运行时都是按main函数开始的,在这个同时,所有的全局变量都会提前在主函数前编译好,可以说在main函数开始之前,全局类就已经开始构造了,并且因为没有加static声明,这两个类的链接属性是外部的,有被其他文件使用的风险,这个需要注意。
另一个则是在main函数里生成的,因为有static声明,编译器将他的生存周期提升到和全局类相同,所以在这句语句执行的那个时刻,将其放到了.data段,也就是我这里的010段内存。关于static的相关介绍会在以后的博文中提到,会持续更新本文。
总结:类处于.data段时,他的生存周期就是从声明的那一刻起,直到文件结束时才会结束,生存周期是最长的一种。
栈区中的数据是由操作系统来进行管理的,反映到源文件中就是main函数中的声明的数据,如局部变量,局部类等,并且栈区数据符合栈的FILO的特点,在测试用例中,good3至good8(除good6),pgood11,rgood12和rgood13都是属于栈区数据,但这些类所调用的函数却有些不同。下面逐语句进行分析:
good3最先在main函数声明时调用无参数的构造函数,在main函数结束时最后析构退出。
good4在main函数声明时调用一个float参数的构造函数,在main函数结束时在good3之前析构退出。
good5在main函数声明时调用char*,int和float参数的构造函数,在main函数结束时在good4之前析构退出。
good3 = 17.5f时,系统进行隐式调用,系统推演出对象类型来生成对象,也就是说,系统此时会在CGood类中寻找是否存在一个float参数的构造函数,如果查找到,则会以生成临时对象的方式生成一个对象(这句话有些绕口,前者是动作,后者是结果),此时这个临时类调用了一个float参数,此时等号左右都是CGood类型,则调用了赋值运算符重载函数,即006FFBA4段,进行赋值操作,由于是以生成临时对象的方式生成的对象,这个对象的生存周期是在一条语句结束的时候(; ?等)结束,即在进行赋值操作结束后本语句结束后进行析构,在运行结果上也是如此。
good3 = CGoods(17.5f);时,属于显式调用,程序员指出对象类型,因为等号左右都是CGood类型,所以调用了赋值运算符重载函数,由系统以生成临时对象的方式生成一个对象,方式和上条隐式调用相同。
good3 = (CGoods)(“good3”, 3, 3.3);时,这是一个陷阱,很多程序员会忽略掉最基础的一个运算符——逗号运算符,逗号运算符的作用是在其并列的几个数据中寻找最后一个作为最后的赋值对象,所以这句等同于good3 = CGoods(3.3);其中临时对象的生命周期也是语句结束时结束。
CGoods good7 = 17.5f;
CGoods good8 = CGoods(“good8”, 8, 8.8);这两句则是标准的隐式调用和显示调用,实际上并没有出现临时对象,可以认为构造时是给左值进行了构造,由结果也可以看出,他的生命周期实在main函数结束时结束。
CGoods* pgood11 = &CGoods(“good11”, 11, 11.11);此语句声明一个CGood类型的指针,存储的是CGood类型的某一个地址,可以看出右值并没有出现对象名,此时编译器的内部优化起作用,生成一个临时量来对声明的指针进行赋值操作,在赋值之后,本临时量的生命周期结束,被系统回收,其所在的地址也回到了系统手中,此时pgood11所指的内存将“不复存在”,也就是说pgood11是没有任何作用的,且有出错的风险,但是因为语句的正确性,我们的编译器放了他一马,我们在编写程序的时候一定要避免这种错误。
CGoods& rgood12 = CGoods(“good12”, 12, 12.12);我们知道,引用本身是不会生成新的数据,引用在底层实现也是以指针的形式实现的,同时引用也可以提升被引用的对象或者变量的生命周期至引用变量的生命周期。
这句语句在编译器认为就是:声明一个引用,他所指向的内存地址就是一个没有名字的CGood类型的对象地址,编译器的内部优化使得右值产生一个临时对象,同时因为引用的作用,这个临时量的生命周期被提升到main函数结束,通过最后的运行结果来看,也确实如此。
const CGoods& rgood13 = 17.5f;此语句需要一些推敲。const的作用是告诉编译器我所指向的数据不会在进行修改,一般用来防止调用一些不能修改的值时修改的误操作,const的一些操作在之后的博文会有提到,这里我们需要知道,此时的const作用是告诉编译器这个rgood13引用的不是17.5f(即使你想这样引用也会是错误的,因为引用不可以引用一个常亮),而是引用一个指向17.5f的临时变量,这样的操作都属于编译器的内部优化,他会推测你的想法,虽然大部分情况编译器总是猜不出你的意思(正确的编程风格才是硬道理)同时也因为有const的存在,让常引用变成合理的存在,又根据引用可以提升生命周期的属性,这个临时量的生命周期被提升到main函数结束,通过最后的运行结果来看,也确实如此。
CGoods* pgood9 = new CGoods(“good9”, 9, 9.9);//堆区
CGoods* pgood10 = new CGoods[2];//堆区
这两句由于是new申请的方式生成的对象,所以他们是在堆区生成,由程序员进行管理,希望释放时需要程序员显式的delete,他们的生命周期在new时进行构造,在delete时进行析构结束生命周期,**需要注意的是,当申请的是对象数组时,并非一次性全部构造好,系统会对每个对象逐个进行构造。**析构时也是逐个进行析构,保证每个对象都可以初始化,每个对象都能被清理掉。
在程序编写的时候,如果成员变量涉及到指针,指向堆内存的情况,那么在清理内存的时候就不能只使用系统提供的默认析构函数,需要将指向堆内存的数据手动清理,以免出现内存泄漏的问题、
耐着性子看完博文的各位辛苦了,想必大家也发现了,虽然我提到了类在三种情况下的生存周期,但是临时对象的生命周期却是最难分析的,他的生命周期会根据前置的操作符改变而改变,这是一个不确定的因素,也是一个切入点,很多编程的方法都可以根据需要来调整对象的生命周期,这对于各位程序员也是一个机遇吧。
隐式调用
系统推演对象类型 生成对象
显式调用
指出对象类型 生成对象
临时量
内置类型生成的临时量 ==> 常量
自定义类型生成的临时量 ==> 变量
隐式生成的临时量 ==> 常量 (必要时需要添加const)
临时对象的优化
条件
临时对象的生成是为了生成新对象
方式
以生成临时对象的方式生成新对象
引用和指针都能提升临时对象的生存周期
把临时对象提升成和引用变量相同的生存周期