C++面向对象的封装特性:构造函数和析构函数
1.一个基础的C++面向对象编程案例
一般用一个
.cpp
和一个.hpp
文件配对来描述一个class
,class
的名字和文件名相同的。
①在头文件person.hpp:
中声明类:
#ifndef PERSON_H
#define PERSON_H
#include<iostream>
#include<string>
namespace xx{
//声明类
class person
{
//访问权限
public:
//属性
std::string name;
int age;
bool male;
//方法
void eat(void);
void work(void);
void sleep(void);
private:
protected:
};
};
#endif
②在源文件person.cpp
中实现类(构造函数,析构函数以及各种必要的方法等):
#include"person.hpp"
void xx::person::eat(void){
std::cout<<"eating..."<<std::endl;
}
void xx::person::work(void){
std::cout<<"working..."<<std::endl;
}
void xx::person::sleep(void){
std::cout<<"sleeping..."<<std::endl;
}
③在主程序main.cpp
中调用之前实现的类和方法:
#include"person.hpp"
//using namespace xx;
int main(int argc,char**argv)
{
xx::person tom;
tom.name="Tom";
tom.age=22;
tom.male=true;
//tom的一天
tom.eat();
tom.work();
tom.eat();
tom.work();
tom.sleep();
return 0;
}
2.构造函数和析构函数引入
2.1 什么是构造函数和析构函数
构造函数(constructor
)字面意思就是用来构造对象的函数;析构函数(destructor
)字面意思是用来析构对象的函数。构造函数和析构函数可以理解为语言自带的一种回调函数(写完之后不用手动调用,满足一定条件后会自动执行)。
在上面第一部分main.cpp
的代码中,首先创建了一个对象,然后依次给对象的属性赋值。这是比较山寨的做法,因为和结构体比较像了。典型的C++
做法是使用new
关键字来创建对象并且使用构造函数来完成初始化操作。
当使用new
关键字来产生对象时构造函数会自动被调用,一般用于初始化对象的属性以及分配对象内部需要的动态内存。而对象消亡时析构函数会自动被调用,一般用于回收析构函数中分配的动态内存,避免内存泄露。
2.2 构造函数和析构函数一般用法
如果在实现一个类的时候不写构造函数和析构函数,C++
会自动提供默认的构造函数和析构函数,它们都是空的,没有返回值没有参数(对应于第一部分中的代码就啥都没写,但是不影响运行)。
当然也可以显式提供默认构造和析构函数:对于构造函数来说,它不需要返回值类型,并且可以带参数也可以不带参数,函数名同类名(如person()
);析构函数也不需要返回值类型,同时不带参数,函数名为类名前加~
符号(如~person()
)。
构造函数可以实现多种方式初始化对象,因此可以重载,也就可以根据实际创建对象时传递的参数来决定具体调用哪个构造函数,但是析构函数不需要重载。
3.构造函数和析构函数实例
3.1 实例
person.hpp:
#ifndef PERSON_H
#define PERSON_H
#include<iostream>
#include<string>
namespace xx{
//声明类
class person
{
public:
//属性
std::string name;
int age;
bool male;
person();//默认构造函数
person(std::string name,int age,bool male);//自定义构造函数
~person();//默认析构函数
};
};
#endif
person.cpp:
#include"person.hpp"
//C++提供的默认构造函数和析构函数都是空的,没有返回类型也没有参数
xx::person::person(){};
xx::person::~person(){};
//自定义构造函数
xx::person::person(std::string name,int age,bool male)
{
this->name=name;
this->age=age;
this->male=male;
}
main.cpp:
#include"person.hpp"
using namespace xx;
int main(int argc,char**argv)
{
//通过自定义构造函数在自由存储区上创建并初始化对象
person* tom = new person("tom",15,true);
std::cout<<tom->name<<";"<<tom->age<<";"<<tom->male<<std::endl;
delete tom;
//通过自定义构造函数在栈上创建并初始化对象
person jack("jack",18,true);
return 0;
}
关于默认构造函数的一些细节:
- 1.当有了自定义构造函数后,编译器不会自动创建默认构造函数,需要手动去写默认构造函数。因为默认构造函数一般是空的,所以一般直接在类内写出来,类似于内联函数,比如
person(){};
。- 2.当有了自定义构造函数后,在栈上分配对象时想使用默认构造函数,语法是
person tom;
,而不是person tom();
,后者会被认为是一个普通函数(函数名为tom
)的调用。
3.2 C++的成员初始化列表
C++
的成员初始化列表用于带参构造函数(不能用于其他普通函数)中,用来给类内的属性传参赋值,语法是成员初始化列表和构造函数之间用冒号间隔,多个列表项之间用逗号间隔。初始化列表可以替代构造函数内的赋值语句,达到同样效果:
xx::person::person(std::string Name,int Age,bool Male):name(Name),age(Age),male(Male)
{}
//等价于:
xx::person::person(std::string Name,int Age,bool Male)
{
this->name=Name;
this->age=Age;
this->male=Male;
}
person* tom=new person("tom",15,true);
3.3 构造函数使用参数默认值
在一个类声明的时候,可以给构造函数的形参赋值默认值,实际调用时如果不传参那就使用默认值。
方法实现时形参可以不写默认值,但是实际是按照声明时的默认值规则的。有默认值情况,要注意实际调用不能有重载歧义,否则编译不能通过。部分默认值的时候要把有默认值的放后面,没有默认值的放在前面。
所有参数都带默认值的构造函数,1个可以代替多个构造函数。
3.4 为什么需要构造函数和析构函数
在C
语言中的结构体没有构造函数概念,所以结构体中需要用到动态内存时必须在定义struct
变量后再次单独申请和释放(因为malloc()
函数没办法在结构体变量创建的时候自动调用,C
语言没有这样的机制。),而这些操作都需要程序员手工完成。
对于类的编写者来说,构造函数可以看作是将来类的实例化创建的对象的初始化式,构造函数可以为对象完成动态内存申请,同时在析构函数中再释放,形成动态内存的完整使用循环,类的使用者可以不必在意这些过程。C++
类的构造和析构特性,是C++
支持面向对象编程的一大语言特性。
4.在构造和析构函数中使用动态内存
4.1 析构函数的使用
析构函数在对象对销毁时自动调用,一般有2种情况:
- 1.用
new
创建的对象,必须要用delete
显式析构才会去调用析构函数。 - 2.分配在自由存储区/栈上的对象,当自由存储区/栈释放时自动调用析构函数。
可以用实验证明以上两点,为了方便调试,可以在函数中加上打印信息:
xx::person::person()
{
std::cout<<"默认构造函数"<<std::endl;
};
xx::person::person(std::string name,int age,bool male)
{
std::cout<<"自定义构造函数"<<std::endl;
this->name=name;
this->age=age;
this->male=male;
}
xx::person::~person()
{
std::cout<<"默认析构函数"<<std::endl;
};
对于第一种,用new
创建的对象,必须用delete
显式析构对象才会去调用析构函数:
int main(int argc,char**argv)
{
person* tom = new person("tom",15,true);
delete tom;
return 0;
}
输出:
自定义构造函数
默认析构函数
当去掉delete
时:
int main(int argc,char**argv)
{
person* tom = new person("tom",15,true);
return 0;
}
输出:
自定义构造函数
对于第二种情况:
int main(int argc,char**argv)
{
person tom("tom",15,true);
return 0;
}
输出:
自定义构造函数
默认析构函数
上述的两种方式也是创建对象的两种方式,一般用不到动态内存的时候析构函数都是空的(上面添加的cout
只是为了看到函数是否执行了)。
4.2 在类中使用动态内存
当我们需要大块内存(比如连续几百个字节的buffer
)且需要按需灵活的申请和释放,用栈怕内存爆满,用全局又怕浪费和死板时,这个情况下就适合用动态内存。
在person
中增加一个int* p_int
和一个int* p_array
指针,分别用于指向一个int
类型元素和int
类型数组的内存空间。则需要在构造函数中分配动态内存,在析构函数中回收动态内存。
person.hpp
:
#ifndef PERSON_H
#define PERSON_H
#include<iostream>
#include<string>
namespace xx{
//声明类
class person
{
private:
//属性
std::string name;
int age;
bool male;
int* p_int;//只是分配了指针p本身的4字节内存,并没有分配p指向的空间内存
int* p_array;//整型变量升级为整型数组
person(){};//默认构造函数
person(std::string name,int age,bool male);//自定义构造函数
~person();//默认析构函数
person(const person&pn);
void print(void);
};
};
#endif
person.cpp
:
#include"person.hpp"
xx::person::person(std::string name,int age,bool male):name(name),age(age),male(male)
{
//在构造函数中对class内需要分配动态内存的指针进行动态分配
this->p_int = new int(55);//分配了一个值为55的int,具体的值也可以通过构造函数参数来传递
this->p_array = new int[55];//分配了55个元素的int数组,具体的大小也可以通过构造函数参数来传递
}
xx::person::~person()
{
std::cout<<"默认析构函数"<<std::endl;
delete this->p_int;
delete[] this->p_array;
}
实际中C++
常用的动态内存往往是容器vector
那些。
对象分配到栈上也不是一个好方法,实际上C++
中大量使用动态内存,即把对象创建在自由存储区上。
5. 拷贝构造函数
参考 【C++内存管理:拷贝构造函数、深拷贝与浅拷贝】。
6.总结
构造函数主要有2个作用:
- 1.初始化对象的属性。
- 2.类内需要使用动态内存的时候分配动态内存。
析构函数在没有用到动态内存的时候并无多大作用,在用到动态内存的时候则可以用来释放动态内存。