构造函数和析构函数

目录

 一、构造函数:

二、析构函数

 三、默认的无参构造

四、拷贝构造函数 

五、浅拷贝与深拷贝

六、构造函数的初始化列表


 一、构造函数:

  • 作用:初始化对象的状态,防止未初始化的成员变量导致错误。

  •  注: 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) { ... }
//          ↑        ↑
//      初始化列表   初始化操作
  1. _hour:类Time的私有成员变量(int _hour;)。
  2. hour:构造函数的参数,类型为int
  3. _hour(hour):将参数hour的值赋给成员变量_hour,相当于成员变量的初始化

关键特性

  1. 初始化 vs 赋值

    • 初始化列表在对象内存分配后立即执行,直接初始化成员变量。
    • 若在构造函数体中赋值(如_hour = hour;),则会先默认初始化_hour,再赋值,效率较低。
  2. 必须使用初始化列表的场景

    • 常量成员const int _hour;
    • 引用成员int& _hour;
    • 没有默认构造函数的类成员(如另一个类Date的对象)
  3. 执行顺序

    • 成员变量按其在类中声明的顺序初始化,而非初始化列表中的顺序。
    • 例如:若类中先声明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对象时,编译器会按以下步骤处理:

  1. 内存分配Person对象分配内存,包括birthday成员。
  2. 成员初始化尝试初始化birthday。由于Date没有默认构造函数,编译器无法完成此步骤,编译失败。
  3. 构造函数体执行即使birthday = Date(2000);在语法上正确,也无法执行,因为birthday根本无法被创建。

正确写法:使用初始化列表

初始化列表允许在对象创建时直接调用带参构造函数,避免默认构造的尝试:

class Person {
private:
    Date birthday;  // 成员变量,类型为Date(无默认构造函数)

public:
    // 正确:在初始化列表中显式调用Date的带参构造函数
    Person() : birthday(2000) {
        // 构造函数体此时可以正常执行,因为birthday已经被正确初始化
    }
};

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值