当前位置: 首页 > article >正文

《C++ 构造、拷贝构造与析构函数:对象的诞生、克隆与消逝之旅》

类的6个默认成员函数

  构造函数

是对一个对象实例化时的初始化

例如在C语言中写的堆的时候要初始化StackInit,而c++祖师爷写的构造函数本质上就是自动调用初始化。

 构造函数=默认构造函数+自己写的(符合规定的显示表达式)

注:一般情况下构造函数都需要自己写少数情况下,默认生成就可以用,例如队列。 

写法特点

一、构造函数的函数名与类名相同
二、构造函数无返回值
 这里所说的构造函数无返回值是真的无返回值,而不是说返回值为void。
三、对象实例化时编译器自动调用对应的构造函数
 当你用类创建一个对象时,编译器会自动调用该类的构造函数对新创建的变量进行初始化。
四、构造函数支持重载
 这意味着你可以有多种初始化对象的方式,编译器会根据你所传递的参数去调用对应的构造函数。
五、无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个
 初学C++时,你可能认为只有当我们不写,编译器自动生成的构造函数才被称为默认构造函数。其实并不是这样的,以下3种都叫做默认构造函数:
 1、我们不写,编译器自动生成的构造函数。
 2、我们自己写的无参的构造函数。
 3、我们自己写的全缺省的构造函数。
总而言之,无需传参就可以调用的构造函数就是默认构造函数

 你有没有这种想法,既然在我们不写的情况下,编译器会自动生成一个构造函数,那我们就没有必要自己写构造函数了。这个是❌的。

例如:

#include <iostream>
using namespace std;
class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1; // 编译器将调用自动生成的默认构造函数对d1进行初始化
	d1.Print();
	return 0;
}

结果是乱码:

六、如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成

3.默认构造函数

1.⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在

2.⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会。存在问题

缺省参数(备胎)-CSDN博客有相关解释

3.总结就是不传实参就能调用的就是就叫默认构造函数

3.1⽆参构造函数

#include<iostream>
using namespace std;
class liu
{
public:
// 1.⽆参构造函数
liu()
{
_year = 1;
_month = 1;
_day = 1;
}
private:
int _year;
int _month;
int _day;
};

3.2全缺省构造函数

#include<iostream>
using namespace std;
class liu
{
public:
// 3.全缺省构造函数
liu(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};

3.3编译器默认⽣成的构造函数

#include<iostream>
using namespace std;
class liu
{
liu(){};
int _year;
int _month;
int _day;
};
int main()
{
liu l1;
return 0;
}

  注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,否则编译器⽆法

 这是一个空的函数,即什么都不做。

默认构造函数也不是空的,这里只是简化一下,不作探讨。

 总结:

1、编译器自动生成的构造函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认构造函数。


析构函数 

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。

特点八条

1. 析构函数名是在类名前加上字符~。

 2. ⽆参数⽆返回值。(这⾥跟构造类似,也不需要加void)

 3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。

4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。

5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会 调⽤他的析构函数。

6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数。

7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数。

8. ⼀个局部域的多个对象,C++规定后定义的先析构。 给出每一点的实例分析

1. 析构函数名是在类名前加上字符 ~

析构函数的名称是类名前加上 ~ 符号,用于标识该函数是析构函数。

#include <iostream>
class MyClass {
public:
    // 析构函数
    ~MyClass() {
        std::cout << "MyClass 析构函数被调用" << std::endl;
    }
};

int main() {
    MyClass obj;
    return 0;
}

在上述代码中,~MyClass() 就是 MyClass 类的析构函数。


 2. ⽆参数⽆返回值

析构函数不能有参数,也没有返回值,并且不需要显式指定 void。 


 3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数

#include <iostream>
class MyClass {
    // 未显式定义析构函数,编译器会自动生成默认析构函数
};

int main() {
    MyClass obj;
    return 0;
}

在上述代码中,MyClass 类没有显式定义析构函数,编译器会自动生成一个默认的析构函数。


 4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数

#include <iostream>
class MyClass {
public:
    ~MyClass() {
        std::cout << "MyClass 析构函数被调用" << std::endl;
    }
};

void test() {
    MyClass obj; // obj 的生命周期在 test 函数结束时结束
}

int main() {
    test();
    std::cout << "test 函数调用结束" << std::endl;
    return 0;
}

在 test 函数中,obj 对象的生命周期在 test 函数结束时结束,此时系统会自动调用 MyClass 的析构函数。


5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数 

 如果不显式定义析构函数,编译器生成的默认析构函数对内置类型成员不做处理,对自定义类型成员会调用其析构函数。

#include <iostream>
class InnerClass {
public:
    ~InnerClass() {
        std::cout << "InnerClass 析构函数被调用" << std::endl;
    }
};

class OuterClass {
private:
    int num; // 内置类型成员
    InnerClass inner; // 自定义类型成员
public:
    // 未显式定义析构函数,使用编译器生成的默认析构函数
};

int main() {
    OuterClass obj;
    return 0;
}

 在上述代码中,OuterClass 未显式定义析构函数,编译器生成的默认析构函数对 num 不做处理,对 inner 会调用 InnerClass 的析构函数。


 6.即使显式定义析构函数,自定义类型成员也会自动调用其析构函数。

  

#include <iostream>
class InnerClass {
public:
    ~InnerClass() {
        std::cout << "InnerClass 析构函数被调用" << std::endl;
    }
};

class OuterClass {
private:
    InnerClass inner; // 自定义类型成员
public:
    ~OuterClass() {
        std::cout << "OuterClass 析构函数被调用" << std::endl;
    }
};

int main() {
    OuterClass obj;
    return 0;
}

在上述代码中,OuterClass 显式定义了析构函数,在 OuterClass 的析构函数调用后,会自动调用 InnerClass 的析构函数。


7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数;如果有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏

如果类中没有申请资源,如 Date 类,可使用默认析构函数;如果有资源申请,如 Stack 类,必须自己写析构函数。

#include <iostream>
// Date 类,没有申请资源
class Date {
private:
    int year;
    int month;
    int day;
public:
    // 不需要显式定义析构函数,使用默认析构函数
};

// Stack 类,申请了资源
class Stack {
private:
    int* data;
    int size;
public:
    Stack() {
        data = new int[10];
        size = 0;
    }
    ~Stack() {
        delete[] data; // 释放申请的内存
        std::cout << "Stack 析构函数被调用,释放内存" << std::endl;
    }
};

int main() {
    Date d;
    Stack s;
    return 0;
}

在上述代码中,Date 类没有申请资源,使用默认析构函数;Stack 类申请了动态内存,需要自己写析构函数来释放内存,否则会造成资源泄漏。


 8. ⼀个局部域的多个对象,C++ 规定后定义的先析构

 在一个局部域中,后定义的对象先析构。

#include <iostream>
class MyClass {
private:
    int id;
public:
    MyClass(int i) : id(i) {
        std::cout << "MyClass " << id << " 构造函数被调用" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass " << id << " 析构函数被调用" << std::endl;
    }
};

int main() {
    MyClass obj1(1);
    MyClass obj2(2);
    return 0;
}

在上述代码中,obj2 后定义,会先析构,输出结果中 MyClass 2 析构函数被调用 会先于 MyClass 1 析构函数被调用 输出。


 拷贝构造

特点六条

果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数 也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。

 1. 拷贝构造函数是构造函数的一个重载

 拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,该对象是另一个同类型对象的副本。它是构造函数的一种重载形式。

#include <iostream>
class MyClass {
private:
    int data;
public:
    // 普通构造函数
    MyClass(int val) : data(val) {
        std::cout << "普通构造函数被调用" << std::endl;
    }
    // 拷贝构造函数
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "拷贝构造函数被调用" << std::endl;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2(obj1); // 调用拷贝构造函数
    return 0;
}

 

 2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用

拷贝构造函数的第一个参数必须是类类型对象的引用,若使用传值方式会导致无穷递归调用。

#include <iostream>
class MyClass {
private:
    int data;
public:
    // 错误示例:传值方式作为拷贝构造函数参数,会引发编译错误
    // MyClass(MyClass other) : data(other.data) {}

    // 正确示例:使用引用作为拷贝构造函数参数
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "拷贝构造函数被调用" << std::endl;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2(obj1); // 调用拷贝构造函数
    return 0;
}

如果使用传值方式 MyClass(MyClass other),当调用拷贝构造函数时,为了传递 other 参数,又会调用拷贝构造函数,从而导致无穷递归调用。 

c++规定,拷贝构造之前要传值传参。 

 3.当自定义类型对象进行传值传参或传值返回时,会调用拷贝构造函数。

#include <iostream>
class MyClass {
private:
    int data;
public:
    MyClass(int val) : data(val) {
        std::cout << "普通构造函数被调用" << std::endl;
    }
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "拷贝构造函数被调用" << std::endl;
    }
};

// 传值传参
void func(MyClass obj) {
    // 函数体
}

// 传值返回
MyClass createObj() {
    MyClass obj(10);
    return obj;
}

int main() {
    MyClass obj1(10);
    func(obj1); // 传值传参,调用拷贝构造函数
    MyClass obj2 = createObj(); // 传值返回,调用拷贝构造函数
    return 0;
}

 

 在上述代码中,func 函数的传值传参和 createObj 函数的传值返回都会调用拷贝构造函数。

 4. 若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。

自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造

 

#include <iostream>
class InnerClass {
private:
    int innerData;
public:
    InnerClass(int val) : innerData(val) {
        std::cout << "InnerClass 普通构造函数被调用" << std::endl;
    }
    InnerClass(const InnerClass& other) : innerData(other.innerData) {
        std::cout << "InnerClass 拷贝构造函数被调用" << std::endl;
    }
};

class OuterClass {
private:
    int outerData;
    InnerClass inner;
public:
    OuterClass(int outerVal, int innerVal) : outerData(outerVal), inner(innerVal) {
        std::cout << "OuterClass 普通构造函数被调用" << std::endl;
    }
    // 未显式定义拷贝构造函数,使用编译器自动生成的拷贝构造函数
};

int main() {
    OuterClass obj1(10, 20);
    OuterClass obj2(obj1); // 调用编译器自动生成的拷贝构造函数
    return 0;
}

 在上述代码中,OuterClass 未显式定义拷贝构造函数,编译器自动生成的拷贝构造函数会对 outerData 进行值拷贝,对 inner 调用 InnerClass 的拷贝构造函数。

 6.并不是所有内置类型都能用拷贝构造的

例如:栈Stack,是内置类型;

现在来拷贝构造,发现报错了,这是为什么呢。

错误原因分析

  1. 默认拷贝构造函数的问题:在 main 函数中执行 Stack st2(st1); 时,由于没有自定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。默认拷贝构造函数只会进行浅拷贝,即简单地将 st1 中成员变量的值复制给 st2 。对于 _a 这个指针成员变量,浅拷贝后 st1._a 和 st2._a 指向同一块堆内存。

  2. 析构函数的重复释放:当程序结束时,st1 和 st2 会依次调用析构函数。由于它们的 _a 指针指向同一块内存,第一次调用析构函数(比如 st1 的析构函数)时释放了这块内存,当 st2 调用析构函数时,再次对已经释放的内存调用 free ,就会导致运行时错误。

  3. 需要自己写拷贝构造
    // 自定义拷贝构造函数
        Stack(const Stack& other)
        {
            _capacity = other._capacity;
            _top = other._top;
            _a = (int*)malloc(sizeof(int) * _capacity);
            if (nullptr == _a)
            {
                perror("malloc申请空间失败");
                return;
            }
            for (size_t i = 0; i < _top; ++i)
            {
                _a[i] = other._a[i];
            }
        }

    总结: 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名 (引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。


http://www.kler.cn/a/579230.html

相关文章:

  • 使用joblib 多线程/多进程
  • WPF基础知识41-60
  • 【新人系列】Golang 入门(三):条件循环
  • P8641 [蓝桥杯 2016 国 C] 赢球票--环形结构问题
  • 深入理解人脸特征向量及图片转换方法与开发架构
  • 大白话react第十四章高阶 React 组件开发和React 状态管理进阶等
  • 第7章 wireshark(网络安全防御实战--蓝军武器库)
  • 2025-03-09 学习记录--C/C++-PTA 习题10-8 递归实现顺序输出整数
  • 一行代码让手机秒变 AI 神器:DeepSeek 手机离线部署
  • 每日一题——三道链表简单题:回文,环形合并有序
  • 大规模语言模型的涌现能力
  • 跟着 Lua 5.1 官方参考文档学习 Lua (10)
  • 智慧城市新基建:AI代理IP如何让城市管理“耳聪目明”?
  • 【07】区块链性能
  • 字符串相乘——力扣
  • vue如何获取session对象
  • docker已创建容器设置自启动
  • PY32MD320单片机 QFN32封装,内置多功能三相 NN 型预驱。
  • 记录小白使用 Cursor 开发第一个微信小程序(二):创建项目、编译、预览、发布(250308)
  • Vue3实战学习(Vue3集成Element-Plus(常用依赖、插件安装与导入 。按钮、图标组件。自定义主题的实现)(超详细))(4)