二、构造 & 析构函数
2.1 构造和析构
2.1.1 功能
构造函数功能
构造函数在类实例化对象时用于完成对象成员的初始化,通常包括以下操作:
1. 为成员变量分配内存空间
2. 初始化成员变量
3. 执行类似打开文件、分配资源等额外操作
析构函数功能
主要作用在于对象 销毁前 系统自动调用,执行一些清理工作。
2.1.2 格式
构造函数
类名( ) { }
- 构造函数与类名同名。
- 构造函数没有返回值,也不能写
void
。- 构造函数通常设置为
public
权限,以便外部能够调用。- 构造函数可以有参数,因此==可以发生重载==
- 序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
- 如果不写构造函数 , 编译器会默认提供一个==构造函数==
析构函数
~类名( ){ }
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
2.1.3 调用时机
构造函数:在类实例化对象时 自动调用,不需要也不允许手动调用。
析构函数:在 对象销毁 或者 程序结束前 调用,不需要手动调用。
注意 :
1、析构函数可以手动调用 但是一般不这么干
MyClass obj;
obj.~MyClass(); // 手动调用析构函数
2、析构函数在堆区 和 栈区的释放方式是不同的
栈区中 程序结束前 系统自动调用
堆区中 程序结束不会调用 需要用户手动调用 delete 才能调用堆区空间
2.1.4 构造和析构调用顺序
2.1.1.1 对于堆区对象:
先new那个对象 就先构造那个对象
先delete 那个对象 就先析构那个对象
所以一般不考虑 堆区的构造函数
2.1.1.1 对于栈区对象:
构造函数的调用顺序: 按顺序调用
析构函数的调用顺序:按逆序调用 // 先进 后出 先构造 后析构(栈的顺序)
2.1.5 栈区与堆区的对象创建
栈区:
当在栈区创建对象时,构造函数会自动调用。
类名 对象名(构造函数的实参表); // 调用构造函数
堆区:
类名 *指针名; // 这一步不会调用构造函数
指针名 = new 类名(); // 这个时候才调用构造函数
在堆区创建对象时,需要使用 new
运算符来分配内存并调用构造函数:
需要特别注意的是,使用 malloc
分配内存时,不会调用构造函数。
malloc
仅分配内存空间,但不会初始化对象。
2.1.6 示例代码一(测试)
#include <iostream>
#include <string>
using namespace std;
class stu
{
private:
/* data */
public:
stu()
{
cout << "我是无参构造" << endl;
}
stu(string str)
{
cout << "我是有参构造" << "传入了:" << str << endl;
}
// 析构函数
~stu()
{
cout << "我是析构" << endl;
}
};
int main(int argc, char const *argv[])
{
stu s1;
s1.~stu();
cout << "hello" << endl;
return 0;
}
运行结果如下:
2.1.7 示例代码二 (调用时机)
#include <iostream>
#include <string>
using namespace std;
int num = 1 ;
class stu
{
private:
int val;
public:
stu()
{
val = num++;
cout << "我是:" << val << "构建了" << endl;
}
stu(int stu_val)
{
val = stu_val;
cout << "我是:" << val << "构建了" << endl;
}
~stu()
{
cout << "我是:" << val << "析构了" << endl;
}
};
int main(int argc, char const *argv[])
{
stu s1(1);
stu s2(2);
stu s3(3);
stu s4(4);
stu * s_p_1 = new stu;
stu * s_p_2 = new stu;
stu * s_p_3 = new stu;
stu * s_p_4 = new stu;
delete s_p_1;
delete s_p_2;
delete s_p_3;
delete s_p_4;
return 0;
}
运行结果如下:
2.2 构造函数的调用方式
对于类中 构造函数的调用方式有三种 :括号法、显示法、隐式转换法
2.2.1 示例代码(构造调用)
#include <iostream>
#include <string>
using namespace std;
class stu
{
private:
string name;
int id;
public:
stu(){}
// 通过括号法
stu(string m_name, int m_id)
{
name = m_name;
id = m_id;
}
public:
void show()
{
cout << "姓名" << name << endl;
cout << "学号" << id << endl;
}
};
int main(int argc, char const *argv[])
{
// 括号调用
stu s1("唐三" , 10);
s1.show();
// 括号调用 s2 = s1
stu s2(s1);
s2.show();
// 匿名对象
stu s3 = stu("萧薰儿" , 11);
s3.show();
return 0;
}
运行结果如下:
2.3 初始化列表
在定义构造函数的时候,可以使用冒号的方式 引出 构造函数的初始化列表
2.3.1 格式
类名(构造函数实参表):成员1(初值1),成员2(初值2)
{
构造函数函数体;
}
2.3.2 注意事项
必须使用初始化列表的场景
场景1:当形参列表 和 成员变量 名字重名的情况下
可以使用初始化表 或者 this指针 解决
场景2:当 成员变量 中 有引用类型的 成员时
这个时候 必须使用 初始化列表
类名(type & num):val(num);
场景3: 当类中有const 修饰的成员变量时
场景4: 当类中有成员子对象时
类中 的 成员子对象 的有参构造
当类中有成员子对象时,需要在构造函数的初始化中调用成员子对象的构造函数
并传参完成对成员子对象初始化,如果没有调用成员子对象的构造函数
默认会调用成员子对象的无参构造函数 但是如果 成员子对象 没有无参构造 则会报错
2.3.3 示例代码 (初始化列表)
#include <iostream>
#include <string>
using namespace std;
/*
使用初始化列表的条件
1、成员变量与构造函数实参重名
2、成员变量中有const
3、成员变量中有 &
4、有成员子类 并且需要调用成员子类的 有参构造
*/
// 成员子类
class stu1
{
private:
/* data */
public:
// 无参构造
stu1(/* args */)
{
cout << "无参构造" << endl;
}
// 有参构造
stu1(int val)
{
cout << "有参构造 val = " << val << endl;
}
};
class stu2
{
private:
// 成员变量
string & name;
const int id;
// 成员子类
stu1 stu;
public:
// 有参
stu2(string name , int id)
: name(name) , id(id) , stu(10)
{
}
};
int main(int argc, char const *argv[])
{
string str;
stu2 s1(str , 10);
return 0;
}
运行结果如下:
2.4 拷贝构造函数
C++ 会为类生成一个默认拷贝构造函数和赋值操作符。这两个默认实现的行为都是逐位复制(即浅拷贝),它只是简单地复制每个成员的值,包括指针的地址,而不分配新的内存空间。
所以我们需要 自己写一个 深拷贝 构造函数来替换掉程序中的 浅拷贝构造函数。
2.4.1 格式
类名(const 类名 & other)
2.4.2 拷贝构造函数启动规则
类名 对象1
启动规则如下:
类名 对象2(对象1)
类名 对象2 = 对象1
类名 * 对象2 = new 类名(对象1)
2.4.3 浅拷贝
浅拷贝只是简单地将对象的值(包括指针的地址)赋值给新的对象。对于指针成员,浅拷贝只是复制指针本身,而不会为指针指向的动态内存分配新的空间。这意味着两个对象中的指针将指向同一块内存。
2.4.3.1 浅拷贝的风险
如果两个对象都指向同一块内存,当其中一个对象修改这块内存时,另一个对象也会受到影响。此外,当对象被销毁时,如果两个对象都尝试释放同一块内存,就会导致 双重释放(double deletion)错误,程序可能崩溃。
2.4.4 深拷贝
是为指针成员分配新的内存空间,并复制原对象中的数据到新的内存。这保证了每个对象都有自己独立的一份数据,不会共享同一块内存,因此也不会引发双重释放的问题
2.4.5 深拷贝和浅拷贝的区别
浅拷贝:
如果类中没有显性的定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数,这个默认的拷贝构造函数,只完成成员之间的简单赋值。如果类中没有指针成员,只用这个默认的拷贝构造函数,是没有问题的。
深拷贝:
如果类中有指针成员,并且使用浅拷贝,指针成员之间也是只做了简单的赋值。相当于两个对象的指针成员指向的是同一块内存空间,调用析构函数的时候,就会出现 double free 的问题。此时, 需要在类中显性的定义拷贝构造函数,并且给新对象的指针成员分配空间,再将旧对象的指针成员指向的空间里的值拷贝一份过来。
2.4.6 示例代码(浅拷贝)
#include <iostream>
#include <string>
using namespace std;
class stu
{
private:
int val ;
public:
int * ptr;
public:
stu():val(10)
{
}
stu(int val) : val(val)
{
}
public:
void show()
{
cout << val << endl;
cout << *ptr << endl;
}
};
int main(int argc, char const *argv[])
{
stu s1(10);
s1.ptr = new int(80);
stu s2 = s1;
*s1.ptr =90;
s2.show();
stu s3(s1);
s3.show();
return 0;
}
运行结果如下:
2.4.7 示例代码(深拷贝)
#include <iostream>
#include <string>
using namespace std;
class stu
{
private:
int val;
public:
int *ptr;
public:
stu() : val(10)
{
}
stu(int val, int ptr_val) : val(val), ptr(new int(ptr_val))
{
}
stu(const stu &other)
{
cout << "调用拷贝构造函数" << endl;
if (NULL == other.ptr)
{
ptr = other.ptr;
}
else
{
ptr = new int(*other.ptr);
}
val = other.val;
}
~stu()
{
if (NULL != ptr)
{
cout << "调用析构 释放空间完成" << endl;
delete ptr;
}
}
public:
void show()
{
cout << val << endl;
cout << *ptr << endl;
}
};
int main(int argc, char const *argv[])
{
stu s1(10 , 20);
stu s2(s1);
*s1.ptr = 90;
s2.show();
return 0;
}
运行结果如下:
标签:调用,对象,成员,C++,面向对象,析构,拷贝,类名,构造函数 From: https://blog.csdn.net/wakkkaaa/article/details/145191265