【028】C++ 类和对象的 构造函数、析构函数、拷贝构造、初始化列表 详解(最全讲解)

您所在的位置:网站首页 奇函数定义式 【028】C++ 类和对象的 构造函数、析构函数、拷贝构造、初始化列表 详解(最全讲解)

【028】C++ 类和对象的 构造函数、析构函数、拷贝构造、初始化列表 详解(最全讲解)

2023-06-17 16:35| 来源: 网络整理| 查看: 265

C++类和对象的构造函数、析构函数、拷贝构造、初始化列表详解 引言一、构造函数1.1、数据初始化和清理1.2、构造函数概述1.3、构造函数的定义1.4、提供构造函数的影响 二、析构函数三、拷贝构造函数3.1、拷贝构造的定义3.2、拷贝构造、无参构造、有参构造 三者的关系3.3、拷贝构造的调用形式3.4、拷贝构造的深拷贝和浅拷贝 四、初始化列表4.1、对象成员4.2、初始化列表的使用 五、explicit关键字——防止构造函数隐式转换六、类的对象数组七、动态对象的创建7.1、c语言的方式创建动态对象7.2、new创建动态对象7.3、delete释放动态对象7.4、动态对象数组 总结

引言

💡 作者简介:专注于C/C++高性能程序设计和开发,理论与代码实践结合,让世界没有难学的技术。包括C/C++、Linux、MySQL、Redis、TCP/IP、协程、网络编程等。 👉 🎖️ CSDN实力新星,社区专家博主 👉 🔔 专栏介绍:从零到c++精通的学习之路。内容包括C++基础编程、中级编程、高级编程;掌握各个知识点。 👉 🔔 专栏地址:C++从零开始到精通 👉 🔔 博客主页:https://blog.csdn.net/Long_xu

🔔 上一篇:【027】C++类和对象的基本概念

一、构造函数 1.1、数据初始化和清理

当实例化一个对象的时候,这个对象应该有一个初始化状态,当对象销毁之前应该销毁自己创建的数据。对象的初始化和清理是非常重要的安全问题,一个对象或变量没有初始化时,对其使用的后果是未知的;同样的,使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。

因此,C++提供了构造函数和析构函数,这两个函数被编译器自动调用,完成对象初始化和对象清理工作。对象的初始化和清理工作是编译器强制要求去做的,即使没有提供初始化操作和清理操作,编译器也会自动添加默认的操作,只是默认的初始化操作不会做任何事。所以,编写类应该顺便提供初始化函数。

初始化操作是必须的,由编译器自动调用,开发人员只需要提供初始化函数。

1.2、构造函数概述

C++构造函数是一种特殊的成员函数,用于初始化类的对象。它们具有与类名称相同的名称,并且不返回任何值(包括void)。它们可以带有参数或不带参数,根据需要进行重载。

当创建一个类对象时,构造函数会被自动调用。如果没有定义构造函数,则编译器会提供一个默认的构造函数。默认构造函数不执行任何操作,但确保对象被正确初始化。

在构造函数中,可以对数据成员进行初始化、分配内存空间和执行其他必要的操作

类实例化对象的时候,系统自动调用构造函数,完成对象的初始化。 如果用户不提供构造函数,编译器会自动添加一个默认的构造函数(空函数)。

1.3、构造函数的定义

构造函数名和类名相同,没有返回值(连void都不能有),可以有参数(即可以重载);权限为public。

实例化对象时先给对象开辟空间,然后调用构造函数进行初始化。

格式:

类名() { // 初始化操作 } 类名(参数列表) { // 初始化操作 }

示例:

#include #include class Person { public: char name[32]; int age; // 构造函数1 Person() { name = ""; age = 0; } // 构造函数2 Person(const char *n, int a) { strcpy(name,n); age = a; } }; int main() { // 使用默认构造函数创建对象p1,隐式调用无参构造函数 Person p1; // 显式调用无参构造函数 Person p2=Person(); // 使用第二个构造函数创建对象p3,隐式调用有参构造函数 Person p3("Tom", 25); // 显式调用有参构造函数 Person p4=Person("Lion",18); // 匿名对象,当前语句结束立即释放 Person(); Person("Long",20); return 0; } 1.4、提供构造函数的影响 如果用户不提供任何构造函数,编译器默认提供一个空的无参构造。如果用户定义了构造函数(不管是有参还是无参),编译器不再提供默认的构造函数。 #include #include class Person { public: char name[32]; int age; // 构造函数 Person(const char *n, int a) { strcpy(name,n); age = a; } }; int main() { //Person obj;//error,没有无参构造函数 Person obj("Lion",18);// OK,提供了有参构造 return 0; } 二、析构函数

析构函数的定义:函数名和类名称相同,在函数名前面添加~符号,没有返回值,不能重载。

~类名() { // 释放内存操作 }

当对象生命周期结束时,系统自动调用析构函数。先调用析构函数,再释放对象的空间。

示例:

#include using namespace std; class Data{ public: int a; public: Data() { a=100; cout cout Data ob02(300); } Data ob03(400); return 0; }

注意:构造和析构的顺序是向反的,先构造的后析构。

一般情况下,空的析构函数就足够;但是,如果一个类中有指针成员,这个类必须写析构函数,释放指针成员所指向的空间。 示例:

#include #include using namespace std; class Data{ public: int a; char *name; public: Data() { a=100; cout cout // 相关操作 }

注意:

一旦实现了拷贝构造函数,必须完成赋值操作。一般情况下,使用系统默认的拷贝构造(浅拷贝)就足够了;只有要完成深拷贝的情况下才需要我们实现拷贝构造函数。

示例:

#include using namespace std; class Data{ public: int a; public: Data() { a=100; cout a=ob.a; } ~Data() { cout public: int a; public: // 拷贝构造 Data(const Data &ob) { a=ob.a; } ~Data() { cout public: int a; public: Data(int p) { a=p; cout Data ob01;//error,找不到无参构造函数 return 0; }

示例三:实现了有参构造或无参构造函数,不影响编译器自动生成默认的拷贝构造函数。

#include using namespace std; class Data{ public: int a; public: Data() { a=100; cout cout public: int a; public: Data() { a=100; cout a=ob.a; cout } int main() { Data ob01(100);// 有参构造 func(ob01);// 拷贝构造 return 0; }

(4)函数返回普通对象,可能会发生拷贝构造(依赖于编译器:在windows的visual studio下会发生拷贝构造,在linux下不会发生拷贝构造)。

Data getObj() { Data ob1(100); return ob1; } int main() { Data ob2=getObj(); }

Windows的过程:先创建一个匿名对象,然后调用拷贝构造给匿名对象赋值,ob1释放,ob2接管匿名对象。 在这里插入图片描述 Linux下的过程:进行了优化,避免了拷贝构造,提高效率。不创建匿名对象,ob2直接接管ob1。 在这里插入图片描述

3.4、拷贝构造的深拷贝和浅拷贝

拷贝构造函数可以执行浅拷贝和深拷贝。

浅拷贝:当使用浅拷贝时,只复制指针或引用而不是整个对象。这意味着新对象与原始对象共享相同的内存位置,因此对其中一个对象所做的更改也会影响另一个对象。如果原始对象被删除,则新对象也会失效。

深拷贝:当使用深拷贝时,会复制整个对象及其所有内容,包括指针所指向的数据。这意味着每个对象都有自己独立的内存位置,因此对其中一个对象所做的更改不会影响另一个对象。如果原始对象被删除,则新对象仍然有效。

默认的拷贝构造都是浅拷贝。如果类中没有指针成员,不用实现拷贝构造和析构函数。

如果类中有指针成员,且指向堆区空间,必须实现拷贝构造函数完成深拷贝。

例如:

#include class Person { public: char* name; int age; Person(int num,const char *str) { age=num; name = new char[strlen(str)+1]; strcpy(name,str); } // 拷贝构造函数 Person(const Person& p) { name = new char[strlen(p.name)+1]; strcpy(name,p.name); age = p.age; } ~Person() { if(name!=NULL) delete [] name; } }; int main() { // 创建Person1 Person person1(18,"Tom"); // 使用深拷贝创建Person2 Person person2 = person1; // 打印person1和person2的信息 cout cout cout cout cout B ob02(100); } return 0; }

输出:

A 无参构造 B 无参构造 B 析构函数 A 析构函数 ------------------- A 无参构造 B 有参构造 B 析构函数 A 析构函数 4.2、初始化列表的使用

类想调用对象成员的有参构造,必须使用初始化列表。

格式:

B类的有参构造函数(参数列表):A类的对象名(参数列表) { // 有参构造函数体 }

示例:

#include using namespace std; class A{ public: int mA; public: A() { cout cout cout mB=b; cout { B ob01; } cout B ob03(200,300); } return 0; }

输出:

A 无参构造 B 无参构造 B 析构函数 A 析构函数 ------------------- A 无参构造 B 有参构造 B 析构函数 A 析构函数 ------------------- A 有参构造 B 有参构造:300 B 析构函数 A 析构函数 五、explicit关键字——防止构造函数隐式转换

C++提供了关键字explicit 禁止通过构造函数进行的隐式转换;声明为explicit的构造函数不能在隐式转换中使用。

explicit用于修饰构造函数构造函数,防止隐式转换;是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参数构造)而言的。

示例:

#include using namespace std; class MyString{ public: explicit MyString(int n) { cout // 语义不明,给字符串赋值还是初始化? // MyString str=100;// 本质是调用MyString(int n)初始化 MyString str(100); // 寓意明确,字符串赋值 MyString str01="abc"; MyString str02("abc"); return 0; } 六、类的对象数组

对象数组本质是数组,数组的每个元素是对象。

对象数组的每个元素都会自动调用构造函数和析构函数。对象数组不初始化,每个元素调用无参构造。对象数组的初始化必须显式使用有参构造,逐个元素初始化。

示例:

#include using namespace std; class Data { public: int mA; public: Data() { cout cout Data data[5]; // 对象数组的初始化必须显式使用有参构造,逐个元素初始化。 Data data2[5] = { Data(100),Data(200), Data(300), Data(400), Data(500) }; for (int i = 0; i public: int mAge; char *mName; public: Person() { mAge=20; mName=(char*)malloc(32); strcpy(mName,"Lion"); } void init() { mAge=20; mName=(char*)malloc(32); strcpy(mName,"Lion"); } void clean() { if(mName!=NULL) free(mName); } ~Person() { if(mName!=NULL) free(mName); } }; int main() { Person* person=(Person*)malloc(sizeof(Person)); if(person==NULL) return -1; // 需要调用初始化函数 person->init(); //...... // 需要调用清理函数 persion->clean(); // 释放对象 free(person); return 0; }

C语言风格创建对象的问题:

必须确定对象的长度。malloc返回void指针,C++不允许将void赋值给其他任何指针,必须强转。malloc可能申请内存失败,所有必须判断返回值来确定内存分配成功。用户使用对象之前必须记住对他初始化,构造函数不能显式调用初始化(构造函数由编译器自动调用),用户可能忘记调用初始化函数。

C语言的动态内存分配函数太复杂,容易令人混淆,C++中推荐使用new和delete。

7.2、new创建动态对象

C++中解决动态内存分配的方案是把创建一个对象所需的操作都结合在一个new的运算符中;当用new创建一个对象时,它就在堆中为对象分配内存并调用构造函数完成初始化。 比如:

Person person=new Person;

new操作符能确定在调用构造函数初始化之前内存分配是成功的,所以不用显式确定调用是否成功。现在在堆中创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆中创建一个对象和在栈中创建一个对象一样简单。

7.3、delete释放动态对象

new表达式的反面是delete表达式。delete先调用析构函数,再释放对象内存。

#include using namespace std; class Person{ public: int mAge; char *mName; public: Person() { mAge=20; mName=new char[32]; strcpy(mName,"Lion"); cout cout delete [] mName; mName=NULL; } } }; int main() { Person *person=new Person;//无参构造 Person *person2=new Person(20,"Long");//有参构造 person->show(); person2->show(); delete person; delete person2; } 7.4、动态对象数组

当创建一个对象数组时,必须为每个数组元素调用构造函数(默认是调用无参构造),除了在栈上可以聚合初始化,必须提供一个默认的构造函数。

#include using namespace std; class Person{ public: int mAge; char *mName; public: Person() { mAge=20; mName=new char[32]; strcpy(mName,"Lion"); cout cout delete [] mName; mName=NULL; } } }; int main() { Person person[]={Person(100,"Lion"),Person(200,"Tom"),Person(300,"Long")}; cout


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3