17.C加加编程

  • 15.计算机图形学OpenGL与C++

    理解GLFW和GLAD

    GLFW()

    GLFW是一个开源的、跨平台的库,用于创建窗口、处理输入和渲染图形。它提供了一种简单的方式来与OpenGL进行交互,使得开发图形应用程序变得更加容易。

  • 14.新标准C加加程序设计

    内存分区模型

    内存代码区的意义:程序代码放在不同的区域,有程序自动管理的代码,也有程序员管理的代码,灵活管理;

    一、程序执行前

    1.代码区 存放二进制指令,就是代码,特点:

  • 13.从C到C++

    指针

    指针的声明: alt text alt text

    数组

    数组的范围必须是常量表达式,同时如果需要变化的范围可以做如下定义: alt text alt text

    2.字符串文字量

    指的是用 ““括起来的字符串。

  • 12.其他库的介绍

    为什么需要哈希函数?

    哈希函数在计算机科学中扮演着重要的角色,其主要作用包括:

    1. 数据检索:哈希函数可以将键(key)映射到哈希表中的一个位置,从而快速检索数据。这是哈希表(如 C++ 中的 std::unordered_mapstd::unordered_set)的基础。

    2. 11.STL库学习之适配器

      整体框架的回顾

      alt text alt text

      存在多种适配器-adapters

      一个人理解,要将适配器理解透彻,我们需要先从别的组件入手,这里以vector容器为例,我们都知道该容器需要迭代器,也就是一些智能指针来确定容器的头尾,以及内容位置,目的是方便后续的算法的怎删改查等操作,假设算法现在要做拿到位置8的数据,那么vector的指针要怎么移动呢?已知指针移动上,vector是随机访问指针,也就是前后都可以跑,而这个操作实际上是迭代器的其中一个内容,而这一内容就需要一个适配器去做适配。

    3. 10.STL库学习之仿函数

      仿函数的个人理解

      仿函数是行为类似函数的一个类,比较明显的特征是仿函数重载了operate(),比如你写了一个struct,并在里面重载了operate(),当调用这个类的()时就会返回响应的数据。

    4. 9.STL库学习之迭代器与算法

      标准库常用算法

      alt text alt text

      迭代器

      迭代器-的分类

      struct input_iterator_tag {}
      struct output_iterator_tag {};
      struct forward_iterator_tag:public input_iterator_tag{};
      struct bidirectional_iterator_tag:public forward_iterator_tag{};
      struct random_access_tag:public bidirectional_iterator_tag {};

      以上5中迭代器的继承关系,如下图所示。 alt text alt text

      1.Input Iterator(输入迭代器): 输入迭代器是最基本的迭代器类型,支持单向遍历,只能向前移动。

    5. 8.STL库学习之容器

      list

      vector

      deque

      alt text alt text

      deque的迭代器 alt text alt text

      deque实现中间插入值的做法:如果欲插入值在最前端(最尾端)调用push_front()push_back()),否则借助insert_aux()迭代器,实现如果欲插入位置靠近前端使用,就让前端的数据往前挪,否则往后挪。

    6. 7.STL库学习之分配器

      分配器源代码位置:xmemory.h

      alt text alt text

      除了array和vector外,其他容器的适配器必须是一个类,

      附件

    7. 6.STL库学习之筑基概要

      STL程序源代码位置

      了解自身编译器STL程序源代码位置。

      OOP 与 GP

      面向对象编程(OOP):

      面向对象编程是一种编程范式,它将数据和处理这些数据的方法封装在对象中。这种方法强调了数据和函数的捆绑,使得代码更加模块化,易于理解和维护。对象可以包含属性(数据)和方法(函数),它们一起工作来模拟现实世界的行为。

    8. 5.STL库之观其大略

      一下主要讲STL组件的测试用例,特别是容器的测试

      学习资料

      • CPLusPlus.com
      • CppReference.com
      • gcc.gnu.org
      • 《STL源码剖析》

      STL六大组件

      • 容器-Containers,申请内存用于存储数据
      • 分配器-Allocators,配合容器分配内存
      • 算法- Algorithms,处理某一数据的最优办法
      • 迭代器- Iterators,指针的泛型,本质与指针类似
      • 适配器- Adapters,
      • 仿函数-Functors,类似函数。

      六者的关系

    9. 4.C++2.0特性的使用

      VS 2022的设置

      首先你可以先用下面的代码测试使用可以执行:

          #include <iostream>
          void printTypes() {
          }
          template <typename T, typename... U>
          void printTypes(const T& t, const U&... u) {
              std::cout << t << std::endl;
              printTypes(u...);
          }
      
          int main() {
              printTypes('a', 1.5, 'b');
          }

      如果报错了,如typename…未定义,那么请你按照如下步骤操作, alt text alt text

    10. 3.导读

      书籍推荐

      学习路线

      路线 路线

    11. 2.组合与继承

      类与类的三种关系

      Composition-复合

      以我个人的理解,复合就是一个类中包含有另外一个类,使用到另一个类的内容。复合的类他们的构造和析构函数运行次序是,构造函数有内到外依次运行,析构函数则相反。可以使用下图表示这种关系

    12. 1.代码编写规范

      如何写一个标准的.h文件

      以下内容来自B站

      接下来将以复数的库文件为例来回答这个问题,下面是一段参考复数库文件代码:

      class complex
      {
      private:
          /* data */
          double re,im;
          friend complex& __doapl (complex*,const complex&);
      public:
          complex (double r,double i)
          :re(r),im(i)                //这是一个构造函数,这种写法意思是初始化变量
          {};
          complex& operator += (complex operator&);
          double read () const {return re;}  //这里需要加const 意思就是修饰函数的返回值,不允许改变返回值类型
          double imag () const {return im;}
      };

      构造函数的特性

      这一点需要关注下面的代码:

17.C加加编程 的子部分

15.计算机图形学OpenGL与C++

理解GLFW和GLAD

GLFW()

GLFW是一个开源的、跨平台的库,用于创建窗口、处理输入和渲染图形。它提供了一种简单的方式来与OpenGL进行交互,使得开发图形应用程序变得更加容易。

GLFW的主要功能包括:

  • 创建窗口
  • 定义上下文
  • 处理用户输入

GLAD()

GLAD是一个开源的、跨平台的库,用于管理OpenGL的函数指针。它提供了一种简单的方式来获取OpenGL的函数指针,使得开发OpenGL应用程序变得更加容易。

相关说明:

  • OpenGL本身知识规范/标准
  • 各个厂家具体实现方式可以不同
  • 不同操作系统处理方式也可以不同

主要功能:

  • 获取OpenGL函数指针
  • 处理OpenGL版本兼容性问题
  • 处理OpenGL扩展函数 ……

如果没有GLAD库,在Windows下:

typedef void (*GL_GENBUFFER) (GLsizei,GLuint*);//声明函数指针
GL_GENBUUFFER glGenBuffer = (GL_GENBUFFER)wglGetProcAddress('glGenBuffer');//获取函数指针
unsigned int buffer;//声明缓冲区对象
glGenBuffer(1,&buffer);//调用函数

有了GLAD库,只需要:

unsigned int buffer;
glGenBuffer(1,&buffer);
附件

14.新标准C加加程序设计

内存分区模型

内存代码区的意义:程序代码放在不同的区域,有程序自动管理的代码,也有程序员管理的代码,灵活管理;

一、程序执行前

1.代码区 存放二进制指令,就是代码,特点:

  • 共享:多个进程的代码区是共享的,因为代码是相同的,所以只需要加载一份到内存中即可,节省内存空间。
  • 只读:代码是只读的,防止程序意外地修改了代码。

2.全局区 全局变量和静态变量存储在全局区,还包括常量区、字符串常量和其他常量(如:const),特点:

  • 全局区是程序结束后由系统释放。
  • 全局变量和静态变量在程序结束后系统会自动释放,而局部变量在函数结束后由系统自动释放。

例子:

#include <iostream>
using namespace std;

//全局变量 
int g_a = 10;
int g_b;

//const 修饰的全局变量
const int c_g_a = 10;

int main(int argc,char *argv[]){
    //局部变量
    int a = 10;
    int b = 20;

    //静态变量
    static int s_a = 10;
    static int s_b;

    //字符串常量
    char *P = "hello world";

    //const修饰的局部变量
    const int c_l_a = 10;

    cout<<"局部变量a的地址:"<<&a<<endl;
    cout<<"局部变量b的地址:"<<&b<<endl;
    cout<<"全局变量a的地址:"<<&g_a<<endl;
    cout<<"全局变量b的地址:"<<&g_b<<endl;
    cout<<"静态变量a的地址:"<<&s_a<<endl;
    cout<<"静态变量b的地址:"<<&s_b<<endl;

    cout<<"字符串常量P的地址:"<<P<<endl;
    cout<<"const修饰的局部变量c_l_a的地址:"<<&c_l_a<<endl;
    cout<<"const修饰的全局变量c_g_a的地址:"<<&c_g_a<<endl;

    return 0;
}

3.总结

  • 代码区:存放二进制指令,共享,只读。
  • 全局区:存放全局变量和静态变量,程序结束后由系统释放。

二、程序运行后

1.栈区

由编译器自动分配释放,存放函数的参数值,局部变量等。

注意: 不要返回局部变量的地址,因为局部变量在函数结束后会被系统释放,再次访问会导致程序崩溃。

例子:

#include <iostream>
using namespace std;

int *GetAddr(int b){//形参数据也放在栈区
    b = 10;
    int a = 10;
    return &a;//返回局部变量的地址
}

int main(){
    int *p = GetAddr();
    cout<<*p<<endl;//第一次可以打印正确的数据,是因为编译器做了保留,
    cout<<*p<<endl;// 但是第二次访问将不再保留
    return 0;
}

2.堆区

由程序员分配释放,如果程序员不释放,程序结束时由系统回收。

在C++中主要利用new和delete操作符进行动态内存管理。

例子1-在堆上开辟空间:

#include <iostream>
using namespace std;
int* getAddr(){

    //在堆上开辟空间,并初始化为10
    int *p = new int(10);//new出来的数据返回的是一个指针
    //使用存在栈区的局部指针指向堆区的数据,栈区的内容会被程序自动释放,但是指向堆区的数据不会被释放
    return p;
}
int main(){

    int *p = getAddr();

    cout<<*p<<endl;//输出10
    delete p;//释放空间
    return 0;
}

图解: alt text alt text

例子2-堆区上的数据开辟与释放

#include <iostream>
using namespace std;
void test(){
    int *P = new int(10);
    cout<<*P<<endl;
    
    delete P;
}
void test2(){
    int *arr = new int[10];
    for(int i = 0;i<10;i++){
        arr[i] = i+10;
    }
    for(int i = 0;i<10;i++){
        cout<<arr[i]<<" ";
    }
    delete[] arr;
}
int main(){
    test();
}

引用

给变量取别名。

一、变量引用

引用注意事项:

  • 引用必须初始化,int &b;//这样是不正确的
  • 引用初始化后不能改变
  • 通过引用参数产生的效果与指针地址传递效果一样,引用语法更清楚;

测试代码:

//值传递
void test1(int a,int b) {

}
//地址传递
void test2(int *a,int *b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
//引用传递
void test3(int &a,int &b) {
	int temp = a;
	a = b;
	b = temp;
}

二、应用做函数的返回值

  • 不要返回局部变量的引用
  • 函数返回值是引用,那么这个函数调用可以作为左值

测试代码:

// 二、引用做函数返回值
	//1.不能返回应用返回值
int& test04()//以引用的方式返回b,相当于给这个b起了一个别名
{
	int b = 10;//局部变量存放在栈区,函数结束后会被释放
	return b;
}
	//2. 函数的调用可以作为左值
int& test05()
{
	static int b = 10;//静态变量存放在全局区,程序结束后由系统自动释放
	return b;
}

int main() 
{
	int& ret04 = test04();
	int& ret05 = test05();
	std::cout << "ret04= "<<ret04 << std::endl;
	std::cout << "ret04= " << ret04 << std::endl;//第二次访问返回的是乱码
	std::cout << "ret05= " << ret05 << std::endl;
	test05() = 130;//函数的返回是引用,函数的调用可以作为左值
	std::cout << "ret05= " << ret05 << std::endl;

	system("pause");
	return 0;
}

测试代码结果 alt text alt text

三、引用的本质

引用的本质在C++内部实现是一个指针常量,引用类型必须和引用实体是同类型。

四、常量引用

作用:用来修饰形参,防止误操作。

代码说明:

//四、常量引用
	//int &ref = 10; 这样的操作是不被允许的,因为赋值给引用的是一个字面量,引用需要一个合法的空间,
	// 程序运行时没有专门为其分配可修改的内存空间
	// 但是如果在前面在上const,这条代码就通过
	// const int &ref = 10;
	//用来修饰形参,防止误操作
void test06(const int &val)
{
	//val = 100;不被允许修改
	std::cout << "val = " << val << std::endl;
}

函数进阶

一、函数默认参数

返回值 函数名(参数 = 默认值){}

注意事项:

1.如果某个位置已经有了默认参数,那么该位置往后都必须有默认参数

//函数默认参数
void test(int a ,int b = 20,int c = 30)
{
	std::cout << "a = " << a << std::endl;
	std::cout << "b = " << b << std::endl;
	std::cout << "c = " << c << std::endl;
}

2.函数的声明有默认参数,函数实现不能有默认参数,声明和实现只能有一个有默认值

#include <iostream>
//函数声明
void test(int a=10,int b = 20);//
//函数实现
void test(int a ,int b)
{
	std::cout << "a = " << a << std::endl;
	std::cout << "b = " << b << std::endl;
}

二、函数占位参数

作用:为了函数声明和定义的统一 语法:返回类型 函数名(数据类型){}

//函数占位参数
void test(int a,int)//第二个参数为占位参数
{
    std::cout << "a = " << a << std::endl;
}

int main() {
    test(10,10);//占位参数必须填补,否则无法运行
}

注意:占位参数可以有默认参数,此时调用时就不用在传值

//函数占位参数
void test(int a,int = 10)//第二个参数为占位参数,含默认值
{
    std::cout << "a = " << a << std::endl;
}

int main() {
    test(10);//不需要传站位参数
}

三、函数重载

满足条件:

  • 在同一个作用域下。
  • 函数名相同,
  • 参数类型不同,
  • 参数个数不同,
  • 参数顺序不同。

作用: 提高复用性

注意:

1.返回值不能作为重载的条件

2.引用作为重载的条件

加const和不加是可以重载的

void fun(int &a){
    std::cout<<"fun(int &a)"<<std::endl;
}
void fun(const int &a){
    std::cout<<"fun(const int &a)"<<std::endl;
}
int main(){
    int a = 10;
    const int &b = 20;
    fun(a);//调用fun(int &a)
    fun(b);//调用fun(const int &a)
    fun(10);//调用fun(const int &a)
    return 0;
}

3.函数重载遇到默认参数

void fun2(int a,int b = 10){
    std::cout<<"fun(int &a)"<<std::endl;
}
void fun2(int a){
    std::cout<<"fun(const int &a)"<<std::endl;
}
int main(){

    fun2(10);//此时程序出现二义性,无法确定调用哪个函数
    return 0;
}

类和对象的基本概念

C++的特性:封装、多态、继承

一、基本概念

  • 抽象:将事物所能进行的的行为归纳出来,形成函数,这些函数可以操作事物归纳后的数据结构。
  • 类: 、

封装

将事物归纳的数据结构和操作该数据结构的算法呈现显而易见的紧密关系,叫封装。

一、 封装的意义

1.将属性和行为作为一个整体,表现生活中的事物。

namespace test_class
{
	//设计一个圆的类
#define PI 3.14
	class Circle
	{
		//访问权限
	public:
		//属性
		int m_r;
		//行为
		double calculateZC()
		{
			return 2 * PI * m_r;
		}
	};
	void test() {
		Circle c1;
		c1.m_r = 10;
		cout << "圆的周长 = " << c1.calculateZC() << endl;
	}
}

2.访问权限

  • public 公共权限
  • protected 保护权限
  • private 私有权限

二、访问权限说明

  • public修饰的成员,可以在类内或者类外直接访问
  • protected修饰的成员,只能在类内和子类中访问
  • private修饰的成员,只能在类内访问

三、struct 与 Class 的区别

  • 默认访问权限不同
  • struct 默认权限为 public
  • class 默认权限为 private

四、设置访问权限

一般对于变量设置为private,当要访问private的变量时,通过public的函数来访问。

namespace test_class
{
	class Person
	{
	private:
		//姓名
		string m_Name;
		//年龄
		int m_Age;
    public:
		void setAge(int age)
		{
			m_Age = age;
        }
    };
    int main(){
        Person p;
        p.setAge(18);
        return 0;
    }
}

对象特性

一、初始化和清零 如果我们不提供构造函数,编译器会自动提供默认构造函数,但两个函数是空实现。

1.作用

  • 构造函数:主要作用在于粗昂见对象那个时为对象成员属性赋值,
  • 析构函数:主要作用在于对象销毁前系统自动调用析构函数,执行一些清理工作。

2.语法 alt text alt text

二、构造函数的分类及调用

  1. 按参数分类:有参构造和无参构造(默认构造函数)
  2. 按类型分类:普通构造和拷贝构造

3.调用规则

  • 如果定义了有参构造,最好显示定义无参构造
  • 如果定义了拷贝构造,最好显示定义赋值构造
namespace test
{
	//构造函数的分类及调用

	//分类 按照参数分:有参构造函数和无参构造函数(默认构造函数)
	//		按照类型分:普通构造和拷贝构造
	class Person
	{
		public:
			Person() {
				cout << "调用无参构造函数" << endl;
			}
			Person(int Age)
			{
				age = Age;
				cout << "调用有参构造函数" << endl;
			}
			Person(const Person &p)
			{
				age = p.age;
				cout << "调用拷贝构造函数" << endl;
			}
			~Person() {
				cout << "调用析构函数" << endl;
			}

		private:
			int age;
	};
	//调用
	void test() {
		//1.括号法
		Person p1;
		Person p2(18);
		Person p3(p2);
		//注意事项
		// 如果写的是 Person p1();那么编译器会认为是函数声明,而不会调用默认构造函数

		//2.显示法
		Person p1;
		Person p2 = Person(10);
		Person p3 = Person(p2);

		//匿名对象:只写Person (10)的形式,特点是当前行执行结束后,系统会立即回收
		// 注意2:
		// 不要使用拷贝构造函数初始化匿名对象、编译器会认为Person (p3) === Person p3;即对象声明
		//3.隐式调用
		Person p4 = 10;//隐式转换为Person p4 = Person(10);
	}
}

三、拷贝构造函数调用时机

1.使用一个已经创建完毕的对象来初始化一个新对象 2.值传递的方式给函数参数传值 3.值方式返回局部对象

namespace test_copy
{
	//拷贝构造函数调用时机
	class Person
	{
	public:
		Person() {
			cout << "调用无参构造函数" << endl;
		}
		Person(int Age)
		{
			age = Age;
			cout << "调用有参构造函数" << endl;
		}
		Person(const Person& p)
		{
			age = p.age;
			cout << "调用拷贝构造函数" << endl;
		}
		int getAge() {
			return age;
		}
		~Person() {
			cout << "调用析构函数" << endl;
		}

	private:
		int age;
	};
	//1.使用一个创建好的对象初始化一个新的对象
	void test01() {
		Person p(19);
		Person p2(p);
	}
	//2.值传递方式传给函数参数传值
	void doWork01(Person p){
		cout << "值传递方式调用拷贝构造函数" << endl;
	}
	//3.以返回值的方式返回
	Person doWork02() {
		Person p1;
		cout << (int*)&p1 << endl;
		return p1;//返回时不直接返回p而是通过拷贝构造函数,拷贝一份返回给外面。当然,也有存在被编译器优化的情况
					//这个时候就不会调用拷贝构造函数
	}
	void test02() {
		Person p = doWork02();
		cout << (int*)&p << endl;
		//cout << p.getAge() << endl;
	}
}

四、构造函数调用规则

1.默认情况下,c++编译器至少给一个类添加3个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性值进行拷贝

2.如果我们提供了有参构造函数,编译器就不会提供默认构造函数,但会提供默认拷贝构造函数

3.如果我们提供了拷贝构造函数,编译器就不会提供其他构造函数

五、深拷贝与浅拷贝

1.浅拷贝:简单的赋值拷贝操作 2.深拷贝:在堆区重新申请空间,进行拷贝操作

namespace test_deepcopy
{
	class Person
	{
	public:
		Person(int age,int height);
		Person(const Person& p);
		~Person();

		int m_Age;
		int* m_Height;
	};

	Person::Person(int age,int height)
	{
		m_Age = age;
		m_Height = new int(height);//在堆区申请一块内存空间用于存储身高
		cout << "调用构有参造函数" << endl;
	}
	Person::Person(const Person& p)
	{
		m_Age = p.m_Age;
		//m_Height = p.m_Height;//这段代码就是浅拷贝操作
		m_Height = new int(*p.m_Height);//这段代码就是在堆区另申请内存空间,存储赋值过来的身高值
	}
	Person::~Person()
	{
		if (m_Height != NULL)
		{
			delete m_Height;
			m_Height = NULL;//避免野指针出现置空
		}
		cout << "调用析构函数" << endl;
	}
	void test()
	{
		Person p(18, 180);
		cout << "person年龄:" << p.m_Age << "身高:" << *p.m_Height << endl;

		// 为测试浅拷贝与深拷贝写如下代码
		Person p1(p);//在没有写拷贝构造函数中的申请内存空间情况下,程序会报错,原因是程序是浅拷贝,导致第二次调用析构函数的时候报错
		cout << "person年龄:" << p1.m_Age << "身高:" << *p1.m_Height << endl;
	}
}

结果如下:

alt text alt text

六、初始化列表

1.作用

用于初始化属性

2.语法 alt text alt text

namespace test_init
{
	class Person
	{
	public:
		//初始化列表
		//Person() :m_A(10), m_B(20), m_C(30) { }
		Person(int a,int b,int c) :m_A(a), m_B(b), m_C(c) { }

		~Person() {}

		int m_A;
		int m_B;
		int m_C;
		
	};
	void test() {
		Person p(10, 20, 30);
		cout << "m_A:" << p.m_A << endl;
		cout << "m_B:" << p.m_B << endl;
		cout << "m_C:" << p.m_C << endl;
	}
}

七、类对象作为类成员

1.类中的成员可以是另一个类的对象,我们称该成员为对象成员

2.类对象作为类成员时,其构造顺序和声明顺序一致,析构顺序和构造相反

八、静态成员

1.静态成员变量

  • 所有对象共享同一份数据
  • 在编译阶段分配内存(代码运行前分配内存,在全局区)
  • 类内声明,类外初始化

2.静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量

3.访问静态成员变量 alt text alt text

4.静态成员变量也有访问权限

  • 公有静态成员变量类外可以访问
  • 私有静态成员变量类外不可以访问
namespace test_static
{
	class Person
	{
	public:
		static int m_A;
		int m_B;
	private:
		static int m_C;
	};
	int Person::m_A = 10;
	void test01() {
		//1.通过对象访问
		Person p;
		p.m_A = 100;
		cout << "p.m_A = " << p.m_A << endl;
		cout << "p.m_B = " << p.m_B << endl;
		//2.通过类名访问
		cout << "Person::m_A = " << Person::m_A << endl;
		//cout << "Person::m_B = " << Person::m_B << endl;//报错,私有成员变量类外不可以访问
	}
}

5.静态成员函数

  • 所有对象共享一个函数
  • 静态成员函数只能访问静态成员变量
namespace test_static_func
{
	class Person
	{
	public:
		static void func() {
			m_A = 100;//静态成员函数可以访问静态成员变量
			//m_B = 100;//静态成员函数不可以访问非静态成员变量
			cout << "static_func调用" << endl;
		
		_}
		static int m_A;
		int m_B;
	}
	int Person::m_A = 10;
	void test01() {
		//1.通过对象访问
		Person p;
		p.func();
		//2.通过类名访问
		Person::func();
	}
}

注意:静态成员函数也有访问权限

九、C++对象模型和this指针

1.成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上,静态成员函数不属于类的对象上。

namespace test_this{
	class Person
	{
	};
	class Person2
	{
		int m_A;
		static m_B;//静态成员,不在类对象上
		void func(){}//非静态成员函数,不在类对象上【原因需要看下一节】
		static void func2(){}//静态成员函数,不在类对象上
	};
	int Person::m_B = 10;
	void test01() {
		Person p;
		//空对象占用内存为1
		//原因:C++编译器给每个空对象也分配一个字节空间,是为了区分空对象占用的内存区域,否则,空对象和字符数组无法区分
		cout << sizeof(p) << endl;//1
	}
	void test02() {
	    Preson2 p2;
		cout <<sizeof(p2)<<endl;
		//空对象占用内存为4
		//原因:此时不是5而是4,当不是空的时候,为int类型,占用4个字节
		//同时也验证了静态成员变量不属于类的对象上。
	}
}

2.this指针

this是指针,谁调用,this指针就指向谁,

用途:

  • 通过this指针访问类的成员
  • this指针的本质是指针常量,即指针的值不能修改
  • 在类的非静态成员函数中,this指针默认作为隐含参数存在,不需要定义

(1)名称冲突:

namespace test_this{
	class Person
	{
	public:
		Person(int age) {
			age = age;
		}
		int age;
	}
	void test01() {
		Person p(19);
	     cout<<"age:"<<p.age<<endl;
	}
}

此时,得到的数据不是18,原因在于“age = age;”与"int age;“名字相同但不是同一个东西,解决的办法有而,第一种是如之前一样定义一个"int m_Age;",然后在代码中使用"m_Age = age;"。

第二种就是使用this指针,代码如下:

namespace test_this{
	class Person
	{
	public:
		Person(int age) {
			this->age = age;
		}
		int age;
	}
	void test01() {
		Person p(19);
	     cout<<"age:"<<p.age<<endl;
	}
}

这种办法也可以实现正常输出,这里就解释了this指针的用途。对象p调用Person(int age)函数,此时this指针指向p,this指针指向p,this->age就相当于p.age。

(2)以返回值方式返回对象本身

namespace test_this {
    class Person {
    public:
        // 修改1:添加类定义结尾的分号
        void addAge(int age) {
            this->m_Age += age;
        }
        int m_Age = 10;
    };  // 修正:补充分号

    void test01() {
        Person p;
        p.addAge(10);
        // 修改2:void 返回值无法输出,需直接访问成员变量
        cout << "age:" << p.m_Age << endl;  // 输出 20
    }
}

此时得到的结果是20,如果想实现p.addAge(10).addAge(10).addAge(10)这样的操作,那么需要修改为如下代码:

namespace test_this {
    class Person {
    public:
        // 返回引用以实现链式调用
        Person& addAge(int age) {
            this->m_Age += age;
            return *this; // 返回对象本身的引用
        }
        int m_Age = 10;
    };

    // 修改3:重载 operator<< 以支持输出 Person 对象
    ostream& operator<<(ostream& os, const Person& p) {
        os << p.m_Age;
        return os;
    }

    void test01() {
        Person p;
        // 链式调用操作的是同一个对象(p)
        p.addAge(10).addAge(10).addAge(10);
        cout << "p.m_Age: " << p.m_Age << endl; // 输出 40
    }
}

这时候输出的结果就是40,实现了链式调用,类似与cout<<a<<b<<c<<endl;这样的操作。

当返回类型是 Person(而非引用)时,p.addAge(10) 会返回一个​​临时副本​​,后续的 .addAge(10) 操作修改的是临时副本的 m_Age,而非原始对象 p。

Person addAge(int age) { /* ... */ } // 返回副本

Person p;
p.addAge(10).addAge(10);  // 第二个 addAge 操作的是临时副本
cout << p.m_Age;          // 输出 20(仅第一次修改生效)

十、空指针的访问函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有访问成员变量。

namespace test_nullptr {
    class Person {
    public:
        void showClassName() {
            cout << "this is Person class" << endl;
        }
        void showPersonAge() {
			//报错的原因是因为传入的指针是空指针,空指针访问成员变量会报错,解决办法如下:
			// if(this == NULL){
			// 	return;
			// }
            cout << "age = " << m_Age << endl;//这条代码的本质:cout << "age = " << this->m_Age << endl;
        }
		int m_Age;
	};
	void test01() {
	    Person *P = NULL;
		p->showClassName();//不报错
		p->showPersonAge();//报错,空指针访问成员变量
	}
}

十一、const修饰成员函数

1.常函数

  • 成员函数后加const,称为常函数
  • 常函数内不可以修改成员变量,除了 mutable修饰的成员变量

2.常对象

  • 声明对象前加const,称为常对象
  • 常对象只能调用常函数
namespace test_const{
//常函数
class Person{
	public:
	//this指针的本质是指针常量,指针指向的内容不可以修改
	//Person * const this
	//如果在成员函数后写了const,那么上面的语句就变成:
	//const Person * const this
	//在成员函数的后面加const,本质上是修饰了this指针,让this指针指向的值不可修改
		void showPerson() const
		{
			//this->m_A = 100;//常函数不可以修改成员变量
			m_B = 100;//在变量前加mutable,让这个变量永远可变
		}
		void func(){
			cout<<"func调用"<<endl;
		}
		int m_A;
		mutable int m_B;//在变量前加mutable,让这个变量永远可变
}
void test01() {
    Person p;
	p.showPerson();
}
//常对象
void test02() {
    const Person p;
	//p.m_A = 100;//常对象不可以修改成员变量
	p.m_B = 100;//可以调用
	p.showPerson();//可以调用
	p.func();//可以调用
}
}

alt text alt text

友元

友元关键字:friend

一、全局函数做友元

作用:全局函数做友元,全局函数就可以访问类中的私有内容

namespace test_friend{
	class Building{
		//告诉编译器GoodGay全局函数是Building类的好朋友,可以访问类中的私有内容
		friend void GoodGay(Building &building);
		public:
			Building(){
				this->m_SittingRoom = "客厅";
				this->m_BedRoom = "卧室";
			}
		public:
			string m_SittingRoom;
		private:
			string m_BedRoom;
	};
	void GoodGay(Building &buiding){
		cout<<"好基友正在访问"<<buiding.m_SittingRoom<<endl;
		cout<<"好基友正在访问"<<buiding.m_BedRoom<<endl;
	}
	void test01(){
	    Building buiding;
		GoodGay(buiding);
	}
}

二、友元类

目的:让一个类可以访问另一个类的私有成员

友元类的所有成员函数都可以访问对方类的私有保护成员。

namespace test_friend{
	class Building;//先写出来防止报错
	class GoodGay{
		public:
			GoodGay();
			void visit();//参观函数访问Building中的成员
			Building *m_Building;//让该类有访问Building的权限
	};
	class Building{
		friend class GoodGay;//告诉编译器GoodGay类是Building的好朋友,可以访问Building的私有内容
		public:
			Building();//构造函数
		public:
			string m_SittingRoom;
		private:
		    string m_BedRoom;
	};
	GoodGay::GoodGay(){
		this->m_Building = new Building();//在对象被初始化时,就创建一个Building对象,方便后续访问Building中的成员
	}
	Building::Building(){//当building对象被创建的时候,给变量初始化
		this->m_SittingRoom = "客厅";
		this->m_BedRoom = "卧室";
	}
	void GoodGay::visit(){
		cout<<"好基友正在访问:"<<this->m_Building.m_SittingRoom<<endl;
		cout<<"好基友正在访问:"<<this->m_Building.m_BedRoom<<endl;//在没有声明GoodGay为Building的友元类时,这句代码不通过。
	}
	void test01(){
		GoodGay gg;
		gg.visit(); 
	}
}

三、成员函数做友元

作用:

```cpp
namespace test_friend{
	class Building;
	class GoodGay{
		public:
			GoodGay();
			void visit();//参观函数访问Building中的成员
			void visit2();//让参观函数不可以访问访问Building中的成员
			Building *building;//让该类有访问Building的权限
	};
	class Building{
		//告诉编译器GoodGay类中的visit成员函数是Building好朋友,可以访问Building的私有内容
		friend void GoodGay::visit();
		public:
			Building();//构造函数
		public:
			string m_SittingRoom;
		private:
		    string m_BedRoom;
	};
	Building::Building(){
		m_SittingRoom = "客厅";
		m_BedRoom ="卧室";

	}
	GoodGay::GoodGay(){
		building = new Building();//在对象被初始化时,就创建一个Building对象
	}
	void GoodGay::visit(){
		cout<<"好基友正在访问:"<<building->m_SittingRoom<<endl;
		cout<<"好基友正在访问:"<<building->m_BedRoom<<endl;
	}
	void GoodGay::visit2(){
		cout<<"好基友正在访问:"<<building->m_SittingRoom<<endl;
	}
	void test01(){
		GoodGay gg;
		gg.visit();
		gg.visit2();
	}
}

未加入关键字friend时: alt text alt text

加入后: alt text alt text

结果: alt text alt text

运算符重载

作用:让自定义的类,对象,也能够进行加减乘除等操作。

加号运算符重载

实现两个自定义数据结构的相加

需求: alt text alt text alt text alt text

namespace test_add
{
	class Person
	{
	public:
		//1、成员函数重载
		/*Person operator+ (Person &p)
		{
			Person temp;
			temp.m_A = this->m_A + p.m_A;
			temp.m_B = this->m_B + p.m_B;
			return temp;
		}*/
		int m_A;
		int m_B;

	};
	Person operator+(Person& p1, Person& p2)
	{
		Person temp;
		temp.m_A = p1.m_A + p2.m_A;
		temp.m_B = p1.m_B + p2.m_B;
		return temp;
	}
	void test() {
		Person p1;
		p1.m_A = 20;
		p1.m_B = 10;
		Person p2;
		p2.m_A = 20;
		p2.m_B = 10;
		//本质:Person ret = p2.operator+(p1)或者Person ret = operator+(p2,p1);简化为如下写法:
		Person p3 = p1 + p2;

		cout << "结果:" << p3.m_A << endl;
		cout << "结果:" << p3.m_B << endl;
	}
}

除了以上的写法我们还可以使用函数重载的知识,将operator+重载实现Person p4 = p1+10;

左移运算符重载

作用:可以输出自定义数据类型

通过上面的学习我们可以使用两种办法实现输出自定义数据类型,但是此处如果使用成员函数重载,那么会出现对象在左侧,而cout在右侧的情况,详细如下图解释: alt text alt text

所以一般的作揖运算符重载使用全局函数重载,代码如下:

namespace test_LeftMove
{
	class Person
	{
		//使用友元思想实现类外函数访问类内私有成员
		friend ostream& operator<<(ostream& cout, Person& p);
	public:
		Person(int a,int b):m_A(a),m_B(b){}
		private://数据一般为私有
			int m_A;
			int m_B;
	};
	//此处传入的值与返回的值都是用引用,对于ostream& cout是唯一的,
	// 所以使用引用,ostream&作为返回值类型是为了实现链式输出
	//即:cout << p << endl;
	ostream& operator<<(ostream& cout, Person &p)
	{
		cout << "m_A = " << p.m_A << "  m_B = " << p.m_B;
		return cout;
	}
	void test() {
		Person p(10,20);
		cout << p << endl;
	}
}

递增运算符重载

作用:实现对自定义数据类型的递增

namespace test_MyInteger {
	class MyInteger
	{
		friend ostream& operator<<(ostream& cout, const MyInteger& myint);
	public:
		MyInteger(){
			Integer = 0;
		}
		//前置运算符
		MyInteger& operator++() {
			Integer++;
			return *this;
		}
		//后置运算符
		MyInteger operator++(int)//int是占位符,用于区分是前置还是后置运算符
		{
			MyInteger myint = *this;//记录当前值
			Integer++;//对象属性自增
			return myint;//返回记录值而不是自增后的值
		}
	private:
		int Integer;
	};
	ostream& operator<<(ostream& cout, const MyInteger& myint)
	{
		cout << myint.Integer;
		return cout;
	}
	
	void test1() {
		MyInteger myint;
		cout << ++(++myint) << endl;//如果返回的不是引用,那么这里的++(++myint)也能通过,但是并不是在原来的对象上进行操作,而是一个新的对象上操作。
		cout << myint << endl;
	}
	void test2() {
		MyInteger myint;
		cout << (myint++)++ << endl;//这样的操作精良避免发生,这不符合C++语法,因为第二次自增是在临时对象上自增而不是原始对象。
		cout << myint << endl;
	}

}

关键点总结

  • ​​右值与左值引用​​:非 const 左值引用 (MyInteger&) 无法绑定到右值(如 myint++ 返回的临时对象)。
  • ​const 引用的灵活性​​:const 左值引用 (const MyInteger&) 可以绑定到右值和左值,解决了参数匹配问题。

所以上面的代码中,«的重载第二个参数是const的引用类型,如果是非const的引用类型,那么在test2()函数中,cout << myint++<<endl;就会报错,因为 myint++的返回值是右值引用。

赋值运算符重载

C++编译器至少给一个类添加4个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性值进行拷贝
  • 赋值运算符重载函数operator=(对属性值进行拷贝)

alt text alt text

解决办法: alt text alt text

namespace test_assig
{
	class Person
	{
	public:
		Person(int age) {
			m_Age = new int(age);
		}

		~Person() {
			if (m_Age != NULL)
			{
				delete m_Age;
				m_Age = NULL;
			}
		}
		Person& operator=(Person &p) {
			//默认赋值语句
			//m_Age= p.age;

			//先判断是否有属性在堆区,如果有先删除干净,再深拷贝
			if (m_Age != NULL)
			{
				delete m_Age;
				m_Age = NULL;
			}
			//深拷贝
			m_Age = new int(*p.m_Age);

			return *this;
		}
		int* m_Age;

	};
	void test() {
		Person p1(18);
		Person p2(20);
		Person p3(90);

		p3=p1 = p2;

		cout << *p1.m_Age << endl;
		cout << *p2.m_Age << endl;
		cout << *p3.m_Age << endl;

	}
}

关系运算符重载

作用:重载关系运算符。可以让两个自定义类型对象进行对比操作。

namespace test_relation
{
	class Person
	{
	public:
		Person(string name, int age)
		{
			m_Name = name;
			m_Age = age;
		}
		bool operator==(Person& p)
		{
			if (m_Age == p.m_Age && m_Name == p.m_Name)
			{
				return true;
			}
			else {
				return false;
			}
		}
		int m_Age;
		string m_Name;

	};
	void test()
	{
		Person p1("JUN", 18);
		Person p2("JUN", 18);

		if (p1 == p2)
		{
			cout << "p1与p2相等" << endl;
		}
		else
		{
			cout << "p1与p2不相等" << endl;
		}
	}
}

函数调用运算符重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称之为仿函数
  • 仿函数没有固定写法,非常灵活
namespace test_call
{
	class MyPrint
	{
	public:
		void operator()(string text)
		{
			cout << text << endl;
		}
		void operator()(int a, int b)
		{
			cout << a + b << endl;
		}
	};
	void test()
	{
		MyPrint myprint;
		myprint("hello world");
		myprint(1, 2);
		cout<<MyPritn()("hello world")<<endl;//MyPritn()是匿名对象,作用域在当前行,所以输出后立即销毁
	}
}

继承

继承的基本语法

class 子类 : 继承方式 父类
{
	//类体
};

继承方式一共有三种:public、protected、private

以下是 publicprotectedprivate 访问权限的总结对比表(Markdown 格式):

访问权限对比表

场景 public protected private
类内部访问 ✅ 直接访问 ✅ 直接访问 ✅ 直接访问
派生类内部访问 ✅ 直接访问 ✅ 直接访问 ❌ 不可直接访问
外部代码访问 ✅ 直接访问 ❌ 不可直接访问 ❌ 不可直接访问
默认继承方式 类默认 private 继承 结构体默认 public 继承 -

继承方式对基类成员权限的影响

基类成员权限 public 继承后 protected 继承后 private 继承后
public 成员 public(保留) protected(降级) private(降级)
protected 成员 protected(保留) protected(保留) private(降级)
private 成员 ❌ 不可见 ❌ 不可见 ❌ 不可见

继承后的访问权限:

  • 父类的私有成员,哪种方式继承都无法访问;
  • public继承,原来是什么还是什么:public–>public,protected–>protected;
  • protected继承,都变保护:public–>protected,protected–>protected;
  • private继承,都变私有:public–>private,protected–>private;

继承中的对象模型

namespace test_model
{
	class Base {
	public:
		int m_A;
	protected:
		int m_B;
	private:
		int m_C;
	};
	class Son :public Base {
	public:
		int m_D;
	};
	void test(){
		cout<<"sizeof(Base) = "<<sizeof(Base)<<endl;
	}
}

结果为16,因此可知父类中的所有非静态成员属性都被子类继承了,并且子类对象中包含了父类对象,父类对象在子类对象中是隐藏的。

另一种方式查看类的大小: alt text alt text

继承构造和析构函数

问题:父类和子类的构造和析构函数的调用顺序是什么?

namespace test_construct_destruct{
	class Base{
		public:
			Base(){
				cout<<"Base构造函数"<<endl;
			}
			~Base(){
				cout<<"Base析构函数"<<endl;
			}
	};
	class Son:public Base{
		public:
			Son(){
				cout<<"Son构造函数"<<endl;
			}
			~Son(){
				cout<<"Son析构函数"<<endl;
			}
	}
}

结果为:

Base构造函数
Son构造函数
Son析构函数
Base析构函数

继承同名成员处理方式

问题:当子类和父类出现同名的成员,如何通过子类对象,访问到父类和子类的同名成员?

  • 访问子类同名成员直接访问即可;
  • 访问父类同名成员需要加作用域;
namespace test_same_name{
	class Base{
		public:
			Base()
			{
				m_A = 100;
			}
			void func(){
				cout<<"Base func()"<<endl;
			}
			void func(int a){
				cout<<"Base func(int a)"<<endl;
			}
			int m_A;
	};
	class Son:public Base{
		public:
			Son()
			{
				m_A = 200;
			}
			void func(){
				cout<<"Son func()"<<endl;
			}
			int m_A;
	};
	//同名的成员属性
	void test01(){
		Son s;
		cout<<s.m_A<<endl;
		cout<<s.Base::m_A<<endl;
	}
	//同名的函数
	void test02(){
		Son s;
		s.func():
		s.Base::func();//通过作用域解决

		//下面的代码不通过,因为当子类和父类存在相同函数名时,子类的同名函数会屏蔽父类的同名函数,要想访问父类的同名函数,需要加作用域
		//s.func(10);
	}
}

继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

  • 访问子类同名静态成员,直接通过子类对象访问即可;
  • 访问父类同名静态成员,需要加作用域;
namespace test_same_static{
	class Base{
		public:
			static int m_A;
			static void func(){
				cout<<"Base func()"<<endl;
			}
	};
	class Son:public Base{
		public:
			static int m_A;
			static void func(){
				cout<<"Son func()"<<endl;
			}
	};


	int Base::m_A = 100;
	int Son::m_A = 200;
	//同名的静态成员属性
	void test01(){
		Son s;

		//通过对象访问
		cout<<s.m_A<<endl;
		cout<<s.Base::m_A<<endl;

		//通过类名访问
		cout<<Son::m_A<<endl;
		cout<<Son::Base::m_A<<endl;
	}
	//同名的静态成员函数
	void test02(){
		Son s;
		s.func();
		s.Base::func();

		//通过对象访问
		Son::func();
		Son::Base::func();
	}
}

多继承

C++允许一个类继承多个类:

语法:class 子类:继承方式 父类1, 继承方式 父类2...

多继承可能会引发父类中有同名成员出现的情况,需要加作用域区分。

C++实际开发中不建议使用多继承,因为会引发很多二义性问题。

namespace test_multi_inherit{
	class Base1{
		public:
			Base1(){
				m_A = 100;
				cout<<"Base1构造函数"<<endl;
			}
			int m_A;
	}
	class Base2{
		public:
			Base2(){
				m_A = 200;
				cout<<"Base2构造函数"<<endl;
			}
			int m_A;
	};
	class Son:public Base1,public Base2{
		public:
			Son(){
				m_A = 300;
				m_B = 400;
				cout<<"Son构造函数"<<endl;
			}
			int m_A;
			int m_B;
	};
	void test(){
		Son s;//调用Base1构造函数,再调用Base2构造函数,最后调用Son构造函数
	}
}

菱形继承

菱形继承的概念:

  • 两个派生类继承同一个基类;
  • 又有一个类同时继承这两个派生类;
  • 这个类就叫做菱形继承;
  • 菱形继承会导致二义性问题

经典菱形继承案例:

alt text alt text

出现的问题: alt text alt text

如何解决:

namespace test_diamond_inherit {
	//动物类
	class Animal {
	public:
		int m_Age;
	};
	//利用虚继承可以解决菱形继承的问题
	//在继承之前加virtual
	//羊类
	class Sheep :virtual public Animal {
	};
	//驼类
	class Tuo :virtual public Animal {
	};
	//羊驼类
	class SheepTuo :public Sheep, public Tuo {
	};
	void test() {
		SheepTuo st;
		//当菱形继承,连个父类拥有相同的数据时,需要加作用域加以区分
		st.Sheep::m_Age = 10;
		st.Tuo::m_Age = 20;

		
		cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
		cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
		//当添加virtual关键字后,就可以使用下面的语句,而且不会出现二义性
		cout << "st.m_Age = " << st.m_Age << endl;
	}
}

在没有使用virtual前,输入cl /d1 repoterSingleClassLayoutSheepTuo 类与对象.cpp后的结果如下图 alt text alt text

加入virtual后,输入cl /d1 repoterSingleClassLayoutSheepTuo 类与对象.cpp后的结果如下图,下图就解释了为什么使用virtual能解决菱形继承问题。 alt text alt text

多态

多态的基本概念

多态的含义:

**​​多态是允许不同对象通过统一的接口执行特定行为的能力,其意义在于提升代码的灵活性和可维护性,实现“接口复用,逻辑分离”。**​​(示例:基类Animal定义虚函数speak(),子类Dog和Cat分别实现自己的speak(),通过基类指针调用时自动执行对应子类逻辑。)

多态时C++三大特性之一,它分为编译时多态和运行时多态。

  • 编译时多态:函数重载与运算符重载属于静态多态,复用函数名
  • 运行时多态:派生类与虚函数实现运行时多态

二者的区别

  • 静态多态函数的地址早绑定,在编译阶段确定函数地址;
  • 动态多态函数的地址晚绑定,在运行阶段确定函数地址;

动态多态的满足条件:

  • 1.有继承关系;
  • 2.子类要重写父类的虚函数;(注意不是重载,是重写,父类中的函数要加virtual,至于子类可写可不写

动态多态的使用方式:

  • 父类的指针或者引用 指向子类对象;
namespace test_polymorphism
{
	class Animal
	{
		public:
		virtual void speak()
		{
			cout << "动物在说话" << endl;
		}
	};
	class Cat :public Animal
	{
		public:
		void speak()
		{
			cou<<"喵喵喵"<<endl;
		}
	};
	void doSpeak(Animal & animal)//父类的指针可以指向子类的对象
	{
		animal.speak();
	}
	void test(){
		Cat cat;
		doSpeak(cat);//在未添加virtual关键字时,输出的是:动物在说话,添加后输出的是:喵喵喵,原因是未添加前,在编译阶段确定了speak()函数地址,而添加后,程序运行阶段才确定speak()函数地址
	}
}

动态的底层原理

namespace test_polymorphism
{
	class Animal
	{
		public:
		virtual void speak()
		{
			cout << "动物在说话" << endl;
		}
	};
	class Cat :public Animal
	{
		public:
		void speak()
		{
			cou<<"喵喵喵"<<endl;
		}
	};
	void doSpeak(Animal & animal)//父类的指针可以指向子类的对象
	{
		animal.speak();
	}
	void test01(){
		Cat cat;
		doSpeak(cat);
	}
	void test02(){
	    cout<<sizeof(Animal)<<endl;
	}
}

如果未添加virtual关键字,那么输出的size大小是1,也就是空类,但是加了virtual后,输出的结果是4,也就是指针。

1.解释方式1

alt text alt text

在上图中,当在子类重写父类方法后,子类的重写的方法会在虚函数表中覆盖从父类继承的方法。当用父类指针(或者引用)指向子类,代码运行时程序就会通过虚函数表找到子类重写的方法,从而实现多态。

2.解释方法2

子类没有重写前,子类的结构: alt text alt text

子类重写后,子类的结构: alt text alt text

经典案例-计算器类

//计算器抽象类
class AbstractCalculator
{
public:
	virtual int getResult()
	{	
		return 0;		
	}
	virtual string reOperator()
	{
		return " ";
	}
	int m_Num1;
	int m_Num2;
};

ostream& operator<<(ostream &cout, AbstractCalculator &abc)
{
	cout << abc.m_Num1 << abc.reOperator() << abc.m_Num2 << "=" << abc.getResult();
	return cout;
}
//加法类
class AddCalculator :public AbstractCalculator
{
public:
	AddCalculator(int a,int b){
		m_Num1 = a;
		m_Num2 = b;
	}
	int getResult()
	{
		return m_Num1 + m_Num2;
	}
	string reOperator()
	{
		return "+";
	}
};
void test01()
{
	AbstractCalculator *abc = new AddCalculator(1,2);
	cout << *abc << endl;
	delete abc;
}

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;

当类中有了纯虚函数,这个类就变成了抽象类

抽象类特点:

  • 1.无法实例化对象;
  • 2.子类必须重写抽象类中的纯虚函数,否则也变成抽象类;
//多态-纯虚函数和抽象类
namespace test_pvfAndAbc
{
	class Base
	{
		//纯虚函数,该类为抽象类
		//1.无法实例化对象
		//2.子类必须重写父类的纯虚函数
	public:
		virtual void func() = 0;
	};
	class Son :public Base
	{
	public:
		void func()
		{
			cout << "func()" << endl;
		}
	};
	void test01() {
		//Base b;抽象类无法实例化对象
		//new Base; 抽象类无法实例化对象
		//Son s;在没有重写抽象类时,无法实例化对象
		Base *base = new Son;
		base->func();
	}
}

纯虚函数案例

//纯虚函数案例
class AbstractDrinking
{
public:
	//煮水
	virtual void Boil() = 0;
	//冲泡
	virtual void Brew() = 0;
	//倒入
	virtual void PourInCup() = 0;
	//加入辅料
	virtual void PutSomething() = 0;
	//制作饮品
	void makeDrink()
	{
		Boil();
		Brew();
		PourInCup();
		PutSomething();
	}
};
class Coffer :public AbstractDrinking
{
public:
	//煮水
	virtual void Boil()
	{
		cout << "煮矿泉水" << endl;
	}
	//冲泡
	virtual void Brew()
	{
		cout << "热水冲泡咖啡" << endl;
	}
	//倒入
	virtual void PourInCup()
	{
		cout << "倒入杯子" << endl;
	}
	//加入辅料
	virtual void PutSomething()
	{
		cout << "加入冰块与牛奶" << endl;
	}
};

class Tea :public AbstractDrinking
{
public:
	//煮水
	virtual void Boil()
	{
		cout << "煮泉水" << endl;
	}
	//冲泡
	virtual void Brew()
	{
		cout << "冲泡茶叶" << endl;
	}
	//倒入
	virtual void PourInCup()
	{
		cout << "倒入杯子" << endl;
	}
	//加入辅料
	virtual void PutSomething()
	{
		cout << "加入红枣" << endl;
	}
};
void doWork(AbstractDrinking* abd)
{
	abd->makeDrink();
	delete abd;
}
void test01() {
	//制作咖啡
	doWork(new Coffer);

	cout << "---------------"<<endl;
	//制作茶
	doWork(new Tea);
}

虚析构和纯虚析构

多态使用时,如果子类有属性开辟到堆区,则父类指针在释放时无法调用子类的析构函数,从而造成内存泄漏。

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 1.可以解决父类指针释放子类对象;
  • 2.都需要有具体的函数实现;

虚析构和纯虚析构区别:

  • 1.如果是纯虚析构,该类属于抽象类,无法实例化对象;

语法:

  • 1.虚析构:virtual ~类名(){}
  • 2.纯虚析构:virtual ~类名() = 0;
namespace test_pureVirtualDestructor
{
	class Animal
	{
	public:
		Animal()
		{
			cout << "Animal构造函数调用" << endl;
		}
		virtual void speak() = 0;
		//下面的写法是虚析构函数
		/*~Animal()
		{
			cout << "Animal析构函数调用" << endl;
		}*/
		//下面的写法是纯虚析构函数
		//这种写法需要类内声明类外实现
		virtual ~Animal() = 0;
	};
	Animal::~Animal()
	{
		cout << "Animal析构函数调用" << endl;
	}
	class Cat :public Animal
	{
	public:
		Cat(string name)
		{
			cout << "Cat构造函数调用" << endl;
			m_Name = new string(name);
		}
		void speak() {
			cout <<*m_Name<< "在喵喵喵" << endl;
		}
		~Cat()
		{
			cout << "Cat析构函数调用" << endl;
			if (m_Name != NULL)
			{
				delete m_Name;
				m_Name = NULL;
			}
		}
		//假设现在使用一个指针来管理猫的名字
		string * m_Name;
	};
	void test01() {
		//父类执政在析构时候不会调用子类中的析构函数,
		// 导致子类如果子类有堆区的数据,将无法释放。
		Animal* anm = new Cat("Tom");
		anm->speak();
		delete anm;
	}
}

上面的代码如果父类的析构函数不加virtual,那么结果如下:

Animal构造函数调用
Cat构造函数调用
Tom在喵喵喵
Animal析构函数调用

子类对象没有被释放掉,是因为父类执政在析构时候不会调用子类中的析构函数,导致子类如果子类有堆区的数据,将无法释放。

利用虚析构函数(或者纯虚析构函数)解决父类指针释放子类对象,如果父类的析构函数加了virtual,那么结果如下:

Animal构造函数调用
Cat构造函数调用
Tom在喵喵喵
Cat析构函数调用
Animal析构函数调用

多态案例-电脑组装

需求:

alt text alt text

namespace test_computer
{
	//抽象不同零件类
	class CPU
	{
	public:
		virtual void calculate() = 0;
	};
	class VideoCard
	{
	public:
		virtual void display() = 0;
	};
	class Memory
	{
	public:
		virtual void storage() = 0;
	};
	//组装电脑的类
	class Computer
	{
	public:
		Computer(CPU* cpu, VideoCard* vc, Memory* mem)//父类的指针,在此处已经涉及多态了
		{
			m_cpu = cpu;
			m_vc = vc;
			m_mem = mem;
		}
		//工作的函数
		//调用接口
		void work()
		{
			m_cpu->calculate();
			m_vc->display();
			m_mem->storage();
		}
		~Computer()
		{
			if (m_cpu != NULL)
			{
				delete m_cpu;
				m_cpu = NULL;
			}
			if (m_vc != NULL)
			{
				delete m_vc;
				m_vc = NULL;
			}
			if (m_mem != NULL)
			{
				delete m_mem;
				m_mem = NULL;
			}
		}
	private:
		CPU* m_cpu;
		VideoCard* m_vc;
		Memory* m_mem;
	};
	class IntelCPU :public CPU
	{
	public:
		void calculate()
		{
			cout << "IntelCPU开始计算" << endl;
		}

	};
	class IntelVideoCard :public VideoCard
	{
	public:
		void display()
		{
			cout << "IntelVideoCard开始显示" << endl;
		}
	};
	class LenoMemory :public Memory
	{
	public:
		void storage()
		{
			cout << "LenoMemory开始存储" << endl;
		}
	};
	void test01() {
		//创建具体品牌的对象
		CPU* intelCpu = new IntelCPU;
		VideoCard* intelVc = new IntelVideoCard;
		Memory* lenoMem = new LenoMemory;
		//创建电脑对象
		Computer* computer = new Computer(intelCpu, intelVc, lenoMem);
		//电脑开始工作
		computer->work();
		//释放资源
		delete computer;
	}
}

文件操作

程序运行时产生的数据都属于临时数据,程序一旦结束都会被释放。 通过文件可以将数据持久化。

文件类型:

  • 文本文件:文件以文本的ASCLL码形式存储在计算机中。
  • 二进制文件: 文本以文本的二进制形式存储在计算机中 。

操作文件的三大类:

  • ofstream:写操作
  • ifstream:读操作
  • fstream:读写操作
文本文件

写文件步骤:

  • 包含头文件:#include
  • 创建流对象:ofstream ofs;
  • 打开文件:ofs.open(“文件路径”,打开方式);
  • 写数据:ofs « “写入的数据”;
  • 关闭文件:ofs.close();

文件打开方式:

  • ios::in 读
  • ios::out 写
  • ios::ate 从文件末尾开始写数据
  • ios::app 追加方式写文件
  • ios::trunc 如果文件存在先删除,再创建
  • ios::binary 二进制方式

示例代码:

//写文件
namespace test_write
{
	//写文本
	void test01()
	{
		//1.包含头文件

		//2.创建流对象
		ofstream ofs;

		//3.制定一个打开的方式
		ofs.open("text.txt", ios::out);
		//4.写内容
		ofs << "姓名:张三" << endl;
		ofs << "性别:男" << endl;
		ofs << "年龄:18" << endl;

		//5.关闭文件
		ofs.close();
	}
	//写二进制文件
	class Person
	{
	public:
		char m_Name[64];
		int m_Age;
	};
	void test02() {
		//1.包含头文件
		//2.创建流对象
		ofstream ofs("person.txt", ios::out | ios::binary);
		//3.打开文件
		//ofs.open("person.txt", ios::out | ios::binary);
		//4.写文件
		Person p = { "zhangsan",18 };
		ofs.write((const char*)&p, sizeof(Person));

		//5.关闭文件
		ofs.close();
	}
}
//读文件
namespace test_read
{
	//读文本
	void test01()
	{
		//1.包含头文件

		//2.创建流对象
		ifstream ifs;

		//3.制定一个打开的方式,判断打开是否成功
		ifs.open("text.txt", ios::in);
		if ( !ifs.is_open() )
		{
			cout << "文件打开失败" << endl;
			return;
		}
		//4.读内容
		//(1)
			/*char buf[1024] = { 0 };
			while (ifs >> buf)
			{
				cout << buf << endl;
			}*/
		//(2)
			/*char buf[1024] = { 0 };
			while (ifs.getline(buf, 1024))
			{
				cout << buf << endl;
			}*/
		//(3)
			string buf;
			while (getline(ifs, buf)) {
				cout << buf << endl;
			}
		//(4)不推荐这种方法
			/*char c;
			while ((c = ifs.get()) != EOF)
			{
				cout << c ;
			}*/

		//5.关闭文件
		ifs.close();
	}

	//读二进制文件
	class Person
	{
	public:
		char m_Name[64];
		int m_Age;
	};
	void test02() {
		//1.包含头文件

		//2.创建流对象
		ifstream ifs;

		//3.制定一个打开的方式,判断打开是否成功
		ifs.open("person.txt", ios::in|ios::binary);
		if (!ifs.is_open())
		{
			cout << "文件打开失败" << endl;
			return;
		}
		//4.读内容
		Person p;

		ifs.read((char*)&p, sizeof(Person));
		cout << "姓名:" << p.m_Name << " 年龄:" << p.m_Age << endl;

		//5.关闭文件
		ifs.close();
	}
}

————–C++的内容就先告一段落,对于以上只是将用一个项目总结前面学过的知识——

——————————————————————————————————————————————————————————————————————————————————————————————-

将数据结构和操作该数据结构的函数捆绑在一起形成一个类。

在类中使用缺省函数

需要注意避免二义性: alt text alt text

构造函数

类中为什么需要构造函数

alt text alt text

构造函数在数组中的使用

class A{
    int a;
    public:
        A(){std::cout<<"hello\n";}//1
        A(int b){std::cout<<"value: "<<b<<std::endl;}//2
}
int main(){
    A arr[2] = {1};
}

上面的语句就调用了第1,2个构造函数,首先数据里的1会调用2,而数组的第二个是空值那么就会调用第1个。

复制构造函数-copy constructor

函数名与类名相同,参数为A(A& a)或A(const A& a)(二选一)。

三种情况下复制构造函数起作用

  • 同类对象a,b,将a的值初始化b;
    • A a; A b(a);
  • 将类当作参数传入函数;
  • 将类作为返回值;

注意:对象之间的复制是不会导致复制构造函数的

转换构造函数

什么是转换构造函数 alt text alt text

示例代码: alt text alt text 在上面的例子当中,c1 = 9;会被自动转换为调用转换构造函数,如果不想让这样的事情发生,可以在转换构造函数前面加上修饰字段 explicit,这样再次使用c1 = 9;时程序会报错。

析构函数-destructors

这里补充几点,一个类只能有一个析构函数;

析构函数与数组:

数组成员的每一次结束时都会调用析构函数,假设类A数组array[2],则生命周期结束时会调用两次析构函数。

析构函数什么时候被调用呢?

    class CMyclass {
    public:
    ~CMyclass() { cout << "destructor" << endl; }
    };
    CMyclass obj;
    CMyclass fun(CMyclass sobj ) { //参数对象消亡也会导致析
    //构函数被调用
    return sobj; //函数调用返回时生成临时对象返回
    }
    int main(){
    obj = fun(obj); //函数调用的返回值(临时对象)被
    return 0; //用过后,该临时对象析构函数被调用
    }

上面结果输出三个destructor,在fun(CMyclass sobj)参数对象消亡调用析构函数(具体可以联想复制构造函数),会调用一次析构函数;当函数返回赋值给obj后,再次调用析构函数;整个程序结束再次调用析构函数。 alt text alt text

this指针

alt text alt text

静态成员函数中可以直接使用this指针来代表指向该函数作用的对象的指针

示例1:

class Complex {
public:
double real, imag;
void Print() { cout << real << "," << imag ; }
Complex(double r,double i):real(r),imag(i)
{ }
Complex AddOne() {
this->real ++; //等价于 real ++;
this->Print(); //等价于 Print
return * this;
}
}; 

int main() {
Complex c1(1,1),c2(0,0);
c2 = c1.AddOne();//调用AddOne函数后内部this指针变为c1,所以是c1中的read++
return 0;
} //输出 2,1

示例2:

    class A
{
int i;
public:
void Hello() { cout << i << "hello" << endl; }
}; // 翻译为void Hello(A * this ) { cout << this->i << "hello"<< endl; }
int main()
{
A * p = NULL;
p->Hello(); //翻译为Hello(p);
} //编译报错

报错的原因是p指针式一个空指针,它并不指向任何A的对象,所以编译到 this->i 会报错。

静态成员变量和函数

基本特点:

  • 普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。
  • 普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。因此静态成员不需要通过对象就能访问。

对第二句话的理解,如下代码示例:

class Dog {
public:
    // 普通成员函数
    void bark() {
        std::cout << "Woof!" << std::endl;
    }

    // 静态成员函数
    static std::string getSpecies() {
        return "Canine";
    }
};

要调用bark我们需要先创建一个Dog的类对象,然后通过 . 调用,也就是非静态成员作用于对象的意思,但是对于 getSpecies 就不需要,可以直接 Dog::getSpecies调用。

如何访问静态成员?

  1. 类名::成员名 CRectangle::PrintTotal();

  2. 对象名.成员名 CRectangle r; r.PrintTotal();

  3. 指针->成员名 CRectangle * p = &r; p->PrintTotal();

  4. 引用.成员名 CRectangle & ref = r; int n = ref.nTotalNumber;

设置静态成员变量的目的是什么?

设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解。比如,考虑一个需要随时知道矩形总数和总面积的图形处理程序,可以用全局变量来记录总数和总面积,用静态成员将这两个变量封装进类中,就更容易理解和维护。

注意事项:

  • 必须在定义类的文件中对静态成员变量进行一次说明或初始化。否则编译能通过,链接不能通过。
  • 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。

成员对象和封闭类

  • 有成员对象的类叫 封闭(enclosing)类。
  • 任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。具体的做法就是:通过封闭类的构造函数的初始化列表。

封闭类的复制构造函数:

    class A
    {
    public:
    A() { cout << "default" << endl; }
    A(A & a) { cout << "copy" << endl;}
    };

    class B { A a; };

    int main()
    {
    B b1,b2(b1);//b2调用自生默认复制构造函数,而内部的A a也会调用默认的复制构造函数
    return 0;
    }

友元

  1. 友元函数: 一个类的友元函数可以访问该类的私有成员。
  2. 可以将一个类的成员函数(包括构造、析构函数)说明为另一个类的友元。
  3. 友元类: 如果A是B的友元类,那么A的成员函数可以访问B的私有成员。
  4. 友元类之间的关系不能传递,不能继承。

常量成员

常量成员函数:内部不能改变属性的值,也不能调用非常量成员函数。

**注意:**如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,那么,最好将其写成常量成员函数。 mutable成员变量 可以在const成员函数中修改的成员变量

    class CTest
    {
    public:
    bool GetData() const
    {
    m_n1++;//这个值更可以更改
    return m_b2;
    }
    private:
    mutable int m_n1;
    bool m_b2;
    };

运算符重载函数

浅拷贝与深拷贝: alt text alt text

为解决这个问题,我们需要修改赋值运算符重载:

    String & operator = (const String & s){
        if( this == & s)
            return * this;
        delete [] str;
        str = new char[strlen(s.str)+1];
        strcpy( str,s.str);
        return * this;
    }

自增自减运算法重载

class CDemo {
private :
int n;
public:
CDemo(int i=0):n(i) { }
CDemo & operator++(); //用于前置形式
CDemo operator++( int ); //用于后置形式
operator int ( ) { return n; }
friend CDemo & operator--(CDemo & );
friend CDemo operator--(CDemo & ,int);
}

operator int ( ) {return n;}

这里,int 作为一个类型强制转换运算符被重载, 此后

Demo s;
(int) s ; //等效于 s.int();

类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型。

继承和派生

**注意:**在派生类的各个成员函数中,不能访问基类中的private成员。

派生类的内存空间

alt text alt text

类之间的关系

继承:“是”关系。

  • 基类 A,B是基类A的派生类。
  • 逻辑上要求:“一个B对象也是一个A对象”。

复合:“有”关系。

  • 类C中“有”成员变量k,k是类D的对象,则C和D是复合 关系
  • 一般逻辑上要求:“D对象是C对象的固有属性或组成部 分”

复合关系的示例: alt text alt text

基类与派生类名字重名的情况

一般来说,基类和派生类不定义同名成员变量。但如果要方位可以使用基类::基类成员来访问。

访问权限

• 基类的private成员:可以被下列函数访问

  • 基类的成员函数
  • 基类的友元函数

• 基类的public成员:可以被下列函数访问

  • 基类的成员函数
  • 基类的友元函数
  • 派生类的成员函数
  • 派生类的友元函数
  • 其他的函数

• 基类的protected成员:可以被下列函数访问

  • 基类的成员函数
  • 基类的友元函数
  • 派生类的成员函数可以访问当前对象和其它对象的基类的保护成

举一个例子:

class Father {
private: int nPrivate; //私有成员
public: int nPublic; //公有成员
protected: int nProtected; // 保护成员
};
class Son :public Father{
void AccessFather () {
nPublic = 1; // ok;
nPrivate = 1; // wrong
nProtected = 1; // OK,访问从基类继承的protected成员
Son f;
f.nProtected = 1; //ok,派生类的成员函数可以访问当前对象和其它对象的基类的保护成
}
};

int main(){
    Father f;
    Son s;
    f.nPublic = 1; // Ok
    s.nPublic = 1; // Ok
    f.nProtected = 1; // error,因为只能在派生类(或友元类)中访问
    f.nPrivate = 1; // error
    s.nProtected = 1; //error
    s.nPrivate = 1; // error
    return 0;
}

派生类构造函数中包含成员变量时该如何写?

    class Bug {
    private :
    int nLegs; int nColor;
    public:
    int nType;
    Bug ( int legs, int color);
    void PrintBug (){ };
    };

    class Skill {
    public:
    Skill(int n) { }
    };

    class FlyBug: public Bug {
    int nWings;
    Skill sk1, sk2;
    public:
    FlyBug( int legs, int color, int wings);
    };

    FlyBug::FlyBug( int legs, int color, int wings):
    Bug(legs,color),sk1(5),sk2(color) ,nWings(wings) { }//注意这种写法。

private 和 protected的继承

• protected继承时,基类的public成员和protected成员成为派生类的protected成员。 • private继承时,基类的public成员成为派生类的private成员,基类的protected成员成 为派生类的不可访问成员。 • protected和private继承不是“是”的关系。

问题:

即便基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类没有,而派生类中有的成员。

解决办法:

通过强制指针类型转换,可以把ptrBase转换成Derived类的指针

    Base * ptrBase = &objDerived;
    Derived *ptrDerived = (Derived * ) ptrBase;

程序员要保证ptrBase指向的是一个Derived类的对象,否则很容易会出错。

虚函数和多态

多态的表现形式

  • 派生类的指针(或引用)可以给基类赋值。
  • 当指针指向基类时调用,基类的函数,当指针指向派生类时,调用调用派生类的函数,这叫做多态。
  • 另外,在非虚函数和非构造、析构函数中调用虚函数,也叫做多态。 例子: alt text alt text

如果在构造、析构函数中调用虚函数,那么会怎么样呢?

如果这么做,那么编译器只会调用当前的虚函数,如果当前没有重写虚函数,则会从基类中找进行调用,为什么要这么做呢?其实这是为了防止未初始化就调用派生类的虚函数的情况。

你想想啊,一类的初始化先从基类的构造函数开始,如果这个时候构造函数具有多态性,那么此时将调用派生类的对应虚函数,但是派生类这个时候都还没有初始化。

例子: alt text alt text`

  • 补充:在上面的图例子中,派生类写了与基类虚函数相同的名字,但是没有在前面加修饰符virtual,这种情况编译器认为是虚函数。

多态的作用

在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

虚函数的访问权限

alt text alt text

动态联编的实现机制

“多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 —- 这叫“动态联编”。

每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的4个字节就是用来放虚函数表的地址的。 alt text alt text

下面一段代码将验证,对象的首地址是存了虚函数表的地址的:

    #include <iostream>
    using namespace std;
    class A {
    public: virtual void Func() { cout << "A::Func" << endl; }
    };
    class B:public A {
    public: virtual void Func() { cout << "B::Func" << endl; }
    };
    int main() {
    A a;
    A * pa = new B();
    pa->Func();
    //64位程序指针为8字节
    long long * p1 = (long long * ) & a;
    long long * p2 = (long long * ) pa;
    * p2 = * p1;
    pa->Func();
    return 0;
    }

纯虚函数和抽象类

  • 纯虚函数:没有函数体的虚函数
  • 抽象类:有纯虚函数的类

注意:

  • 包含纯虚函数的类叫抽象类
    • 抽象类只能作为基类来派生新类使用,不能创建独立的抽象类的对象
    • 抽象类的指针和引用可以指向由抽象类派生出来的类的对象
    A a ; // 错,A 是抽象类,不能创建对象
    A * pa ; // ok,可以定义抽象类的指针和引用
    pa = new A ; //错误, A 是抽象类,不能创建对象
  • 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。
  • 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。
附件

13.从C到C++

指针

指针的声明: alt text alt text

数组

数组的范围必须是常量表达式,同时如果需要变化的范围可以做如下定义: alt text alt text

2.字符串文字量

指的是用 ““括起来的字符串。

  • 长字符串编写: alt text alt text
  • 带有L的字符是宽字符,如L"sddsf”,类型是const wchar_t.

3.指向数组的指针

4.常量 alt text alt text

5.指针和常量

使用一个指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用cons“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,我们必须使用声明运算符*const,而不能只用简单的const。

定义常量指针的声明运算符是const。并没有cons** 声明符,所以出现在之前的conr是作为基础类型的一部分。例如

// 到char的hconst指针
char *const cp;
char const* pc;
// 到const char的指针
const char* pc2;
//到const char的指针

有人发现从右向左读这种定义很有帮助。例如,“cp是一个conse指针到char”,以及“pe2是一个指针措到const chor”

引用

void f(){
    int ii=1;
    int &rr = ii;
    rr++;//实际上是ii++,rr不能像指针一样操作。
    int *pp = &ii;
}

alt text alt text

附件

12.其他库的介绍

为什么需要哈希函数?

哈希函数在计算机科学中扮演着重要的角色,其主要作用包括:

  1. 数据检索:哈希函数可以将键(key)映射到哈希表中的一个位置,从而快速检索数据。这是哈希表(如 C++ 中的 std::unordered_mapstd::unordered_set)的基础。

  2. 快速比较:在数据库和各种数据结构中,哈希函数可以快速比较字符串或对象,常用于检查两个字符串是否相等。

  3. 数据分布:在分布式系统中,哈希函数可以将数据均匀地分布到多个节点上,以实现负载均衡。

  4. 密码学应用:在密码学中,哈希函数用于生成数据的摘要,用于数据完整性验证和数字签名。

  5. 缓存实现:哈希函数可以用于实现缓存机制,通过快速定位缓存中的数据。

  6. 负载均衡:在网络流量管理中,哈希函数可以用于将请求分配到不同的服务器上。

C++ 库中哈希函数的实现和底层逻辑

在 C++11 及以后的版本中,标准库提供了一个专门的哈希函数库 <functional>,其中包含了 std::hash 模板类。std::hash 为基本数据类型(如 intdoublestd::string 等)提供了特化版本。

实现底层逻辑:

  1. 整数类型:对于整数类型,std::hash 直接返回数值本身或其变体。例如,对于 int 类型,std::hash<int>()(10) 可能直接返回 10 或者 10 的一个简单变换。

  2. 浮点类型:对于浮点类型,std::hash 通常会将浮点数的位表示转换为整数,然后应用整数的哈希函数。

  3. 字符串类型:对于字符串,std::hash 通常会遍历字符串中的每个字符,将每个字符的哈希值组合起来,生成最终的哈希值。这可以通过位操作和数学函数(如乘法、加法、异或等)来实现。

  4. 自定义类型:对于自定义类型,如果需要使用 std::hash,则需要为该类型提供自定义的哈希函数。

  5. 组合哈希:对于复合类型(如结构体或类),哈希函数需要将各个成员的哈希值组合起来。这通常通过将成员的哈希值进行某种形式的数学运算(如加权求和、异或等)来实现。

  6. 均匀分布:一个好的哈希函数应该能够产生均匀分布的哈希值,以减少哈希冲突。

  7. 确定性:哈希函数应该是确定性的,即对于同一个输入总是产生相同的哈希值。

  8. 快速计算:哈希函数的计算应该尽可能快,以减少对性能的影响。

在 C++ 中,std::hash 的实现可能因编译器和平台而异,但基本原理是相似的。正确实现哈希函数对于哈希表的性能至关重要,因为它直接影响到哈希表的冲突率和检索效率。

hash function-哈希函数

如果要自己给自己的数据写一个哈希函数,那又该怎么写呢?能否基于这些数据计算出hash code的呢?

编写模版规范

    #include <functional>
    namespace test_hash_function
    {
        class Customer {
            private:
            string fname;
            string lname;
            int on;

            public:
                Customer( string fn,  string ln, int id) : fname(fn), lname(ln), on(id) {}
                bool operator==(const Customer& other) const {//需要重载==
                    return fname == other.fname && lname == other.lname && on == other.on;
                }
                friend class CustomerHash;
        };
        class CustomerHash {
        public:
            std::size_t operator()(const Customer& c) const {
                return ...;
            }
        };
        unordered_set<Customer, CustromerHash> custsct;
    }
   #include <functional>
    namespace test_hash_function
    {
        template <typename T>
        inline void hash_combine(size_t& seed, const T& val) {
            seed ^= hash<T>()(val) +
                0x9e3779b9 +
                (seed << 6) +
                (seed >> 2);
        }
        template <typename T>
        inline void hash_val(size_t& seed, const T& val) {
            hash_combine(seed, val);
        }
        template <typename T, typename... Types>
        inline void hash_val(	size_t& seed,
                                    const T& val,
                                    const Types&... args) 
        {
            hash_combine(seed, val);
            hash_val(seed, args...);
        }
        template <typename... Types>
        inline size_t hash_val(const Types&... args) {
            size_t seed = 0;
            hash_val(seed, args...);
            return seed;
        }
        class Customer {
        private:
            string fname;
            string lname;
            int on;

        public:
            Customer( string fn,  string ln, int id) : fname(fn), lname(ln), on(id) {}
            bool operator==(const Customer& other) const {
                return fname == other.fname && lname == other.lname && on == other.on;
            }
            friend class CustomerHash;
        };

        class CustomerHash {
        public:
            size_t operator()(const Customer& c) const {
                return hash_val(c.fname, c.lname, c.on);
            }
        };

        void test() {
            unordered_set<Customer, CustomerHash> s;
            s.insert(Customer("Asd", "dfw", 1L));
            s.insert(Customer("Dfg", "kjt", 2L));
            s.insert(Customer("VVB", "ert", 3L));
            s.insert(Customer("TgR", "uik", 4L));
            s.insert(Customer("Gdf", "pii", 5L));
            s.insert(Customer("Gdf", "pii", 6L));
            s.insert(Customer("Gdf", "pii", 7L));//8
            cout << s.bucket_count() << endl;

            CustomerHash hh;
            cout << hh(Customer("Asd", "dfw", 1L)) % 8 << endl;//4
            cout << hh(Customer("Dfg", "kjt", 2L)) % 8 << endl;//0
            cout << hh(Customer("VVB", "ert", 3L)) % 8 << endl;//3
            cout << hh(Customer("TgR", "uik", 4L)) % 8 << endl;//7
            cout << hh(Customer("Gdf", "pii", 5L)) % 8 << endl;//2
            cout << hh(Customer("Gdf", "pii", 6L)) % 8 << endl;//7
            cout << hh(Customer("Gdf", "pii", 7L)) % 8 << endl;//4

            for (unsigned i = 0; i < s.bucket_count(); i++) {
                cout << "bucked #" << i << "has" << s.bucket_size(i) << "element\n";
            }
        }
    }
    // bucked #0has1element
    // bucked #1has0element
    // bucked #2has1element
    // bucked #3has1element
    // bucked #4has2element
    // bucked #5has0element
    // bucked #6has0element
    // bucked #7has2element
    #include <functional>
    namespace test_hash_function
    {
        class Customer {
            private:
            string fname;
            string lname;
            int on;

            public:
                Customer( string fn,  string ln, int id) : fname(fn), lname(ln), on(id) {}
                bool operator==(const Customer& other) const {//需要重载==
                    return fname == other.fname && lname == other.lname && on == other.on;
                }
                friend class CustomerHash;
        };
        class CustomerHash {
        public:
            size_t customer_hash_func(const Customer& c) const {
                return ...;
            }
        };
        unordered_set<Customer, size_t(*)(const Custromer&)> custsct(20,customer_hash_func);//注意与类型1的不同写法
    }

第三种方式是对类里面的hash进行偏特化,比如你使用了unordered_set那么对于它的Hash,可以单独进行偏特化。 alt text alt text

tuple

在C++中,tuple 是一个标准库模板类,它提供了一种方式来存储不同类型数据的集合。tuple 类似于一个固定大小的数组,但是数组中的每个元素可以是不同的类型。这个特性使得 tuple 非常灵活,可以用于多种场景。

alt text alt text

附件

2024年10月29日

11.STL库学习之适配器

整体框架的回顾

alt text alt text

存在多种适配器-adapters

一个人理解,要将适配器理解透彻,我们需要先从别的组件入手,这里以vector容器为例,我们都知道该容器需要迭代器,也就是一些智能指针来确定容器的头尾,以及内容位置,目的是方便后续的算法的怎删改查等操作,假设算法现在要做拿到位置8的数据,那么vector的指针要怎么移动呢?已知指针移动上,vector是随机访问指针,也就是前后都可以跑,而这个操作实际上是迭代器的其中一个内容,而这一内容就需要一个适配器去做适配。

下面做一个模拟问答,来加以了解:

—c 模拟问答 算法:你好,vector的迭代器,我需要知道你的迭代器类别,这样我才能正确地进行操作。

vector的迭代器:好的,我先问一下我的适配器。

vector的迭代器:适配器,算法想知道 iterator_traits<InputIterator>::iterator_category 是什么类型的迭代器?

vector_iterator的适配器:你好,我们这里是随机访问迭代器(Random Access Iterator)。

算法:太好了,随机访问迭代器可以让我进行更高效的操作。那么,如果我想访问第8个元素,我该怎么做?

vector_iterator的适配器:很简单,你可以直接通过加上偏移量来访问第8个元素。比如,如果你有一个指向第一个元素的迭代器 first,那么 first + 7(因为迭代器是从0开始计数的)就会给你第8个元素的迭代器。

算法:明白了,那我可以直接使用 *(first + 7) 来获取第8个元素的值了。

vector_iterator的适配器:是的,完全正确。

算法:如果我需要反向迭代器,或者插入迭代器,你们能提供吗?

vector_iterator的适配器:当然可以。我们 vector 提供了多种迭代器适配器,包括反向迭代器(reverse_iterator)、插入迭代器(insert_iterator)等,以满足不同的需求。

算法:那太好了,这样我可以更灵活地处理 vector 中的数据了。谢谢你的帮助!

STL中适配器的重要特性

对于容器、迭代器和仿函数它们第二次迭代器都有一个重要的特性,就是适配器会包含(也有继承)对应的类,比如stack包含了deque,然后使用了deque的某些功能,屏蔽了deque的某些某能,实现了先进后出的功能。

仿函数适配器

binder2nd适配器

该适配器是用于绑定的,比如对于一个容器,需要操作比88小于数,这使用可以用binder2nd,第一个参数可以传入比较大小的less仿函数,第二个传入比较的数值x,本质上在binder2nd中,less的第二个参数传入就是x,这样就达到比较的目的了。 alt text alt text 小结:把A类和数值a传入另一个类B中,在B中再操作A和a.这样就实现了绑定。

not1

对结果取反 alt text alt text

一些新的适配器

alt text alt text

bind

alt text alt text

迭代器适配器

reverse——iterator

三个细节:

  • 1.begin=rend,end=rbigin
  • 2.取值是取后一个,所以内部实现的*是做–操作;
  • 3.operate ++()操作重载后内部实现为–,operate –()重载后内部实现++操作。

具体图下图所示 alt text alt text

insert_iterator

在下面的案例中copy(bar.begin(),bar.end(),inserter(foo,it));欲将list的bar数据从it指向的位置 开始插入,但是foo的空间不够,为什么还能成功?

设计的小技巧,当传入copy中时,因为inserter对operate =()做了重载,重载中调用了insert()该函数能决绝以上问题, 因此不会因为foo的内存不足导致插入失败。 alt text alt text

ostream_iterator

下面通过一个案例说明该迭代器的一些特性和使用方法:

1.当执行初始化操作std::ostream_iterator out_it(std::cout,",")时调用构造函数,并初始化。 2.执行copy操作时,因为ostream_iterator对operate =()重载,所以会将值value传给*out_stream,通过这个方法把数据打印在屏幕上。 alt text alt text

istream_iterator

下面也是两个例子说明istream_iterator的特性:

1.std::istream_iterator<double> iit(std::cin);时同样会调用对应的构造函数,并且此处会调用operate ++()重载,在该重载中,*in_stream接收按键输入的一个值,也就是说才定义时,就已经在准备接收一个值,如果此时在该语句下写一段打印操作没见无法看到输出! alt text alt text

2.下面是copy操作,对于这个操作,我们可以观察是否有对*=的操作符进行重载,对于=的重载上述已经介绍, *的重载是将value返回,也就是 *first的操作取得的是容器中的值。 alt text alt text

附件
2024年10月29日

10.STL库学习之仿函数

仿函数的个人理解

仿函数是行为类似函数的一个类,比较明显的特征是仿函数重载了operate(),比如你写了一个struct,并在里面重载了operate(),当调用这个类的()时就会返回响应的数据。

你的理解是正确的,仿函数(functor)是 C++ 中的一个概念,它指的是那些可以像函数一样被调用的对象。这些对象通常重载了函数调用操作符 operator(),使得对象可以被当作函数来使用。以下是对你描述的一些补充说明:

仿函数的特点

  1. 重载 operator():仿函数类必须重载函数调用操作符 operator(),这样对象就可以被像函数那样调用。

  2. 可以携带状态:与普通函数不同,仿函数可以拥有成员变量,这意味着它们可以携带状态。这些状态可以是私有的,也可以是公开的,取决于仿函数的设计。

  3. 可以进行重载:仿函数可以重载 operator(),以接受不同数量和类型的参数。

  4. 可以定义在类中:仿函数可以是类的成员函数,也可以是独立的类。

  5. 可以有返回值:当仿函数被调用时,可以返回一个值,就像普通函数那样。

  6. 可以作为参数传递:由于仿函数可以像函数一样被调用,它们可以作为参数传递给接受函数作为参数的函数。

仿函数的应用

  1. 算法的参数:仿函数经常用作 STL 算法的参数,例如 std::sortstd::find_if,因为这些算法需要一个可以被调用的实体来比较元素或测试条件。

  2. 回调函数:在事件驱动的编程中,仿函数可以作为回调函数,响应特定的事件。

  3. 策略模式:在设计模式中,仿函数可以用于实现策略模式,允许算法的行为在运行时动态改变。

  4. 简化代码:通过使用仿函数,可以减少函数指针和全局函数的使用,使得代码更加简洁和易于管理。

示例代码

struct Max {
    // 重载()操作符,使其可以被调用
    int operator()(int a, int b) const {
        return (a > b) ? a : b;
    }
};

int main() {
    Max max_functor;
    int result = max_functor(10, 20); // 使用仿函数对象调用
    std::cout << "Max value: " << result << std::endl;
    return 0;
}

在这个例子中,Max 是一个仿函数,它重载了 operator() 来比较两个整数并返回最大值。然后,我们可以像调用函数一样调用 max_functor 对象。

仿函数内部一般设计什么?

仿函数一般重载operate()后,会在内部设计运算操作,如算数、逻辑、相对关系等运算。如下图所示。 alt text alt text 从上面一张图也可以发现,plus,minus,logical_and,equal_to,less类都集成了binary_function,该类的作用是什么呢?

std::binary_function 的主要作用是:

  1. 泛型框架:提供一个泛型框架,允许函数对象接受任意类型的参数。
  2. 类型转换:允许参数类型和返回类型的转换,使得函数对象可以用于不同的数据类型。
  3. 模板编程:支持模板编程,使得可以创建通用的算法和函数。
  • 原型

std::binary_function 的原型如下:

template <class Arg1Type, class Arg2Type, class ResultType>
struct binary_function {
    typedef Arg1Type first_argument_type;
    typedef Arg2Type second_argument_type;
    typedef ResultType result_type;
};
  • Arg1Type:第一个参数的类型。

  • Arg2Type:第二个参数的类型。

  • ResultType:函数返回值的类型。

  • 继承和使用

函数对象如 std::plus, std::minus, std::logical_and, std::equal_to, std::less 等都继承自 std::binary_function。这意味着这些函数对象都定义了 first_argument_type, second_argument_type, 和 result_type 这三个类型别名,它们分别表示函数对象接受的第一个参数类型、第二个参数类型和返回值类型。

关于 std::binary_function的具体细节在适配器一节会重点讲到。

下图是是否继承binary_function的一些示例,如果不继承会怎么样呢?以我个人的理解,继承会让仿函数的扩展性更高,它将来可以扩展出更好的功能。如下图所示。 alt text alt text

除了上面讲到的binary_function还有unary_function,该类是针对单个变量的操作,比如取反,加加。如下图: alt text alt text

关于binary_function的细节此处先不介绍,下一节将着重说明。

对于仿函数,鲜明的特点就是对operate()做了重载,这样的类创建出来的对象叫仿函数对象,具有函数的行为。如果要对重载做更多操作需要结合一些仿函数适配器,比如相面讲到的binary_function和unary_funtion。

附件
2024年10月29日

9.STL库学习之迭代器与算法

标准库常用算法

alt text alt text

迭代器

迭代器-的分类

struct input_iterator_tag {}
struct output_iterator_tag {};
struct forward_iterator_tag:public input_iterator_tag{};
struct bidirectional_iterator_tag:public forward_iterator_tag{};
struct random_access_tag:public bidirectional_iterator_tag {};

以上5中迭代器的继承关系,如下图所示。 alt text alt text

1.Input Iterator(输入迭代器): 输入迭代器是最基本的迭代器类型,支持单向遍历,只能向前移动。

用法示例:

     std::vector<int> vec = {1, 2, 3, 4, 5};
     std::input_iterator<int> it = vec.begin();
     while (it != vec.end()) {
         std::cout << *it << " ";
         ++it;
     }

2.Forward Iterator(前向迭代器): 前向迭代器支持双向遍历,可以向前和向后移动。

用法示例:

     std::list<int> lst = {1, 2, 3, 4, 5};
     std::forward_iterator<int> it = lst.begin();
     while (it != lst.end()) {
         std::cout << *it << " ";
         ++it;
     }

3.Output Iterator(输出迭代器): 输出迭代器允许对容器中的元素进行写操作,但不支持读操作。

用法示例:

     std::vector<int> vec;
     std::back_insert_iterator<std::vector<int>> it(vec);
     *it = 1; // 写操作
     ++it;
     *it = 2; // 写操作

4.Bidirectional Iterator(双向迭代器): 双向迭代器支持双向遍历,可以向前和向后移动。

用法示例:

     std::list<int> lst = {1, 2, 3, 4, 5};
     std::bidirectional_iterator<int> it = lst.end();
     while (it != lst.begin()) {
         --it;
         std::cout << *it << " ";
     }

5.Random Access Iterator(随机访问迭代器): 随机访问迭代器支持任意位置的快速访问,类似于指针操作。

用法示例:

     std::vector<int> vec = {1, 2, 3, 4, 5};
     std::random_access_iterator<int> it = vec.begin();
     it += 2; // 随机访问
     std::cout << *it << " ";

各种容器的迭代器类型

测试代码:

    #include <iterator> 
    namespace test_iterator_category
    {
        template <typename T>
        void display_category(T itr) {
            typename iterator_traits<T>::iterator_category cagy; // 使用 std::iterator_traits 获取迭代器类别
            // 这里不需要递归调用 display_iterator 函数
            cout << typeid(cagy).name() << endl;
        }

        void test() {
            cout << "\ntest_iterator_category....................\n";
            display_category(array<int, 10>::iterator());
            display_category(vector<int>::iterator());
            display_category(list<int>::iterator());
            display_category(forward_list<int>::iterator());
            display_category(deque<int>::iterator());

            display_category(set<int>::iterator());
            display_category(map<int, int>::iterator());
            display_category(multiset<int>::iterator());
            display_category(multiset<int, int>::iterator());

            display_category(istream_iterator<int>());
            display_category(ostream_iterator<int>(cout,""));
        }
    }

此处可以参考一下display_category(array<int, 10>::iterator());中传递array<int, 10>::iterator()的方法。

测试结果:

test_iterator_category....................
struct std::random_access_iterator_tag
struct std::random_access_iterator_tag
struct std::bidirectional_iterator_tag
struct std::forward_iterator_tag
struct std::random_access_iterator_tag
struct std::bidirectional_iterator_tag
struct std::bidirectional_iterator_tag
struct std::bidirectional_iterator_tag
struct std::bidirectional_iterator_tag
struct std::input_iterator_tag
struct std::output_iterator_tag

迭代器对算法的影响

例子1-distance

迭代器如何计算容器元素之间的距离呢?当调用迭代器时,迭代器是如何操作以协助算法完成后续的增删改查的?

在计算距离方面,我们可以看到调用接口需要传入容器的头,以及容器尾部,接着使用:

    ...
    typedef typename iterator_traits<InputIterator>::itrerator_category category;
    return __distance(first,last, category);

typedef typename iterator_traits<InputIterator>::itrerator_category category;这一句会判断迭代器的类型,进而执行加操作或一步一步加操作。详细如下图所示。 alt text alt text

例子2-copy

copy的动作也不仅仅是对迭代器类型的判断,还做了许多特化、偏特化。如下图所示。 alt text alt text

例子3-destroy

alt text alt text

例子4-__unique_copy

alt text alt text

对传入的迭代器类型的暗示

在调用一些需要传入迭代器的函数时,从函数的形参名可以判断传入的迭代器类型,如InputIterator,由于forward_iterator_tag、bidirection_iterator_tag、random_asccess_iterator_tag与input_iterator的继承关系可知,前三者都可以传入,以此类推如果形参是forward_iterator_tag那么除了其本身,bidirection_iterator_tag、random_asccess_iterator_tag也可传入。

alt text alt text

算法源码剖析

accumulate-累计

alt text alt text

测试用例:

    #include <functional>//std::minus
    #include <numeric>//std::accumulate
    namespace test_accumulate
    {
        int myfcn(int x, int y) { return x + 2 * y; }
        struct myclass {
            int operator()(int x, int y) { return x + 3 * y; }
        } myobj;
    }
    void test(){
        int init = 10;
        int nums[] = {10,10,10};
        cout << accumulate(nums, nums + 3, init)<<endl;//40
        cout << accumulate(nums, nums + 3, init, test_accumulate::myfcn)<<endl;//70
        cout << accumulate(nums, nums + 3, init, test_accumulate::myobj) << endl;//100
    }

for_each

拿到每一个元素时都对该元素执行某一操作。 alt text alt text

测试用例:

    namespace test_for_each
    {
        void myfcn(int i) { cout<<i<<"-"; }
        struct myclass {
            void operator()(int i) { cout << "[" << i<<"] "; }
        } myobj;
        void test() {
            vector<int> c;
            c.push_back(10);
            c.push_back(20);
            c.push_back(30);
            for_each(c.begin(), c.end(), test_for_each::myfcn);//10-20-30-
            cout << "\n";
            for_each(c.begin(), c.end(), test_for_each::myobj);//[10] [20] [30]
        }
    }

replace,replace_if,rreplace_copy

replace用于将新值替换为旧值,例如将数组中所有的8替换为9.详细源码如下图所示。 alt text alt text

replacereplace_ifreplace_copy 是 C++ 标准库中的算法,它们用于在容器或数组中替换元素。这些函数属于 <algorithm> 头文件。下面是每个函数的详细介绍和作用:

1. replace

replace 函数用于将容器中所有满足特定条件的元素替换为另一个值。这个函数直接在原容器上操作,不创建新的容器。

函数原型:

void replace(ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value);
  • first, last:定义了要替换元素的范围。
  • old_value:要被替换的值。
  • new_value:替换后的值。

示例:

vector<int> v = {1, 2, 3, 4, 3, 2};
replace(v.begin(), v.end(), 2, 5);
// v 变为 {1, 5, 3, 4, 3, 5}

2. replace_if

replace_if 函数用于将容器中满足特定条件的元素替换为另一个值。与 replace 不同,replace_if 需要一个谓词(条件函数),只有满足这个条件的元素才会被替换。这个函数也直接在原容器上操作。

函数原型:

void replace_if(ForwardIterator first, ForwardIterator last, Predicate pred, const T& new_value);
  • first, last:定义了要替换元素的范围。
  • pred:一个谓词函数,返回 true 表示替换,false 表示不替换。
  • new_value:替换后的值。

示例:

vector<int> v = {1, 2, 3, 4, 3, 2};
replace_if(v.begin(), v.end(), [](int i) { return i == 2; }, 5);
// v 变为 {1, 5, 3, 4, 3, 5}

3. replace_copy

replace_copy 函数用于将容器中所有满足特定条件的元素复制到另一个容器中,并替换为另一个值。这个函数不会改变原容器,而是创建一个新的容器,其中包含替换后的元素。

函数原型:

template <class InputIterator, class OutputIterator>
OutputIterator replace_copy(InputIterator first, InputIterator last, OutputIterator result, const T& old_value, const T& new_value);
  • first, last:定义了要替换元素的范围。
  • result:指向目标容器的迭代器,用于存储替换后的元素。
  • old_value:要被替换的值。
  • new_value:替换后的值。

示例:

vector<int> v = {1, 2, 3, 4, 3, 2};
vector<int> v2(v.size());
replace_copy(v.begin(), v.end(), v2.begin(), 2, 5);
// v 保持不变,v2 变为 {1, 5, 3, 4, 3, 5}

count,count_if

源代码如下: alt text alt text

小结: alt text alt text

find,find_if

alt text alt text

sort

alt text alt text

alt text alt text

算法原理:

二分查找的基本思想是将一个有序序列分为两半,通过比较中间元素和目标值来缩小搜索范围。如果中间元素正好是目标值,则查找成功;如果目标值小于中间元素,则在序列的左半部分继续查找;如果目标值大于中间元素,则在右半部分继续查找。这个过程不断重复,直到找到目标值或搜索范围为空。

函数原型:

binary_search 的函数原型如下:

template <class ForwardIterator, class T>
bool binary_search(ForwardIterator first,
                   ForwardIterator last,
                   const T& value);
  • first, last:定义了要搜索的范围,first 是序列的开始迭代器,last 是序列的结束迭代器(指向序列末尾的下一个位置)。
  • value:要查找的值。

返回值:

  • 如果在序列中找到 value,则返回 true
  • 如果没有找到,则返回 false

示例代码:

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int target = 5;

    bool found = std::binary_search(data.begin(), data.end(), target);

    if (found) {
        std::cout << "Element found in the array." << std::endl;
    } else {
        std::cout << "Element not found in the array." << std::endl;
    }

    return 0;
}

注意事项:

  1. 有序序列binary_search 要求序列必须是有序的,否则查找结果将不可预测。
  2. 效率:二分查找的时间复杂度为 O(log n),其中 n 是序列中的元素数量。这使得它比线性查找(O(n))更高效,尤其是在大数据集上。
  3. 稳定性:如果存在多个相同的目标值,binary_search 将返回第一个匹配的位置。如果需要找到所有匹配项,可能需要使用其他方法。
附件
2024年10月26日

8.STL库学习之容器

list

vector

deque

alt text alt text

deque的迭代器 alt text alt text

deque实现中间插入值的做法:如果欲插入值在最前端(最尾端)调用push_front()push_back()),否则借助insert_aux()迭代器,实现如果欲插入位置靠近前端使用,就让前端的数据往前挪,否则往后挪。

deque中+=的实现:1.先计算+=后是否超过缓冲区,如果没有则直接+=,否则需要判断需要跨几个缓冲区,然后再去确定前进几个缓冲区。 alt text alt text

Vector的内存扩充:

std::vector 在需要更多空间时,通常会将容量增加到当前大小的两倍(这是最常见的增长策略,但不是唯一的,具体实现可能有所不同)。

当发生扩充时,std::vector 会分配一个新的更大的内存块,然后将所有元素从旧内存块复制到新内存块,最后释放旧内存块。

Deque的内存扩充:

std::deque(双端队列)的内存管理机制与 std::vector 不同。std::deque 是一个使用多个固定大小的内存块来存储元素的容器,这些内存块被称为“chunks”或“maps”。

当 std::deque 需要更多空间时,它可能会在已有的内存块之后添加新的内存块,或者在前面添加,这取决于哪个方向上的空间更紧张。 std::deque 不会像 std::vector 那样将所有元素复制到一个新的连续内存块中,而是在多个内存块之间分配元素。

stack和queue

stack和queue默认使用deque的功能来分别实现先进后出和先进先出的功能,除此之外还可以使用list,stack还可以使用vector,但是deque就不能用vector,以我的理解,是因为vector不提供后端操作。

Rb-tree

在C++中,set和map的底层实现都是基于红黑树(RB-tree)。红黑树是一种自平衡的二叉搜索树,能够保证在最坏情况下,插入、删除和查找操作的时间复杂度均为O(log n)。

    template <  int key,
                int value,
                identity<int>,
                less<int>,
                alloc
                >
    class rb_tree;

set 和 map 的区别:

set:set中的元素是唯一的键值,即Key和Value是同一个。插入时使用insert_unique()方法,确保键值不重复。

map:map中的元素是键值对(Key-Value),键值用于索引,值表示与索引相关联的数据。插入时使用insert_equal()方法,允许键值重复。

红黑树的结构:

红黑树的结构包括节点、根节点、最左节点和最右节点等。根节点通过header.parent保存,最左节点通过header.left保存,最右节点通过header.right保存。

  • 插入操作
    • set:插入操作调用insert_unique(),确保键值唯一。
    • map:插入操作调用insert_equal(),允许键值重复。
  • 迭代器
    • set:迭代器是RB-tree的const_iterator,不允许修改元素值。
    • map:迭代器也是RB-tree的const_iterator,但允许修改元素的值(因为值部分不是键)。

set和multiset

set与map的底层实现基本可以参考下面的源码图,

alt text alt text

需要说明的一点是,在不允许更改键值上,二者的设计还是有区别,主要体现在set使用const_iterator,这样迭代器指向的键值就无法更改,而map在设计上在传进来的Key设置为const,确保键值不会被更改。

hashtable-哈希表

在C++中,hash_set、hash_map、hash_multiset和hash_multimap等数据结构的底层实现确实采用了哈希表的思想。具体来说,这些数据结构的设计核心是通过哈希函数将键(key)映射到数组的索引位置,从而实现高效的插入、删除和查找操作。

数据存储的设计思想

每个键值通过一个哈希函数计算出一个哈希值。这个哈希值通常是一个整数,用于确定键值在数组中的存储位置。例如,对于一个整数键值12,哈希函数可能会直接返回12,这样12就会被存储在数组的第12个位置。

哈希表的核心是一个数组(通常是一个vector),数组的每个元素(即每个位置)可以存储一个指针。这个指针指向一个单向链表的头节点。如果多个键值通过哈希函数计算出相同的哈希值,它们会被存储在同一个位置的链表中。参考下图: alt text alt text 当多个键值映射到同一个哈希值时,这些键值会被存储在一个链表中。链表的每个节点包含一个键值和指向下一个节点的指针。这种设计允许在发生哈希冲突时,通过遍历链表来查找或插入特定的键值。 alt text alt text 为了保持高效的性能,哈希表通常会有一个动态扩容机制。当哈希表中的元素数量超过一定阈值时,哈希表会自动重新分配更大的数组,并重新计算所有键值的哈希值,以减少链表的长度,从而提高查找效率。

在实际应用中,哈希函数可能会导致不同的键值计算出相同的哈希值,这种现象称为哈希冲突。常见的解决策略包括链地址法(即使用链表存储冲突的元素)和开放地址法(即在数组中寻找下一个空闲位置)。

具体实现过程

  • 当插入一个新键值时,首先通过哈希函数计算出其哈希值,然后根据哈希值找到对应的数组位置。如果该位置已经有元素(即发生冲突),则将新元素插入到该位置的链表中。
  • 查找操作同样通过哈希函数计算出键值的哈希值,然后根据哈希值找到对应的数组位置。如果该位置有元素,则遍历链表,直到找到匹配的键值或链表结束。
  • 删除操作首先通过哈希函数计算出键值的哈希值,然后根据哈希值找到对应的数组位置。如果该位置有元素,则遍历链表,找到并删除匹配的键值。

实现代码

hashtable类的实现提供了六个模板参数。

  template <class value,
            class key,
            class hashFcn,//计算编号
            class extractKey,//取出键值
            class EqualKey,//判断类型是否相等
            class Alloc=alloc
            >

alt text alt text

如果要自己使用这个哈希表,需要写hashFcn()的重载,对齐进行偏特化等操作,如下图: alt text alt text

如果传入的是字符串,那么字符串的编号计算可以参考下图计算方法: alt text alt text

附件

2024年10月26日

7.STL库学习之分配器

分配器源代码位置:xmemory.h

alt text alt text

除了array和vector外,其他容器的适配器必须是一个类,

附件

6.STL库学习之筑基概要

STL程序源代码位置

了解自身编译器STL程序源代码位置。

OOP 与 GP

面向对象编程(OOP):

面向对象编程是一种编程范式,它将数据和处理这些数据的方法封装在对象中。这种方法强调了数据和函数的捆绑,使得代码更加模块化,易于理解和维护。对象可以包含属性(数据)和方法(函数),它们一起工作来模拟现实世界的行为。

补充说明:OOP 通过类(Class)来定义对象的蓝图,支持继承、封装和多态等特性,从而提高代码的重用性和灵活性。例如list中有自己的sort()函数,专门为list而设计,比::sort()的算法更优。

泛型编程(GP):

泛型编程是一种编程范式,它允许开发者定义操作类型数据结构的函数和类,而不需要在编译时指定具体的数据类型。这种方法强调了数据和处理方法的分离,提供了更高的代码复用性和类型安全性。

补充说明:GP 通过模板(Templates)或泛型容器(如 std::vector、std::map)来实现,允许开发者写出与数据类型无关的代码,从而在不同的数据类型间共享相同的算法逻辑。例如vector和deque没有为自身设计sort(),在做排序操作时,需要调用::sort()来实现。

操作符重载

STL源码程序中会包含很多的操作符重载,一般你会看到operate修饰符,后面就紧跟着操作符。注意:::..*:?不能进行操作符重载。

在我看来操作符重载是一个重要的特性,它可以让你实现任何类的运算法则,这些法则有你自己规定,操作空间还是比较宽泛。

模版-template

最常见的写法如下:

    template <typename T>
    class FOO{
        ...
    }

类模板

最常见的写法如下:

    template <class T>
    class FOO{
        ...
    }

成员模版

泛化、特化和偏特化

以我个人的理解,泛化是为了应对大部分的情况,特化是为了应对特殊的情况,或使用单独的方法处理对某些情况而言更好,偏特化比偏特化更近一步,把处理的手段限定在一定范围内,举个简单的例子,假设为设计计算两种情况的相加做如下定义:

1.泛化

泛化编程在C++中通过模板实现。以下是一个泛化编程的示例,其中 AND 类是一个模板类,可以处理任何类型的数据。

    template <class T,class U>
    class AND{
        ...
    }

2.特化

特化是对模板类或函数的特定实例化。以下是特化的示例,其中 AND 类被特化为处理 string 和 Text 类型:

    template <class T, class U>
    class AND {
        // ... 类的泛型实现
    };

    template <>
    class AND<string, Text> {
        // ... 类的特化实现
    };

3.偏特化

偏特化允许对模板类的部分参数进行特化。以下是偏特化的示例,其中 AND 类被偏特化为处理第一个类型为 int 的情况:

    template <class T, class U>
    class AND {
        // ... 类的泛型实现
    };

    template <class U>
    class AND<int, U> {
        // ... 类的偏特化实现
    };
附件

5.STL库之观其大略

一下主要讲STL组件的测试用例,特别是容器的测试

学习资料

  • CPLusPlus.com
  • CppReference.com
  • gcc.gnu.org
  • 《STL源码剖析》

STL六大组件

  • 容器-Containers,申请内存用于存储数据
  • 分配器-Allocators,配合容器分配内存
  • 算法- Algorithms,处理某一数据的最优办法
  • 迭代器- Iterators,指针的泛型,本质与指针类似
  • 适配器- Adapters,
  • 仿函数-Functors,类似函数。

六者的关系

alt text alt text

begin()和end()

以迭代器为例,begin()指向迭代器的首地址,而end()指向迭代器尾地址的下一位,可以用前闭后开区间来表示,即**[ )**

容器的分类

alt text alt text alt text alt text

数组容器的使用

array 的使用

示例1
        #ifndef __AUXFUN__
        #define __AUXFUN__
        #include <iostream>
        #include <string>
        #define RAMD_MAX 32767
        using namespace std;
        long get_a_target_long()
        {
            long target = 0;
            cout << "target (0--" << RAMD_MAX << "):";
            cin >> target;
            return target;
        }

        //将数值转为string,这样可以测试类似object的情况
        string get_a_target_string()
        {
            long target = 0;
            char buf[10];

            cout << "target (0--" << RAMD_MAX << "):";
            cin >> target;
            snprintf(buf, 10, "%d", target);

            return string(buf);
        }
        int compareLongs(const void* a, const void* b)
        {
            return (*(long*)a - *(long*)b);
            /*
            1.类型转换:因为参数是const void* 类型,你需要将它们转换为具体的数据类型指针(在这个例子中是long* ),以便可以解引用并获取它们的值。

            2.解引用:在转换之后,通过在类型转换的结果前使用* 操作符来获取指针指向的实际值。

            3.强制转换的结果:* (long*)a实际上是一个long值,它是通过解引用转换后的指针得到的。*/
        }
        int compareString(const void* a, const void* b)
        {
            if (*(long*)a > *(long*)b)
                return 1;
            else if (*(long*)a < *(long*)b)
                return -1;
            else
                return 0;
        }
        #endif // !__ENTRY__
    #include "TestHeardFiles/AuxFun.h"
    #include <ctime>
    #include <cstdlib>//qsort()、 bsearch()、 NULL
    #include <array>

    const size_t ASIZE = 50000;

    namespace t01
    {
        void test_array()
        {
            cout << "\ntest_array().............. \n";
            array<long, ASIZE> c;//使用数组容器

            clock_t timeStart = clock();
            for (long i = 0; i < ASIZE; ++i)
            {
                c[i] = rand();//可以产生随机数的函数
            }
            cout << "milli_seconds: " << (clock() - timeStart) << endl;
            cout << "array.size(): " << c.size() << endl;//返回数组大小
            cout << "array.back(): " << c.back() << endl;//返回数组最后一个数
            cout << "array.data: " << c.data() << endl;//返回第一个数的地址

            long target = get_a_target_long();
            timeStart = clock();//clock()返回毫秒数
            qsort(c.data(), ASIZE, sizeof(long), compareLongs);//数组排序
            long* pItem = (long*)bsearch(&target, (c.data()), ASIZE, sizeof(long), compareLongs);//数组查找
            cout << "qsort()+bsearch(),milli-seconds:" << clock() - timeStart << endl;
            if (pItem != NULL)
            {
                cout << "found," << *pItem << endl;
            }
            else
                cout << "Not found!" << endl;
        }
    }
    int main() {
        t01::test_array();
    }

alt text alt text

vector 的使用

vector是一种向后自动以2次方增加的内存的容器,一般使用push_back向后添加数据;

vector也有一定的缺点,假设我只需要5个内存空间,但是vector分配的是8个,剩下的3个后面如果不使用就会浪费。

示例2
    #include <vector>
    #include <stdexcept> //obort()
    #include <cstdio>	//snprintf()
    #include <algorithm>//sort()
    namespace t02
    {
        void test_vector(long& value) {
            cout << "\ntest_vector()..............\n";
            vector<string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push_back(string(buf));//将随机数转为string类型,并使用push_back()将数据存入容器尾部,这是vector的特性
                    //同时值得注意,当空间不足时,内存会自动增加,怎么加方式是2的平方
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "vector.size(): " << c.size() << endl;
            cout << "vector.front(): " << c.front() << endl;
            cout << "vector.back(): " << c.back() << endl;
            cout << "vector.data(): " << c.data() << endl;
            cout << "vector.capacity(): " << c.capacity() << endl;//返回的是向量当前分配的存储空间可以容纳的元素个数
            string target = get_a_target_string();//获取输入的数字,并转为string类型返回
            //使用算法find查找
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//返回类型是:使用全局::find()寻找目标,这是循环寻找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;

                //判断有没有找到
                if (pItem != c.end()) {
                    cout << "found," << *pItem << endl;
                }
                else
                {
                    cout << "Not fount!" << endl;
                }
            }
            //进行排序后使用bsearch查找。
            {
                timeStart = clock();
                sort(c.begin(), c.end());//该算法将任何类型进行排序,需要提供相应的比较函数
                string* pItem = (string*)bsearch(&target, (c.data()), c.size(), sizeof(string), compareString);
                cout << "sort()+bsearch(),milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != NULL) {
                    cout << "found," << *pItem << endl;
                }
                else
                {
                    cout << "Not fount!" << endl;
                }
            }
        }
    }

alt text alt text

从上面的结果来看,排序后使用二分查找反而比循环查找慢,原因在于排序话费了很多时间,因为string类型是一个object,其本身大小比较大,所以面对一个object进行查找时不建议使用排序后二分查找。

在实际应用中,选择排序加二分查找还是直接线性查找,需要根据具体情况来决定:

  • 如果容器已经排序,或者数据量不大,直接使用二分查找可能更简单、更快。
  • 如果容器未排序,且数据量很大,那么可能需要考虑是否值得为了一次查找而进行排序,或者是否可以通过其他方式(如使用更高效的数据结构,如哈希表)来提高查找效率。
  • 如果查找操作非常频繁,而插入和删除操作不频繁,那么维护一个已排序的容器可能是合理的,这样每次查找都可以利用二分查找的效率。

总之,选择哪种方法取决于具体的应用场景和性能要求。在某些情况下,可能需要通过实验或性能分析来确定最佳的策略。

链表容器-list

std::list 是 C++ 标准库中的一个容器,它提供了双向链表的实现。

std::list 容器的特点:

  • 双向链表结构:std::list 由一系列节点组成,每个节点包含一个元素和两个指针,分别指向前一个节点和后一个节点。
  • 动态内存分配:std::list 中的每个节点都是独立分配内存的。这意味着当你添加或删除元素时,std::list 会为新元素分配内存,或释放不再使用的元素所占用的内存。
  • 内存空间利用:由于 std::list 的元素是单独分配的,因此不存在像 std::vector 那样的连续内存块,也就不会有额外的内存浪费。每个元素恰好占用它所需的空间,加上一些指针存储开销。

查询操作效率:

  • 查询效率较低:与 std::vector 或 std::array 这样的随机访问容器相比,std::list 的查询操作通常较慢。这是因为 std::list 没有提供快速的随机访问能力。
  • 顺序访问:在 std::list 中,要访问一个特定位置的元素,你需要从头开始遍历,直到到达那个位置。这意味着访问时间与列表的大小成正比,最坏情况下的时间复杂度为 O(n)。
  • 没有跳跃访问:与数组或 std::vector 不同,std::list 没有提供直接跳到任意位置的能力。在数组中,你可以通过简单的指针算术来访问任意位置的元素,而在 std::list 中,你必须遍历链表。

测试代码:

示例3
    #include <list>
    namespace t03
    {
        void test_list(long& value)
        {
            cout << "\ntest_list()..............\n";
            list<string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push_back(string(buf));//将随机数转为string类型,并使用push_back()将数据存入容器尾部,这是vector的特性
                    //同时值得注意,当空间不足时,内存会自动增加,怎么加方式是2的平方
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "list.size(): " << c.size() << endl;
            cout << "list.max_size(): " << c.max_size() << endl;
            cout << "list.front(): " << c.front() << endl;
            cout << "list.back(): " << c.back() << endl;

            string target = get_a_target_string();//获取输入的数字,并转为string类型返回
            //使用算法find查找
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//返回类型是:使用全局::find()寻找目标,这是循环寻找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;

                //判断有没有找到
                if (pItem != c.end()) {
                    cout << "found," << *pItem << endl;
                }
                else
                {
                    cout << "Not fount!" << endl;
                }
            }
            //测试排序花费的时间
            {
                timeStart = clock();
                c.sort();//使用list自带的排序函数,当然也可以用全局的sort,一般如果容器自带,则选择自带的。
                cout << "list.sort(),milli-seconds: " << (clock() - timeStart) << endl;
            }
        }
    }
    How much memory space you need: 1000000

    test_list()..............
    milli-seconds: 1073
    list.size(): 1000000
    list.max_size(): 329406144173384850
    list.front(): 41
    list.back(): 12679
    target (0--32767):12345
    ::find(),milli-seconds: 4
    found,12345
    list.sort(),milli-seconds: 1611

总结: std::list 的优点在于它的灵活性和高效的插入/删除操作,这些操作可以在 O(1) 时间内完成,因为它们只需要修改几个指针。然而,这种灵活性的代价是牺牲了快速随机访问的能力。因此,如果你的应用场景中需要频繁地在序列中间插入或删除元素,而不太关心随机访问性能,std::list 是一个很好的选择。反之,如果随机访问是一个关键操作,那么可能需要考虑使用其他类型的容器。

链表容器-forward_list

std::forward_list 是 C++ 标准库中的一个容器,它提供了单向链表的实现。

std::forward_list 容器的特点:

  • 单向链表结构:std::forward_list 由一系列节点组成,每个节点包含一个元素和一个指向下一个节点的指针。
  • 内存分配:与 std::list 类似,std::forward_list 的元素也是动态分配内存的。但是,由于它是单向链表,所以只能从链表的前端(头部)开始进行内存分配。
  • 操作限制:由于 std::forward_list 的单向特性,它只提供了 push_front() 和 pop_front() 操作来在链表的前端添加或删除元素。不支持快速的随机访问,也不支持在链表的中间或末尾进行插入和删除操作。

内存使用效率:

  • 无尾插法:std::forward_list 没有 push_back() 方法,只能使用 push_front() 在链表的头部插入元素。这意味着,如果你需要在链表的末尾添加元素,你将不得不遍历整个链表以到达末尾,这在大型数据集中可能效率较低。
  • 节省空间:与 std::list 相比,std::forward_list 每个节点只需要存储一个指向下一个节点的指针,因此它的内存开销比 std::list 小。

适用场景:

  • 空间敏感的应用:如果你的应用对内存使用非常敏感,且需要从链表的前端进行频繁的插入和删除操作,std::forward_list 是一个不错的选择。
  • 单向遍历:如果你的应用只需要从链表的前端开始遍历元素,那么 std::forward_list 可以提供良好的性能。

测试代码

示例3
    #include <forward_list>
    namespace t04
    {
        void test_forward_list(long& value)
        {
            cout << "\nforward_list()..............\n";
            forward_list<string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push_front(string(buf));//将随机数转为string类型,并使用push_frond()将数据存入容器头部,forward_list没有push_back()
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "forward_list.max_size(): " << c.max_size() << endl;
            cout << "forward_list.front(): " << c.front() << endl;
            //cout << "forward_list.back(): " << c.back() << endl;	没有该函数
            //cout << "forward_list.size(): " << c.size() << endl;	没有该函数

            string target = get_a_target_string();//获取输入的数字,并转为string类型返回
            //使用算法find查找
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//返回类型是:使用全局::find()寻找目标,这是循环寻找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;

                //判断有没有找到
                if (pItem != c.end()) {
                    cout << "found," << *pItem << endl;
                }
                else
                {
                    cout << "Not fount!" << endl;
                }
            }
            //测试排序花费的时间
            {
                timeStart = clock();
                c.sort();//使用forward_list自带的排序函数,当然也可以用全局的sort,一般如果容器自带,则选择自带的。
                cout << "forward_list.sort(),milli-seconds: " << (clock() - timeStart) << endl;
            }
        }
    }
    How much memory space you need: 1000000

    forward_list()..............
    milli-seconds: 965
    forward_list.max_size(): 384307168202282325
    forward_list.front(): 12679
    target (0--32767):12345
    ::find(),milli-seconds: 3
    found,12345
    forward_list.sort(),milli-seconds: 1595

值得注意的是: std::forward_list 专为高效的前端插入和删除操作设计,std::forward_list 没有 push_back() 是因为它是单向链表, 只能高效地从前面操作。它没有 size() 函数,因为计算链表长度需要遍历整个链表,这与它优化前端操作的设计目标不符。

总结:

std::forward_list 是一个轻量级的容器,它在内存使用上比 std::list 更为高效,但在功能上也更为有限。它适合于那些只需要单向遍历和操作的场景。由于其单向链表的特性,std::forward_list 在进行元素插入和删除时,只能从链表的前端进行,这限制了它的使用场景。在选择 std::forward_list 时,需要根据应用的具体需求来权衡其优势和局限性。

链表容器-slist

slist容器与forward_list容器一样,只是slis容器存在于ext\slist头文件中。

测试代码:

示例3
    #include <forward_list>
    namespace t04
    {
        void test_forward_list(long& value)
        {
            cout << "\nforward_list()..............\n";
            forward_list<string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push_front(string(buf));//将随机数转为string类型,并使用push_frond()将数据存入容器头部,forward_list没有push_back()
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "forward_list.max_size(): " << c.max_size() << endl;
            cout << "forward_list.front(): " << c.front() << endl;
            //cout << "forward_list.back(): " << c.back() << endl;	没有该函数
            //cout << "forward_list.size(): " << c.size() << endl;	没有该函数

            string target = get_a_target_string();//获取输入的数字,并转为string类型返回
            //使用算法find查找
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//返回类型是:使用全局::find()寻找目标,这是循环寻找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;

                //判断有没有找到
                if (pItem != c.end()) {
                    cout << "found," << *pItem << endl;
                }
                else
                {
                    cout << "Not fount!" << endl;
                }
            }
            //测试排序花费的时间
            {
                timeStart = clock();
                c.sort();//使用forward_list自带的排序函数,当然也可以用全局的sort,一般如果容器自带,则选择自带的。
                cout << "forward_list.sort(),milli-seconds: " << (clock() - timeStart) << endl;
            }
        }
    }
    How much memory space you need: 1000000

    forward_list()..............
    milli-seconds: 965
    forward_list.max_size(): 384307168202282325
    forward_list.front(): 12679
    target (0--32767):12345
    ::find(),milli-seconds: 3
    found,12345
    forward_list.sort(),milli-seconds: 1595

双向容器-deque

alt text alt text

std::deque(双端队列)是一种容器,它允许在序列的前端和后端快速插入和删除元素。尽管从概念图上看, std::deque 似乎是一个连续的存储空间,但实际上它并不是连续的。std::deque 的实现通常是由一个或多个固定大小的连续内存块 (通常称为“节点”或“块”)组成的,这些块通过指针连接在一起。

以下是对您提供内容的整理:

  • 存储机制:std::deque 由多个指针组成,每个指针指向一个具有一定容量的连续内存块。这些内存块被组织在一起,形成一个能够从两端快速增长的容器。
  • 内存管理:当 std::deque 中的一个内存块满了,需要更多的空间时,它会“自动跳转”到下一个空闲的内存块的开始位置。如果所有现有的内存块都已满,std::deque 会分配一个新的内存块,并更新指针以指向这个新的块。
  • 内存效率:使用固定容量的内存块可以减少内存浪费。例如,如果每个内存块的大小为8个元素,那么即使只存储一个元素,也只会浪费7个元素的空间(因为第一个块始终被使用)。这与 std::vector 相比,后者可能会因为频繁的内存重新分配而导致更多的内存浪费。
  • 动态增长:当 std::deque 需要更多内存时,它会动态地增加新的内存块。这种设计使得 std::deque 能够在不牺牲太多内存的情况下,提供快速的插入和删除操作。
  • 连续性:尽管 std::deque 在内部不是完全连续的,但它提供了随机访问的能力,这意味着你可以像访问 std::vector 或数组一样,通过索引来访问 std::deque 中的任何元素。

总的来说,std::deque 是一个灵活且高效的容器,适用于需要在序列的两端进行频繁插入和删除操作的场景。它的设计既考虑了性能, 也考虑了内存使用效率。

测试代码:

示例3
    #include <deque>
    namespace t05
    {
        void test_deque(long& value)
        {
            cout << "\ntest_deque()..............\n";
            deque<string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push_back(string(buf));//将随机数转为string类型,并使用push_back()将数据存入容器尾部
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "deque.max_size(): " << c.max_size() << endl;
            cout << "deque.front(): " << c.front() << endl;
            cout << "deque.back(): " << c.back() << endl;
            cout << "deque.size(): " << c.size() << endl;

            string target = get_a_target_string();//获取输入的数字,并转为string类型返回
            //使用算法find查找
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//返回类型是:使用全局::find()寻找目标,这是循环寻找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;

                //判断有没有找到
                if (pItem != c.end()) {
                    cout << "found," << *pItem << endl;
                }
                else
                {
                    cout << "Not fount!" << endl;
                }
            }
            //测试排序花费的时间
            {
                timeStart = clock();
                sort(c.begin(), c.end());//使用全局的sort,deque没有自己的sort。
                cout << "deque.sort(),milli-seconds: " << (clock() - timeStart) << endl;
            }
        }
    }
    How much memory space you need: 1000000

    test_deque()..............
    milli-seconds: 1055
    deque.max_size(): 461168601842738790
    deque.front(): 41
    deque.back(): 12679
    deque.size(): 1000000
    target (0--32767):12345
    ::find(),milli-seconds: 4
    found,12345
    deque.sort(),milli-seconds: 3838

特殊容器-stack和queue

std::stack(栈)

定义:

std::stack 是一个遵循后进先出(LIFO,Last In First Out)原则的容器适配器。它只能在序列的一端(栈顶)进行添加(push)和移除(pop)操作。

主要操作:

  • push():在栈顶添加一个元素。
  • pop():移除栈顶元素。
  • top():返回栈顶元素的引用,不移除它。

特点:

  • 只能单端操作,即只能在栈顶进行操作。
  • 没有提供直接的迭代器支持,但提供了 top() 方法来访问栈顶元素。

std::queue(队列)

定义:

std::queue 是一个遵循先进先出(FIFO,First In First Out)原则的容器适配器。它只能在序列的一端(队尾)进行添加(push)操作,在另一端(队首)进行移除(pop)操作。

主要操作:

  • push():在队尾添加一个元素。
  • pop():移除队首元素。
  • front():返回队首元素的引用,不移除它。
  • back():返回队尾元素的引用,不移除它。

特点:

  • 双端操作,即在队首进行移除操作,在队尾进行添加操作。
  • 提供了迭代器支持,允许遍历队列中的所有元素。

与 std::deque 的不同.

  • 操作限制:std::stack 和 std::queue 限制了操作的位置,而 std::deque 允许在两端自由操作。
  • 迭代器支持:std::stack 不支持迭代器,std::queue 支持迭代器但只能访问队列中的元素,而 std::deque 提供了随机访问迭代器,可以高效地访问序列中的任何位置。
  • 使用场景:如果你需要一个简单的后进先出或先进先出的数据结构,std::stack 或 std::queue 可能更适合。如果你需要一个能够在两端快速操作且支持随机访问的容器,std::deque 是更好的选择。

测试代码:

示例4
    #include <stack>
    namespace t06
    {
        void test_stack(long& value)
        {
            cout << "\ntest_stack()..............\n";
            stack <string> c;//stack又叫栈,特点是先进后出
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push(string(buf));//将随机数转为string类型,并使用push()将数据压入容器
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "stack.size(): " << c.size() << endl;
            cout << "stack.top(): " << c.top() << endl;
            c.pop();
            cout << "stack.size(): " << c.size() << endl;
            cout << "stack.top(): " << c.top() << endl;

            //与前面的容器不同,stack不可以进行查找操作
        }
    }
    #include <queue>
    namespace t07
    {
        void test_queue(long& value)
        {
            cout << "\ntest_queue()..............\n";
            queue <string> c;//queue又叫队列,特点是先进先出
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.push(string(buf));//将随机数转为string类型,并使用push()将数据压入容器
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "queue.size(): " << c.size() << endl;
            cout << "queue.front(): " << c.front() << endl;
            cout << "queue.back(): " << c.back() << endl;
            c.pop();
            cout << "queue.size(): " << c.size() << endl;
            cout << "queue.front(): " << c.front() << endl;
            cout << "queue.back(): " << c.back() << endl;

            //与前面的容器不同,queue也不提供查找操作
        }
    }
    How much memory space you need: 1000000

    test_stack()..............
    milli-seconds: 1050
    stack.size(): 1000000
    stack.top(): 12679
    stack.size(): 999999
    stack.top(): 17172

    test_queue()..............
    milli-seconds: 1027
    queue.size(): 1000000
    queue.front(): 21384
    queue.back(): 1461
    queue.size(): 999999
    queue.front(): 10793
    queue.back(): 1461

红黑树-multiset容器

multiset容器结构类似二叉树,准确说是红黑树,这种结构的的查找会快很多,但是插入则会耗费一定时间,这些时间都花费在了排序上。

测试代码:

示例8
    #include <set>
    namespace t08
    {
        void test_multiset(long& value)
        {
            cout << "\ntest_multiset()..............\n";
            multiset <string> c;//multiset结构式红黑树
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.insert(string(buf));//将随机数转为string类型,并使用insert()将数据存入容器
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "multiset.size(): " << c.size() << endl;
            cout << "multiset.max_size(): " << c.max_size() << endl;
            
            string target = get_a_target_string();
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//使用全局::fingd()查找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != c.end()) {
                    cout << "found, " << *pItem << endl;
                }
                else
                {
                    cout << "not found!" << endl;
                }
            }
            {
                timeStart = clock();
                auto pItem = c.find(target);//使用multiset自身的fingd()查找
                cout << "c.find(),milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != c.end()) {
                    cout << "found, " << *pItem << endl;
                }
                else
                {
                    cout << "not found!" << endl;
                }
            }
        }
    }
    #include <map>
    namespace t09
    {
        void test_multimap(long& value)
        {
            cout << "\ntest_multimap()..............\n";
            multimap <long,string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.insert(pair<long,string>(i,buf));//此处使用了pair设置键值对
                    //值得注意,multimap不可以用[]做insert,这一点可以结合map容器理解,
                    //另外key是不会重复的,因为接收的是0-1000000,而值可能会重复,因为值是通过随机数函数rand()产生,范围为0-32767
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "multimap.size(): " << c.size() << endl;
            cout << "multimap.max_size(): " << c.max_size() << endl;

            long target = get_a_target_long();
                timeStart = clock();
                auto pItem = c.find(target);
                cout << "c.find(),milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != c.end()) {
                    cout << "found, (" << (*pItem).first <<", " <<pItem->second<<")" << endl;
                }
                else
                {
                    cout << "not found!" << endl;
                }
            
        }
    }
    How much memory space you need: 1000000

    test_multiset()..............
    milli-seconds: 3235
    multiset.size(): 1000000
    multiset.max_size(): 256204778801521550
    target (0--32767):23456
    ::find(),milli-seconds: 59
    found, 23456
    c.find(),milli-seconds: 0
    found, 23456

    test_multimap()..............
    milli-seconds: 1843
    multimap.size(): 1000000
    multimap.max_size(): 230584300921369395
    target (0--32767):23456
    c.find(),milli-seconds: 1
    found, (23456, 31622)

从测试结果看,因为不会抛弃重复的值,multiset的元素个数与大小相等,且存值会话费很多时间。 相比之下multimap存值时间会话费少一些。

unordered_multiset和unordered_multimap

std::unordered_map 和 std::unordered_set 是 C++ 标准库中的两种容器,它们基于哈希表实现。这两种容器提供平均时间复杂度为 O(1) 的快速访问,插入和删除操作。下面简单解释它们的概念图,包括桶和负载因子的说明。

概念图简述:

桶(Buckets):

在 std::unordered_map 和 std::unordered_set 中,哈希表被分为多个“桶”。 每个桶存储指向一个链表的指针(在 std::unordered_map 的情况下)或直接存储元素(在 std::unordered_set 的情况下)。 当元素被插入时,它们的哈希值决定了它们将被存储在哪个桶中。

链表(Chaining):

如果多个元素具有相同的哈希值(哈希冲突),它们将被存储在同一个桶中,并通过链表连接起来。 在 std::unordered_map 中,每个桶可能包含一个链表,链表中的每个节点存储一个键值对。 在 std::unordered_set 中,每个桶可能包含一个链表,链表中的每个节点存储一个元素。

负载因子(Load Factor):

负载因子是哈希表中已存储元素数量与桶数量的比率。 负载因子影响哈希表的性能。较高的负载因子可能导致更多的哈希冲突,从而增加链表的长度,降低性能。 当负载因子超过一个预设的最大值时,哈希表可能会进行重新哈希(rehashing),即增加桶的数量并重新分配所有元素到新的桶中。

概念图说明:

alt text alt text

  • 桶:哈希表被分为多个桶,每个桶可以存储一个链表的头部指针。
  • 指针:在 std::unordered_map 中,每个指针指向一个链表节点,该节点包含一个键值对。在 std::unordered_set 中,每个指针直接指向存储的元素。
  • 元素:链表中的元素,可能是键值对或单个值。

负载因子说明:

重新哈希(Rehashing):当负载因子超过 max_load_factor() 时,哈希表会进行重新哈希,增加桶的数量,并将所有元素重新分配到新的桶中。 动态调整:这种动态调整机制使得 std::unordered_map 和 std::unordered_set 能够根据元素数量的变化自动调整大小,以保持操作的性能。 通过这种结构,std::unordered_map 和 std::unordered_set 能够在大多数情况下提供快速的访问和修改操作,但需要注意,极端情况下(如所有元素哈希到同一个桶中)性能可能会退化到 O(n)。

测试代码:

示例9
    #include <unordered_set>
    namespace t10
    {
        void test_unordered_multiset(long& value)
        {
            cout << "\ntest_unordered_multiset()..............\n";
            unordered_multiset <string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.insert(string(buf));//将随机数转为string类型,并使用insert()将数据存入容器
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "unordered_multiset.max_size(): " << c.max_size() << endl;// 打印 unordered_multiset 能存储的最大元素数量
            cout << "unordered_multiset.bucket_count(): " << c.bucket_count() << endl;// 打印 unordered_multiset 中桶的数量,即存储指向一定长度链表的指针的个数
            cout << "unordered_multiset.max_load_factor(): " << c.max_load_factor() << endl;// 打印 unordered_multiset 可以使用的最大负载因子,它影响桶中链表的最大长度
            cout << "unordered_multiset.max_bucket_count(): " << c.max_bucket_count() << endl;// 打印 unordered_multiset 可以使用的最大桶的数量,这个值通常由桶的增长策略和哈希表的容量决定
            for (unsigned i = 0; i < 20; i++) {
                cout << "bucket # " << i << " has " << c.bucket_size(i) << " element.\n";
            }
            string target = get_a_target_string();
            {
                timeStart = clock();
                auto pItem = ::find(c.begin(), c.end(), target);//使用全局::fingd()查找
                cout << "::find(),milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != c.end()) {
                    cout << "found, " << *pItem << endl;
                }
                else
                {
                    cout << "not found!" << endl;
                }
            }
            {
                timeStart = clock();
                auto pItem = c.find(target);//使用multiset自身的fingd()查找
                cout << "c.find(),milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != c.end()) {
                    cout << "found, " << *pItem << endl;
                }
                else
                {
                    cout << "not found!" << endl;
                }
            }
        }
    }
    How much memory space you need: 1000000

    test_unordered_multiset()..............
    milli-seconds: 1867
    unordered_multiset.max_size(): 329406144173384850
    unordered_multiset.bucket_count(): 1048576
    unordered_multiset.max_load_factor(): 1
    unordered_multiset.max_bucket_count(): 1152921504606846975
    bucket # 0 has 0 element.
    bucket # 1 has 0 element.
    bucket # 2 has 0 element.
    bucket # 3 has 0 element.
    bucket # 4 has 0 element.
    bucket # 5 has 0 element.
    bucket # 6 has 0 element.
    bucket # 7 has 0 element.
    bucket # 8 has 0 element.
    bucket # 9 has 0 element.
    bucket # 10 has 0 element.
    bucket # 11 has 0 element.
    bucket # 12 has 0 element.
    bucket # 13 has 0 element.
    bucket # 14 has 0 element.
    bucket # 15 has 0 element.
    bucket # 16 has 0 element.
    bucket # 17 has 0 element.
    bucket # 18 has 0 element.
    bucket # 19 has 0 element.
    target (0--32767):12345
    ::find(),milli-seconds: 108
    found, 12345
    c.find(),milli-seconds: 0
    found, 12345
    #include <unordered_map>
    namespace t11
    {
        void test_unordered_multimap(long& value)
        {
            cout << "\ntest_unordered_multimap()..............\n";
            multimap <long, string> c;
            char buf[10];
            clock_t timeStart = clock();
            for (long i = 0; i < value; ++i)
            {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c.insert(pair<long, string>(i, buf));
                }
                catch (exception& p)
                {
                    cout << "i=" << i << " " << p.what() << endl;
                    abort();//发生异常使用该函数退出程序
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "unordered_multimap.size(): " << c.size() << endl;
            cout << "unordered_multimap.max_size(): " << c.max_size() << endl;

            long target = get_a_target_long();
            timeStart = clock();
            auto pItem = c.find(target);
            cout << "c.find(),milli-seconds: " << (clock() - timeStart) << endl;
            if (pItem != c.end()) {
                cout << "found, (" << (*pItem).first << ", " << pItem->second << ")" << endl;
            }
            else
            {
                cout << "not found!" << endl;
            }

        }
    }
    How much memory space you need: 1000000

    test_unordered_multimap()..............
    milli-seconds: 1942
    unordered_multimap.size(): 1000000
    unordered_multimap.max_size(): 230584300921369395
    target (0--32767):12345
    c.find(),milli-seconds: 0
    found, (12345, 5839)

set和map容器

测试代码:

示例10
    namespace t12
    {
        void test_set(long& value){
            cout << "\ntest-set()............................\n" << endl;
            set<string> c;
            char buf[10];
            clock_t timeStart = clock();
            
                for (long i = 0; i < value; ++i) {
                    try
                    {
                        snprintf(buf, 10, "%d", rand());
                        c.insert(string(buf));
                    }
                    catch (exception& p)
                    {
                        cout << "i = " << i << p.what() << endl;
                        abort();
                    } 
                }
                cout << "milli-seconds: " << (clock() - timeStart) << endl;
                cout << "set.max_size() " << c.max_size() << endl;
                cout << "set.size() " << c.size() << endl;
                string target = get_a_target_string();
                {
                    timeStart = clock();
                    auto pItem = ::find(c.begin(), c.end(), target);
                    cout << "::find(), milli-seconds: " << (clock() - timeStart) << endl;
                    if (pItem != c.end()) {
                        cout << "found, " << *pItem << endl;
                    }
                    else
                        cout << "not fount!" << endl;
                }
                {
                    timeStart = clock();
                    auto pItem = c.find(target);
                    cout << "find(), milli-seconds: " << (clock() - timeStart) << endl;
                    if (pItem != c.end()) {
                        cout << "found, " << *pItem << endl;
                    }
                    else
                        cout << "not fount!" << endl;
                }
            
        }
    }
   namespace t13
    {
        void test_map(long& value) {
            cout << "\ntest_map()............................\n" << endl;
            map<long,string> c;
            char buf[10];
            clock_t timeStart = clock();

            for (long i = 0; i < value; ++i) {
                try
                {
                    snprintf(buf, 10, "%d", rand());
                    c[i] = string(buf);//注意这种使用方式
                }
                catch (exception& p)
                {
                    cout << "i = " << i << p.what() << endl;
                    abort();
                }
            }
            cout << "milli-seconds: " << (clock() - timeStart) << endl;
            cout << "map.max_size() " << c.max_size() << endl;
            cout << "map.size() " << c.size() << endl;
            long target = get_a_target_long();
            {
                timeStart = clock();
                auto pItem = c.find(target);
                cout << "::find(), milli-seconds: " << (clock() - timeStart) << endl;
                if (pItem != c.end()) {
                    cout << "found value, " << pItem->second << endl;
                }
                else
                    cout << "not fount value!" << endl;
            }
        }
    }
    How much memory space you need: 1000000

    test-set()............................

    milli-seconds: 2133
    set.max_size() 256204778801521550
    set.size() 32768
    target (0--32767):12345
    ::find(), milli-seconds: 1
    found, 12345
    find(), milli-seconds: 0
    found, 12345

    test_map()............................

    milli-seconds: 1937
    map.max_size() 230584300921369395
    map.size() 1000000
    target (0--32767):12345
    ::find(), milli-seconds: 0
    found value, 5839

分配器测试程序

alt text alt text

附件

4.C++2.0特性的使用

VS 2022的设置

首先你可以先用下面的代码测试使用可以执行:

    #include <iostream>
    void printTypes() {
    }
    template <typename T, typename... U>
    void printTypes(const T& t, const U&... u) {
        std::cout << t << std::endl;
        printTypes(u...);
    }

    int main() {
        printTypes('a', 1.5, 'b');
    }

如果报错了,如typename…未定义,那么请你按照如下步骤操作, alt text alt text

  • C++语言标准选择预览 - 最新 C++ 工作草案中的功能 (/std:c++latest)
  • 启用实验性C++标准库模块–
  • 生成ISO C++23标准库模块 –

const补充

const 参数 non-const 参数
const 函数 可执行 可执行
non-const 函数 报错 可执行

使用const需要注意两个问题,参数是否需要修改,参数进入函数内后数据是都会修改。解决这两个问题,在需要的地方加上const

还有一种情况,

    template<typename T>
    class foo {
    public:
        T fooTo(T str) const { return str; };
        T fooTo(T str)  {return str;};
    };

    int main() {
        foo < char > f;
        const char x = 'a';
        char y = 'c';
        std::cout <<  f.fooTo(x) << std::endl;
        std::cout <<  f.fooTo(y) << std::endl;
    }

运行结果如下: alt text alt text 在这个例子中,带const的参数只会执行带const的函数,而不带const的参数会只会执行不带const的函数,这就是带const和不带const函数出现时的情况。 但是这个结果我并不是很满意,应为我试着在不带const函数内部修改传入的参数,代码执行时将不会以我上面说的结论出现。或许还需要在研究研究……

附件

3.导读

书籍推荐

学习路线

路线 路线

conversion function-转换函数

接下来以一个分数的代码例子说明转换函数。

情况1:让值转为其他类型

    class Fraction
    {
    private:
        int m_num, m_den;
    public:
        Fraction(int num, int den = 1)
        : m_num(num),m_den(den){}
        opertaor double() const {
            return (double)(m_num/m_den);
        }
    };

    //使用示例:
    Fraction f(2,5);
    double d = 4+f;

在上述的示例中,d = 4+f;程序会先判断有没有写opertaor +,如果没有会试着将f通过opertaor double()转为double类型。

情况2:non-explicit-one-argument-ctor,不带explicit的一个参数的构造函数,将其他类型转为预所写类的类型

    class Fraction
    {
    private:
        int m_num, m_den;
    public:
        Fraction(int num, int den = 1)
        : m_num(num),m_den(den){}
        opertaor double() const {
            return (double)(m_num/m_den);
        }
    };

    //使用示例:
    Fraction f1(2,5);
    Fraction f2 = 4+f1;

同样在这个地方会先将4转为Fraction,然后在进行相加,转换函数使用的是构造函数,构造函数默认第二个参数是1,也就一个参数时也可用,但这个意思并不是说f1(2)你不写第二个参数。

虽然向上面的情况可行,但是当你把两个情况结合在一起时,就会报错,例如你如下写:

    class Fraction
    {
    private:
    int m_num, m_den;
    public:
        Fraction(int num, int den = 1)
        : m_num(num),m_den(den){}
        opertaor double() const {
            return (double)(m_num/m_den);
        }
        Fraction operator + (const Fraction& f){
            return Fraction(……);
        }
    };

情况3:explicit-one-argument-ctor,带explicit的一个参数的构造函数 这个时候再使用刚才的例子,程序就会报错,因为此时是两种情况的结合,意味着两种情况都可以实现,编译器此时就不知道该使用哪个方法,为了解决这个问题,你可以使用关键字explicit来约束构造函数,让其他类型如法转为所写类,这个时候编译器会报错说无法将double类型转为Fraction。如下:

    class Fraction
    {
    private:
        int m_num, m_den;
    public:
        explicit Fraction(int num, int den = 1)
        : m_num(num),m_den(den){}
        opertaor double() const {
            return (double)(m_num/m_den);
        }
        Fraction operator + (const Fraction& f){
            return Fraction(……);
        }
    };
    //示例
    Fraction f1(2,5);
    Fraction f2 = 4+f1;

编写的类大两个大方向

智能指针

alt text alt text alt text alt text

伪函数

这样的类会像函数一样接收参数,返回某一类型的值通常会看到类中重载operate() (……){……}函数。

特化

alt text alt text

偏特化–对应泛化

  • 个数上的偏

以我的理解就是假设原先设计的模版为

    template <typename a, typename b……>
    class A
    {……}

现在我使用偏特化,写为

    template <typename b>
    class A<bool, typename b……>
    {……}

也就是第一个参数已经确定是bool类型了。(大概先这么理解吧:smile: :smile: :smile:)

  • 范围上的偏特化

一般我们的类模版你可以任意指定类型,但是现在我想写一个用指针指向的类模板,那么他就被限制在一定的范围内了

    template <typename a>
    class A
    {……}
    //范围偏特化
    template <typename a>
    class A<typename* a>
    {……}

模板模板类

假设我要创建一个容器,并指定这个容器内容的类型,这个时候可以使用模版模版类,详细看下图: alt text alt text

附件

2.组合与继承

类与类的三种关系

Composition-复合

以我个人的理解,复合就是一个类中包含有另外一个类,使用到另一个类的内容。复合的类他们的构造和析构函数运行次序是,构造函数有内到外依次运行,析构函数则相反。可以使用下图表示这种关系

alt text alt text

delegation-委托

当一个类欲用一个指针指向另一个类,以达到想使用时就指向这个类的这种关系就叫delegation,假设我要实现一个计算,此时指针指向这个函数,让函数去做这个计算。这有些类似于复合,但是这里重点在于指针的使用。在写一些功能的时候也建议使用这种关系的特性,因为这样就将两者隔开,起到一定的保护作用。参考下图: alt text alt text

Inheritance-继承

在C++有三种继承方式,其中public是常用的一种,继承表示从一个类中集成某些属性成为 另外一种 类。

alt text alt text

Inheritance关系的两个类它们的构造函数与析构函数执行次序与之前描述复合时一样。如下图是这种关系的表示方法: alt text alt text

  • 构造函数(constructor):由内到外,代码表示为SunFunction::SunFunction(…):base(){…};
  • 析构函数(destructor):由外到内,代码表示为SunFunction::SunFunction(…){……~base();};

值得注意的是,如果一个类将来可能是父类,那么需要在析构函数前加virtual关键字,否则会报错undefine behavior

设计模式

**参考书籍:**Design PatternsExplained Simply

附件

1.代码编写规范

如何写一个标准的.h文件

以下内容来自B站

接下来将以复数的库文件为例来回答这个问题,下面是一段参考复数库文件代码:

class complex
{
private:
    /* data */
    double re,im;
    friend complex& __doapl (complex*,const complex&);
public:
    complex (double r,double i)
    :re(r),im(i)                //这是一个构造函数,这种写法意思是初始化变量
    {};
    complex& operator += (complex operator&);
    double read () const {return re;}  //这里需要加const 意思就是修饰函数的返回值,不允许改变返回值类型
    double imag () const {return im;}
};

构造函数的特性

这一点需要关注下面的代码:

complex (double r,double i)
    :re(r),im(i)                //这是一个构造函数,这种写法意思是初始化变量
    {};

关于const修饰符

如上面说到的,函数返回最好是加const,这样可以应付下面情况的发生。

const的使用
    double read () const {return re;}  
    double imag () const {return im;}

此时做如下两种使用都是正确的

    complex com1(1,2);
    cout<<com1.real()<<endl;
    const complex com1(1,2);
    cout<<com1.real()<<endl;
    double read ()  {return re;}  
    double imag ()  {return im;}

此时做如下两种使用只有第一种可以被执行,第二种会报错。

    complex com1(1,2);
    cout<<com1.real()<<endl;
    const complex com1(1,2);
    cout<<com1.real()<<endl;

函数传递和返回值的两种方式

pass by values:传值。在传输的事单字节或者字节数较少的情况下使用,比如传一个字符。

pass by reference:传引用。在C++中引用的底层逻辑就是传指针,也就是类似穿了地址,也就是只传输4个字节, 在这种情况下你可以使用const修饰符,迫使函数不能修改值,如果希望函数对值进行处理则可以不加

    void re(const classname& cl){};//加了const
    void re(classname cl){};//没有加const

对于返回值,我们可以返回值,也可以返回引用,但什么情况下返回的是引用呢?

在设计类中的函数时,可以先考虑返回类型适不适合引用,如果返回的是已经存在的地址,那么可以选择返回类型为引用,否则不使用。

inline complex&
        __ap(complex * this ,const complex & c){
            this->re += c.re;
            this->im += c.im;

            return *this;
        }

向上面的函数就是使用了引用返回。

操作符重载-1:成员函数

操作符重载 操作符重载

操作符重载-2:非成员函数

假设现在要计算复数的加减,如下:

    complex com1(1,0);
    complex com2(2,9);
    complex com3;
    
    com3 = com1 + com2;
    com3 = com1 + 2;
    com3 = 0 + com2;

    ……

对于这段代码在库文件中,要对加法做非成员函数重载,以应付 不同的情况。

接下来在考虑一个问题,就是如果我们只是做cout<<com1;那么对于«函数返回值可以是void类,但是如果我们做的是cout<<com1<<com2<<endl; 这样的操作返回类型为void类型,那么当运行cout<<com1后就无法运行<<com2<<endl的代码.

很显然要让代码继续运行,我们还需要返回iostream类型,而且使用return by reference返回方法,也就是返回引用。如下图: alt text alt text

小结

通过以上的简单讲解,我们可以总结一下,再写一个类的时候,我们需要注意以下几点:

  • 确定类的数据并写在private中;
  • 对于构造函数要善于使用初始化数据方法;
  • 对于函数要想一想,返回类型是否可以改变,要不要写const;
  • 对于函数类型和返回值,要传值还是传引用,要返回值还是返回引用。

是否返回引用最最好的判断标准就是,值在经过函数运算后存储在函数外,或说是一个以存在的存储地址,否则不返回引用

接下来通过学习string.h库,进一步了解指针的使用

一般有指针的类需要写三个特殊函数拷贝构造、拷贝析构、拷贝赋值,

String String alt text alt text alt text alt text alt text alt text

有时候还需要用到new 和 delete。

stack(栈)和heap(堆)

如下代码:

    class String{……};
    ……
    {
        String str("hello");
        String str1 = new String();
        ……
    }

上面的{}内的代码就是存储在stack中,而经过new的变量存在heap中;

stack object 的生命周期

在{}内的代码执行结束后,自动调用析构函数将变量清理掉,如下代码,

    class String{……};
    ……
    {
        String str("hello");
        ……
    }

static local object 的生命周期

添加关键字static的变量成为静态变量,在作用于如下面代码的{}结束后不会被析构函数清理掉, 变量会存储直到程序结束。

    class String{……};
    ……
    {
        static String str("hello");
        ……
    }

global objects 生命周期

全局变量在程序接收后才会被清理掉。

    class String{……};

    String str("hello");
    ……
    {
        ……
    }

调用new和delete的过程

在使用new的地方需要使用delete清理内存,防止内存泄露。

调用new过程:

alt text alt text

调用delete的过程:

alt text alt text alt text alt text

代码 分配内存大小左图是调试模式下的内存分配,右图是非调试模式下分配的内存

alt text alt text

new与delete的搭配,在删除数组时需要加上[]否则编译器不知道你要删的是数组:

alt text alt text

new和delete的补充

对new和delete的重载,用于设计内存管理,如内存池等操作。

重载::new,::delete

重载的函数中,new操作会接收一个大小,该大小由编译器传入,而delete传入的是一个指针,如下图; alt text alt text

如果不想使用类中的new和delete,可以在二者之前加上:,如::new,::delete,这样在调用函数时就不会调用类设计中的new和delete重载,而是全局的。

类中重载的new和delete

//类
class F{
    ...
    public:
        void* operate new(size_t);//1
        void operate delete(void*,size_t);//2,size_t可不写
    ...
}
//代码段1的内部操作
try{
    void* mem = operate new(size_t);
    p = static_cast<F*>(mem);//类型转换
    p->F::F();//构造函数
}
//代码段2的内部操作
p->~F();
operate delete(p);

// main函数调用示例
{
    F* p = new F();
    ...
    delete p;
}

类中重载的new[]和delete[]

代码整体流程与上一个情况类似,区别在于new[]是申请数组内存,在销毁的时候如果不使用delete[]释放内存,delete只会执行一次,导致内存没有完全释放,详细代码如下:

//类
class F{
    ...
    public:
        void* operate new(size_t);//1
        void operate delete(void*,size_t);//2,size_t可不写
    ...
}
//代码段1的内部操作
try{
    void* mem = operate new(size_t*N + 4);//指针占4个字节
    p = static_cast<F*>(mem);//类型转换
    p->F::F();//构造函数
}
//代码段2的内部操作
p->~F();//析构函数
operate delete(p);

// main函数调用示例
{
    F* p = new F[N];
    ...
    delete[] p;
}

new,delete使用示例

    using namespace std;
class Foo
{
private:
    int _id;//4个字节
    long _data;//4个字节
    string _str;//40个字节
public:
    Foo() :_id(0)       { cout << "default ctor.this = " << this << "id: " << _id << endl; };
    Foo(int i) :_id(i)  { cout << "ctor.this = " << this << "id: " << _id << endl; };
    ~Foo()              { cout << "dtor.this = " << this << "id: " << _id << endl; };
    static void* operator new(size_t size);
    static void operator delete(void* p, size_t size);
    static void* operator new[](size_t size);
    static void operator delete[](void* p, size_t size);
};

void* Foo::operator new(size_t size)
{
    Foo* p = (Foo*)malloc(size);
    cout << "new size = " << size << endl;
    return p;
}

void Foo::operator delete(void* p, size_t size)
{
    cout << "delete size = " << size << endl;
    free(p);
}

void* Foo::operator new[](size_t size)
{
    Foo* p = (Foo*)malloc(size);
    cout << "new[] size = " << size << endl;
    return p;
}

void Foo::operator delete[](void* p, size_t size)
{
    cout << "delete[] size = " << size << endl;
    free(p);
}

运行结果如下:

new size = 48
default ctor.this = 00000191EC67A460id: 0
dtor.this = 00000191EC67A460id: 0
delete size = 48
_____________________________________________
new[] size = 104
default ctor.this = 00000191EC673438id: 0
default ctor.this = 00000191EC673468id: 0
dtor.this = 00000191EC673468id: 0
dtor.this = 00000191EC673438id: 0
delete[] size = 104

为什么new[]操作会多出8个字节呢?

我看课程视频,老师说new[]分配的内存块会在最前面存储计数量,占用4个字节,表示数组大小,但是我在电脑上运行会多出8个字节,查阅资料可能与系统、对齐方式、编译器等有关。

alt text alt text

对new的分配额外内存

假设分配内存的同时需要额外分配一定的内存,可以参考下面的代码: alt text alt text

小结

对于包含有指针的类,在必要时写上拷贝构造、拷贝赋值、析构函数,如下代码

    class String
    {
    private:
        char* m_data;
    public:
        String(const char* cstr = 0);           //构造函数
        String(const String& str);              //拷贝构造函数
        String& operator= (const String& str);  //拷贝赋值函数
        ~String();                              //析构函数
        char* get_c_str() const {return m_data;}
    };
    String::~String()
    {
        delete[] m_data;
    }
    inline 
    String::String(const char* cstr = 0)
    {
        if (cstr)
        {
            m_data = new char[strlen(cstr)+1];
            strcpy(m_data, cstr);
        }else{
            m_data = new char[1];
            strcpy(m_data,'\0');
        }
    }
    inline
    String::String(const String& cstr){
        m_data = new char[strlen(cstr.m_data) + 1];
        strcpy(m_data, cstr);
    }
    inline 
    String& String::operator=(const String& str)
    {
        if(this == &str)
        return *this;
        delete[] m_data;
        m_data = new char[strlen(str.m_data) + 1];
        strcpy(m_data, str);
        return *this;
    }

补充内容static

alt text alt text

static在private中的使用例子

alt text alt text alt text alt text

class template

alt text alt text

function template

member template 成员模板

类模板中,还有类模板,通常是对构造函数的操作,如下图: alt text alt text

如下图代码 alt text alt text

用意就是当一个指针指向父类时,初始化让他指向子类,那么此时需要实现上图类中的代码。

namespace(命令空间)

使用示例:

    using namespace std;
    {
        ……
    }

使用方法:

    #include <iostream>
    using namespace std;

    int main()
    {
        cout<<……;
        cin<<……;
        return 0;
    }
    #include <iostream>
    using std::cout;

    int main()
    {
        cout<<……;
        std::cin<<……;
        return 0;
    }

或者

    #include <iostream>
    int main()
    {
        std::cout<<……;
        std::cin<<……;
        return 0;
    }
附件