C++ 学习笔记

1. c++变量

常见数据类型:

数据类型 说明 字节数
int 整型 4字节
short 短整型 2字节
long 长整型 4字节
long long 长整型 8字节
float 单精度浮点型 4字节,6位有效数字
double 双精度浮点型 8字节,10位有效数字
long double 扩展双精度浮点型 16字节,10位有效数字
char 字符型 1字节
bool 布尔型 1字节,true or false
  • 数据可以有正负,最高位表示正负,因此int整型表示的最大范围是-2^31 ~ 2^31即-2147483648 ~ 2147483648。

  • 数据也可以没有正负,即无符号数,用unsigned表示,此时数据表示的数永远是非负的,如unsigned int表示的范围是0 ~ 2^32即0 ~ 4,294,967,296。

char a = 'A';  //在内存中存储的是A对应的ASII码值
std::cout << a << std::endl;
a = 50;  //此时字符a表示ASII码50对应的字符即2
std::cout << a << std::endl;
  • 数据类型之间的最大区别是创建该类型的变量时分配的内存大小
float b = 5.5;  // 变量b其实是双精度浮点型即double;
float c = 5.5f;  // 变量c此时是单精度浮点型即float;
float d = 1f;  //错误
double var = 5.2;
  • sizeof()可以查看数据占用的字节数。
bool variable1 = false;
std::cout << sizeof(variable1) << std::endl;  // 1

2. C++函数

  • 函数的定义:返回类型、传入参数、函数体、返回值。
int Multiply(int a, int b)
{
	return a * b;
}
  • 只有main函数可以不设置返回值(main函数默认返回0),其他指定了返回类型的函数必须要设置返回值。

  • 在Debug模式下,指定了返回类型的函数必须要设置返回值,否则或出现“未定义行为”错误,但在Release模式下函数不设置返回值也不会报错。

  • 函数通常分为定义和声明,声明通常存储在头文件中,定义则在C++源文件或者翻译单元中编写。

3. C++头文件

头文件通常用于声明某些类型的函数,以便能够在程序中使用

如果在一个文件中定义函数, 当想在另一个文件中使用它时,在编译该文件时,C++此时不知道这个函数存在,将会出现错误,因此需要头文件这一公共区域来存储函数的声明,告诉编译器这些函数存在。

如果调用一个在其他源文件中函数定义的函数时,需要先声明该函数,或者包含声明了该函数的头文件

void Log(const char* message);  //调用前先声明

void InitLog()
{
	Log("Initializing Log!");
}
  • 带#的都是预处理命令,它将会在编译此文件之前先被处理

  • include预处理命令具有复制和粘贴的能力,它把包含的头文件复制粘贴入另一文件中。

  • #pragma once表示只包含该头文件一次#pragma once监督一个头文件,阻止单个头文件被多次包含,并转换为单个翻译单元。如果不小心多次包含了一个文件并转换成一个翻译单元,将会出现复制错误。

#pragma once

void Log(const char* message);
void InitLog();
struct Player{};
  • 另一种常用的防止头文件被多次包含的预处理命令是#ifndef
#ifndef _LOG_H  //如果_LOG_H没有被定义,则#ifndef和#endif中间的部分才会被包含进来
#define _LOG_H  //定义_LOG_H

void Log(const char* message);
void InitLog();

struct Player {};

#endif _LOG_H

#include头文件时,#include <xxx.h>#include "xxx.h"的区别:

  • #include <>包含文件时通常搜索的是包含路径文件夹一般是标准库头文件所在目录
  • #include ""包含文件时通常包含的是相对于当前文件的文件一般是搜索源文件的同一目录

因此<>只能包含编译器指定的包含路径的文件,而""可以包含任何文件但通常还是包含在相对路径的文件。

#include "iostream"
#include "../Log.h"

C标准库有.h后缀,而C++标准库没有。

4. C++条件分支

if语句和分支通常有较大开销,优化的代码需要特别避免分支。

if语句的语句块中只有一句时,可以不写花括号。

检验指针是否为空:nullptrNULL

const char* ptr = "Hello";
if (ptr)  //检查指针是否为空
	Log(ptr);

const char* ptr1 = nullptr;
if (ptr1 != nullptr)  // 或 ptr1 != NULL
	Log(ptr1);
else
	Log("Ptr is null!");	

if...else if...else...语句中,只有if语句条件判定失败才会执行else if并再进行条件判定,如果再失败则执行else语句;若if语句判定成功,则会直接跳过else if和 else,无论它们的条件判定是否成功。

const char* ptr = "Hello";
if (ptr) 
	Log(ptr);
else if (ptr == "Hello")
	Log("ptr is Hello!");
else
	Log("ptr is null!");
//结果将会只打印"Hello"

else if不是C++的关键字,它实际上是一种隐藏技巧,等同于else { if ...}。

const char* ptr = "Hello";
if (ptr) 
	Log(ptr);
else if (ptr == "Hello")
	Log("ptr is Hello!");

//等同于:
const char* ptr = "Hello";
if (ptr) 
	Log(ptr);
else
{
	if (ptr == "Hello")
		Log("ptr is Hello!");
}

5. C++循环

for循环:for(变量声明;条件判定;表达式)。for循环头包括以下三部分:

变量声明:初始化声明的变量;

条件判定:条件为真则执行循环体,是一个bool值;

表达式:表达式将会在for循环的下一次迭代之前被调用;

for (int i = 0; i < 5; i++)
{
	Log("Hello world!");
}
//效果相同:
int x = 0;
bool condition = true;
for (; condition;)
{
	Log("Hello world!");
	x++ ;
	if (!(x < 5))
		condition = false;
}

while循环:

while (i < 5)
{
	Log("Hello world!");
	i++;
}

do...while()循环:do...while与while的区别是,无论条件如何,do...while()的循环体总是会先执行一次;

do
{
	Log("Hello world!");
} while (false);  
//仍然会打印一次;

6. C++控制流语句

continue:只能在循环中使用,表示继续进行循环的下一个迭代,如果还有下一个迭代的话,否则循环结束

for (int y = 0; y < 5; y++)
{
	if (y % 2 == 0)
		continue;
	Log("Hello world!");
	std::cout << y << std::endl;
}

break:可以在循环和switch语句中使用;表示跳出循环终止循环

for (int y = 0; y < 5; y++)
{
	if ((y + 1) % 2 == 0)
		break;
	Log("Hello world!");
	std::cout << y << std::endl;
}

return:可以用在任何地方;表示退出函数,return必须要返回一个指定函数返回类型的值

for (int y = 0; y < 5; y++)
{
	if ((y + 1) % 2 == 0)
		return 0;
	Log("Hello world!");
	std::cout << y << std::endl;
}

7. C++指针

指针是一个整数,一种存储内存地址的数字所有类型的指针都是保存内存地址的整数

int var = 8;
void* ptr = &var;  //&表示取内存地址

指针的类型指的是指针所保存的地址对应的数据被假设为该类型,但类型并不会改变指针的本质。

  • 空指针:将指针保存的地址置0,0是一个无效的内存地址。
void* ptr = 0;
void* ptr1 = NULL;
void* ptr2 = nullptr;
  • 逆向引用指针:*可以逆向引用指针,访问指针所指向的数据。
int var = 8;
int* ptr = &var;
*ptr = 10;   //此时var被重新写入为10
std::cout << var << std::endl;
  • 申请一定大小的内存块并返回指向内存块起始位置的指针:
char* buffer = new char[8];  //申请8字节的内存,并返回一个指向该内存开始的指针
memset(buffer, 0, 8);    //用0填充该8字节的内存块
delete[] buffer;  //删除申请的内存块

指针本身也是变量,也存储在内存中,也有内存地址,于是就可以得到双指针或者三指针,表示指向指针的指针。

char* buffer = new char[8]; 
memset(buffer, 0, 8); 
char** ptr1 = &buffer;  //指针ptr1就是双指针,它指向指针buffer

delete[] buffer;  

8. C++引用

根本上,引用通常只是指针的伪装,它只是指针上的语法糖。

引用是一种引用现有变量的方式,不像指针的是,指针可以创建一个新的指针变量,然后设置它等于空指针,但引用不能这样做,因为引用必须“引用”已经存在的变量,引用本身并不是新的变量,引用并不占内存,没有真正的存储空间。

  • 简单引用
int a = 5;
int& ref = a;  //ref就是对变量a的引用,相当于给a创建了一个别名,ref变量并不存在
//int& 整个表示类型,即引用,此处&并不表示取地址
ref = 2;
std::cout << a << std::endl;  //a=2
  • 值传参、指针传参与引用传参

值传参时,会把变量的值拷贝,然后复制给函数的形参(会创建一个新的形参变量,其值为拷贝过来的值)

void Increment(int value)
{
	value++;
}
int a = 5;
Increment(a);
std::cout << a << std::endl;  //a=5

void Swap(int a, int b)  //交换a和b的值
{
    int temp = a;
    a = b;
    b = temp;
}
int b = 3, c = 2;
void Swap(b, c);
cout << b << " " << c << end;  // 3 2,b和c的值并没有交换

指针传参时,实际上是把变量的内存地址传递给了函数的形参,这样函数将会查找内存地址,然后直接修改内存地址处的数据。

void Increment1(int* value)
{
	(*value)++;
}
a = 5;
Increment1(&a);
std::cout << a << std::endl;  //a=6

void Swap1(int* a, int* b)  //交换a和b的值
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
int b = 3, c = 2;
void Swap1(b, c);
cout << b << " " << c << end;  // 2 3,b和c的值交换了

引用传参时,是将变量的引用传递给了函数的形参

void Increment2(int& value)
{
	value++;
}
a = 5;
Increment2(a);
std::cout << a << std::endl;  //a=6

void Swap2(int& a, int& b)  //交换a和b的值
{
    int temp = a;
    a = b;
    b = temp;
}
int b = 3, c = 2;
void Swap2(b, c);
cout << b << " " << c << end;  // 2 3,b和c的值交换了

对于引用,没有什么是指针不能做到的,指针就像引用,只是比引用更加强大。如果可以使用引用,那么就使用引用,这会使代码更简洁简单。

一旦声明了一个引用,就不能改变它引用的变量

int b = 5;
int c = 8;
int& ref = b;
ref = c;  //这样改变引用是错误的

如果真的想更改一个引用所引用的变量,可以使用指针:

int* ref1 = &b;  //ref1指向b
*ref1 = 2;
int* ref1 = &c;  //ref1改为了指向c
*ref1 = 1;
std::cout << b << std::endl;  //b=2
std::cout << c << std::endl;  //c=1

9. C++类

C++的类只是对数据和函数组合在一起的一种方法。

  • 类的创建:

C++中使用class关键字来创建类,创建类时,类名必须唯一,因为类名就是类的类型,注意不要忘了花括号之后的分号

class Player
{
public:
    int x, y;
	int speed;
};

当创建类时,可以指定类中的可见性(public、private、static),默认情况下一个类中的所有东西都是私有的,即只有类中的函数才能访问这些变量,然而如果想要类以外的函数能够访问类中的变量时,应该把这些变量的可见性设置为public。

类中可以包含函数,类中的函数被称为方法

class Player
{
public:
    int x, y;
	int speed;

    void Move(int xa, int ya)
	{
	    x += xa * speed;
		y += ya * speed;
	}
};
  • 类的使用
Player player;  //实例化了一个Player类,实例化的类为player
player.x = 2;
player.y = 3;
player.speed = 1;  //访问player中的类变量
player.Move(1, -1);  //使用player中的方法

10. C++类和结构体的对比

类的成员可见性默认是private,而结构体的成员可见性默认是public。

实际的用法中,当只表示一些变量的结构时,尽量使用结构体;当想要大量功能或者当数据需要继承时,尽量使用类。

11. 如何编写一个C++类

class Log
{
public:
	const int LogLevelError = 0;
	const int LogLevelWarning = 1;
	const int LogLevelInfo = 2;
private:
	int m_LogLevel = LogLevelInfo;  
	//m_ 表示这是一个私有的成员变量,默认日志等级为Info
public:
	void SetLevel(int level)
	{
		m_LogLevel = level;  
	}
    void Error(const char* message)
   {
	    if (m_LogLevel >= LogLevelError)
		    std::cout << "[ERROR]" << message << std::endl;
    }
    void Warn(const char* message)
    {
	    if (m_LogLevel >= LogLevelWarning)
		    std::cout << "[Warning]" << message << std::endl;
    }
    void Info(const char* message)
    {
	    if (m_LogLevel >= LogLevelInfo)
		std::cout << "[Info]" << message << std::endl;
    }
};

int main()
{
    Log log;
	log.SetLevel(log.LogLevelWarning);
	log.Error("Hello!");
	log.Warn("Hello!");
	log.Info("Hello!");  //只打印两次Hello!
}

12. C++中的静态(Static)

C++的static关键字有两个意思,其中之一是在类或结构体之外使用static关键字,另一种是在类或者结构体内部使用static。类外面的static意味着声明为static的变量,链接将只是在内部,这意味着它只能对定义它的翻译单元可见;然而,类或结构体内部的static变量,该变量实际上将与类的所有实例共享内存,意味着该静态变量在创建的所有实例中,静态变量只有一个实例。

类和结构体之外的静态变量或者函数意味着,当需要将这些函数或者变量与实际定义的符号链接时,链接器不会在这个翻译单元的作用域之外去寻找那个符号定义。

// static.cpp
static int s_variable = 8;  //静态变量
static void function()    //静态函数
{
    
}

//static1.cpp
int s_variable = 5;  //全局变量
void funtion()  //全局函数
{
    
}
int main()
{
    LOG(s_variable);  // s_variable = 5;
    funtion();  //调用的是本文件中的function函数
}    

在main()函数之外定义的变量都是全局变量,两个全局变量之间不能同名

// static.cpp
int s_variable = 8;  

//static1.cpp
int s_variable = 5;  

//这将会报错

extern关键字表示的变量将会在外部翻译单元寻找变量的值(external linkage or extern linking)(extern关键字表示声明一个变量而不定义它)

// static.cpp
extern int s_variable;  

//static1.cpp
int s_variable = 5; 

如果不需要变量是全局变量时,就需要尽可能地使用静态变量。

13. C++类和结构体中的静态(static)

  • 对于类和结构体中的static变量,在类或结构体的所有实例中,该变量只有一个实例如果某个实例改变了这个静态变量,那么它将会在所有实例中反映这个变化。正因如此,通过类实例来引用静态变量是没有意义的,因为这就像类的全局实例一样。
struct Entity
{
	static int x, y;
	static void Print()
	{
		std::cout << x << "," << y << std::endl;
	}

};

int Entity::x;
int Entity::y;  //定义静态变量

int main()
{
	LOG(s_variable);

	Entity e1;
	e1.x = 3;
	e1.y = 2;  //这样引用其实是没有意义的
	
	Entity e2;
	Entity::x = 5;
	Entity::y = 8;
	
	Entity::Print();  //Print()也是静态的
	Entity::Print();  //将打印两次5,8	

}
  • 静态方法也是一样,静态方法可以不需要通过类的实例被调用静态方法不能访问非静态变量静态方法没有类实例!
struct Entity
{
	static int x, y;
	static void Print()
	{
		std::cout << x << "," << y << std::endl;
	}

};
//Entity类中的Print静态方法就仿佛是如下一般:
static void Print(Entity e)
{
	std::cout << e.x << "," << e.y << std::endl;
}

Entity::Print();  //调用静态方法

C++类和结构体中的静态变量就好像是在名为类名的命名空间中创建了两个变量,它们好像是并不属于类。

如果类中的变量和方法都是静态的,那么甚至不需要对该类进行实例化。

类的非静态方法的调用要与特定对象绑定!

14. C++中的局部静态(Local Static)

声明一个变量,需要考虑两个因素——变量的生存期和变量的作用域。

  • 变量的生存期指的是变量实际存在的时间,即在它被删除时,它会内存中存在多久;
  • 变量的作用域是指可以访问变量的范围。如果在一个函数内部声明一个变量,则在其它函数中不能访问该变量,因为声明的变量对于声明的函数是局部的。

局部静态(Local Static)变量允许声明一个变量,它的生存期基本上相当于整个程序的生存期,然而它的作用范围被限制在这个函数内(不一定用在函数中,可以用在任何位置,比如if语句、循环语句等)。

void Function()
{
	int i = 0;
	i++;
	std::cout << i << std::endl;
}

void Function1()
{
	static int i = 0;
	i++;
	std::cout << i << std::endl;
}

int main()
{
	Function();  //1
	Function();  //1
	Function();  //1
    //每次调用Function()都会创建一个新的变量i,调用结束之后变量i就被销毁
    
	Function1();  //1    
	Function1();  //2
	Function1();  //3
    //三次调用Function1()只创建了一个局部静态变量i,调用结束之后变量i不会被销毁,只有在main函数退出时才会被销毁
	std::cin.get();
}

单例类(类中只存在一个实例的类):

class Singleton    //不使用局部静态的单例类
{
private:
	static Singleton* s_Instance;
public:
	static Singleton& Get() { return *s_Instance; }  //注意是static
	void Hello() {}
};

Singleton* Singleton::s_Instance = nullptr;  //实例的定义
//如果不使用局部静态,直接返回实例的话,返回的实例在函数退出后就会被销毁,此时再对返回的实例进行访问的话是错误的。

class Singleton1    //使用局部静态的单例类
{
public:
    static Singleton1& Get()
    {
        static Singleton1 instance;  //注意是static
        return instance;
    }
    void Hello() {}
}

int main()
{
    Singleton::Get().Hello();
    Singleton1::Get().Hello();    
}

15. C++枚举

C++枚举(ENUM)是一个数值集合,它是给一个值命名的一种方法,它能将一组数值集合作为类型。

C++枚举默认下第一个数是0,此后的数自第一个数开始递增,当然也可以给枚举中的数赋值,只是枚举中的数值必须是整型

enum Example
{
	A , B , C  //A=0,B=1,C=2
};
enum Example1
{
	A=5 , B , C  //A=5,B=6,C=7
};
enum Example2
{
	A, B = 5, C  //A=0,B=5,C=6
};
enum Example3 : char  //指定想要给枚举赋值的整数类型,不能指定为double或者float
{
    a, b, c
}
int main()
{
	Example value = A;  //value只能被赋值为A、B或C,否则错误
}

C++枚举在日志类Log中的运用:

class Log
{
public:
	enum Level
    {
        LevelError = 0, LevelWarning, LevelInfo
    }
private:
	Level m_LogLevel = Info;  //注意是Level
	//m_ 表示这是一个私有的成员变量,默认日志等级为Info
public:
	void SetLevel(Level level)
	{
		m_LogLevel = level;  
	}
    void Error(const char* message)
   {
	    if (m_LogLevel >= LevelError)
		    std::cout << "[ERROR]" << message << std::endl;
    }
    void Warn(const char* message)
    {
	    if (m_LogLevel >= LevelWarning)
		    std::cout << "[Warning]" << message << std::endl;
    }
    void Info(const char* message)
    {
	    if (m_LogLevel >= LevelInfo)
		std::cout << "[Info]" << message << std::endl;
    }
};

int main()
{
    Log log;
	log.SetLevel(Log::LevelWarning);//注意是Log::Warning,枚举Level并不是命名空间
	log.Error("Hello!");
	log.Warn("Hello!");
	log.Info("Hello!");
}

16. C++构造函数

构造函数是一种特殊类型的方法,它在每次实例化对象时运行

//类实例没有初始化:
class Entity
{
public:
	float X, Y;
	void Print()
	{
		std::cout << X << ", " << Y << std::endl;
	}
};
int main()
{
	Entity e;
	e.Print();  //将会打印随机的值,因为实例化e时没有初始化内存
	std::cout << e.X << ", " << e.Y << std::endl;  //会报错,因为没有初始化局部变量
}

构造函数的函数名与类名保持一致,构造函数可以有参数。

class Entity1
{
public:
	float X, Y;

	Entity1()    //不带参数的构造函数
	{
		X = 0.0f;
		Y = 0.0f;  //初始化
	}
	void Print()
	{
		std::cout << X << ", " << Y << std::endl;
	}
};

class Entity2
{
public:
	float X, Y;

	Entity2(float x, float y)    //带参数的构造函数
	{
		X = x;
		Y = y;
	}
	void Print()
	{
		std::cout << X << ", " << Y << std::endl;
	}
};

int main()
{
	Entity1 e1;
	e1.Print();  //将打印"0, 0"

	Entity2 e2(10.0f, 5.0f);
	e2.Print();  //将打印"10, 5"
}

创建好一个C++类时,C++类中其实有一个默认的构造函数,只是这个构造函数什么也不做。

类的构造函数只在实例化对象时运行,因此如果不实例化对象如只使用一个类的静态方法,构造函数不会运行当用new关键字创建一个对象实例时,也会调用构造函数

删除构造函数的方法:将构造函数设置为private;设置构造函数为delete:

class Log
{
private:
    Log() {}  //将构造函数设置为private
public:
    static int write()
    {
        
    }
};

class Log
{
public:
    Log() = delete;  //设置构造函数为delete
    static int write()
    {
        
    }
}

17. C++析构函数

构造函数是创建一个新的实例对象时运行析构函数是在销毁实例化对象时运行。

构造函数通常是设置变量或者做任何需要的初始化,析构函数是卸载变量等,并清理使用过的内存。

析构函数以~标识,析构函数名同样与类名保持一致。

class Entity
{
public:
	float X, Y;
	Entity()
	{
		X = 0.0f;
		Y = 0.0f;
		std::cout << "Created Entity!" << std::endl;
	}
	~Entity()
	{
		std::cout << "Destroyed Entity!" << std::endl;
	}
    void Print()
	{
		std::cout << x << ", " << y << std::endl;
	}
};

析构函数同时适用于栈和堆分配的对象。栈分配的对象在超出作用域时会被销毁。

void Function()
{
	Entity e;
	e.Print();
}

int main()
{
	Function();  //在函数Function返回退出时,对象e被销毁。
	std::cin.get();
}

构造函数也可手动调用(e.~Entity()),只是很少有人这么做。

18. C++继承

继承允许我们有一个相互关联的类的层次结构,换句话说,它允许我们有一个包含公共功能的基类,然后它允许我们从那个基类中分离出来,从最初的父类中创建子类。类和继承可以很好地帮助我们避免代码重复。

我们可以把类之间的所有公共功能放在一个基类(父类)中,然后从基类(父类)中创建(派生)一些类,这些类可以稍微改变以下功能,或者引入全新的类。

class Entity
{
public:
	float x, y;

	void Move(float xa, float ya)
	{
		x += xa;
		y += ya;
	}
};

class Player : public Entity    //子类Player继承了父类Entity中所有的东西
{
private:
	const char* Name;

	void PrintName()
	{
		std::cout << Name << std::endl;
	}
};

int main()
{
	Player player;  //实例化的player对象不仅可以访问Player类的数据和方法,还可以访问Entity类中所有public的东西,player此时是“多态”,即既是Player类型也是Entity类型。Player类型是Entity类型的超集,可以在任何想要使用Entity的地方使用Player,例如,假设函数PrintName(Entity e),调用PrintName()时可以传入Entity也可以传入Player。
	player.PrintName();
	player.Move(5, 5);
    player.X = 2;
}

C++继承是我们扩展现有类并为基类提供新功能的一种方式。当创建一个子类时,它将继承父类所包含的一切。

int main()
{
	std::cout << sizeof(Entity) << std::endl;  //8
    std::cout << sizeof(Player) << std::endl;  //12
}

C++继承的子类的构造函数中,可以调用父类的构造函数来初始化

class Base
{
private:
	int x, y;
public:
	Base(int x, int y)  //父类构造函数
    {
        this->x = x;
        this->y = y;
    }
};

class Sub : public Base    
{
private:
    int z;
    
public:
	Sub(int x, int y, int z) : Base(x, y)  //子类构造函数中调用父类构造函数来初始化子类中的父类的变量
    {
        this->z = z;
    }
};

int main()
{
    Sub(1, 2, 3);
}

19. C++虚函数

虚函数允许我们在子类中重写方法。例如假设有两个类,A和B,B是A的子类(B是A派生出来的),如果我们在A类中创建一个方法,标记为virtual,我们可以选择在B类中重写该方法,让它做不同的事情。

通常在我们声明函数时,我们的方法通常在类的内部起作用,然后当要调用方法时,会调用属于该类型的方法,对于多态的类型,调用函数时只调用属于传入形参的这一类型的方法

//不重写虚函数的问题:
class Entity
{
public:
	std::string GetName() { return "Entity"; }
};

class Player : public Entity
{
private:
	std::string m_Name;
public:
	Player(const std::string& name)
		: m_Name(name) {}    //初始化列表

	std::string GetName() { return m_Name; }

};

void PrintName(Entity* entity)
{
	std::cout << entity->GetName() << std::endl;
}

int main()
{
	Entity* e = new Entity();
	std::cout << e->GetName() << std::endl;  //打印"Entity"
	PrintName(e);  //打印"Entity"

	Player* p = new Player("Nirvana");
	std::cout << p->GetName() << std::endl;  //打印"Nirvana"
	PrintName(p);  //打印"Entity",尽管p指向的是Player,但Player是多态,既是Player类型又是Entity类型
	
	Entity* entity = p;  //设置一个Entity指针entity指向Player p
	std::cout << entity->GetName() << std::endl;  //打印"Entity"
	PrintName(entity);  //打印"Entity",尽管entity指向的p是Player
	
	std::cin.get();
}

虚函数引入了一种动态联编(Dynamic Dispatch),它通常通过v表(虚函数表)来实现编译,v表是一个包含基类(父类)中所有虚函数的映射,这样在它运行时,可以将它们映射到正确的覆写(override)函数。

如果想要覆写一个函数,必须将基类中的基函数标记为virtual,并且可以将覆写的函数标记为override(override可以省略)

class Entity
{
public:
	virtual std::string GetName() { return "Entity"; }  //标记基函数
};

class Player : public Entity
{
private:
	std::string m_Name;
public:
	Player(const std::string& name)
		: m_Name(name) {}    //构造函数,相当于m_Name = name

	std::string GetName() override { return m_Name; }  //标记覆写函数

};

void PrintName(Entity* entity)
{
	std::cout << entity->GetName() << std::endl;
}

int main()
{
	Entity* e = new Entity();
	PrintName(e);  //打印"Entity"

	Player* p = new Player("Nirvana");
	PrintName(p);  //打印"Nirvana"
    
	std::cin.get();
}

虚函数是有开销的,有两种与虚函数相关的运行时成本:

  • 需要额外的内存来存储v表,以分配到正确的函数;基类中要有一个成员指针指向v表
  • 每次调用虚函数时,需要遍历整个v表来确定映射到哪个函数。

20. C++接口(纯虚函数)

C++中的纯虚函数本质上与其它语言(如java或C#)中的抽象方法或者接口相同,基本上,纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数

在面向对象编程中,创建一个只由未实现的方法组成的类,然后强制子类去实现它们,这是非常常见的,这通常被称为接口,因此类中的接口只包含未实现的方法,作为模板,由于整个接口类实际上并不包含方法实现,我们实际上不可能实例化这个类

只有实现了这些纯虚函数之后,子类及更上层的类才能够实例化

class Printable    //只包含纯虚数,是接口,不能实例化
{
public:
	virtual std::string GetClassName() = 0;  //纯虚函数
};

class Entity : public Printable  //继承接口Printable
{
public:
	virtual std::string GetName() { return "Entity"; }

	std::string GetClassName() override { return "Entity"; }  //如果不实现,则Entity类不能实例化

};

class Player : public Entity    
{
private:
	std::string m_Name;
public:
	Player(const std::string& name)
		: m_Name(name) {}    //构造函数,初始化列表

	std::string GetName() override { return m_Name; }
	
	std::string GetClassName() override { return "Player"; }  //如果不实现,则Player类不能实例化

};

void PrintName(Printable* obj)
{
	std::cout << obj->GetClassName() << std::endl;
}

int main()
{
	Entity* e = new Entity();
	PrintName(e);  //打印"Entity"

	Player* p = new Player("cherno");
	PrintName(p);  //打印"Player"
	
	std::cin.get();
}

21. C++可见性

可见性是一个属于面向对象编程的概念,它指的是类的某些成员或方法实际上有多可见,即可见性是指谁能看到它们,谁能调用它们,谁能使用它们等。

可见性对程序实际运行方式完全没有影响,对程序性能或类似的东西也没有影响。可见性是为了使代码更容易维护和理解。

C++中有三个基础的可见性修饰符,private、protected、public。

  • private声明的变量只有该类和它的友元才能访问这些变量(哪怕是子类也不能访问)
class Entity
{
private:
	int X, Y;  //private意味着只有该类(或者该类的友元friend)可以访问这些变量或方法
	//friend关键字,可以让类或者函数成为该类的友元,这样类的友元可以访问private的变量或方法
	void Print() {}
public:
	Entity()
	{
		X = 0;
		Y = 0;
		Print();  //可以访问
	}
};

class Player : public Entity  
{
public:
	Player()
	{
		X = 1;  //即使Player继承了Entity也不可访问
		Y = 1;
		Print();  //即使Player继承了Entity也不可访问
	}
};

int main()
{
	Entity e;
	e.X = 1;  //不可访问
	e.Print();  //不可访问
}
  • protected比private更可见,protected声明的变量或方法只在类和类的所有子类中可访问
class Entity
{
protected:
	int X, Y; 
	void Print() {}
public:
	Entity()
	{
		X = 0;
		Y = 0;
		Print();  //可以访问
	}
};

class Player : public Entity  
{
public:
	Player()
	{
		X = 1;  //可以访问
		Y = 1;
		Print();  //可以访问
	}
};

int main()
{
	Entity e;
	e.X = 1;  //不可访问
	e.Print();  //不可访问
}
  • public声明的变量或方法,类内和类外等所有地方都可以访问它们。
class Entity
{
public:
	int X, Y; 
	void Print() {}

	Entity()
	{
		X = 0;
		Y = 0;
		Print();  //可以访问
	}
};

class Player : public Entity  
{
public:
	Player()
	{
		X = 1;  //可以访问
		Y = 1;
		Print();  //可以访问
	}
};

int main()
{
	Entity e;
	e.X = 1;  //可以访问
	e.Print();  //可以访问
}

22. C++数组

数组是按特定顺序排列的相同类型的元素的集合。

  • 数组索引下标以0开始,注意数组不要越界
int example[5];
example[0] = 2;  //数组第一个元素
example[4] = 4;  //数组最后一个元素
example[5] = 1;
example[-1] = 1;  //内存访问违规,在release模式下不会报错

int a = example[0];

std::cout << example[0] << std::endl;
std::cout << example << std::endl;  //打印整个数组,example实际上是指针
  • 数组遍历
int example[5];
for(int i = 0; i < 5; i++)
    example[i] = 1;

数组其实是一个指针,指向分配给数组的那段内存的起始地址。

用数组索引访问数组变量实际上是通过指针加上相应偏移量实现的。

int* ptr = example1;
example1[2] = 5;
*(ptr + 2) = 6;  // example1[2]=6
//由于指针ptr是整型,因此加上的偏移量实际上是2*4即8个字节
*(int*)((char*)ptr + 8) =7;
//将ptr转换成char类型之后,同样要访问example1[2],要加上的偏移量同样是8个字节,即1*8
//注意这里还要将ptr转换回int类型,否则写入的7就是char类型了。

在栈和堆上创建数组的生存期不同:

  • 在栈上创建的数组,在程序跳出作用域时,数组就会被销毁
  • 在堆上创建的数组,直到程序销毁时,数组才会被销毁(因此数组在内存中将会一直存在,直到我们删除它,所以需要主动删除)
int example2[5];  //在栈上创建的数组,在程序运行到主函数花括号时,数组销毁
int* another = new int[5];  //在堆上创建的数组,程序销毁时,数组销毁
delete[] another;

如果想定义一个函数,该函数返回一个在函数内部创建的数组,那么必须使用一个new关键字来创建数组分配内存,否则数组将在程序运行至跳出函数时就被销毁。

int a[5];
int count = sizeof(a) / sizeof(int);  //  size = 20/4 = 5

int* e = new int[5];
int count1 = sizeof(e) / sizeof(int);  //  size = 4/4 = 1
//因为e是整型指针

维护数组的规模:

class Entity
{
public:
	static const int exampleSize = 5;
	int example[exampleSize];  //exampleSize必须是确定的,因为栈在分配给数组内存时必须知道数组所占内存空间大小,exampleSize也必须是static,否则错误,因为非静态变量的引用要与特定对象相对。
    Entity()
    {
        for(int i = 0; i < exampleSize; i++)
            example[i] = 1;
    } 
};

#include <array>
class Entity1
{
public:
    std::array<int, 5> another  //用array库创建数组int another[5]
        
    Entity1()
    {
        for(int i = 0; i < another.size(); i++)
            another[i] = 1;
    } 
};

原始数组和std::array:

  • 原始数组总是更快一些,并且不需要额外的开销

  • std::array需要额外的开销(边界检查),但std::array更加安全

23. C++字符串

字符串一个接一个的一组字符,字符包括字母、数组、符号等。

  • C++中的字符串实际上是字符数组。

对于const char*,字符串的成员不能被更改。
基本上""或者''包裹的字符串都是const char*

const char* name = "cherno";
// delete[] name; 不是堆分配(new关键字)的字符串就不能用delete删除;
// const表示该字符串是不变的,不可被更改,因此name[2] = a是错误的

C++字符串有一个字节以0结尾,这是字符串的空终止符,便于确定字符串内存大小,字符串从指针的内存地址开始,到遇到终止符(0)结束。

char name2[7] = { 'c','h','e','r','n','o','\0' };  
// 字符数组,'\0'表示空终止符(也可直接写0),不添加的话编译器无法知晓字符串的结尾
std::cout << name2 << std::endl;
  • C++标准库string类

在C++中使用字符串,可以使用std::string,std::string基本上是一个字符数组和一些操作这个字符数组的函数。

std::string name3 = "cherno";
std::cout << name3 << std::endl;  //这里必须#include <string>,否则会报错

//字符串成员添加:
std::string name3 = "cherno" + "hello";  //这是错误的,因为cherno和hello是const char*而不是string
name3 += "hello";  //这是可以的,因为string类重载了+=运算符
std::string name3 = std::string("cherno") + "hello";  //或者也可以这样

string类有一个构造函数,它接受char*或const char*参数,然后返回一个string字符串。

string类方法

方法 作用 备注
string(char* or const char*) 构造函数 返回一个string
size() 查询字符串大小 返回字符串字符个数
length() 查询字符串长度
find() 查找字符串 返回查找到的字符串所在的首位置
append() 在字符串末尾添加字符串
insert() 在指定位置插入字符
replace(pos, n, c) 替换字符串 将从pos位置开始往后的n个字符替换成字符c
substr(pos, npos) 切割字符串 返回从pos位置开始到npos位置结束的子串
//定义一个string类对象
string http = "www.runoob.com";

//打印字符串长度
cout<<http.length()<<endl;

//拼接
http.append("/C++");
cout<<http<<endl; //打印结果为:www.runoob.com/C++

//删除
int pos = http.find("/C++"); //查找"C++"在字符串中的位置
cout<<pos<<endl;
http.replace(pos, 4, "");   //从位置pos开始,之后的4个字符替换为空,即删除
cout<<http<<endl;

//找子串runoob
int first = http.find_first_of("."); //从头开始寻找字符'.'的位置
int last = http.find_last_of(".");   //从尾开始寻找字符'.'的位置
cout<<http.substr(first+1, last-first-1)<<endl; //提取"runoob"子串并打印
  • 将string类作为参数传递给函数

如果直接把string类对象传递给一个函数时,实际上是在复制这一个string类对象,这样对复制的string类对象做更改时,变化将不会反映会原string对象。同时由于直接把string类作为对象传递,将会复制该对象,堆会分配一个全新的字符数组来存储,然而字符串复制是一个非常缓慢的过程。

因此当传递一个字符串给函数时,并且字符串是只读的情况下,确保通过常量引用(const std::string&)传递它。(如果想要字符串可写那么就直接通过引用(std::strng&)传递)

void PrintString(std::string str)  //直接传递string类
{
	std::cout << str << std::endl;
}

void PrintString1(const std::string& str)  //常量引用传递string类
{
    std::cout << str << std::endl;
}

int main()
{
    std::string name = "cherno";
    PrintString()
}

24. C++字面量

字符串字面量是在双引号之间的一串字符。

字符串字面量默认隐式地在字符串的最后有一个额外的字符'\0'(空终止符)

const char name[8] = "che\0rno";
std::cout << strlen(name) << std::endl;  //3
//strlen只计算从字符串开始到遇到的第一个\0之间的长度,因为C++认为字符串到\0就结束了
const char name1[8] = "cherno";
std::cout << strlen(name1) << std::endl;  //6

字符串字面量是只读的,字符串字面量通常是存储在内存的只读区域的因为对字符串字面量进行修改会出现未定义错误。(因为试图对只读内存进行写操作)

const char* name2 = "cherno";
name2[2] = 'a';  //错误,const char* 字符串不可更改

char* name3 = "cherno";
name3[3] = 'a'; //会导致未定义错误,在debug模式下会抛出异常,在release模式下该更改不会生效

//若想对字符串字面量进行修改,应该定义一个字符数组而不是字符串指针:
char name4[] = "cherno";
name[4] = 'a';

//C++11开始,有些编译器只不允许将字符串字面量(const char*)给char*进行赋值。
char* name5 = "cherno";  //报错
char* name5 = (char*) "cherno";  //将字符串字面量给char*赋值时需要先进行类型转换

其它字符类型的字符串字面量

const char* str0 = u8"Cherno";  //utf8
const wchar_t* str1 = L"cherno";  //宽字符,两个字节,字符串字面量前要加L
const char16_t* str2 = u"cherno";  //两个字节的字符,utf16,字符串字面量前要加u
const char32_t* str3 = U"cherno";  //四个字节的字符,utf32, 字符串字面量前要加U

// C++14中引入了std::string_literals
using namespace std::string_literals;
std::string name = "Cherno"s + " hello!";//s是一个操作符函数,它返回标准字符串对象 
std::wstring name1 = L"Cherno"s + L" hello!";
std::u32string name2 = U"Cherno"s + U" hello!";

const char* example = R"(line1  
line2
line3)"; 
//R表示忽略转义字符

const char* ex = "line1\n"
"line2\n"
"line3\n";

字符串字面量永远只保存在内存的只读区域!

25. C++中的Const

const就像是做出的一个承诺,承诺某些东西是将是不变的,但它只是一个可以绕过的承诺,是否遵守这个承诺取决于程序员自己。然而我们实际上应当遵守这个承诺,因为这样可以简化很多代码。

  • const变量
const int MAX_RANGE = 100;
MAX_RANGE = 90; //报错,不可修改
  • const指针
const int* b = new int;
//与 int const* b = new int相同意义;
*b = 2;  // 报错,不能更改指针指向的内存的内容
b = (int*)&a;  //可以改变指针的指向

int* const c = new int;
*c = 2;  // 可以更改指针指向的内存的内容
c = (int*)&a;  // 不能改变指针的指向

const int* const d = new int;
*d = 2;  // 不可以更改指针指向的内存的内容
d = (int*)&a;  // 也不能改变指针的指向
  • 类中的const

const可以放在类中的方法名之后,这时,const意味着这个方法不会修改类的任何东西。

class Entity
{
private:
	int m_X, m_Y;
public:
	int GetX() const  //保证不修改Entity
	{
		m_X = 2;  //报错,不允许修改类变量
		return m_X;
	}
	void SetX(int x)  //方法想要修改成员变量时就不应该加上const
	{
		m_X = x;
	}
};

class Entity1
{
private:
	int* m_X, *m_Y;  //注意m_Y前也要加*,否则m_Y只是整型而不是指针
public:
	const int* const GetX() const  //承诺该方法不会对类成员指针做任何修改
	{
		return m_X;
	}
};

void PrintEntity(const Entity& e)  //因为传入的是const Entity&,因此只能调用Entity类中的const方法。
{
    std::cout << e.GetX() << std::endl;  //GetX()这里必须是const的
}

注意,如果类的方法实际上没有修改类或它本不该修改类,那么记得将该方法标记为const。

26. C++中的Mutable

Mutable通常有两种用法,其一是与const一起使用;其二是用在lambda表达式中。

在一个类中,如果有一个方法是const的,此时在该方法内部不应当修改任何类成员的值,但是假如我们需要有一个类成员在该方法中被修改,那么此时就可以使用mutable声明该类成员,表示它是允许在const方法中被修改的。

  • 在类成员中使用mutable
class Entity
{
private:
	std::string m_Name;
	mutable int m_DebugCount = 0;
public:
	const std::string& GetName() const
	{
		m_DebugCount++;
		return m_Name;
	}

};

int main()
{
	const Entity e;
	e.GetName();
}
  • 在lambda表达式中使用mutable

lambda表达式就像是一个一次性的小函数,可以给lambda表达式赋值一个变量,我们可以像调用函数一样调用它。

int x = 8;
auto f = [=]() mutable  // lambda表达式,值传参,这里mutable表示lambda表达式捕获的变量是可改变的。
{
	x++;  //如果不加mutable,则此处会发生错误
	std::cout << x << std::endl;
};
f();  //x = 8

27. C++的成员初始化列表

成员初始化列表是我们在构造函数中初始化类成员的一种方式。

在构造函数中,通常有两种方法初始化类成员:

  • 常见的方法:
class Entity
{
private:
	std::string m_Name;
public:
	Entity()
	{
		m_Name = "Unknown";
	}
	Entity(const std::string& name)
	{
		m_Name = name;
	}
};
  • 成员初始化列表:

成员初始化列表要按照类成员定义的顺序来写,否则编译器可能会报错,因为不论怎么写初始化列表,编译器都会按成员定义的顺序进行初始化。

class Entity1
{
private:
	std::string m_Name;
	int m_Score;
public:
	Entity1()
		: m_Name("Uknown"), m_Score(0) {}  //初始化成员列表

	Entity1(const std::string& name)
        :m_Name(name) {}
	
	const std::string& GetName() const 
    { 
        return m_Name; 
    }
};

使用成员初始化列表的原因:避免类成员对象多次被构造造成性能的浪费。

class Example
{
public:
	Example()
	{
		std::cout << "Created Entity!" << std::endl;
	}
	Example(int x)
	{
		std::cout << "Created Entity with" << x << "!" << std::endl;
	}
};

class Entity2
{
private:
	std::string m_Name;
	Example m_Example;  //这里先用无参数的构造函数实例化了一个Example对象
public:
	Entity2()
	{
		m_Name = "Unknown";
		m_Example = Example(8);//这里又用有参数的构造函数实例化了另一个Example对象
	}
	const std::string& GetName() const 
    { 
        return m_Name; 
    }
};

class Entity3
{
private:
	std::string m_Name;
	Example m_Example; 
public:
	Entity3()
        :m_Name("Uknown"), m_Example(Example(8))
	const std::string& GetName() const { return m_Name; }
};

int main()
{
    Entity2 e2;
    //此时会打印"Created Entity!" "Created Entity with 8!"
    //即这时创建了两个Example对象,一个是用无参数的构造函数构建,另一个是用有参数的构造函数构建,也就是说我们先创建了一个Example对象,然后用一个新的Example对象覆盖了它,这里就浪费了一个Example对象。
    Entity3 e3;
    //如果用成员初始化列表,就只会创建一个实例对象。
    //此时只会打印"Created Entity with 8!"
}

28. C++三元运算符

三元运算符即? :,它实际上是if语句的语法糖。

if (s_Level > 5)
	s_Speed = 10;
else
	s_Speed = 5;	
//等同于:
s_Speed = s_Level > 5 ? 10 : 5;
std::string rank = s_Level > 10 ? "Master" : "Beginner";

std::string otherRank;  //创建了一个空的string类otherRank
if (s_Level > 10)
	otherRank = "Master";  //用新的对象覆盖了原对象 
    //otherRank = std::string("Master")
else
	otherRank = "Beginner";
//这一段代码会比使用三元运算符的代码更慢

三元运算符的嵌套:

s_Speed = s_Level > 5 ? s_Level > 10 ? 15 : 10 : 5;

//相当于:
if (s_Level > 10)
     s_Speed = 15;
else if (s_Level > 5);
     s_Speed = 10;
else
     s_Speed = 5;

29. 创建并初始化C++对象

当我们创建完一个C++类之后,开始使用C++类之前,我们必须需要对这个类进行实例化,除非它是完全静态的

当我们创建一个C++类对象时,它需要占用一些内存,即使它是一个完全是空的类(没有类成员变量也没有类方法)也需要至少占用一个字节的内存。应用程序会将内存主要分为两个部分,堆和栈,当然还有其它部分的内存,比如源代码的内存区域(这里都是存放机器代码),在C++中我们要选择将对象存在何处。

栈对象有一个自动的生存期,它的生存期实际上是由它声明的地方作用域决定的,只要超出作用域,内存就被释放了,因为当作用域结束的时候,栈会弹出,栈上的任何东西都会被释放;但堆对象是不同的,一旦在堆中分配一个对象,实际上已经在堆上创建了一个对象,它会一直待在那里,直到我们决定释放这个对象。

在栈上创建并初始化C++对象:

class Entity
{
private:
	std::string m_Name;
public:
	Entity() : m_Name("Uknown") {}  //初始化成员列表
	Entity(const std::string& name) : m_Name(name) {}
	
	const std::string& GetName() const { return m_Name; }
};

void Function()
{
	int i = 1;
	Entity e;  //对象e和变量i会在这个函数的作用域结束即}后被释放
}

int main()
{
	Entity entity;  //调用了默认的构造函数,在栈上创建了对象并初始化了
    std::cou << entity.GetName() << std::endl;  //打印Unknown
    Entity entity1("Cherno");  //调用了带参数的构造函数
    std::cou << entity1.GetName() << std::endl;  //打印Cherno
    
    Function();  //调用结束后Function()内的对象e和变量i被释放
    
    //作用域不一定是在函数内,也可以是if语句甚至是空的:
    Entity* e;
    {
        Entity entity("Cherno");
        e = &entity;
        std::cout << entity.GetName() << std::endl;
    }
    //指针e在跳出上面这个空作用域即{}时,指针e将不再指向entity
	std::cin.get();
}

什么时候应该在堆上创建对象:一是当我们想要创建一个在作用域之外还能继续存在的对象时;二是如果想创建的对象的规模很大时,可能没有足够的空间在栈上分配(栈通常非常小)

int main()
{
	Entity* entity = new Entity("Cherno");  //在堆上创建对象,new关键字,返回创建的对象在堆上被分配的内存地址
    delete entity;  //delete释放在堆上该对象被分配的内存
    
    Entity* e;
    {
        Entity* entity = new Entity("Cherno");
        e = entity;
        std::cout << (*entity).GetName() << std::endl;
    }  //哪怕跳出这个空作用域,对象e还是存在
    
    delete e;  
    //直到delete释放内存后,对象e才被销毁了
}

在堆上创建对象和在栈中创建对象的区别:在堆上分配要比栈花费上更多时间,而且在堆上分配的话必须要手动释放被分配的内存(delete)。

30. C++ new关键字

new的主要作用是在堆上分配内存。

使用new关键字时,它会在内存中找寻指定大小的内存,一旦找到后,它将返回一个指向该内存的指针。

使用new关键字会消耗时间(找寻内存,空闲列表)

使用new关键字给类对象分配内存时,还会调用构造函数。

new其实是一个操作符,因此可以对它进行重载。

当使用了new关键字之后,记得必须用delete释放内存,否则被分配出去的这块内存就不能再被new调用后再分配,因为它不会被放回空闲列表。如果使用new分配内存时使用了数组操作符[],就要使用delete[]来释放内存。

int a = 2;
int* b = new int[50];  //200字节,50个连续的4字节内存空间


Entity* e = new Entity[50];
delete[] e;
Entity* e1 = new Entity();  //new 关键字不仅分配内存,还调用构造函数
delete e1;
Entity* e2 = (Entity*) malloc(sizeof(Entity));  //使用malloc申请内存

Entity* e3 = new(b) Entity;  //placement new,可以决定在一个指定的内存地址中创建对象分配内存,这里Entity对象e3是在内存块b中创建并初始化的

31. C++隐式转换和Explicit关键字

C++编译器允许对代码进行一次隐式转换,如果有两个不同类型的数据,C++允许在两者之间隐式进行转换而不需要用cast做强制转换。

class Entity
{
private:
	std::string m_Name;
	int m_Age;
public:
	Entity(const std::string& name)
		:m_Name(name), m_Age(-1) {}
	Entity(int age)
	    :m_Name("Unknown"), m_Age(age) {}
};

void PrintEntity(const Entity& entity)
{
	std::cout << "Entity" << std::endl;
}


int main()
{
	Entity a("Cherno");
	Entity b(22);

	Entity c = "Cherno";  
	Entity d = 22;  //隐式转换,隐式地将22转换成Entity并调用构造函数,因为Entity有一个接受int参数的构造函数
	
	PrintEntity(std::string("Cherno"));  // 隐式转换顺序:const char* -> string -> Entity
	PrintEntity(Entity("Cherno"));  // const char* -> string
	PrintEntity(22);  // int -> Entity

}

explicit关键字禁止进行隐式转换,将explicit关键字放在构造函数前面,意味着禁止进行隐式转换,如果要构造一个类对象,则必须显式地调用它的构造函数。

class Entity1
{
private:
	std::string m_Name;
	int m_Age;
public:
	Entity(const std::string& name)  //允许进行隐式转换
		:m_Name(name), m_Age(-1) {}
	explicit Entity(int age)  //禁止进行隐式转换
	    :m_Name("Unknown"), m_Age(age) {}
};

int main()
{
    Entity1 e = std::string("Cherno");  //允许
	Entity1 f = 22;  //报错
    Entity1 g(22);  //允许
    Entity1 h = (Entity)22;  //强制类型转换,cast
}

32. C++运算符及其重载

运算符重载是指给运算符赋予新的含义。

重载的运算符的左侧对象默认会绑定到this参数上。

struct Vector2  //2维向量
{
	float X, Y;
	Vector2(float x, float y)
		: X(x), Y(y) {}

	Vector2 Add(const Vector2& other) const  //向量相加,返回新向量,不会更改原向量
	{
		return Vector2(X + other.X, Y + other.Y);
	}
	Vector2 operator+(const Vector2& other) const  //重载运算符+
	{
		return Add(other);
	}
	
	Vector2 Multiply(const Vector2& other) const  //向量相乘
	{
		return Vector2(X * other.X, Y * other.Y);
	}
	Vector2 operator*(const Vector2& other) const  //重载运算符*
	{
		return Multiply(other);
	}
	
	bool operator==(const Vector2& other) const  //重载运算符==
	{
		return X == other.X && Y == other.Y;
	}
	
	bool operator!=(const Vector2& other) const  //重载运算符!=
	{
		return !operator==(other);
		// return !(*this == other);
	}

};

std::ostream& operator<<(std::ostream& stream, const Vector2& other)  //在cout类中重载运算符<<
{
	stream << other.X << ", " << other.Y;
	return stream;
}

int main()
{
	Vector2 position(4.0f, 6.0f);
	Vector2 speed(0.5f, 1.5f);
	Vector2 powerup(1.1f, 1.1f);

	Vector2 result = position.Add(speed.Multiply(powerup));  //result = postion + speed * powerup
	//等同于:
	Vector2 result1 = position + speed * powerup;
	
	std::cout << result << std::endl;  //不重载<<的话是不能打印result的
	
	if (result == result1)
	{
	
	}
}

33. C++ this关键字

C++中有一个this关键字,通过它可以实现访问成员函数,成员函数是指属于某个类的函数或方法,在方法的内部可以引用this,this是一个指向当前对象实例的指针,该方法属于这个对象实例。

class Entity
{
public:
	int x, y;

	Entity(int x, int y)
	{
		Entity* const e = this;  // 在非const函数中使用this,this的类型是Entity*
		
		this->x = x; // 直接写x = x是没有意义的
		// (*this).x = x;
		this->y = y;
		// (*this).y = y;
	}
	
	int GetX() const
	{
		const Entity* e = this;  // 在const函数中使用this,this的类型是const Entity*
		// Entity* e = this是错误的,因为GetX()是const的
	}
};

在类中调用类外函数并传入当前类实例:

void PrintEntity(Entity* e)  //传入类指针
{
    //PrintSomething
}

void PrintEntity1(const Entity& e)  //传入类引用
{
    //PrintSomething
}

class Entity
{
public:
	int x, y;

	Entity(int x, int y)
	{
		this->x = x;
		this->y = y;
        PrintEntity(this);
        PrintEntity1(*this);
	}
};

34. C++拷贝

(1) 拷贝构造函数

拷贝构造函数用于返回一个自身类的拷贝。

拷贝构造函数的第一个参数是自身类类型的引用(几乎总是const引用),且任何额外参数都有默认值。

class Entity
{
private:
    string name;
    int age;
    double height;
public:
    Entity();  //构造函数
    Entity(const Entity&)  //拷贝构造函数

};

由于拷贝构造函数在一些情况下会被隐式引用,因此通常不会声明为explicit的

如果类没有定义自身的拷贝构造函数的话,编译器默认会为类合成一个。默认情况下,合成的拷贝构造函数会把类内的参数的非static成员拷贝到正在创建的对象中。

//默认合成的拷贝构造函数等同于:
Entity::Entity(const  Entity& e)
    :name(e.name), age(e.age), height(e.height) { }

使用拷贝初始化时,是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。

string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null_book = "9-999-99999-9"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化

拷贝初始化通常由拷贝构造函数来完成但如果一个类拥有移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。

发送拷贝初始化的情况:

  • 用=定义变量;
Entity e();
Entity e1 = e;
  • 将对象作为实参传递给非引用类型的形参;
void Function(Entity entity)
{
    //......
}
Entity e;
Function(e);
  • 从返回类型为非引用类型的函数返回对象
Entity Function(Entity entity)
{
    //......
    return 
}
  • 用花括号列表初始化数组中的元素或者聚合类中的成员

(2) 拷贝赋值运算符

拷贝赋值运算符通常应该返回一个指向其左侧对象的引用,拷贝赋值运算符的第一个参数通常也是类类型的引用。

class Entity
{
public:
    Entity& operator=(const Entity&);  //拷贝赋值运算符

};

如果类未定义自己的拷贝赋值运算符,编译器默认会为类合成一个。默认情况下,合成拷贝赋值运算符会将其右侧运算对象的非static成员逐个赋值给左侧运算对象的对应成员,之后返回左侧运算对象的引用。

//默认合成的拷贝赋值运算符等同于:
Entity& Entity::operator=(const Entity& e)
{
    name = e.name;
    age = e.age;
    height = e.height;
    return *this;  //返回一个左侧对象的引用。
}

使用 =default可以显式地要求编译器合成拷贝构造函数和拷贝赋值运算符,使用 =delete可以显式地删除拷贝构造函数和拷贝赋值运算符来阻止拷贝。

class Entity
{
public:
    Entity() = default;  //要求编译器合成构造函数
    Entity(const Entity&) = default;  //要求编译器合成拷贝构造函数
    Entity operator=(const Entity&) = default;  //要求编译器合成拷贝赋值运算符
};

class Entity1
{
public:
    Entity() = delete;  //删除构造函数
    Entity(const Entity&) = default;  //删除拷贝构造函数
    Entity operator=(const Entity&) = default; //删除拷贝赋值运算符
};

(3) 深拷贝和浅拷贝

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。

以下情况都会调用拷贝构造函数:

  • 一个对象以值传递的方式传入函数体
  • 一个对象以值传递的方式从函数返回
  • 一个对象需要通过另外一个对象进行初始化

浅拷贝就是把一个对象的数据成员的值全部复制给另一个对象。

默认的拷贝构造函数就是浅拷贝。

浅拷贝可能出现的问题:如下面的Person类,该类中包含两个类变量,其中一个是指针类型,如果只是简单地浅拷贝即把对象的值全部复制给另一个,如 Person p2 = p1,这时因为p1中的成员变量name指针已经申请了内存,那么浅拷贝之后p2的name指针复制了该值,此时p1和p2两个的name指针都指向同一块内存,如果p1把内存释放了(调用析构函数),那么此时p2内的name指针就是野指针了(指针悬挂),将会出现运行错误。

class Person
{
private:
    char* name;
    int age;
public:
    Person(const char* name, int age)  //构造函数
    {
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        this->age = age;
    }
    Person(const Person& p)  //拷贝构造函数,是深拷贝
    {
        this->name = new char[strlen(p.name) + 1];
        strcpy(this->name, p.name);
        this->age = age;
    }
    void showPerson()
    {
        cout << name << " " << age << endl;
    }
    
    ~Person()  //析构函数
    {
       if (name != nullptr) 
       {
           delete[] name;
           name = nullptr;
       }
    }
};

int main()
{
    char name[100] = {0};
    int age;
    
    cin >> name;
    cin >> age;
    
    Person p1(name, age);
    Person p2 = p1;  //将对象p1复制给p2,此时调用了拷贝构造函数
    P2.showPerson();
}

深拷贝就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中已有的对其他资源的引用的指针或引用进行单纯的赋值。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝反之,没有重新分配资源,就是浅拷贝

通常如果一个类的成员变量中有指针,那么这时拷贝构造函数就需要重写为深拷贝。

35. C++友元

类可以允许其他类或函数访问它的非公有成员,方法是使用关键字friend将其他类或函数声明为它的友元。

友元声明只能出现在类定义的内部,具体位置不限,通常情况下,最好在类定义开始或结束前的位置集中声明友元。友元不是类的成员,也不受它所在区域访问级别的约束。

//声明一个全局函数为友元
class Entity
{
    friend void PrintEntity(const Entity& entity);  //友元的声明
private:
    string s;
public:
    Entity(string s)
    {
        this->s = s;
    }
};

void PrintEntity(const Entity& entity)  //如果不声明友元,则无法访问类成员变量s
{
    cout << entity.s << endl;  
}

int main()
{
    Entity e("Cherno");
    PrintEntity(e);  
}

友元声明仅仅指定了访问权限,而并非一个通常意义上的函数声明。如果希望类的用户能调用某个友元函数,就必须在友元声明之外再专门对函数进行一次声明(部分编译器没有该限制)。

可以为类的友元的,除了全局函数还可以是其他类或其他类的函数。

//声明另一个类为友元
class Entity
{
    friend class Entity1;  //友元的声明,声明类Entity1是类Entity的友元
    //......
};

class Entity1
{
    //......
};

36. C++序列容器

C++中的顺序容器:

类型 特性
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表。只支持双向顺序访问。在任何位置插入/删除速度都很快
forward_list 单向链表。只支持单向顺序访问。在任何位置插入/删除速度都很快
array 固定大小数组。支持快速随机访问。不能添加/删除元素
string 类似vector,但用于保存字符。支持快速随机访问。在尾部插入/删除速度很快

forward_listarray是C++11新增类型。与内置数组相比,array更安全易用。forward_list没有size操作。

(1) 容器通用库

容器创建和初始化

方式 说明
C c; 容器的默认构造,创建一个空的容器
C c1(c2); 创建容器c1为容器c2的拷贝(c1和c2必须是同类型的容器)
C c(b,e); 拷贝另一容器迭代器范围[b,e)的元素(array不可用)
C c{a, b, c...}; 构造并列表初始化容器
C seq(n); 创建一个有n个元素的序列容器(仅序列容器可用,string不可用)
C seq(n, t); 创建一个有n个值为t的元素(仅序列容器可用)
vector<int> vec;
vector<int> v1(3){1, 2, 3};
vector<int> v2(v1);
vector<int> v3(v1.begin(), v1.end());
vector<int> v4(10,0);

容器赋值和交换

方式 说明
c1 = c2 用容器c2给容器c1赋值(c1和c2必须是同类型的容器和元素类型)
c1 = 用列表中的元素给c1赋值
seq.assign(n, t) 将n个值为t的元素赋值给顺序容器
seq.assign(first,last) 将另一个容器的[first,last)范围中的元素赋值给顺序容器
seq.assign(il) 将初始化了的列表il给顺序容器赋值
swap(c1, c2) 交换c1和c2中的元素(c1和c2必须是同类型的容器)
c1.swap(c2) 用容器c2中的元素与c1交换

赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作交换容器内容,不会导致迭代器、引用和指针失效(arraystring除外)。

list<string> l1 = {"hello", "hi"};
list<const char*> l2;
vector<string> v1;
v1 = l1 //错误,不能赋值,容器类型不同
l2 = l1;  //错误,list中元素类型不同

list<string> l3 = {"abc", "def"}
swap(l1, l3);
l1.swap(l3);

容器大小

方式 说明
c.size() 返回容器c中的元素个数(forward_list不可用)
c.max_size() 返回容器c中的最大容量
c.empty() 若容器c为空,返回true,否则返回false

添加和删除元素

方式 说明
c.insert(pos, elm) 在容器c的迭代器位置pos之前的位置插入元素elm,返回新插入的元素的位置的迭代器
c.insert(pos, n, elm) 在容器c的迭代器位置pos之前的位置插入n个相同的元素elm,返回新插入的元素的位置的迭代器
c.insert(pos, first, last) 在容器c的迭代器位置pos之前的位置插入另一个相同类型容器[first,last)范围的元素,返回新插入的元素的位置的迭代器
c.emplace(pos, args) 在容器c的迭代器位置pos之前的位置只插入一个元素,args是新插入元素类型的构造函数的多个参数,返回新插入的元素的位置的迭代器
c.erase(pos) 删除容器c中迭代器pos处的元素,返回删除元素的后一个元素位置的迭代器
c.erase(first, last) 删除容器c中[first,last)范围的元素,返回删除元素的后一个元素位置的迭代器
c.clear() 移除容器中的所有元素
vector<string> vec;
vec.insert(vec.begin(), "hello");

class Entity
{
private:
    double x, y;
public:
    Entity(double x, double y)
    {
        this->x = x;
        this->y = y;
    }
};

list<Entity> l1;
l1.emplace(l1.begin(), 1.1, 5.2);

vector<int> a(3){1, 2, 3};
a.erase(a.begin() + 1);
a.clear()

容器支持的运算符

运算符 说明
== != 相等和不相等,所有容器都支持
>、>=、<、<= 关系运算符,除无序关联容器外,其他容器都支持

容器的运算符两侧的容器类型和保存元素类型都必须相同。

两个容器的比较实际上是对元素的逐个比较,其工作方式与字符串的比较类似。

vector<int> v1 = { 1, 3, 5, 7, 9, 12 };
vector<int> v2 = { 1, 3, 9 };
vector<int> v3 = { 1, 3, 5, 7 };
vector<int> v4 = { 1, 3, 5, 7, 9, 12 };

v1 < v2;  //true
v1 < v3;  //false
v1 == v4;  //true
v1 == v2;  //false

容器的访问

方式 说明
c.back() 返回容器c的最后一个元素的引用
c.front() 返回容器c的第一个元素的引用
c[n] 返回容器c的索引为n的元素的引用
c.at[n] 返回容器c的索引为n的元素的引用n不可越界

容器的迭代器

迭代器 说明
c.begin()、c.end() 返回容器c指向第一个元素最后一个元素位置之后的正向迭代器
c.cbegin()、c.cend() 返回容器c指向第一个元素和最后一个元素位置之后的const正向迭代器
c.rbegin()、c.rend() 返回容器c指向第一个元素和最后一个元素位置之后的反向迭代器
c.crbegin()、c.crend() 返回容器c指向第一个元素和最后一个元素位置之后的const反向迭代器

一个迭代器范围由一对迭代器表示。这两个迭代器通常被称为beginend,分别指向同一个容器中的元素或尾后地址。end迭代器不会指向范围中的最后一个元素,而是指向尾元素之后的位置。这种元素范围被称为左闭合区间,其标准数学描述为[begin,end)。迭代器beginend必须指向相同的容器,end可以与begin指向相同的位置(此时范围为空),但不能指向begin之前的位置(由程序员确保)。

while (begin != end)
{
    *begin = val; 
    ++begin;  
}

(2) 迭代器

迭代器是一种检查容器内元素并遍历元素的数据类型,通常用于对C++中各种容器内元素的访问,不同的容器有不同的迭代器。

  • 迭代器的定义

容器类名::迭代器类型 迭代器名;

  • 迭代器的类型
类型 说明 示例
iterator 正向迭代器 vector<int>::iterator i;
const_iterator 常量正向迭代器 vector<int>::const_iterator i;
reverse_iterator 反向迭代器 vector<int>::reverse_iterator i;
const_reverse_iterator 常量反向迭代器 vector<int>::const_reverse_iterator i;
  • 迭代器的使用

通过迭代器可以读取它指向的元素,*迭代器名就表示迭代器指向的元素。通过非常量迭代器还能修改其指向的元素。

反向迭代器和正向迭代器的区别在于:正向迭代器的begin()指向容器的首元素,而反向迭代器的rbegin()则指向容器的尾元素;正向迭代器的对正向迭代器进行++操作时,迭代器会指向容器中的后一个元素;而对反向迭代器进行++操作时,迭代器会指向容器中的前一个元素。

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> vec;
    for(int i = 0; i < 5; i++)
        vec.push_back(i);
    
    vector<int>::iterator iter;  //正向迭代器遍历数组
    for(iter = vec.begin(); iter != vec.end(); iter++)
        cout << *iter << " ";
    
    cout << endl;

    vector<int>::reverse_iterator iter1;  //反向迭代器遍历数组
    for(iter1 = vec.rbegin(); iter1 != vec.rend(); iter1++)
        cout << *iter1 << " ";
    
    return 0;
}
  • 迭代器的辅助函数

迭代器提供了以下几个辅助函数,需要包含头文件#include <algorithm>

辅助函数 说明
advance(it, n) 使迭代器向前或向后前进n个位置,n为正向前,n为负向后
distance(it1, it2) 计算两个迭代器之间的距离
iter_swap(it1, it2) 交换两个迭代器指向的元素
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
list<int> lst(a, a + 10);
list<int>::iterator it1 = lst.begin();

advance(it1, 2);  //it1向后移动两个元素,指向3
cout << "当前it1指向的元素:" << *it1 << endl;  //输出3
advance(it1, -1);  //it1向前移动一个元素,指向2
cout << "当前it1指向的元素:" << *it1 << endl;  //输出2

list<int>::iterator it2 = lst.end();
it2--;  //it2指向最后一个元素的位置,即10的位置
cout << "it1和it2的距离" << distance(it1, it2) << endl;  //输出8

cout << "交换前打印:";
for (it1 = begin(lst); it1 != end(lst); ++it1)
{
    cout << *it1 << " ";  //打印 1 2 3 4 5 6 7 8 9 10
}

it1 = begin(lst);
iter_swap(it1, it2); //交换 1 和 10
cout << "\n交换后打印:";
for (it1 = begin(lst); it1 != end(lst); ++it1)
{
    cout << *it1 << " ";  //打印 10 2 3 4 5 6 7 8 9 1
}
cout << endl;
  • 迭代器的功能分类

迭代器按照功能可以分为输入迭代器、输出迭代器、正向迭代器、反向迭代器和随机访问迭代器。

这五类迭代器的通用功能有:

比较两个迭代器是否相等(==、!=)、前置和后置递增运算(++)、读取元素的解引用运算符(*)、箭头运算符(->)。

五类迭代器的功能差异如下:

常见的容器的迭代器类型:

容器 迭代器功能类型
vector 随机访问迭代器
deque 随机访问迭代器
list 双向迭代器
set / multiset 双向迭代器
map / multimap 双向迭代器
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

(3) Vector

想要使用vector容器就需要包含文件#include <vector>

vector是可变长的动态数组,可以存放任意数据类型和对象,是一种顺序容器。vector 容器经常用来构造顺序表,支持随机访问迭代器,且所有 STL 算法都能对 vector 进行操作。vector 容器使用连续的内存来存放元素,且和数组一样支持随机访问,如果在非头尾的位置插入或删除数据的效率较低。vector 容器的存储空间是自动维护的,当往 vector 填充数据元素时,vector 容器往往会分配比所需空间更大的存储空间,以便于开发者向容器中填充新的数据。当存储空间不足以存储数据时,vector 会自动以 2 倍的大小进行扩容,相比于限定存储大小且需要时刻提防越界的数组而言,vector 具有更强大的功能。

  • vector容器的创建

定义:vector<数据类型> 变量名;

vector<int> vec;  //一维动态数组
vector<vector<int>> vec1;  //C++11之前要有空格:vector<vector<int> > vec1;  // 二维动态数组

构造函数:

vector<int> v1;  //v1是空的vector
vector<int> v2(50);  //创建大小为50的vector容器v2
vector<int> v3(50, 1);  //创建大小为50的vector容器v2,并用1填充
vector<int> v4(v2);  //v4是v2的拷贝
int array[5] = {12345}
vector<int> v5(array, array + 5)  //将数组array的[array, array + 5)范围的元素复制到容器v5中
  • vector容器添加和删除元素
方法 说明 示例
void push_back(elm) 在vector容器末尾添加元素elm v1.push_back(1)
iterator insert(it, elm) 在vector容器中迭代器it指向的位置之前添加元素elm v1.insert(v1.end(), 2)
iterator insert(it, n, elm) 在vector容器中迭代器it指向的位置之前添加n个元素elm v1.insert(v1.begin(), 3, 5)
iterator insert(it, first, last) 在vector容器中迭代器it指向的位置之前添加另一个相同类型的vector容器中[first, last)范围的所有元素 v1.insert(v1.begin() + 3, v2.begin(), v2.end())
iterator emplace(it, args) 在vector容器中迭代器it指向的位置之前添加一个元素elm, args是新元素类型的构造函数的参数 v1.emplace(v1.begin(), 1.1, 5.2)
iterator erase(it) 删除vector容器中迭代器it指向的位置处的元素elm v1.erase(v1.begin())
iterator erase(first, last) 删除vector容器中迭代器[first, last)范围的所有元素 v1.erase(v1.begin(), v1.end())
void pop_back() 删除vector容器中的最后一个元素 v1.pop_back()
void clear() 删除vector容器中的所有元素 v1.clear()
  • vector容器遍历
方法 说明 示例
reference front() 返回vector容器的首元素的引用 v1.front()
reference back() 返回vector容器的尾元素的引用 v1.back()
reference at(int n) 返回vector容器位置n处的元素的引用 v1.at(2)
  • vector容器大小判断
方法 说明 示例
int size() 返回vector容器中的元素个数 v1.size()
int max_size() 返回vector容器中的最大可容纳元素个数 v1.max_size()
int capacity() 返回vector容器中的可容纳的元素的最大值 v1.capacity()
bool empty() 判断容器是否为空 v1.empty()
  • Vector动态二维数组

vector动态二维数组的定义为:

vector<vector<int> > vec(m, vector<int>(n)); //创建一个m*n的二维数组

对于vector的二维动态数组,vec.size()是二维数组的行数,vec[m].size()是二维数组的列数。

vector<vector<int> > v(3, vector<int>(2));  //3*2的二维数组
v = {
    {0, 1},
    {2, 3},
    {4, 5}
}  // 二维动态数组初始化
// v = {0, 1, 2, 3, 4, 5};  

//二维数组的遍历
for(int i = 0; i < v.size(); i++)  // v.size()获取数组行向量的大小
{
    for(int j = 0; j < v[i].size(); j++)  //v[i].size()获取数组列向量的大小
    {
        cout << v[i][j];
    }
    cout << endl;   
}

int n = 3;
vector<vector<int> > v1(n);  //二维数组,目前只定义了行数
int m = 3;  //设置列数
for(int i = 0; i < v1.size(); i++)  // v.size()获取数组行向量的大小即行数
{
    for(int j = 0; j < m; j++)  // m即为列数
    {
        v1.push_back(i*j);
    }  
}

/*v1此时为:
0 0 0
1 2 3
2 4 6
*/

//在v1最后一行后面再插入一行全为0的行;新插入一行相当于在每一列末尾插入一个元素
vector<int> temp;
v1.push_back(temp);
int pos = v1.size() - 1;  //pos此时为新行的起始地址
v1[pos].push_back(0);
v1[pos].push_back(0);
v1[pos].push_back(0);
/*v1此时为:
0 0 0
1 2 3
2 4 6
0 0 0
*/

//在v1最后一列后面再插入一列全为1的列;新插入一列相当于在每一行末尾插入一个元素
pos = v1.size();  //pos此时是新列的起始地址
for(int i = 0; i < pos; i++)
    v1.push_back(1);
/*v1此时为:
0 0 0 1
1 2 3 1
2 4 6 1
0 0 0 1
*/

(4) Deque

想要使用容器deque需要包含文件#include <deque>

deque 是 double-ended queue 的缩写,它是双端队列容器。与vector相比,deque不论在尾部或头部插入元素,都十分迅速,而在中间插入元素则会比较费时,因为必须移动中间其他的元素。deque是一种随机访问的数据类型,提供了在序列两端快速插入和删除操作的功能,它可以在需要的时候改变自身大小,完成了标准的C++数据结构中队列的所有功能。

  • deque的操作

Deque的创建与增删元素等操作与vector相同,只是Deque允许额外的操作在序列头部添加和删除元素

方法 说明 示例
void push_front(elm) 在双端队列容器deque的头部插入元素elm d1.push_front(0)
void pop_front() 删除双端队列容器deque的头部元素 d1.pop_front()
deque<int> d1(5) = {1, 2, 3, 4, 5}
vector<int> v1(5) = {1, 2, 3, 4, 5}
d1.push_front(0);  // 0, 1, 2, 3, 4, 5
v1.push_front(0);  //错误
d1.pop_front();  //1, 2, 3, 4, 5
v1.pop_front();  //错误

(5) List

想要使用容器deque需要包含文件#include <list>

list 容器是双向链表容器,是以双向链表的形式实现的一种顺序型容器。由于 list 容器在底层是以链表的形式实现,因此容器中的元素并不存储于连续的内存空间中对于已知的任何位置都能做到快速插入或删除元素list 容器并不能像 vector 容器和数组那样支持随机访问也不能用中括号访问数据,想要获取其中的某个数据就必须遍历。由于 list 容器实现的是双向链表,因此容器中任何一个元素访问其前驱和后继都很方便。若在实际情况下不需要对容器进行随机访问,且容器将会进行大量添加或删除元素的操作,建议使用 list 容器来存储。

  • list容器的创建
list<int> l1;  //l1是空的list
list<int> l2(50);  //创建大小为50的list容器l2
list<int> l3(50, 1);  //创建大小为50的list容器l3,并用1填充
list<int> l4(l2);  //l4是l2的拷贝
int array[5] = {12345}
list<int> l5(array, array + 5)  //将数组array的[array, array + 5)范围的元素复制到容器l5中
  • list容器的操作

对list容器的操作与vector容器相同,以下是list容器常用的方法:

方法 说明 示例
void push_back(elm) 在list容器尾部增加一个元素elm l1.push_back(1)
void pop_back() 删除list容器尾部的元素 l1.pop_back()
void push_front(elm) 在list容器头部增加一个元素elm l1.push_front(1)
void pop_front() 删除list容器头的元素 l1.pop_front()
void sort() 对列表进行快速排序 l1.sort()
void remove(const T &x) 删除list列表中和x相等的元素 l1.remove(0)
  • vector、deque和list的选择

(1)如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector

(2)如果需要随机存取,而且关心两端数据的插入和删除,则应使用deque

(3)如果需要大量的插入和删除,而不关心随机存取,则应使用list

(6) Array

想要使用vector容器就需要包含文件#include <array>

array 容器是 C++11 中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全,且效率并没有因此变差。和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法增加或移除元素,它只允许访问或者替换存储的元素

  • array的定义

定义array时,必须要指定array的数据类型和大小,并且array数组的大小不能由变量来指定(C++内置数组可以)

array<int,5> arr1;
array<int,10> arr2{1, 2, 3, 4, 5};  //只初始化了前5个元素,其他元素会被初始化为0
arr1 = arr2;
array<double,5> arr3(arr2);  //array数组可以用另外一个array数组构造
int n = 5;
array<int, n> arr4;  //错误,array数组大小不能由变量指定

int a[5] = {1, 2, 3, 4, 5};  //内置数组
array<int,5> arr5 = a;  //错误,array数组不能用内置数组构造
  • array数组的访问
方式 说明 示例
at(int n) 返回索引为n的元素的引用,同时进行越界检查 arr1.at(2)
[] 返回指定的元素的引用 arr1[2]
front() 返回第一个元素的引用 arr1.front()
back() 返回最后一个元素的引用 arr1.back()
data() 返回指向内存中数组第一个元素的指针 int* p = arr1.data()

array同样也支持迭代器访问,包括begin()、end()、rbegin()、rend()等。

正是由于 array 容器中包含了 at() 这样的下标越界检查函数,使得array数组操作元素时比普通数组更安全。

(7) String

想要使用string类和容器需要包含文件#include <string>

  • string的构造
方式 说明
string s; 构造空字符串s
string s(string s1) 构造字符串s为字符串s1的拷贝
string s(int n, char c) 构造n个c字符组成的字符串s
string s(string s1, int index) 构造字符串s为字符串s1从下标index开始到结束部分的字符串
string s(string s1, int index, int len) 构造字符串s为字符串s1从下标index开始长度为len部分的字符串
string str1;  //空字符串
string str2 = "abcdef";
string str3(str2);  //字符串str3是字符串str2的拷贝,str3 = "abcdef"
string str4(10,'A')  // str4 = "AAAAAAAAAA"
string str5(str3,1)  //str5 = "bcdef"
string str6(str3, 0, 3)  //str6 = "abc"
  • string的大小
方式 说明
size()和length() 返回字符串长度即字符的个数
max_size() 返回string对象最多包含的字符数,超出会抛出length_error异常
capacity() 返回重新分配内存之前,string对象能包含的最大字符数
string s = "12345";

cout << s.size() << " " << s.length() << " " << s.max_size() << " " << s.capacity() << endl;
//运行后的结果: 5 5 4611686018427387903 15
  • string比较

string的比较支持运算符== != < > <= >=等,同时也可以使用s.compare(s1)方法来比较,若s == s1则返回0,若s > s1则返回1,若s < s1则返回-1。

string s = "helloworld";
string s1 = "hello";
s > s1  //true
int res = s.compare(s1)  //res = 1

  • string增删
方式 说明 示例
void push_back(char c) 在字符串的末尾插入字符c s.push_back('a')
iterator insert(pos, char c) 在字符串指定位置pos之前插入字符c s.insert(s.begin(), 'b')
iterator erase(pos) 删除字符串pos处的字符 s.erase(s.begin() + 1)
iterator erase(first, last) 删除字符串迭代器范围[first, last)的所有字符 s.erase(s.begin(), s.end())
string& erase(pos, int len) 删除字符串从pos位置开始长度为len的字符 s.erase(s.begin(), 5)
void clear() 删除字符串的所有字符 s.clear()
string s = "123456789";
s.erase(s.begin()+1, s.end()-2);  //s = "189"
s.insert(s.begin() + 12);  //s = "1289"
  • string拼接

可以使用s.append(s1)方法在字符串s后拼接字符串s1,或者使用运算符+(s += s1)

string s = "abc";
string s1 = "def";
s.append(s1);  // s = "abcdef"
string s3 = s1 + s;  // s = "defabcdef"
  • string访问

可以使用下标来访问字符串的字符,也可通过迭代器遍历访问字符串。

  • string查找和替换
方法 说明
size_t find(const char* s, size_t pos = 0) const; 在当前字符串中查找子串s返回找到的第一个子串的位置索引,不指定pos则pos默认为0,查找不到则返回npos(-1)
size_t find(char c, pos = 0) const; 在当前字符串中查找字符c返回找到的第一个字符的位置索引,不指定pos则pos默认为0,查找不到则返回npos(-1)
size_t rfind (const char* s, size_t pos = npos) const; 在当前字符串中反向查找子串s返回找到的第一个子串的位置索引,不指定pos则pos默认为-1,查找不到则返回npos(-1)
size_t rfind (char c, size_t pos = npos) const; 在当前字符串中反向查找字符c返回找到的第一个字符的位置索引,不指定pos则pos默认为-1,查找不到则返回npos(-1)
size_t find_first_of (const char* s, size_t pos = 0) const; 在当前字符串中查找位于字符串s中的字符,返回找到的第一个字符的位置索引,不指定pos则pos默认为0,查找不到则返回npos(-1)
size_t find_first_not_of (const char* s, size_t pos = 0) const; 在当前字符串中查找不位于字符串s中的字符,返回找到的第一个字符的位置索引,不指定pos则pos默认为0,查找不到则返回npos(-1)
size_t find_last_of(const char* s, size_t pos = npos) const; 在当前字符串中查找位于字符串s中的字符,返回找到的最后一个字符的位置索引,不指定pos则pos默认为0,查找不到则返回npos(-1)
size_t find_last_not_of (const char* s, size_t pos = npos) const; 在当前字符串中查找不位于字符串s中的字符,返回找到的最后一个字符的位置索引,不指定pos则pos默认为0,查找不到则返回npos(-1)
string& replace(size_t pos, size_t n, const char *s); 将当前字符串从pos索引开始的n个字符,替换成字符串s
string& replace(size_t pos, size_t n, size_t n1, char c); 将当前字符串从pos索引开始的n个字符,替换成n1个字符c
string& replace(first, last, const char* s) 将当前字符串的迭代器范围[first, last)的字符替换为字符串s
string s("abcdefabcdef")
s.find("abc");  // 返回 0
s.find('c');  // 返回 2
s.rfind("abc");  // 返回 6
s.rfind('c');  // 返回 8
s.find_first_of("1Aa%bC")  // 返回 0,s的第一个属于字符串"1Aa%bC"的字符是a
s.find_first_not_of("1Aa%bC")  // 返回2,s的第一个不属于字符串"1Aa%bC"的字符是c
s.find_last_of("1Aa%bC")  // 返回 7, s的最后一个属于字符串"1Aa%bC"的字符是第二个b
s.find_last_not_of("1Aa%bC")  // 返回 11, s的最后一个不属于字符串"1Aa%bC"的字符是第二个f
s.replace(0, 3, "123");  // s = "123defabcdef"
s.replace(0, 6, '0');  // s = "000000abcdef"
s.replace(s.begin(), s.end(), "123abc");  // s = "123abc"

//子串的查找判断:
string s1 = "aabcddd";
string s2 = "abc";
if(s1.find(s2) != s1.npos)
{
    cout << s2 << "是" << s1 << "的子串!" << endl;
}

  • string的分割和截取
方法 说明
char* strtok(char* str, char* split) 将字符串str根据分割字符串split的字符进行分割,返回分割后的第一个子字符串
substr(pos, n) 将从pos位置开始的n个字符截取为子串,n可省略默认为字符尾索引
char str[] = "I,am,a,student; hello world!";
const char *split = ",; !";
char *p = strtok(str,split);  // 获取第一个子字符串
while( p != NULL )
{
    cout<<p<<endl;
    p = strtok(NULL,split);  // 获取其他子字符串
}
//最终打印:
I
am
a
student
hello
world

string s1 = "123456";
string s2 = s1.substr(2,4)  // s2 = "3456"
  • string的字符判断函数
字符判断函数 说明 备注
isalnum() 判断字符是否为字母和数字字符 包括0-9、A-Z、a-z
isalpha() 判断字符是否为字母字符 包括A-Z、a-z
islower() 判断字符是否为小写字母字符 包括a-z
issupper() 判断字符是否为大写字母字符 包括A-Z
isdigit() 判断字符是否为数字字符 包括A-Z、a-z
isspace() 判断字符是否为空白字符 包括空格、换页、换行、回车、制表符
isblank() 判断字符是否为空格字符 包括空格、水平制表符
ispunct() 判断字符是否为标点字符 包括!"#$%&’()*+,-./:;<=>?@[]^_`{

上面这些函数如果是相应字符则返回非零值,否则返回0。

  • string的转换

string字符串中字母大小写转换:

方法 说明
tolower() 将给定字符转换为小写
tosupper() 将给定字符转换为大写

这两个字符转换函数如果转换成功则返回一个可被隐式转换为char类型的int值()(大写字母6590,小写97122)

string字符串与char*、const char*、char[]的转换:

c_str()和data()方法都可以将string字符串转换为const char*,通过将char*char[]赋值给string对象可以转换为string类型。

string与数字的转换:

  • 将字符串转换为数字

(注意:使用这些函数前,需要包含头文件#include <cstring>

方法 说明
stoi(s,p,b) 把字符串s从p开始转换成b进制的int
stol(s,p,b) 把字符串s从p开始转换成b进制的long
stoul(s,p,b) 把字符串s从p开始转换成b进制的unsigned long
stoll(s,p,b) 把字符串s从p开始转换成b进制的long long
stoull(s,p,b) 把字符串s从p开始转换成b进制的unsigned long long
stof(s,p) 把字符串s从p开始转换成float
stod(s,p) 把字符串s从p开始转换成double
stold(s,p) 把字符串s从p开始转换成long double
string s="111.11";
int i = stoi(s,0,10);  // i = 111
int l = stol(s,0,10);  // l = 111
float f = stof(s,0);  // f = 111.11
double d = stod(s,0);   // d = 111.11
  • 将数字转换为字符串

可以用to_string()函数将数字转换为字符串,也可以使用stringstream将数字转换为字符串

double pi = 3.1415926;
string s = "pi is about " + to_string(pi);  // s = "pi is about 3.1415926"

stringstream ss;
ss << pi;
string s1;
ss >> s1;  // s1 = "pi is about 3.1415926"
  • string与输入输出流
方式 说明
os << s 将字符串s写入输出流os中,返回输出流os
is >> s 从输入流is中读入一个以空格分割的字符串给s,返回输入流is
getline(is, s) 从输入流中读入一行内容给字符串s
string s; 
getline(cin,s); // 从输入流cin中读取一行给字符串s
stringstream ss; // 字符串流stringstream
ss << s; //把字符串s写入字符串流ss中
string p; 
while(ss >> p){  // ss >> p是指从输入流ss中读入一个空白分割的字符串给p
    cout << p << endl; 
}

(8) stack和queue

想要使用stack就要包含文件#include <stack>

stack是栈容器,从数据结构的角度上说栈是一种操作受限的线性表,访问、添加和删除元素都只能对栈顶进行操作栈内的元素是不允许访问的,要访问栈内的元素就只能将其上方的元素出栈,使之变成新的栈顶。

  • stack容器的定义

stack容器定义:stack<数据类型名> 变量名;

  • stack容器操作
方法 说明
void pop() 栈顶元素出栈
T & top() 返回栈顶元素的引用
void push (const T & x) 元素x入栈
bool empty() const 判断栈容器是否为空
int size() 返回栈的元素个数
stack<int> s;
s.push(1);
s.push(2);  
s.push(3);  // 三个元素入栈

while(s.empty() != true)  //栈不为空
{
    cout << s.top() << " ";  // 打印栈顶元素
    s.pop();  // 栈顶元素出栈
}
// 打印结果为: 3 2 1

想要使用queue就要包含文件#include <queue>

queue 是队列容器,从数据结构的角度上说队列也是一种操作受限的线性表,只能访问队列头和队列尾的元素添加和删除元素分别只能对队列尾和队列头进行操作队列内的元素是不允许访问的,要访问队列内的元素就只能将队列头的元素出队列,使之变成新的队列头。

  • queue容器的定义

定义:queue<数据类型> 变量名;

  • queue容器操作
方法 说明
void pop() 队列头元素出队列
T & front() 返回队列头元素的引用
T & back() 返回队列尾元素的引用
void push (const T & x) 元素 x 入队列
bool empty() const 判断容器是否为空
int size() const 返回容器中元素的个数
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
// 三个元素入队列

while(q.empty() != true)  // 队列不为空
{
    cout << q.front() << " ";  // 打印队列头元素
    q.pop();  // 队列头元素出队列
}
//打印结果为:1 2 3

37. C++关联容器

关联容器支持高效的关键字查找和访问操作,2个主要的关联容器类型是mapset

  • map中的元素是一些键值对(key-value):关键字起索引作用,值表示与索引相关联的数据。

  • set中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在set中。

STL中一共提供了八种关联容器:

关联容器类型 说明
map 关联数组,储存键值对
multimap 关键字可以重复出现的map
set 存储关键字的容器
multiset 关键字可以重复出现的set
unordered_map 由哈希函数组织的map
unordered_multimap 关键字可以重复出现的unordered_map
unordered_set 由哈希函数组织的set
unordered_multiset 关键字可以重复出现的unordered_set

(1) 定义关联容器

想要使用map/multimap需要包含文件#include <map>

想要使用set/multiset需要包含文件#include <set>

map和multimap大体相同,唯一的不同是,map的键值key不可重复,而multimap可以,也正是由于这种区别,map支持[ ]运算符,multimap不支持[ ]运算符。

map容器的定义:map<数据类型名1, 数据类型名2> 变量名;

set容器的定义:set<数据类型名> 变量名;

定义好关联容器之后,就可以往其中插入键值对或关键字了。

键值对pair是一个模板类型,保存两个名为firstsecond的公有数据成员。map所使用的pairfirst成员保存关键字,用second成员保存对应的值。

map<string, int> twords; 
twords["C++"] = 1;  // 往words中添加键值对 {"C++": 1}

//统计输入的每个单词的个数
map<string, int> words;
string word;
while(cin >> word)
{
    words[word]++;
}

// 打印map
for (const auto &w : words)    // 范围for循环
    cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << endl;

(2) 关联容器初始化

初始化map时,提供的每个键值对用花括号{}包围。

map<string, string> authors =
{
    {"Joyce", "James"},
    {"Austen", "Jane"},
    {"Dickens", "Charles"}
};

set<string> languages = {"C++", "Java", "Python"};

(3) 关联容器类型别名

关联容器定义了类型别名来表示容器关键字和值的类型:

类型别名 说明
key_type 关联容器的关键字的类型
value_type 关联容器的值的类型
mapped_type 关联容器中与关键字关联的值的类型,只有map容器才有

对于set类型,key_typevalue_type是一样的,set中保存的值就是关键字。对于map类型,元素是关键字-值对,即每个元素是一个pair对象,包含一个关键字和一个关联的值。由于元素关键字不能改变,因此pair的关键字部分是const的。另外,只有map类型(unordered_mapunordered_multimapmultimapmap)才定义了mapped_type

set<string>::value_type v1;        // v1 is a string
set<string>::key_type v2;          // v2 is a string
map<string, int>::value_type v3;   // v3 is a pair<const string, int>
map<string, int>::key_type v4;     // v4 is a string
map<string, int>::mapped_type v5;  // v5 is an int

(4) 关联容器迭代器

关联容器同样支持begin()、end()、rbegin()、rend()、cbegin()、cend()、crbegin()、crend()这八个迭代器。

解引用关联容器迭代器时,会得到一个类型为容器的value_type的引用。对map而言,value_typepair类型,其first成员保存const的关键字,second成员保存值。

auto map_it = words.begin();
cout << map_it->first << " " << map_it->second << endl;
cout << (*map_it).first << " " << (*map_it).second << endl;

虽然set同时定义了iteratorconst_iterator类型,但两种迭代器都只允许只读访问set中的元素。类似mapset中的关键字也是const的。

set<int> iset = {0,1,2,3,4,5,6,7,8,9};
set<int>::iterator set_it = iset.begin();
*set_it = 10;  // 错误,set的迭代器是只读的

(5) 关联容器插入

insert()方法和emplace()方法都可以在关联容器中插入元素。

map<string, int> student;
student.insert(pair<string, int>("Bob", 14));
student.insert(pair<string, int>("Alice", 13));
student.insert(student<string, int>::value_type("Jack", 15));
student.insert({"Cindy", 14});
student.insert(make_pair("David", 12));

set<string> names;
names.insert("Bob");
names.insert("Alice");
names.insert("Jack");

insertemplace的返回值依赖于容器类型和参数:

  • 对于map/set容器,添加单一元素的insertemplace方法会返回一个pair,表示操作是否成功,pairfirst成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,如果关键字已在容器中,则insert直接返回,bool值为false;如果关键字不存在,元素会被添加至容器中,bool值为true

  • 对于multimap/multiset容器,添加单一元素的insertemplace返回指向新元素的迭代器。

(6) 关联容器遍历

// 迭代器遍历map
map<string, int> student;
map<string, int>::iterator map_it;
for(map_it = student.cbegin(); map_it != student.cend(); ++map_it)
    cout<< map_it.first << " " << map_it.second << endl;

// 迭代器遍历set
set<string> names;
set<string> set_it;
for(set_it = names.cbegin(); set_it != set_it.cend(); ++set_it)
    cout << *set_it << " ";

(7) 关联容器删除

方法 说明
c.erase(k) 在容器c中删除匹配关键字k的所有元素,返回实际删除的元素数量
c.erase(p) 在容器c中删除迭代器p所指向的位置的元素,返回删除元素之后的下一个元素位置的迭代器
c.erase(b,e) 删除容器c中迭代器范围为[b,e)的所有元素,返回迭代器e

对于不包含重复关键字的关联容器,erase的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。

(8) 关联容器查找

方法 说明
c.find(k) 返回一个指向关键字为k的(第一个)元素的迭代器,如果未找到则返回c.end()
c.count(k) 返回关键字为k的元素的个数,对于关键字不可重复的容器,该方法的返回值总为0或1
c.lower_bound(k) 返回一个指向第一个键值大于等于关键字为k的元素的迭代器
c.upper_bound(k) 返回一个指向第一个键值大于关键字为k的元素的迭代器
c.equal_range(k) 返回一个迭代器对指向查到关键字为k的元素

lower_boundupper_bound操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,lower_bound返回的迭代器会指向第一个匹配给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配元素之后的位置。如果关键字不在multimap中,则lower_boundupper_bound会返回相等的迭代器,指向一个不影响排序的关键字插入位置。因此用相同的关键字调用lower_boundupper_bound会得到一个迭代器范围,表示所有具有该关键字的元素范围。

equal_range操作接受一个关键字,返回一个迭代器pair。若关键字存在,则第一个迭代器指向第一个匹配关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置。若关键字不存在,则两个迭代器都指向一个不影响排序的关键字插入位置。

(8) 关联容器排序

map和set中的元素是自动按Key升序排序,所以不能对map用sort函数;

STL中默认是采用小于号来排序的,因此当关联容器的关键字的类型为结构体或类等类型的时候,需要重载<运算符。

38. C++泛型算法

STL算法部分主要由头文件 <algorithm>,<numeric>,<functional> 组成。要使用 STL中的算法函数必须包含头文件 <algorithm>,对于数值算法须包含 <numeric><functional> 中则定义了一些模板类,用来声明函数对象。

下面是一些常用的泛型算法,其中除了算术生成算法是在头文件<numberic>中之外,其他算法都在头文件 <algorithm>中。

(1) 遍历算法

算法 说明
for_each(iterator beg,iterator end,_func) 在迭代器范围内遍历容器,然后进行自定义操作_fun
transform(iterator beg1,iterator end1,iterator beg2, _func) 按照搬运规则函数_fun将原容器迭代器范围[beg1,end1]内的元素搬运到目标容器的起始迭代器beg2处

注意:数组的首地址即第一个元素的地址相当于起始迭代器begin(),数组的最后一个元素的下一个位置的地址相当于结尾迭代器end()。

void printArr(int val)  //自定义操作:打印
{
	cout << val << " ";
}

int targetArr(int val)  //搬运规则函数,在原数据基础上加100
{
	return val + 100;
}

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };  //原数组
	int target[15];  //目标数组,目标数组的长度必须不小于原数组
	transform(arr, arr + 10, target, targetArr);  // 将原数组全部数据加100之后搬运到目标数组中,注意是arr + 10
	cout << "原数组:";
	for_each(arr, arr + 10, printArr);  // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    cout << endl;
	cout << "目标数组:";
	for_each(target, target + 10, printArr);  // 100, 101, 102, 103, 104, 105, 106, 107, 108, 109
}

(2) 查找算法

算法 说明
iterator find(iterator beg, iterator end, value) 查找迭代器范围内与值value相等的元素,若找到则返回指向该元素的迭代器,否则返回迭代器end
iterator find_if(iterator beg, iterator end, _callback); 按照查找规则_callback在迭代器范围内查找元素, _callback是bool类型的函数,若找到则返回指向该元素的迭代器,否则返回迭代器end
iterator adjacent_find(iterator beg, iterator end); 查找迭代器范围内相邻且重复的元素,若找到则返回相邻重复元素第一个位置的迭代器,否则返回迭代器end
bool binary_search(iterator beg, iterator end, value); 查找迭代器范围内与值value相等的元素,若找到则返回true,否则返回false
int count(iterator beg, iterator end, value); 查找迭代器范围内与值value相等的元素出现的次数,若找到则返回相同元素出现的次数,否则返回0
int count_if(iterator beg, iterator end, _callback); 按照查找规则_callback在迭代器范围内查找元素, _callback是bool类型的函数,若找到则返回满足查找规则的元素个数,否则返回0
iterator max_element(interator beg, iterator end); 查找容器迭代器范围内的最大元素,返回指向查找到的第一个最大元素位置的迭代器
iterator max_element(interator beg, iterator end); 查找容器迭代器范围内的最小元素,返回指向查找到的第一个最小元素位置的迭代器
int a[] = {1, 2, 3, 4, 5, 6, 7, 8};
vector<int> v1(a);

bool Function(int value)  //谓词,判断value是否大于5
{
    return value > 5;
}


auto it1 = v1.find(v1.begin(), v1.end(), 5);  //查找元素5
cout << *it1 <<endl;  // 5

auto it2 = v1.find_if(v1.begin(), v1.end(), Function);  //查找大于5的元素
cout << *it1 <<endl;  // 6

int b[] = {1, 2, 3, 3, 5, 6, 7, 8};
vector<int> v2(b);
auto it3 = v1.adjacent_find(v2.begin(), v2.end());  //查找相邻且重复的元素
cout << *it3 <<endl;  // 3

v2.binary_search(v2.begin(), v2.end(), 9);  //是否有元素9,结果返回false

int count1 = v2.count(v2.begin(), v2.end(), 3)  //查找元素3的个数,count1 = 2

int count2 = v2.count(v2.begin(), v2.end(), Function)  //查找大于5的元素的个数, count2 = 3

查找算法中的value可以是内置数据类型,也可以是自定义数据类型,若是自定义数据类型如类,则需要重载==运算符:

class Person
{
private:
    string m_name;
    int m_age;
public:
    Person(string name, int age)
        :m_name(name), m_age(age) {}
    
    string GetName() const
    {
        return m_name;
    }
    
    int GetAge() const
    {
        return m_age;
    }
    
    bool operator==(const Person& p)  //重载了运算符==
    {
        if(m_age == p.GetAge())
            return true;
        return false;
    }
};

vector<Person> v;
v.push_back(Person("Alice", 13));
v.push_back(Person("Bob", 12));
v.push_back(Person("Cindy", 14));

Person p("David", 14);
auto it = find(v.begin(), v.end(),p); //查找与David年龄相等的人
cout << (*it).GetName() << " " << (*it).GetAge() << endl;  // 打印:Cindy 14

(3) 排序算法

算法 说明
sort(iterator beg,iterator end, _Pred); 对容器或数组的迭代器范围内的元素进行排序,默认是升序,时间复杂度为O(nlog(n))
stable_sort(iterator beg,iterator end, _Pred); 对容器或数组的迭代器范围内的元素进行排序,默认是升序,但不改变相等元素的相对位置
random_shuffle(iterator beg,iterator end); 对容器或数组的迭代器范围内的元素随机调整次序(打乱),之前最好先撒下随机数种子srand((unsigned)time(NULL)),否则每次随机的结果都是一样的。
merge(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator target_beg); 将两个有序的容器或数组的迭代器范围内的元素合并到目标容器中,合并后的目标容器仍然有序
reverse(iterator beg,iterator end); 将容器或数组的迭代器范围内的元素反转
#include <iostream>
#include <ctime>

void myPrint(int val)  //自定义操作,打印
{
	cout << val << " ";
}

bool myComepare(int v1,int v2)  //排序比较规则,降序
{
	return v1 > v2;
}

int main()
{
	int a[10] = { 1,4,7,2,5,8,3,6,9 };  //原数组,乱序
	sort(a, a + 10);  // a数组升序排序,a[10] = { 1,2,3,4,5,6,7,8,9 };
    sort(a, a + 10, myCompare);  // a数组降序排序,a[10] = { 9,8,7,6,5,4,3,2,1 };
    
        
    int b[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    srand((unsigned)time(NULL));  // 撒下随机数种子,否则每次打乱数组得到的结果都是一样的
    random_shuffle(b, b + 10);  //打乱数组b, b[10] = {1, 9, 0, 7, 6, 2, 5, 3, 4, 8}
    random_shuffle(b, b + 10);  //再次打乱,b[10] = {7, 3, 4, 0, 2, 1, 5, 6, 9, 8}
    
    int c[5] = {1, 2, 3, 4, 5};
    int d[5] = {5, 4, 3, 2, 1};
    vector<int> v;
    merge(a, a+5, b, b+5, v.begin());  // 合并数组a和数组b到容器v中
    for_each(v.begin(), v.end(), myPrint);  // 此时容器v:1, 1, 2, 2, 3, 3, 4, 4, 5, 5
    
    reverse(d, d + 5);  //反转数组d,此时数组d为 d[5] = {1, 2, 3, 4, 5}   
    return 0;   
}

(4) 替换算法

算法 说明
swap(container c1,container c2); 交换容器c1和容器c2内的元素,或只交换两个元素,两个容器必须是相同类型的
replace(iterator beg,iterator end,oldvalue,newvalue); 将容器迭代器范围内的旧元素oldvalue替换为新元素newvalue
replace_if(iterator beg,iterator end,_Pred,newvalue); 将容器迭代器范围内的满足谓词_Pred条件的所有元素替换为新元素newvalue
bool GreaterThanFive(int value)  // 谓词,判断value是否大于5
{
    return value > 5;
}

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
vector<int> v1(10);
vector<int> v2(10);

for(int i = 0; i < 10; i++)
{
    v1.push_back(a[i]);  // v1 : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    v2.push_back(9 - a[i]);  // v2 : 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
}

swap(v1, v2);  //交换容器v1、v2内的元素
// 此时,v1 : 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ;  v2 :  0, 1, 2, 3, 4, 5, 6, 7, 8, 9

replace(v1.begin(), v1.end(), 1, 2);  // v1 : 9, 8, 7, 6, 5, 4, 3, 2, 2, 0

replace_if(v2.begin(), v2.end(), GreaterThanFive, 0);  // v2 :  0, 1, 2, 3, 4, 5, 0, 0, 0, 0

(5) 集合算法

算法 说明
set_intersection(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator target_beg) 求容器1和容器2对应迭代器范围内的元素的交集,并把这个交集传给目标容器;
set_union(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator target_beg) 求容器1和容器2对应迭代器范围内的元素的并集,并把这个并集传给目标容器;
set_difference(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator target_beg) 求容器1和容器2对应迭代器范围内的元素的差集,并把这个差集传给目标容器;容器1相对于容器2的差集是,容器1内元素去除容器1和容器2交集部分所剩余的元素

求交集、并集、差集的两个容器必须都是有序序列,并且同序(同为升序或同为降序),否则会报错。

int a[5] = {1, 2, 3, 4, 5};
vector<int> v1(a);
int b[7] = {3, 4, 5, 6, 7, 8, 9};
vector<int> v2(b);

vector<int> target_vector1;  
target_vector1.resize(min(v1.size(), v2.size()));  // 设置目标容器的大小
set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), target_vector1);  // 求容器v1和v2元素的交集,把交集传给target_vector1, target_vector1: 3, 4, 5

vector<int> target_vector2;
target_vector2.resize(v1.size() + v2.size()); // 设置目标容器的大小
set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), target_vector2);  // 求容器v1和v2元素的并集,把交集传给target_vector2,target_vector2:1,2, 3, 4, 5, 6, 7, 8, 9

vector<int> target_vector3;
target_vector3.resize(v1.size()));  // 设置目标容器的大小
set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), target_vector2);  // 求容器v1相对容器v2的差集,把这个差集传给target_vector3,target_vector3:1,2

vector<int> target_vector4;
target_vector4.resize(v2.size());  // 设置目标容器的大小
set_union(v2.begin(), v2.end(), v1.begin(), v1.end(), target_vector2);  // 求容器v2相对容器v1的差集,把这个差集传给target_vector4,target_vector4:6,7,8,9

(6) 算术生成算法

算法 说明
accumulate(iterator beg,iterator end,value); 计算容器迭代器范围内的元素总和与起始值value的和,并把计算结果返回,若不需要起始值value可设置为0
fill(iterator beg,iterator end,value); 向容器的迭代器范围内填充指为value的元素
#include <numberic>  //注意算术生成算法的头文件
int a[] = {1, 2, 3, 4, 5};
vector<int> v1(a);

int sum = v1.accumulate(v1.begin(), v1.end(), 0);  // sum = 15;

vector<int> v2(5);
v2.fill(v2.begin(), v2.end(), 1);  // v2: 1 1 1 1 1

(7) 排列算法

算法 说明
bool next_permutation (first, last, cmp); 根据比较规则cmp(默认是<),生成一个序列的重排列(生成的重排列大于原序列),若序列已是最大排列,则返回false
bool prev_permutation (first, last, cmp); 根据比较规则cmp(默认是>),生成一个序列的重排列(生成的重排列小于原序列),若序列已是最小排列,则返回false
bool is_permutation (first1, last1, first2, end2, cmp); 根据排序规则cmp,检查序列1是不是序列2的排列,若是则返回true,否则返回false
#include <algorithm>
//利用next_permutation()求序列的全排列
int a[3] = {1, 2, 3};
sort(a, a+3);  //注意,求全排列前要先排序
while(next_premutation(a, a+3))
{
    for(int i = 0; i < 3; i++)
        cout << a[i] << " ";
    cout << endl;
}
/*将打印:
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
*/

int b[3] = {3, 2, 1};
while(prev_premutation(b, b+3))
{
    for(int i = 0; i < 3; i++)
        cout << b[i] << " ";
    cout << endl;
}
/*将打印:
3 1 2
2 3 1
2 1 3
1 3 2
1 2 3
*/

vector<int> v1 = {1, 2, 3, 4};
vector<int> v2 = {1, 2, 4, 3};
vector<int> v3 = {5, 1, 2, 4, 3};
is_permutation(v1.begin(), v1.end(), v2.begin(), v2.end());  //true;
is_permutation(v2.begin(), v2.end(), v3.begin());  //false;
posted @   N1rv2na  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示