目录
一、构造函数:
-
作用:初始化对象的状态,防止未初始化的成员变量导致错误。
-
注: 1.构造函数的名称必须与类名相同
2.并且没有返回值类型!!!
3.构造函数可以有参数,也可以没有参数。
-
构造函数分为三类:
1.带参数的构造函数
2.默认的构造函数(如果没有提供,编译器会自动生成一个随机数)
3.无参的构造函数(如果没有提供,编译器会自动生成一个随机数)
class Test
{
public:
// 带参数的构造函数
Test(int a, int b) {
m_a = a;
m_b = b;
cout << "带参数的构造函数被调用" << endl;
}
// 默认的构造函数(如果没有提供,编译器会自动生成一个随机数)
Test(int a)
{
m_a = a;
m_b = 0; // 默认值
cout << "默认构造函数被调用" << endl;
}
//无参的构造函数
Test()
{
m_a = 0;
m_b = 0; // 默认值
cout << "无参构造函数被调用" << endl;
}
private:
int m_a;
int m_b;
};
int main()
{
Test t1(10, 20);
Test t2(30); // 只传一个参数,m_b会被设置为0
Test t3; // 不传参数,m_a和m_b都被设置为0
return 0;
}
注:调用无参构造函数时不需要写(),和Test t3() 这种声明函数的方式不同!!!
// 创建对象时,调用无参构造函数 Test t3; // 不传参数,m_a和m_b都被设置为0 //注意和Test t3(); 这种声明函数的方式不同!!!
二、析构函数
-
作用:在对象生命周期结束时自动调用,通常用于清理工作(编译器帮助我自动触发)
-
调用顺序:跟构造相反,谁先构造的,谁后析构
注: 1.析构函数的名称与类名相同,但前面加一个波浪号(~)
2. 没有返回值类型。
3.没有参数
// 析构函数(不管写不写,都会触发析构)
~Test() {
//每个Test对象在生命周期结束时都会调用这个函数
}
当在类中定义指针,并为其动态开辟空间使其指向这片空间时,需手动释放这块空间
// 构造函数
Test(int a, int b) {
m_a = a;
m_b = b;
name = (char*)malloc(100); // 动态分配内存
strcpy(name, "Zhang3");
}
// 析构函数
~Test() {
if (name != NULL) {
free(name); // 释放动态分配的内存
name = nullptr; // 防止悬空指针
cout << "释放了动态分配的内存" << endl;
}
}
分析各个变量的内存分布及是否需要释放情况:
1. m_a, m_b, name指针:
三者均分布在栈区
• m_a、m_b 是普通的 int 类型成员变量,随着类对象的销毁自动释放,无需手动释放。
• name 是一个指针变量,它的生命周期和对象一致,也会自动释放,无需手动释放。
2. name指针指向的空间:
分布在堆区
• name 指向的堆区内容(即 malloc 分配的空间)必须在不用时释放,否则会造成内存泄漏
3. "Zhang3":
分布在常量区
在常量区的 "Zhang3" 不需要释放,也不能释放。
原因:
• 字符串常量(如 "Zhang3")由编译器自动分配和管理,存放在程序的常量区(只读数据段)。
• 你不能对它调用 free() 或 delete,否则会导致程序崩溃或未定义行为。
三、默认的无参构造
当你没有写任何显示的构造函数(显示的无参构造、显示的有参构造、显示的拷贝构造),编译器会自动生成一个默认构造函数,如下:
Test() { }
此时在main函数里直接定义对象t1时,编译器并不报错
但仔细想想确实不会报错,因为就相当于之前最开始学的用无参的方式定义对象t1
所以只要你用无参的方式定义对象,编译器就会自动调用该类的默认构造函数(前提是你没有写任何显示的构造函数)
int main() { Test t1; }
注:当你写了显示拷贝构造后,默认的构造函数便会消失,此时不能用无参方式(如 Test t1;)定义对象,否则会编译错误
四、拷贝构造函数
函数原型通常是这样的:类名(const 类名 &);
使用场景:构造函数是对象初始化的时候调用
当你自己构造一个拷贝构造函数时,用你自己的拷贝构造函数,未定义时,则使用下面系统提供的一个默认的拷贝构造函数。
Test(const Test &another) { m_x = another.m_x; m_y = another.m_y; }
在main函数里调用拷贝构造函数有两种等价的方法:(以t1和t2为例)
1.按照函数原型严格套用
int main() { Test t1(100,200); Test t2(t1); return 0; }
2.采用赋值号
注意和以下写法的区别:int main() { Test t1(100,200); Test t2 = t1; //依然是初始化t2的时候调用t3构造函数,依然是调用t3的拷贝构造函数 return 0; }
int main() { Test t1(100,200); Test t2; t2 = t1; return 0; }
此时调用的不是t3拷贝构造函数,而是调用t3的赋值操作符函数,其函数原型如下:
void operator=(const Test &another) { m_x = another.m_x; m_y = another.m_y; }
五、浅拷贝与深拷贝
浅拷贝(默认行为)
如果类包含指针成员,默认的赋值运算符(=
)和拷贝构造函数会执行浅拷贝,即只复制指针值,而不复制指向的对象
class Teacher {
public:
Teacher(int id, char *name)
{
//赋值id
m_id = id;
//赋值name
int len = strlen(name);
m_name = (char*)malloc(len + 1);// /0的存在
strcpy(m_name, name);
}
~Teacher() {
if(m_name != NULL) // 释放内存
{
free(m_name);
m_name = NULL;
}
}
private:
int m_id;
char *m_name;
};
int main() {
Teacher t1(1,"zhang3");
Teacher t2(t1);//使用默认拷贝构造函数->执行浅拷贝
return 0;
}
t1
和t2
的m_name指针指向同一块内存,由于最后会对类对象进行析构,当先释放t2动态开辟的空间时,t1动态开辟的空间已经被释放了(其实是同一块空间),再对t1进行析构,就会出现双重释放的问题(访问非法空间)
所以需要显示的提供一个拷贝构造函数,来完成深拷贝动作
深拷贝(手动实现)
为避免浅拷贝问题,可以自定义拷贝构造函数和赋值运算符,实现深拷贝:
Teacher(const Teacher &another)
{
m_id = another.m_id;
//深拷贝动作
int len = strlen(another.m_name);
m_name = (char*)malloc(len + 1);
strcpy(m_name, another.m_name);
}
六、构造函数的初始化列表
先看一段代码:
class Time {
public:
Time(int hour) : _hour(hour) {
cout << "Time()" << endl;
}
private:
int _hour;
};
其中_hour(hour)
是构造函数初始化列表的语法,用于在对象创建时初始化成员变量。具体来说:
Time(int hour) : _hour(hour) { ... }
// ↑ ↑
// 初始化列表 初始化操作
_hour
:类Time
的私有成员变量(int _hour;
)。hour
:构造函数的参数,类型为int
。_hour(hour)
:将参数hour
的值赋给成员变量_hour
,相当于成员变量的初始化
关键特性
-
初始化 vs 赋值
- 初始化列表在对象内存分配后立即执行,直接初始化成员变量。
- 若在构造函数体中赋值(如
_hour = hour;
),则会先默认初始化_hour
,再赋值,效率较低。
-
必须使用初始化列表的场景
- 常量成员(
const int _hour;
) - 引用成员(
int& _hour;
) - 没有默认构造函数的类成员(如另一个类
Date
的对象)
- 常量成员(
-
执行顺序
- 成员变量按其在类中声明的顺序初始化,而非初始化列表中的顺序。
- 例如:若类中先声明
int a;
后声明int b;
,则a
总是先被初始化。
其中以没有默认构造函数的类成员(如另一个类Date
的对象)为例介绍相关内容
当类包含一个没有默认构造函数的成员时(如Date date;
),编译器无法在对象创建时自动构造该成员,因为它不知道使用什么参数调用构造函数。此时,必须通过初始化列表显式调用该成员的带参构造函数。
class Date {
public:
Date(int year, int month) : year_(year), month_(month) {} // 带参构造函数(无默认构造函数)
private:
int year_;
int month_;
};
class Person {
public:
/* Person()
{
birthday = Date(2000, 1);// 赋值操作,而非初始化
} // 错误:无法编译!编译器不知道如何构造birthday
*/
Person() : birthday(2000, 1) {} // 初始化列表中调用Date(int, int)
// 正确:使用初始化列表显式调用Date的带参构造函数
private:
Date birthday; // 成员变量,类型为Date(无默认构造函数)
};
为什么赋值操作失败?
在创建一个Person对象时,编译器会按以下步骤处理:
- 内存分配:为
Person
对象分配内存,包括birthday
成员。 - 成员初始化:尝试初始化
birthday
。由于Date
没有默认构造函数,编译器无法完成此步骤,编译失败。 - 构造函数体执行:即使
birthday = Date(2000);
在语法上正确,也无法执行,因为birthday
根本无法被创建。
正确写法:使用初始化列表
初始化列表允许在对象创建时直接调用带参构造函数,避免默认构造的尝试:
class Person {
private:
Date birthday; // 成员变量,类型为Date(无默认构造函数)
public:
// 正确:在初始化列表中显式调用Date的带参构造函数
Person() : birthday(2000) {
// 构造函数体此时可以正常执行,因为birthday已经被正确初始化
}
};