C++类的构造函数和析构函数
1.构造函数和析构函数是什么
1.1构造函数
通常一个类,其内部包含有变量和函数,当我们想要使用类的时候,总是会不得不面临这样一个问题,需要对类进行初始化,否则内部这些变量就会是随机值,导致程序出现异常。
为此,我们需要在使用类之前对它进行初始化,C++就提供了这样一类特殊的函数——构造函数,它在创建类的时候会被自动调用,对类进行初始化。
1.2析构函数
析构函数和构造函数类似,它会在类对象被销毁时自动调用,主要负责一些清理工作。通常在函数结束后,在当前函数内生成的那些类就会被调用。
如果构造函数没有使用new来创建堆内存对象的话,一般是不需要析构函数做任何处理的,否则需要在析构函数内使用delete来释放这些堆内存,以避免出现内存泄漏的风险。
2.构造函数
2.1.基本形式
举个例子,测试类当中有三个变量a、b、c需要初始化,可以这样写。
class test { public: test(int a, int b, int c = 0) : m_a(a),m_b(b),m_c(c) { m_a = 100; //return; 错误,构造函数不允许return } void display() { cout << m_a << " " << m_b << " " << m_c << endl; } private: int m_a; // m_a = 1,再次赋值,m_a = 100 int m_b; // m_b = 2 int m_c; // m_c没有输入,默认 = 0 }; int main(){ test t(1,2): t.display(); return 0; }
基本格式是 classname() : {} 或 classname() {}
由以下几个特征:
1.没有返回类型,同时也不能有返回值;
2.括号()中间是函数的输入变量,可以在后面赋值,这样输入就会在没有输入的情况下赋默认值(这一条适用于所有函数,但是必须保证默认赋值变量的顺序是从后往前);
3.冒号:后面的内容是初始化列表,使用A(B)的方式,将B赋值给A,用逗号,隔开,但最后一个变量不能有逗号,这段内容写在()和{}中间。需要注意的是,初始化列表不是必须的,可以完全不使用初始化列表,全部都在{}内赋值也可以。
4.花括号{}中间的内容,就像正常的函数实现一样,在初始化时,会执行一次内部的程序。
因此,最后输出的结果如注释写的那样:m_a = 100, m_b = 2, m_c = 0。
2.2.特殊构造函数
构造函数是运行重载的,下面这些构造函数本质和其它构造函数没有任何区别,只是这些用法比较多,从而有了一些特殊的名称而已。
默认构造函数:
当我们没有写任何构造函数的时候,系统也会隐含存在一个构造函数,只不过它的输入变量、初始化列表,以及函数内容都是空的,不进行任何初始化操作。如果我们写一个构造函数不带任何输入,那么就会覆盖掉默认的构造函数,使用人工编写的构造函数。
注意:默认构造函数是公有的
class test { public: //默认构造函数,就算不写出来,程序也会默认附带这样一个构造函数 test() {} private: int m_a; }; int main(){ test t; return 0; }
拷贝构造函数:
以相同的类来作为当前类的唯一输入的构造函数。这里有两个关键点,相同的类和唯一输入,说大白话就是,将同样的类A拷贝到B。至于如何进行初始化,那就要针对不同的类来考虑了,通常都需要手动对类的内部变量进行一一拷贝赋值。
class test { public: test(const test& other) //将other中的所有值赋值给当前类 : m_a(other.m_a) m_b(other.m_b) {} private: int m_a; int m_b; }; int main(){ test t1; test t2(t1); return 0; }
转换构造函数:
和拷贝构造函数类似,只不过是将另外一个类,转化为当前类,那么情况就更为复杂,需要依据使用场景和类的具体内容,进行初始化赋值。
例如这个例子,需要用test_a给test_b赋值,但是又无法访问a的私有成员,就需要使用get_a函数,且我们不希望更改test_a的内容加上了const,但是赋值时又需要去掉const,从而使用const_cast修饰。
class test_a { public: test_a(int a) : m_a(a) {} const int get_a() { return m_a; } private: int m_a; }; class test_b { test_b(const test_a& t) : m_b(const_cast<test_a&>(t).get_a()) {} private: int m_b; }; int main(){ test_a ta(1); test_b tb(ta); return 0; }
3.析构函数
class test { public: test() { } ~test() { } };
基本格式是 ~classname() {}
由以下几个特征:
1.没有返回类型,同时也不能有返回值;
2.类名前面需要加一个~,代表析构函数名;
3.花括号{}中间的内容,就像正常的函数实现一样,在类被销毁时,会执行一次内部的程序。
注意:通常情况下,我们不会主动调用析构函数,都是让系统自动去调用。
析构函数使用场景一般像下面这样,用于释放构造时占用的堆内存:
class test{ public: test() : m_p(new int) {} ~test() { delete m_p; } private: int* m_p; }
4.explicit显式构造
在初始化赋值时,我们通常还习惯这样一种用法:
int a = 2.5;
这里实际上包含有两个步骤:
1.将(2.5)由类型float隐式转化为类型int
2.再将转化后的int类型值,赋值给a
4.1.隐式构造
例如下面这个例子,类的隐式构造当中,就会先调用Implicit(int num = 0)函数,将1转化为Implicit类,然后赋值给Implicit a。
class Implicit { public: Implicit(int num = 0) :m_num(num) {} ~Implicit() {}; Implicit& operator=(Implicit& other) { m_num = other.getNum(); } const int getNum() { return m_num; } private: int m_num; }; int main(){ Implicit ip = 1; //允许操作 return 0; }
4.2.显式构造
而在构造函数前加上explicit关键字,代表只允许进行显式转换,因此自动调用Implicit(int num = 0)将1转化为Implicit类的操作就会变成非法的,所以我们得想想其他办法。
例如,显式将1转化为Implicit,或者创建中间变量等。
class Explicit { public: explicit Explicit(int num = 0) :m_num(num) {} ~Explicit() {} ; Explicit& operator=(Explicit& other) { m_num = other.getNum(); } const int getNum() { return m_num; } private: int m_num; }; int main(){ //Explicit ep = 1; //不允许操作 Explicit ep = Explicit(1); //允许操作 Explicit ep = Explicit::Explicit(1); //允许操作 Explicit temp(1); Explicit ep = temp; //允许操作 return 0; }
5.私有构造函数
私有构造函数有以下几个特性:
5.1.私有构造函数不能在外部直接调用
通常利用这种特性,可以屏蔽某些不想要开放的构造函数。例如,禁止拷贝类的时候,可以私有化拷贝构造函数。
5.2.重载时,始终可以调用那些公有的析构函数
如果重载了多个构造函数,只要存在公有的构造函数,就可以直接调用那些公有的构造函数来构造这个类,而不会出现无法构造的情况。
5.3.可以在类的内部调用构造函数的方式来进行私有化构造
例如单例模式当中,内部含有一个指向自身类型的静态指针,通过公有的函数来调用构造函数,并将类赋值给这个指针并返回,来实现类的构造和获取。
class PrivateConstruct { public: ~PrivateConstruct() { cout << "destroy PrivateConstruct" << endl; m_pInstance = nullptr; }; static PrivateConstruct* GetInstance() { if(m_pInstance == nullptr) m_pInstance = new PrivateConstruct(); return m_pInstance; } PrivateConstruct(int num) { }; //对应2 private: PrivateConstruct() { cout << "creat PrivateConstruct" << endl; }; PrivateConstruct(const PrivateConstruct& other) { }; //对应1 static PrivateConstruct* m_pInstance; }; PrivateConstruct* PrivateConstruct::m_pInstance = nullptr; //对应3 int main(){ //PrivateConstruct pc1; //不允许外部构造,PrivateConstruct()为私有 PrivateConstruct pc2(1); //允许外部构造,PrivateConstruct(int)为公有 //PrivateConstruct pc3(pc2); //不允许外部构造,PrivateConstruct(PrivateConstruct&)为私有 PrivateConstruct* p_pc = PrivateConstruct::GetInstance(); return 0; }
原则上,只有析构函数是公有的,两种方式都可以析构,并释放内存。
p_pc->~PrivateConstruct(); delete p_pc;
6.私有析构函数
私有析构函数有以下几个特性:
6.1.私有析构函数会导致类无法直接被构造
6.2.在构造函数为公有的情况下,可以通过new创建堆对象的方式构造
可以通过这种方式,来创建类的指针对象。
6.3.成功创建类指针对象之后,也无法通过调用析构函数或delete的方式释放
因为此时的析构函数为私有,外部的delete方法也相当于从外部直接调用析构函数。
6.4.析构函数本身无法在类的内部被直接调用
该特性适用于所有的析构函数。
6.5.可以通过在类内部delete this的方式,间接调用析构函数
class PrivateDestruct { public: PrivateDestruct() { cout << "creat PrivateDestruct" << endl; } void Destroy() { //~PrivateDestruct(); //error,对应4 delete this; //ok,对应5 } private: ~PrivateDestruct() { cout << "destroy PrivateDestruct" << endl; } }; int main(){ //PrivateDestruct pd1; //error,对应1 PrivateDestruct* p_pd2 = new PrivateDestruct; //ok,对应2 //p_pd2->~PrivateConstruct(); //error,对应3 //delete p_pd2; //error,对应3 p_pd2->Destroy(); //ok,对应5 return 0; }
7.总结
不会用不建议瞎用,那些花里胡哨的用法,平时99%的时间都用不到,把常规玩法玩明白,需要的时候翻翻这页资料,找几个demo就行了。