【③C++ | 构造与析构函数】特性介绍 | 调用机制 | 用途展示 | 深浅拷贝 | 拷贝构造中的骚操作

在这里插入图片描述

前言

✨欢迎来到小KC++专栏,本节将为大家带来C++构造与析构函数的特性介绍 | 调用机制 | 用途展示 | 深浅拷贝 | 拷贝构造中的骚操作 的分享


1、构造函数

创建一个对象时,常常需要做某些初始化的工作,例如对数据成员赋初值。

为了解决这个问题,C++提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。

特点:

  • 构造函数名和类名相同
  • 构造函数可以重载和缺省
  • 构造函数没有返回类型声明

调用:

  • 自动调用(隐式) 一般情况下C++编译器会自动调用构造函数(无参构造)
  • 手动调用(显示) 在一些情况下则需要手工调用构造函数(有参构造)

2、析构函数

当对象释放时,我们可能需释放/清理对象里面的某些资源(比如:动态内存释放)。

为了解决这个问题,C++提供了析构函数(destructor)来处理对象的清理工作。析构函数和构造函数类似,不需要用户来调用它,而是在释放对象时自动执行。

特点:

  • 析构函数名和类名相同,但是得在前面加一个波浪号 ~
  • 析构函数只能有一个
  • 构造函数没有返回类型声明
  • 对象死亡(生命周期结束)的最后一个事情是调用析构函数

什么时候需要写析构函数?

当类的成员new了内存就需要自己手动写析构函数

3、构造/析构函数调用机制(顺序问题)

当定义了多个对象时,构造与析构的顺序是怎么样的呢?

在这里插入图片描述

结论:

  • 先创建的对象先构造,后创建的对象后构造
  • 先创建的对象后析构,后创建的对象先析构
  • new对象,调用delete直接被释放
  • static对象,最后释放(生命周期最长)

4、构造/析构函数用途展示

构造函数:可以用来初始化对象,而且不需要显式调用,方便,快捷

析构函数:可以用来释放对象, 一次写好,没有后顾之忧(经常忘记delete?)

class Man
{
public:
    Man()
    {
        age = 18;
        name = new char[20]{0};
        strcpy(name,"maye");
    }
    ~Man()
    {
        if(name!=nullptr)
        {
            delete[] name;
            name = nullptr;
        }
    }
    void print()
    {
        cout<<age<<" "<<name<<endl;
    }
private:
    int age;
    char* name;
}

5、构造函数分类

构造函数是可以重载的,根据参数类型和作用可以分为以下几类:

A、无参构造函数

  • 直接创建对象即可自动调用
  • Test te; 注意:不要在对象后面加(),无参构造函数不能显式调用

B、有参构造函数

  • 有三种调用方法

    //1,括号法
    Test t1(20,"cc");
    t1.print();
    //2,赋值符号
    Test t2 = {18,"maye"};
    t2.print();
    //3,匿名对象
    Test t3 = Test(90,"plus");
    t3.print();
    //注意:
    Test tt;	//error:类Test不存在默认构造函数
    
  • 如果没有写有参构造函数,那么C++编译器会自动帮我们生成一个无参构造函数
    如果写了有参构造函数,那么就不会帮我们生成了,必须自己写一个无惨构造函数,才能直接定义对象。

C、拷贝构造函数(赋值构造)

特性:

1. 不写拷贝构造函数,存在一个默认拷贝构造函数
2. 拷贝构造函数名和构造函数一样,算是构造函数特殊形态

3. 拷贝构造函数唯一的一个参数就是对对象的引用

  • 普通引用
  • const引用
  • 右值引用——>移动拷贝

4. 当我们通过一个对象产生另一个对象时候就会调用拷贝构造函数

  • 用一个对象去初始化另一个对象时(函数传参也会拷贝),需要拷贝构造(如果自己没有写,编译器会自动帮我们生成)

    Test t(1,"2");
    //1,赋值符号
    Test t1 =t;
    //2,参数方法
    Test t2(t);
    
    t2 = t1;	//这个调用的是赋值运算符重载函数
    
  • 注意:定义之后进行赋值不会调用拷贝构造函数,而是调用赋值函数,这是运算符重载

D、移动构造函数

  • 移动构造函数数用来实现移动语义,转移对象之间的资源(如果自己没有写,编译器会自动帮我们生成)

6、深拷贝和浅拷贝

首先,明确一点深拷贝和浅拷贝是针对类里面有指针的对象的,因为基本数据类型在进行赋值操作时(也就是拷贝)是直接将值赋给了新的变量,也就是该变量是原变量的一个副本,这个时候你修改两者中的任何一个的值都不会影响另一个,而对于对象来说在进行浅拷贝时,只是将对象的指针复制了一份,也就内存地址,即两个不同的对象里面的指针指向了同一个内存地址,那么在改变任一个对象的指针指向的内存的值时,都是该变这个内存地址的所存储的值,所以两个变量的值都会改变。

简而言之,当数据成员中有指针时,必须要用深拷贝。

  • 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址。

    • 使用浅拷贝,释放内存的时候可能会出现重复释放同一块内存空间的错误。
  • 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。

    • 使用深拷贝下,释放内存的时候不会因为出现重复释放同一个内存的错误。

注意

  • C++类中默认提供的拷贝构造函数,是浅拷贝的;拷贝构造函数中做普通的赋值操作也是浅拷贝
  • 要想实现深拷贝,必须自己手动实现拷贝构造函数

案例分析:
错误代码,本例就是使用默认的拷贝构造函数创建了一个对象,导致两个对象的内存是同一快地方,两个对象生命周期结束的时候都释放了一次这块内存~同一块内存释放了两次

#include<iostream>
#include<cstring>
using namespace std;
class MM{
public:
	MM(const char* str,int num)
	{
		int length=strlen(str)+1;
		name=new char[length];
		strcpy_s(name,length,str);
		age=num;
	}
	~MM()
	{
		if(name!=nullptr)
		{
			delete[] name;
			name=nullptr;
		}
	}
protected:
	char* name;
	int age;
};
void testQuestion(){
	MM mm("zhangzhang",19);
	MM xx=mm;
}
int main() 
{
	testQuestion();
	return 0;
}

原因如下图:
在这里插入图片描述

正确代码,自己手写拷贝构造函数

#include<iostream>
#include<cstring>
using namespace std;
class MM {
public:
	MM(const char* str, int num)
	{
		int length = strlen(str) + 1;
		name = new char[length];
		strcpy_s(name, length, str);
		age = num;
	}
	MM(const MM& object)
	{
		//深拷贝
		int length = strlen(object.name) + 1;
		name = new char[length];
		strcpy_s(name, length, object.name);
		age = object.age;
	}
	~MM()
	{
		if (name != nullptr)
		{
			delete[] name;
			name = nullptr;
		}
	}
protected:
	char* name;
	int age;
};
void testQuestion() {
	MM mm("zhangzhang", 19);
	MM xx = mm;
}
int main()
{
	testQuestion();
	return 0;
}

解释:

在这里插入图片描述

7、构造函数的初始化参数列表

先来看一下什么是类的组合,组合(有时候叫聚合)是将一个对象放到另一个对象里)。它是一种 has-a 的关系。

简单来说,就是一个类的对象作为另一个类的成员,这就叫做类的组合。

如果我们有一个类成员,而且这个成员它只有一个带参数的构造函数,没有默认构造函数。这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数

在这里插入图片描述

基本数据类型的对象也可以,使用初始化参数列表哟~(多个对象初始化,中间用逗号隔开)

本类和对象成员都需要执行构造函数,那么谁先执行呢?有什么样的顺序呢?

  • 先执行被组合对象的构造函数,如果组合对象有多个,按照定义顺序,而不是按照初始化列表的顺序!

  • 析构和构造顺序相反

  • 注意:类不能包含自身对象,否则会形成死循环

8、类中类

  • 类中类依旧受权限限定,就相当于把一个类丢到另外一个类中,他们两个没有关系
  • 访问必须要类名::剥洋葱的方式访问
#include<iostream>
using namespace std;
class xx {
public:
	xx(){cout<<"外面的构造函数"<<endl;}
protected:
public:
	//类中类依旧受权限限定,就相当于把一个类丢到另外一个类中,他们两个没有关系
	class dd
	{
	public:
		dd()
		{
			cout<<"类中类构造函数"<<endl;
		}
		void print(){cout<<"类中类构造函数"<<endl;}
	protected:
	};
};
void testlzl()
{
	xx::dd bb;
	bb.print();
}
int main()
{
	testlzl();
	return 0;
}

在这里插入图片描述

9、拷贝构造函数中的骚操作

A、explicit

C++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。(构造只能使用带括号的方式)

[注意]explicit用于修饰构造函数,防止隐式转换。是针对单个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
在这里插入图片描述

在实际代码中的东西可不像这种故意造出的例子。

发生隐式转换,除非有心利用,隐式转换常常带来程序逻辑的错误,而且这种错误一旦发生是很难察觉的。

原则上应该在所有的单个参数的构造函数前加explicit关键字,当你有心利用隐式转换的时候再去解除explicit,这样可以大大减少错误的发生。

B、default

default 函数特性仅适用于类的构造和析构函数,且构造函数必须是默认构造函数

如果类 A 有用户自定义的构造函数,却没有自己实现默认构造函数,那么会报错!因为编译器将不再会自动为它隐式的生成默认构造函数。如果需要用到默认构造函数来创建类的对象时,程序员必须自己显式的定义默认构造函数。

我们只需在函数声明后加上=default;,就可将该函数声明为 default 函数,编译器将为显式声明的 default 函数自动生成函数体。

class Int
{
private:
	int m_number;
public:
	 Int() = default;
	 Int(int number) { m_number = number; }
};
int main()
{
	Int i = 2;
	Int a;
	return 0;
}
C、delete

对于 C++ 的类,如果程序员没有为其定义特殊成员函数,那么在需要用到某个特殊成员函数的时候,编译器会隐式的自动生成一个默认的特殊成员函数,比如拷贝构造函数,或者拷贝赋值操作符。

程序员不需要自己手动编写拷贝构造函数以及拷贝赋值操作符,依靠编译器自动生成的默认拷贝构造函数以及拷贝赋值操作符就可以实现类对象的拷贝和赋值。这在某些情况下是非常方便省事的,但是在某些情况下,假设我们不允许发生类对象之间的拷贝和赋值,可是又无法阻止编译器隐式自动生成默认的拷贝构造函数以及拷贝赋值操作符,那这就成为一个问题了。

为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:deleted 函数。程序员只需在函数声明后加上“=delete;”,就可将该函数禁用。例如,我们可以将类 X 的拷贝构造函数以及拷贝赋值操作符声明为 deleted 函数,就可以禁止类 X 对象之间的拷贝和赋值。

用法示例:禁止拷贝

class Int
{
private:
	int m_number;
public:
	 Int(int number) { m_number = number; }
	 Int(const Int& other) = delete;
};
int main()
{
	Int i = 2;
	Int a = i;		//error C2280: “Int::Int(const Int &)”: 尝试引用已删除的函数
	return 0;
}

总结

构造函数和析构函数对于类的使用和对象的生命周期非常重要。构造函数负责初始化对象的状态,确保对象在创建后处于可使用的有效状态。析构函数则负责清理对象所占用的资源,防止内存泄漏和资源泄漏。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

热爱编程的张同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值