对象的初始化和清理
- 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
- C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 person() 7 { 8 cout << "person的构造函数" << endl; 9 } 10 ~person() 11 { 12 cout << "person的析构函数函数" << endl; 13 } 14 }; 15 //构造与析构都必须有实现,如果我们不提供,编译器会自动提供一个空实现的构造与析构· 16 void test01() 17 { 18 person p;//栈区函数,函数结束后会自动重置 19 } 20 21 int main() 22 { 23 //test01(); 24 person p; 25 system("pause");//再按任意键之前main函数不会结束所以析构函数不会出现 26 return 0; 27 }
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 person() 7 { 8 cout << "person的构造函数" << endl; 9 } 10 ~person() 11 { 12 cout << "person的析构函数函数" << endl; 13 } 14 }; 15 //构造与析构都必须有实现,如果我们不提供,编译器会自动提供一个空实现的构造与析构· 16 void test01() 17 { 18 person p;//栈区函数,函数结束后会自动重置 19 } 20 21 int main() 22 { 23 test01();//执行完后析构函数将自动出现 24 //person p; 25 system("pause"); 26 return 0; 27 }
构造函数分类与三种输出方法:
分类与括号法:
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 //构造函数按参数分为: 有参构造和无参构造(默认构造) 7 //普通构造函数 8 person() 9 { 10 cout << "person 无参构造函数" << endl; 11 } 12 person(int a) 13 { 14 age = a; 15 cout << "person 有参构造函数" << endl; 16 } 17 //构造函数按类型分为: 普通构造和拷贝构造 18 //拷贝构造 19 person(const person& p)//const保证不修改原函数,用引用的方式是因为我们不是调用原函数 20 { 21 //将传入的人的所有的属性拷贝到当前对象上 22 age = p.age; 23 cout << "person 拷贝构造函数" << endl; 24 }; 25 int age; 26 }; 27 void test01() 28 { 29 //1.括号法 30 person p1;//无参构造函数 31 /* 注意事项1 32 调用默认构造函数时不要加() 33 因为编译器会认为是一个函数声明 34 person p1(); 35 void fuc() 这就是函数声明;*/ 36 person p2(10);//有参构造函数 37 person p3(p2);//拷贝构造函数 里面是p1 p2均可 38 cout << "p2年龄为" << p2.age << endl; 39 cout << "p3年龄为" << p3.age << endl; 40 } 41 int main() 42 { 43 test01(); 44 }
显示法:
1 2 // 显示法 3 person p1; 4 person p2 = person(10);//与括号法调用方式 :person p2(10);对比 5 person p3 = person(p2);//与括号法调用方式 :person p3(p2);对比
匿名对象:
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 //构造函数按参数分为: 有参构造和无参构造(默认构造) 7 //普通构造函数 8 person() 9 { 10 cout << "person 无参构造函数" << endl; 11 } 12 person(int a) 13 { 14 age = a; 15 cout << "person 有参构造函数" << endl; 16 } 17 //构造函数按类型分为: 普通构造和拷贝构造 18 //拷贝构造 19 person(const person& p)//const保证不修改原函数,用引用的方式是因为我们不是调用原函数 20 { 21 //将传入的人的所有的属性拷贝到当前对象上 22 age = p.age; 23 cout << "person 拷贝构造函数" << endl; 24 }; 25 ~person() 26 { 27 cout << "person 析构函数调用" << endl; 28 } 29 int age; 30 }; 31 void test01() 32 { 33 person(10);//匿名对象 特点:当前执行结束后,系统会立即回收掉匿名对象 34 cout << "aaa"; 35 } 36 int main() 37 { 38 test01(); 39 }
注意事项二 :不要用拷贝构造函数初始化匿名对象
隐式转换法
1 person p4 = 25;//相当于写了 person p4 = person(10);有参构造函数
2 person p5 = p4;//拷贝构造函数
拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 person() 7 { 8 cout << "person 的无参构造函数的调用" << endl; 9 } 10 person(int age) 11 { 12 m_age = age; 13 cout << "person 的有参构造函数的调用" << endl; 14 cout << "参构造函数中的m_age:" << m_age << endl; 15 } 16 ~person() 17 { 18 cout << "person 的析构函数的调用" << endl; 19 } 20 person(const person& p)//传入类的别名并且限制修改类中的元素和修饰类的地址 21 { 22 cout << "person 的拷贝构造函数的调用" << endl; 23 //m_age = p.m_age;//将 24 //cout << "拷贝构造函数中的" << m_age << endl; 25 cout << "拷贝构造函数中的 p.m_age:" << p.m_age << endl; 26 } 27 int m_age; 28 }; 29 //使用一个已经创建完毕的对象来初始化一个新对象 30 void test01() 31 { 32 person p1(20); 33 person p2(p1); 34 } 35 //2. 值传递(给一个函数的参数进行传值)的方式给函数参数传值 36 //相当于Person p1 = p; 37 void dowork(person p1) 38 { 39 40 } 41 void test02() 42 { 43 person p; 44 dowork(p);//值传递的本质是给原函数拷贝一个副本出来输出,这符合拷贝构造函数的调用条件 45 } 46 //3. 以值方式返回局部对象 47 person dowork2() 48 { 49 person p1;//再执行完之后会释放掉,所以会触发析构函数调用 50 cout << (int*)&p1 << endl;//(int *) 是强制转化为整型指针类型,所以最后就是一个指向p1 的int 指针 51 return p1;//不会返回person p1中的p1,而是拷贝一份返回出去,这符合拷贝构造函数调用条件 52 } 53 void test03() 54 { 55 person p = dowork2(); 56 cout << (int*)&p << endl; 57 } 58 int main() 59 { 60 //test01(); 61 //test02(); 62 test03(); 63 } 64 65
构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
-
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
-
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
1 class Person { 2 public: 3 //无参(默认)构造函数 4 Person() { 5 cout << "无参构造函数!" << endl; 6 } 7 //有参构造函数 8 Person(int a) { 9 age = a; 10 cout << "有参构造函数!" << endl; 11 } 12 //拷贝构造函数 13 Person(const Person& p) { 14 age = p.age; 15 cout << "拷贝构造函数!" << endl; 16 } 17 //析构函数 18 ~Person() { 19 cout << "析构函数!" << endl; 20 } 21 public: 22 int age; 23 }; 24 25 void test01() 26 { 27 Person p1(18); 28 //如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作 29 Person p2(p1); 30 31 cout << "p2的年龄为: " << p2.age << endl; 32 } 33 34 void test02() 35 { 36 //如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造 37 Person p1; //此时如果用户自己没有提供默认构造,会出错 38 Person p2(10); //用户提供的有参 39 Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供 40 41 //如果用户提供拷贝构造,编译器不会提供其他构造函数 42 Person p4; //此时如果用户自己没有提供默认构造,会出错 43 Person p5(10); //此时如果用户自己没有提供有参,会出错 44 Person p6(p5); //用户自己提供拷贝构造 45 } 46 47 int main() { 48 49 test01(); 50 51 system("pause"); 52 53 return 0; 54 }
深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 person() 7 { 8 cout << "person的默认构造函数调用" << endl; 9 } 10 person(int age ,int height) 11 { 12 m_age = age; 13 m_height = new int(height); 14 cout << "person的有参构造函数调用" << endl; 15 } 16 int m_age; 17 int* m_height; 18 ~person()//析构代码的用途 : 释放构造函数在堆区的内存 19 { 20 if (m_height != NULL) 21 { 22 delete m_height; 23 m_height = NULL; 24 } 25 cout << "person的析构函数调用" << endl; 26 } 27 person(const person& p) 28 { 29 cout << "person拷贝构造函数调用的调用" << endl; 30 //系统会自动执行实现 m_ age = p.m_age; m_height =p.m_height; 31 //但因为拷贝函数和原函数的height指向同一个地址开辟在堆区,释放时会有冲突,所以我们需要 32 //深拷贝操作 33 m_height = new int(*p.m_height); 34 } 35 }; 36 void test01() 37 { 38 person p1(18,160); 39 cout << "p1 age is :" << p1.m_age << ",and his height is : " << *p1.m_height<< endl; 40 person p2(p1); 41 cout << "p2 age is :" << p2.m_age << ",and his height is : " <<* p2.m_height << endl; 42 } 43 int main() 44 { 45 test01(); 46 }
初始化列表
作用:
C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)... {}
初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
首先把数据成员按类型分类并分情况说明:
- 1.内置数据类型,复合类型(指针,引用)- 在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
- 2.用户定义类型(类类型)- 结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
注意点:
初始化列表的成员初始化顺序:
C++ 初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
1 class CMyClass { 2 CMyClass(int x, int y); 3 int m_x; 4 int m_y; 5 }; 6 7 CMyClass::CMyClass(int x, int y) : m_y(y), m_x(m_y) 8 { 9 };
你可能以为上面的代码将会首先做 m_y=I,然后做 m_x=m_y,最后它们有相同的值。但是编译器先初始化 m_x,然后是 m_y,,因为它们是按这样的顺序声明的。结果是 m_x 将有一个不可预测的值。有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 person(int a,int b ,int c) :m_a(a), m_b(b), m_c(c) 7 { 8 9 } 10 int m_a; 11 int m_b; 12 int m_c; 13 }; 14 void test01() 15 { 16 person p(30,20,10); 17 cout << "m_a = " << p.m_a << endl 18 << "m_b = " << p.m_b << endl 19 << "m_c = " << p.m_c << endl; 20 21 } 22 int main() 23 { 24 test01(); 25 }
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
例如:
1 class A {}
2 class B
3 { A a; }
B类中有对象A作为成员,A为对象成员
那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 class phone 5 { 6 public: 7 string m_pname;//创建一个名为m_pname 的string型变量 8 phone(string pname) 9 { 10 m_pname = pname; 11 cout << "phone的构造函数调用" << endl; 12 } 13 ~phone() 14 { 15 cout << "phone的析构函数调用" << endl; 16 } 17 }; 18 class person 19 { 20 public: 21 person(string name,string pname):m_name(name), m_phname(pname) 22 { 23 cout << "person的构造函数调用" << endl; 24 } 25 ~person() 26 { 27 cout << "person的析构函数调用" << endl; 28 } 29 phone m_phname;//m_phname 为phone类的变量 30 string m_name; 31 }; 32 void test01() 33 { 34 //当类中成员是其他类对象时,我们称该成员为 对象成员 35 //构造的顺序是 :先调用对象成员的构造,再调用本类构造 36 //析构顺序与构造相反,因为栈区有先进后出的原则 37 person p("张三","iphone"); 38 cout << "名字 :" << p.m_name << ",手机 :" << p.m_phname.m_pname << endl; 39 } 40 int main() 41 { 42 test01(); 43 }
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
-
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 //所有对象都共享同一份数据 7 // 编译阶段就分配了内存 8 //类内声明,类外初始化操作 9 static int m_a;//类内声明,编译阶段分配了内存 10 //静态成员变量也是有访问权限的 11 private: 12 static int m_b; 13 }; 14 int person::m_a = 100;//类外初始化 15 int person::m_b = 200; 16 void test01() 17 { 18 person p; 19 cout <<"修改前p.m_a :" << p.m_a << endl; 20 person p2; 21 p2.m_a = 200; 22 cout << "利用p2修改后p.m_a :" << p.m_a << endl;//所有对象都共享同一份数据 ,当一个对象对数据进行修改,其他的对象共享的数据也变成了修改后的数据 23 cout << "p2.m_a :" << p2.m_a << endl; 24 } 25 void test02() 26 { 27 //静态成员变量 不属于某一个对象上 所有对象都共享同一份数据 28 //因此静态成员变量有两种的访问方式 29 30 //第一种:通过对象进行访问 31 /* person p; 32 cout << p.m_a << endl;*/ 33 //第二种:通过类目进行访问 34 cout << person::m_a << endl; 35 // cout << person::m_b << endl; 类外访问不到私有的静态成员变量 36 } 37 int main() 38 { 39 //test01(); 40 test02(); 41 }
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
1 #include <iostream> 2 using namespace std; 3 //静态成员函数 4 //所有对象共享同一个函数 5 //静态成员函数只能访问静态成员变量 6 class person 7 { 8 public: 9 //静态成员函数 10 static void func() 11 { 12 m_a = 100;//静态成员函数可以访问静态成员变量,所有对象共享的 13 m_b = 200;//静态成员函数不可以访问非静态成员变量,无法区分到底是哪个对象的m_b属性;运行时会报错 14 cout << "static void func()的调用" << endl; 15 } 16 static int m_a;//静态成员变量 17 private: 18 static void func2() 19 { 20 cout << "static void func2()的调用" << endl; 21 } 22 }; 23 int person::m_a = 0;//静态成员变量 24 int m_b = 1;//非静态成员变量 25 //有两种访问方式 26 void test01() 27 { 28 //通过对象进行访问 29 person p; 30 p.func(); 31 //通过类名进行访问 32 person::func(); 33 //person::func2();//错误操作,私有作用域下类外不可访问 34 } 35 int main() 36 { 37 test01(); 38 }
C++对象模型和this指针
4.3.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
- 静态成员函数
-
1 #include <iostream> 2 using namespace std; 3 //成员变量和成员函数是分开存储的 4 class person 5 { 6 int m_a;//非静态成员变量 属于类的对象的 7 static int m_b;//不属于类的对象上 8 void func()//非静态成员函数 不属于类的对象上 9 { 10 11 } 12 static void func2()//静态成员函数 不属于类的对象上 13 { 14 15 } 16 }; 17 int person::m_b = 100; 18 void test01() 19 { 20 person p; 21 //空对象占内存:1 22 //c++编译器会给每个空对象分配一个字节的内存空间,是为了区分空对象占内存的位置 23 //每个空对象也应该有个独一无二的内存地址 24 cout << "size of p =" << sizeof(p) << endl 25 << "size of &p =" <<&p ; 26 } 27 void test02() 28 { 29 person p; 30 cout << "size of p =" << sizeof(p) << endl 31 << "size of &p =" << &p; 32 } 33 int main() 34 { 35 //test01(); 36 test02(); 37 }
this指针概念
通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
指向正在调用的成员函数
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 person(int age) 7 { 8 this -> age = age;//系统认为三个age是同一份和下面定义的 int age (属性age)不是一个东西,只有加上this->才能区分 9 } 10 //this指针指向被调用的成员函数所属的对象,也就是指向现在调用的p1 11 int age; 12 person& personaddage(person& p)//如果 person& personaddage中不加&,那么调用函数时就会调用拷贝构造函数,return的是一个副本,然后当副本的p2和.personaddage(p1)发生关系,返回的又是一个新的副本p2,也就是相当于最后只有一个没用调用过然后.personaddage(p1)和一个.personaddage(p1)发生关系,最后结果就和p2、.personaddage(p1)一样,而不是p2.personaddage(p1).personaddage(p1).personaddage(p1);的结果40了 13 //用&的方式返回那么返回的是地址,不会调用拷贝构造函数,调用的一直都是最初的p2 14 { 15 this->age += p.age;//例如:“b+=c”,就是b = b+c的意思。 16 //调用test02时,this是指向p2的指针,而*this指向的就是p2这个对象的本体 17 return *this; 18 } 19 }; 20 //解决名称冲突 21 void test01() 22 { 23 person p1(18); 24 cout << "p1的年龄为:" << p1.age << endl; 25 } 26 //返回对象本身用*this 27 void test02() 28 { 29 person p1(10); 30 person p2(10); 31 //链式编程思想 32 p2.personaddage(p1).personaddage(p1).personaddage(p1);//p2.personaddage(p1)执行完后因为返回值是p2,所以继续和后面的).personaddage(p1)发生关系 33 cout << "p2的年龄为:" << p2.age << endl; 34 } 35 int main() 36 { 37 //test01(); 38 test02(); 39 }
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
1 #include <iostream> 2 using namespace std; 3 class person 4 { 5 public: 6 void showcalssname() 7 { 8 cout << "this is person class" << endl; 9 } 10 void showpersonage() 11 { 12 //报错原因是因为传入指针为NULL也就是0; 13 //为了防止传入空指针我们可以这样做 14 if (this == 0) 15 { 16 return; 17 } 18 cout << "age :" << this->m_age << endl;//调用属性三前面都默认加了this->指针,系统自动加,这里加上只是为了演示 19 } 20 int m_age; 21 }; 22 void test01() 23 { 24 person* p = 0;//因为这里指向空的地方,没有任何对象,所以this->没有指向一个确切的值,所以对象没用确定,哪里有年龄,所以调用p->showpersonage();会报错 25 //p -> showcalssname(); 26 p->showpersonage(); 27 } 28 int main() 29 { 30 test01(); 31 }
const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
1 #include <iostream> 2 using namespace std; 3 //常函数 4 class person 5 { 6 public: 7 //this指针的本质 是指针常量 指针的指向是不可以修改的,值是可以修改的 8 // this 指针相当于person* const this; ,再加一个const 就相当于const person* const this; 9 //在成员函数后面加const,修饰的是this指针,相当于常量this指针,让指针指向的值也不可以修改 10 void showperson() const 11 { 12 //this->m_a = 100;//类内的变量系统都默认加了this->修饰,加了const后不允许修改 13 //this = NULL;//this指针是不可以修改指针指向的 14 this->m_b = 100; 15 } 16 void func() 17 { 18 m_a = 100; 19 } 20 int m_a; 21 mutable int m_b;//加上关键字mutable使其变成 特殊变量,即使在常函数中,也可以修改这个值, 22 }; 23 void test01() 24 { 25 person p; 26 p.showperson();//this指针指向这里 27 } 28 //常对象 29 void test02() 30 { 31 const person p;//在对象前加const,变为常对象,不允许修改指针指向的值 32 // p.m_a = 100;//会报错 因为在常对象里面 33 p.m_b = 100;//mutable 修饰的特殊值,在常对象下也可以修改 34 p.showperson(); 35 // p.func();//会显示不兼容,因为函数func中是可以修改指针指向的值的,但是常对象下指针指向的值是不可以修改的 36 } 37 38 int main() 39 { 40 test01(); 41 test02(); 42 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具