一、初始化和清理的概念(了解)
对象的初始化和清理是两个非常重要的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化和清理操作,编译器也会给你增加默认操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。
-
当对象产生时,必须初始化成员变量;在对象销毁前,必须清理对象
-
初始化用构造函数,清理用析构函数,这两个函数是编译器调用
二、构造函数和析构函数(重点)
2.1.概念
构造函数和析构函数没有返回值,定义方法是函数名与类名一致
class Maker1() { public: // 构造函数:作用是初始化成员变量,是编译器去自动调用的 // 无参构造函数 Maker1() { cout << "构造函数" << endl; } // 析构函数:在对象销毁前,编译器调用析构函数 ~Maker1() { cout << "析构函数" << endl; } }; void test() { Maker1 m; // 此时会打印“构造函数” } // test函数结束时,会打印“析构函数”
class Maker2() { public: // 构造函数:作用是初始化成员变量,是编译器去自动调用的 // 有参构造函数 Maker2(char *p, inr age) { cout << "构造函数" << endl; pName = (char*)malloc(strlen(p) + 1); strcpy(pName, p); mAge = age; } void printMaker2() { cout << "name:" << pName << " age:" << mAge << endl; } // 此时构造函数中创造的p堆区空间并未被释放,需要我们定义一个析构函数 ~Maker2() { cout << "析构函数" << endl; // 释放堆区空间 if(pName != NULL) { free(pName) pName = NULL; } } private: char *pName; int mAge; }; void test() { Maker2 m; // 此时会打印“构造函数” } // test函数结束时,会打印“析构函数”
2.2. 注意项
有对象产生必然会调用构造函数,有对象销毁必然会调用析构函数
-
注意1:构造函数和析构函数必须是公有权限
-
注意2:构造函数可以重载
-
注意3:构造函数和析构函数没有返回值,不能用void。构造函数可以有参数,析构函数没有参数
-
注意4:有多少个对象产生就会调用多少次构造函数,有多少个对象销毁就会调用多少次析构函数
2.3.默认的构造和析构
编译器默认提供一个构造函数和析构函数,函数体都为空
2.4.拷贝构造函数
1)什么是拷贝构造
拷贝构造函数是在面向对象编程中的一个特殊函数,用于创建一个对象,其内容与另一个已存在的对象相同。通常情况下,拷贝构造函数会在对象被复制时被调用,用于初始化新对象,使其内容与原始对象相同。
拷贝构造函数的作用是创建一个新对象,并将另一个对象的内容复制到新对象中。这在需要不修改原始对象的情况下创建其副本时非常有用。
拷贝构造函数通常用于以下情况: 1. 当一个对象作为参数传递给函数,并且需要创建该对象的副本以在函数内部使用时。 2. 当一个对象被赋值给另一个对象时,即对象之间的赋值操作。 3. 当使用对象初始化另一个对象时,如在声明对象时。
class Maker { public: Maker() { cout << "无参构造函数" << endl; a = 20; } // 拷贝构造函数 Maker(const Maker &m) { cout << "拷贝构造函数" << endl; a = m.a } ~Maker() { } private: int a; }; void test() { Maker m1; // 用一个已有的对象去初始化另一个对象 Maker m2(m1); // 此时会打印“拷贝构造函数” }
2)编译器提供了默认的拷贝构造函数
默认拷贝构造函数会进行成员变量的简单拷贝
3)拷贝构造函数中的形参要用引用
// 如果拷贝构造函数中的形参不是引用 class Maker { public: Maker(int a); { ma = a; } // Maker m4(m1); Maker(const Maker m) // 由于没有引用,此时编译器会把这串代码转成:const Maker m = m1; // 而对于代码const Maker m = m1;编译器会将它看成const Maker m(m1); // 于是进行死循环 { cout << "拷贝构造函数" << endl; } }; Maker m1(10); // 调用有参构造函数 Maker m4(m1); // 调用拷贝构造 Maker m5 = m1; // 调用拷贝构造
三、构造函数的分类及调用
3.1.分类
-
按照参数
-
无参构造函数
-
有参构造函数
-
-
按照类型
-
普通构造函数
-
拷贝构造函数
-
3.2.类默认提供了哪些函数
-
默认的构造函数
-
默认的析构函数
-
默认的拷贝构造函数
-
默认的赋值函数
3.3.调用
Maker m1(10); // 调用有参构造函数 Maker m2 = Maker(10); // 调用有参构造 Maker m3 = 10; --> Maker m3=Maker(10) Maker m4(m1); // 调用拷贝构造 Maker m5 = m1; // 调用拷贝构造 Maker m6; m6 = m1; // 赋值操作
四、匿名对象
4.1.概念
匿名对象是在创建对象时没有指定对象名称的对象。它们通常用于临时的、一次性的操作,不需要在后续代码中引用或操作该对象。
4.2.作用
匿名对象的用途包括但不限于以下几点:
-
临时对象:匿名对象可以用于表示临时的中间结果或临时对象,避免创建不必要的变量。
-
简化代码:在某些情况下,使用匿名对象可以简化代码结构,使代码更加清晰和简洁。
-
减少内存开销:由于匿名对象是一次性的,不需要额外的内存空间来存储对象的名称,可以减少内存开销。
4.3.注意
-
匿名对象的声明周期通常很短,一旦超出作用域就会销毁。
-
因此在使用时,需要注意避免产生悬空指针或未定义行为
-
void test() { Maker(); // 匿名对象的声明周期在当前行 Maker(10); // 注意,如果对象有名字来接,就不是匿名对象 Maker m1 = Maker(); }
五、拷贝构造函数调用的时机(重点)
class Maker { public: Maker() { cout << "无参构造函数" << endl; } Maker(int a) { ma = a; cout << "有参构造函数" << endl; } Maker(const Maker &m) { cout << "拷贝构造函数" << endl; } ~Maker() { cout << "析构函数" << endl; } };
5.1.对象以值方式传给函数
void func(Maker m){} void test() { Maker m; func(m); // 会显示”拷贝构造函数“ }
5.2.用一个已有的对象去初始化另一个对象
void test() { Maker m1; Maker m2(m1);// 会显示“拷贝构造函数” }
5.3.函数的局部对象以值的方式从函数返回(需看编译环境)
Maker func() { Maker m; cout << "局部对象的地址: " << &m << endl; return m; } void test() { Maker m1 = func(); cout << "m1对象的地址:" << &m1 << endl; };
输出结果
-
vs Debug模式下
-
无参数构造函数 局部对象的地址:0x1 拷贝构造函数 析构函数 m1对象的地址为:0x2(不同) 析构函数
-
由于拷贝构造函数的位置是在析构函数的前面,所以 拷贝构造函数 是在“return m”这串代码发生的。
-
-
vs release模式下(不会调用拷贝构造)
-
无参数构造函数 局部对象的地址:0x1 m1对象的地址:0x1 析构函数
-
可以看出来,析构函数不是发生在func函数结束的时候,而是在main函数结束后,而且m1和m的地址相同
-
也就是说,func函数的m对象内存被保留下来,并赋值给m1
-
-
qt模式下
-
同release模式
-
六、拷贝构造函数的调用规则
-
1)如果程序员提供了有参构造,那么编译器不会提供默认构造函数(无参),但是会提供默认的拷贝构造
-
class Maker { public: Maker(int a) { } }; // Maker m; // 报错,没有无参构造函数可以接受 Maker m1(10); Maker m(m1); // 不会报错
-
-
2)如果程序员提供了拷贝构造函数,那么编译器不会提供默认的构造函数和默认的拷贝构造函数
七、多个对象的构造函数和析构函数
7.1.构造函数均为无参
1.如果类有成员对象,那么先调用成员对象的构造函数,再调用本身的构造函数
而析构函数的调用顺序反之
2.成员对象的构造函数调用和定义顺序一样
3.注意,如果有成员对象,那么实例化对象时,必须保证成员对象的构造和析构能被调用
7.2.其他采用初始化列表
1)初始化列表
-
在主对象使用,指定调用成员对象的某个构造函数
-
初始化列表只能写在构造函数
2)注意项
-
可以使用对象的构造函数传递数值给成员对象的变量
-
如果使用了初始化列表,那么所有的构造函数都要写初始化列表
-
class Bmw { 构造函数、析构函数 }; class Bui { 构造函数、析构函数 }; class Maker { public: // 初始化列表,bmw对象 //Maker():bmw(10) //{ // //} //另一种形式 Maker(int a):bmw(a) { } Maker(const Maker &m):bmw() { } ~Maker() { } private: Bmw bmw; Bui bui; };
-
-
如果有多个对象需要指定调用某个构造函数,用逗号隔开
-
class Maker { public: // 初始化列表,bmw、bui两个对象 Maker(int a, int b. int c):bmw(a), bui(b, c) { } ~Maker() { } private: Bmw bmw; Bui bui; };
-
八、对象的深浅拷贝(重点难点)
8.0.须知
char* p1; char* p2; p1 = (char*)malloc(128); p2 = (char*)malloc(128); p1 = "小家伙"; strcpy(p2,p1); 这串代码中,p2和p1指向的地址一样吗,存储的地址一样吗,为什么
在这段代码中,p1和p2分别被分配了128个字节的内存空间,然后p1指向了一个字符串常量"小家伙"的地址,p2也指向了另一个128字节的内存空间的地址。在使用strcpy函数将p1指向的字符串复制到p2指向的内存空间时,实际上是将"小家伙"的内容复制到了p2指向的地址中,而不是改变p2本身的指向。 意思就是说,将p2指向的空间的内容修改成p1指向的内容 因此,p2和p1指向的地址是不一样的,存储的地址也是不一样的。
8.1.浅拷贝
就是默认拷贝构造函数;
深浅拷贝是面试的一个经典问题,也是常见的一个坑
8.2.浅拷贝出现的问题
调用默认拷贝函数,可能出现同一块空间被释放两次的情况
class Student { public: Student(const char *name, int age) { pName = (char*)malloc(strlen(name) + 1); strcpy(pName, name); Age = age; } ~Student() { if(pName != NULL) { free(pName); pName =NULL; } } private: char* pName; int Age; }; void test() { Student s1("笑话", 10); Student s2(s1); // 调用默认拷贝函数,报错:原因是同一块空间被释放两次 }
代码问题
1.第二个pName有没有空间,它存储的内容是不是和pName一样? 2.用strcpy是不是能创造新的空间,来存储原字符串,并使指针指向它?
1.有。是,只不过不是同一个东西,只是内容一样 2.不是。事实上,使用strcpy是将p1指向的内容赋值给p2指向的空间 所以使用strcpy前,p2必须提取申请空间
8.3.深拷贝解决浅拷贝问题
解决方案就是:自己写拷贝构造函数
Student(const Student &stu) { // 1.申请空间 pName = (char*)malloc(strlen(stu.pName) + 1); // 2.拷贝数据 strcpy(pName, stu.pName); age = stu.age; };
九、explicit关键字的应用(了解)
9.1.概念
explicit是C++中的一个关键字,*它用来修饰只有一个参数的类构造函数,以表明该构造函数是显式的,而非隐式的。*当使用explicit修饰构造函数时,它将*禁止类对象之间的隐式转换,以及禁止隐式调用拷贝构造函数。*
即禁止优化:Maker m = 10;
9.2.为什么需要explicit
explicit关键字的作用是确保类型转换的清晰和明确,避免不必要、可能引发错误的隐式转换。
9.3.使用explicit
在构造函数名前加上explicit
注意:构造函数只能有一个参数或者其他参数都有默认参数
class Maker { public: // 构造函数**只能有一个参数**或者**其他参数都有默认参数** explicit Maker(int a) { } };
十、new和delete申请堆区空间和释放堆区空间(重点)
10.1.为什么不用malloc和free
因为C语言的动态分配内存太复杂,太乱,不好用,而且给类对象指针申请空间时,C语言不会调用构造函数和析构函数
1)程序员必须确定对象的长度 2)malloc返回一个void*指针,C++不允许将void*赋值给其他任何指针,必须强转 3)malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功 4)用户在使用对象之前必须记住对它初始化,构造函数不能显示调用初始化(构造函数是有编译器调用),用户有可能忘记调用初始化函数
10.2.使用
Maker *m1 = new Maker; Maker *m2 = new Maker(10); // 可以传参
2)堆区申请和释放数组空间
-
1.new创建基础类型的数组
int *p = new int[10]; // 注意:如果new有中括号,delete也要加 delete[] p;
-
2.new创建对象数组
Maker *ms = new Maker[2];//调用无参构造 // 大部分编译器不支持这种写法(聚合初始化) Maker *m = new Maker[2]{Maker(10), Maker(20)};// 调用有参构造 delete[] ms; delete[] m;
十一、静态成员(重点)
11.1.概念
使用static关键字 修饰 类的 成员变量 ,那么该成员变量就被声明为“静态成员变量”
11.2.静态成员
11.2.1.静态成员分为:
-
静态成员变量
-
1)静态成员变量的生命周期是整个程序,作用域在类内
-
2)静态成员变量要在类内声明,类外初始化(私有权限的成员也可以初始化)
-
3)静态成员变量属于类,不属于对象,是所有对象共享
-
4)静态成员变量可以用类访问,也可以用对象访问
-
-
静态成员函数
-
1)所有成员共享同一个函数
-
2)静态成员函数只能访问静态成员变量
-
11.2.2.注意项
-
静态成员也有权限,如果为私有,类外也不可以访问
-
const修饰的静态成员变量,可以在 类内或类外 初始化(最好在类内初始化)
-
普通成员函数 可以访问 静态成员变量
十二、c++类对象模型(了解)
12.1.成员变量和函数的存储
-
1)空类的大小为1
-
因为编译器可以从内存更好区分对象
-
class Maker{}; // 大小为1
-
-
2)类的成员函数、静态成员变量、静态成员函数不占用;普通成员变量占用类的大小
-
class Maker { public: void func(); static int a; static void func(); }; // 依旧为1 int Maker::a = 100;
-
class Maker() { public: int a; };// 类的大小为4
-
推断出:c++类对象中的变量和函数是 分开存储 的
-
-
3)c++编译器对普通成员函数的内部处理(不传入参数却能直接使用成员变量)
12.2.this指针(重点难点)
12.2.1.前言
通过上例,我们得知:c++的数据和操作也是分开存储,并且每一个非内联成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
问题是:这一块代码是如何区分哪个对象调用自己的呢?
c++通过提供特殊的对象指针:this指针,解决上述问题。this指针指向被调用的成员函数**所属的对象**(谁调的,this指针就指向谁)
12.2.2.概念
this指针是隐含在对象的非静态成员函数的一种指针(当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this)
所以,this指针无需定义,直接使用即可
注意:1)静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量 2)this指针的指向不能改变,也就是说this是Maker*const this
12.2.3.用途
-
1)当形参和成员变量同名时,可用this指针区分
-
class Maker { public: Maker(int id) { this->id = id; } public: int id; };
-
-
2)在类的非静态成员函数中返回对象本身,可使用return *this(运算符重载时有用)
-
Maker &getMaker() { return *this; };
-
12.2.4.拓展
-
this指针指向的空间 没有 存储静态成员变量
-
需要防止 空指针 调用成员函数
-
class Maker() { public: void func() { if(this==NULL) { cout<< "this==NULL" << endl; return; } cout << a(this->a) << endl; } public: a = 20; private: int a; }; Maker*m = NULL; // 这里为空时,this指针也会为空,所以可以用来判断(不判断则报错) m->func();
-
12.3.常函数&&常对象
12.3.1.常函数
-
即用const修饰的成员函数
-
用const修饰成员函数时,const其实是修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量
-
当成员变量类型符前用 mutable 修饰时例外
public: Maker(int ID, int a) { this->ID = ID; this->a = a; } void putong() { this->ID = 10; this-a = 100; } void func()const { // this->ID = 10; // 不可以修改 this-a = 100; // 在常函数中 可以修改 } private: int ID; mutable int a;
12.3.2.常对象
常对象:在数据类型前面加上const
-
常对象不能改变普通成员变量的值
-
常对象不能调用普通成员函数
-
常对象可以调用常函数(普通对象也可以调用常函数)
-
常对象可以修改mutable修饰的成员
const Maker m(1,18); // 常对象 // m.ID = 100; // 常对象不能改变普通成员变量的值 // m.putong(); // 常对象不能调用普通成员函数 m.func(); // 常对象可以调用常函数 m.a = 500; // 常对象可以修改mutable修饰的成员
十三、友元(重点难点)
13.1.概念
1)友元概念
-
友元是赋予:全局函数、类的成员函数、类 有访问别的类的私有成员权限。
2)友元函数概念
-
友元函数是一种特权函数,c++允许这个特权函数访问私有成员
-
注意:友元函数 不是 类的成员函数
-
13.2.使用
0.语法
-
friend关键字只出现在声明处
-
其他类、类成员函数、全局函数都可以声明为友元
-
友元函数可访问对象任意成员属性,包括私有属性
1.全局友元函数
-
class Building { // 声明这个全局函数为Building类的友元函数 friend void Good(Building &bd); public: Building() { keting = "客厅"; woshi = "卧室"; } public: string keting; private: string woshi; }; void Good(Building &bd) { cout << bd.woshi << endl; // 可以访问 }
2.友元类
-
1)通过 传入参数 来访问类的私有成员
-
class Building { // 声明GoodF类 为Building类的友元类 friend class GoodF; public: Building() { keting = "客厅"; woshi = "卧室"; } public: string keting; private: string woshi; }; class GoodF { public: void Good(Building &bd) { // 可以访问 cout << bd.woshi << endl; } };
-
-
2)通过 类内指针 来访问类的私有成员
-
class Building { // 声明GoodF类 为Building类的友元类 friend class GoodF; public: Building() { keting = "客厅"; woshi = "卧室"; } public: string keting; private: string woshi; }; class GoodF2 { public: GoodF2() { pbu = new Building; } void Good(Building &bd) { // 可以访问 cout << pbu->woshi << endl; } ~GoodF2() { if(pbu != NULL) { delete pbu; pub = NULL; } } public: BUilding* pbu; };
-
3.类的友元成员函数(难点)
-
错误写法(编译器知道类的声明,不知道类的结构)
-
class Building { // 声明GoodF类 为Building类的友元类 friend void GoodF::func(Building &bud); // 编译器不知道这个类有没有func函数 public: Building() { keting = "客厅"; woshi = "卧室"; } public: string keting; private: string woshi; }; class GoodF { public: void func(Building &bd) { cout << bd.woshi << endl; // 报错,编译器知道类的声明,不知道类的结构 } };
-
-
正确写法
class Building; // 需要提前声明 class GoodF // 需要提前声明 { public: void func(Building &bd); // 这里需要提前声明Building类 }; class Building { // 声明GoodF类 为Building类的友元类 friend void GoodF::func(Building &bud); public: Building() { keting = "客厅"; woshi = "卧室"; } public: string keting; private: string woshi; }; // 函数实现放到全局 void GoodF::func(Building &bd) { cout << bd.woshi << endl; }
13.3.注意项
友元类注意
-
友元关系不能被继承
-
友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友
-
友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友
十四、单例模式(重点难点)
14.1.概念
单例模式是指:在内存中只会创建且仅创建一次对象的设计模式。
在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
14.2.实现思路
-
1)把无参构造函数和拷贝构造函数私有化
-
2)定义一个类内的静态成员指针
-
3)在类外初始化时,new一个对象
-
4)把指针的权限设置为私有,然后提供一个静态成员函数让外面获取这个指针
// 单例模式是一个类只能实例化一个对象 class Maker { private: // 1.把构造函数和拷贝构造私有化 Maker() // 这样就无法直接实例化对象 { } Maker(const Maker &m) { } public: static Maker *getMaker() { // 第三点也可以在类内申请空间,在类外把它赋值空指针nullptr; //if (!pMaker) { // pMaker = new Maker(); //} return pMaker; } private: // 在私有权限内定义,是因为防止唯一对象指针被赋值为空值 // 2.定义一个类内的静态成员指针 static Maker *pMaker; }; // 3.在类外初始化时,new一个对象 Maker *Maker::pMaker = new Maker;// 这里可以new,是因为有Maker::作用域,编译器这时把它当成类内 void test() { // Maker m; //无法直接实例化对象 Maker *m = Maker::getMaker(); // Maker *m2 = Maker::getMaker(); // 上面两地址一样 }
面试答题:单例模式 https://blog.csdn.net/weixin_41949328/article/details/107296517?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171411657516800215098961%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=171411657516800215098961&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-107296517-null-null.142^v100^pc_search_result_base3&utm_term=%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F&spm=1018.2226.3001.4187
###
Q:在没有对象下,为什么私有成员不是静态成员变量,在类外就不能给它赋值,而静态成员变量就可以 A:私有成员:私有成员是属于类的实例的,每个类的对象都有自己的私有成员变量。私有成员只能在类的成员函数中访问,外部代码无法直接访问或修改私有成员。如果没有类的对象实例,私有成员也不存在,因此在类外部无法给私有成员变量赋值。 静态成员:静态成员是属于类的,而不是类的实例。静态成员变量在类中只有一份拷贝,所有类的对象共享这一份拷贝。静态成员可以在类外部直接访问和修改,因为它属于类而不是类的对象。在没有类的对象实例的情况下,静态成员仍然存在,可以通过类名和作用域解析符号::来访问静态成员。