C++初阶到进阶_c++初阶组和高阶组年龄区别-程序员宅基地

技术标签: c++  开发语言  

文章目录

C++初级篇

一:C++入门

1.什么是C++

C++是在C的基础之上,容纳进去了面向对象编程的思想,并增加了许多有用的库,以及编程范式的一门通用的高级语言,广泛应用于编写系统软件、游戏引擎、图形界面应用程序和其他性能要求较高的软件系统。C++是C语言的扩展,具备了C语言的底层编程能力和高级语言特性。

以下是C++的一些特点和优势:

面向对象:C++支持面向对象编程范式,可以使用类、对象、继承、多态等特性来组织和管理代码,提高代码的可重用性和可维护性。

高性能:C++可以直接访问内存,并且提供了指针和引用等底层特性,使得其可以进行高效的内存管理和操作,同时允许直接操作硬件,因而具有卓越的性能。

泛型编程:C++支持泛型编程,通过模板(template)机制可以编写通用的代码,适用于多种数据类型的处理,提高了代码的复用性和灵活性。

强大的标准库:C++提供了丰富的标准库,包括容器、算法、输入输出、多线程等模块,可以方便地进行常见任务的编程,减少了开发工作量。

可移植性:C++的代码可以在多个平台上编译和运行,具备很强的可移植性,适用于开发跨平台的软件系统。

扩展性:C++支持使用C语言的库和代码,并且可以扩展为其他语言的接口,可以与其他语言进行混合编程,具有很好的兼容性和扩展性。

2.C++关键字

C++总计拥有63个关键字, C语言有32个关键字;不过,这一节,只是用于知晓C++有多少关键字,并不会对关键字进行具体的讲解,后面会细讲。

以下就是C++的63个关键字

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vKZGVrfQ-1688661160502)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

3.命名空间

3.1命名空间出现的原因

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

#include <iostream>

int rand = 10;

//C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决

int main()
{
    
print("%d\n", rand);
return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s3k10LC9-1688661160502)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这段代码编译后会报错,如下

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7NhlsbC6-1688661160502)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

其原因是,在iostream这个头文件中,有一个rand函数,而此刻你又重新定义了一个rand变量,两者发生命名冲突。

3.2命名空间的定义

定义命名空间,需要用到namespace关键字,后面跟命名空间的名字,然后接一对花括号或曰大括号,即{}。{}中即为命名空间的成员。

//mhzly是命名空间的名字

//1.正常的命名空间定义
namespace mhzly
{
    
	//命名空间中可以定义变量/函数/类型
	int rand = 10;

	int Add(int left, int right)
	{
    
		return left + right;
	}

	struct Node
	{
    
		struct Node* next;
		int val;
	};
}

//2.命名空间可以嵌套
namespace N1
{
    
	int a;
	int b;
	int Add(int left, int right)
	{
    
		return left + right;
	}

	namespace N2
	{
    
		int c;
		int d;
		int Sub(int left, int right)
		{
    
			return left - right;
		}
	}
}

//3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成到同一个命名空间中。
//即下面的N1会和上面的N1合并起来。

namespace N1
{
    
	int Mul(int left, int right)
	{
    
		return left * right;
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKZtc3tI-1688661160502)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是,一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。

3.3命名空间的使用

该如何使用命名空间中的成员呢?

#include <stdio.h>
namespace mhzly
{
    
	//命名空间中可以定义变量/函数/类型
	int a = 0;
	int b = 1;

	int Add(int left, int right)
	{
    
		return left + right;
	}

	struct Node
	{
    
		struct Node* next;
		int val;
	};
}

int main()
{
    
	printf("%d\n",  a);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Snpc3jIR-1688661160503)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面的这段代码会导致编译报错,如下。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmUgnnPE-1688661160503)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

因为我们是在命名空间中定义的a,他被封装了起来,如果不是正常调用的话,编译器是找不到这个命名空间中的a的。

命名空间的使用有以下三种方式:

3.3.1加命名空间名称及作用域限定符
#include <stdio.h>
namespace mhzly
{
    
	//命名空间中可以定义变量/函数/类型
	int a = 0;
	int b = 1;

	int Add(int left, int right)
	{
    
		return left + right;
	}

	struct Node
	{
    
		struct Node* next;
		int val;
	};
}

int main()
{
    
	printf("%d\n",  mhzly::a);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0pRuzcWR-1688661160503)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.3.2使用using将命名空间中某个成员引入

这种方法就是只将命名空间中部分变量解放出来,从而让全局可以看到。

#include <stdio.h>
namespace mhzly
{
    
	//命名空间中可以定义变量/函数/类型
	int a = 0;
	int b = 1;

	int Add(int left, int right)
	{
    
		return left + right;
	}

	struct Node
	{
    
		struct Node* next;
		int val;
	};
}

using mhzly::b;

int main()
{
    
	printf("%d\n",  mhzly::a);
	printf("%d\n", b);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7S0ng89f-1688661160503)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.3.3使用using namespace命名空间名称,将整个命名空间解放,使其中的所有元素完全暴露在全局中。
#include <stdio.h>
namespace mhzly
{
    
	//命名空间中可以定义变量/函数/类型
	int a = 0;
	int b = 1;

	int Add(int left, int right)
	{
    
		return left + right;
	}

	struct Node
	{
    
		struct Node* next;
		int val;
	};
}

using namespace mhzly;

int main()
{
    
	printf("%d\n",  mhzly::a);
	printf("%d\n", b);
	Add(10, 20);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2Aa2I5G-1688661160503)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.C++的输入输出

C++虽然兼容C语言的输入输出方式,但是C++也有属于自己的一套输入输出方式,并且比C语言提供的更加直观,便捷及易用。

#include <iostream>

//std 是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;

int main()
{
    
	cout << "hello,world!!!" << endl;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6AGATPkh-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上述这段代码的运行结果就是在屏幕上将hello,world!!!打印出来,如下图。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9I1boNcF-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

4.1说明:

1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含头文件,以及按照命名空间使用方法使用std。

2.cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在头文件中。

  1. << 是流插入运算符, >>是流体去运算符。

4.使用C++输入输出方式更加的方便,不需要像printf、scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。

5.实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识后续会说到。

需要注意的是,早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需要包含对应的头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h。

#include <iostream>

using namespace std;

int main()
{
    
	int a;
	double b;
	char c;

	//可以自动识别变量的类型
	cin >> a;
	cin >> b >> c;

	cout << a << endl;
	cout << b << "  " << c << endl;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v8tk7JcJ-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.2 std命名空间的使用惯例:

std是C++标准库的命名空间,如何展开std使用更合理?

1.在日常练习中,可以直接using namespace std,将std完全展开,这样很方便

2.using namespace std 展开,标准库就全部暴露了,如果定义了跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多,规模较大,就会容易出现冲突。所以在项目中,不要全部展开,而是可以像std::cout这样使用时直接指定命名空间 + using std::cout展开常用的库对象/类型等方式。

5.缺省参数

5.1缺省参数的定义

C++的缺省参数是一种函数参数的特性,它允许在函数声明中为参数提供默认值。当调用该函数时,如果对应的参数没有提供实际的值,那么就会使用默认值来代替。

使用缺省参数可以使函数的调用更加灵活,可以减少在不同调用中重复编写相同的参数值。它使得函数在有默认行为的情况下可以以更简洁的方式被调用。

缺省参数的定义是在函数声明或函数定义中为参数赋予初始值。以下是一个使用缺省参数的函数声明示例。

void greet(const std::string& name, const std::string& greeting = "Hello");

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kzWwpieh-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

在上面的示例中,函数greet有两个参数,其中第二个参数greeting有一个缺省参数值"Hello"。这意味着如果调用greet时不提供第二个参数的值,将默认使用"Hello"作为参数值。

下面时几个使用缺省参数的函数调用的示例:

greet("Alice");             // 使用默认的 greeting 参数值,输出:Hello Alice
greet("Bob", "Hi");         // 提供了自定义的 greeting 参数值,输出:Hi Bob

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-guYcBnaO-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的时,在函数定义中给出的缺省参数值只能在函数声明中指定一次。这句话的意思是,在函数定义中指定的缺省参数值只能在函数声明中指定一次。换句话说,如果一个函数在多个地方进行了声明(例如,函数的原型在头文件中声明,而函数的实现在源文件中定义),那么缺省参数值只能在其中一个声明中指定。

还需要知道的是,缺省参数不能在函数声明和定义中同时出现。

另外,在函数重载的情况下,缺省参数的使用可能会导致歧义,因此在设计函数接口时应谨慎使用缺省参数,以避免产生歧义和不明确的调用。

5.2缺省参数分类
5.2.1全缺省参数

全缺省参数是指函数的所有参数都具有默认值的情况。在C++中,可以通过给函数的所有参数提供默认值来实现全缺省参数。

全缺省参数允许函数在调用时不传递任何参数,使得函数的调用更加简洁和灵活。当函数的所有参数都有默认值时,可以选择性地省略参数,使用默认值替代

void Func(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpTyxQ27-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

5.2.2半缺省参数

半缺省,指的是在所有参数中只有部分参数给了默认值。但是不是随便的给默认值的,只能时从右往左依次给出,不能间隔给。

void Func(int a, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZVxRa8I-1688661160504)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.函数重载

6.1函数重载概念

在自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

函数重载,是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

其特点是:

  1. 函数名称相同,但参数列表必须不同。参数列表可以包括参数的类型、个数和顺序。
  2. 返回类型与函数重载无关,只根据函数名称和参数列表进行匹配。
  3. 函数重载可以在同一个类中定义,也可以在不同的类中定义。
  4. 函数重载使程序更加灵活,能够根据不同的参数类型自动选择合适的函数进行调用。
#include <iostream>

using namespace std;

//1.参数类型不同
int Add(int left, int right)
{
    
    cout << "int Add(int left, int right)" << endl;

    return left + right;
}

doutble Add(double left, double right)
{
    
    cout << "double Add(double left, double right)" << endl;
    
    return left + right;
}

//2.参数个数不同
void f()
{
    
    cout << "f()" << endl;
}

void f(int a)
{
    
    cout << "f(int a)" << endl;
}

//3.参数类型顺序不同
void f(int a, char b)
{
    
    cout << "f(int a, char b)" << endl;
}

void f(char b, int a)
{
    
    cout << "f(char b, int a)" << endl;
}

int main()
{
    
    Add(1, 2);
    Add(1.0, 2.0);

    f();
    f(1);

    f(1, 'a');
    f('a', 1);

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VdE0oz3I-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面代码的运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5oCVm2gZ-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

6.2 C++支持函数重载的原理 --名字修饰

为什么C++支持函数重载,而C语言不支持函数重载呢?

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

1.实际项目中,通常是由多个头文件和多个源文件构成,如果有C语言的基础,就可以知道,如果当前a.cpp中调用了b.cpp中定义Add函数时,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。

2.所以链接阶段就是专门处理这种问题,连接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。

3.链接时,面对Add函数,链接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。

4.以下采用linux下g++的修饰规则演示,通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成[_Z+函数长度+函数名+类型首字母]。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLAq5tVP-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3m0yqLU-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

到了这里应该就能知道为什么C语言没办法重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

如果两个函数的函数名一样,参数一样,只有返回值不同是不能构成重载的,因为调用时编译器没办法区分。

6.引用

6.1 引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,引用变量和其所引用的变量公用同一块内存空间。

比如:李逵, 在家叫铁牛,在江湖上叫黑旋风。

在C++中,引用是对变量的别名,它提供了一种通过不同的名称访问同一内存位置的方法。

6.2 如何创建一个引用变量

类型& 引用变量名(对象名) = 引用实体。

void TestRef()
{
	int a = 10;
	int& ra = a; // <======定义引用的类型

	printf("%p\n", &a);
	printf("%p\n", &ra);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHADyqb8-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是,引用类型和引用实体或者说引用变量,这两者的数据类型必须相同。

6.3 引用特性

1.引用在定义时必须被初始化。

2.一个变量可以用多个引用。

3.引用一旦引用一个实体,在不能引用其他实体。

void TestRef()
{
	int a = 10;
	// int& ra;   //该语句编译时会报错

	int& ra = a;
	int& rra = a;
	printf("%p %p %p\n", &a, &ra, &rra);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CFgne8jU-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.4 引用搭配const

在C++中,将引用与const关键字一起使用可以创建常量引用或避免对引用变量进行修改。以下是引用搭配const关键字使用时需要注意的几个点:

常量引用:通过在引用声明中添加const关键字,可以创建常量引用。常量引用表示该引用所引用的变量是只读的,即不允许修改其值。常量引用主要用于函数参数传递,以确保传递的参数不会被修改。

避免非常量引用绑定到临时对象:在C++中,非常量引用不能直接绑定到临时对象。因此,如果试图将非常亮引用绑定到临时对象,会导致编译错误。但是,const引用可以绑定到临时对象,因为const引用不允许修改对象的值。

void TestConstRef()
{
	const int a = 10;

	//int &ra = a;  //该语句编译时会出错,因为a为常量。
	const int& ra = a;
	
	//int &b = 10;  //该语句编译时会出错,因为10是临时变量
	const int& b = 10;

	double d = 12.34;
	//int &rd = d;  //该语句编译时会出错,因为两者类型不同。
	const int& rd = d;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XO3biFzp-1688661160505)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.5 引用的使用场景

1.引用做参数。

引用做参数有以下好处:

使用引用作为参数有以下几个好处:

  1. 避免对象拷贝:当将对象作为参数传递给函数时,如果使用引用,可以避免进行对象的拷贝操作,提高程序的性能和效率。对象拷贝可能会涉及到内存的分配和复制大量数据,而使用引用可以直接操作原始对象,避免了额外的开销。
  2. 修改原始对象:通过引用传递参数,函数可以直接修改原始对象的值。在函数内部对引用所指的对象进行修改,这种修改是可见的,并且不需要返回值来传递修改后的结果。这样可以方便地对对象进行更新,减少了代码的复杂性。
  3. 支持返回多个值:通过引用参数,函数可以返回多个值。在函数内部,可以通过修改引用所指的对象来返回额外的值。这样可以避免使用指针或者返回结构体等复杂的方式来实现多个返回值的需求。
  4. 传递大型对象时的效率和内存优化:如果对象较大,将其作为引用参数传递可以避免对象的拷贝,减少内存的使用和传递的开销。
void Swap(int& left, int& right)
{
    
	int temp = left;
	left = right;
	right = temp;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sZeOuCC1-1688661160506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.引用做返回值

当使用引用作为函数的返回值时,需要注意以下几点

  1. 避免返回局部变量的引用:不要返回指向函数内部局部变量的引用,因为局部变量在函数执行完毕后会被销毁,返回其引用将导致悬空引用,产生未定义行为。
  2. 避免返回临时对象的引用:不要返回指向临时对象的引用,例如在函数内部创建的临时对象。这些临时对象在函数执行完毕后会被销毁,返回其引用同样会导致悬空引用。
  3. 返回引用时需确保引用有效性:确保返回的引用所指向的对象在函数外部仍然有效。这意味着返回的引用要么是函数外部的静态变量、全局变量,或者是函数参数中传入的对象的成员。
  4. 避免返回引用时的拷贝:返回引用时,应该避免返回指向大型对象的引用,以避免产生额外的拷贝操作。如果需要返回大型对象,可以考虑使用指针或者智能指针。
  5. 返回引用时需要提供足够的访问权限:确保返回的引用所指向的对象具有足够的可访问性,以允许在函数外部使用该引用进行操作。
int& Count()
{
    
	static int n = 0;
	n++;
	// ...
	return n;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PjmIqu5q-1688661160506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

#include <iostream>
using namespace std;

int& Add(int a, int b)
{
    
	int c = a + b;
	return c;
}

int main()
{
    
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is : " << ret << endl;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yC7c67tt-1688661160506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面这段代码的运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0GGZ2Xqa-1688661160506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

因为在函数返回时,出了函数作用域后,其变量的栈空间被系统回收了。但是内存还在,所以返回的时内存地址。

简单的说,就是函数返回时,除了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

6.6 传值、传引用效率比较

如果以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份零时的拷贝,因此使用值作为参数或者返回值,效率非常低下,尤其是当参数或者返回值类型非常大时,效率就更低。

6.6.1值和引用作为返回值类型的性能比较
#include <iostream>
using namespace std;

struct A {
    
    int a[1000] = {
     0 };
};

  void TestFunc1(A a) {
    }

  void TestFunc2(A& a) {
    }

 void TestRefAndvalue() {
    
        A a;

        size_t begin1 = clock();
        for (size_t i = 0; i < 10000; ++i)
        {
    
            TestFunc1(a);
        }
        size_t end1 = clock();

        size_t begin2 = clock();
        for (size_t i = 0; i < 10000; ++i)
        {
    
            TestFunc2(a);
        }
        size_t end2 = clock();

        //分别计算两个函数运行结束后的时间
        cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
        cout << "TestFunc1(A&)-time:" << end2 - begin2 << endl;
    }
    int main()
    {
    
        TestRefAndvalue();
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BopuOSI8-1688661160506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这段代码的运行结果如下,由此我们就能看出传值的效率较低。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1fOmN9gH-1688661160506)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

6.6.2值和引用作为返回值类型的性能比较
#include <time.h>
#include <iostream>
using namespace std;

struct A {
    
    int a[1000] = {
     0 };
};

A a;
  A TestFunc1() 
  {
    
      return a;
  }

  A& TestFunc2() 
  {
    
      return a;
  }

 void TestReturnByRefAndvalue() {
    

     //以值作为函数的返回值类型
        size_t begin1 = clock();
        for (size_t i = 0; i < 100000; ++i)
        {
    
            TestFunc1();
        }
        size_t end1 = clock();

        //以引用作为函数的返回值类型
        size_t begin2 = clock();
        for (size_t i = 0; i < 100000; ++i)
        {
    
            TestFunc2();
        }
        size_t end2 = clock();

        //分别计算两个函数运行结束后的时间
        cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
        cout << "TestFunc1(A&)-time:" << end2 - begin2 << endl;
    }
    int main()
    {
    
        TestReturnByRefAndvalue();
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7r7FgA66-1688661160507)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果是:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GDOayLoV-1688661160507)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

6.7引用和指针的区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体公用同一块空间。但是在底层实现上实际是有空间的,因为引用时按照指针方式来实现的。

引用和指针的不同点:

1.引用概念上定义一个变量的别名,指针存储一个变量地址

2.引用在定义时必须初始化,指针没有要求。

3.引用在初始化时引用一个实体后,就不能在引用其他实体,而指针可以在任何时候指向任何一个同类型实体。

4.没有NULL引用,但有NULL指针

5.在sizeof中含义不同:引用结果为引用类型大小,但指针始终是地址空间所在字节个数(32位平台下占4个字节)

6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。

7.有多级指针,但是没有多级引用

8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理。

9.引用比指针使用起来更加安全。

8.内联函数

8.1内联函数的概念

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数可以提升程序运行的效率。

8.2内联函数的特性

1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。

2.inline对于编译器来说只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就找不到了。

9.auto关键字(C++11)

随着程序愈发复杂,程序中可能用到的类型也愈发复杂,经常体现在:

1.类型难以拼写。

2.含义不明确导致容易出错

#include <string>
#include <map>

int main()
{
	std::map<std::string, std::string> m{
   {"apple", "苹果"}, { "orange", "橘子" }, { "pear", "梨" };

	std::map<std::string, std::string> ::iterator it = m.begin();
	while (it != m.end())
	{
		// ....
	}
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YfxwNixS-1688661160507)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

std::map<std::string, std::string>是一个类型,但是该类型太长了,特别容易写错。虽然我们可以通过typedef给类型取别名,如:

#include <string>
#include <map>

typedef std::map<std::string, std::string> Map;
int main()
{
	Map m{
   {"apple", "苹果"}, { "orange", "橘子" }, { "pear", "梨" };

	Map ::iterator it = m.begin();
	while (it != m.end())
	{
		// ....
	}
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7R1p1dz-1688661160507)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

使用typedef给类型取别名确实可以简化代码,但是typedef也会遇到新的问题。

typedef char* pstring;
int main()
{
	const pstring p1 = nullptr;
	const pstring* p2 = nullptr;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QuRtnIVD-1688661160507)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

9.2 auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

#include <iostream>

using namespace std;

int TestAuto()
{
	return 10;
}

int main()
{
	int a = 10; 
	auto b = a;
	auto c = 'a';
	auto d = TestAuto();

	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vmujsK4B-1688661160507)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是,使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的占位符,编译器在编译期会将auto替换为变量实际的类型。

9.3 auto的使用细则

1.auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto* 没有任何区别,但是auto声明引用类型时则必须加上取地址符&。

#include <iostream>

using namespace std;

int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	auto& c = x;

	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	
	*a = 20;
	*b = 30;
	c = 40;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Be04wL7x-1688661160508)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果是:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qMyicgwE-1688661160508)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

2.在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只会对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
	auto a = 1, b = 2;
	auto c = 3, d = 4.0;  //改行代码会编译失败,因为c和d的初始化表达式类型不同 
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJmFsLUm-1688661160508)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

9.3 auto不能推导的场景

1.auto不能作为函数的参数

//此处代码会编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u1mekfQ0-1688661160508)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.auto不能用来声明数组

void TestAuto()
{
	int a[] = { 1, 2, 3 };
	auto b[] = { 4, 5, 6 };
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNV4fpQm-1688661160508)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

10.基于范围的for循环(C++11)

10.1范围for循环的语法

在C++98中如果要遍历一个数组,可以按照以下方式进行:

#include <iostream>

using namespace std;

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
	{
		array[i] *= 2;
	}
	for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
	{
		cout << *p << endl;
 	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hza33Olq-1688661160508)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

对于一个有范围的集合而言,由程序员来说明循环的范围有些多余,有时候还容易犯错误。因此C++中引入了基于范围的for循环。for循环后的括号由冒号 “ : ” 分为两个部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

#include <iostream>

using namespace std;

void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (auto& e : array) 
	{
		e *= 2;
	}
	for (auto e : array)
	{
		cout << e << " ";
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22hneff7-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是:与普通循环类似,范围for循环也可以用continue来结束本次循环,也可以用break来跳出整个循环。

10.2 范围for的使用条件

1.for循环迭代的范围必须是确定的

对于数组来说,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

#include <iostream>

using namespace std;

void TestFor(int array[])
{
	for (auto& e : array)
		cout << e << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pdkMTHFa-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面的代码有问题,因为for的范围不确定。

11.指针空值nullptr(C++11)

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们一般是用以下的方法对其进行初始化。

void TestPtr()
{
	int* p1 = NULL;
	int* p2 = 0;

	// .....
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cgNU4nq6-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

NULL实际上是一个宏,在C的头文件(stddef.h)中,可以看到如下代码:

#ifdef NULL
#ifdef __cplusplus;
#define NULL   0
#else
#define NULL   ((void*)0)
#endif
#endif

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iozkVZIs-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

由以上的代码可以看到,NULL可能被定义为字面常量0,或者被定义为五类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

#include <iostream>

using namespace std;


void f(int)
{
	cout << "f(int)" << endl;
}

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9q8msGPo-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eX8kfFz4-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初中相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,则需要 对其进行强转(void*)0。

而nullptr不一样,它有着自己的类型 nullptr_t。这就使得其在进行类型检查时更加严格,可以避免类型混淆和错误的隐式转换。

需要注意的是:

1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新的关键字引入的

2.在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。

3.为了提高代码的健壮性,在后续表示指针空值时建议使用nullptr。

二:类和对象

1.面向过程和面向对象

面向过程和面向对象是两种不同的编程范式

面向过程编程是一种以过程为中心的编程方式,将程序看作是一系列顺序执行的操作步骤。他将问题分解为一系列的函数或过程,并通过调用这些函数或过程来完成任务。面向过程变成强调的是步骤和算法的顺序性,关注问题的解决过程。其更加关注算法和流程控制,注重函数和数据的分离,以实现代码的模块化和复用。

面向对象编程(OOP)是一种以对象为中心的编程方式,将程序看作是一组相互作用的对象集合。对象是具有状态(属性)和行为(方法)的实体,通过定义对象的类来描述对象的属性和行为,并通过对象之间的交互来解决问题。面向对象编程强调的是对象的抽象、封装、继承和多态等特性,关注问题的模型化和对象之间的关系。面向对象编程通过封装、继承和多态等机制提供了更好的代码组织结构和可扩展性,提高了代码的可维护性和可重用性。

2.类的引入

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如下面就是用C++中的struct实现的一个简单的栈。不过,C++更加喜欢使用class来实现类。

#include <corecrt_malloc.h>
#include <iostream>


typedef int DataType;
struct Stack
{
	void Init(size_t capacity)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(const DataType& data)
	{
		//扩容
		_array[_size] = data;
		++_size;
	}

	DataType Top()
	{
		return _array[_size - 1];
	}

	void Destory()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

	DataType* _array;
	size_t _capacity;
	size_t _size;
};

int main()
{
	Stack s;
	s.Init(10);
	s.Push(1);
	s.Push(2);
	s.Push(3);

	std::cout << s.Top() << std::endl;
	s.Destory();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k0z8RUP2-1688661160509)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.类的定义

class className
{
	//类体:由成员函数和成员变量组成
};  //一定要注意这个分号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjYAJA9e-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

class为定义类的关键字,className为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。

类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数成为类的方法或者成员函数。

类的两种定义方式:

3.1.类的两种定义方式

1.声明和定义全部放在类体中。

#include <iostream>
class Person
{
public:
	//显示基本信息
	void showInfo()
	{
		std::cout << _name << "_" << _sex << "_" << _age << std::endl;
	}

public:
	char* _name;  //姓名
	char* _sex;     //性别
	int     _age;    //年龄
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tIajCt3w-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是,在类中定义成员函数可能存在一些问题:

1.可见性问题:成员函数默认具有类的访问权限,可能会导致对于外部代码来说不必要的访问权限,破坏了封装性。

2.内联性问题:如果成员函数在类定义中实现,编译器可能会自动将其内联,导致代码膨胀和可读性降低。特别是对于较大的成员函数,内联可能会增加代码的大小并影响性能。

3.编译依赖问题:如果成员函数在类定义之前使用,会导致编译器无法识别成员函数的存在,从而导致编译错误。

4.可维护性问题:如果类的接口在类定义中随意修改,可能会导致依赖该接口的其他代码需要进行相应的更改,增加了代码的耦合性和维护成本。

3.2.类声明和定义分离,类声明放在.h文件中,成员函数放在.cpp文件中。
class Person
{
public:
	//显示基本信息
	void showInfo();
public:
	char* _name;  //姓名
	char* _sex;      //性别
	int     _age;      //年龄
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NuprQ17I-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

#include "person.h"
#include <iostream>
//显示基本信息,实现:输出 名字、性别、年龄
void Person::showInfo()
	{
		std::cout << _name << "_" << _sex << "_" << _age << std::endl;
	}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nnjvqk17-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是:在定义的时候需要在成员函数名前加类名。

3.3成员变量命名规则的建议
class Date
{
public:
	void Init(int year)
	{
		//这里的year到底是成员变量,还是函数形参?
		year = year;
	}
private:
	int year;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-waprwBkp-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

成员变量的命名应当选择具有描述和清晰意义的变量名称,使得代码更易于理解和维护。所以下面的代码是一个更好的选择:

class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
private :
	int _year;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HQ9yt0oQ-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

并不是说一定需要用_year来做区分,只要能区分的变量名称都是好的名称。

4.类的访问限定符及封装

4.1访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将接口提供给外部用户使用。

C++中的访问限定符有以下三种:

1.公有访问(public):公有成员在类的内部和外部都是可以访问的。它们可以被类的对象、类的成员函数和类的派生类访问。公有成员通常用于定义类的接口,即外部代码可以直接访问的成员。

2.私有访问(private):私有成员只能在类的内部被访问,对外部是不可见的,包括其派生类。私有成员通常用于封装类的内部实现细节,防止外部代码直接访问和修改类的内部状态。私有成员只能被类的成员函数访问。

3.保护访问(protected):保护成员在类的内部和派生类中可访问,但对于类的外部代码是不可见的。保护成员通常用于实现类的继承和派生,允许派生类访问基类的成员。保护成员在继承链中具有继承性,可以被派生类的成员函数和派生类的派生类访问。

在类中修饰的访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止。

class的默认访问权限为private,struct为public(因为struct需要去兼容C)

另外需要注意的是,访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

小题目:

C++中struct和class的区别是什么?

答案:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。

4.2封装

面向对象拥有三大特性:封装、继承、多态。

那么,什么是封装呢?

封装是将数据和操作数据的函数组成一个单元,形成一个类。封装的目的是将数据和相关的操作封装在一个对象内部,隐藏内部的实现细节,对外部提供统一的接口访问数据和执行操作。

5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符来指明成员属于哪个类域。

class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int   _age;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IK4olWaL-1688661160510)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

#include <iostream>
//这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
	{
		std::cout << _name << "_" << _gender << "_" << _age << std::endl;
	}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrBiLAVH-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.类的实例化

用类类型创建对象的过程,称为类的实例化

类是对对象进行描述的,是一个模型一样的东西,限定了类中有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。或者说类就像是设计图,而类实例化对象就像使用建筑设计图建造出房子。

一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类的成员变量。

7.类对象模型

7.1如何计算类对象的大小
#include <iostream>

class A
{
public:
	void PrintA()
	{
		std::cout << _a << std::endl;
	}
private:
	char _a;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PWVhxifM-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

问题:类中既可以用成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

7.2 C++中类的存储方式

1.成员变量存储在对象内存中:类的成员变量通常直接存储在对象的内存中。每个对象都有自己的的一份成员变量的副本。这些成员变量的大小和布局由编译器决定。

2.静态成员变量:静态成员变量在类的所有对象之间共享。它们被存储在数据段或全局静态存储区中,并在类的任何对象之间共享。全局静态区是在整个项目或者工程中共享的,而不限于单个文件。在C++中,全局静态变量和静态成员变量都存储在全局静态区中,它们在程序运行期间始终存在,并且可以被项目中的多个文件访问和共享。

3.成员函数:成员函数通常不存储在对象内存中。它们被视为类的共享代码,并在需要时通过对象访问。通常情况下,类的成员函数,存储在公共代码区。

一个类的大小,实际就是该类中“成员变量”之和,不过需要注意内存对齐。

注意空类的大小,空类较为特殊,编译器给空类一个字节来唯一标识这个类的对象。也就是说空类的对象,其大小为一个字节。

7.3结构体内存对齐规则

1.第一个成员在与结构体偏移量为0的地址处。

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数 = 编译器默认的第一个对齐数与该成员大小的较小值。

在visual stdio中默认的对齐数为8字节。

也就是说如果第一个成员是int的话,其对对齐数就是4(4 < 8)。然后下一个成员要对齐到4的整数倍地址处,即与结构体偏移量为4的地址处。下一个成员的对齐数依然是和默认对齐数比较。

3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

4.如果嵌套了结构的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

小问题:

什么是大小端?如何测试某台机器是打大端还是小端?

大小端是指在存储多字节数据时,字节的存储顺序。

大端: 高位字节存储在低地址,低位字节存储在高地址。

小端:低位字节存储在低地址,高位字节存储在高地址。

//高字节                            低字节
    00000000 00000000 00000000 00000001

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eEQieveG-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

union EndianTest {
    uint32_t value;
    uint8_t bytes[4];
};
EndianTest test;
test.value = 0x01020304;
if (test.bytes[0] == 0x01) {
    cout << "Big Endian" << endl;
} else {
    cout << "Little Endian" << endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bJOfI822-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

8.this指针

8.1 this指针的引出

我们首先来定义一个日期类:

#include <iostream>

using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date t1, t2;
	t1.Init(2023, 7, 1);
	t2.Init(2023, 7, 2);

	t1.Print();
	t2.Print();

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8HpHkPF-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NI0d6R8L-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

对于上述类,有一个小细节:

Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那当t1调用Init函数时,该函数是如何知道应该设置t1对象,而不是设置t2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作都是用户看不到的,即用户不用自己去传递,编译器自动完成。

8.2 this指针的特性

1.this指针的类型:类类型*const,即成员函数中,不能给this指针赋值。

2.只能在成员函数中的内部使用

3.this指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。

4.this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。

小题目:

1.this指针存在哪里?

2.this指针可以为空吗?

当一个对象调用某成员函数时会隐式的传入一个参数,这个参数就是this指针。this指针中存放的就是这个对象的首地址。编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址存放在了寄存器ECX中(VC++编译器是放在ECX中,其它编译器有可能不同)。也就是成员函数的其它参数正常都是存放在栈中。而this指针参数则是存放在寄存器中。
类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量。

在C++中,‘this’指针可以为空,但是不应为空。

//1.下面程序编译运行结果是?  A.编译报错  B.运行崩溃  C.正常运行
#include <iostream>

class A
{
public:
	void Print()
	{
		std::cout << "Print()" << std::endl;
	}

private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cIOLDh2R-1688661160511)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这段代码可以正常运行,因为this指针不存在对象中,而是直接由编译器在编译时确定成员函数的地址,在调用该函数时直接将该地址赋给this指针。也就是说全程不和对象产生关联,对象为空也不妨碍正常调用该成员函数。

//1.下面程序编译运行结果是?  A.编译报错  B.运行崩溃  C.正常运行
#include <iostream>

class A {
public:
	void Print()
	{
		std::cout << _a << std::endl;
	}
private:
	int _a;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjKEXBJb-1688661160512)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面这段代码运行崩溃,调用函数正常,但是该函数内部需要访问_a这个成员变量。而你又把对象置空了,一个空指针怎么访问内存呢?所以会崩溃报错。

9. 类的默认成员函数

如果一个类中什么成员都没有,简称为空类。但是空类中并不是什么都没有,因为编译器会自动帮其生成6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jVWkwUnO-1688661160512)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

10. 构造函数

10.1 概念

对于以下的Date类:

#include <iostream>

using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date t1, t2;
	t1.Init(2023, 7, 1);
	t2.Init(2023, 7, 2);

	t1.Print();
	t2.Print();

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTbqQxzl-1688661160512)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

对于Date类,可以通过Init公有方法对对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建的时候,就将信息设置进去呢?

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用。以保证每个数据都有一个合适的初始值,并且在对象整个声明周期内只调用一次。

10.2特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

1.函数名与类名相同。

2.无返回值。

3.对象实例化时编译器自动调用对应的构造函数。

4.构造函数可以重载。

class Date
{
public:
	//1.无参构造函数
	Date()
	{}

	//2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void TestDate()
{
	Date d1;//调用无参的构造函数

	Date d2(2023, 7, 1); //调用带参的构造函数

	//注意:如果无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xh3pAYs8-1688661160512)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

5.如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。

#include <iostream>

class Date
{
public:
	//如果用户显示定义了构造函数,编译器将不再生成

	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	void Print()
	{
		std::cout << _year << "-" << _month << "-" << _day << std::endl;
	}

private :
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date t1;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3QpQcaO-1688661160512)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GVyYyAe2-1688661160512)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

这段代码会报错没有合适的默认构造函数可用,这是因为我们显式的创建了构造函数,编译器就不会为我们在生成一个默认的构造函数。

而如果我们将自己创建的构造函数屏蔽后,编译器会自动帮我们生成一个默认的构造函数。

#include <iostream>

class Date
{
public:
	//如果用户显示定义了构造函数,编译器将不再生成

	//Date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	
	void Print()
	{
		std::cout << _year << "-" << _month << "-" << _day << std::endl;
	}

private :
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date t1;
	t1.Print();
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vxe8IFsP-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

但是这段代码的运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tlocvgl1-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

是一串随机值,这样看起来的话,默认构造函数挺没用的。虽然可以初始化对象,但是只能初始化成一串随机值。

C++把类型分成内置类型和自定义类型。内置类型就是语言本身提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序。就会发现编译器生成默认的构造函数会对自定义类型成员调用它的默认成员函数。

#include <iostream>

class Time
{
public:
	Time()
	{
		std::cout << "Time()" << std::endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	//基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	//自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WJkfiao3-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

也就是说,我们不用在显示的定义自定义类型的构造函数,因为编译器已经帮我们自动生成了。其实这里的自定义变量的成员函数可能是编译器默认生成的无参构造函数,也可能是其自身显示定义的构造函数。区别在于其有没有显示定义。

不过在C++11中也针对内置类型成员初始化后是随机值的问题,打了个补丁。即:内置类型成员变量在类中声明时可以给默认值。

#include <iostream>

class Time
{
public:
	Time()
	{
		std::cout << "Time()" << std::endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	//基本类型(内置类型)
	int _year = 2023;
	int _month = 7;
	int _day = 1;

	//自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WqJtW1Ly-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

也就是说,这时候的默认构造函数初始化后,会将对象的成员变量初始化为设置的默认值。

6.无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数,全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为时默认构造函数。

class Date
{
public:
	Date()
	{
		_year = 2023;
		_month = 7;
		_day = 1;
	}

	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private :
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Date t1;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AunA77s1-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这段代码会报错

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5h5u5ju-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

11.析构函数

析构函数时特殊的成员函数,其特征如下:

1.析构函数名是在其类名前面加上~。

2.无参数无返回值类型。

3.一个类只能有一个析构函数。若为显示定义,系统会自动生成默认的析构函数。值得注意的是,析构函数不像构造函数一样可以重载。

4.对象生命周期结束时,C++编译系统自动调用析构函数。

#include <malloc.h>
#include <cstdio>

typedef int DataType;

class Stack
{
public:
	Stack(int capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		//checkCapacity();
		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CoruEtcx-1688661160513)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

5.关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。

#include <iostream>

class Time
{
public:
	~Time()
	{
		std::cout << "~Time()" << std::endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	//基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;

	//自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kji5KBtx-1688661160514)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lQHnX4qX-1688661160514)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

也就是说,析构函数和构造函数一样也有内置类型和自定义类型的区分。内置类型,编译器会自定帮清理,而自定义类型编译器会调用其自身拥有的析构函数。如果其没有析构函数,那么就会报错。

12.拷贝构造函数

12.1概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象的时候,可否创建一个与已存在对象一模一样的新对象呢?

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新的对象时由编译器自动调用。

12.2特征

拷贝构造函数也是特殊的成员函数,其特征如下:

1.拷贝构造函数是构造函数的一个重载形式。

2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷的递归调用。

这是因为传值传递,传递的是该变量副本,然后我们定义了拷贝构造函数,在传值传递的时候,会调用这个构造函数去构造这个副本,然后这个构造函数的参数同样是传值传递,然后继续调用拷贝构造函数,由此无限递归调用。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//Date(const Date& d)  //正确写法
	Date(const Date d)  //错误写法:编译报错,会引发无穷递归调用。
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2(d1);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1yBo2rmC-1688661160514)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

#include <iostream>

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}

	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		std::cout << "Time::Time(const Time&)" << std::endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	//基本类型(内置类型)
	int _year = 2023;
	int _month = 7;
	int _day = 1;

	//自定义类型
	Time _t;
};

int main()
{
	Date d1;

	//用已经存在d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	//但Date类并没有显示定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数

	Date d2(d1);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYWVrRZo-1688661160514)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

浅拷贝或者说值拷贝。也就是说拷贝的值一样,但是内存中的对象只有一份。这会产生析构两次的问题,因为值有两份,或者说有两个指针指向了同一块内存空间,而每个指针都会析构一次。

#include <cstdio>
#include <malloc.h>
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u1U6mC7I-1688661160514)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面这段代码会报错,就是因为会有两次析构。

类中如果没有涉及到资源申请的时候,拷贝构造函数是否写都可以;但是一旦涉及到资源申请,及申请内存空间的时候,拷贝构造函数是一定要写的,否则就是浅拷贝。

  1. 拷贝构造函数的典型调用场景

1.使用已存在对象创建新对象。

2.使用参数类型为类类型对象

3.函数返回值类型为类类型对象。

13.赋值运算符重载

13.1运算符重载

C++中为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型operator操作符(参数列表)

注意:

1.不能通过连接其他符号来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型参数

3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+, 不能改变其含义

4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

  1. .* :: sizeof ?: . 以上五个运算符不能重载。
#include <iostream>
//全局的operator == 
class Date
{
public:
	Date(int year = 2023, int month = 7, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

//private:
	int _year;
	int _month;
	int _day;
};


//这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,该如何保证封装?
//可以用友元解决
//但是还是建议重载成成员函数
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

void Test()
{
	Date d1(2023, 7, 1);
	Date d2(2023, 7, 2);

	std::cout << (d1 == d2)<< std::endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHhg6jH6-1688661160514)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

class Date
{
public:
	Date(int year = 2023, int month = 7, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

    //这里的第一个参数是隐藏的this,指向调用函数的对象
	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}

//private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KRSVoxel-1688661160515)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

13.2赋值运算符重载

1.赋值运算符重载格式

参数类型:const T&,传递引用可以提高高传参效率。

返回值类型:T&,返回引用可以提高返回的效率,由返回值目的是为了支持连续赋值

检测是否自己给自己赋值

返回*this:要符合连续赋值的含义。

class Date
{
public:
	Date(int year = 2023, int month = 7, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaDKrXww-1688661160515)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.赋值运算符只能重逢成类的成员函数不能重载成全局函数

这是因为:赋值运算符也是类的默认成员函数,如果我们不显式实现,编译器会生成一个默认的。此时用户如果在类外自己实现了一个全局的赋值运算符重载,就会和类中生成的默认运算符重载冲突。

3.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

需要注意的是:内置类型成员变量是直接赋值的,而自定义类型成员需要调用对应类的赋值运算符重载完成赋值。

13.3 前置++和后置++重载
#include <iostream>

class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//前置++:返回+1之后的结果
	//注意:this指向的对象函数结束后不会销毁,故以引用的方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}


	//后置++
	//前置++和后置++都是一元运算符,为了让前置++与后置++区分
	//C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
	//注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需要在实现时先将this保存一份,然后给this+1
	//而temp是临时对象,因此只能以值的方式返回,不能返回引用

	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}

	void Print()
	{
		std::cout << _day << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	Date d1(2023, 7, 1);

	d = d1++;
	d.Print();

	d = ++d1;
	d.Print();

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UWZOZito-1688661160515)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果是:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EDWy78ql-1688661160515)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

14.const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该册灰姑娘元函数中不能对类的任何成员进行修改

#include <iostream>
class Date
{
public:
	//显式日期信息:年-月-日
	void Display()  const
	{
		std::cout << _year << "-" << _month << "-" << _day << "-" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sZenlnRN-1688661160515)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上面的代码其实就是下面的代码:

#include <iostream>
class Date
{
public:
	//显式日期信息:年-月-日
	void Display(const Date* this)
	{
		std::cout << this->_year << "-" <<this-> _month << "-" << this->_day << "-" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wmEBsuBE-1688661160515)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

小问题:

  1. const对象可以调用非const成员函数吗?

答:不可以。因为const对象被视为只读对象,为了确保对象的状态不被修改,编译器会阻止其对非const成员函数的调用。如果非const成员函数被声明为const成员函数,那么它可以被const对象调用。

  1. 非const对象可以调用const成员函数吗?

可以调用。因为非const对象既可以修改成员变量也可以只读成员变量。因此能够安全的调用const成员函数。在这种情况下,const成员函数被视为只读操作,不会修改对象的状态。

  1. const成员函数内可以调用其它的非const成员函数吗?

可以调用。const成员函数内部可以调用任何类型的成员函数,包括非const成员函数。这是因为在const成员函数内部,编译器会将this指针视为指向const对象的指针,确保对象的状态不被修改。

  1. 非const成员函数内可以调用其它的const成员函数吗?

可以。非const成员函数可以修改对象的状态,但也可以安全的调用const成员函数。在非const成员函数内部,编译器不会强制要求调用的成员函数是非const的,因为非const成员函数可以修改对象的状态,而调用const成员函数只涉及读取对象的状态。因此,非const成员函数可以调用const成员函数。

15.取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义,编译器默认会生成。且一般不需要重载。

16.构造函数一些进阶的知识

16.1构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lOMpbofq-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体可以多次赋值。

16.2初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1RBzoWYM-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是:

1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2.类中包含以下成员,必须放在初始化列表位置进行初始化:

引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};

class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		,_ref(ref)
		,_n(10)
	{}

private:
	A _aobj;        //没有默认构造函数
	int& _ref;      //引用
	const int _n;  //const
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X2IucC09-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

#include <iostream>
class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{
		std::cout << "Time()" << std::endl;
	}

private:
	int _hour;
};

class Date
{
public:
	Date(int day)
	{}

private:
	int _day;
	Time _t;
};

int main()
{
	Date d(1);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lBZL9x9Q-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果是:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SC2QvnUz-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

4.成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

#include <iostream>
class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{}

	void Print() {
		std::cout << _a1 << "  " << _a2 << std::endl;
	}
private:
	int _a2;
	int _a1;
};

int main()
{
	A aa(1);
	aa.Print();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4OFc1s8-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J4Wo6gDu-1688661160516)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

其原因就是C++中成员变量的初始化顺序是声明的顺序,而非初始化列表中的先后次序。这段代码中,首先用1去初始化_a2, 然后再用还是随机值的_a1去赋值_a2,然后再用1去赋值_a1。

16.3 explicit 关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

而explicit关键字的作用就是用于修饰单参数的构造函数,用于防止隐式类型转换。当构造函数声明为explicit时,它将只能被显式的调用,而不能被隐式地用于类型转换。

class Date
{
public:
	//1.单参构造函数,没有使用explicit修饰,具有类型转换作用
	//explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
		:_year(year)
	{}

	//2.虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用。
	//explicit修饰构造函数,禁止类型转换
	//explicit Date(int year, int month = 1, int day = 1)
	//	:_year(year)
	//	,_month(month)
	//	,_day(day)
	//{}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Date d1(2022);

	//用一个整形变量给日期类型对象赋值
	//实际编译器背后会构造一个无名对象,最后用无名对象给d1对象进行赋值
	//d1 = 2023;
	
	
	// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bTGR4aFs-1688661160517)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

17.static成员
17.1概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,是类的静态成员变量。用static修饰的成员函数,是静态成员函数,静态成员函数一定要在类外进行初始化。因为静态成员函数不属于任何特定的对象,而是属于整个类。所以他的初始化不能放在类的内部,而是需要在类外进行。

17.2特性

1.静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3.类静态成员即可用 类名::静态成员或者对象.静态成员来访问

4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5.静态成员也是类的成员,受到C++访问限定符的限制

6.非静态成员可以访问静态成员,因为静态成员属于整个类,而不是类的实例

18. 友元

友元提供了一种突破封装的方式,可以增加一些便利性,但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:友元函数和友元类。

18.1友元函数

问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数,也就是左操作数。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但是这样又会导致没办法访问类内成员,此时就可以通过友元函数来解决。operator>>同理。

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	//d1<<cout; ->d1.operator<<(&d1, cout); 不符合常规调用
	//因为成员函数第一个参数一定是隐藏的this,所有d1必须放在<<的左侧

	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q4D0nNid-1688661160517)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

友元函数可以直接访问类的私有成员,他是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

#include <iostream>

using namespace std;

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>> (istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}

int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-24dijJwU-1688661160517)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

友元函数可以访问类的私有和保护成员,但不是类的成员函数,也就不可能拥有this指针。

友元函数不能用const修饰,友元函数不能使用const修饰的原因是,const关键字表示函数不会修改类的成员变量。然而,友元函数被授权访问类的私有成员,包括可以修改它们。因此,将友元函数声明为const是不合适的。

友元函数可以在类定义的任何地方声明,不受类访问限定符的限制。

一个函数可以是多个类的友元函数。

友元函数的调用与普通函数的调用原理相同。

18.2友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元关系是单向的,不具有交换性。比如A是B的友元,但B不一定就是A的友元,除非在A中使用friead修饰。友元函数不能继承。

class Time
{
	friend class Date;  //声明日期类为时间类的友元类,则在日期类中就可以直接访问Time类中的私有成员变量。

public:
		Time(int hour = 0, int minute = 0, int second = 0)
			:_hour(hour)
			,_minute(minute)
			,_second(second)
		{}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	void SetTimeOfDate(int hour, int minute, int second)
	{
		//直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}

private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gWZ3Vr8P-1688661160517)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

19.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。不过内部类天然是外部类的友元类,即内部类可以自由的访问外部类的所有成员。

1.内部类可以定义在外部类的public、protected、private都是可以的

2.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名

3.sizeof(外部类)= 外部类,和内部类没有任何关系。

#include <iostream>

class A
{
private:
	static int k;
	int h;
public:
	class B  //B天生是A的友元
	{
	public:
		void func(const A& a)
		{
			std::cout << k << std::endl;
			std::cout << a.h << std::endl;
		}
	};
};


int A::k = 1;

int main()
{
	A::B b;

	b.func(A());

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eg1wWCiy-1688661160517)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHhLxxYA-1688661160517)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

20.匿名对象

#include <iostream>
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		std::cout << "A(int a)" << std::endl;
	}

	~A()
	{
	std:: cout << "~A()" << std::endl;
	}
private:
	int _a;
};

class Solution 
{
public:
	int Sum_Solution(int n)
	{
		//...
		return n;
	}
};

int main()
{
	A aa1;

	//不能这样定义对象,以为编译器无法识别下面是一个函数声明,还是对象定义

	//A aa1();

	//但是我们可以这么定义匿名对象,匿名对象的特定是不用取名字
	//但是他的声明周期只有这一行,我们可以看到下一行他就会自动调用析构函数

	A();


	//匿名对象在这样的场景下就很好用
	Solution().Sum_Solution(10);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zo8sYAJN-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

21.拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。

#include <iostream>

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		std::cout << "A(int a)" << std::endl;
	}
	
	A(const A& aa)
		:_a(aa._a)
	{
		std::cout << "A(const A& aa)" << std::endl;
	}

	A& operator=(const A& aa)
	{
		std::cout << "A& operator=(const A& aa)" << std::endl;

		if (this != &aa)
		{
			_a == aa._a;
		}

		return *this;
	}

	~A()
	{
		std::cout << "~A()" << std::endl;
	}

private:
	int _a;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}

int main()
{
	//传值传参
	A aa1;
	f1(aa1);

	//传值返回
	f2();


	//隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);

	//一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));

	//一个表达式中,连续拷贝构造+拷贝构造->优化为一个拷贝构造
	A aa2 = f2();


	//一个表达式中,连续拷贝构造+赋值构造->无法优化
	aa1 = f2();

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QnJcvv1s-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

上述代码运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-475h3TXB-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

三:C/C++内存管理

1.C/C++内存分布

#include<malloc.h>

int globalvar = 1;

static int staticGlobal = 1;

void Test()
{
	static int staticvar = 1;

	int localvar = 1;

	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dcBMXby7-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

  1. 选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar在哪里?C staticGlobalVar在哪里?C staticVar在哪里?_C

localVar在哪里?A num1 在哪里?A

char2在哪里?A *char2在哪里?_ A__ pChar3在哪里?A

*pChar3在哪里?D ptr1在哪里?A *ptr1在哪里?B

  1. 填空题:

sizeof(num1) = 40; sizeof(char2) = 5; strlen(char2) = 4;

sizeof(pChar3) = 8; strlen(pChar3) = 4; sizeof(ptr1) = 8;

#include<malloc.h>
#include <iostream>

int globalvar = 1;

static int staticGlobal = 1;

void Test()
{
	static int staticvar = 1;

	int localvar = 1;
	size_t a = 0;
	int num1[10] = { 1, 2, 3, 4 };
	a = sizeof(num1);
	std::cout << a << std::endl;

	char char2[] = "abcd";
	a = sizeof(char2);
	std::cout << a << std::endl;

	a = strlen(char2);
	std::cout << a << std::endl;

	const char* pChar3 = "abcd";
	a = sizeof(pChar3);
	std::cout << a << std::endl;

	a = strlen(pChar3);
	std::cout << a << std::endl;

	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	a = sizeof(ptr1);
	std::cout << a << std::endl;


	free(ptr1);
}

int main()
{
	Test();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aY4lrads-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TczpVsz0-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

  1. sizeof 和 strlen 区别?

sizeofstrlen是C/C++中的两个不同的操作符,用于不同的目的。

  1. sizeof
    • sizeof是C/C++的关键字,用于计算数据类型或变量所占用的内存大小(字节数)。
    • sizeof可以用于任何数据类型、变量或表达式,包括基本数据类型(如intfloat)、自定义数据类型(如结构体、类)、数组、指针等。
    • sizeof的结果在编译时确定,是一个编译时常量。
    • 例如,sizeof(int)返回4,表示int类型占用4个字节的内存。
  2. strlen
    • strlen是C/C++的字符串处理函数,用于计算字符串的长度(不包括结尾的空字符\0)。
    • strlen只能用于以空字符结尾的字符串,即以'\0'为结束标志的字符数组或字符指针。
    • strlen的结果在运行时确定,需要遍历字符串的每个字符直到遇到空字符\0
    • 例如,strlen("Hello")返回5,表示字符串"Hello"的长度为5。

总结:

  • sizeof用于计算数据类型或变量的内存大小,是在编译时确定的。
  • strlen用于计算以空字符结尾的字符串的长度,需要在运行时遍历字符串,直到遇到空字符。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1h6FLe06-1688661160518)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

2.C语言中动态内存管理方式:malloc/calloc/realloc/free

1.malloc(内存分配)

用于动态分配指定字节大小的内存块。

接受内存块大小作为参数,并且返回指向分配内存块的void指针(‘void*’)。所以需要我们手动强转其类型为我们需要的类型。

int* ptr = (int*)malloc(10 * sizeof(int)); // 分配内存以存储10个整数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kJjf3hEA-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.calloc(内存分配并初始化)

用于动态分配用于存储数组元素的内存块,并将其初始化为零。

接受两个参数,元素的个数和每个元素的大小(以字节为单位)

返回指向分配内存块的void指针,所以同样需要我们强转其类型。

int* ptr = (int*)calloc(10, sizeof(int)); // 分配内存以存储10个初始化为零的整数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTzISvtn-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.realloc (重新分配内存)

用于调整或重新分配先前分配的内存块的大小。

接受两个参数:指向先前分配内存块的指针和新的大小(以字节为单位)

返回指向重新分配内存块的void指针(void*),可能是原始指针或新位置。

int* ptr = (int*)malloc(10 * sizeof(int)); // 分配内存以存储10个整数
ptr = (int*)realloc(ptr, 20 * sizeof(int)); // 调整内存块大小以容纳20个整数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aqGeYXEj-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.free(释放内存)

用于释放先前通过malloc、calloc或realloc分配的内存。

接受指向内存块的指针作为参数。

调用free后,释放的内存块将可用于重新分配。

int* ptr = (int*)malloc(10 * sizeof(int)); // 分配内存以存储10个整数
// 使用分配的内存
free(ptr); // 释放内存块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PkW98kLy-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是:malloc、calloc、realloc和free是C标准库(<stdlib.h>)的一部分,虽然在C++中也可以使用,但通常不推荐。在C++中,建议使用语言提供的内存管理机制,如new、new[ ]、delete和delete[ ]。

3.C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

3.1new/delete操作内置类型
void Test()
{
	//动态申请一个int类型的空间
	int* ptr4 = new int;

	//动态申请一个int类型的空间并初始化为10
	int* ptr5 = new int(10);

	//动态申请10个int类型的空间
	int* ptr6 = new int[10];

	delete ptr4;
	delete ptr5;
	delete[] ptr6;
}

int main()
{
	Test();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u4HDUcfG-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uqb9eqlD-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

需要注意的是:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。并且需要两两匹配。

3.2new和delete操作自定义类型
#include <iostream>
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		std::cout << "A():" << this << std::endl;
	}

	~A()
	{
		std::cout << "~A():" << this << std::endl;
	}

private:
	int _a;
};

int main()
{
	// new/delete 和malloc/free最大区别是 new/delete对于自定义类型除了开空间还会调用构造函数和析构函数

	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);

	free(p1);
	delete(p2);

	//内置类型几乎是一样的
	int* p3 = (int*)malloc(sizeof(int));  // c
	int* p4 = new int;

	free(p3);
	delete p4;

	A* p5 = (A*)malloc(sizeof(A) * 10);
	A* p6 = new A[10];
	free(p5);
	delete[] p6;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dgr7nlOD-1688661160519)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

operator new是用于动态分配内存的运算符。它的主要作用是在堆上分配一块指定大小的内存空间,并且返回一个指向该内存空间的指针。通常,我们使用new关键字来调用operator new运算符,他会根据所需的类型大小自动调用适当版本的operator new。operator new可以接受一个size_t类型的参数,表示要分配的内存大小。

operator delete是用于释放动态配分的内存的运算符。它的主要作用是将之前通过operator new分配的内存空间释放,并将该内存空间返回给系统。通常,我们使用delete关键字来调用operator delete运算符,它会根据所需的类型自动调用适当版本的operator delete。operator delete接受一个指向要释放的内存空埃及你的指针作为参数

需要注意的是,C++标准库还提供了operator new[]和operator delete[]运算符,用于动态分配和释放数组类型的内存空间。它们与operator new和operator delete的作用类似,只是用于处理数组类型的内存分配和释放。

5.new和delete的实现原理

5.1内置类型

如果使用new操作符申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[ ]和delete[ ]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

5.2自定义类型

new的原理

1.调用operator new申请空间。

2.在申请的空间上执行构造函数,完成对象的构造

delete的原理

1.在空间上执行析构函数,完成对象中资源的清理工作

2.调用operator delete函数释放对象的空间

new className[N]的原理

1.调用operator new[ ]函数,在operator new[ ]中实际调用operator new函数完成N个对象空间的申请。

2.在申请的空间上执行N次构造函数。

delete[ ]的原理

1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

2.调用operator delete[ ]释放空间,实际在operator delete[ ]中调用operator delete 来释放N次

6.定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

new (ptr) Type(constructor_arguments);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c7ksqUme-1688661160520)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

  • ptr 是指向已分配内存的指针,表示在该地址上构造对象。
  • Type 是要构造的对象类型。
  • constructor_arguments 是用于构造对象的参数列表。

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的表达式进行显式调构造函数进行初始化。

#include <iostream>

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		std::cout << "A():" << this << std::endl;
	}

	~A()
	{
		std::cout << "~A():" << this << std::endl;
	}
private:
	int _a;
};


//定位new
int main()
{
	//p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  //注意:如果A类的构造函数不是缺省函数,或者无参函数,此处需要传参,以初始化
	p1->~A();
	free(p1);

	A* p2 = (A*)operator new(sizeof(A));


	new(p2)A(10);
	p2->~A();
	operator delete(p2);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L9aSykn1-1688661160520)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

7.malloc/free和new/delete的区别

其共同点是:都是从堆上申请空间,并且需要用户手动释放。

其不同点:

1.malloc和free是函数,new和delete是操作符

2.malloc申请的空间不会初始化,new可以初始化。

3.malloc申请空间时需要手动计算空间大小并传递,new只需要在其后跟上空间的类型即可,如果时多个对象,[ ]中指定对象个数即可。

4.malloc的返回值为void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型

5.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。

6.申请自定义对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

四:模板初阶

1.泛型编程

泛型编程是一种编程范式,其目的是实现通用的、可重用的代码,使得代码能够处理多种不同类型的数据而不需要针对每种类型编写特定的代码。

泛型编程的核心思想是参数化类型,通过使用类型参数来定义算法和数据结构,使得它们可以适用于多种不同的数据类型。这样可以提高代码的复用性、灵活性和可扩展性。

其特点如下:

1.参数化类型:通过在算法和数据结构中使用类型参数,使得它们可以处理不同的数据类型,而不是针对特定类型进行硬编码。

2.类型安全性:泛型编程在编译时进行类型检查,确保类型的一致性和安全性。这可以减少运行时错误,并提供更好的代码健壮性。

3.代码重用性:通过编写通用的算法和数据结构,可以在不同的场景中重用代码,避免重复编写相似的逻辑。

4.抽象和泛化:泛型编程强调对问题的抽象和泛化,而不是针对具体的实现细节。这使得代码更加通用、灵活和易于扩展。

小问题:如何实现一个通用的交换函数呢?

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}

void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NK8RH41n-1688661160520)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

如上述代码一般,使用重载可以实现,但是有几个问题,就是代码的复用率很低,只要有新的类型出现,就需要增加对应的函数;并且代码的可维护性比较低,一个出错可能所有的重载都会出错。

那么是否可以有一个模板,让编译器根据不同的类型利用该模板来生成代码呢?

C++这门语言就有这样的一种模板。在C++中,模板是泛型编程的基础,一共有两种,一个是函数模板,一个是类模板。

2.函数模板

2.1函数模板的概念

在C++中,函数模板是一种通用的函数定义,允许在单个函数定义中使用参数化类型,从而使函数可以处理多种不同类型的数据,实现泛型编程。

函数模板的定义使用关键字template,后跟一个或多个类型参数。类型参数可以用于函数的参数类型,返回类型或局部变量的类型。在函数模板中,类型参数用typename或class关键字声明,然后在函数体内就可以向普通类型一样使用这些类型参数。

template <typename T>
T Add(T a, T b) {
    return a + b;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8YVGNrc1-1688661160520)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.2函数模板的原理

函数模板是一个蓝图,其本身并不是函数,是编译器使用方式产生特定具体类型函数的模具。所以其实模板就是将我们原本需要重复写的代码交给编译器去生成了。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XyKmqasE-1688661160520)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

在编译器编译阶段:对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

2.3函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

1.隐式实例化:让编译器根据实参推演模板参数的实际类型。

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2);
	Add(d1, d2);


	//该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
	//通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T
	//编译器无法确定此处到底是该将T确定为int还是double
	//需要注意的是:编译器一般不会再模板中进行类型转换操作,因为可能出问题
	//Add(a1, d1);
	
	//此处有两种处理方式
	//1:用户自己来强制转化
	//2:使用显式实例化

	Add(a1, (int)d1);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZyxKEJ8-1688661160520)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.显式实例化

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}

int main(void)
{
	int a = 10;
	double b = 20.0;

	//显式实例化
	Add<int>(a, b);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vPKh3TZ-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.4模板参数的匹配原则

1.一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。

//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}

//通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}

void Test()
{
	Add(1, 2);           //与非模板函数匹配,编译器不需要特化
	Add<int>(1, 2);  //调用编译器特化的Add版本
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXjievZr-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板

//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}

//通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}

void Test()
{
	Add(1, 2);           //与非模板函数匹配,编译器不需要特化
	Add(1, 2.0);        //模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6NVsN9k-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

3.类模板

3.1类模板的概念

在C++中,类模板是一种通用的类定义,允许在单个类定义中使用参数化类型,从而使类可以适用于多种不同类型的数据,实现泛型编程

template <typename T1, typename T2>
class Pair {
public:
    T1 first;
    T2 second;
    
    Pair(T1 a, T2 b) {
        first = a;
        second = b;
    }
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLcjxZaT-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以上就是一个简单的类模板示例,实现了一个包含两个成员变量的Pair类。在使用这个类模板时,需要在类名后面使用具体的类型实参来实例化类。

Pair<int, double> p1(5, 3.14);       // 实例化Pair类,类型参数为int和double
Pair<std::string, int> p2("hello", 10); // 实例化Pair类,类型参数为std::string和int

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HD2oPPb5-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

需要注意的是:类模板中的函数,放在类外进行定义的时候,需要使用模板参数限定符 template <typename T1, typename T2, ...> 并指定模板参数类型。这样可以将成员函数与类模板的定义分离,使代码更加清晰和模块化。

五.STL

1.什么是STL

STL是C++标准模板库(Standard Template Library)的简称。它是C++标准库的重要组成部分,提供了一套丰富的模板类和函数,用于实现常用的数据结构和算法。

STL的设计目标是提供一种通用的、可重用的软件组件,以增加C++的表达能力和编程效率。它提供了多个容器类(如向量、链表、队列、栈、集合、映射等)和算法(如排序、搜索、遍历、变换等),以及与之配套的迭代器(iterator)和函数对象(functionobject)等。

通过使用STL,开发人员可以更加方便地编写高效且可重用地代码。STL地容器类提供了方便地数据存储和访问方式,算法类提供了常用地操作和算法实现,而迭代器和函数对象则使得代码更具灵活性和可扩展性。

STL地设计思想包括泛型编程、容器与算法地分离、迭代器和函数对象的概念等。这些思想在C++编程中具有广泛的应用价值。STL的使用不仅可以提高开发效率,还可以减少代码的错误和提高程序的性能。

2.STL的六大组件

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AuS341T2-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

1.容器(Containers):容器是用于存储和管理数据的类模板,提供了不同类型的数据结构,如向量(vector)、链表(list)、集合(set)、映射(map)等。容器提供了一系列操作函数,例如插入、删除、搜索等,使得数据的管理和访问更加方便和高效。

2.算法(Algorithms):算法是用于对容器中的数据执行各种操作和算法的函数模板。STL提供了大量的常用算法,例如排序、查找、替换、合并等,这些算法可以直接用与不同类型的容器,无需自行实现。算法是通过迭代器来访问容器中的元素,从而实现对数据的处理。

3.迭代器(Iterators):迭代器用于遍历和访问容器中的元素。它提供了统一的接口,使得可以以类似指针的方式对容器进行遍历和操作。迭代器分为输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器等不同类型,具有不同的访问和操作能力。

4.仿函数(Functors):仿函数是一种函数对象,它可以像函数一样被调用。STL中的算法往往可以接受仿函数作为参数,用于定义操作的行为。仿函数是通过重载函数调用操作符(operator())来实现的,可以在算法中根据需要自定义或使用预定义的仿函数。

5.适配器(Adapters):适配器用于将一个容器或迭代器的接口适配为另一种接口形式。STL提供了多种适配器,例如栈(Stack)、队列(queue)、优先队列(priority_queue)等。适配器可以改变容器或迭代器的行为方式,使其符合特定的需求或接口要求。

6.分配器(Allocators):分配器用于控制容器中元素的内存分配和释放。STL提供了默认的内存分配器,也允许自定义分配器,以满足特定的内存管理需求。分配器可以通过模板参数来指定,从而实现对容器的内存管理机制的定制。

六.string类

1.标准库中的string类

在C++标准库中,std::string是一个非常常用的字符串类,他提供了一系列用于操作字符串的成员函数和重载运算符。标准的字符串类提供了对此类对象的支持,其接口类似于标准容器的接口,但添加了专门用于操作单字节字符串的设计特性。

string类是使用char(即作为他的字符类型,使用它的默认char_traits和分配器类型。string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_trairs和allocator作为basic_string的默认参数。

需要注意的是,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及他的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

std::string类具有以下特点和功能:

1.动态大小:std::string对象的大小可以根据字符串的长度自动调整,不需要手动管理内存。

2.字符串操作:std::string提供了丰富的字符串操作函数,例如插入、删除、替换、查找、拼接等,使得字符串的处理变得更加的方便。

3.字符串访问:可以通过下标运算符[]或at()函数来访问字符串中的单个字符,也可以通过成员函数c_str()获取以NULL结尾的C风格字符串。

4.字符串比较:可以使用重载的比较运算符(如 == 、!=、<、>等)来比较字符串的大小和相等性。

5.迭代器支持:std::string支持使用迭代器遍历字符串中的字符,从而可以进行循环操作和部分算法应用。

6.输入输出:可以使用输入输出流来读取和输出std::string对象,方便与其他数据进行交互。

7.字符串转换:std::string提供了函数std::stoi、std::stof等,用于将字符串转换为其他数据类型,以及反向的转换函数。

8.内存操作:可以使用成员函数resize()调整字符串的大小,使用clear()清空字符串内容。

2.string类的常用接口

2.1.string类对象的常见构造

1.默认构造函数:

string();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYR3e2Nq-1688661160521)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

即构造一个空字符串。

2.使用C风格字符串构造

string(const char* s);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xes3KNIG-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其作用是将C风格字符串(即以空字符\0结尾的字符串)转换为std::string对象。它会将C字符串中的字符逐个复制到新创建的std::string对象中,知道遇到空字符为止。

const char* cstr = "Hello, world!";
std::string str(cstr);
// 从C风格字符串创建一个std::string对象

const char* name = "John Doe";
std::string fullName(name);
// 从C风格字符串创建一个std::string对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-71gqfVo3-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.使用重复字符构造:

string(size_t n, char c);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HwY3LgPK-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

用于创建一个包含指定数量重复字符的字符串对象。

std::string str(5, 'A');
// 创建一个包含5个字符'A'的字符串: "AAAAA"

std::string spaces(10, ' ');
// 创建一个包含10个空格字符的字符串: "          "

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzzgvvmy-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.使用字字符串构造:

string(const string& str, size_t pos, size_t len = npos);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-myrOpHFq-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其作用是创建一个新的std::string对象,该对象包含从源字符串对象std中的位置pos开始的长度为len的子字符串。参数len是可选的,默认值为npos,表示从pos开始到字符串末尾的所有字符都被包含在新的字符串对象中,也就是说,可以不写len这个参数,因为有默认值。

std::string original = "Hello, world!";
std::string subString(original, 7, 5); // 从位置 7 开始提取长度为 5 的子串

std::cout << subString << std::endl;  // 输出 "world"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qf57k4FC-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

5.使用迭代器范围构造:

template <class InputIterator>
string(InputIterator first, InputIterator last);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q4VDFglf-1688661160522)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这个函数的作用是根据给定的迭代器范围[first, last)构造一个新的std::string对象,其中包含从first到last(不包括last)的字符序列。

这个构造函数允许你使用迭代器指定要构造的字符串的内容。你可以传递任何类型的迭代器,只要迭代器的值类型是字符类型即可吗,例如指向字符数组、字符串对象或容器的迭代器。

char arr[] = {'H', 'e', 'l', 'l', 'o'};
std::string str(arr, arr + 5);  // 使用迭代器范围构造字符串

std::cout << str << std::endl;  // 输出 "Hello"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZUZwFSsC-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.string类型的拷贝构造函数

string(const string& str);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LmMsqJhf-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这个函数是根据已有的std::string对象str创建一个新的字符串对象,新对象与str具有相同的内容。在构造过程中,会复制str中的字符序列,并分配新的内存来存储该字符序列。

std::string original = "Hello";
std::string copy(original);  // 使用拷贝构造函数创建副本

std::cout << copy << std::endl;  // 输出 "Hello"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-le5zUIHf-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.2string对象的容量操作

1.size(),起作用是返回字符串对象的大小(字符数,不是字节数或者内存空间大小),其返回一个size_t类型的值,表示字符串的大小。

size_t size() const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVzrNjqg-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

一下是该函数的简单用例:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    
    // 使用 size() 函数获取字符串的大小
    size_t len = str.size();
    
    std::cout << "String length: " << len << std::endl;
    
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pydkQ0uD-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出如下:

String length: 13

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zWUgVDnU-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.length(),其作用与size()函数一致,都是返回string对象中的字符串的长度(字符数)。

size_t length() const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s30CjRgf-1688661160523)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是其简单的例子:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    
    // 使用 length() 函数获取字符串的长度
    size_t len = str.length();
    
    std::cout << "String length: " << len << std::endl;
    
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WxcfM5Qm-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出为:

String length: 13

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnFKzu8P-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.capacity()函数,其函数原型为

size_t capacity() const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7qa4XYvP-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其作用是返回string对象内部分配的存储空间的容量。它表示在重新分配内存之前,string对象可以存储的最大字符数。实际存储的字符数可能小于或等于容量。字符串实际的大小和其存储空间的容量不一定一致。

以下是该函数的简单例子:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";
    
    std::cout << "String capacity: " << str.capacity() << std::endl;
    
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sw4siD2n-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出为:

String capacity: 15

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ngH49ejG-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

string对象容量的具体值可能因实现而异,并且可能随着字符串操作的变化而变化。所以,这个15不是一定的。字符串超过容量时,string类会自动重新分配内存以容纳更多的字符。

4.empty()函数,其函数原型为:

bool empty() const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Tlp8Adm-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其作用适用于检查string对象是否为空,即字符串是否不包含任何字符。如果字符串为空,函数返回true;如果字符串非空,函数返回false。

下面是个简单的例子,用于讲解empty函数的用法:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2;

    if (str1.empty()) {
        std::cout << "str1 is empty" << std::endl;
    } else {
        std::cout << "str1 is not empty" << std::endl;
    }

    if (str2.empty()) {
        std::cout << "str2 is empty" << std::endl;
    } else {
        std::cout << "str2 is not empty" << std::endl;
    }

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9kjlbyn-1688661160524)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出是:

str1 is not empty
str2 is empty

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JV87Hnty-1688661160525)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

5.clear()函数,其函数原型是:

void clear();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AUqaK8WQ-1688661160525)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

该函数的作用是将字符串中的所有字符清除,使其成为空字符串,即长度为0,不过容量不一定为0。调用clear后,字符串对象将不包含任何字符。

下面是个简单的例子用以讲解clear函数的用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";
    std::cout << "Before clear: " << str << std::endl;

    str.clear();

    std::cout << "After clear: " << str << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2UMvM25-1688661160525)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出为:

Before clear: Hello, World!
After clear: 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-saUaNl1Y-1688661160525)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.reserve()函数,其函数原型是:

void reserve(size_t new_cap);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dODXovAN-1688661160525)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其作用用于为字符串预留至少new_cap个字符的内存空间。预留内存可以减少频繁的内存重新分配操作,提高字符串的性能。

当预留的内存空间大于当前字符串的容量时,reserve函数会重新分配内存空间,使得字符串的容量至少为new_cap。如果预留的内存空间小于或等于当前字符串的容量,则不进行任何操作。也就是说reserve可以增大string对象的内存空间,但不可以减少string对象的内存空间。

以下是个简单的例子用于讲解reserve的用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";
    std::cout << "Before reserve: capacity = " << str.capacity() << std::endl;

    str.reserve(20);

    std::cout << "After reserve: capacity = " << str.capacity() << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T9UMXNbY-1688661160525)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出为:

Before reserve: capacity = 5
After reserve: capacity = 20

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UTWXdNvJ-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

7.resize()函数,其函数原型为:

void resize(size_t n);
void resize(size_t n, char c);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sx1dlc6D-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

第一个重载形式的resize()函数用于将字符串的大小调整为指定的大小n。如果n小于当前字符串的大小,则字符串被阶段为前n个字符;如果n大于当前字符串的大小,则字符串被扩展并用空字符填充新添加的位置。如果n大于当前对象的容量,那么会触发字符串的重新分配内存操作。重新分配内存意味着会申请一个新的内存块来存储新的大小的字符串,并将原来的字符串内容复制到新的内存中。

第二个重载形式的resize()函数的作用和第一个差不多,只是用于填充的字符不再为空,而是为字符c。也就是说如果n大于当前字符串的大小,则字符串被扩展并用字符c填充新添加的位置。

以下是其简单的用法例子:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";

    // 调整字符串大小为 8,多出的位置用空字符填充
    str.resize(8);
    std::cout << str << std::endl;  // 输出:Hello\0\0\0

    // 调整字符串大小为 5,截断多余的字符
    str.resize(5);
    std::cout << str << std::endl;  // 输出:Hello

    // 调整字符串大小为 10,并用字符 '!' 填充新添加的位置
    str.resize(10, '!');
    std::cout << str << std::endl;  // 输出:Hello!!!!!

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q1exVXr8-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.3string类对象访问及遍历操作

1.operator[ ]是std::string类中的成员函数,用于访问字符串对象中指定位置的字符。它允许通过索引来访问和修改字符串中的单个字符。

其函数原型为:

char& operator[](size_t pos);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mtp1wYIx-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是个简单的例子,用以演示其用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    // 通过索引访问和修改字符串中的字符
    std::cout << str[0] << std::endl;    // 输出:H
    str[7] = 'W';
    std::cout << str << std::endl;       // 输出:Hello, WWrld!

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-57iLB3qT-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.begin+end是std::string类中的成员函数,用于获取字符串对象的起始和结束位置的迭代器。

其函数原型为:

iterator begin() noexcept;
const_iterator begin() const noexcept;
const_iterator cbegin() const noexcept;

iterator end() noexcept;
const_iterator end() const noexcept;
const_iterator cend() const noexcept;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mn6mAHgh-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

begin()函数返回一个迭代器,指向字符串的起始位置(第一个字符)。end()函数返回一个迭代器,指向字符串的结束位置(最后一个字符的下一个位置)。迭代器可以用于遍历字符串对象中的字符或进行其他操作。

以下是个简单的例子用于讲解其用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    // 使用迭代器遍历字符串中的字符
    for (auto it = str.begin(); it != str.end(); ++it) {
        std::cout << *it;
    }
    std::cout << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwZzS1tL-1688661160526)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cv3bqo9f-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

3.rbegin+rend。这两个函数是std::string类中的成员函数,用于获取字符串对象的反向迭代器的起始和结束位置。

以下是其函数原型:

reverse_iterator rbegin() noexcept;
const_reverse_iterator rbegin() const noexcept;
const_reverse_iterator crbegin() const noexcept;

reverse_iterator rend() noexcept;
const_reverse_iterator rend() const noexcept;
const_reverse_iterator crend() const noexcept;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkDQnvIh-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

rbegin()函数的作用是返回一个反向迭代器,指向字符串的最后一个字符。而rend函数返回一个反向迭代器,指向字符串的第一个字符的前一个位置。

以下是个简单的例子用以讲解这两个函数的作用:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    // 使用反向迭代器逆序遍历字符串中的字符
    for (auto it = str.rbegin(); it != str.rend(); ++it) {
        std::cout << *it;
    }
    std::cout << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nBTGWEWw-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.范围for(C++11支持的更简洁的遍历方式)

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";

    // 使用范围for循环遍历字符串中的字符
    for (char c : str) {
        std::cout << c << " ";
    }
    std::cout << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OsMHSaJG-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.4string类对象的修改操作

1.push_back()函数是将给定的字符c添加到字符串的末尾,扩展字符串的长度。添加后,新的字符将成为字符串的最后一个字符。

void push_back(char c);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNofhXI5-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是个简单的例子,用以讲解其用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";

    str.push_back('!');
    std::cout << str << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JD51zzxd-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其运行结果如下:

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JzAOS67A-1688661160527)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

可以看到,hello后面多了个 ! 。

2.append()函数,该函数的作用是将一个字符串或字符序列追加到另一个字符串的末尾。

其函数原型为:

string& append(const string& str);
string& append(const char* s);
string& append(const char* s, size_t n);
string& append(size_t n, char c);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9RXq0xu-1688661160528)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

可以看到,append函数有多个重载形式,意味着它可以接受不同的参数形式,包括另一个字符串、C风格字符串、字符序列和重复的字符串;然后添加到另一个字符串的末尾。

以下是一个简单的例子,用以演示其用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";

    str.append(" World");
    std::cout << str << std::endl;

    str.append("!!!", 3);
    std::cout << str << std::endl;

    str.append(2, '.');
    std::cout << str << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VJFmQ6tn-1688661160528)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出为:

Hello World
Hello World!!!
Hello World!!!..

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d6moW5Bo-1688661160528)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.operator+=是string类中的成员函数,其重载了操作符+=。其作用是用于将一个字符串或字符序列追加到当前字符串的末尾。

string& operator+=(const string& str);
string& operator+=(const char* s);
string& operator+=(char c);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CwcmMHSj-1688661160528)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

可以看到其同样有着多种重载形式,也就是说它也可以接受不同的参数形式,包括另一个字符串、C风格的字符串和单个字符。

下面是个简单的例子,用以演示其用法:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";

    str += " World";
    std::cout << str << std::endl;

    str += "!!!";
    std::cout << str << std::endl;

    str += '.';
    std::cout << str << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hItu85tF-1688661160528)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这段代码的输出为:

Hello World
Hello World!!!
Hello World!!!.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GkYXRtJ-1688661160528)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

4.c_str函数返回一个指向正规C字符串的指针,内容与本string串相同。这个字符数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。所以要么现用现转换,要么就是把他的数据复制到用户自己可以管理的内存中。

其函数原型为:

const char* c_str() const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XnDWQWCv-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是其简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";

    const char* cstr = str.c_str();
    std::cout << cstr << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-stXfqtPz-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

Hello

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cjg84fB4-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

但是这种方法不是很好,可能会出现问题。所以更推荐使用下面这种方法。

#include <iostream>
#include <cstring>
using namespace std;
 
int main()
{
	//更好的方法是将string数组中的内容复制出来 所以会用到strcpy()这个函数
	char *c = new char[20];
	string s = "1234";
	// c_str()返回一个客户程序可读不可改的指向字符数组的指针,不需要手动释放或删除这个指针。
	strcpy(c,s.c_str());
	cout<<c<<endl;
	s = "abcd";
	cout<<c<<endl;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vj9CEbG-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这样就不会因为s的改动,而改动了我们用c_str获取的C风格字符串了。

5.find+npos

在std::string类中,find()函数用于在字符串中搜索指定的子字符串,并且返回第一次出现的位置。npos是std::string::npos的常量值,他表示搜索失败的特殊位置,一般是字符串最后一个字符的下一个位置。

size_t find(const std::string& str, size_t pos = 0) const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XjbEpBsc-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

find()函数在调用的字符串中搜索给定的子字符串str,并且返回第一次出现的位置。搜索从指定的pos位置开始,默认从字符串的开头开始搜索。如果找到子字符串,则返回子字符串的起始位置;如果未找到,则返回std::string::npos。

以下是简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    size_t position = str.find("World");
    if (position != std::string::npos) {
        std::cout << "Substring found at position: " << position << std::endl;
    } else {
        std::cout << "Substring not found." << std::endl;
    }

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXmJHTQ6-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

Substring found at position: 7

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6WHei0bX-1688661160529)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

6.rfind。

rfind()函数用于在字符串中从后往前搜索指定的子字符串,并返回最后一个出现的位置。

size_t rfind(const std::string& str, size_t pos = npos) const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2iqzAmDC-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是其简单的示例:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, Hello, World!";

    size_t position = str.rfind("Hello");
    if (position != std::string::npos) {
        std::cout << "Last occurrence found at position: " << position << std::endl;
    } else {
        std::cout << "Substring not found." << std::endl;
    }

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-USazSPLB-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

Last occurrence found at position: 7

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lFdWdzov-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

7.substr

在C++的std::string类中,substr()函数用于提取字符串的子串,即从原字符串中截取指定位置和长度的子字符串。

以下是其函数原型:

std::string substr(size_t pos = 0, size_t len = npos) const;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OBOKvJFn-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

substr函数返回一个新的std::string对象,还对象包含从指定位置pos开始的长度为len的字串。如果未指定len,则默认提取从pos到字符串末尾的所有字符。

以下是其简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, World!";

    std::string sub1 = str.substr(7); // Starting from index 7 to the end
    std::string sub2 = str.substr(0, 5); // Starting from index 0, length 5

    std::cout << "sub1: " << sub1 << std::endl; // Output: "World!"
    std::cout << "sub2: " << sub2 << std::endl; // Output: "Hello"

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svqlRYrx-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

sub1: World!
sub2: Hello

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9pC36Ez-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

2.5sting类非成员函数

1.operator+

这个函数用于字符串的链接操作,也就是将两个字符串拼接成一个新的字符串。

其函数原型为:

std::string operator+(const std::string& lhs, const std::string& rhs);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c941eHDh-1688661160530)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是个简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2 = "World";

    std::string result = str1 + " " + str2;

    std::cout << "Result: " << result << std::endl; // Output: "Hello World"

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGhdfNQp-1688661160531)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

Result: Hello World

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SapR3TW5-1688661160531)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

但是不太建议使用这个函数,因为其采用的是传值返回,效率不高。

2.operator>>和operator<<这两个函数分别用于输入和输出字符串。

operator>>用于从输入流中读取字符串,并将其存储到std::string对象中,其函数原型如下:

std::istream& operator>>(std::istream& is, std::string& str);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LEHjDN80-1688661160531)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是其简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string name;

    std::cout << "Enter your name: ";
    std::cin >> name;

    std::cout << "Hello, " << name << "!" << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fu6zaFo3-1688661160531)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

如果我们输入Json,其输出结果为:

Hello, John!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4AsnvDfB-1688661160531)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

operator<<用于将std::string对象的内容输出到输出流中。其函数原型如下:

std::ostream& operator<<(std::ostream& os, const std::string& str);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OSfyamsj-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是其简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string message = "Hello, world!";

    std::cout << message << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9KQDkzGZ-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

Hello, world!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Acq6cU8i-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.getline

getline函数用于从输入流中读取一行文本并存储到字符串对象中,其函数原型为:

std::istream& getline(std::istream& is, std::string& str, char delim);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEnFNDRN-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其中,is是输入流对象,str是要存储读取的行文本的std::string对象的引用,delim是可选的定界符(默认是换行符\n)。

以下是其简单的用法示例:

#include <iostream>
#include <string>

int main() {
    std::string line;

    std::cout << "Enter a line of text: ";
    std::getline(std::cin, line);

    std::cout << "You entered: " << line << std::endl;

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NkAp2PIH-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

其输出结果为:

Enter a line of text: Hello, world!
You entered: Hello, world!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3AsIlTWx-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

std::getline()函数会读取输入流中的字符,知道遇到定界符(默认为换行符\n)为止,并将读取的字符存储到str对象中。如果遇到文件结束符或发生输入错误,函数将终止读取。

需要注意的是,getline函数会舍弃定界符,并将其从输入流中移除,不会存储到字符串中。如果需要保留定界符,可以使用重载版本的std::getline()函数,其中的delim参数可以设置为字符串类型,用于指定定界符。

4.relational operators

在C++的标准库中,std::string 类提供了一组关系运算符(relational operators),用于比较两个字符串对象之间的大小关系。这些关系运算符包括:

  • ==:等于运算符,用于判断两个字符串是否相等。
  • !=:不等于运算符,用于判断两个字符串是否不相等。
  • <:小于运算符,用于判断一个字符串是否小于另一个字符串。
  • >:大于运算符,用于判断一个字符串是否大于另一个字符串。
  • <=:小于等于运算符,用于判断一个字符串是否小于或等于另一个字符串。
  • >=:大于等于运算符,用于判断一个字符串是否大于或等于另一个字符串。

这些运算符都返回一个bool类型的结果,即true或false,表示比较结果的真假。

以下是一个简单的示例:

#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello";
    std::string str2 = "World";

    if (str1 == str2) {
        std::cout << "str1 is equal to str2" << std::endl;
    } else {
        std::cout << "str1 is not equal to str2" << std::endl;
    }

    if (str1 < str2) {
        std::cout << "str1 is less than str2" << std::endl;
    } else {
        std::cout << "str1 is not less than str2" << std::endl;
    }

    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iOfkpc98-1688661160532)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.string类的一些进阶问题

首先我们来看一下 下面的代码有什么问题。

#include <assert.h>
#include <string.h>

//为了和标准库区分,此处使用String
class String
{
public:
	String(const char* str = "")
	{
		//构造String类对象时,如果传递nullptr指针,可以认为程序非法
		if (nullptr == str)
		{
			assert(false);
			return;
		}

		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	~String()
	{
		if (_str)
		{
			delete[]_str;
			_str = nullptr;
		}
	}

private:
	char* _str;
};

void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yq2eDLTH-1688661160533)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

这段代码会报错,因为出现了两次析构的问题。为什么会出现两次析构呢?

是因为上述的String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,是浅拷贝或值拷贝。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEL55Ywd-1688661160533)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

3.1浅拷贝

浅拷贝:也称值拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进进行操作时,就会发生了违规访问。

3.3深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显示给出。一般情况都是按照深拷贝方式提供。因为深拷贝不是简单的拷贝值,而是需要重新开辟一个内存空间用以存放这份复制的资源。这样就不会出现对同一份资源析构两次的问题,因为空间有两份,资源也有两份。

img[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-54W58Et4-1688661160533)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]编辑

3.4string的简单模拟
3.4.1传统写法
#include <assert.h>
#include <string.h>
class String
{
public:
	//构造函数
	//缺省参数
	String(const char* str = "")
	{
		//构造String类对象时,如果传递nullptr指针,可以认为程序非法
		if (nullptr == str)
		{
			assert(false);
			return;
		}

		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	//拷贝构造
	String(const String& s)
		:_str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	//重载赋值
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			char* pStr = new char[strlen(s._str) + 1];
			strcpy(pStr, s._str);
			delete[]_str;
			_str = pStr;
		}
		return *this;
	}

	~String()
	{
		if (_str)
		{
			delete[]_str;
			_str = nullptr;
		}
	}


private:
	char* _str;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GDR7v3EG-1688661160533)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

3.4.2现代写法
#include <assert.h>
#include <string.h>
#include <type_traits>

using namespace std;
class String
{
public:
	//构造函数
	//缺省参数
	String(const char* str = "")
	{
		//构造String类对象时,如果传递nullptr指针,可以认为程序非法
		if (nullptr == str)
		{
			assert(false);
			return;
		}

		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	//拷贝构造
	//该拷贝构造函数采用了委托构造的方式,通过传递另一个String对象来构造新的String对象
	//在构造临时String对象strTmp时,它会使用传入的String对象的_str成员来构造新的字符串,并将新的字符串指针交换给当前对象的_str成员。
	//这样做的目的是为了实现深拷贝,避免多个String对象指向同一块内存。
	String(const String& s)
		:_str(nullptr)
	{
		String strTmp(s._str);
		swap(_str, strTmp._str);
	}

	//重载赋值
	//该赋值成员函数采用了值传递的方式,将传入的String对象作为参数。
	//通过传值的方式创建了一个临时的String对象,这样会调用拷贝构造函数来创建临时对象,再通过交换指针的方式来实现资源的转移。
	//最后返回当前对象的引用,以支持连续赋值操作。
	String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}

	~String()
	{
		if (_str)
		{
			delete[]_str;
			_str = nullptr;
		}
	}


private:
	char* _str;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dyJktzdB-1688661160533)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

以下是两种赋值成员函数的实现方式。

	//赋值成员函数
    String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}

	//赋值成员函数
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			String strTmp(s);
			swap(_str, strTmp._str);
		}
		return *this;
	}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUuyd4Pj-1688661160533)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

综合来说,第一种赋值成员函数的实现方式更优异。它能够复用拷贝构造函数,并且通过值传递的方式避免了不必要的拷贝操作。而第二种方式需要额外的拷贝构造操作,对于大型对象或者频繁赋值的场景可能会产生性能损耗。因此,第一种方式更常见和推荐使用。

七:vector类

1.vector的介绍和使用

1.1vector的介绍:

在C++中,vector是一个动态数组容器,他提供了动态大小的数组功能。vector是标准库中最常用的容器之一,位于头文件中。

vector的特点包括:

1.动态大小:vector的大小可以在运行时动态改变,可以根据需要自动调整容器的大小。

2.连续存储:vector使用连续的内存存储元素,因此可以通过索引直接访问元素,并支持快速的随机访问。

3.可以自动扩容:当需要插入更多元素的时候,也就是已插入元素的容量大于等于vector分配容量之后,vector会自动申请一个新的内存空间,并将vector对象中的值复制到新的内存位置。

4.冗余空间分配:vector会分配额外的空间以适应可能的增长,也就是说vector的存储空间可能比实际需要的存储空间更大。即创建一个五个元素大小的vector其实际存储空间可能比五个元素更大。

5.尾部插入和删除:vector支持在尾部进行高效的元素插入和删除操作。因为vector是连续存储的,如果要在头部进行元素移动操作,需要移动整个vector,牵一发而动全身。只有在尾部移动不会影响其他元素。

6.可以存储任意类型的元素:vector可以存储任意类型的对象,包括内置类型和自定义类型。

1.2vector的使用

在学习计算机的相关知识时,最重要的就是要去查阅官方文档。

以下是C++中关于vector的文档链接:

C++vector文档

1.3vector的构造函数

1.vector的默认构造函数vector(),其作用是创建一个空的vector对象。默认构造函数不需要任何参数,即是一个无参函数。

使用默认构造函数创建的vector对象是空的,没有任何元素。可以在之后使用其他的方法向其中插入元素。

下面是其简单的用法示例:

\#include <iostream>

\#include <vector>

 

int main() {

  std::vector<int> vec; // 创建一个空的int类型的vector

 

  // 检查vector是否为空

  if (vec.empty()) {

​    std::cout << "Vector is empty" << std::endl;

  } else {

​    std::cout << "Vector is not empty" << std::endl;

  }

 

  return 0;

}

如果我们运行这段代码,其输出结果如下:

Vector is empty

2.带有初始大小的构造函数:vector(size_type count)

这个函数的作用是创建一个包含指定数量默认构造的元素的vector对象。

以下是其简单的用法示例:

\#include <iostream>

\#include <vector>

 

int main() {

  std::vector<int> vec(5); // 创建包含5个默认构造的int元素的vector

 

  // 输出vector中的元素

  for (int i : vec) {

​    std::cout << i << " ";

  }

  std::cout << std::endl;

 

  return 0;

}

其输出结果如下:

0 0 0 0 0

由上述结果可以看出,这个函数有着两个参数,第二个参数可以不写是因为其有着默认值,为0。也就是说,不输入第二个参数,其创建的vector对象中的元素的值都为默认值0。

3.带有初始值的构造函数 vector(size_type count, const T& value)

这个函数的用法同上述函数一致,只是因为其将第二个参数显式输入,为创建的vector对象中的每个元素的值初始化为value。

4.范围构造函数:vector(InputIt first, InputIt last)

这个函数的作用是使用一个已创建的vector对象,并使用其迭代器范围中的元素创建一个vector对象。需要注意的是这个范围是左闭右开的,也就是说不会获取最右边的值。

以下是其简单的用法示例:

\#include <iostream>[]()

\#include <vector>

 

int main() {

  std::vector<int> sourceVec = {1, 2, 3, 4, 5};

  std::vector<int> vec(sourceVec.begin() + 1, sourceVec.end() - 1);

 

  // Print the elements of vec

  for (int num : vec) {

​    std::cout << num << " ";

  }

  std::cout << std::endl;

 

  return 0;

}

其输出结果如下:

2 3 4

因为end()指向的时候vector中最后一个元素的下一个位置,所以减1之后,其指向的位置是最后一个元素的位置,也就是 ‘5’。

5.拷贝构造函数:vector(vector&& other)

这个函数和上述使用迭代器区间构造对象的函数有些相像,其也是通过已有的vector对象创建一个新的vector对象。不过这个函数创建的对象和已有的对象除了内存空间不一致之外,其余全都一致。或者说这个函数创建的对象是已有函数的副本。

以下是其简单的用法示例:

\#include <iostream>

\#include <vector>

 

int main() {

  std::vector<int> sourceVec = {1, 2, 3, 4, 5};

  

  // 使用拷贝构造函数创建一个新的 vector 对象

  std::vector<int> vec(sourceVec);

  

  // 打印新的 vector 中的元素

  for (int num : vec) {

​    std::cout << num << " ";

  }

  std::cout << std::endl;

  

  return 0;

}

其输出结果如下

1 2 3 4 5

1.4 vector iterator的使用

  1. begin()和end()

STL中的迭代器其使用方式基本是一致的,就为了令使用者可以迅速方便的上手,学会其中的一个其他的基本也能触类旁通。

vector中的begin()和end(),同string中的用法是一样的,都是用于获取指向容器中第一个元素和最后一个元素之后的位置的迭代器。这两个函数通常一起使用,用于遍历容器中的元素。

以下是简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用迭代器遍历 vector 中的元素
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

其输出结果为:

1 2 3 4 5

2.rbegin()和rend()

rbegin()函数返回一个反向迭代器,指向容器中最后一个元素。

rend()函数返回一个反向迭代器,指向容器中第一个元素之前的位置。

其用法同string类中的一致。

1.5vector的空间增长问题

1.size()

size()函数返回一个无符号整数类型(size_type)的值,用以表述vector容器中元素的个数。

以下是个简单的用法例子:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用 size() 函数获取 vector 中的元素数量
    std::cout << "Vector size: " << vec.size() << std::endl;
    
    return 0;
}

其输出结果为:

Vector size: 5

2.capacity()

capacity()函数用于返回当前vector()对象的容量。容量表示在重新分配内存之前,vector对象可以容纳的元素数量。容量的大小同元素的个数不一定相同,因为vector中的容量可能会大于元素的个数,以应对可能的插入。因为扩容的代价很大。

下面个简单的用法示例以演示其用法:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用capacity()函数获取vector的容量
    std::cout << "Vector capacity: " << vec.capacity() << std::endl;
    
    return 0;
}

其输出结果为:

Vector capacity: 5

3.empty()

这个函数用于判断vector对象是否为空,如果vector对象中没有任何元素,则返回true,否则返回false。

以下是个简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1; // 空的vector
    std::vector<int> vec2 = {1, 2, 3}; // 非空vector
    
    // 使用empty()函数检查vector是否为空
    if (vec1.empty()) {
        std::cout << "vec1 is empty" << std::endl;
    } else {
        std::cout << "vec1 is not empty" << std::endl;
    }
    
    if (vec2.empty()) {
        std::cout << "vec2 is empty" << std::endl;
    } else {
        std::cout << "vec2 is not empty" << std::endl;
    }
    
    return 0;
}

上述代码的输出结果为:

vec1 is empty
vec2 is not empty

4.resize()

resize函数用以改变的vector()的大小,可能有以下几种情况:

1.新的大小等于当前的大小,不会发生任何改变。

2.新的大小小于当前大小,vector会缩小为指定的大小,丢弃超出范围的元素。

3.新的大小大于当前的大小,但是小于当前的容量,vector会增大为指定的大小,并在尾部插入新的元素,新的元素值为默认值。如果vector其中的元素类型是类对象,则会调用默认构造函数创建新的元素。

4.新的大小大于当前的容量,vector会扩容,申请一块新的内存空间,并将现有的元素的值拷贝到新的内存空间,并且插入新的元素,新的元素的值为默认值。如果vector其中的元素类型是类对象,则会调用默认构造函数创建新的元素。

以下是其简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec(1);

    vec[0] = 1;

    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    std::cout << "Current size: " << vec.size() << std::endl;
    std::cout << "Current capacity: " << vec.capacity() << std::endl;

    vec.resize(3); // 增大为10个元素

    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    std::cout << "New size after resize: " << vec.size() << std::endl;
    std::cout << "New capacity after resize: " << vec.capacity() << std::endl;

    vec.resize(10); // 增大为10个元素

    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    std::cout << "New size after resize: " << vec.size() << std::endl;
    std::cout << "New capacity after resize: " << vec.capacity() << std::endl;

    return 0;
}

其输出结果如下:

1
Current size: 1
Current capacity: 1
1 0 0
New size after resize: 3
New capacity after resize: 3
1 0 0 0 0 0 0 0 0 0
New size after resize: 10
New capacity after resize: 10

5.reserve()

reserve()函数用于预分配存储空间的容量。它接受一个参数,表示要预分配的元素数量,以便于在添加元素时避免不必要的重新分配和复制操作。预先分配存储空间的容量并不会改变向量的大小(即:size()不会受到影响),而是仅仅增加了std::vector内部的容量,一边容纳更多的元素。

需要注意的是:reserve()函数指只会增大容量,不会缩小容量,也就是说,如果reserve函数的参数值小于当前的容量,其不会对当前vector对象的容量做任何操作。

以下是个简单的例子,用以演示其用法:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;

    std::cout << "Current size: " << vec.size() << std::endl;
    std::cout << "Current capacity: " << vec.capacity() << std::endl;

    vec.reserve(10); // 预分配至少可以容纳10个元素的空间

    std::cout << "New size after reserve: " << vec.size() << std::endl;
    std::cout << "New capacity after reserve: " << vec.capacity() << std::endl;

    vec.push_back(1); // 添加一个元素
    vec.push_back(2); // 添加另一个元素

    std::cout << "Size after adding elements: " << vec.size() << std::endl;
    std::cout << "Capacity after adding elements: " << vec.capacity() << std::endl;

    return 0;
}

其输出结果为:

Current size: 0
Current capacity: 0
New size after reserve: 0
New capacity after reserve: 10
Size after adding elements: 2
Capacity after adding elements: 10

1.6 vector扩容倍数

vector扩容机制在不容编译器下面是不同
在g++下是2倍增长
而在vs下是按照1.5倍增长的

#include <vector>
#include <iostream>

using namespace std;

//测试vector的默认扩容机制

void TestVectorExpand()
{
	size_t sz;
	vector<int> v;
	sz = v.capacity();
	cout << "making v grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

int main()
{
	TestVectorExpand();
}

其输出结果如下:

making v grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141

可以看到其增长速度基本是以1.5倍的速度增长的。

1.7 vector的增删查改

1.push_back()

该函数每次将一个元素添加到当前vector对象的末尾。

以下是该元素的简单用法:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;
    vec.push_back(10);
    vec.push_back(20);
    vec.push_back(30);

    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果如下:

10 20 30

2.pop_back()

这个函数用于移除当前函数的末尾元素,每次只能移除一个元素。

以下是其简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {10, 20, 30};
    vec.pop_back();

    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

10 20

3.at()

这个函数用与访问vector中指定位置的元素,因为vector是连续存储的,所以可以通过索引来访问vector中的元素。其索引从0开始。同时该函数提供了越界检查,如果访问超出vector的有效范围,将会抛出std::out_of_range异常。注意是vector的大小,也就是说访问的位置大于等于其大小,就会抛出异常。

以下是其简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {10, 20, 30};
    std::cout << vec.at(1) << std::endl;

    return 0;
}

输出结果为:20

4.operator[ ]

该函数同at()的用法基本一致,都是用于访问vector中指定位置的元素。不同的是,该函数不会进行越界检查。也就是说如果越界,会产生未定义行为,即程序的行为并不确定,可能崩溃。

以下是其简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3};

    std::cout << "Initial vector: ";
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;

    // 修改向量中的元素
    vec[1] = 5;

    std::cout << "Modified vector: ";
    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果如下:

Initial vector: 1 2 3
Modified vector: 1 5 3

5.insert()函数

这个函数用于在指定位置插入一个或多个元素到向量中。

其函数的原型如下:

iterator insert(iterator position, const T& value);
iterator insert(iterator position, size_type count, const T& value);
template <class InputIterator>
iterator insert(iterator position, InputIterator first, InputIterator last);

可以看到其有三个重载形式。

position:要插入元素的位置的迭代器。

value:要插入的元素的值。

count:要插入的元素的数量。

first和last:表示要插入元素范围的迭代器。

insert函数会在指定位置插入元素,并返回指向插入的第一个元素的迭代器。如果插入多个元素,则返回指向第一个插入元素的迭代器。

以下是其简单的用法示例:

std::vector<int> vec = {1, 2, 3, 4, 5};

// 在位置2插入元素6
vec.insert(vec.begin() + 2, 6);

// 在位置3插入3个元素9
vec.insert(vec.begin() + 3, 3, 9);

// 在位置4插入另一个向量的元素
std::vector<int> anotherVec = {10, 11, 12};
vec.insert(vec.begin() + 4, anotherVec.begin(), anotherVec.end());

需要注意的是,由于insert插入元素之后,可能导致容量的变化,也就导致其存储的内存空间会发生变化,从而可能导致迭代器失效。因为迭代器底层封装的是指针,其指向内存地址。而现在内存空间可能发生变化,因此迭代器可能失效。

6.erase()函数

erase函数用于从vector对象中删除一个或多个元素。可以根据指定的位置或范围进行删除,并返回指向被删除元素之后的位置的迭代器。

erase()函数有着两种常用的用法:

a.删除单个元素:指定要删除的元素的位置。

b.删除一段元素:执行要删除的元素范围。

删除单个元素的用法示例:

std::vector<int> vec = {1, 2, 3, 4, 5};

// 删除第三个元素
vec.erase(vec.begin() + 2);

// 输出: 1 2 4 5
for (int num : vec) {
    std::cout << num << " ";
}
std::cout << std::endl;

其输出为:

1 2 4 5

删除一段元素的用法示例:

std::vector<int> vec = {1, 2, 3, 4, 5};

// 删除第二个到第四个元素(包括第二个和第四个)
vec.erase(vec.begin() + 1, vec.begin() + 4);

// 输出: 1 5
for (int num : vec) {
    std::cout << num << " ";
}
std::cout << std::endl;

其输出为:

1 5

需要注意的是:erase也有返回值,其返回值为删除元素的下一个位置的迭代器。删除范围元素也是一样,会返回这段范围的最后一个元素的下一个位置的迭代器。

还需要注意的是,erase同样可能导致vector的原有容量大小发什了变化,所以同样可能导致迭代器失效。

7.find()

find函数用于在vector对象中查找到指定值的元素,并且返回一个指向该元素的迭代器,如果未找到,则返回指向vector末尾的迭代器,也就是最后一个元素的的下一个位置的迭代器。

其函数原型为:

iterator find (const T& value);
const_iterator find (const T& value) const;

其中,T为vector对象中存储的元素类型,iterator为可修改元素的迭代器类型,const_iterator为只读元素的迭代器类型。

下面是一个简单的示例:用于演示如何使用find函数在vector中查找特定元素

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // 查找元素3
    auto it = std::find(vec.begin(), vec.end(), 3);

    if (it != vec.end()) {
        std::cout << "Element found: " << *it << std::endl;
    } else {
        std::cout << "Element not found" << std::endl;
    }

    return 0;
}

其输出结果为:

Element found: 3

8.swap()

swap函数用于交换两个vector的内容。它接受另一个容器作为参数,并将两个容器的元素进行交换。不过swap函数只会交换两个容器的内容,而不会影响两个容器的容量,交换后的两个容器仍然保留其各自的容量和其他属性。在交换的过程中,swap函数会交换两个向量的内部指针,从而实现高效的交换操作。

以下是其简单的用法示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::vector<int> vec2 = {4, 5, 6};

    std::cout << "Before swap:" << std::endl;
    std::cout << "vec1: ";
    for (int num : vec1) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "vec2: ";
    for (int num : vec2) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    vec1.swap(vec2);

    std::cout << "After swap:" << std::endl;
    std::cout << "vec1: ";
    for (int num : vec1) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "vec2: ";
    for (int num : vec2) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

以下是其输出结果:

Before swap:
vec1: 1 2 3
vec2: 4 5 6
After swap:
vec1: 4 5 6
vec2: 1 2 3

1.8迭代器失效

迭代器失效指的是在对容器进行操作(如插入、删除、修改元素)后,之前获取二点迭代器可能不再有效或指向了错误的位置。这是因为容器的内部结构发生了改变,导致原先的迭代器无法正确的访问容器的元素。

迭代器失效的情况因容器类型和操作类型而异常,不同的容器和操作会有不同的迭代器失效规则。一般而言,以下操作可能导致迭代器失效:

1.插入元素:如果插入元素之后容器的容量足够容纳新的元素,不需要内存重新分配的话,在插入元素后,插入点之前的迭代器保持有效,但插入点之后的迭代器可能失效。因为插入点之后的所有元素都往后移动了一个元素的位置,而原先的迭代器指向的位置是前一个位置。如果插入元素之后,需要扩容,则所有的迭代器都会失效。因为其重新分配了一个新的内存空间用以存储原先的元素,原先的迭代器指向的还是原来的内存空间。

2.删除元素:在删除元素后,被删除元素之后的迭代器可能失效。这个原因都上述原因基本相同,也是因为删除元素后,被删除元素之后的所有元素全部往前移动了删除元素个数的位置。而迭代器还是指向原先的位置。

3.改变容器大小:改变容器大小,可能导致所有迭代器失效,因为容器的内存重新分配会导致指向就内存的迭代器变得无效。

4.清空容器:使用clear()函数清空容器会导致所有迭代器失效。清空之后,所有元素的都被移除,原先的迭代器将无法指向有效的元素。

5.在迭代过程中对容器进行修改:在使用迭代器遍历容器时,对容器进行插入、删除、改变大小等操作会导致迭代器失效。因为其同样会移动元素的位置。

八:list容器

1.list的介绍

std::list是C++标准库中的一个双向链表容器,它可以存储任意类型的元素,并且支持高效的插入和删除操作。与std::vector不同,std::list在内存中使用链表的形式存储元素,而不是连续的内存块。所以其不能随机读取,只能遍历链表从而找到想要读取的节点。

std::list是一个双向链表节点,它不包含头节点。相比于其他容器,std::list的实现不需要头节点来维护容器的结构。

其特点如下:

1.双向性:std::list是双向链表,每个节点都包含指向前一个节点和后一个节点的指针,这使得在列表中的任意位置进行插入、删除操作都非常高效。

2.动态大小:std::list的大小可以根据许哟啊动态增长或缩小,不需要预先指定容器的大小。因为其扩容相比vector消耗小很多。

3.遍历访问:由于std::list是一个链表,访问元素时需要从头或尾部开始遍历链表,因此不支持使用下标进行随机访问。要访问特定位置的元素,需要通过迭代器进行遍历。

4.高效的插入和删除:std::list提供了高效的插入和删除操作。在链表中插入或删除元素的时间复杂度为O(1),而在std::vector中,如果插入或删除操作发生在中间或开头位置,则需要移动后续元素,时间复杂度为O(n)。

5.不连续的内存:与std::vector不同,std::list的元素在内存中并不连续存储,而是通过节点的指针链接在一起。这样使得插入和删除操作中不需要移动大量元素,更加高效。

2.list的使用

2.1list的构造

1.默认构造函数:std::list myList;

该函数的作用是创建一个空的std::list对象,其中T是链表存储的元素类型。

2.指定大小和默认值的构造函数:std::list myList(size, value);

创建一个包含size个元素的链表,并使用value初始化每个元素的值。

3.范围构造函数:std::list myList(first, last);

从迭代器first指向的位置开始,复制到迭代器last指向的位置结束(不包括last本身),创建一个新的链表。

4.拷贝构造函数:std::list myList(otherList);

使用另一个std::list对象otherList的副本创建一个新的链表。

以下是这些构造函数的简单示例用法:

#include <iostream>
#include <list>

int main() {
    // 默认构造函数
    std::list<int> myList1;

    // 指定大小和默认值的构造函数
    std::list<int> myList2(5, 10); // 包含 5 个值为 10 的元素

    // 范围构造函数
    std::list<int> sourceList = {1, 2, 3, 4, 5};
    std::list<int> myList3(sourceList.begin() + 1, sourceList.end() - 1); // 从第二个元素到倒数第二个元素创建链表

    // 拷贝构造函数
    std::list<int> myList4(myList2);

    return 0;
}

2.2 list的迭代器:

a.begin()和end():这两个迭代器,用于返回第一个元素的迭代器和最后一个元素的下一个位置的迭代器。

b. rbegin()和rend():这两个反向迭代器,用于返回最后一个元素的位置和第一个元素之前的位置。

2.3 list的容量相关函数

a.empty():用于检查列表是否为空。他返回一个布尔值,表示列表是否为空。当列表为空时,即没有任何元素存储在列表中时,返回true;如果列表包含至少一个元素,则返回false。

以下是empty()函数的简单示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList;

    if (myList.empty()) {
        std::cout << "List is empty." << std::endl;
    } else {
        std::cout << "List is not empty." << std::endl;
    }

    myList.push_back(42);

    if (myList.empty()) {
        std::cout << "List is empty." << std::endl;
    } else {
        std::cout << "List is not empty." << std::endl;
    }

    return 0;
}

其输出结果如下:

List is empty.
List is not empty.

b.size():这个函数用于返回列表中元素的数量。size()函数没有参数,它返回一个整数,表示列表中元素的数量。

以下是其简单的用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    std::cout << "Size of list: " << myList.size() << std::endl;

    return 0;
}

其输出结果如下:

Size of list: 5

c.front():用于获取list列表的第一个元素的引用,这个函数是个无参函数,其返回值是一个引用。

以下是其简单的用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    int& firstElement = myList.front();
    std::cout << "First element: " << firstElement << std::endl;

    return 0;
}

其输出结果为:

First element: 1

d.back():这个函数获取list列表的最后一个元素的引用,其同样是一个无参函数。

以下是其简单的用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    int& lastElement = myList.back();
    std::cout << "Last element: " << lastElement << std::endl;

    return 0;
}

其输出结果为:

Last element: 5
2.4 list容器的修改操作

1.push_front:这个函数接受一个参数,该参数是要插入到列表开头的元素的值或对象。这个函数的作用是将元素插入到列表的开头位置。

以下是这个函数的简单用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {2, 3, 4, 5};

    myList.push_front(1);

    for (int num : myList) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

1 2 3 4 5

可以看到我们在原链表的开头位置插入了一个元素,其值为1。

2.pop_front():这个函数没有参数,会直接删除列表的第一个元素,并将列表中的其他元素向前移动,填补删除元素的空缺。

以下是其简单的用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    myList.pop_front();

    for (int num : myList) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果如下:

2 3 4 5

可以看到我们把原链表的第一个元素删除了。

3.push_back():这个函数接受一个参数,即要插入的元素的值,他将该元素添加到列表的末尾。这个参数可以是指针也可以是引用,它们将被隐式的转换为列表所存储的元素类型。

以下是其简单的用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    myList.push_back(6);

    for (int num : myList) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

1 2 3 4 5 6

4.pop_back():这个函数是个无参函数,用于删除列表的第一个元素,并将列表中的其他元素向前移动,填补删除元素的空缺。

以下是其简单的用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    myList.pop_front();

    for (int num : myList) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

2 3 4 5

5.insert():这个函数用于在指定位置插入一个或多个元素到列表中。其有多个重载形式,提供了灵活的用法。该函数的返回值是一个迭代器,指向插入的第一个元素。如果插入了多个元素,同样返回指向第一个插入元素的迭代器。

a.插入单个元素:在position所指示的位置之前插入一个新元素,其值为value, 类型为T。

iterator insert (const_iterator position, const T& value);

b.插入多个元素:在position所指示的位置之前插入n个新元素,每个元素的值都为value,类型为T。

void insert (const_iterator position, size_type n, const T& value);

c.使用迭代器区间插入:在position所指示的位置之前插入范围[first,last)内的元素。

template <class InputIterator>
void insert (const_iterator position, InputIterator first, InputIterator last);

以下是上述函数的简单用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    // 在位置 2 插入新元素 10
    auto it = std::next(myList.begin(), 2);
    myList.insert(it, 10);

    // 在位置 4 插入三个新元素,值为 20
    it = std::next(myList.begin(), 4);
    myList.insert(it, 3, 20);

    // 在位置 0 插入另一个列表的元素
    std::list<int> anotherList = {30, 40, 50};
    myList.insert(myList.begin(), anotherList.begin(), anotherList.end());

    for (int num : myList) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

以下是上述代码的输出结果:

30 40 50 1 2 10 3 20 20 20 4 5

6.erase():这个函数是用于从std::list容器中删除一个或多个元素的成员函数。它接受一个迭代器作为参数,并在该迭代器指向的位置上删除元素。需要注意的是,删除之后,这个迭代器就失效了,但是其他的迭代器无影响。

erase()函数有两个重载形式:

a.iterator erase(iterator position); 用于删除指定位置的元素,并返回指向删除元素后面元素的迭代器。

b.iterator erase(iterator first, iterator last); 删除指定范围内的元素,即从first到last(不包括last)之间的元素,并且返回指向删除元素后面元素的迭代器。

以下是上述代码的简单用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};

    auto it = myList.begin();
    std::advance(it, 2);  // 移动迭代器到位置 2

    it = myList.erase(it);  // 删除位置 2 的元素

    for (int num : myList) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

1 2 4 5

7.swap():这个函数用于交换两个list容器的内容,不会影响容器的其他属性,如容量、分配器等。也就是说,swap函数会交换两个容器内部的指针,以及两个容器的链表结构。

void swap(list& other);

以下是该函数的简单用法示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> list1 = {1, 2, 3};
    std::list<int> list2 = {4, 5, 6};

    std::cout << "Before swap:" << std::endl;
    std::cout << "list1: ";
    for (int num : list1) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "list2: ";
    for (int num : list2) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    list1.swap(list2);

    std::cout << "After swap:" << std::endl;
    std::cout << "list1: ";
    for (int num : list1) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "list2: ";
    for (int num : list2) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果如下:

Before swap:
list1: 1 2 3
list2: 4 5 6
After swap:
list1: 4 5 6
list2: 1 2 3

8.clear():这个函数用于清空容器中的所有元素,即将容器恢复到初始状态,不保留任何元素。但是其容量不受影响。

3.list和vector的对比

  1. 存储方式:std::list 是双向链表,而 std::vector 是动态数组。这意味着在内存中存储和访问元素的方式不同。
  2. 内存管理:std::list 动态分配节点,每个节点包含一个元素和指向前一个和后一个节点的指针。而 std::vector 在连续的内存块中存储元素。
  3. 插入和删除操作:由于 std::list 是链表,插入和删除元素的时间复杂度为 O(1),不受容器大小的影响。而 std::vector 插入和删除元素可能涉及内存重新分配和移动元素,时间复杂度为 O(n),其中 n 是容器的大小。
  4. 随机访问:std::list 不支持常量时间的随机访问,需要通过迭代器进行顺序访问。而 std::vector 可以通过索引直接访问元素,具有常量时间的随机访问能力。
  5. 内存占用:由于 std::list 动态分配节点,每个节点都需要额外的指针开销,因此在相同数量的元素情况下,它通常比 std::vector 占用更多的内存空间。
  6. 迭代器失效:对于 std::list,插入和删除操作不会使得迭代器失效,而 std::vector 的插入和删除操作可能会使得迭代器失效。

根据以上特点,可以根据具体需求选择合适的容器。如果需要频繁的插入和删除操作,并且不要求随机访问,则 std::list 是一个不错的选择。如果需要高效的随机访问和在尾部进行插入和删除操作,则 std::vector 更为适合。

九.stack和queue

1.stack的介绍:

在C++中,std::stack是标准库提供的一个容器适配器,用于实现后进先出的栈数据结构。它基于其他容器实现,通常使用 std::deque 作为默认底层容器。

容器适配器基于现有的容器类型,提供了一种不同的接口和功能。其设计目的是通过封装现有容器的特性,以简化和提供特定的容器行为。容器适配器提供了一组特定的操作和接口,使其更适合特定的场景和使用方式。它们的接口可能会限制或修改原始容器的某些功能,以确保其符合适配器的特性。总而言之,容器适配器是一种用于修改现有容器行为的工具,通过封装现有容器的功能和接口,提供了一种简化和特定功能的容器。

以下是std::stack的一些重要特性和功能:

1.栈特性:std::stack类型封装了栈的基本特性,仅支持在栈顶进行元素的插入和删除操作。他不提供迭代器,因此不能直接访问栈中的其他元素。

2.底层容器:std::stack可以使用不同的底层容器实现,包括std::deque、std::vector和std::list等。可以通过指定第二个模板参数来选择底层容器类型。例如:

std::stack<int, std::vector<int>>

使用std::vector作为底层容器。

3.入栈和出栈操作:std::stack提供了push()和pop()成员函数用于将元素推入栈顶和从栈顶弹出元素。这些操作在栈顶执行,不会对其他元素产生影响。

4.访问栈顶元素:std::stack提供了top()成员函数,用于返回栈顶的元素,但并不将其从栈中移除。

下面是std::stack的简单用法:

#include <iostream>
#include <stack>

int main() {
    std::stack<int> myStack;

    // 入栈操作
    myStack.push(10);
    myStack.push(20);
    myStack.push(30);

    // 访问栈顶元素
    std::cout << "Top element: " << myStack.top() << std::endl;

    // 出栈操作
    myStack.pop();

    // 访问栈顶元素
    std::cout << "Top element: " << myStack.top() << std::endl;

    // 检查栈是否为空
    if (myStack.empty()) {
        std::cout << "Stack is empty" << std::endl;
    } else {
        std::cout << "Stack is not empty" << std::endl;
    }

    // 输出栈的大小
    std::cout << "Stack size: " << myStack.size() << std::endl;

    return 0;
}

以下是其输出结果:

Top element: 30
Top element: 20
Stack is not empty
Stack size: 2
1.1最小栈:
#include <stack>

class MinStack
{
public:
	void push(int x)
	{
		//只要是压栈,先将元素保存到_elem中
		_elem.push(x);

		//如果x小于_min中栈顶的元素,将x再压入_min中
		if (_min.empty() || x <= _min.top())
		{
			_min.push(x);
		}
	}

	void pop()
	{
		//如果_min栈顶的元素等于出栈的元素,_min顶的元素要移除
		if (_min.top() == _elem.top())
		{
			_min.pop();
		}

		_elem.pop();
	}

	int top()
	{
		return _elem.top();
	}

	int getMin()
	{
		return _min.top();
	}


private:
	//保存栈中的元素
	std::stack<int> _elem;

	//保存栈的最小值
	std::stack<int> _min;
};
1.2栈的弹出压入序列:
#include <vector>
#include <stack>

class Solution
{
public:
	bool IsPopOrder(std::vector<int> pushV, std::vector<int> popV)
	{
		//入栈和出栈的元素个数必须相同
		if (pushV.size() != popV.size())
		{
			return false;
		}

		//用s来模拟入栈和出栈的过程。

		int outIdx = 0;
		int inIdx = 0;
		std::stack<int> s;

		while (outIdx < popV.size())
		{
			//如果s是空,或者栈顶元素与出栈的元素不相等,就入栈
			while (s.empty() || s.top() != popV[outIdx])
			{
				if (inIdx < pushV.size)
				{
					s.push(pushV[inIdx++]);
				}
				else
				{
					return false;
				}
			}

			//栈顶元素与出栈的元素相等,出栈
			s.pop();
			outIdx++;
		}
		return true;
	}
};
1.3stack的模拟实现

下面使用vector容器来模拟实现stack

#include <vector>

template <class T>
class stack
{
public:
	stack()
	{}
	
	void push(const T& x)
	{
		_c.push_back(x);
	}

	void pop()
	{
		_c.pop_back();
	}

	T& top()
	{
		return _c.back();
	}

	const T& top() const
	{
		return _c.back();
	}

	size_t size() const
	{
		return _c.size();
	}

	bool empty()
	{
		return _c.empty();
	}

private:
	std::vector<T> _c;
};

2.queue的介绍和使用

2.1queue的介绍

在C++中,queue是一种容器适配器,它提供了先进先出的数据结构,类似于现实生活中的队列。队列中的元素按照插入的顺序排列,新的元素总是在队列的末尾添加,而从队列中移除元素时,总是从队列的头部移除。

queue作为一种容器适配器,同样是基于其他的底层容器实现的,常用的底层容器有deque和list,默认情况下是使用deque作为底层容器。

以下是一些常用的queue操作:

  1. push(element):将元素 element 添加到队列的末尾。
  2. pop():移除队列头部的元素,不返回任何值。
  3. front():返回队列头部的元素,但不移除它。
  4. back():返回队列尾部的元素,但不移除它。
  5. empty():检查队列是否为空,如果为空则返回 true,否则返回 false
  6. size():返回队列中元素的个数。
#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;

    // 添加元素到队列
    q.push(10);
    q.push(20);
    q.push(30);

    // 获取队列的头部元素
    std::cout << "Front element: " << q.front() << std::endl;

    // 移除队列的头部元素
    q.pop();

    // 获取队列的新头部元素
    std::cout << "Front element after pop: " << q.front() << std::endl;

    // 检查队列是否为空
    if (q.empty()) {
        std::cout << "Queue is empty" << std::endl;
    } else {
        std::cout << "Queue is not empty" << std::endl;
    }

    // 获取队列中的元素个数
    std::cout << "Queue size: " << q.size() << std::endl;

    return 0;
}
2.2queue的模拟实现:
#include <list>

template<class T>
class queue
{
public:
	queue()
	{}

	void push(const T& x)
	{
		_c.push_back(x);
	}

	void pop()
	{
		_c.pop_front();
	}

	T& back()
	{
		return _c.back();
	}

	const T& back() const
	{
		return _c.back();
	}

	T& front()
	{
		return _c.front();
	}

	const T& front() const
	{
		return _c.front;
	}

	size_t size() const
	{
		return __c.size();
	}

	bool empty() const
	{
		return _c.empty();
	}

private:
	std::list<T> _c;
};

3.priority_queue的介绍和使用

C++中,priority_queue是一种容器适配器,它提供了一个优先级队列的数据结构。也就是说,其中的元素按照一定的优先级进行排序,具有最高优先级的元素始终处于队列的前部。

默认情况下,priority_queue使用vector作为底层容器,并且通过堆这种数据结构来实现元素的排序。通过自定义的比较函数,可以对元素进行自定义的优先级排序。因此需要支持随机访问迭代器,以便始终在内部保持堆结构。

以下是一些常用的priority_queue操作:

  1. push(element):将元素 element 添加到优先级队列中,并根据优先级重新排序。
  2. pop():移除优先级队列的最高优先级元素,不返回任何值。
  3. top():返回优先级队列的最高优先级元素,但不移除它。
  4. empty():检查优先级队列是否为空,如果为空则返回 true,否则返回 false
  5. size():返回优先级队列中元素的个数。
#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> pq;

    // 添加元素到优先级队列
    pq.push(30);
    pq.push(10);
    pq.push(50);

    // 获取优先级队列的最高优先级元素
    std::cout << "Top element: " << pq.top() << std::endl;

    // 移除优先级队列的最高优先级元素
    pq.pop();

    // 获取优先级队列的新最高优先级元素
    std::cout << "Top element after pop: " << pq.top() << std::endl;

    // 检查优先级队列是否为空
    if (pq.empty()) {
        std::cout << "Priority queue is empty" << std::endl;
    } else {
        std::cout << "Priority queue is not empty" << std::endl;
    }

    // 获取优先级队列中的元素个数
    std::cout << "Priority queue size: " << pq.size() << std::endl;

    return 0;
}

a.默认情况下,优先级队列(priority_queue)采用的是大堆。

#include <vector>
#include <queue>
#include <functional>
#include <iostream>

void TestPriorityQueue()
{
	//默认情况下,创建的是大堆,其底层按照小于号比较
	std::vector<int> v{3, 2, 7, 6, 0, 4, 1, 9, 8, 5};
	std::priority_queue<int> q1;

	for (auto& e : v)
	{
		q1.push(e);
	}
	std::cout << q1.top() << std::endl;

	//如果要创建小堆,将第三个模板参数换成greater比较方式
	//std::priority_queue<int, std::vector<int>, std::greater<int>> q2;
}

int main()
{
	TestPriorityQueue();
}

其输出结果为:

9

b.如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供大于号( > )或者小于号( < )的重载。

#include <iostream>
#include <queue>
#include <vector>

class Date
{
public:
	Date(int year = 2023, int month = 7, int day = 4)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	template <typename T>
	struct MyShorter {
		bool operator()(const T& d1, const T& d2) const {
			return (d1._year < d2._year) ||
				(d1._year == d2._year && d1._month < d2._month) ||
				(d1._year == d2._year && d1._month == d2._month && d1._day < d2._day);
		}
	};

	template <typename T>
	struct MyGreater {
		bool operator()(const T& d1, const T& d2) const {
			return (d1._year > d2._year) ||
				(d1._year == d2._year && d1._month > d2._month) ||
				(d1._year == d2._year && d1._month == d2._month && d1._day > d2._day);
		}
	};

	friend std::ostream& operator<<(std::ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}

private:
	int _year;
	int _month;
	int _day;
};

void TestPriorityQueue()
{
	// 大堆,使用自定义的 MyShorter 结构体
	std::priority_queue<Date, std::vector<Date>, Date::MyShorter<Date>> q1;
	q1.push(Date(2023, 10, 29));
	q1.push(Date(2023, 10, 28));
	q1.push(Date(2023, 10, 30));
	std::cout << q1.top() << std::endl;

	// 小堆,使用自定义的 MyGreater 结构体
	std::priority_queue<Date, std::vector<Date>, Date::MyGreater<Date>> q2;
	q2.push(Date(2023, 10, 29));
	q2.push(Date(2023, 10, 28));
	q2.push(Date(2023, 10, 30));
	std::cout << q2.top() << std::endl;
}

4.容器适配器

4.1 什么是容器适配器

适配器是一种设计模式(设计模式是一套被反复使用的,多数人知晓的、经过分类编目的、代码设计经验的总结),适配器模式是将一个类的接口转换成客户希望的另外一个接口。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZebfOyb-1688661160534)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704133824309.png)]

4.2 STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和queue只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iFGXAX4L-1688661160534)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704134039587.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oziVsdvz-1688661160534)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704134054597.png)]

4.3 deque的简单介绍
4.3.1 deque的介绍

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成,实际deque类似于一个动态的二维数组。

4.3.2 deque的缺陷

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬大量的元素,因此其效率时比vector高的。

与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实例中,需要线性结构时,大多数情况还是优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

4.4 为什么选择deque作为stack和queue的底层默认容器

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器。比如vector和list都可以;queue是先进先出的特殊线性结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:

a.stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一段或者两端进行操作。

b.在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时候,deque不仅效率高,而且内存使用率高。

十:模板进阶

1.非类型模板参数

模板参数分为类型形参和非类型形参。

类型参数:即出现在模板参数列表中,跟在class或者typename之后的参数类型名称。

非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。非类型模板参数可以是整数、枚举、指针、引用或指向对象的指针,但不能是浮点数、类对象以及字符串。并且分类型模板参数必须在编译器就能确认结果,使得编译器可以根据不同的参数值生成不同的代码。

非类型模板参数有以下几个特点:

1.必须在编译时就能确定值。

2.可以用于类型推断和函数重载。

3.可以在模板中使用,例如作为数组大小、模板实例化数量等。

4.可以具有限定条件,例如必须是整数类型、枚举类型等。

//定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
	T& operator[](size_t index)
	{
		return _array[index];
	}

	const T& operator[](size_t index) const
	{
		return _array[index];
	}

	size_t size() const
	{
		return _size;
	}

	bool empty()
	{
		return 0 == _size();
	}

private:
	T _array[N];
	size_t _size;
};

以上代码定义了一个模板类 array,他表示一个静态数组,具有固定的大小N。也就是说该模板类实例化出的数组是不能改变大小的,因为在编译器就已经确定了其的大小为N。

2.模板的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用于进行小于比较的函数模板。

#include <iostream>

using std::cout;
using std::endl;

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确
	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确
	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; // 可以比较,结果错误
	return 0;
}

这段代码中,Less多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述实例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址吗,这就无法达到与其而错误。

此时,就需要对模板进行特化。即:在原模板的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化。

2.1函数模板特化

函数模板的特化步骤:

1.必须要先有一个基础的函数模板

2.关键字template后面接一堆空的尖括号<>

3.函数名后跟一对尖括号<>,尖括号中指定需要特化的类型。

4.函数形参表:必须要和模板函的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。


#include <iostream>

// 通用的模板函数
template <typename T>
void PrintType() {
    std::cout << "Unknown type" << std::endl;
}

// 针对int类型的模板特化
template <>
void PrintType<int>() {
    std::cout << "Type: int" << std::endl;
}

// 针对double类型的模板特化
template <>
void PrintType<double>() {
    std::cout << "Type: double" << std::endl;
}

int main() {
    PrintType<char>();     // 输出:Unknown type
    PrintType<int>();      // 输出:Type: int
    PrintType<double>();   // 输出:Type: double

    return 0;
}
2.2类模板特化
2.2.1 全特化

全特化即是将模板参数列表中所有的参数都确定化。

#include <iostream>
using std::cout;
using std::endl;

template<class T1, class T2>
class Data
{
public:
	Data()
	{
		cout << "Data< T1, T2>" << endl;
	}

private:
	T1 _d1;
	T2 _d2;
};

template<>
class Data<int, char>
{
public:
	Data()
	{
		cout << "Data<int, char>" << endl;
	}
private:
	int _d1;
	char _d2;
};

void Test()
{
	Data<int, int>d1;
	Data<int, char> d2;
}

int main()
{
	Test();
}

其输出结果如下:

Data< T1, T2>
Data<int, char>
2.2.2 偏特化

偏特化:任何针对模板参数进一步进行条件设计的特化版本。或者说,只要部分修改了对与模板参数的限制条件,就可以说是偏特化。

比如针对以下的模板类:

template<class T1, class T2>
class Data
{
public:
 Data() {cout<<"Data<T1, T2>" <<endl;}
private:
 T1 _d1;
 T2 _d2;
};

偏特化有以下两种表现方式:

a.部分特化

将模板参数类表中的一部分参数特化。

#include <iostream>
using std::cout;
using std::endl;

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};


template<class T1>
class Data<T1, int>
{
public:
	Data()
	{
		cout << "Data<T1, int>" << endl;
	}
private:
	T1 _d1;
	int _d2;
};

b.参数更进一步的限制

偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。

#include <iostream>
using std::cout;
using std::endl;

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};

template<class T1>
class Data<T1, int>
{
public:
	Data()
	{
		cout << "Data<T1, int>" << endl;
	}
private:
	T1 _d1;
	int _d2;
};

template<typename T1, typename T2>
class Data <T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*>" << endl;
	}

private:
	T1* _d1;
	T2* _d2;
};

//两个参数偏特化为引用类型
template<typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data(const T1& d1, const T2& d2)
		:_d1(d1)
		,_d2(d2)
	{
		cout << "Data<T1&, T2&>" << endl;
	}

private:
	const T1& _d1;
	const T2& _d2;
};

void test()
{
	Data<double, int> d1; // 调用特化的int版本
	Data<int, double> d2; // 调用基础的模板 
	Data<int*, int*> d3; // 调用特化的指针版本
	Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

int main()
{
	test();
}

以下是上述代码的输出结果:

Data<T1, int>
Data<T1, T2>
Data<T1*, T2*>
Data<T1&, T2&>

3.模板分离编译

3.1什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

3.2模板的分离编译

将模板的声明与定义分离开来,在头文件中声明,源文件中完成定义。但是要想使用模板,必须要让模板的定义在使用它的源文件中可见,以便进行实例化。这意味着在需要使用模板的源文件中,需要包含模板的定义或将模板定义的文件包含进来。要想确保无误,可以将声明和定义放在一个头文件中,在引用头文件即可。

C/C++程序要运行,一般要经历以下步骤:

预处理 -> 编译 ->汇编 ->链接。

编译:对程序按照语言特性进行词法、语法、语义分析,错误检查无误后生成汇编代码。注意头文件不参与编译,编译器对工程中的多个源文件是分离开单独编译的。

链接:将多个obj文件合并成一个,并处理没有解决的地址问题。

C++进阶篇:

一.继承

1.基层的概念及定义

1.1继承的概念

继承是面向对象编程中的一种重要概念,它允许一个类(称为子类或者派生类)从另一个类(称为父类或基类)继承属性和方法。通过继承,子类可以获得父类的数据成员和成员函数,并且可以添加自己的额外成员和方法。继承实现了代码的重用和扩展,使得在父类的基础上创建新的类变得更加方便。

在继承关系中,子类继承了父类的所有成员,即父类的所有公有、受保护成员和私有成员。但是父类的私有成员对子类来说是不可访问的。父类可以通过继承获得父类的接口和实现,并可以对齐进行修改、扩展或重写。

继承也有权限限定符。通常使用public、protected或provate来指定继承的访问级别。public继承表示子类继承的父类成员在子类中的访问级别同父类的访问级别一致,即父类的受保护成员在子类这里依然是受保护的,公有成员依然是公有的。而protected继承表示子类继承的父类成员在子类中的访问权限是受保护的,即子类的公有成员在子类中是受保护的,类外不能访问。private继承表示子类继承的所有成员,在子类中的访问权限全是私有的,不管是类外还是派生类都无权访问。

继承还支持多层次继承,即派生类或者说子类可以是另一个类的父类,形成继承链。继承是类设计层面的复用。

#include <iostream>
#include <string>

using std::cout;
using std::endl;

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

private:
	std::string _name = "peter";    //姓名
	int _age = 18;                         //年龄
};


//继承后,父类Person的成员都会变成子类的一部分。
//这里体现了Student和Teacher复用了Person的成员。
class Student : public Person
{
protected:
	int _stuid;     //学号
};


class Teacher : public Person
{
protected:
	int _jobid;     //工号
};

int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();
}

上述这段代码的输出结果为:

name:peter
age:18
name:peter
age:18

可以清晰的看到这两个派生类复用了父类Person的成员函数。

1.2 继承定义
1.2.1定义格式

下面我们可以看到Person是父类,也称作基类。Student是子类,也称作派生类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r7cWHt1Y-1688661160534)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704195225939.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5vXkuOLm-1688661160534)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704195247234.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sHQIcDFs-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704195325482.png)]

2.基类和派生类对象转换

派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫做切片或者切割,即将派生类中继承父类的部分切来赋值过去。

基类对象不能赋值给派生类对象,因为基类对象中不含有派生类衍生的变量,不能为其赋值。

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针指向派生类对象时才是安全的。因为派生类包含了基类的成员和特性;反过来则不行,因为派生列中可能包含基类无法处理的额外成员和特性。这里基类如果是多态类型,即基类和派生类之间存在虚函数。可以使用RTTI的dynamic_cast来进行识别后进行安全转换。

#include <iostream>
#include <string>

using std::cout;
using std::endl;

class Person
{
protected:
	std::string _name; //姓名
	std::string _sex;     //性别
	int _age;               //年龄
};

class Student : public Person
{
public:
	int  _studyid;      //学号
};

void Test()
{
	Student s;

	//1.子类对象可以赋值给父类对象/指针/引用
	Person p = s;
	Person* pp = &s;
	Person& index_p = s;

	//基类对象不能赋值给派生类对象
	//s = p;     //编译器会报错

	//基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &s;
	Student* ps1 = (Student*)pp;    //这种情况转换时时可以的
	ps1->_studyid = 10;

	pp = &p;
	Student* ps2 = (Student*)pp;   //这种情况转换时虽然可以,但是会存在越界访问的问题


	ps2->_studyid = 10;  //这里会报错
}

int main()
{
	Test();
}

上述代码会报错,是因为产生了越界访问的问题。其中,pp原本是一个基类指针,但是强转成了派生类Student指针ps2。但是因为基类对象中没有派生类衍生的属性和成员,就像上述代码中Student类衍生出了一个学号成员。所以在ps2访问这个派生类衍生出来的属性时会发生越界访问,从而报错。

3.继承中的作用域

1.在继承体系中基类和派生列都有独立的作用域。

2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,或者叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显式访问)

3.需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

#include <string>
#include <iostream>

using std::cout;
using std::endl;

//Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
	std::string _name = "小李子";   //姓名
	int _num = 111;                       //身份证号
};


class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "身份证号:" << Person::_num << endl;
		cout << "学号:" << _num << endl;
	}

protected:
	int _num = 999;    //学号
};

void Test()
{
	Student s1;
	s1.Print();
}


int main()
{
	Test();
}

上述代码的输出结果如下:

姓名:小李子
身份证号:111
学号:999

#include <iostream>

// B中的fun和A中的fun不是构成重载,因为不是同一作用域
//B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏
class A
{
public:
	void fun()
	{
		std::cout << "func(A)" << std::endl;
	}
};

class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		std::cout << "func(int i) ->" << i << std::endl;
	}
};

void Test()
{
	B b;
	b.fun(10);
}

int main()
{
	Test();
}

上述代码输出结果为:

func(A)
func(int i) ->10

4.派生类的默认成员函数

C++的类中有六个默认成员函数,默认的意思是如果开发者不手动生成这六个成员函数,编译器会自动生成一个。那么在派生类中的成员函数又会是怎样的呢?

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部份成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。

2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

3.派生类的operator=必须要调用基类的operator=完成基类的复制。

4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序。

5.派生类对象初始化先调用基类构造在调派生类构造

6.派生类对象析构清理先调用派生类析构在调用基类的析构。

7.如果父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。因为编译器可能会对析构函数进行特殊处理,处理成destrutor()。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqL0sWQM-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704213853036.png)]

#include <iostream>

using std::cout;
using std::endl;
class Person
{
public:
    Person(const char* name = "peter")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person & p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;

        return *this;
    }

    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    std::string _name; // 姓名
};


class Student : public Person
{
public:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }

    Student(const Student& s)
        : Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }

    Student& operator = (const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            Person::operator =(s);
            _num = s._num;
        }
        return *this;
    }

    ~Student()
    {
        cout << "~Student()" << endl;
    }
protected:
    int _num; //学号
};

void Test()
{
    Student s1("jack", 18);
    Student s2(s1);
    Student s3("rose", 17);
    s1 = s3;
}


int main()
{
    Test();
}

上述代码的运行结果如下:

Person()
Student()
Person(const Person& p)
Student(const Student& s)
Person()
Student()
Student& operator= (const Student& s)
Person operator=(const Person& p)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()

由此我们可以清晰看到派生类中的默认成员函数的运行情况。

5.继承与友元

友元关系不能继承,也就是说基类友元不能直接访问子类的成员。如果想要使得基类可以访问子类的成员,可以通过虚函数和多态性来6实现基类间接访问子类的成员。

6.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。也就是说静态成员所有对象共享,即静态成员被基类对象和派生类对象所共享。无论派生出多少个子类,都只有一个static成元实例。

7.复杂的菱形继承和菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

菱形继承:菱形继承是多继承的一种特殊情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G80q4tva-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704231947602.png)]

菱形继承的问题:菱形继承会有数据冗余和二义性的问题。

a.冗余问题:由于两个派生类都继承了同一个基类,而派生类之间又没有任何成员变量的定义,导致在派生类中存在冗余的数据。如上述图片中,Person类就被继承了两次,所以在Assistant类中有着两份Person成员。

b.二义性:如果两个基类都定义了相同的成员函数,在派生类中调用这个成员函数时会出现二义性,编译器无法确定应该使用哪个基类的成员函数。

class A
{
public:
	int _a;
};
// class B : public A
class B :  public A
{
public:
	int _b;
};
// class C : public A
class C :  public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

在上述代码运行的过程中,可以选择查看内存来查看具体的内存数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeEIAilD-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230704235511766.png)]

可以看到菱形继承产生了数据冗余的问题。

为了解决菱形继承的问题,C++提供了虚继承(virtual inheritance)的机制。使用虚继承可以避免冗余数据和二义性问题。虚继承的特点是,在派生类对共同基类进行继承时,使用关键字 virtual 来声明继承关系。这样,派生类就只会继承一份共同基类的数据和函数,而不会重复继承。

class A
{
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

上述代码只修改了两个部分,就是在菱形继承的中间节点加上virtual关键字。实现虚继承。需要注意的是virtual应当选在菱形继承的中间节点。即如果现有基类A,派生类B,C继承A。那么应该在B,C继承A的时候使用虚继承,添加virtual关键字。

还是通过查看内存分布,来探究虚继承如何解决数据冗余和二义性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3tdnoo8-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705000010355.png)]

可以看到,原本冗余数据的地方出现了两个指针,这两个指针其实是虚基表指针,指向的是虚基表。虚基表存储的是偏移量,这个偏移量是派生类D中继承来的a的偏移量。现在D中只有这一个a,这样就避免了数据冗余。又因为现在只有这一个变量,所以其不能存放在任何一个基类对象中。

但是其位置需要根据这个偏移量找到,虚基表中存储的这个偏移量是相对派生类对象的起始地址,也就是相对d这个对象在内存中的起始位置的偏移量。

二.多态

1.多态的概念

1.1 概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

2.多态的定义以及实现

2.1 多态的构成条件

多态是在不同继承关系的类对象,去调用同意函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

1.必须通过基类的指针或者引用调用虚函数。

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EaIyHRqr-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705003414831.png)]

2.2 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.3 虚函数的重写

虚函数的重写或者说覆盖,即派生类中有一个跟基类的虚函数的返回值类型、函数名字、参数列表完全相同的虚函数。这也称子类的虚函数重写了基类的虚函数。

#include <iostream>

using std::cout;
using std::endl;

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
	这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

虚函数重写的两个例外:

a.协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};

b.析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,斗鱼基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名并不相同,看起来违反了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,将析构函数的名称统一处理成destructor。

class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;
 return 0;
}
2.4 C++11 override 和 final

从上面的说明中可以看出来,C++对虚函数重写的要求比较严格,但是有些情况由于疏忽,可能会导致函数名 字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的。只有在程序运行时没有得到预期结果在debug有些得不偿失。

因此C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

1.final:用于修饰虚函数,表示该虚函数不能被重写

class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写就编译错误。

class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
2.5 重载、覆盖(重写)、隐藏(重定义)的对比

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBDmyiu3-1688661160535)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705005127949.png)]

3.抽象类
3.1 概念

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化处对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 virtual void Drive()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}
3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

4.多态的原理

4.1 单继承虚函数表
#include <iostream>

class Base
{
public:
	virtual void Func1()
	{
		std::cout << "Func1()" << std::endl;
	}
private:
	int _b = 1;
};

上述代码的中Base类在内存会占用多少字节呢?

通过vs的监视窗口我们可以发现b对象占用了八个字节,其中除了_b成员,还多一个指针放在对象前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DqUby8TE-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705133942544.png)]

对象中的这个指针C++叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中至少都会有一个这样的虚函数表指针,一位虚函数的的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上述代码我们做出一下改造

1.我们增加一个派生类Derive去继承Base

2.Derive中重写Func1

3.Base在增加一个虚函数Func2和一个普通函数Func3

#include <iostream>

class Base
{
public:
	virtual void Func1()
	{
		std::cout << "Base::Func1()" << std::endl;
	}

	virtual void Func2()
	{
		std::cout << "Base::Func2()" << std::endl;
	}

	void	Func3()
	{
		std::cout << "Base::Func3()" << std::endl;
	}

private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		std::cout << "Derive::Func1()" << std::endl;
	}
private:
	int _d = 2;
};


int main()
{
	Base b;
	Derive d;
	return 0;
}

通过观察和测试,我们可以看到发现以下几点:

1.派生类对象d中由两部分构成,一部分是父类继承下来成员,虚表指针也就是存在这继承下来的父类成员中,另一部分是派生类自己的成员。

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存放的是重写Derive::Func1,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4.虚函数表本质上是一个存放虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5.总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6.需要注意的是:虚函数并不存在虚表中,虚表中存放的是虚函数指针,并不是虚函数。虚函数和普通的函数一样,都是存放在代码段中,只是为了实现多态,需要将其的指针存放在虚表中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2725x9tD-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705142840496.png)]

4.2 多继承虚表

多继承虚表同单继承的虚表相差不大,唯一不同的是,派生类中有两份虚表。因为多继承,继承了至少两个类,所以至少会有两张虚表。

但是派生类重写的虚函数,和他新加的虚函数还是都在继承的第一张需表中,按照顺序往下排。

#include <iostream>

class Base
{
public:
	virtual void Func1()
	{
		std::cout << "Base::Func1()" << std::endl;
	}

	virtual void Func2()
	{
		std::cout << "Base::Func2()" << std::endl;
	}

	void	Func3()
	{
		std::cout << "Base::Func3()" << std::endl;
	}

private:
	int _b = 1;
};

class Base2
{
public:
	virtual void	Func4()
	{
		std::cout << "Base::Func4()" << std::endl;
	}
};

class Base3
{
public:
	virtual void	Func5()
	{
		std::cout << "Base::Func5()" << std::endl;
	}
};

class Derive : public Base , public Base2 , public Base3
{
public:
	virtual void Func1()
	{
		std::cout << "Derive::Func1()" << std::endl;
	}

	virtual void	Func4()
	{
		std::cout << "Derive::Func4()" << std::endl;
	}

	virtual void	Func5()
	{
		std::cout << "Derive::Func5()" << std::endl;
	}

	virtual void	Func6()
	{
		std::cout << "Derive::Func6()" << std::endl;
	}

private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tigyfiTl-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705144154788.png)]

4.3 多态的原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qlfGv9G-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705144624780.png)]

这里Func函数传Person调用的 Person类中的虚函数,传Student调用的是Student的虚函数。

#include <iostream>

using std::cout;
using std::endl;

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

1.观察下图的红色箭头我们可以看到,p是指向mike对象时,p->BuyTicket在mike的需表中找到虚函数是Person::BuyTicket。

2.观察下图的蓝色箭头我们看到,p是指向johnson对象时候,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。

3.这样就实现了不同对象去完成同一行为时,展现出不同的形态。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WC7abXLU-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705145706608.png)]

4.反过来思考我们要达到多态,有两个条件,一个数虚函数覆盖,一个是对象的指针或引用调用虚函数。

5.满足多态后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的。不满足多态的函数调用是编译时确认好的。

4.4 动态绑定与静态绑定

a.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。

b.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的程序来确定程序的具体行为,调用具体的函数,也成为动态多态

三:二叉树进阶

1.二叉搜索树

二叉搜索树又称为二叉排序树,他或者是一颗空树,或者是具有以下性质的二叉树:

a.若他的左子树不为空,则左子树所有节点的值都小于根节点的值。

b.若他的右子树不为空,则右子树上所有节点的值都大于根节点的值

c.它的左右子树也分别为二叉搜索树。

2.二叉搜索树操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgzjBIrn-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705152406449.png)]

1.二叉搜索树的查找

a.从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。

b.最多查找高度次,走到空还没找到,这个值就不存在这颗二叉树中。

2.二叉搜索树的插入

a.数为空,则直接新增节点,赋值给root指针。

b.树不空,按二叉搜索树性质查找插入位置,插入新节点。

3.二叉搜索树的删除

首先查找该元素是否在二叉搜索树中,如果不存在,则返回,否则要删除的节点可能分下面四种情况:

a.要删除的节点无孩子节点。

b.要删除的节点只有左孩子节点

c.要删除的节点只有右孩子节点

d.要删除的节点有左、右孩子节点

虽然理论上有上述四种情况,但是实际上a可以和b或c合并起来采用一样的解决方案。因此真正的删除过程有如下三种

情况b:删除该节点且使被删除节点的双亲节点指向被删除节点的左孩子节点 --直接删除

情况c:删除该节点且使被删除节点的双亲节点指向被删除节点的右孩子节点 --直接删除

情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该节点的删除问题 --替换法删除。

2.2 二叉搜索树的实现
template<class T>
struct BSTNode
{
	BSTNode(const T& data = T())
		: _pLeft(nullptr), _pRight(nullptr), _data(data)
	{}
	BSTNode<T>* _pLeft;
	BSTNode<T>* _pRight;
	T _data;
};

template<class T>
class BSTree
{
	typedef BSTNode<T> Node;
	typedef Node* PNode;
public:
	BSTree() : _pRoot(nullptr)
	{}
	// 同学们自己实现,与二叉树的销毁类似
	~BSTree();
	// 根据二叉搜索树的性质查找:找到值为data的节点在二叉搜索树中的位置
	PNode Find(const T& data);
	bool Insert(const T& data)
	{
		// 如果树为空,直接插入
		if (nullptr == _pRoot)
		{
			_pRoot = new Node(data);
			return true;
		}
		// 按照二叉搜索树的性质查找data在树中的插入位置
		PNode pCur = _pRoot;
		// 记录pCur的双亲,因为新元素最终插入在pCur双亲左右孩子的位置
		PNode pParent = nullptr;
		while (pCur)
		{
			pParent = pCur;
			if (data < pCur->_data)
				pCur = pCur->_pLeft;
			else if (data > pCur->_data)
				pCur = pCur->_pRight;  // 元素已经在树中存在
			else
				return false;
		}
		// 插入元素
		pCur = new Node(data);
		if (data < pParent->_data)
			pParent->_pLeft = pCur;
		else
			pParent->_pRight = pCur;
		return true;
	}
	bool Erase(const T& data)
	{
		// 如果树为空,删除失败
		if (nullptr == _pRoot)
			return false;
		// 查找在data在树中的位置
		PNode pCur = _pRoot;
		PNode pParent = nullptr;
		while (pCur)
		{
			if (data == pCur->_data)
				break;
			else if (data < pCur->_data)
			{
				pParent = pCur;
				pCur = pCur->_pLeft;
			}
			else
			{
				pParent = pCur;
				pCur = pCur->_pRight;
			}
		}
		// data不在二叉搜索树中,无法删除
		if (nullptr == pCur)
			return false;
		// 分以下情况进行删除,同学们自己画图分析完成
		if (nullptr == pCur->_pRight)
		{
			// 当前节点只有左孩子或者左孩子为空---可直接删除
		}
		else if (nullptr == pCur->_pRight)
		{
			// 当前节点只有右孩子---可直接删除
		}
		else
		{
			// 当前节点左右孩子都存在,直接删除不好删除,可以在其子树中找一个替代结点,
			比如:
				// 找其左子树中的最大节点,即左子树中最右侧的节点,或者在其右子树中最小的节
				点,即右子树中最小的节点
				// 替代节点找到后,将替代节点中的值交给待删除节点,转换成删除替代节点
		}
		return true;
	}
private:
	PNode _pRoot;
};
2.3 二叉搜索树的性能分析

插入和删除操作都必须要先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是节点在二叉搜索树的深度的函数,即节点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHCkg9qw-1688661160536)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230705155643589.png)]

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: O(log n)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:O(n)

四:map和set

1.关联式容器

我们已经接触过STL中的部分容器,比如:vector、list、deque等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那么什么是关联式容器?

关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key,value>结构的键值对,在数据检索时比序列式容器效率更高。

2.键值对

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词和与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过单词,那在词典中就应该找到与其对应的中文含义。

SGI-STL中关于键值对的定义:

template <class T1, class T2>
struct pair
{
	typedef T1 first_type;
	typedef T2 second_type;

	T1 first;
	T2 second;
	pair() : first(T1()), second(T2())
	{}

	pair(const T1& a, const T2& b): first(a), second(b)
	{}
};

3.树形结构的关联式容器

根据应用场景的不同,STL总共实现了两种不同结构的管理式容器,树形结构和哈希结构。树形结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结构、容器中的元素是一个有序的序列。

3.1 set
3.1.1 set的介绍

1.set是按照一定次序存储元素的容器

2.在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。

3.在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。

4.set容器通过key访问单个元素的速度通常比unordered_set容器满,但他们允许根据顺序对子集进行直接迭代。

5.set在底层使用二叉搜索树(红黑树)实现的。

需要注意的是:

1,与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只存放value。不过其在底层实际存放的是由<value, value>构成的键值对。

2.在set中插入元素时,只需要插入value即可,不需要构造键值对。

3.set中的元素不会重复(因此可以使用set进行去重)

4.使用set的迭代器遍历set中的元素,可以得到有序序列。

5.set中的元素默认按照小于来比较。

6.set中查找某个元素,时间复杂度为:O(log n)。其中n为set中元素的数量。

7.set中的元素不允许修改。

3.1.2 set的使用

1.set的模板参数列表

template<
    class Key,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<Key>
> class set;

Key: set中存放元素的类型,实际在底层存储的键值对。

Compare:set中元素默认按照小于来比较

Allocator:set中元素空间的管理方式,使用STL提供的空间配置器管理

2.set的构造

a.默认构造函数:

std::set();

创建一个空的std::set对象。

b.范围构造函数:

template <class InputIterator>
std::set(InputIterator first, InputIterator last);

创建一个std::set对象,并使用迭代器范围[first, last)内的元素进行初始化。

c.拷贝构造函数:

std::set(const std::set& other);

创建一个新的std::set对象,其元素和顺序与另一个std::set对象other相同。

以下是上述代码的简单使用示例:

#include <iostream>
#include <set>

int main() {
    // 默认构造函数,创建一个空的 std::set 对象
    std::set<int> mySet1;
    std::cout << "mySet1 size: " << mySet1.size() << std::endl;  // 输出: mySet1 size: 0

    // 范围构造函数,使用迭代器范围初始化 std::set 对象
    std::set<int> mySet2{3, 1, 4, 1, 5, 9};
    std::cout << "mySet2 size: " << mySet2.size() << std::endl;  // 输出: mySet2 size: 5

    // 拷贝构造函数,创建一个与现有 std::set 对象相同的新对象
    std::set<int> mySet3(mySet2);
    std::cout << "mySet3 size: " << mySet3.size() << std::endl;  // 输出: mySet3 size: 5
    
    return 0;
}

3.set的迭代器

std::set提供了迭代器用于遍历集合中的元素。C++STL中std::set的迭代器遵循随机访问迭代器的特性,因此支持前进、后退和随机访问等操作。

std::set 提供了多种迭代器用于遍历和访问容器中的元素。以下是 std::set 的迭代器类型:

  1. 正向迭代器 (iterator):用于正向遍历容器的元素。
  2. 反向迭代器 (reverse_iterator):用于反向遍历容器的元素。
  3. 常量正向迭代器 (const_iterator):用于以只读方式正向遍历容器的元素。
  4. 常量反向迭代器 (const_reverse_iterator):用于以只读方式反向遍历容器的元素。

以下是set的迭代器的用法示例:

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {5, 2, 7, 1, 8};

    // 使用迭代器遍历输出所有元素
    std::cout << "Forward iteration: ";
    for (std::set<int>::iterator it = mySet.begin(); it != mySet.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 使用 const_iterator 遍历输出所有元素(常量迭代器,不可修改元素)
    std::cout << "Forward const iteration: ";
    for (std::set<int>::const_iterator cit = mySet.cbegin(); cit != mySet.cend(); ++cit) {
        std::cout << *cit << " ";
    }
    std::cout << std::endl;

    // 使用反向迭代器遍历输出所有元素
    std::cout << "Reverse iteration: ";
    for (std::set<int>::reverse_iterator rit = mySet.rbegin(); rit != mySet.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    // 使用 const_reverse_iterator 遍历输出所有元素(常量反向迭代器,不可修改元素)
    std::cout << "Reverse const iteration: ";
    for (std::set<int>::const_reverse_iterator crit = mySet.crbegin(); crit != mySet.crend(); ++crit) {
        std::cout << *crit << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

Forward iteration: 1 2 5 7 8
Forward const iteration: 1 2 5 7 8
Reverse iteration: 8 7 5 2 1
Reverse const iteration: 8 7 5 2 1

4.set的容量:

在C++STL中,std::set提供了以下函数来查询容器的容量信息:

a.size():返回std::set容器中元素的个数。

b.empty():检查std::set容器是否为空,如果容器为空则返回true,否则返回false。

c.max_size():返回set::set容器可以容纳的最大元素数量。

以下是上述函数的用法示例:

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {5, 2, 7, 1, 8};

    std::cout << "Size of mySet: " << mySet.size() << std::endl;
    std::cout << "Is mySet empty? " << (mySet.empty() ? "Yes" : "No") << std::endl;
    std::cout << "Max size of mySet: " << mySet.max_size() << std::endl;

    return 0;
}

其输出结果为:

Size of mySet: 5
Is mySet empty? No
Max size of mySet: 576460752303423487

5.set的修改操作

a.insert():将元素插入到std:set中。有多个重载形式,可以插入单个元素、一堆迭代器范围呢ide元素,或者使用初始化列表插入多个元素。

b.erase():从std::set中删除一个或多个元素。有多个重载形式,可以删除指定值的元素、指定迭代器范围内的元素,或者清空整个std::set。

c.clear():清空std::set中的所有元素,使其称为空集合。

d.swap():交换两个std::set容器的内容,使它们的元素互换。

以下是上述代码的用法示例:

#include <iostream>
#include <set>
#include <vector>

int main() {
    std::set<int> mySet = {1, 2, 3, 4, 5};

    // 插入单个元素
    mySet.insert(6);

    // 插入一对迭代器范围内的元素
    std::vector<int> vec = {7, 8, 9};
    mySet.insert(vec.begin(), vec.end());

    // 使用初始化列表插入多个元素
    mySet.insert({10, 11, 12});

    // 删除指定值的元素
    mySet.erase(3);

    // 删除指定迭代器范围内的元素
    auto it = mySet.find(4);
    if (it != mySet.end()) {
        mySet.erase(it, mySet.end());
    }

    // 清空整个集合
    // mySet.clear();

    // 交换集合内容
    std::set<int> otherSet = {100, 200, 300};
    mySet.swap(otherSet);

    // 输出修改后的集合
    for (const auto& element : mySet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

100 200 300

6.set的查找操作

a.find()函数

iterator find(const key_type& key);
const_iterator find(const key_type& key) const;

在集合中查找具有给定键值key的元素。如果找到,则返回指向该元素的迭代器;如果未找到,则返回end()或cend迭代器。

b.count()函数

size_type count(const key_type& key) const;

返回集合中具有给定键值key的元素的个数。由于std::set中的元素唯一,因此返回值只能是0或1。

3.2 map
3.2.1 map的介绍

1.map是关联容器,它按照特定的次序(按照key来比较)存储有键值key和值value组合而成的元素。

2.在map中,键值key通常用于排序和唯一的标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可以不同、可以相同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair。

typedef pair<const key, T> value_type;

3.在内部,map中的元素总是按照键值key进行比较排序的。

4.map中通过键值访问单个元素的速度通常比unordered_map容器慢,但是map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。

5.map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。

6.map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

3.2.2 map的模板参数
template<
    class Key,
    class T,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<std::pair<const Key, T>>
> class map;

Key:键值中key的类型

T:键值中value的类型

Compare:比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)

Allocator:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。

3.2.3 map的构造函数:

a.默认构造函数:

std::map();

创建一个空的std::map对象。

b.范围构造函数:

template <class InputIterator>
map(InputIterator first, InputIterator last);

创建一个std::map对象,并将范围[first,lat)中的元素插入到新的std::map中。要求元素类型可转换为std::pair<const Key, T>。

3.拷贝构造函数:

map(const map& other);

创建一个新的std::map对象,并使用另一个std::map对象中的元素进行初始化。

4.自定义比较函数对象的构造函数:

template <class Compare>
map(const Compare& comp);

创建一个新的std::map对象,并使用自定义的比较函数对象comp进行键的比较。

5.自定义比较函数对象和分配器的构造函数:

template <class Compare, class Allocator>
map(const Compare& comp, const Allocator& alloc);

创建一个新的std::map对象,并使用自定义的比较函数独享comp进行键的比较,以及自定义的分配器alloc进行内存分配。

以下上述代码的简单用法示例:

#include <iostream>
#include <map>
#include <string>

int main() {
    // 默认构造函数
    std::map<int, std::string> map1;
    map1[1] = "apple";
    map1[2] = "banana";
    map1[3] = "orange";

    // 范围构造函数
    std::map<int, std::string> map2(map1.begin(), map1.end());

    // 拷贝构造函数
    std::map<int, std::string> map3(map2);

    // 自定义比较函数对象的构造函数
    std::map<int, std::string, std::greater<int>> map4;
    map4[4] = "grape";
    map4[5] = "melon";

    // 自定义比较函数对象和分配器的构造函数
    std::map<int, std::string, std::greater<int>, std::allocator<std::pair<const int, std::string>>> map5(map4);

    // 输出各个 map 的内容
    for (const auto& pair : map1) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    std::cout << std::endl;

    for (const auto& pair : map2) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    std::cout << std::endl;

    for (const auto& pair : map4) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    std::cout << std::endl;

    for (const auto& pair : map5) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    std::cout << std::endl;

    return 0;
}

其输出结果为:

1: apple
2: banana
3: orange

1: apple
2: banana
3: orange

5: melon
4: grape

5: melon
4: grape
3.2.4 map的迭代器

std::map 提供了多种迭代器用于遍历和访问容器中的元素。以下是 std::map 的迭代器类型:

  1. 正向迭代器 (iterator):用于正向遍历容器的元素。
  2. 反向迭代器 (reverse_iterator):用于反向遍历容器的元素。
  3. 常量正向迭代器 (const_iterator):用于以只读方式正向遍历容器的元素。
  4. 常量反向迭代器 (const_reverse_iterator):用于以只读方式反向遍历容器的元素。

以下是这些迭代器的示例代码:

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> map = {
        {1, "apple"},
        {2, "banana"},
        {3, "orange"}
    };

    // 正向迭代器
    for (auto it = map.begin(); it != map.end(); ++it) {
        std::cout << it->first << ": " << it->second << std::endl;
    }

    // 反向迭代器
    for (auto rit = map.rbegin(); rit != map.rend(); ++rit) {
        std::cout << rit->first << ": " << rit->second << std::endl;
    }

    // 常量正向迭代器
    for (auto cit = map.cbegin(); cit != map.cend(); ++cit) {
        std::cout << cit->first << ": " << cit->second << std::endl;
    }

    // 常量反向迭代器
    for (auto crit = map.crbegin(); crit != map.crend(); ++crit) {
        std::cout << crit->first << ": " << crit->second << std::endl;
    }

    return 0;
}

以下是上述代码的输出结果:

1: apple
2: banana
3: orange
3: orange
2: banana
1: apple
1: apple
2: banana
3: orange
3: orange
2: banana
1: apple
3.2.5 map的容量与元素访问函数

1.empty()函数

bool empty() const;

返回一个布尔值,表示std::map是否为空。如果std::map中没有任何元素,则返回true;否则返回false。

2.size()函数

size_type size() const;

返回std::map中元素的个数。

3.max_size()函数

size_type max_size() const;

返回std::map支持的最大元素个数。

4.operator[]运算符

mapped_type& operator[](const key_type& key);

允许通过键值key访问和修改std::map中的元素。如果key不存在,则会自动插入一个具有该键值的元素,并返回对该元素值的引用。

5.at()函数

mapped_type& at(const key_type& key);
const mapped_type& at(const key_type& key) const;

返回与给定键值key关联的元素的引用。如果key不存在,则抛出std::out_of_range异常。

以下是上述函数的简单示例:

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap;

    // 1. 使用 empty() 函数检查 map 是否为空
    if (myMap.empty()) {
        std::cout << "Map is empty" << std::endl;
    } else {
        std::cout << "Map is not empty" << std::endl;
    }

    // 2. 使用 size() 函数获取 map 中元素的个数
    std::cout << "Map size: " << myMap.size() << std::endl;

    // 3. 使用 max_size() 函数获取 map 支持的最大元素个数
    std::cout << "Map max size: " << myMap.max_size() << std::endl;

    // 4. 使用 operator[] 运算符访问和修改 map 中的元素
    myMap[1] = "One";
    myMap[2] = "Two";
    myMap[3] = "Three";

    std::cout << "Value at key 2: " << myMap[2] << std::endl;

    // 5. 使用 at() 函数访问 map 中的元素
    try {
        std::cout << "Value at key 4: " << myMap.at(4) << std::endl;
    } catch (const std::out_of_range& e) {
        std::cout << "Key 4 not found in the map" << std::endl;
    }

    return 0;
}

3.2.6 map中元素的修改

a.insert()函数

std::pair<iterator, bool> insert(const value_type& value);
iterator insert(iterator hint, const value_type& value);
template <class InputIt>
void insert(InputIt first, InputIt last);

insert()函数用于将元素插入到std::map中。它接受一个或多个键值对作为参数,并将它们插入到std::map中。返回一个迭代器指向插入的元素(或已存在的元素),以及一个布尔值指示插入是否成功。

b.erase()函数

iterator erase(iterator pos);
iterator erase(iterator first, iterator last);
size_type erase(const key_type& key);

erase()函数用于从std::map中删除元素。它可以通过迭代器指定要删除的元素,也可以通过键值指定要删除的元素。返回一个指向下一个元素的迭代器。

c.clear()函数

void clear();

clear()函数用于清空std::map中的所有元素,将其恢复为空状态。

d.swap()函数

void swap(map& other);

swap()函数用于交换两个std::map容器的内容。将当前std::map的元素与另一个std::map的元素进行交换。

以下是其简单的用法示例:

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap;

    // 使用 insert() 函数插入元素
    myMap.insert(std::make_pair(1, "One"));
    myMap.insert(std::make_pair(2, "Two"));
    myMap.insert(std::make_pair(3, "Three"));

    // 使用 erase() 函数删除元素
    myMap.erase(2);

    // 使用 clear() 函数清空容器
    myMap.clear();

    // 使用 swap() 函数交换容器的内容
    std::map<int, std::string> anotherMap;
    anotherMap.insert(std::make_pair(10, "Ten"));
    anotherMap.insert(std::make_pair(20, "Twenty"));
    myMap.swap(anotherMap);

    // 遍历输出交换后的容器
    std::cout << "Swapped Map: ";
    for (const auto& pair : myMap) {
        std::cout << "{" << pair.first << ", " << pair.second << "} ";
    }
    std::cout << std::endl;

    return 0;
}
3.2.7 map的查找

1.find函数

iterator find(const key_type& key);

返回一个指向键为key的元素的迭代器,如果找到了该元素;否则返回end()迭代器。

2.count()函数

size_type count(const key_type& key) const;

返回具有给定键值key的元素在std::map中的个数,可能为0或1。可以使用返回的个数来检查键值是否存在。

3.3 multiset和multimap

std::multisetstd::set 都是 C++ 标准库提供的关联容器,它们之间的区别在于如下几点:

  1. 元素的唯一性:在 std::set 中,每个元素都是唯一的,不允许有重复的元素存在;而在 std::multiset 中,允许存储重复的元素,即可以有相同键值的多个元素。
  2. 插入操作:向 std::set 插入重复的元素会被忽略,因为每个元素必须是唯一的;而向 std::multiset 插入重复的元素会被接受并存储在容器中。
  3. 元素查找:在 std::set 中,可以使用 find() 函数查找特定的元素,返回的迭代器指向该元素;而在 std::multiset 中,find() 函数只能找到第一个匹配的元素,如果需要找到所有匹配的元素,可以使用 equal_range() 函数。

multimap同map之间的区别同上述差不多,可以参照理解。

五:哈希

1.unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到O(logN),也就是说最差情况下需要比较红黑树的高度次,当树中的节点非常多时,擦汗寻效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了四个unordered系列的关联式容器,如:unordered_map和unordered_multimap 、unordered_set和unordered_nultiset。这四个容器与红黑树的关联式容器使用方式基本类似,只是其底层结构不同。

1.1 unordered_map
  1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与 其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此 键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内 找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭 代方面效率较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问 value。
  6. 它的迭代器至少是前向迭代器。
1.2 unordered_map的构造函数

a.默认构造函数:

unordered_map();

创建一个空的std::unordered_map

b.范围构造函数:

template <class InputIt>
unordered_map(InputIt first, InputIt last, size_type bucket_count = implementation-defined, const hasher& hash = hasher(), const key_equal& equal = key_equal(), const allocator_type& alloc = allocator_type());

使用范围 [first, last) 中的键值对创建 std::unordered_map。可以指定初始的桶数量 bucket_count(默认为实现定义),哈希函数 hash(默认为 std::hash<Key>),键的相等比较函数 equal(默认为 std::equal_to<Key>),以及分配器 alloc(默认为 std::allocator<value_type>

c.拷贝构造函数:

unordered_map(const unordered_map& other);

创建一个std::unordered_map,其元素和other中的元素相同。

以下是上述函数的简单用法示例:

#include <iostream>
#include <unordered_map>

int main() {
    // 默认构造函数
    std::unordered_map<int, std::string> map1;
    map1[1] = "One";
    map1[2] = "Two";

    // 范围构造函数
    std::unordered_map<int, std::string> map2(map1.begin(), map1.end());

    // 拷贝构造函数
    std::unordered_map<int, std::string> map3(map2);

    // 输出 map1 的元素
    std::cout << "map1: ";
    for (const auto& pair : map1) {
        std::cout << "{" << pair.first << ", " << pair.second << "} ";
    }
    std::cout << std::endl;

    // 输出 map2 的元素
    std::cout << "map2: ";
    for (const auto& pair : map2) {
        std::cout << "{" << pair.first << ", " << pair.second << "} ";
    }
    std::cout << std::endl;

    // 输出 map3 的元素
    std::cout << "map3: ";
    for (const auto& pair : map3) {
        std::cout << "{" << pair.first << ", " << pair.second << "} ";
    }
    std::cout << std::endl;

    return 0;
}
1.3 unordered_map的容量函数:

a.empty()

bool empty() const;

返回一个布尔值,表示std::unordered_map是否为空,如果std::unordered_map中没有任何元素,则返回true;否则返回false。

b.size()

size_type size() const;

返回std::unordered_map在元素的个数

以下是上述代码的简单示例:

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<int, std::string> myMap;

    std::cout << "Is myMap empty? " << (myMap.empty() ? "Yes" : "No") << std::endl;
    std::cout << "myMap size: " << myMap.size() << std::endl;

    myMap[1] = "One";
    myMap[2] = "Two";

    std::cout << "Is myMap empty? " << (myMap.empty() ? "Yes" : "No") << std::endl;
    std::cout << "myMap size: " << myMap.size() << std::endl;

    return 0;
}
1.4 unordered_map的迭代器:
  1. iterator:用于遍历非常量 std::unordered_map 容器中的元素。可以通过 begin()end() 函数获得迭代器的起始和结束位置。
  2. const_iterator:用于遍历常量 std::unordered_map 容器中的元素。可以通过 cbegin()cend() 函数获得常量迭代器的起始和结束位置。
#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<int, std::string> myMap = {
   {1, "One"}, {2, "Two"}, {3, "Three"}};

    // 使用迭代器遍历容器并输出键值对
    std::cout << "Using iterator: ";
    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        std::cout << "{" << it->first << ", " << it->second << "} ";
    }
    std::cout << std::endl;

    // 使用常量迭代器遍历容器并输出键值对
    std::cout << "Using const_iterator: ";
    for (auto cit = myMap.cbegin(); cit != myMap.cend(); ++cit) {
        std::cout << "{" << cit->first << ", " << cit->second << "} ";
    }
    std::cout << std::endl;

    return 0;
}
1.5 unordered_map的元素访问

a.使用下标操作符[]

mapped_type& operator[](const key_type& key);

允许使用键值key访问和修改std::unordered_map中的元素。如果键值存在,则返回对应元素的引用,如果键值不存在,则自动插入一个具有该键值的元素,并返回对该元素值的引用。

b.使用at()函数

mapped_type& at(const key_type& key);
const mapped_type& at(const key_type& key) const;

返回与给定键值 key 关联的元素的引用。如果键值不存在,则抛出 std::out_of_range 异常。const 重载版本用于访问常量 std::unordered_map 中的元素。

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> myMap;

    // 使用下标操作符 [] 添加或修改元素
    myMap["one"] = 1;
    myMap["two"] = 2;

    // 使用下标操作符 [] 访问元素
    std::cout << "Value of 'one': " << myMap["one"] << std::endl;
    std::cout << "Value of 'two': " << myMap["two"] << std::endl;

    // 使用 at() 函数访问元素
    try {
        int value1 = myMap.at("one");
        int value2 = myMap.at("two");
        std::cout << "Value of 'one' (at()): " << value1 << std::endl;
        std::cout << "Value of 'two' (at()): " << value2 << std::endl;
    } catch (const std::out_of_range& e) {
        std::cout << "Key not found: " << e.what() << std::endl;
    }

    // 访问不存在的键值,会自动插入新的元素
    std::cout << "Value of 'three' (at()): " << myMap.at("three") << std::endl;

    return 0;
}
1.6 unordered_map的查询函数

a.find()

iterator find(const key_type& key);
const_iterator find(const key_type& key) const;

std::unordered_map 中查找具有给定键值 key 的元素。如果找到了该键值对应的元素,则返回指向该元素的迭代器;如果未找到,则返回指向容器末尾的迭代器 end()const 重载版本用于在常量 std::unordered_map 中进行查找。

b.count()

size_type count(const key_type& key) const;

返回具有给定键值key的元素在std::unordered_map中出现的次数。由于std::unordered_map中的键值对应唯一的元素,因此count()函数的返回值只能是0和1。

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<int, std::string> myMap = {
   {1, "One"}, {2, "Two"}, {3, "Three"}};

    // 使用 find() 函数查找元素
    auto it = myMap.find(2);
    if (it != myMap.end()) {
        std::cout << "Element found: {" << it->first << ", " << it->second << "}" << std::endl;
    } else {
        std::cout << "Element not found" << std::endl;
    }

    // 使用 count() 函数统计元素出现次数
    int count1 = myMap.count(1);
    int count4 = myMap.count(4);
    std::cout << "Count of element with key 1: " << count1 << std::endl;
    std::cout << "Count of element with key 4: " << count4 << std::endl;

    return 0;
}
1.7 unordered_map的增删查改

a.insert()

std::pair<iterator, bool> insert(const value_type& value);
iterator insert(const_iterator hint, const value_type& value);
template <class InputIt>
void insert(InputIt first, InputIt last);

插入元素到 std::unordered_map 中。可以插入单个元素或一对迭代器指定的范围内的多个元素。

b.erase()函数

iterator erase(const_iterator pos);
iterator erase(const_iterator first, const_iterator last);
size_type erase(const key_type& key);

c.clear()函数

void clear();

清空 std::unordered_map 中的所有元素,使容器变为空。

d.swap()

void swap(unordered_map& other);

该函数接受另一个 std::unordered_map 容器 other 作为参数,并在常数时间内交换两个容器的内容。注意,交换的是容器的内容,而不是仅交换容器的迭代器。

#include <iostream>
#include <unordered_map>

int main() {
    // insert() 函数
    std::unordered_map<int, std::string> map;

    // 使用 insert() 函数插入单个元素
    auto result = map.insert(std::make_pair(1, "One"));
    if (result.second) {
        std::cout << "插入成功: {" << result.first->first << ", " << result.first->second << "}" << std::endl;
    }

    // 使用迭代器范围插入多个元素
    std::unordered_map<int, std::string> source = {
   {2, "Two"}, {3, "Three"}, {4, "Four"}};
    map.insert(source.begin(), source.end());

    // erase() 函数
    // 使用迭代器移除元素
    auto it = map.find(1);
    if (it != map.end()) {
        map.erase(it);
        std::cout << "移除键为 1 的元素。" << std::endl;
    }

    // 使用迭代器范围移除一系列元素
    auto rangeStart = map.find(2);
    auto rangeEnd = map.find(4);
    if (rangeStart != map.end() && rangeEnd != map.end()) {
        map.erase(rangeStart, rangeEnd);
        std::cout << "移除键从 2 到 4 的元素。" << std::endl;
    }

    // 使用键移除元素
    size_t erasedCount = map.erase(3);
    std::cout << "移除键为 3 的 " << erasedCount << " 个元素。" << std::endl;

    // clear() 函数
    map.clear();
    std::cout << "清空 map。大小: " << map.size() << std::endl;

    // swap() 函数
    std::unordered_map<int, std::string> map1 = {
   {1, "One"}};
    std::unordered_map<int, std::string> map2 = {
   {2, "Two"}};

    std::cout << "交换前:" << std::endl;
    std::cout << "map1: {" << map1.begin()->first << ", " << map1.begin()->second << "}" << std::endl;
    std::cout << "map2: {" << map2.begin()->first << ", " << map2.begin()->second << "}" << std::endl;

    map1.swap(map2);

    std::cout << "交换后:" << std::endl;
    std::cout << "map1: {" << map1.begin()->first << ", " << map1.begin()->second << "}" << std::endl;
    std::cout << "map2: {" << map2.begin()->first << ", " << map2.begin()->second << "}" << std::endl;

    return 0;
}

2.底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用哈希结构。

2.1 哈希概念

哈希是通过哈希函数将数据转换为唯一的标识码,用于快速定位、比较和检索数据。哈希函数通常用于快速定位和检索数据。通过对输入数据进行哈希运算,可以生成一个唯一的哈希值,哈希函数具有以下特性

a.固定长度:无论输入数据的长度是多少,哈希函数都会生成具有固定长度的哈希值。

b.独特性:不同的输入数据经过哈希函数计算后,应该生成不同的哈希值。理想情况下,每个不同的输入都应该产生唯一的哈希值。

c.均匀性:哈希函数应该能够将输入数据的分布均匀的映射到哈希空间中,这意味着相似的数据数据应该生成不同的哈希值,以避免哈希冲突。

2.2 哈希冲突

不同关键字通过相同的哈希函数计算出相同的哈希地址,该种现象称为哈希冲突。

2.3 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:

a.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间

b.哈希函数计算出来的地址能均匀分布在整个空间中

c.哈希函数应该比较简单

常用的哈希函数有以下几种:

a.直接寻址法:

取关键字的某个线性函数为散列地址:Hash(Key) = A*Key + B。其优点是:简单、均匀。缺点是:需要事先知道关键字的分布情况。使用场景:适合查找比较小且连续的情况。

b.除留余数法:

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key % p(p<=m)。将关键码转换成哈希地址。

c.平方取中法:

假设关键字为1234,对它平方就是1522756,抽中间的三位227作为哈希地址;再比如关键字为4321,对其平方就是18671941,抽取中间的三位671(或710)作为哈希地址。平方取中法比较适合:不知道关键字的分布,位数又不是很大的情况。

2.4 哈希冲突解决

解决哈希冲突的两种常见的方法是:闭散列和开散列。

2.4.1闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表没有被装满,说明在哈希表中还有空位置,那么就可以把key存放到冲突位置的下一个空位置去。那么如果寻找下一个空位置呢?

a.线性探测:从发生冲突的位置开始,一次向后探测,知道寻找到下一个空位置为止。

插入:

通过哈希函数获取待插入元素在哈希表中的位置;如果该位置没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新的元素。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYoyRp8r-1688661160537)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230706151642146.png)]

删除:

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。

因为线性探测依赖于元素在哈希表中的位置,以便进行正确的定位和搜索。当要删除一个元素时,如果直接将其从哈希表中移除,就会导致问题。由于探测方法依赖于哈希表中的元素位置进行的,如果一个位置为空,探测方法会终止搜索。

所以线性探测采用标记的伪删除法来删除一个元素。在删除元素后,通常采用一些特殊的标记来标识该位置已经被删除。常用的标记方式是将被删除的位置标记为删除状态,而不是真正地将元素从哈希表中移除。这样,在搜索操作中,当遇到被删除的位置时,可以继续探测下一个位置,知道找到下一个可用的元素。

线性探测的实现;


//假设实现的哈希表中元素唯一,即key相同的元素不再进行插入
//为了实现简单,此哈希表中我们将比较直接与元素绑定在一起。

template<class K, class V>
class HashTable
{
	struct Elem
	{
		pair<k, v> _val;
		State _state;
	};

public:
	HashTable(size_t capacity = 3)
		:_ht(capacity)
        ,_size(0)
    {
        for(size_t i = 0; i < capacity; ++i)
        {
            _ht[i]._state = EMPTY;
        }
    }

    bool Insert(const pair<k, v>& val)
    {
        //检测哈希表底层空间是否充足
        //_CheckCapacity();
        size_t hashAddr = HashFunc(key);
        //size_t startAddr = hashAddr;
        while(_ht[hashAddr]._state != Empty)
        {
            if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
            {
                return false;
            }
            hashAddr++;  //探测下一个位置
            if(hashAddr == _ht.capacity())
            {
                hashAddr = 0;
            }
            
            //遍历完整个哈希表依然没有找到
            //需要注意的是,动态哈希表会自动扩容,这种情况不必考虑
            // if(hashAddr == startAddr)
            // {
            //     return false;
            // }
        }

        //插入元素
        _ht[hashAddr]._state = EXIST;
        _ht[hashAddr]._val = val;
        _size++;
        return true;
    }

    int Find(const k& key)
    {
        size_t hashAddr = HashFunc(key);
        while(_ht[hashAddr]._state != Empty)
        {
            if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
            {
                return hashAddr;
            }
            hashAddr++;
        }
        return hashAddr;
    }

    bool Erase(const k& key)
    {
        int index = Find(key);
        if(-1 != index)
        {
            _ht[index]._state = DELETE;
            _size--;
            return true;
        }
        return fasle;
    }

    size_t Size() const;
       bool Empty() const;    
   void Swap(HashTable<K, V, HF>& ht);
private:
    size_t HashFunc(const K& key)
   {
        return key % _ht.capacity();
   }
private:
    vector<Elem> _ht;
    size_t _size;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QTT7TJ1V-1688661160537)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230706154358624.png)]

void CheckCapacity()
{
    if (_size * 10 / _ht.capacity() >= 7)
    {
        HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity));
        for (size_t i = 0; i < _ht.capacity(); ++i)
        {
            if (_ht[i]._state == EXIST)
                newHt.Insert(_ht[i]._val);
        }

        Swap(newHt);
    }
}

线性探测的优点:实现非常简单。

线性探测缺点:一旦发生哈希冲突,所有的冲突全部连在一起,容易产生数据堆积,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。可以采用二次探测进行解决。

二次探测:

线性探测的缺陷是产生冲突的数据堆积在一起,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:

hash(key, i) = (hash(key) + c1 * i^2 + c2 * i) % table_size

其中,key 是要插入或查找的键值,i 是冲突的次数,table_size 是哈希表的大小,c1c2 是用于调整探测序列的常数。

其整个计算过程包括以下三个部分:

a.hash(key):使用普通哈希函数计算初始的哈希值。

b.c1 * i^2 + c2 * i:通过二次项和线性项来调整探测序列的步长。

3.(hash(key) + c1 * i^2 + c2 * i) % table_size:将结果取模以确保得到的位置在哈希表的有效范围内。

2.4.2 开散列:

1.开散列概念:

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头节点存储在哈希表中。

开散列中每个桶存放的都是发生哈希冲突的元素。

2.开散列实现:

template<class V>
struct HashBucketNode
{
    HashBucketNode(const V& data)
    :_pNext(nullptr)
    ,_data(data)
    {}
    HashBucketNode* _pNext;
    V _data;
};

//本次实现的哈希桶中key是唯一的
template<class V>
class HashBucket
{
    typedef HashBucketNode<V> Node;
    typedef Node* PNode;
public:
    HashBucket(size_t capacity = 3)
    :_size(0)
    {
        _ht.resize(GetNextPrime(capacity), nullptr);
    }

    //哈希桶中的元素不能重复
    PNode* Insert(const V& data)
    {
        //确认是否扩容
        //_CheckCapacity();

        //1.计算元素所在的桶号
        size_t bucketNo = HashFunc(data);

        //2.检查该元素是否在桶中
        PNode pCur = _ht[bucketNo];
        while (pCur)
        {
            if(pCur->_data == data)
            return pCur;

            pCur = pCur->_pNext
        }

        //3.插入新元素
        pCur = new Node(data);
        pCur->_pNext = _ht[bucketNo];
        _ht[bucketNo] = pCur;
        _size++;
        return pCur;
    }

      // 删除哈希桶中为data的元素(data不会重复),返回删除元素的下一个节点
    PNode* Erase(const V& data)
   {
        size_t bucketNo = HashFunc(data);
        PNode pCur = _ht[bucketNo];
        PNode pPrev = nullptr, pRet = nullptr;
        
        while(pCur)
       {
            if(pCur->_data == data)
           {
                if(pCur == _ht[bucketNo])
                    _ht[bucketNo] = pCur->_pNext;
                else
                    pPrev->_pNext = pCur->_pNext;
                
                pRet = pCur->_pNext;
                delete pCur;
                _size--;
                return pRet;
           }
       }
        
        return nullptr;
   }
    
    PNode* Find(const V& data);
    size_t Size()const;
    bool Empty()const;
    void Clear();
    bool BucketCount()const;
    void Swap(HashBucket<V, HF>& ht);
    ~HashBucket();
private:
    size_t HashFunc(const V& data)
       {
        return data%_ht.capacity();
   }
private:
    vector<PNode*> _ht;
    size_t _size;      // 哈希表中有效元素的个数
};

3.开散列增容

哈希桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,在继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

void _CheckCapacity()
{
    size_t bucketCount = BucketCount();
    if(_size == bucketCount)
   {
        HashBucket<V, HF> newHt(bucketCount);
        for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx)
       {
            PNode pCur = _ht[bucketIdx];
            while(pCur)
           {
                // 将该节点从原哈希表中拆出来
                _ht[bucketIdx] = pCur->_pNext;
                
                // 将该节点插入到新哈希表中
                size_t bucketNo = newHt.HashFunc(pCur->_data);
                pCur->_pNext = newHt._ht[bucketNo];
                newHt._ht[bucketNo] = pCur;
                pCur = _ht[bucketIdx];
           }
       }
        
        newHt._size = _size;
        this->Swap(newHt);
   }
}

4.开散列的思考

a.只能存储key为整形的元素,其他类型怎么解决?


// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为
整形的方法
// 整形数据不需要转化
template <class T>
class DefHashF
{
public:
    size_t operator()(const T &val)
    {
        return val;
    }
};
// key为字符串类型,需要将其转化为整形
class Str2Int
{
public:
    size_t operator()(const string &s)
    {
        const char *str = s.c_str();
        unsigned int seed = 131; // 31 131 1313 13131 131313
        unsigned int hash = 0;
        while (*str)
        {
            hash = hash * seed + (*str++);
        }

        return (hash & 0x7FFFFFFF);
    }
};
// 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起
template <class V, class HF>
class HashBucket
{
    // ……
private:
    size_t HashFunc(const V &data)
    {
        return HF()(data.first) % _ht.capacity();
    }
};

5.开散列和闭散列比较

应用链地址法处理移除,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探测法要求装载因子 a<=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

六:C++11

1.C++11简介

C++11是C++98/03之后C++标准委员会增加的新的C++标准版本,他与2011年发布,引入了许多新的功能和语言特性,以增强C++的表达能力和编程效率。

2.统一的列表初始化

2.1 {}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int array1[] = { 1, 2, 3, 4, 5 };
 int array2[5] = { 0 };
 Point p = { 1, 2 };
 return 0;
 }

C++11扩大了用花括号扩起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int x1 = 1;
 int x2{ 2 };
 int array1[]{ 1, 2, 3, 4, 5 };
 int array2[5]{ 0 };
 Point p{ 1, 2 };
 // C++11中列表初始化也可以适用于new表达式中
 int* pa = new int[4]{ 0 };
 return 0;
}

创建对象时也可以使用列表初始化调用构造函数初始化

创建对象时也可以使用列表初始化方式调用构造函数初始化
struct Point
{
 int _x;
 int _y;
};
int main()
{
 int array1[] = { 1, 2, 3, 4, 5 };
 int array2[5] = { 0 };
 Point p = { 1, 2 };
 return 0;
}
struct Point
{
 int _x;
 int _y;
};
int main()
{
 int x1 = 1;
 int x2{ 2 };
 int array1[]{ 1, 2, 3, 4, 5 };
 int array2[5]{ 0 };
 Point p{ 1, 2 };
 // C++11中列表初始化也可以适用于new表达式中
 int* pa = new int[4]{ 0 };
 return 0;
}
class Date
{
public:
 Date(int year, int month, int day)
 :_year(year)
 ,_month(month)
 ,_day(day)
 {
 cout << "Date(int year, int month, int day)" << endl;
 }
private:
 int _year;
 int _month;
 int _day;
 };
int main()
{
 Date d1(2022, 1, 1); // old style
 // C++11支持的列表初始化,这里会调用构造函数初始化
 Date d2{ 2022, 1, 2 };
 Date d3 = { 2022, 1, 3 };
 return 0;
}

2.2 std::initializer_list

std::initializer_list是C++引入的一种特殊类型,用于方便的初始化对象列表。它是一个轻量级的容器,允许以初始化列表的形式初始化对象。

std::initializer_list提供了一种统一的初始化语法,通过花括号{}来创建并初始化对象。可以将多个值用逗号分隔,形成一个初始化列表。

例如:

std::initializer_list<int> numbers = {1, 2, 3, 4, 5};

使用std::initializer_list可以将初始化列表传递给函数或构造函数,作为参数来初始化对象。这种语法非常简洁和直观,可以方便的传递多个值,并且保留了顺序。

让模拟实现的vector也支持{}初始化和赋值

namespace bit
{
template<class T>
class vector {
public:
     typedef T* iterator;
     vector(initializer_list<T> l)
     {
         _start = new T[l.size()];
         _finish = _start + l.size();
         _endofstorage = _start + l.size();
         iterator vit = _start;
         typename initializer_list<T>::iterator lit = l.begin();
         while (lit != l.end())
         {
             *vit++ = *lit++;
         }
         //for (auto e : l)
         //   *vit++ = e;
     }
     vector<T>& operator=(initializer_list<T> l) {
         vector<T> tmp(l);
         std::swap(_start, tmp._start);
         std::swap(_finish, tmp._finish);
         std::swap(_endofstorage, tmp._endofstorage);
         return *this;
     }
private:
     iterator _start;
     iterator _finish;
     iterator _endofstorage;
 };
}

3.声明

C++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样必须进行显式初始化,让编译器将定义对象的类型设置为初始化值的类型。

int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
3.2 decltype

关键字decltype可以推导出表达式的类型,并将其作为一个类型表示符使用。

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p;      // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可以会带来一些问题,因为0既能表示指针常量,又能表示整型常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

3.4 范围for循环

范围for循环(Range-based for loop)是C++11引入的一种循环语法,用于遍历容器、数组和其他序列类型的元素。它提供了一种简洁和直观的方式来遍历序列,而无需手动处理迭代器或索引。

for (element_declaration : sequence) {
    // 循环体
}

4.右值引用和移动语义

4.1 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以现在开始之前的引用就叫左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对他赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}

什么是右值?什么是右值引用?

右值是一个表达式的属性,指示该表达式可以出现在赋值的右边,或者是一个临时的、即将被销毁的值。

右值可以是字面量、临时对象、返回右值的函数调用、显式转换为右值的表达式等。

右值引用是C++引入的一种引用类型,用于绑定到右值。它的语法是在类型名后面加上&&。右值引用允许我们获取对右值的持久引用,以便对其进行操作或延长其声明周期。

右值引用有以下几个重要的特点:

  1. 绑定到右值:右值引用只能绑定到右值,不能绑定到左值。这意味着右值引用只能引用临时对象或即将被销毁的对象。
  2. 延长生命周期:使用右值引用可以延长右值的生命周期,使其在引用的作用域内持久存在。
  3. 移动语义:右值引用与移动语义密切相关。通过右值引用,可以将资源从一个对象转移到另一个对象,而无需进行深拷贝操作,从而提高性能。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地 址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。

int main()
{
 double x = 1.1, y = 2.2;
 int&& rr1 = 10;
 const double&& rr2 = x + y;
 rr1 = 20;
 rr2 = 5.5;  // 报错
 return 0;
}
4.2 右值引用与左值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。

  2. 但是const左值引用既可引用左值,也可引用右值。

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{
 // 右值引用只能右值,不能引用左值。
 int&& r1 = 10;
 
 // error C2440: “初始化”: 无法从“int”转换为“int &&”
 // message : 无法将左值绑定到右值引用
 int a = 10;
 int&& r2 = a;
 // 右值引用可以引用move以后的左值
 int&& r3 = std::move(a);
 return 0;
}
4.3 右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值又可以引用右值,那为什么C++11还要提出右值引用呢?

左值引用的短板:

当函数返回对象是一个局部变量,那么当其出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如to_string函数中就可以看到,这里只能使用传值返回,传值返回会导致至少一次拷贝构造(如果是旧一点的编译器可能是两次拷贝构造)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vx4JffNV-1688661160537)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230706210847790.png)]

这里应该是两次拷贝构造,但是新的编译器会优化,只有一次拷贝。

右值引用和移动语义可以解决上述的问题

就是在其中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

#include <string>
#include <iostream>

using namespace std;

class string
{
public:
    typedef char* iterator;
    iterator begin()
    {
        return _str;
    }

    iterator end()
    {
        return _str + _size;
    }
    
    //默认构造函数
    string(const char* str = "")
    :_size(strlen(str))
    ,_capacity(_size)
    {
        //cout<<"string(char* str)"<<endl;"

        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }

    //s1.swap(s2);
    void swap(string& s)
    {
        ::swap(_str, s._str);
        ::swap(_size, s._size);
        ::swap(_capacity, s._capacity);
    }

    //拷贝构造
    string(const string& s)
    :_str(nullptr)
    {
        cout<<"string(const string& s) -- 深拷贝" << endl;

        string tmp(s._str);
        swap(tmp);
    }

    //赋值重载
    string& operator=(const string& s)
    {
        cout<<"string(const string& s) -- 深拷贝" << endl;
        
        string tmp(s);
        swap(tmp);

        return *this;
    }

    //移动构造
    string(string&& s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
    {
        cout<<"string(string&& s) -- 移动语义" << endl;
        swap(s);
    }

    //移动赋值
    string& operator=(string&& s)
    {
        cout<<"string(string&& s) -- 移动语义" << endl;

        swap(s);
        return *this;
    }

    ~string()
    {
        delete[] _str;
        _str = nullptr;
    }

    char& operator[](size_t pos)
    {
        assert(pos < _size);
        return _str[pos];
    }

    void reverse(size_t n)
    {
        if(n > _capacity)
        {
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;

            _capacity = n;
        }
    }

    void push_back(char ch)
    {
        if(_size >= _capacity)
        {
            size_t = newcapacity = _capacity == 0 ? 4 : _capacity * 2;
            reserve(newcapacity);
        }
        _str[_size++] = ch;
        _str[_size] = '\0';
    }


    //string operator+=(char ch)
    string& operator+=(char ch)
    {
        push_back(ch);
        return *this;
    }

    const char* c_str() const
    {
        return _Str;
    }
    
private:
    char* _str;
    size_t _size;
    size_t _capacity;  //不包含最后做标识的\0
};
4.4 右值引用引用左值以及一些更深入的使用场景分析。

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值取引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,它并不搬运任何东西。唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
 return ((typename remove_reference<_Ty>::type&&)_Arg);
}

4.5 完美转发

模板中的&& 万能引用

模板中的&&并不代表右值引用,而是万能引用,其既能会接受左值,又能接受右值。模板的万能引用只是提供了能够接受两种引用的能力,但是引用类型的唯一作用就是限制了接受的类型,后续使用中都退化成了左值。

如果我们希望能够在传递过程中保持它的左值或者右值的属性,就需要用到完美转发。

template<typename T>
void PerfectForward(T&& t)
{
 Fun(t);
}
int main()
{
 PerfectForward(10);           // 右值
 int a;
 PerfectForward(a);            // 左值
 PerfectForward(std::move(a)); // 右值
 const int b = 8;
 PerfectForward(b);      // const 左值
 PerfectForward(std::move(b)); // const 右值
 return 0;
}

完美转发是用于在函数调用中将参数以原样传递给其他函数,同时保持参数的值类别和常量行。其目的是解决函数模板中的参数传递问题。

在C++11中,引入了两个与完美转发相关的特性:

a.右值引用和引用折叠:右值引用的引入使得我们可以将右值绑定到引用类型,并保持右值的特性。引用折叠规则允许我们在函数模板中通过引用折叠的方式处理左值和右值引用。

b.std::forward是一个函数模板,用于在函数模板中实现完美转发。它通过保持参数的值类别和常量性,将参数原样转发给其他函数。

下面是一个使用完美转发的简单实例:

#include <iostream>
#include <utility>

void process(int& x) {
    std::cout << "Lvalue reference: " << x << std::endl;
}

void process(int&& x) {
    std::cout << "Rvalue reference: " << x << std::endl;
}

template <typename T>
void forwardValue(T&& value) {
    process(std::forward<T>(value));
}

int main() {
    int x = 42;

    forwardValue(x);        // 传递左值
    forwardValue(123);      // 传递右值

    return 0;
}

在上述示例中,process 函数重载了左值引用和右值引用参数。forwardValue 函数是一个模板函数,接受一个通用引用 value。通过使用 std::forwardvalue 原样转发给 process 函数,保持参数的值类别和常量性。

其输出结果为:

Lvalue reference: 42
Rvalue reference: 123

5.新的类功能

5.1 新的默认成员函数

在C++98的类中,有6个默认成员函数:

  1. 构造函数 2. 析构函数 3. 拷贝构造函数 4. 拷贝赋值重载 5. 取地址重载 6. const 取地址重载

在C++11中新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的地方:

如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

5.2 类成员变量初始化

C++11允许在类定义的时候给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。

强制生成默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显式指定生成默认移动构造。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person(Person&& p) = default;
private:
	mhzly::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

禁止生成默认函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是将该函数设置成private,并且只声明不实现,这样别人就无法调用。在C++11中更加简单,只需在该函数声明中加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p) = delete;
private:
 bit::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

6.可变参数模板:

可变参数模板是C++引入的一种模板技术,允许函数模板或类模板接受任意数量和任意类型的参数。在传统的函数模板或类模板中,参数数量和类型是固定的。但是,有时我们希望能够处理可变数量的参数,以便更加灵活地适应不同地需求。可变参数模板就提供了这样的能力。

通过使用可变参数模板,可以在模板定义中指定一个参数包,即一个占位符,表示可变数量的参数。这样我们就可以在函数体内使用参数包,对参数进行处理或展开。

以下是一个使用可变参数模板的示例:

#include <iostream>

// 递归终止条件
void print() {
    std::cout << std::endl;
}

// 模板函数的可变参数版本
template <typename T, typename... Args>
void print(T value, Args... args) {
    std::cout << value << " ";
    print(args...); // 递归调用
}

int main() {
    print(1, 2, 3);               // 输出: 1 2 3
    print("Hello", "World");      // 输出: Hello World
    print(3.14, "C++", true);     // 输出: 3.14 C++ 1

    return 0;
}

此外,可变参数模板还有一种展开方式,就是逗号表达式展开。逗号表达式允许在一个表达式中使用逗号分隔的多个子表达式,并将整个逗号表达式的值为最后一个子表达式的值。

(expression, ... )

以下是一个简单示例:

#include <iostream>

// 递归终止条件
void print() {
    std::cout << std::endl;
}

// 可变参数模板的展开版本
template <typename... Args>
void print(Args... args) {
    (std::cout << ... << args);  // 逗号表达式展开参数包
    print();
}

int main() {
    print("Hello", " ", "World");  // 输出: Hello World

    return 0;
}

7.lambda表达式

lambda表示式是C++11引入的一种匿名函数形式,他可以在需要函数对象的地方进行定义和使用。而无需定义一个具名函数。

以下是一个lambda表示式简单示例:

#include <iostream>

int main() {
    int x = 5;
    int y = 3;

    // Lambda表达式求和
    auto sum = [x, &y]() {
        return x + y;
    };

    int result = sum();  // 调用Lambda表达式

    std::cout << "Result: " << result << std::endl;  // 输出: Result: 8

    return 0;
}

lambda表达式的基本语法如下:

[capture list] (parameters) -> return_type {
    // 函数体
}

其中,capture list 是用于捕获外部变量的列表,可以为空;parameters 是函数参数列表;return_type 是函数返回类型;函数体 是Lambda函数的具体实现。

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

注意:
a.父作用域指包含lambda函数的语句块
b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c.捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]: = 已经以值传递方式捕捉了所有变量,捕捉a重复
d.在块作用域以外的lambda函数捕捉列表必须为空。
e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f.lambda表达式之间不能相互赋值,即使看起来类型相同

8.包装器

std::function包装器是一个通用的函数包装器,它可以接受不同类型的可调用对象(函数指针、函数对象、Lambda表达式等),并提供了一致的接口来调用这些可调用对象。

使用std::function,我们可以将函数或可调用对象作为参数传递给其他函数,存储在容器中,或者将其作为返回值返回。这种灵活性使得在运行时能够动态的选择和调用不同的函数或可调用对象。

#include <iostream>
#include <functional>

// 函数原型
int add(int a, int b) {
    return a + b;
}

int main() {
    // 创建函数包装器
    std::function<int(int, int)> func = add;

    // 调用函数包装器
    int result = func(3, 4);

    std::cout << "Result: " << result << std::endl;  // 输出: Result: 7

    return 0;
}

在上述示例中,我们首先定义了一个名为add的函数,它接受两个整数参数并返回它们的和。然后,我们使用std::functionadd函数包装为函数包装器func,并指定其函数类型为int(int, int),即接受两个整数参数并返回一个整数。

通过调用函数包装器func,我们可以像调用普通函数一样调用被包装的add函数,并得到正确的结果。

函数包装器提供了一种方便且灵活的方式来处理函数和可调用对象,使得代码能够更加模块化和可复用。它在许多场景下非常有用,例如在回调函数、事件处理和泛型编程中。

std::bind是C++11中的一个函数模板,用于创建一个新的可调用对象,将函数或成员函数与参数进行绑定。std::bind可以用于延迟调用函数,修改函数签名或固定部分参数。

std::bind的基本语法如下:

std::bind(function, args...);

其中,function可以是函数指针、函数对象、成员函数指针或函数对象的成员函数指针。args...是要绑定到函数的参数。

通过std::bind创建的可调用对象可以在需要的时候被调用,即使在不同的上下文中传递和执行。通过绑定参数,我们可以固定部分参数的值,并在调用时提供剩余的参数。

下面是一个简单示例:

#include <iostream>
#include <functional>

// 函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 使用 std::bind 绑定函数和参数
    auto func = std::bind(add, 2, std::placeholders::_1);

    // 调用可调用对象
    int result = func(3);

    std::cout << "Result: " << result << std::endl;  // 输出: Result: 5

    return 0;
}

在上述示例中,我们定义了一个名为 add 的函数,它接受两个整数参数并返回它们的和。然后,我们使用 std::bindadd 函数与参数进行绑定,将第一个参数固定为2,并将第二个参数通过占位符 _1 留待调用时提供。

通过调用可调用对象 func,我们可以向其提供第二个参数,并获得正确的结果。在这个例子中,结果为5,因为2(固定的参数)加上3(通过 func 提供的参数)等于5。

std::bind 还支持更复杂的绑定操作,例如绑定成员函数和对象、绑定函数对象的成员函数等。它提供了一种灵活和方便的方式来修改和延迟调用函数,使得代码能够更加模块化和可复用。

9.线程库

9.1 thread类的简单介绍

C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含头文件。

以下是线程类的各种常用函数:

a.构造函数

thread(Function&& f, Args&&... args);

该构造函数用于创建一个新的线程对象,并将可调用对象 f 与参数 args 进行绑定。可调用对象可以是函数指针、函数对象、Lambda 表达式等。

b.分离函数

void detach();

detach()函数用于将线程与线程对象分离,使得线程可以在后台运行,不再受到线程对象的控制。分离后的线程在运行结束后会自动释放资源。

c.获取线程id

std::thread::id get_id() const;

get_id()函数用于获取当前线程的唯一标识符,返回类型为 std::thread::id。每个线程都有一个唯一的ID,可以用于标识和区分不同的线程。

d.加入函数

void join();

oin()函数用于等待线程的完成,即阻塞当前线程直到被调用的线程执行完毕。如果线程已经完成执行,或者线程对象已经与线程分离,那么join()函数会立即返回。

e.判断线程是否可执行

bool joinable() const;

joinable()函数用于判断线程是否可执行,即线程是否与线程对象关联。如果线程对象已经与线程分离或者没有关联任何线程,该函数返回false;否则返回true

以下是上述函数的简单用法示例:

#include <iostream>
#include <thread>
#include <chrono>

// 线程函数
void threadFunction(int value) {
    std::cout << "Thread ID: " << std::this_thread::get_id() << ", Value: " << value << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Thread ID: " << std::this_thread::get_id() << " finished." << std::endl;
}

int main() {
    // 创建线程并执行函数
    std::thread t1(threadFunction, 10);

    // 获取线程ID
    std::thread::id threadId = t1.get_id();
    std::cout << "Thread ID: " << threadId << std::endl;

    // 等待线程完成执行
    if (t1.joinable()) {
        t1.join();
        std::cout << "Thread joined." << std::endl;
    }

    // 分离线程
    std::thread t2(threadFunction, 20);
    t2.detach();

    // 检查线程是否可执行
    if (!t2.joinable()) {
        std::cout << "Thread detached." << std::endl;
    }

    return 0;
}

在上述示例中,我们首先定义了一个名为threadFunction的线程函数,它接受一个整数参数并在标准输出中打印线程ID和参数值。然后,在main函数中,我们创建了两个线程对象t1t2,并将threadFunction作为参数传递给它们。

通过调用t1.get_id(),我们获取了线程t1的唯一标识符,并将其打印到标准输出中。然后,我们使用t1.join()等待线程t1完成执行,并在完成后打印一条消息。

接下来,我们创建了线程对象t2,并使用t2.detach()将其与线程分离。然后,我们使用t2.joinable()检查线程是否可执行,并在不可执行时打印一条消息。

最后,我们使用std::this_thread::sleep_for()函数在线程函数中引入了一个延时,模拟线程执行的耗时操作。这样可以更清楚地观察到线程的执行过程。

通过这个示例,我们可以看到如何使用std::thread类的各种函数来管理线程的创建、执行和结束。这些函数提供了灵活和强大的工具,帮助我们实现多线程编程中的并发操作和线程间的协作。

9.2 线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

#include <thread>
void ThreadFunc1(int& x)
{
 x += 10;
}
void ThreadFunc2(int* x)
{
 *x += 10;
}
int main()
{
 int a = 10;
 // 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
 thread t1(ThreadFunc1, a);
 t1.join();
 cout << a << endl;
 // 如果想要通过形参改变外部实参时,必须借助std::ref()函数
 thread t2(ThreadFunc1, std::ref(a);
 t2.join();
 cout << a << endl;
 // 地址的拷贝
 thread t3(ThreadFunc2, &a);
 t3.join();
 cout << a << endl;
 return 0;
}

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

9.3 原子性操作库

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会设计对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

  1. 竞态条件(Race Condition):多个线程同时访问和修改共享数据时,由于执行顺序不确定,可能导致结果的不确定性。这种情况下,最终的结果可能与期望不符,引发程序错误。
  2. 数据不一致:多个线程同时修改共享数据时,如果没有适当的同步机制,可能会导致数据不一致的问题。例如,在一个线程读取共享数据的同时,另一个线程可能正在修改该数据,导致读取到的数据是不正确或不一致的。
  3. 死锁(Deadlock):当多个线程同时竞争锁资源时,可能发生死锁现象。死锁是指两个或多个线程无限期地等待对方所持有的资源,导致程序无法继续执行。
  4. 数据竞争(Data Race):多个线程同时读取和写入相同的内存位置时,可能会发生数据竞争。数据竞争是指多个线程并发访问相同的内存位置,并且至少有一个线程对该内存位置进行写操作。数据竞争是一种未定义行为,可能导致程序崩溃、产生不可预测的结果或破坏数据。

C++98中传统解决方式:可以对共享修改的数据进行加锁保护。

#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{
 for (size_t i = 0; i < num; ++i)
 {
 m.lock();
 sum++;
 m.unlock();
 }
}
int main()
{
 cout << "Before joining,sum = " << sum << std::endl;
 thread t1(fun, 10000000);
 thread t2(fun, 10000000);
 t1.join();
 t2.join();
 cout << "After joining,sum = " << sum << std::endl;
 return 0;
}

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IVCzcclR-1688661160537)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230706231248257.png)]

#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
 for (size_t i = 0; i < num; ++i)
 sum ++;   // 原子操作
}
int main()
{
 cout << "Before joining, sum = " << sum << std::endl;
 thread t1(fun, 1000000);
 thread t2(fun, 1000000);
 t1.join();
 t2.join();
 
 cout << "After joining, sum = " << sum << std::endl;
 return 0;
}

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。

更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic<T> t;    // 声明一个类型为T的原子类型变量t

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算 符重载默认删除掉了。

七:异常

1.C语言传统的处理错误的方式

传统的错误处理机制:

a.终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。

b.返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

2.C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可抛出异常,让函数的直接或间接地调用者处理这个错误。

throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。

catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异 常,可以有多个catch进行捕获。

try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛 出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

try
{
  // 保护的标识代码
}catch( ExceptionName e1 )
{
  // catch 块
}catch( ExceptionName e2 )
{
  // catch 块
}catch( ExceptionName eN )
{
  // catch 块
}

3.异常的使用

3.1异常的抛出和捕获

异常的抛出和匹配原则

1.异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。

2.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

3.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。

4.catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。

5.实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用。

在函数调用链中异常栈展开匹配原则

  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则 调到catch的地方进行处理。
  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
  3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的 catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异 常,否则当有异常没捕获,程序就会直接终止。
  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ERNeDwrF-1688661160538)(C:\Users\mhc\AppData\Roaming\Typora\typora-user-images\image-20230707002614556.png)]

double Division(int a, int b)
{
    // 当b == 0时抛出异常
 if (b == 0)
   throw "Division by zero condition!";
    else
        return ((double)a / (double)b);
}
void Func()
{
 int len, time;
 cin >> len >> time;
 cout << Division(len, time) << endl;
}
int main()
{
 try {
 Func();
 }
 catch (const char* errmsg) {
 cout << errmsg << endl;
 }
    catch(...){
   cout<<"unkown exception"<<endl;           
   }
 return 0;
}
3.2 异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

double Division(int a, int b)
{
 // 当b == 0时抛出异常
 if (b == 0)
 {
 throw "Division by zero condition!";
 }
 return (double)a / (double)b;
}
void Func()
{
 // 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
 // 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
 // 重新抛出去。
 int* array = new int[10];
 try {
 int len, time;
 cin >> len >> time;
 cout << Division(len, time) << endl;
 }
 catch (...)
 {
 cout << "delete []" << array << endl;
 delete[] array;
 throw;
 }
 // ...
 cout << "delete []" << array << endl;
 delete[] array;
}
int main()
{
 try
 {
 Func();
 }
 catch (const char* errmsg)
 {
 cout << errmsg << endl;
 }
 return 0;
}
3.3 异常安全

构造函数完成对象的构造和初始化,最好不要再构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化

析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内 存泄漏、句柄未关闭等)

3.4 异常规范
  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的 后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

  2. 函数的后面接throw(),表示函数不抛异常。

  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

    // 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
    void fun() throw(A,B,C,D);
    // 这里表示这个函数只会抛出bad_alloc的异常
    void* operator new (std::size_t size) throw (std::bad_alloc);
    // 这里表示这个函数不会抛出异常
    void* operator delete (std::size_t size, void* ptr) throw();
    // C++11 中新增的noexcept,表示不会抛异常
    thread() noexcept;
    thread (thread&& x) noexcept;
    

    int x = 5;
    int y = 3;

    // Lambda表达式求和
    auto sum = x, &y {
    return x + y;
    };

    int result = sum(); // 调用Lambda表达式

    std::cout << "Result: " << result << std::endl; // 输出: Result: 8

    return 0;
    }


lambda表达式的基本语法如下:

[capture list] (parameters) -> return_type {
// 函数体
}


其中,`capture list` 是用于捕获外部变量的列表,可以为空;`parameters` 是函数参数列表;`return_type` 是函数返回类型;`函数体` 是Lambda函数的具体实现。

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

注意:
a.父作用域指包含lambda函数的语句块
b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c.捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]: = 已经以值传递方式捕捉了所有变量,捕捉a重复
d.在块作用域以外的lambda函数捕捉列表必须为空。
e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f.lambda表达式之间不能相互赋值,即使看起来类型相同

### 8.包装器

std::function包装器是一个通用的函数包装器,它可以接受不同类型的可调用对象(函数指针、函数对象、Lambda表达式等),并提供了一致的接口来调用这些可调用对象。

使用std::function,我们可以将函数或可调用对象作为参数传递给其他函数,存储在容器中,或者将其作为返回值返回。这种灵活性使得在运行时能够动态的选择和调用不同的函数或可调用对象。

#include
#include

// 函数原型
int add(int a, int b) {
return a + b;
}

int main() {
// 创建函数包装器
std::function<int(int, int)> func = add;

// 调用函数包装器
int result = func(3, 4);

std::cout << "Result: " << result << std::endl;  // 输出: Result: 7

return 0;

}


在上述示例中,我们首先定义了一个名为`add`的函数,它接受两个整数参数并返回它们的和。然后,我们使用`std::function`将`add`函数包装为函数包装器`func`,并指定其函数类型为`int(int, int)`,即接受两个整数参数并返回一个整数。

通过调用函数包装器`func`,我们可以像调用普通函数一样调用被包装的`add`函数,并得到正确的结果。

函数包装器提供了一种方便且灵活的方式来处理函数和可调用对象,使得代码能够更加模块化和可复用。它在许多场景下非常有用,例如在回调函数、事件处理和泛型编程中。

std::bind是C++11中的一个函数模板,用于创建一个新的可调用对象,将函数或成员函数与参数进行绑定。std::bind可以用于延迟调用函数,修改函数签名或固定部分参数。

std::bind的基本语法如下:

std::bind(function, args…);


其中,`function`可以是函数指针、函数对象、成员函数指针或函数对象的成员函数指针。`args...`是要绑定到函数的参数。

通过`std::bind`创建的可调用对象可以在需要的时候被调用,即使在不同的上下文中传递和执行。通过绑定参数,我们可以固定部分参数的值,并在调用时提供剩余的参数。

下面是一个简单示例:

#include
#include

// 函数
int add(int a, int b) {
return a + b;
}

int main() {
// 使用 std::bind 绑定函数和参数
auto func = std::bind(add, 2, std::placeholders::_1);

// 调用可调用对象
int result = func(3);

std::cout << "Result: " << result << std::endl;  // 输出: Result: 5

return 0;

}


在上述示例中,我们定义了一个名为 `add` 的函数,它接受两个整数参数并返回它们的和。然后,我们使用 `std::bind` 将 `add` 函数与参数进行绑定,将第一个参数固定为2,并将第二个参数通过占位符 `_1` 留待调用时提供。

通过调用可调用对象 `func`,我们可以向其提供第二个参数,并获得正确的结果。在这个例子中,结果为5,因为2(固定的参数)加上3(通过 `func` 提供的参数)等于5。

`std::bind` 还支持更复杂的绑定操作,例如绑定成员函数和对象、绑定函数对象的成员函数等。它提供了一种灵活和方便的方式来修改和延迟调用函数,使得代码能够更加模块化和可复用。

### 9.线程库

#### 9.1 thread类的简单介绍

C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含<thread>头文件。

以下是线程类的各种常用函数:

a.构造函数

thread(Function&& f, Args&&… args);


该构造函数用于创建一个新的线程对象,并将可调用对象 `f` 与参数 `args` 进行绑定。可调用对象可以是函数指针、函数对象、Lambda 表达式等。

b.分离函数

void detach();


`detach()`函数用于将线程与线程对象分离,使得线程可以在后台运行,不再受到线程对象的控制。分离后的线程在运行结束后会自动释放资源。

c.获取线程id

std::thread::id get_id() const;


`get_id()`函数用于获取当前线程的唯一标识符,返回类型为 `std::thread::id`。每个线程都有一个唯一的ID,可以用于标识和区分不同的线程。

d.加入函数

void join();


`oin()`函数用于等待线程的完成,即阻塞当前线程直到被调用的线程执行完毕。如果线程已经完成执行,或者线程对象已经与线程分离,那么`join()`函数会立即返回。

e.判断线程是否可执行

bool joinable() const;


`joinable()`函数用于判断线程是否可执行,即线程是否与线程对象关联。如果线程对象已经与线程分离或者没有关联任何线程,该函数返回`false`;否则返回`true`。

以下是上述函数的简单用法示例:

#include
#include
#include

// 线程函数
void threadFunction(int value) {
std::cout << "Thread ID: " << std::this_thread::get_id() << ", Value: " << value << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << “Thread ID: " << std::this_thread::get_id() << " finished.” << std::endl;
}

int main() {
// 创建线程并执行函数
std::thread t1(threadFunction, 10);

// 获取线程ID
std::thread::id threadId = t1.get_id();
std::cout << "Thread ID: " << threadId << std::endl;

// 等待线程完成执行
if (t1.joinable()) {
    t1.join();
    std::cout << "Thread joined." << std::endl;
}

// 分离线程
std::thread t2(threadFunction, 20);
t2.detach();

// 检查线程是否可执行
if (!t2.joinable()) {
    std::cout << "Thread detached." << std::endl;
}

return 0;

}


在上述示例中,我们首先定义了一个名为`threadFunction`的线程函数,它接受一个整数参数并在标准输出中打印线程ID和参数值。然后,在`main`函数中,我们创建了两个线程对象`t1`和`t2`,并将`threadFunction`作为参数传递给它们。

通过调用`t1.get_id()`,我们获取了线程`t1`的唯一标识符,并将其打印到标准输出中。然后,我们使用`t1.join()`等待线程`t1`完成执行,并在完成后打印一条消息。

接下来,我们创建了线程对象`t2`,并使用`t2.detach()`将其与线程分离。然后,我们使用`t2.joinable()`检查线程是否可执行,并在不可执行时打印一条消息。

最后,我们使用`std::this_thread::sleep_for()`函数在线程函数中引入了一个延时,模拟线程执行的耗时操作。这样可以更清楚地观察到线程的执行过程。

通过这个示例,我们可以看到如何使用`std::thread`类的各种函数来管理线程的创建、执行和结束。这些函数提供了灵活和强大的工具,帮助我们实现多线程编程中的并发操作和线程间的协作。

#### 9.2 线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

#include
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}


注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

#### 9.3 原子性操作库

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会设计对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

1. 竞态条件(Race Condition):多个线程同时访问和修改共享数据时,由于执行顺序不确定,可能导致结果的不确定性。这种情况下,最终的结果可能与期望不符,引发程序错误。
2. 数据不一致:多个线程同时修改共享数据时,如果没有适当的同步机制,可能会导致数据不一致的问题。例如,在一个线程读取共享数据的同时,另一个线程可能正在修改该数据,导致读取到的数据是不正确或不一致的。
3. 死锁(Deadlock):当多个线程同时竞争锁资源时,可能发生死锁现象。死锁是指两个或多个线程无限期地等待对方所持有的资源,导致程序无法继续执行。
4. 数据竞争(Data Race):多个线程同时读取和写入相同的内存位置时,可能会发生数据竞争。数据竞争是指多个线程并发访问相同的内存位置,并且至少有一个线程对该内存位置进行写操作。数据竞争是一种未定义行为,可能导致程序崩溃、产生不可预测的结果或破坏数据。

C++98中传统解决方式:可以对共享修改的数据进行加锁保护。

#include
using namespace std;
#include
#include
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
{
m.lock();
sum++;
m.unlock();
}
}
int main()
{
cout << "Before joining,sum = " << sum << std::endl;
thread t1(fun, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}


虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。

[外链图片转存中...(img-IVCzcclR-1688661160537)]

#include
using namespace std;
#include
#include
atomic_long sum{ 0 };
void fun(size_t num)
{
for (size_t i = 0; i < num; ++i)
sum ++; // 原子操作
}
int main()
{
cout << "Before joining, sum = " << sum << std::endl;
thread t1(fun, 1000000);
thread t2(fun, 1000000);
t1.join();
t2.join();

cout << "After joining, sum = " << sum << std::endl;
return 0;
}


在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。

更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic t; // 声明一个类型为T的原子类型变量t


注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算 符重载默认删除掉了。

## 七:异常

### 1.C语言传统的处理错误的方式

传统的错误处理机制:

a.终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。

b.返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

### 2.C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可抛出异常,让函数的直接或间接地调用者处理这个错误。

throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。 

catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异 常,可以有多个catch进行捕获。 

try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛 出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}


### 3.异常的使用

#### 3.1异常的抛出和捕获

异常的抛出和匹配原则

1.异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。

2.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

3.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。

4.catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。

5.实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用。

在函数调用链中异常栈展开匹配原则

1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则 调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的 catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异 常,否则当有异常没捕获,程序就会直接终止。 
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

[外链图片转存中...(img-ERNeDwrF-1688661160538)]

double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw “Division by zero condition!”;
else
return ((double)a / (double)b);
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try {
Func();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
catch(…){
cout<<“unkown exception”<<endl;
}
return 0;
}


#### 3.2 异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw “Division by zero condition!”;
}
return (double)a / (double)b;
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
// 重新抛出去。
int* array = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (…)
{
cout << “delete []” << array << endl;
delete[] array;
throw;
}
// …
cout << “delete []” << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}


#### 3.3 异常安全

构造函数完成对象的构造和初始化,最好不要再构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化

析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内 存泄漏、句柄未关闭等)

#### 3.4 异常规范

1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的 后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

2. 函数的后面接throw(),表示函数不抛异常。

3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

   

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;




版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/mhzly1/article/details/131587874

智能推荐

工程数学基础 考试 要点 复习:矩阵论,数值计算_工程矩阵论期中考试-程序员宅基地

文章浏览阅读737次。v向量的1-范数∥x∥1=∑k=1n∣ξk∣\|x\|_1=\sum_{k=1}^n|\xi_k|∥x∥1​=∑k=1n​∣ξk​∣向量的2-范数∥x∥2=(∑k=1n∣ξk∣2)12\|x\|_2=(\sum_{k=1}^n|\xi_k|^2)^\frac12∥x∥2​=(∑k=1n​∣ξk​∣2)21​向量的\infty-范数∥x∥∞=max⁡1≤k≤n∣ξk∣\|x\|_\infty=\max_{1\leq k \leq n}|\xi_k|∥x∥∞​=max1≤k≤n​∣ξk​∣列范数=_工程矩阵论期中考试

opencsv解析CSV文件_opencsv 读取csv-程序员宅基地

文章浏览阅读1k次。opencsv解析CSV文件_opencsv 读取csv

2024年【N1叉车司机】考试题库及N1叉车司机考试资料-程序员宅基地

文章浏览阅读670次,点赞16次,收藏10次。47、【多选题】《安全生产法》规定,生产经营单位的决策机构、主要负责人或者个人经营的投资人不依照《安全生产法》规定保证安全生产所必需的资金投入,致使生产经营单位不具备安全生产条件的,责令限期改正,提供必需的资金;逾期未改正的,责令生产经营单位停产停业整顿。39、【单选题】特种设备生产、经营、使用单位应当遵守《中华人民共和国特种设备安全法》和其他有关法律、法规,建立、健全特种设备安全和节能责任制度,加强特种设备安全和节能管理,确保特种设备生产、经营、使用安全,符合()要求。事故原因分析:()( B )

安装和初步使用 nn-Meter_nn-meter部署-程序员宅基地

文章浏览阅读396次。安装和初步使用 nn-Meter_nn-meter部署

消息队列RabbitMQ介绍和使用_rabbitmq消费端 使用 class-程序员宅基地

文章浏览阅读634次。消息队列RabbitMQ介绍和使用_rabbitmq消费端 使用 class

MySQL_DQL语句(分组,筛选)_数据库的备份/还原/约束_mysql备份筛选数据-程序员宅基地

文章浏览阅读118次。DQL语句之分组查询:group byselect 字段列表 from 表名 group by 分组字段名称;注意事项:1) 查询的字段列表中可以使用 分组字段 ;2) group by之后不能使用聚合函数 ;3) 带条件分组查询的语法: where 条件必须放在group by 之前,否则语法错误! ;4) select 字段列表包含分组字段,聚合函数.. from 表名 where 条件 group by 分组字段 ;DQL语句之筛选查询:havingselect 字段列_mysql备份筛选数据

随便推点

打开 IOS开发者模式_ios17.3开发者模式怎么打开-程序员宅基地

文章浏览阅读1.4k次,点赞2次,收藏2次。需要1、辅助设备:苹果电脑;2、辅助应用:Xcode;3、准备工作:苹果手机使用数据线连接苹果电脑;_ios17.3开发者模式怎么打开

1024 程序员日,全年最大红包,小小心意请笑纳~-程序员宅基地

文章浏览阅读583次。有这么一群神奇的生物:他们喝的是咖啡,挤的是代码……他们总会被黑无趣,却致力于让世界变得有趣……他们靠技术吃饭,但大多数人只关注他们的服装和发量……他们的名字是:程序员转眼 1024 程..._程序员节红包

抖音快手小动画推广项目变现,从快手小游戏到多平台多种形式变现-程序员宅基地

文章浏览阅读180次。快手小游戏推广从二月份的微光计划,大力扶持到现在的对游戏号要求逐步提升,对于视频质量和游戏号粉丝也要求越来越高了,不像之前零粉丝就会给你投放粉条助推。在分享之前先简单说一下我对短视频这个领域这段时间的感悟,最好在某个细分领域深耕,专研贯通后再多平台探索同一个板块会更容易一些。

动态规划之0-1背包问题_0-1背包满足最优子结构特性-程序员宅基地

文章浏览阅读940次。问题描述 0−1背包问题是应用动态规划设计求解的典型例题 已知n种物品和一个可容纳c重量的背包,物品i的重量为w[i],产生的效益为p[i]。在装包时物品i可以装入,也可以不装,但不可拆开装。 问如何装包,所得装包总效益最大。算法分析 最优子结构特性 0−1背包的最优解具有最优子结构特性。 与一般背包问题不同,0−1背包问题要求 即物品i不能折开,或者整体装入,或者不装。当约定每件物品_0-1背包满足最优子结构特性

Hybrid移动应用在多页面大数据复杂业务背景下的优化实践方案_hybrid 大数据-程序员宅基地

文章浏览阅读2.7k次。前言对于混合应用而言,性能问题一直被吐槽,虽然设备的内存的不断增大,很大程度上缓解了这个一问题,但是和原生应用来讲还是有很大区别,本人从Phonegap2.x开始,一直的探索和使用混合应用技术。当时的2.x性能真是不怎么样,首次加载时间也比较长,后来phonegap被apache纳入旗下以后,更名为Cordova,可以说从此以后,性能问题得到了很大的改善,占用内存也越来越小,到如今使用的版本已经变为_hybrid 大数据

炬力北方AM8360D无线图传介绍_am8360d升级-程序员宅基地

文章浏览阅读73次。为了优化和普及无线显示,炬力北方提供具有高质量图像、小 尺寸、低延迟、低功耗以及更具性价比的无线显示解决方案。AM8360D支持1080P60Hz以及4K30Hz.AM8360D是炬力北方为无线化图传提供解决方案。支持即插即用,兼容多种设备,复制或者拓展屏幕。不论您是在家庭娱乐、办公会议,还是教育演示,这款投屏器能轻松应对各种场景。稳定可靠,兼容性强,让您的投屏体验更上一层楼。开发与支持服务:QQ: 670976930 微信号:X4263478。_am8360d升级

推荐文章

热门文章

相关标签