企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持知识库和私有化部署方案 广告
[TOC] # 简介 当我们创建对象的时候,这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。 对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。 无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。 为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现 # 构造函数和析构函数 构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。 析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。 构造函数语法: * 构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数。 ~~~ ClassName(){} ~~~ 析构函数语法: * 析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。 ~~~ ~ClassName(){} ~~~ ~~~ class Person{ public: Person(){ cout << "构造函数调用!" << endl; pName = (char*)malloc(sizeof("John")); strcpy(pName, "John"); mTall = 150; mMoney = 100; } ~Person(){ cout << "析构函数调用!" << endl; if (pName != NULL){ free(pName); pName = NULL; } } public: char* pName; int mTall; int mMoney; }; void test(){ Person person; cout << person.pName << person.mTall << person.mMoney << endl; } ~~~ # 注意点 1. 构造函数和析构函数权限必须是公有的 2. 构造函数可以重载 3. 构造函数没有返回值,不能用void,构造函数可以有参数,析构函数没有返回值,不能用void,没有参数 4. 如果类有成员对象,先调用成员对象的构造函数,再调用自己的,析构顺序反之 5. 成员对象的构造函数调用和定义顺序一样 # 构造函数的分类 * 按参数类型:分为无参构造函数和有参构造函数 * 按类型分类:普通构造函数和拷贝构造函数(复制构造函数) 拷贝构造是没有对象的时候调用的 ~~~ class Person{ public: Person(){ cout << "no param constructor!" << endl; mAge = 0; } //有参构造函数 Person(int age){ cout << "1 param constructor!" << endl; mAge = age; } //拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象 Person(const Person& person){ cout << "copy constructor!" << endl; mAge = person.mAge; } //打印年龄 void PrintPerson(){ cout << "Age:" << mAge << endl; } private: int mAge; }; //1. 无参构造调用方式 void test01(){ //调用无参构造函数 Person person1; person1.PrintPerson(); //无参构造函数错误调用方式 //Person person2(); //person2.PrintPerson(); } //2. 调用有参构造函数 void test02(){ //第一种 括号法,最常用 Person person01(100); person01.PrintPerson(); //调用拷贝构造函数 Person person02(person01); person02.PrintPerson(); //第二种 匿名对象(显示调用构造函数) Person(200); //匿名对象,没有名字的对象 Person person03 = Person(300); person03.PrintPerson(); //注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型 Person person06(Person(400)); //等价于 Person person06 = Person(400); person06.PrintPerson(); //第三种 =号法 隐式转换 Person person04 = 100; //Person person04 = Person(100) person04.PrintPerson(); //调用拷贝构造 Person person05 = person04; //Person person05 = Person(person04) person05.PrintPerson(); } ~~~ b为A的实例化对象,`A a = A(b)` 和 A(b)的区别? 当A(b) 有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接的时候,编译器认为你A(b) 等价于 `A b.` 注意:不能调用拷贝构造函数去初始化匿名对象,也就是说以下代码不正确: ~~~ class Teacher{ public: Teacher(){ cout << "默认构造函数!" << endl; } Teacher(const Teacher& teacher){ cout << "拷贝构造函数!" << endl; } public: int mAge; }; void test(){ Teacher t1; //error C2086:“Teacher t1”: 重定义 Teacher(t1); //此时等价于 Teacher t1; } ~~~ # 构造函数调用规则 默认情况下,c++编译器至少为我们写的类增加3个函数 1. 默认构造函数(无参,函数体为空) 2. 默认析构函数(无参,函数体为空) 3. 默认拷贝构造函数,对类中非静态成员属性简单值拷贝 * 如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数 * 如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造 # 拷贝构造为什么是const引用 ~~~ CBox(CBox initB);//复制构造函数一开始想到的原型 CBox cigar; CBox myBox(cigar); //如果编写这样一条语句 //那么将生成这样一条对复制构造函数的调用CBox::CBox(cigar); ~~~ 因为实参是通过按值传递机制传递的。在可以传递对象cigar之前,编译器需要安排创建该对象的副本。因此,编译器为了处理复制构造函数的这条调用语句,需要调用复制构造函数来创建实参的副本。但是,由于是按值传递,第二次调用同样需要创建实参的副本,因此还得调用复制构造函数,就这样持续不休。最终得到的是对复制构造函数的无穷调用。(其实就是创建副本也是需要调用复制构造函数的) 所以解决办法先是要将形参改为引用形参: ~~~ CBox (CBox &initB); ~~~ 其实,这里,如果不去改变实参的值的话,不加const的效果和加const的效果是一样的,而且不加const编译器也不会报错,因为函数的形参是引用,则调用函数时不需要复制实参,函数是直接访问调用函数中的实参变量的。但是为了整个程序的安全,还是加上const,防止对实参的意外修改 # 返回值优化 局部变量,内存不释放 relase地址一样,debug不一样 ~~~ //局部对象以值的方式从函数返回 //vs Debug模式下会调用拷贝构造,relase模式下 Person show() { //局部对象 Person p; cout << "局部对象的地址: " << &p << endl; return p; } int main() { Person p1 = show(); cout << "m1对象的地址: " << &p1 << endl; system("pause"); return EXIT_SUCCESS; } ~~~ # 多个对象 构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。 在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做对象成员。 C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,c++编译器必须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办? 那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。 解决办法非常简单:对于子类调用构造函数,c++为此提供了专门的语法,即构造函数初始化列表。 当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。 析构函数和构造函数调用顺序相反,先构造,后析构。 ~~~ //初始化列表可以指定调用构造函数 Person(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){ cout << "Person 构造函数!" << endl; } ~~~