构造函数与析构函数
一、构造函数
1. 知识点介绍
构造函数是一种特殊的函数,主要用来在创建对象时初始化对象,即为对象的成员变量赋初始值(在构造函数中对类中的数据成员赋值)。
2. 构造函数的定义
{
public:
student(){} // 无参(默认)构造函数
student(int a){} // 有参(带参)构造函数
};
3. 构造函数的调用时机
在创建一个新的对象的时候会自动调用对应的构造函数:
调用无参构造函数:
student *p = new student; // 堆上创建对象,调用无参构造
调用带参构造函数:
student *p = new student(1); // 堆上创建对象,调用带参构造
注意:必须要有对应的构造函数才能创建对象。如果使用student stu1(1);定义对象,但没有对应的构造函数,那么创建对象会失败。
4. 构造函数的特点
- 构造函数名和类名相同
- 构造函数没有返回值类型,也没有返回值
- 构造函数可以重载,需要满足函数重载的条件
- 如果一个类中没有显式的给出构造函数,系统会自动地给出一个缺省的(隐式)什么都不干的构造函数
- 如果类中有多个构造函数,那么通常会有不同的参数列表和函数体(构造函数可以重载)
- 如果用户提供了无参(有参)构造,那么系统就不再提供默认构造
- 类中如果只有有参构造,没有无参构造,那么就不能调用默认构造的方式初始化对象,想用这种方式初始化对象那么就需要提供无参构造
5. 拷贝构造函数(一种特殊的构造函数)
拷贝构造是一种特殊的构造函数,用自身这种类型来构造自身。
student stu2 = stu1; // 这里会调用拷贝构造函数
stu2 = stu1; // 这样不会调用拷贝构造函数(这是赋值操作)
拷贝构造函数的定义
-
如果没有自定义拷贝构造函数,系统会提供一个隐式的拷贝构造函数,其操作可以理解为用’=’号逐个将对象中的数据成员赋值给新创建的对象。
-
自定义拷贝构造函数的格式:
类名(const 类名& 引用名){}这里的const不加也可以,但加上可以防止被拷贝的对象被修改。在函数体里面通常的做法是逐一拷贝传进来的对象的成员,赋值给新的对象。
示例:
class Student
{
int id;
public:
Student(const Student& stu)
{
this->id = stu.id; // 把传进来对象的id赋值给当前调用对象的id
}
};
拷贝构造函数的调用场景
-
用同种类的对象初始化另一个对象(定义对象时的初始化,不是赋值):
Student stu1;
Student stu2 = stu1; // 隐式调用拷贝构造
Student stu3(stu2); // 显式调用拷贝构造
Student *pStu = new Student(stu3); // 动态创建时调用 -
函数传参时,函数的形参是类对象:
void fun(Student stu){}
fun(stu1); // 函数调用传参时调用拷贝构造 -
函数返回值类型是对象,在函数调用结束返回对象时:
Student fun1(Student stu){return stu;}
// 这里会调用两次拷贝构造:函数传参一次,函数返回时一次
拷贝构造的问题
1. 浅拷贝
系统提供的默认拷贝构造函数会将类中数据成员用’=’号逐个进行赋值,这称为浅拷贝。这种方式在使用指针时可能会出现问题:两个指针指向同一个内存,当其中一个对象释放该内存后,另一个对象的指针会变成野指针。
2. 深拷贝
当浅拷贝出现问题时需要使用深拷贝。深拷贝通过自定义拷贝构造函数,为新对象的指针申请独立内存来存储内容,而不是让两个对象的指针指向同一个内存。
深拷贝示例:
{
char *name;
public:
// 普通构造函数
Student(char *name)
{
this->name = new char[strlen(name)+1]; // 加1是为了保存’\0′
strcpy(this->name, name);
}
// 自定义拷贝构造函数(深拷贝)
Student(const Student& stu)
{
// 为新对象的指针申请内存
this->name = new char[strlen(stu.name)+1];
strcpy(this->name, stu.name);
// 如果有其他成员,也需要相应赋值
}
};
什么时候需要自己写拷贝构造函数?
当类中有动态分配的内存时,必须自定义拷贝构造函数实现深拷贝。
二、析构函数
1. 知识点介绍
析构函数和构造函数一样,也是一种特殊的函数,主要作用是在对象结束生命周期时,由系统自动调用,做一些清理工作(如释放对象中动态分配的内存),避免内存泄漏。
2. 析构函数的定义
{
public:
~Student() // 析构函数
{
// 清理工作,如释放动态分配的内存
delete[] name;
}
};
3. 析构函数的调用时机
- 析构函数可以通过对象主动调用,但析构函数必须是公有属性
- 在对象死亡时(生命周期结束),系统会自动调用其析构函数
- 栈上的对象在超出作用域时自动调用析构函数
- 堆上的对象在使用delete释放时调用析构函数
4. 析构函数的特点
- 函数名与类名相同,在前面加上一个~(波浪号)
- 没有返回值类型和返回值,也没有参数
- 如果类中没有自定义析构函数,系统会提供一个隐式的什么都不做的析构函数
- 一个类只有一个析构函数,不能重载
- 析构函数的主要工作是对对象做清理工作(如释放内存)
- 主动调用析构函数并不会释放对象本身
三、this指针
1. 知识点介绍
- this指针是系统自动生成且隐藏的,我们看不到其定义但可以使用
- this指针并不是对象本身的一部分,它的作用域在类的内部
- 当类的普通函数访问类的成员时,this指针总是指向调用该函数的对象
- 可以理解为this指针是一个指向当前调用者对象的指针
2. this指针的使用
- 必须在类中使用,在类外无法使用
this->成员名;或(*this).成员名;表示访问当前对象的某个成员return this;表示返回当前对象的地址(指针)return *this;表示返回当前对象本身
3. this指针在代码中的表现
当类中函数的形参和类的成员变量同名时,this指针可以区分它们:
{
int sum; // 成员变量
public:
void fun(int sum) // 形参sum与成员变量同名
{
this->sum = sum; // this->sum表示成员变量,sum表示形参
}
};
如果直接写sum = sum;,则两个sum都表示形参,无法正确给成员变量赋值。