【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)

ephemerals__ 2024-09-13 09:35:04 阅读 51

🌟🌟作者主页:ephemerals__

🌟🌟所属专栏:C++

目录

前言

什么是默认成员函数

一、构造函数

二、析构函数

三、拷贝构造函数

四、赋值重载

1. 运算符重载

2. 赋值运算符重载

总结


前言

        之前我们在 类和对象(上)中了解了关于类的定义、对象的创建等一些基本知识:

【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)-CSDN博客

今天,我们深入学习类和对象中默认成员函数相关的内容。

什么是默认成员函数

        所谓默认成员函数,就是在类当中我们没有显示实现,但是编译器自动生成的成员函数称之为默认成员函数。在c++11之前,默认成员函数一共有六个:

接下来我们会根据它们的特点,使用规则以及自实现方面逐一讲解。

一、构造函数

        构造函数的主要作用是:在对象被创建时,调用该函数对其成员变量进行初始化。就像我们在实现栈和队列时写的Init函数一样,会对它的成员先赋初值。

它的特点如下:

1. 构造函数的函数名与类名相同

2. 构造函数无返回值。(连void都不写)

3. 构造函数可以重载

4. 当对象被创建时,自动调用构造函数。

代码示例:

<code>#include <iostream>

using namespace std;

class MyClass

{

public:

//这里我们手动创建一个构造函数

MyClass(int a = 0, int b = 0, int c = 0)//不传参时给个默认值为0

{

_a = a;

_b = b;

_c = c;

}

void Print()

{

cout << _a << endl;

cout << _b << endl;

cout << _c << endl;

}

private:

int _a;

int _b;

int _c;

};

int main()

{

MyClass a;

//打印一下数据

a.Print();

return 0;

}

运行结果:

可以看到,三个成员变量的值被初始化为0。这说明对象在创建时构造函数自动调用的。接下来我们尝试给构造函数传参

<code>int main()

{

//可以用类似函数调用的方式传参

MyClass a(1, 2, 3);

//也可以使用类似结构体初始化的方式传参

MyClass b = { 4,5,6 };

//打印一下数据

a.Print();

b.Print();

return 0;

}

运行结果:

它还有以下三点特性:

5. 当我们在类中没有显示地定义构造函数时,编译器会自动生成一个无参的构造函数,用于创建对象时的初始化。一旦用户显示定义了构造函数之后,编译器则不会生成

6. 显示定义的无参构造函数全缺省构造函数,以及编译器自动生成的构造函数统称为默认构造函数。在一个类当中,这三种函数必须且只能存在一个。总的来说,不传参就可以调用的构造函数称之为默认构造函数。

7. 对于编译器自动生成的构造函数,当其对对象成员变量进行初始化时,如果成员是内置类型,则编译器通常不会为其赋初值;如果成员是由class或者struct创建的自定义类型(也就是类嵌套的情况),则会自动调用该自定义类型的默认构造函数。如果该成员没有默认构造函数,就会报错这也就是默认构造函数必须存在的原因

总结

        构造函数就是用于对创建的对象进行初始化的函数。我们在创建对象时,编译器会自动调用构造函数对成员变量进行初始化,这样我们就不需要单独定义或者使用Init函数对某个类进行初始化了

二、析构函数

        与构造函数相反,析构函数是在对象销毁时调用的,它的作用是在对象被销毁时完成对对象生成的资源的清理释放工作。就像我们在实现队列时使用的Destroy函数一样,完成对数据的销毁。

它的特点如下:

1. 析构函数的函数名是在类名之前加一个波浪号(~)

2. 析构函数无返回值(void也不写),且不能加入参数

3. 一个类当中只能有一个析构函数。

4. 当一个对象的生命周期结束之时,会自动调用析构函数

5. 当我们没有在类中显示定义析构函数时,编译器会自动生成一个析构函数,供对象调用。

代码示例:

<code>#include <iostream>

using namespace std;

class MyClass

{

public:

//构造函数

MyClass(int a = 0, int b = 0, int c = 0)

{

_a = a;

_b = b;

_c = c;

}

//析构函数

~MyClass()

{

_a = 0;

_b = 0;

_c = 0;

}

void Print()

{

cout << _a << endl;

cout << _b << endl;

cout << _c << endl;

}

private:

int _a;

int _b;

int _c;

};

int main()

{

MyClass a(1, 2, 3);

return 0;

}

调试观察:

可以看到,程序中我们创建对象时,给三个成员变量分别赋初值1、2、3,而当程序运行结束时,这三个成员变量的值已经变为了0,这说明对象销毁时确实自动调用了析构函数

6. 与构造函数类似,对于编译器自己生成的析构函数,当其对象被销毁时,内置类型成员变量通常不被处理;对于自定义类型成员变量,则会调用其析构函数

7. 对于一个局部域中的多个对象在进行销毁时,c++规定后创建的对象先析构

那么我们什么时候该显示写析构函数呢?来看一段代码:

<code>class A

{

public:

//...

private:

int _a;

char _c;

};

class B

{

public:

B(int n = 4)//初始化时在堆区申请内存空间

{

_p = (int*)malloc(n * sizeof(int));

if (_p == nullptr)

{

perror("malloc");

exit(1);

}

_n = n;

}

private:

int* _p;

int _n;

};

对于类A,他所创建的对象并没有申请额外的内存空间,在销毁时不会造成内存泄漏,此时我们就不需要手动写析构函数;对于类B,由于它在创建时在堆区申请了空间,它在销毁时编译器自己生成的析构函数并不会将这部分空间销毁掉,需要我们手动释放,所以此时就需要我们显示地写析构函数。

总的来说,如果类中没有申请资源,一般不需要手动写析构函数;如果申请了资源,就需要写析构函数,否则会造成内存泄漏。

三、拷贝构造函数

        拷贝构造函数是构造函数的一个重载,它用于完成对象的拷贝。它的特点如下:

1. c++规定对象只要发生拷贝行为,就必须调用拷贝构造,包括对象传参或者做返回值,都需要产生一份临时拷贝。

2. 拷贝构造函数的第一个参数必须是类类型的引用,而不是对象的值。因为对象在传值传参的时候需要调用拷贝构造,如果拷贝构造的参数带有对象的临时拷贝,那就会再次调用拷贝构造,以至于发生无限递归

3. 如果我们没有显示定义拷贝构造函数,编译器会自动生成一个拷贝构造。这个自动生成的拷贝构造在完成拷贝工作时,对内置类型会完成它的浅拷贝,对类类型则会调用该类的拷贝构造函数

接下来我们尝试写一个拷贝构造函数并且使用它:

#include <iostream>

using namespace std;

class MyClass

{

public:

//构造函数

MyClass(int a = 0, int b = 0, int c = 0)

{

_a = a;

_b = b;

_c = c;

}

//拷贝构造函数

MyClass(const MyClass& m)//确保源数据不被修改,在引用之前加上const

{

//逐一完成成员变量的复制

_a = m._a;

_b = m._b;

_c = m._c;

}

//析构函数

~MyClass()

{

_a = 0;

_b = 0;

_c = 0;

}

void Print()

{

cout << _a << endl;

cout << _b << endl;

cout << _c << endl;

}

private:

int _a;

int _b;

int _c;

};

int main()

{

MyClass a1(1, 2, 3);//创建对象a1并对其初始化

MyClass a2(a1);//调用拷贝构造,将a1拷贝给a2

//打印一下a2

a2.Print();

return 0;

}

运行结果:

可以看到,我们通过拷贝构造函数将a1拷贝给了a2。

        那么我们什么时候需要显示写拷贝构造函数供我们使用呢?之前我们提到,编译器自动生成的拷贝构造完成的是浅拷贝。这就意味着如果我们在类中有向堆区申请内存空间的方法,浅拷贝就无法达到预期效果

所以对于这种情况(类中有额外申请资源),我们就需要手动去写一个拷贝构造函数,实现深拷贝将申请的内存也复制一份出来

小技巧:是否需要显示写拷贝构造函数,就看类中是否有显示写析构函数。如果有写析构函数,那么通常需要写拷贝构造。

         当我们在某个函数当中将对象作为返回值时,由于这个返回值是一份临时拷贝,所以会自动调用拷贝构造函数,造成运行效率的下降。所以此时我们可以考虑返回该对象的引用,避免发生拷贝,提高运行效率需要注意的是:一定要确保该对象在函数栈帧销毁后仍然存在,避免出现悬挂引用

四、赋值重载

        在了解赋值重载之前,我们先学习一个概念:运算符重载

1. 运算符重载

        所谓运算符重载,指的就是当对象在使用一些运算符时,我们可以为该运算符设定新的含义。而这种含义的实现方式就是通过定义函数,该函数就叫做运算符重载

        当对象在使用运算符时,如果没有对应的运算符重载,就会发生报错。

        它的定义方式如下:

(返回值类型) operator(运算符)(函数参数)

{

        (函数体)

}

这里的operator是一个关键字,与需要定义的运算符相连接,构成函数名

关于运算符重载,有以下要注意的几点:

        1. 运算符重载的参数个数与该运算符的操作数一样多。例如 + 号进行重载时,第一个参数表示左操作数,第二个参数表示右操作数。如果这个运算符重载是成员函数一定要注意成员函数第一个位置已经有一个参数是this指针,所以我们要少写一个参数。

        2. 当我们使用一个运算符重载时,要注意该运算符本来的优先级和结合性是不变的

        3. 不能以“莫须有”的方式去重载本来就没有的运算符,例如operator@。

        4. 这五个运算符不能重载: .*    : :    sizeof    ? :    .  

        5. 我们在定义运算符重载时,必须要有类类型的参数,否则就会与重载的本意相悖。

        6. 对于++和--运算符的重载,由于前置和后置无法区分,所以c++规定:对于后置++/--,需要在函数的参数中增加一个哑元(通常是int类型),这个参数不在函数体中使用,但是有了这个参数就表示重载的是后置++/--

小知识

第 4 点中有一个运算符 “ .* ”,有很多人可能没有接触过这个运算符,我们来介绍一下它。

首先让我们创建一个类,这个类当中只有一个成员函数:

<code>class A

{

public:

void fun()

{

cout << "Hello World" << endl;

}

};

接下来,我们将函数的地址存储在一个函数指针当中:

int main()

{

void (A::*pf)() = &A::fun;

}

可以看到,以上代码非常奇怪。实际上,对于类的成员函数,我们在声明它的类型时,要表明它所在的类域。其次,对于类的成员函数,想要得到它的地址,需要加上&符号,而普通函数是否加&都表示它的地址。

接下来,我们创建一个A类对象,并通过该指针调用函数fun:

int main()

{

void (A::*pf)() = &A::fun;

A a;

(a.*pf)();

}

运行结果:

可以看到,运行成功了。这里我们在调用函数时,就使用到了“ .* ”运算符,它用于通过函数指针调用类的成员函数

接下来,我们针对MyClass类,尝试实现运算符重载:+ 。

<code>#include <iostream>

using namespace std;

class MyClass

{

public:

//构造函数

MyClass(int a = 0, int b = 0, int c = 0)

{

_a = a;

_b = b;

_c = c;

}

//拷贝构造函数

MyClass(const MyClass& m)

{

_a = m._a;

_b = m._b;

_c = m._c;

}

//析构函数

~MyClass()

{

_a = 0;

_b = 0;

_c = 0;

}

//+号重载

//我们定义一个含义:对象加上一个整数,该对象的所有成员变量都加上这个整数

MyClass operator+(int a)

{

MyClass tmp(*this);//将该对象的内容拷贝给临时变量tmp

tmp._a += a;

tmp._b += a;

tmp._c += a;

return tmp;//返回tmp的临时拷贝,表示的值就是加后的值,并且原对象未发生改变

}

void Print()

{

cout << _a << endl;

cout << _b << endl;

cout << _c << endl;

}

private:

int _a;

int _b;

int _c;

};

接着,我们来使用这个运算符重载:

int main()

{

MyClass a;//创建对象a

//将a与数字相加的值拷贝给其他对象

MyClass b(a + 1);//可以直接使用运算符

MyClass c(a.operator+(3));//也可以使用函数调用的方式

b.Print();

cout << endl;

c.Print();

return 0;

}

运行结果:

可以看到,运算符重载的编写成功了。注意:不管是用什么方式去使用运算符重载,本质都是函数调用。

2. 赋值运算符重载

        了解了运算符重载的概念、特性、定义方法以及使用方法之后,我们切入正题--赋值重载

        顾名思义,赋值重载就是对赋值运算符的重载函数,这个函数有点类似于拷贝构造,它的功能是完成已经存在的对象的拷贝赋值,这一点要和拷贝构造区分。

它的特点如下:

1. 赋值重载是运算符重载中的一种必须重载为成员函数。一般情况下,它的参数和返回值都是当前类类型的引用,这样会减少拷贝提高效率。

2. 当我们没有显示写出赋值重载时,编译器会自动生成。自动生成的赋值重载会对内置类型成员变量完成浅拷贝,对于自定义类型成员变量,则会调用其赋值重载函数

3. 与拷贝构造相同,如果我们的类中申请了资源,则需要自己显示写赋值重载来完成深拷贝;若没有申请资源,则可直接使用自动生成的赋值重载

小技巧:是否需要显示写赋值重载函数,就看类中是否有显示写析构函数。如果有写析构函数,那么通常需要写赋值重载。

接下来我们针对MyClass类实现一个简单的赋值重载:

<code>#include <iostream>

using namespace std;

class MyClass

{

public:

//构造函数

MyClass(int a = 0, int b = 0, int c = 0)

{

_a = a;

_b = b;

_c = c;

}

//拷贝构造函数

MyClass(const MyClass& m)

{

_a = m._a;

_b = m._b;

_c = m._c;

}

//析构函数

~MyClass()

{

_a = 0;

_b = 0;

_c = 0;

}

//+号重载

MyClass operator+(int a)

{

MyClass tmp(*this);

tmp._a += a;

tmp._b += a;

tmp._c += a;

return tmp;

}

//赋值重载

MyClass& operator=(MyClass& src)

{

_a = src._a;

_b = src._b;

_c = src._c;

return *this;//返回当前对象的引用可以完成连续赋值

}

void Print()

{

cout << _a << endl;

cout << _b << endl;

cout << _c << endl;

}

private:

int _a;

int _b;

int _c;

};

int main()

{

MyClass a(1, 2, 3);

MyClass b;

MyClass c;

c = b = a;

b.Print();

cout << endl;

c.Print();

return 0;

}

运行结果:

可以看到,我们成功将a的内容赋值给了b和c。

总结

        今天我们学习了四个类的默认成员函数以及它们的特点、使用方法:构造函数、析构函数、拷贝构造函数和赋值重载,它们能够确保资源的正确管理和对象状态的正确维护。之后博主会和大家分享其余的两个默认成员函数,并实现日期类。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。