基于COCOS&c++以及部分算法的整理目录[持续更新]

基于COCOS&c++以及部分算法的整理目录

C++与COCOS Engine

Cocos Creator 既是一款高效、轻量、免费开源的跨平台 2D&3D 图形引擎,也是一个实时 2D&3D 数字内容创作平台。拥有 高性能、低功耗、流式加载、跨平台 等诸多优点,您可以用它来创作 游戏、车机、XR、元宇宙 等领域的项目。
而C++正是其支持的主要语言。
本文章是作者自己学习和记录的工具,也欢迎大家进行斧正

1 C/C++语言

C++ 是一种高级语言,它是由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,是一种面向对象的程序设计语言。

1.1 c++是一门面向对象语言

面向对象编程(Object Oriented Programming)简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象(Object)包含了数据操作数据的函数。对象可达可小,可以是一个学校,一个班级,甚至一个学生,对象(Object)含有属性(attribute),一般是静态的,例如班级有所属院系,班级人员等;也含有**行为(behavior)**例如开会,上课等。

不同于c的面向过程,c++是一门面向对象的语言。

在C++中,每个对象(Object)都是由数据(data)函数(function)组成。数据对应上文的属性,例如一个四边形,四条边即为它的属性函数对应行为,例如将这个四边形的边长计算以及输出,计算四边形的周长在OOP称之为方法(method),调用对象的函数事项对象传送一个消息(message)

1.1.1 类

是 C++ 的核心特性,通常被称为用户定义的类型。
类是对象的抽象,而对象是类的具体实例(instance)

类用于指定对象的形式,是一种用户自定义的数据类型,它封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。

类和结构体类似:
结构体声明

struct User
	{
	char account[10];
	char password[50];
	char name[24];
	};
//声明了一个结构体类型 User 模拟用户
User u1,u2;
//定义了两个结构体变量u1和u2

类的声明:

class User 
//User类的声明
	{
	private: 
//私有属性,如果不指定,默认为私有
		{
		char account[10];
		char password[50];
		char name[24];
		}
	public: 
//公开类型
		void PrintUserInfo() 
//成员函数,打印用户信息
			{
			cout<<"账号"<<account<<endl;
			cout<<"密码"<<password<<endl;
			cout<<"用户名"<<name<<endl;
			}
	}u1,u2; 
//这里也是定义两个User类对象,少用
User user1,user2; 
//定义两个User类对象,常用

也可以类外定义成员函数:

class User 
//User类的声明
	{
	private: 
//私有属性,如果不指定,默认为私有
		{
		char account[10];
		char password[50];
		char name[24];
		}
	public: 
//公开类型
		void PrintUserInfo() 
//成员函数,打印用户信息
	};
void User::PrintUserInfo()
//类外定义,::为作用域限定,用来限定在这个类里定义
{
	cout<<"账号"<<account<<endl;
	cout<<"密码"<<password<<endl;
	cout<<"用户名"<<name<<endl;
}
User user1,user2; 
//定义两个User类对象

当然,在C++中stract也具有类的作用只不过,默认为public公开属性:

struct User
	{
	private: 
//私有属性,如果不指定,默认为公开
		{
		char account[10];
		char password[50];
		char name[24];
		}
	public: 
//公开类型
		void PrintUserInfo() 
//成员函数,打印用户信息
			{
			cout<<"账号"<<account<<endl;
			cout<<"密码"<<password<<endl;
			cout<<"用户名"<<name<<endl;
			}
	};
User u1,u2;
//定义了两个结构体变量u1和u2

两者有异同,这里简单分析两者异同点,具体请移至

1.1.2内联 inline

为了减少时间开销,如果类体中定义的成员函数中不包括循环等控制结构,C++自动视其为内联函数(inline),又称内置函数,也就是说,程序调用成员函数时候,知识吧函数代码嵌入程序的调用点,减少调用成员函数的开销。
inline的使用。上面代码已经给出,只需将

void PrintUserInfo()

改为

inline void PrintUserInfo()

若是类外定义,系统不会默认其为内联,需添加inline

1.1.3类成员的引用

访问有以下三种方法(使用上面关于User的定义):

  • 通过对象名和成员运算符访问对象中的成员;
  • 通过指向对象的指针访问对象中的成员;
  • 通过对象的引用变量访问对象中的成员。
1.1.3.1 通过对象名和成员运算符访问对象中的成员
User1.acconut = "10000";

上式访问是错误的,因为在User类中,成员account为私有成员,不能直接访问,类外只能调用公有成员,调用类型与上式一致。

1.1.3.2 通过指向对象的指针访问对象中的成员
class Monster
	//简单定义一个怪物的信息
{
	//公开怪物基本信息
public:
	
	char MonsterId[16];
	char MonsterName[50];
	char MonsterInfo[500];
	
private:

	int Hp;
	int Atk;

public:

	bool IsDead(int hp, int damage)
		//判断是否死亡
	{
		if (hp - damage > 0)return false;
		else return true;
	}

};
//指针访问
Monster m1,*p;
p = &m1;
//定义对象m1和指向Monster类的指针变量p
cout<< p->MonsterId;
//输出Monster对象的成员MonsterId
1.1.3.3 通过对象的引用变量访问对象中的成员

使用上面Monster对象,进行以下定义

Monster noob;
Monster &master = noob;
//定义Monster类引用变量master,并使之初始化为noob
cout<< master.MonsterInfo<<endl; 

noob和master共占一段储存单元,两者一致。

1.1.4 构造函数

构造函数就是初始化对象成员,很好理解,具体参考下面代码及其注释:
我们将Monster函数进行一定的扩充与修改

class Monster
	//简单定义一个怪物的信息
{
	//公开怪物基本信息
public:
	char MonsterId[16];
	char MonsterName[50];
	char MonsterInfo[500];
	Monster() 
	//利用构造函数对对象中的数据成员赋初值
	{
		strcpy_s(this->MonsterId ,"12345678901234");
		strcpy_s(this->MonsterName , "Noob");
		strcpy_s(this->MonsterInfo ,"none");
		Hp = 100;
		Atk = 5;
	}
private:
	int Hp;
	int Atk;
public:
//判断是否死亡
	bool Isdead(int damage)
	{
		int hp = this->Hp;
		if (hp - damage > 0)
		{
			this->Hp = hp - damage;
			return false;
		}
		else
		{
			this->Hp = 0;
			return true;
		}
	}
//设置怪物血量
	void SetHp(int Hp)
	{
		this->Hp = Hp;
		return;
	}
//打印血量
	void ShowHp()
	{
		std::cout << this->Hp;
		return;
	}
};

主函数

#include<stdio.h>
#include"Monster.h"
int main()
{


	Monster noob;
	int Damage;
	std::cout << "输入你的攻击伤害" << std::endl;
	std::cin >> Damage;
	std::cout << "在你面前的是" << noob.MonsterName << std::endl << "它:" << noob.MonsterInfo << std::endl;
	std::cout << "血量";
	noob.ShowHp();
	puts(" ");
	if (noob.Isdead(Damage))
	{
		std::cout << "怪物已死亡" << std::endl;
	}
	else
	{
		std::cout << "剩余血量:";
		noob.ShowHp();
		std::cout << std::endl;
	}

	return 0;
}

程序运行情况为
在这里插入图片描述

1.1.4.1 重载

在一个类中可以定义多个构造函数,供用户使用,这些构造函数具有相同的名字,而参数的个数和参数的类型不同,这边成为对象函数的重载
例如上式的

public:
	char MonsterId[16];
	char MonsterName[50];
	char MonsterInfo[500];
	Monster() 
	{
		strcpy_s(this->MonsterId ,"12345678901234");
		strcpy_s(this->MonsterName , "Noob");
		strcpy_s(this->MonsterInfo ,"none");
		Hp = 100;
		Atk = 5;
	}
private:
	int Hp;
	int Atk;

可以改为

public:
	char MonsterId[16];
	char MonsterName[50];
	char MonsterInfo[500];
	Monster(int h, int a) :Hp(h), Atk(a) {
		strcpy_s(this->MonsterId, "12345678901234");
		strcpy_s(this->MonsterName, "Noob");
		strcpy_s(this->MonsterInfo, "none");
	};
	//声明一个有参的构造函数,用参数的初始话表对数据成员初始化
	Monster();
	//声明一个无参的构造函数
private:
	int Hp;
	int Atk;

主函数前添加

	Monster::Monster(100, 5);
	//*或将	Monster noob;修改为	Monster noob(100,5);*

可见,构造函数的方法也能用于定义私有属性。
注意

在调用构造函数是不必给出实参的构造函数,成为默认构造函数(default constructor)又称缺省构造函数,无参的构造函数属于上者,一个类中只能有一个默认构造函数,否则系统无法辨别应该执行那个构造函数

1.1.5 析构函数

析构函数(destructor)也是一个特殊成员函数,类目的前面加一个"~",又因为该符号又为取反运算符,因此,析构函数始于构造函数相反的函数。
当函数声明期结束,会自动执行析构函数。执行情况为:

  1. 如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
  2. static 局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static 局部对象的析构函数。
  3. 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用 exit 函数)时,调用该全局对象的析构函数。
  4. 如果用new运算符动态地建立了一个对象,当用delete 运算符释放该对象时,先调用该对象的析构函数。

构造函数的作用不是删除对象,而是撤销对象占用内存之前完成一些清理
析构函数不返回任何值,不能吧被重载,一个类有且只有一个构造函数

我们可以在上式中,Monster类里面添加

	~Monster()
	//定义析构函数
	{
		puts("Destructor called");
	}

运行后,可发现
在这里插入图片描述
可知,在主函数结束后,noob对象的生命期也结束,在撤销对象之前的最后一项工作便是调用解析函数,这里只用到输出一个信息,没有实质作用。
在微软文章中,可考虑String类的以下声明(部分修改翻译)

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // 构造函数声明
        ~String();               // 析构函数声明
    private:
        char* _text{nullptr};	//nullptr 为指向地址为空
};

// 定义构造函数
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // 增加结束符'\0'

    // 动态分配正确的内存
    _text = new char[sizeOfText];

    // 如果分配成功,将字符串赋值。
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// 定义析构函数
String::~String()
{
    puts(_text);
    // 释放为字符串保留的内存
    delete[] _text;
}

在主函数内添加

	String str("We love C++");

运行
在这里插入图片描述
预处理顺序

#include"Monster.h"
#include"textClass.h"

这也可以知,首先调用的构造函数,最后运行析构函数,这一点相当于

1.1.6 对象的动态建立和释放

C++很多时候默认都是静态建立的,这并不好,容易将内存搞得很大,因此,C用new动态的分配内存,使用delete释放这些空间,从而提高空间的使用率。
例如前面的Monster类。我们可以建立一个新的动态对象

new Monster;

此时系统开辟了新的空间,但是不知道这个对象的名字和它的地址,它存在,但我不知道它
用new对象运算符动态分配内存后,将返回一个指向新的对象1指针的值,就是分配内存空间的地址,我们可以通过这个地址来访问这个对象。如下:

Monster *noob_one;
noob_one = new Monster;

//此时noob_one指向的就是这个新对象的地址,当然也可以将其初始化,初始化参考下面继承,这里先给代码。

Monster *noob_one = new Monster(100,10,"101","一号菜鸟","稍微厉害一点,战十渣");

当要释放空间时候,我们可以

delete noob_one;

我们可以在一个怪物死亡时候,将其释放,从而优化空间,就像大自然分解尸体一样,达到清理的目的。

1.1.7 对象的赋值和复制

赋值的前提是,两个对象必须属于同一个类,对象的赋值只能对其中的数据成员赋值,而不能对成员函数赋值。
复制复形式为 子对象(父对象);
下面两种写法均为复制:

Monster noob_one(noob);
Monster noob_one=noob;

1.2 面对对象的三特性

OOP一般具有三个特性:封装,继承,多态。

1.2.1 封装

封装简单来讲就是打包一个类,将公开的方法和把一些对象属性私有化,毕竟有些属性我们不想让外界访问,但也需要公开的方法使这个类有一定的意义。
公有成员函数是用户使用类的公用接口(public interface 又称对外接口),虽然私有成员不饿能直接访问,但是可以使用公有函数修改,例如上式,
即便Monster对象中的Hp为私用成员,我们也可以修改它。当接口与实现(对数据的操作)分离时,只要类的接口没有改变,对私有实现的修改不会影响程序的其他部分。
注:上式代码已经将类声明和成员函数定义分离。
封装的优点:

良好的封装能减少耦合(Coupling)
类的内部结构可以任意修改
可以对成员更好的控制
隐藏私有成员

1.2.2 继承

继承(inheritance)可以实现类的扩展,如同名字一样,简单理解,就是一个类继承另一个类的同时还能创建新的成员,一般的,这两个类分别成为基类(base class)和派生类(derived class)或者父类(father class)和子类(son clase)
派生类是基类的具体化,而基类则是派生类的抽象。
我们可以创建新的头文件MonsterBoss.h的信息,继承类Monster

#pragma once
#pragma once
#include "Monster.h"

class MonsterBoss :public Monster
//public为继承方式
{
	private :
		int df;		//新增防御
	public :
		MonsterBoss() {};	//要有默认构造函数,不然会报错LNK2019
		MonsterBoss(int df, int atk, int hp, const char* id, const char* name, const char* info):df(df)
		{
			strcpy_s(this->MonsterId, id);
			strcpy_s(this->MonsterInfo, info);
			strcpy_s(this->MonsterName, name);
			this->df = df;
			this->SetHp(hp);
			this->SetAtk(atk);
		};
		bool Isdead(int damage)
		{
			if (df - damage >= 0)
			{ 
				df -= damage;
				return false;
			}
			else if (df == 0)
			{
				return this->Monster::Isdead(damage);
			}
			else
			{
				int remain_df = damage-df;
				df = 0;
				return this->Monster::Isdead(remain_df);
			}
		}
};

继承方式有三种分别是 pubilc、protect、private

1.2.2.1 public公有继承

公有继承比较好理解,基本上就是属性不变,该公开公开,改私有私有,不同的是,如果要调用原先的基类的私有成员,我们可以在基类设置可以修改私有成员的公有函数,再在派生类内调用,甚至类外调用可以达到目的。

1.2.2.2 private 私有继承

私有继承就是将所有成员属性变为私有,但基类的私有成员已经是私有了所以仍然不可访问。私有继承能将原来所有成员隐藏。

1.2.2.3 protected保护继承

顾名思义,protected是用来保护成员的,它是用来声明成员的访问权限的,受保护的成员不能被类外访问,但是特别的,保护成员可以被派生类引用,这个和私有继承不一致。

1.2.2.4 三种继承方式特点

不同方式派生类对基类成员的访问情况不同,具体可以看下面的表:

继承方式\ 基类成员公有成员保护成员私有成员
public可以访问,原有属性可以访问,原有属性不可访问
protected可以访问,保护属性可以访问,保护属性不可访问
private可以访问,私有属性可以访问,私有属性不可访问
我们可以简单的编辑一个程序来验证这些特性
class father_class
{
public: 
	int a = 1; 
protected:
	int b = 2;
private:
	int c = 3;
};

//公开继承
class public_son_class: public father_class{};
//保护继承
class protected_son_class : protected father_class{};
//私有继承
class private_son_class : private father_class {};

在公开继承添加简单的函数使其得到调用可发现a,b均正常访问,c却不可
公开继承
这点在保护继承和私有继承也是一致的
在这里插入图片描述
在这里插入图片描述
我们再将三种派生类实例化
在这里插入图片描述
划红线的均为不可访问,因为保护成员,私有成员均在类外不可访问,继续我们以public继承的方式继承protect_son_class和private_son_class且将其实例化
在这里插入图片描述
我们发现,新派生类仍然可以访问其父类成员,可是类外无法访问
私有类同理,因此派生类可以访问自己任意上层的保护成员(前提没有私有继承)。

1.2.2.4友元

友元(friend),包括友元函数和友元类,可以访问友好关系的私有成员,就好像朋友一样,其他的不算,但是朋友可以访问。
我们可以建造一个技能(imporved_boss)展现Boos的权能,
在MonsterBoss.h内,添加:

friend void attack_boss(MonsterBoss& boss);	
//声明attack_boss为MonsterBoss的友函数

友行的具体使用可以看下节。

1.2.3 多态

多态(polymorphism)是面对对象程序的重要特征,如果没有,一般不能称之为OOP,C++支持多态性,其实,函数的重载,运算符的重载都属于多态性的体现,多态性分为两类,静态多态和动态多态,分别是编译的时候和运行的时候,动态多态性多见于虚函数(virtual function)

动态多态表现为 可以把父类的指针指向子类的对象,程序运行时回根据子类对象的实际类型调用具体的函数。 这一行为通过虚指针实现,有虚函数的类在被示例化时,会生成一个虚指针,虚指针指向虚表地址,虚表中存放了虚函数的地址,因此父类指针能直接调用到子类的虚函数。
该类的所有实例化对象共用一张虚表,虚表在编译时就被生成。拥有虚函数的父类其析构函数必须为虚函数,否则释放时会导致内存泄露。

下面的代码,让我们展现多态性以及虚函数的使用 ,我们全部重写上面的代码,使他具有一定的逻辑,Main.cpp是源代码,将Monster.h和MonsterBoss.h合并,Monster.h储存着Monster类、和MonsterBoss类继承Monster。
Main.cpp包含:

//main.cpp   author hebness
#include<stdio.h>
#include"Monster.h"
//#include"textClass.h"
#include<windows.h>

//
Monster master(1000, 100, "1", "大师", "有点难打哦");
//master.Isdead(100);
Monster noob(100, 5, "2", "菜鸟", "没啥攻击力,战五渣");
MonsterBoss noob_boss(100, 50, 100, "01", "菜鸟king", "有点攻击力,战五十");
MonsterBoss* noboss = nullptr;
Monster* nomonster = nullptr;
Monster* monster = nullptr;

void intt()
{
	return;
}

void battle(int Hp, int Atk,MonsterBoss &boss,Monster &Mstr)
{
	if (&boss != nullptr)
	{
		monster = dynamic_cast<MonsterBoss*>(monster);
		monster = new MonsterBoss();
		*monster = boss;
	}
	else if(&Mstr != nullptr)
	{
		monster = dynamic_cast<Monster*>(monster);
		monster = new Monster();
		*monster = Mstr;
	}

	std::cout << "在你面前的是" << monster->Monster::MonsterName << std::endl << "它:" << monster->Monster::MonsterInfo << std::endl;
	//std::cout << "血量";
	//monster->Monster::ShowHp();
	puts("\n你发动了攻击");
	if (monster->Isdead(Atk))
	{
		std::cout << monster->Monster::MonsterName <<"已死亡" << std::endl;
		//释放空间
		delete monster;
	}
	else
	{
		std::cout << "剩余血量:";
		monster->ShowHp();
		std::cout << std::endl<<"怪物发动了攻击\n";
		if (Hp - monster->GetAtk() <= 0)
		{
			std::cout << "....你死了"<<std::endl;
				return;
		}
		system("pause");
		system("cls");
		std::cout << "你的攻击是: " << Atk << "    生命值还剩: " << Hp<<std::endl;
		battle(Hp - monster->GetAtk(),Atk,*noboss,*nomonster);
	}
	return;
}

int main()
{
	int atk, hp , model;
	std::cout << "输入你的生命值" << std::endl;
	std::cin >> hp;
	std::cout << "输入你的攻击伤害" << std::endl;
	std::cin >> atk;
	error_number:
	std::cout << "选择你挑战的怪物" <<"1.大师"<<"2.菜鸟"<<"3.菜鸟Boss" << std::endl;
	std::cin >> model;
	switch (model)
	{
	case 1 :
		battle(hp, atk, *noboss, master);
		break;
	case 2 :
		battle(hp, atk, *noboss, noob);
		break;
	case 3 :
		battle(hp, atk, noob_boss, *nomonster);
		break;
	default:
		goto error_number;
	}

	
	return 0;
}

Monster.h包含

#pragma once
#include<iostream>
#include<string.h>

class Monster
	//简单定义一个怪物的信息
{
	//公开怪物基本信息
public:
	
	char MonsterId[16];
	char MonsterName[50];
	char MonsterInfo[500];
	Monster(int h, int a,const char *Id,const char *Name,const char *Info) :
		Hp(h), Atk(a){
		strcpy_s(this->MonsterId,Id);
		strcpy_s(this->MonsterName, Name);
		strcpy_s(this->MonsterInfo, Info);
	}
	//声明一个有参的构造函数,用参数的初始花对数据成员初始化
	Monster() {};
	//声明一个无参的构造函数

	~Monster()
	//定义析构函数
	{
		
		puts("Destructor called");
	}

	//添加虚函数

private:

	mutable int Hp;
	mutable int Atk;

public:
	//因为Boss和普通小怪的判断不同,所以我们可以将基类Monster中判断死亡的函数重调
	virtual bool Isdead(int damage)
	{
		int hp = this->Hp;
		if (hp - damage > 0)
		{
			this->Hp = hp - damage;
			return false;
		}
		else
		{
			this->Hp = 0;
			return true;
		}
	}

	void SetHp(int Hp)
	{
		this->Hp = Hp;
		return;
	}

	void SetAtk(int Atk)
	{
		this->Atk = Atk;
	}

	void ShowHp() const
	{
		std::cout << this->Hp;
		return;
	}

	void ShowAtk() const
	{
		std::cout << this->Atk;
		return;
	}

	int GetHp() const
	{
		return this->Hp;
	}

	int GetAtk() const
	{
		return this->Atk;
	}
};


class MonsterBoss :public Monster
	//public为继承方式,定义Boss信息
{
private:

	int df;
	int improved_boss_sleep = 3;
public:

	MonsterBoss(){};
	MonsterBoss(int df, int atk, int hp, const char* id, const char* name, const char* info) :df(df)
	{
		strcpy_s(this->MonsterId, id);
		strcpy_s(this->MonsterInfo, info);
		strcpy_s(this->MonsterName, name);
		this->df = df;
		this->SetHp(hp);
		this->SetAtk(atk);
	};

	//声明attack_boss为MonsterBoss的友函数
	friend void imporved_boss(MonsterBoss& boss);

	bool Isdead(int damage)
	{
		if (df - damage > 0)
		{
			df -= damage;
			std::cout << "未击破护甲,护甲还剩余:" << df << std::endl;
			return false;
		}
		else if (df == 0)
		{
			if (improved_boss_sleep != 0)
			{
				puts("Boss正在恢复实力");
				improved_boss_sleep--;

			}
			//没有防御值了Boss展现威能,因为对象是自己本身,我们可以this引用
			if (improved_boss_sleep == 0)
			{
				imporved_boss(*this);
				improved_boss_sleep = 3;
			}
			return this->Monster::Isdead(damage);
		}
		else
		{
			puts("护甲击破!!!");
			int remain_df = damage - df;
			df = 0;
			return this->Monster::Isdead(remain_df);
		}
	}
};

//声明attack_boss在主函数定义其内容(重载)
//void imporved_boss(MonsterBoss& boss) {};
//重载函数imporved_boss,这里只是展现友元的作用
void imporved_boss(MonsterBoss& boss)
{
	if (boss.df == 0)
	{
		boss.df += 100;
		//当Boss失去它的防御值后,它的血量将会提升10的同时防御回调100,并且伤害增加1
		boss.Monster::SetHp(boss.Monster::GetHp() + 10);
		boss.Monster::SetAtk(boss.Monster::GetAtk() + 1);
		//同时输出,Boss增强了
		std::cout << boss.MonsterName << "感到了威胁,变得更强了!"<<std::endl;
	}
	return;
}

这里可以实现一个小小的游戏,实际上,怪物信息可以读取至数据库,在添加玩家信息等进行管理,这里只简单体现虚函数友元的作用。
友元
Monster.h中我们可以看到类Monsterboss里面添加了新的声明

friend void imporved_boss(MonsterBoss& boss);

使其可以在类外可以调用来自MonsterBoss类的私有成员,不仅如此保护成员照样可以用。
虚函数
同时,在Monster类和MonsterBoss类中,均使用了Isdead函数,但两者处理不同,可以看到,小怪和Boss的区别就是小怪没有护甲,其他都是公有的。要想实现小怪和Boss的死亡判定,我们可以:

  1. 命名两个新指针,分别指向Monster和MonsterBoss
  2. 命名两个新对象,分别复制于新建的两种类型的怪物
  3. 使用虚函数

第一第二个方法都很好理解,但是在大多数应用场景中,一个基类可以诞生多个派生类,多个派生类(包括基类和它本身的派生类)都使用同名的函数但执行不同的效果,这显然第一个和第二个(尤其第二个)要造成巨大的内存损耗,因此,使用虚函数便能很好的解决这个问题,上面的代码中,如果我们使用的是boss类型的,判断的时候会自动调用命名域为MonsterBoss类的Isdead函数,同样的使用小怪,则是Monster中的Isdead函数。这样大大减少了空间损耗,也减少了阅读代码的难度。
虚基类
当我们将一个派生类有多个基类,这些基类又有一个共同的基类,比如说,多边形有边数,

1.2.4 抽象

特别的,关于面对对象的特性,我们可以谈到抽象,参考上面代码,如果一个类存在至少一个虚函数为纯虚函数时,它便是抽象类。
纯虚函数,简单理解,就是将一个函数赋值,例如将Monster类中的IsDead()的{}内容删掉,换成=0,它边成为了一个抽象类。
注意
当你这样修改了之后,运行主函数时候,便会报错,因为

抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。这里很类似于Java。

1.3 智能指针

面试常问)在我们使用指针的时候,大肆使用时,很容易产生野指针,悬空指针,还有内存泄漏,C++11因此引入了智能指针来管理内存,分别为:

#include<memory>
//注意头文件
auto_ptr
unique_ptr
shared_ptr
weak_ptr

1.3.1 auto_ptr

auto_ptr是C++ 98 定义,可以将new(直接、间接均可)获得的地址赋予给这个对象,当对象过期时候,其析构函数将使用delete来释放内存(好耶,不要delete了)
下面我来展示用法

	std::auto_ptr<bgpic>newbg(new bgpic(100,100));
	newbg->ShowArea();
	std::auto_ptr<photo>uphoto(new photo(50,50));
	uphoto->ShowArea();

对象bgpic和uphoto很简单,就是返回一个面积,他们继承于一个抽象基类pic,只包含于它的长宽以及一个展示函数和得到面积的纯虚函数。智能指针auto_ptr一般使用三个函数(以上面代码为base):

代码作用使用方式
get()获得地址bgpic *p = newbg.get()
release()取消对new内存的托管*bgpic p = newbg.release()
reset()重置,原对象将会被析构newbg.reset(bew bgpic(1,1))

第三种最常用,可以自动delete

  • 不要将智能指针指向另一个智能指针

  • List item

建议使用unique_ptr

1.3.2 unique_ptr

因为auto_ptr 如果有两个指针,指向不同的地址:

//ptr1、ptr2两个不同的auto_ptr指针
ptr1 = ptr2;

结果,ptr1原先的对象被析构了,这是我们想要的,但是ptr2的未析构的同时指向了空地址,这显然不好,因为造成了内存泄漏。同时ptr1和ptr2如果指向的是STL容器,容器内的元素也必须可赋值和可复制。
unique_ptr的使用和auto_ptr基本一致:

	std::unique_ptr<bgpic>newbg(new bgpic(100,100));
	newbg->ShowArea();
	std::unique_ptr<photo>uphoto(new photo(50,50));
	uphoto->ShowArea();
  1. unique_ptr 不允许两个指针指向同一个资源(和auto一致 ) unique = 独一无二
  2. 在让容器保存指针是安全的
  3. 但如果保存了一个对象的指针,unique_ptr本身离开作用域时会自动释放它指向的对象
  4. 允许临时右值赋值构造和赋值,使用move,可以体验和auto一样的效:newbg = std::move (photo)
  5. 支持对象数组管理(auto_ptr不能)。

针对他们的排他性,可以使用shared_ptr,(share 分享):

1.3.3 shared_ptr

使用方式仍然类似上两者,但是可以共同托管一个地址,也可以调用make_shared来初始化对象。
可以调用use_count()函数可以显示当前指针的引用计数。

	std::shared_ptr<bgpic>newbg(new bgpic(100,100));
	std::cout << "use_count = " << newbg.use_count() << std::endl;
	std::shared_ptr<bgpic>uphoto(newbg);
	std::cout << "use_count = " << newbg.use_count() << std::endl;
	std::cout << "use_count = " << newbg.use_count() << std::endl;

在这里插入图片描述
可见,当第一创建shared_ptr指针的时候,指向新对象的指针为1,
赋值之后,就变成2了。
但是复制不同
稍微修改下上面代码:

	std::shared_ptr<bgpic>newbg(new bgpic(100,100));
	std::shared_ptr<bgpic>uphoto(new bgpic(100, 100));
	bgpic* p = newbg.get() ;
	std::cout << "use_count = " << newbg.use_count() <<"   nphoto address: " << p << std::endl;
	newbg = uphoto;
	std::cout << "use_count = " << newbg.use_count() << std::endl;
	std::cout << "use_count = " << uphoto.use_count() << "   nphoto address: " << p << std::endl;
	p->ShowArea();
	return 0;

在这里插入图片描述
正常输出对吧?其实并没有,因为ShowArea()会将当时的面积打印,按逻辑讲,是会显示10000,
Vs也报错:

在这里插入图片描述

this-> 是 0xFFFFFFFFFFFFFFFF。

newbg原来对象已经被析构了,以至于我们原先用于追踪它的指针指向了一个没有意义的地址空间,因此报错。
但是我们可以交换对象

	std::swap(newbg,uphoto);
	//亦或是
	newbg.swap(uphoto);

尽量不要循环使用shared_ptr

1.3.4 weak_ptr(弱指针)

这是用来配合shared_ptr的一种智能指针,因此:
weak_ptr只可以从一个shared_ptr或另一个weak_ptr构造

	std::shared_ptr<bgpic>newbg(new bgpic(100,100));
	std::shared_ptr<bgpic>uphoto = std::make_shared<bgpic>(50,50);
	//定义一个弱指针,用newbg构造,下面的用刚定义的弱指针构造
	std::weak_ptr<bgpic>weakptrdemo(newbg);
	std::weak_ptr<bgpic>ifromweakptrdemo(weakptrdemo);

弱指针可以使用use.count()函数
弱指针无法使用 * 和->对指针的访问
可以调用lock()来转化为共享指针: weakptrdemo.lock() 为共享指针,可以赋值和复制。

expired函数
它的作用是用于weak_ptr指针是否含有托管的对象,如果没有,则会返回false 反之,返回true(bool变量)。我们用代码来实现这个功能。

	std::shared_ptr<bgpic>newbg(new bgpic(100,100));
	std::shared_ptr<bgpic>uphoto = std::make_shared<bgpic>(50,50);
	//定义一个弱指针,用newbg构造
	std::weak_ptr<bgpic>weakptrdemo(newbg);
	std::cout << "weakptrdemo 's use_count() = " << weakptrdemo.use_count() << "  weakptrdemo 's expired() = " << weakptrdemo.expired() << std::endl;
	//重置newbg分享指针
	newbg.reset();
	std::cout << "weakptrdemo 's use_count() = " << weakptrdemo.use_count() << "  weakptrdemo 's expired() = " << weakptrdemo.expired() << std::endl;

在这里插入图片描述
可见,我们也可以使用use.count()!=0来判断。

1.3.4 智能指针总结

  1. 不能用多个智能指针管理一个普通指针(会玩坏的)
  2. 可以使用release()函数来进行函数保护
  3. 不要主动delete智能指针返回的地址
  4. 不要用智能指针的地址来初始化另一个智能指针

1.4 cast类型

**转换(cast)**可以将一个类型转换为另一个类型,目前有四种转换:dynamic_cast、static_cast、reinterpret_cast、const_cast。

1.4.1 dynamic_cast

动态转换(dynamic_cast)用于基类指针或引用转换为派生类指针或引用,上行转换安全,下行转换较安全(错误会返回NULL),但不同于下者的静态转换,它不可作用域基本数据类型(int,double等)。
dynamic_cast会在程序运行的时候进行类型检查,失去精度或不安全的转换均无法进行,什么是不安全的转换?例如,派生类指针转换为基类指针(派生类有相关补充),这会非常不安全,就好像在一些游戏中(例如Minecraft)高版本的存档运行于低版本中易出现崩溃性错误,反之则很少出现。
动态转换在无法转换的时候会返回NULL因此我们可以用try catch 来检测是否

pic* testptr; 
	testptr = new pic;
	//动态转换基类对象Testptr于它的派生类bgpic,当转换失败,会返回NULL,如果是引用转换失败,则是抛出异常
	try {
		bgpic* p = dynamic_cast<bgpic*>(testptr);
		//当p为空,抛出异常
		if (p == nullptr)throw testptr;
	}
	catch (pic *p)
	{
		std::cout << "error !can't cast ptr ,cast pter addree is null , ptr address is :" << p << std::endl;
		return 0;
	}

try catch是处理异常的,类似Java的同名操作,当转换出现异常,将会返回NULL,但如果是引用则会抛出异常
在这里插入图片描述
dynamic_cast有它自己的缺点,因为在运行的时候检查,所以会有一定的性能消耗,当然,我们也可以使用static_cast

1.4.2 static_cast

**静态转换(static_cast)**是一种较安全的转换方式,它会在编译时候检查转换的合法性,通常用于基类指针或引用转换为派生类指针或引用( 向上转换[安全]{和动态转换是一样的}或者向下转换[不安全]),或者具有相关转换操作符。

static_cast 运算符可用于将指向基类的指针转换为指向派生类的指针等操作。 此类转换并非始终安全。
通常使用 static_cast 转换数值数据类型,例如将枚举型转换为整型或将整型转换为浮点型,而且你能确定参与转换的数据类型。
微软文章,点击访问

代码模拟如下

//声明了一个pic类型的testptr
	pic* testptr; 
	testptr = new pic;
	//强制转换testptr,并进行重载,static_cast返回值新的地址
	testptr = static_cast<bgpic*>(testptr);
	*testptr = bgpic(1, 1);
	testptr->ShowArea();

在这里插入图片描述
因为static_cast是编译时检查,因此,运行开销相对动态转换要少。

1.4.3 reinterpret_cast

重新解释转换(reinterpret_cast) 适用于执行一些低级别,不安全的类型转换,它较比其他的cast提供更少的类型检查,几乎可以将任何指针类型转换为其他指针类型,用法和上两者一样。
当然,它很不安全,它不坚持,在上两者cast能适用的情况,请谨慎使用reinterpret_cast
用法:

	//声明了一个pic类型的testptrz指针
	pic* testptr = new pic;
	//重新解释转换
	bgpic* newptr = reinterpret_cast<bgpic*>(testptr);

1.4.4 const_cast

const_cast它能在有常量和没常量的重载函数进行转换
例如我们简单定义两个函数

void function(int *a)
{
	std::cout << "done : " << *a <<std::endl;
	return;
}
int main()
{
	const int *A = new int(1000);
	//错误 C++ 类型的实参与 类型的形参不兼容
	//function(A);

	//常量转换,可以使用函数function进行打印
	function(const_cast<int*>(A));
	return 0;
}

1.4.5 typeid

每次转换时,我们可以使用typeid返回名称
使用方式typeid(变量).name()
当然也可以typeid(校对变量) == typied(你想要核对的变量)
使用及其验证方式如下

		int a = 1, aa = 2;
	double b = 2, bb = 2;
	const int aaa = 3;
	const double bbb = 3;
	pic *aaaa = new pic;
	bgpic* bbbb = new bgpic(1, 1);
	std::cout << "a type is " << typeid(a).name() << std::endl;
	std::cout << "b type is " << typeid(b).name() << std::endl;
	std::cout << "aaa type is " << typeid(aaa).name() << std::endl;
	std::cout << "bbb type is " << typeid(bbb).name() << std::endl;
	std::cout << "aaaa type is " << typeid(aaaa).name() << std::endl;
	std::cout << "bbbb type is " << typeid(bbbb).name() << std::endl;
	//验证const int 和 int 在type是否一致
	if (typeid(a) == typeid(aaa))puts("const int type == int type");
	else puts("const int type != int type");

运行结果:
在这里插入图片描述
注意
cosnt修饰的变量类型和相同的没修饰const的变量类型是一致的。

1.5 const修饰符

又称常量即便类中拥有private保护数据,但是实参与形参、变量与其应用、数组与指针以至于可以在布胡同的场合通过不同的途径访问同一个对象,这将会造成想保护的数据改变,那么要保持一个数据既能共享又不能被任意修改,这是可以使用const将其定义为常量

1.5.1 常对象

让我们将master对象修改为常对象。

const Monster master(1000,100);
//等价于 Monster const master(1000,100);

注意如下面添加master.ShowHp();来显示血量,会非法,因为调用的是常对象中的非const型函数
如何使用它呢?我们可以修改Monster类,将void ShowHp()后增加 const 修饰
同时,我们也要将成员变量前面增加修饰符mutable ,也就是说,Monster私有成员变为可变的数据成员,这样即可正常

	mutable int Hp;
	mutable int Atk;

const是函数类型的一部分具体调用可以看下方表

数据成员非const成员函数const成员函数
非const成员可以引用,可以改变可以引用,不可改变
const成员可以引用,不可改变可以引用,不可改变
const对象的数据成员不可引用,不可改变可以引用,不可改变

1.5.2 常指针

将指针前用const修饰,使该指针指向的地址不变,特别的指向对象的常指针格式可以这样:

Monster * const noob_find = &noob;

1.6 static修饰符

静态(static),区别于动态(dynamic),简单来讲,内存分为三个类 栈区(stack), 堆区(heap) 和静态区 (static),栈区用于存放临时数据,堆区用于存放动态数据,而静态区,就是不会变,会在程序结束后释放。
如一个代码


//全局变量,会一直到程序结束
const int N = 1;
void f1() {
	static int a = 10;
	void f2();
	//这里,如果我们不用static修饰,将会陷入无止境的递推,迟迟不能递归
	//静态变量,会一直到程序结束
	f2();
	std::cout << a << std::endl;
	if (--a != 0)return f1();
	else return;
}

void f2()
{
	//临时变量,当这个函数周期结束(出了栈作用域),自动销毁
	int a = 10;
	std::cout << a<<" ";
	return;
}

int main()
{
	//动态分配,我们可以delete它,我们给一个无类型指针分配10字节空间,new也属于动态分配
	void* p = (void*)malloc(10);
	//重置,这里用了不安全的用法
	p = (void *)realloc(p,20);
	//释放
	free(p);
	f1();
}

运行结果:
在这里插入图片描述

  • static可以将数值默认初始化为0,和全局变量一样,在静态存储区,所有字节默认值为0x00。

1.6.1 静态成员

在多个对象中有些东西可以统一,比如怪物有多个,我想记录怪物数量怎么办?不可能一个一个核对,这对空间和时间都是莫大的浪费,C++中我们允许用static修饰从而使它的以及它本身的派生类的数据成员共有(一定要分清抽象类,不能实例化对象)。
我们重新上面关于图片的代码

#pragma once
//Mytest.h 用于简单的验证
#include<memory>

class pic
{//类似图片
private :
	char pid[101];
	char place[1000];
	int width;
	int length;
	static int countAll;
	//基类添加静态数据count 用于计数
	static void countAll_count(const int count_number)
	{
		countAll += count_number;
	}
public:
	friend class bgpic;
	friend class photo;
	virtual int GetArea() = 0;
	void ShowArea()
	{
		printf("area is %d\n", GetArea());
	}
	int GetcountAll()
	{
		return countAll;
	}
	~pic()
	{
		puts("~pic is runned");
		countAll_count(-1);
		//析构函数定义,如果这个图形被delete了,自动减少coutALL
	}

};

class bgpic : public pic
{
private:
	bgpic* NodeNext = nullptr;
	bgpic* NodeBack = nullptr;
	int No= 1;
	static int count_bgpic;
public:
	bgpic(int a, int b)
	{
		bgpic(a, b, nullptr, nullptr);
	}
	bgpic(int a, int b,bgpic* Next, bgpic* Back)
	{
		width = a;
		length = b;
		NodeBack = Back;
		NodeNext = Next;
		//如果上一个不为空,自动加一
		//if (Back != nullptr )count = Back-> count+1;
		if (NodeBack == nullptr && NodeNext == nullptr && No != 1)
		{
			std::cout << "error! no Next or Back";
			exit(0);
		}
		else
		{
			if (No != 1)Back->NodeNext = this;
		}
		No++;
		countAll++;
		count_bgpic++;
	}

	~bgpic()
	{
		//析构,首先进行首尾判断,判断是否有头或者尾
		if (NodeBack == nullptr);
		else
		{
			if (NodeNext != nullptr)
			{
				//删除对应操作,类似单链表
				NodeBack->NodeNext = NodeNext;
				NodeNext->NodeBack = NodeBack;
			}
			else
			{
				NodeBack->NodeNext = nullptr;
			}
		}
		puts("~bgpic is runned");
		count_bgpic--;
	}
	int Getbgpiccount()
	{
		return count_bgpic;
	}
	int GetNo()
	{
		return No;
	}
	int GetArea()
	{
		return width * length;
	}
	int GetCir()
	{
		return 2 *(width + length);
	}
	void ShowCir()
	{
		printf("circumference is = %d\n", bgpic::GetCir());
	}

};

class photo : public pic
{
public:
	photo(int a, int b)
	{
		width = a;
		length = b;
		countAll++;
	}
	int GetArea()
	{
		return width * length;
	}
};

int pic::countAll = 1;
int bgpic::count_bgpic = 1;

并且编写一个简单的主函数进行调用验证:

int main()
{
	bgpic* p = new bgpic(1, 1);
	bgpic* p1 = new bgpic(2, 2, nullptr, p);
	puts("创建对象p、p1,类型为 bgpic 创建成功");
	std::cout<< "这里是从对象p中调用得到的所有pic的派生类对象数量"<< p->GetcountAll() << std::endl;
	photo* p2 = new photo(1,2);
	puts("创建对象p2,类型为 photo 创建成功");
	std::cout << "这里是从对象p1中调用得到的所有pic的派生类对象数量" << p1->GetcountAll() << std::endl;
	std::cout << "这里是从对象p2中调用得到的所有pic的派生类对象数量" << p2->GetcountAll() << std::endl;
	std::cout << "这里是从对象p1中调用得到的所有bgpic及其派生类对象数量" << p1->Getbgpiccount() << std::endl;
	//countAll统计所有pic的派生类的对象的数量,注意在使用静态初始化值时,相关析构函数以及被运行了一次!
	//countbgpic统计所有bgpic类的对象及其派生类对象的数量
	delete p2;
	puts("p2已删除");
	std::cout << "这里是从对象p1中调用得到的所有pic的派生类对象数量" << p1->GetcountAll() << std::endl;
	std::cout << "这里是从对象p1中调用得到的所有bgpic及其派生类对象数量" << p1->Getbgpiccount() << std::endl;
	delete p1;
	puts("p1已删除");
	std::cout << "这里是从对象p中调用得到的所有pic的派生类对象数量" << p->GetcountAll() << std::endl;
	std::cout << "这里是从对象p中调用得到的所有bgpic及其派生类对象数量" << p->Getbgpiccount() << std::endl;
}

运行结果:
在这里插入图片描述
我们看到在开始的时候,就已经运行了static成员所处类的析构函数,那是因为在我们初始化static成员时候,一定要注意值(当我们自定义化了它的析构函数时)。我们去除static修饰的时候会产生下面的结果
在这里插入图片描述
明显乱套了,那是因为count本是公有的数据,没了static修饰,每个对象都会给它开辟空间,一定要注意在对象中,只有构造函数的时候以及静态成员函数才能引用静态数据,并且使用静态数据时候必须先将其初始化,不然会报错(Error LNK2001)无法解析的外部符号

1.7 struct与class区别

我们都知道,struct和class都可以使用public、protected、private修饰,并且在定义的时候具有十分相似的特性。那么,他们有什么区别呢?下面不同会用红色指出
这是面试常问之一

1.7.1struct

  • 结构体(struct) 的默认访问权限为 公开(public)
  • 结构体可以定义自己的数据成员和成员函数,并将它们实例化
  • struct拥有析构和构造函数,但是应用较窄
  • 结构体适用于轻量化内容

1.7.2 class

  • 类(class) 关于类的详细介绍可以查看1.1.1,默认权限为 私有(private)
  • 类可以定义自己的数据成员和成员函数,并将它们实例化
  • 类拥有析构和构造函数、应用很广
  • 类适用于实现更复杂的内容
    C++中,两者确实非常相似,但是Class具有面对对象最基本特征,关于两者的使用,需要具体考虑,具体分析,总的来说,两个都可以。

1.8 STL容器

标准模板库(Stand Template Library 简称 STL),是集合所有的C++模板类和函数,提供了一些通用并且可复用的算法和数据结构。
STL分为多个组件包括:**容器(Containers)、迭代器(Iterators)、算法(Algorithms)、函数对象(Function Objects)和适配器(Adapters)**等。

使用STL的好处:

代码复用:STL 提供了大量的通用数据结构和算法,可以减少重复编写代码的工作。
性能优化:STL 中的算法和数据结构都经过了优化,以提供最佳的性能。
泛型编程:使用模板,STL 支持泛型编程,使得算法和数据结构可以适用于任何数据类型。
易于维护:STL 的设计使得代码更加模块化,易于阅读和维护。
相关链接

1.8.1 数组 vector deque list set map unorder_map

1.8.1.1 向量(vector)

下面是使用vector的代码示例(注意头文件以及前面定义的Mytest.h文件):

int main()
{
	// 创建一个向量存储 photo*
	std::vector<photo*> vec;
	int i;

	// 显示 vec 的原始大小
	std::cout << "vector size = " << vec.size() << std::endl;

	// 推入 5 个值到向量中
	for (i = 0; i < 5; i++) {
		vec.push_back(new photo(9+i,9+i));
	}

	// 显示 vec 扩展后的大小
	std::cout << "extended vector size = " << vec.size() << std::endl;

	// 访问向量中的 5 个值,输出其面积
	for (i = 0; i < 5; i++) {
		std::cout << "the photo s length is [" << i << "] the photo s width is [" << i << "]   Area is = " << vec[i]->GetArea() << std::endl;
	}

	// 使用迭代器 iterator v 访问值
	std::vector<photo*>::iterator v = vec.begin();
	while (v != vec.end()) {
		std::cout << "value of v = " << (*v)->GetArea()<< std::endl;
		v++;
	}

	return 0;
}

vector相关用法也可以参考文章->点击链接
最主要的用法罗列(其他也如下)在下:

Vector主要成员函数作用
1. back()用于返回最后一个元素
2. assign()用于赋值 用法 vec.assign(2,3)或vec.assign(cpvec.begin,cpvec.end())
3. front()用于赋值 用法 vec.assign(2,3)或vec.assign(cpvec.begin,cpvec.end())
4. clear()清理元素
5. empty()判断是否为空,是返回true反之 false
6. pop_back()删除末尾元素
7.erase(删除首位置,删除末位置的下一位)删除指定元素
8.push_back(值)向末尾插入元素
9.insert()插入,用法:vec.insert(被插入向量首位置,插入向量首位\插入个数,插入向量末位的下一位\插入元素
10. size()返回向量元素个数
11. capacity()返回向量可放置的最大元素数量
12. resize()重置个数 用法:vec.resize(目标数量,校正值) 校正值可无,当如果是增加则添加
13. reserve(值)重置向量的可容纳最大元素量
14. swap(向量)与括号内的值进行交换
1.8.1.2 双头队列(queue)

不同于vector的单向开口,它是双向开口 因此,没有容量的概念。
使用方式如下:

//queue默认构造函数
	std::deque<photo*>new_deque;
	//判空函数
	if (new_deque.empty() == true)std::cout << "new deque has no element!" << std::endl;
	for (int i = 1; i <= 3; i++)
	{
		//双向插入操作
		new_deque.push_back(new photo(2 * i, 2 * i));
		new_deque.push_front(new photo(1 * i, 1 * i));
	}
	
	//使用迭代器遍历
	std::deque<photo*>::iterator v = new_deque.begin();
	while (v != new_deque.end())
	{
		std::cout << "the photo area is " << (*v)->GetArea() << std::endl;
		v++;
	}
	for (int i = 1; i <= 3; i++)
	{
		//双向删除操作,不要忘了释放和删除哦
		delete new_deque.back();
		delete new_deque.front();
		new_deque.pop_back();
		new_deque.pop_front();
		std::cout << "new deque has deleted two element!" <<"now remains " <<new_deque.size()<<" element(s)" << std::endl;
	}
	//也可以直接快速使用
	//new_deque.clear()
	//new_deque.erase(new_deque.begin(),new_deque.end())
	//erase()->会返回下一个数据的位置
	return 0;
1.8.1.3 队列(list)

队列(stack)满足先进先出,不同于栈(stack)

list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。
参考链接

转载自https://c.biancheng.net/view/6892.html

1.8.1.4 集合(set)

集合(set),集合的元素不能包含重复元素,它可以维护一个有序又唯一的元素集合,set保持元素有序,set内部实现基于红黑树,是的操作的时间复杂度保持在O(log n)。
示范代码如下

//用于查找返回,分别时面积和传递过来的迭代器p,用来检验
photo* photo_find_for_set(int area, set<photo* >::iterator p)
{
	if ((*p)->GetArea() == area)return *p;
	else return nullptr;
}
int main()
{
	//set 的使用 默认构造函数进行定义,因为set.count()返回的时指定元素个数,这种结果只可能时1或者0,set具有去重性,所以我们可以简单定义一个变量存储
	static int NumSet = 0;
	set<photo* >NewSet;
	for (int i = 1; i <= 5; i++)
	{
		//insert用于插入 如果插入的元素已经存在,会忽视这个操作,emplace()也可以
		NewSet.emplace(new photo(i * 10, i * 10));
		NumSet++;
	}
	//erase用于删除,它可以通过元素删除,这里我们先删除面积为 100 的photo对象,用迭代器遍历查询
	set<photo* >::iterator v = NewSet.begin();
	//定义一个静态变量来表示存储现在的数量
	static int PhotoRemain = (*v)->GetcountAll();
	while (v != NewSet.end())
	{
		if ((*v)->GetArea() == 100 )
		{
			delete (*v);
			//通过迭代器指向位置释放 删除(注意顺序)
			NewSet.erase(*v);
			std::cout << "the photo whos area is 100 is deleted " << std::endl;
			std::cout << "now photos remain " << "" << --PhotoRemain << ",and NewSet elements remain" << --NumSet <<std::endl;
			break;
		}
		v++;
	}
	//也可以用find查找,例如这里用于删除面积为400的photo对象,这里用了一个函数来传递,v要重置指向头
	v = NewSet.begin();
	v = NewSet.find(photo_find_for_set(400,v++));
	delete (*v);
	//通过迭代器指向位置释放 删除(注意顺序)
	NewSet.erase(*v);
	std::cout << "the photo whos area is 400 is deleted " << std::endl;
	std::cout << "now photos remain " << "" << --PhotoRemain << ",and NewSet elements remain" << --NumSet << std::endl;

我们可以通过调用函数来返回一个我们想要的地址,传递当前迭代信息,进行判定,就如上代码的

photo* photo_find_for_set(int area, set<photo* >::iterator p)
{
	if ((*p)->GetArea() == area)return *p;
	//如果找出于地址相同的,则返回当前迭代器指向对象地址
	//如果找不出则返回空指针
	else return nullptr;
}

运行结果如下
在这里插入图片描述

1.8.1.5 映射(map)

**映射(map)**具有键值和值,实际上他便是一种pair。
使用方法如下

photo* photo_find_for_map(int area, map<int ,photo*>::iterator p)
{
	//原理和SET命名时一样
	if (p->second->GetArea() == area)
		return p->second;
	else   return nullptr;
}
int main()
{
	//set的使用 构造函数如下,第一个为键,第二个为值,注意键不可重复!:
	map<int, photo*>Newmap;
	for (int i = 1; i <= 5; i++)
	{
		//插入数据方法,第一种字最少,选它!
		Newmap[i] = new photo(1*i,1*i);
		//也可以使用 Newmap.insert(pair<int,photo*>(i,new photo(1*i,1*i)))
		//或是: Newmap.insert(map<int,photo*>::value_type(i,new photo(1*i,1*i))
		std::cout << "id :" << i <<" area is " << Newmap[i]->GetArea()<<std::endl;
	}
	std::cout << " newmap maps " << Newmap.size() << std::endl;
	//find查找,和set的find一样,例如我这里删除一个键值为1的,以及面积为4的photo类型
	//定义一个迭代器v,最好定义一个初值
	map<int, photo* >::iterator v = Newmap.begin();
	//删除键值为1的
	while (v != Newmap.end())
	{
		if (v->first == 1 || v->second == photo_find_for_map(4, v))
		{
			std::cout << "now delete id :" << v->first << " area is " << v->second->GetArea() << " newmap maps " << Newmap.size() << std::endl;
			delete v->second;
			//要用v来接受返回值,如果不接的化会报错!!!
			v = Newmap.erase(v);
			continue;
		}
		v++;
	}
	return 0;
}

注意,在使用删除函数的时候,map.erase()需要有迭代器来接应返回值(老版本也许不用),VS2022需要。

1.8.1.6 无序映射(unorder_map)

无序映射(unorder_map),就是无序map容器,它于map不同于一点,便是unordered_map时无序的,底层是用哈希表实现,而map为红黑树。
定义模板如下所示:

template < class Key,                        //键值对中键的类型
           class T,                          //键值对中值的类型
           class Hash = hash<Key>,           //容器内部存储键值对所用的哈希函数
           class Pred = equal_to<Key>,       //判断各个键值对键相同的规则
           class Alloc = allocator< pair<const Key,T> >  // 指定分配器对象的类型
           > class unordered_map;

代码模拟如下:


	//无序映射定义,
	std::unordered_map<int,photo* >unmap;
	for (int i = 1; i <= 5; i++)
	{
		unmap.insert(std::pair<int, photo*>(i, new photo(i * 2, i * 2))) ;
		//也可以跟map一样赋值
	}
	//删除 键值为1的 可以auto n = unmap.erase(1);
	for (auto x : unmap)
	{
		std::cout << " value is " << x.first << " its area is " <<x.second->GetArea()<< std::endl;
	}
	

1.8.2 vector 自动扩容原理

vector容器提供动态数组,它的底层实现是通过连续的内存块储存元素,使其能在O(1)的时间复杂度下进行访问。

当创建一个std::vector对象时,它会分配一块初始大小的内存空间来存储元素。这个初始大小可以通过构造函数的参数指定,如果没有指定,默认为0。std::vector还会维护两个重要的变量:size和capacity。size表示当前已存储元素的数量,而capacity表示当前分配的内存空间的大小。
当我们向std::vector中添加元素时,它会首先检查是否有足够的容量来存储新的元素。如果当前容量不足,std::vector就需要进行扩容操作。
std::vector的扩容机制是通过重新分配内存来实现的。当容量不足时,std::vector会分配一个更大的内存块,并将原来的元素复制到新的内存中。通常,新的容量会比原来的容量大一些,以便减少频繁的扩容操作。具体来说,std::vector通常会将容量扩大为原来的两倍,但这并不是绝对的,具体实现可能会有所不同。
在进行内存重新分配时,std::vector会调用元素类型的拷贝构造函数来复制元素。如果元素类型没有提供拷贝构造函数,则无法使用std::vector存储该类型的对象。
一旦完成内存的重新分配和元素的复制,std::vector会释放原来的内存,并更新size和capacity的值。这样,我们就可以继续向std::vector中添加新的元素了。
文章链接

1.8.3 map 与 unorder_map 区别 以及应用场景

mapunorder_map
map是基于红黑树实现的关联容器,它会根据key的值进行自动排序是基于哈希表实现的关联容器,它不会对key进行排序,而是根据哈希函数直接定位元素。
map中的元素是按照key值从小到大排列的,因此可以快速查找元素,但是插入和删除操作的时间复杂度相对较高中的元素存储位置是无序的,因此插入和删除操作速度较快,但无法保证元素的顺序
适合有序的数据访问,对元素的插入和删除次数较少的情况适合对元素的插入和删除操作频繁,而不关心元素顺序的情况
应用场景
如果需要保持元素有序,并且对元素的查找操作较多,可以选择使用map。
如果对元素的插入、删除操作较频繁,并且不需要保持元素有序,可以选择使用unordered_map。
总的来说,如果对元素的访问是有序的、查找操作较多,并且对内存占用要求不那么苛刻,可以选择map;如果对元素的插入、删除操作较频繁,不关心顺序、并且对查找操作有一定要求,可以选择unordered_map。

1.9 其他

1.9.1 多线程编程 mult_thread

待完善 具体参考->点击链接

1.9.2 C++防止重复包含 #ifdef #define #endif 关键字

在c/c++编程中,头文件常用于定义函数声明,宏,常量,对象等,以供其他源文件使用,如果不小心处理,可能会重复包含头文件,比如说,1.cpp 包含 1.h 和2.h 而且1.h包含 2.h
显然有重复
例如我们创建一个新头文件 MyTest_.h

//这个也是避免重复包含的方式
#pragma once

//这个也是避免重复包含的方式
#ifndef TEST
#define TEST
//这里TEST 是一个宏,用于标识头文件是否被包含,如果它没被定义,就会被定义,然后执行后面的代码,如果已经定义
//处理器会忽略后面的代码

//这里用来放你定义的代码啦

#endif // !TEST 当你创建ifdef系统自动给解释 !TEST 这便是上面说的意义

具体可以查看 点击链接

2 数据结构与C++

在这里,我们主要讨论的是算法,需要有基本的数据结构(数据结构这些都应掌握)知识,以及其他的基本计算机知识。
使用C++,并且会围绕游戏内容展开。

  • 待完成,还会添加不断完善

2.1 树

2.2 图论

2.3 排序算法

排序算法具体可以分以下十种,我们将会将它们的代码给模拟出来:

2.3.1 冒泡排序

冒泡排序是一种从后往前遍历的算法,它是一种稳定的算法
它的思路是从前往后遍历,遍历的途中比较并且交换,每一次遍历将会把该次遍历的尾标取到正确位置,这是一种稳定的算法,代码模拟如下:

//冒泡排序,含义单指针交换,两次循环
void Bubble_Sort(vector<int>& Array)
{
	int Length = Array.size();
	int mid;
	//从后向前遍历,寓意尾标 
	for (int i = Length -1; i >= 1; i--)
	{
		for (int j = 0; j <= i - 1; j++)
		{
			if (Array[j] > Array[j + 1])
			{
				//正序交换
				std::swap(Array[j], Array[j + 1]);
			}
		}
	}
	//空间复杂度为O(1),时间复杂度为 O(n2)
	return;
}

2.3.2 选择排序

选择排序的思路十分简单,这里就不提供代码演示了,思路是:遍历,每次遍历将该次遍历的最小位置给排好,这是一种稳定的算法。

2.3.3 插入排序

插入排序的思路也十分好理解,从正序遍历一次,遍历途中比较交换,只不过交换后要进行数组操作,这是一种稳定的算法,代码实现如下:

void Insert_Sort(vector<int>& Array)
{
	int Length = Array.size();
	//交换下标 cnt 从 1 开始
	int cnt = 1 , mid;
	for (cnt; cnt < Length; cnt++)
	{
		mid = Array[cnt];
		//从元素后向前比较交换, 当然从头也可以,注意限制符
		//实际上这是一次查找过程,可以进行类似于二分的优化
		for (int i = cnt - 1; i >= 0; i--)
		{
			//刚开始比就比到了,可以直接break
			if (mid >= Array[i] && i == cnt - 1 )break;
			//当 i = 0意思时比到头了,如果此时还未插入,那他插入表头
			if (i == 0 && mid < Array[i] )
			{
				for (int j = cnt - 1; j >= 0 ; j--)
				{ 
					 Array[j + 1] = Array[j] ;
				}
				Array[0] = mid;                              
				break;
			}
			if (Array[i] > mid && Array[i - 1] <= mid)
			{
				//查找到位置标 i 也就是插入到I中;
				for (int j = cnt - 1; j >= i; j--)
				{
					Array[j + 1] = Array[j];
				}
				Array[i] = mid;
				break;
			}
		}
		//空间复杂度O(1),时间复杂度O(n 2)
	}
}
2.3.4 快速排序

快速排序的思路的核心右三个,分别是:左指针(left)右指针(start)基准值(base),具体思路具有分治的思想,这是一种不稳定的算法,它会改变元素的相对位置,C++代码如下:

//快速排序
void Quick_Sort(vector<int>& Array , int start , int end )
{
	if (start < end)
	{
		int base = Array[start];
		int left = start;
		int right = end;
		while (left < right) {
			//右指针开始查找
			while (left < right&& Array[right] >= base)
			{
				right--;
			}
			//交换
			Array[left] = Array[right];

			//左指针开始查找
			while (left < right && Array[left] <= base)
			{
				left++;
			}
			Array[right] = Array[left];
		}
		//出循环时候,指针已经相互相等,将base重新带入
		Array[left] = base;
		//排列比base 小的数字和数组
		Quick_Sort(Array, start, left - 1);
		Quick_Sort(Array, left + 1, end);
	}
	//空间复杂度为O(log N),平均时间复杂度为O(Nlog N)
}

2.4 游戏常用算法

2.4.1 寻路算法

2.4.2 碰撞检测算法

2.4.3 物理模拟算法

2.4.4 音频算法

2.4.5 随机数算法

2.4.6 AI算法

3 COCOS 引擎

  • 待完成

3.1 COCOS引擎界面

作者的版本为 Cocos creator 3.8

Cocos Creator 编辑器由多个面板组成,面板可以自由移动、组合,以适应不同项目和开发者的需要。

例如新建的HelloWorld项目界面:
在这里插入图片描述

3.1.1 层级管理器

在这里插入图片描述

层级管理器 面板上主要包括 工具栏节点列表
两部分内容,用于展现当前场景中可编辑的节点之间的关系。场景中仍有一些不可见的私有节点,不会在此显示。

简单的粘贴复制一个士兵(solder),可以在层级编辑器处选取再在屏幕拖动,为了便于理解,简单拖动就好
在这里插入图片描述
面板支持的键盘快捷方式

复制:Ctrl/Cmd + C粘贴:Ctrl/Cmd + V克隆:Ctrl/Cmd + DCtrl + 拖动节点
删除:Delete上下选择:上下箭头节点的折叠:左箭头节点的展开:右箭头
多选:Ctrl or Cmd + 点击连续多选:Shift + 点击重命名:Enter/F2取消输入:Esc

3.1.2 资源管理器(ASSETS)

在这里插入图片描述
Assets可以显示资源,可以使用
internal是默认内置资源(只读),内置资源可以复制,但不能进行增删改操作。

3.1.3 场景编辑器

在这里插入图片描述

场景编辑器 是内容创作的核心工作区域,用于选择和摆放场景图像、角色、特效、UI 等各类游戏元素。在这个工作区域内可以选中并通过 变换工具 修改节点的位置、旋转和缩放等属性,并可以获得所见即所得的场景效果预览。

工具栏
在这里插入图片描述
分别为平移、缩放、旋转、吸附、锚点/中心、本地/世界坐标系
具体可见官方文档https://docs.cocos.com/creator/manual/zh/editor/scene/
特别的关于2d的内容,工具对其与分布中可参考文档

3.1.4 动画编辑器

3.1.5 属性检查器

3.1.6 项目预览

3.2 设计模式

3.2.1 单例模式

3.2.2 观察者模式

3.2.3 工厂模式

3.2.4 MVC

3.3 cocos内存管理机制

3.4 引用计数法

3.5 cocos渲染机制

4 基于C 的Lua 脚本语言

参考文献

https://github.com/huihut/interview?tab=readme-ov-file#cc
C/C++ 技术面试基础知识总结,包括语言、程序库、数据结构、算法、系统、网络、链接装载库等知识及面试经验、招聘、内推等信息

https://docs.cocos.com/creator/manual/zh/
Cocos Creator 3.8 用户手册

《C++面对对象程序设计》—谭浩强 清华大学出版社

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值