C++基础
C++编译器
编译器要完成的就是生成obj文件
文件只是提供给编译器源代码的一种方式
文件后缀的默认约定
我们负责告诉编译器输入的是什么类型的文件,以及编译器该如何处理它(类似.cpp编译器当做C++文件,.c当做c语言文件,.h当做头文件,这些是默认的约定,事实上我命名一个.abc后缀的文件,只要我告诉编译器这是C++,就能正常编译)
1.命令行参数:如果你使用命令行编译器,你可以在编译命令中使用参数来指定文件类型。例如,使用-x参数指定文件类型,如-x c++表示该文件是C++文件。例如:
g++ -x c++ myfile.abc -o myprogram
2.集成开发环境(IDE)设置**:如果你使用集成开发环境(如Visual Studio、Xcode、Eclipse等),通常可以在项目设置或者文件属性中指定文件类型。在项目或文件属性设置中,通常有一个选项可以指定文件类型或编译器应该如何处理该文件。
3.文件头注释**:有些编译器可以通过检查文件的内容来确定文件类型。你可以在文件的开头添加注释,明确指定文件类型。例如,在C++文件中,你可以添加类似于以下内容的注释:
// File: myfile.abc
// Language: C++
预处理语句
C++预处理器是C++编译器的一部分,它在实际的编译过程之前对源代码进行处理。预处理器的主要任务是执行预处理指令,这些指令以#字符开头。预处理器并不关心C++语法,它只是简单地根据指令对文本进行替换、删除或添加。
预处理器的本质可以概括如下:
1.文本替换:预处理器会执行类似于宏替换的功能,即将代码中出现的宏名称替换为其定义的文本内容。例如,#define指令可以定义宏,并在代码中进行替换。例如
#define INTEGER int
作用是将代码中所有出现的 INTEGER 替换为 int。
2.文件包含:预处理器可以将其他文件的内容包含到当前文件中,使用#include指令。这样可以使得代码模块化,方便管理和维护。
3.条件编译:预处理器可以根据条件来决定编译哪些代码,使用诸如#if、#ifdef、#ifndef、#else和#endif等指令。这样可以根据不同的条件编译不同的代码,实现跨平台兼容性或者调试功能。
4.其他预处理指令:预处理器还支持其他一些指令,如#error用于产生错误消息、#pragma用于向编译器传递特定的指令等。
设置预处理结果到文件
设置完后,按ctrl+F7,编译(设置成true,不会生成.obj文件,因此无法生成成功)
后生成当前文件.i文件有预处理后的代码
汇编程序输出
编译器生成的obj文件是机器码,完全不可读,可以设置汇编程序输出为/FA,将会生成.asm文件
这里面内容很多,这是因为我们是在调试模式下编译的,它没有做任何优化,并做了很多额外的事,使得代码很容易调试
代码优化设置
在优化中选择最大速度优化,直接编译会报错
“/O2”和“/RTC1”命令行选项不兼容
需要把运行时基本检查设置为默认值
C++链接
catl+F7只会进行编译,永远不会进行链接
只有按下生成按钮或者F5进行运行时才会进行链接
定义
C++链接的本质是将多个编译生成的目标文件(.o、.obj等)合并成一个可执行文件或者动态链接库(DLL),并解决符号引用与定义的匹配、地址重定位等问题,以确保程序能够正确地运行。
错误类型辨别
字母C开头,说明错误发生在编译阶段
error C2143: 语法错误: 缺少“;”(在“}”的前面)
LNK开头说明错误发生在链接阶段
error LNK2019: 无法解析的外部符号 main,函数 "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) 中引用了该符号
visual studio最佳设置
输出目录(Platform)$(Configuration)\
..\CppProject\bin\Win32\Debug\
中间目录(Platform)$(Configuration)\
..\CppProject\bin\intermediates\Win32\Debug\
C++指针(*)
指针是一个整数,一种存储内存地址的数字
创建一个空指针
void* ptr = 0;
0不是一个有效的内存地址,0是无效的,是无法从内存地址0中读取或写入的
void* ptr = NULL;
void* ptr = nullptr;
同上
&取地址运算符
int var = 8;
void* ptr = &var;
& 符号是 C++ 中的取地址运算符
&var 就是取变量 var 的地址,然后将该地址存储到 ptr 指针中。
这个地址就是变量var的内存地址
在该图片的目录下打开内存视图
输入地址,可以看到前4个字节就是int变量var的值
对于内存地址来说类型无关紧要,但类型对该内存的操作很有用
因为编译器知道一个int是4个字节,当我对int设置值时,它会设置4个字节的内存,但对于内存来讲类型没有意义
*指针的逆向引用
当我们逆向引用指针的时候,如何指针类型是空类型,编译器不知道void是什么,要把10转成2个字节的short,还是4个字节的int,还是8个字节的long long,它不知道要多少个字节
我们需要告诉编译器指针的类型,这样它就知道要多少个字节了
从这里看出,当指定指针的类型后,通过逆向引用对指针内存进行了重新赋值(0a就是代表16进制的10)
指针只是指向内存的一个位置,我们不知道指针指向的数据有多大,因为指针不包含数据,指针是一个整数,是一个内存地址
动态地址分配及释放
char* buffer = new char[8];
memset(buffer, 0, 8);
delete[] buffer;
先动态分配了一个长度为 8 字节的 char 数组,buffer是最开始的char的地址
通过memset方法将从buffer地址开始的第0位,往后8个地址都初始化为0
使用 delete[] 将动态分配的内存释放掉
这里使用了 delete[] 而不是 delete 是因为动态分配的是一个数组,而不是单个对象。在释放动态分配的数组时,应该使用 delete[],而不是 delete,以确保释放整个数组所占用的内存。
指针的指针
指针本身是相当于一个变量来存储内存地址,它指向另一个变量
char* buffer = new char[8];
memset(buffer, 0, 8);
char** ptr = &buffer;
可以看到ptr的内存地址是0x0000005e1fb4fc58,它的值是0x0000020d77a69ea0,这正是buffer的内存地址
C++引用(&)
定义
C++ 中的引用是一种对已存在的变量起的别名或者另一个名称。引用提供了一种让多个名称指向同一块内存地址的机制。引用本质上是一个别名,它在内存中不会额外分配空间,它只是原变量的另一个名字。
引用的本质可以从以下几个方面来理解:
别名:引用是变量的另一个名称,它允许在代码中使用不同的名称来访问相同的内存位置。
不同于指针:引用与指针不同,指针是一个对象,它存储了另一个对象的地址;而引用是目标对象的别名,它在语法上类似于目标对象本身,但在内部实现上,编译器会将其转换为指向目标对象的指针。
操作符重载:在 C++ 中,引用的本质可以通过对引用进行操作符重载来理解。对引用的赋值操作实际上是对目标对象的赋值,对引用的取地址操作实际上是对目标对象的地址取值。
传递参数:引用在函数参数传递中起到了很重要的作用,它可以使得函数能够直接修改传入的参数,而不是创建参数的副本。
总的来说,引用是 C++ 中的一种重要机制,它提供了简洁、方便的方式来操作变量,并且在函数参数传递等方面有着重要的应用。
与指针的区别
-
语法:
- 引用使用
&符号声明,例如int& ref = var;,其中ref是var的引用。 - 指针使用
*符号声明,例如int* ptr = &var;,其中ptr是指向var的指针。
- 引用使用
-
内存占用:
- 引用不会占用额外的内存空间,它只是目标变量的别名。
- 指针需要额外的内存空间来存储目标变量的地址。
-
赋值和初始化:
- 引用在声明时必须进行初始化,并且不能在后续改变引用的目标。
- 指针可以先声明再赋值,并且可以修改指针指向的目标。
-
空值:
- 引用必须始终引用一个有效的对象,不能引用空值。
- 指针可以为空,即指向空地址或者空对象。
-
解引用:
- 引用在使用时无需解引用操作,直接使用引用即可访问目标对象。
- 指针需要通过解引用操作(
*ptr)来访问目标对象。
-
函数参数传递:
- 引用作为函数参数时,可以直接修改原始变量的值。
- 指针作为函数参数时,需要通过指针的解引用来修改原始变量的值。
用例
别名
#include <iostream>
#define Log(x) std::cout << x << std::endl;
int main()
{
int a = 5;
int& b = a;
b = 10;
Log(a);
std::cin.get();
}
我们创建了a的引用b,对b进行操作,本质上是对a进行操作,最后打印a,结果是10
传递参数
#include <iostream>
#define Log(x) std::cout << x << std::endl;
void Increment(int* value)
{
(*value)++;
}
int main()
{
int a = 5;
Increment(&a);
Log(a);
std::cin.get();
}
这个是用指针实现参数传递,先用取地址运算符传递变量内存地址,方法内再通过指针的逆向引用,解地址,对地址的值进行操作,可以看到相当复杂
#include <iostream>
#define Log(x) std::cout << x << std::endl;
void Increment(int& value)
{
value++;
}
int main()
{
int a = 5;
Increment(a);
Log(a);
std::cin.get();
}
这是用引用实现的参数传递,可以看到,对比起来简洁了很多,相当于是语法糖,引用能做的,指针全能做
引用特性
引用申明不可变更
可以看到 在ref变量声明为a后,再次对ref进行赋值,进行的是值操作不再是声明,所有打印出来的a和b的值都是8
而指针是可以变更地址的
C++类
在C语言中是没有面向对象编程的概念的,C++中添加了这一特性
Q&A
class Player
{
public:
int x, y;
int speed;
};
int main()
{
Player player;
player.x = 5;
std::cin.get();
}
C++中面向对象编程的概念,和类,以及C++中的类实例化为什么不需要new,以及设置public为什么在类里面?
1.面向对象编程(OOP)是一种编程范式,它将数据与操作数据的方法(函数)组合到一个单元中,称为对象。面向对象编程的核心概念包括类(Class)、对象(Object)、封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)
2.类是一种用户定义的数据类型,它定义了一组属性(成员变量)和方法(成员函数),用于描述一类对象的共同特征和行为。 在类中,属性和方法被组织成了一个整体,使得数据和操作数据的函数能够紧密地结合在一起。
3.在 C++ 中,通过声明一个类的对象来创建该类的实例,不需要使用 new 关键字,是因为 C++ 支持自动变量的概念,对象的生命周期由其所在的作用域管理,当对象离开作用域时,其内存会被自动释放。例如,Player player; 就创建了一个名为 player 的 Player 类对象
4.这些关键字的使用方式是在类的定义内部,而不是在类名之前。这种风格是C++的语法规定,与Java或C#等其他语言略有不同
类的本质其实也是语法糖,用类能解决的问题,不用类也能解决,所以才会有C这样完全没有类的语言存在,类能用了组织我们的代码,使得更容易维护
C++结构体(struct)
结构体(struct)和类(class)在语法上非常相似,可以这样说,基本没有区别,只有关于可见度的小区别,在实际使用中,C++保留结构体的唯一原因就是,与c保持向后兼容,c没有类但有结构体
Q&A
结构体和类的区别
默认访问权限
- 在结构体中,默认的访问权限是
public,这意味着结构体的成员在外部可以被直接访问。 - 在类中,默认的访问权限是
private,这意味着类的成员在外部不可以被直接访问,需要通过公有的成员函数进行访问。
成员函数
- 在类中,我们可以定义成员函数(也称为方法),用于操作类的成员变量。
- 在结构体中,也可以定义成员函数,但通常情况下不常用。
继承
- 类支持继承(inheritance),可以从一个类派生出另一个类,从而实现代码重用和扩展。
- 结构体不支持继承。
用途
- 结构体通常用于表示一组相关的数据,比如表示坐标、点等简单的数据结构。
- 类则更适合用于建模复杂的对象,它不仅包含数据,还可以包含用于操作数据的方法,从而实现面向对象编程的特性。
默认继承方式:
- 当你声明一个类时,默认继承方式是
private。 - 当你声明一个结构体时,默认继承方式是
public。
C++保留结构体的原因
C++保留结构体的一个重要原因是为了与C语言保持向后兼容性。在C语言中,结构体是一种非常常见的数据类型,用于组织相关的数据项。当C++设计时,为了使现有的C代码能够在C++中继续使用,保留了对结构体的支持。
在C++中,结构体的功能已经被扩展,使其更接近于类。这样一来,你可以在结构体中定义成员函数、使用访问控制等特性,使得结构体更加灵活,可以用于更复杂的任务。
因此,尽管C++提供了类这样更强大的数据抽象和封装机制,但结构体仍然保留着其在C语言中的特性,以便与C代码进行互操作,并且在某些情况下,结构体仍然是一个合适的选择,特别是对于一些简单的数据组织任务。
理想情况下,将c语言中的struct替换成class,然后将其变成public,就能解决c与c++的兼容性问题吗
在理想情况下,确实可以通过将C语言中的结构体替换为C++中的类,并将其成员变量和成员函数设置为public,来解决C与C++的兼容性问题。这种方法可以确保现有的C代码可以在C++中继续使用,同时还能够利用C++提供的更强大的面向对象特性。
然而,在实际情况下,这种替换可能并不总是可行或合适的。因为在C和C++中,结构体和类的语义以及用法有一些不同之处。例如,在C中,结构体通常用于组织数据,而在C++中,类不仅仅是数据的容器,还包含了成员函数和其他面向对象的特性。因此,简单地将C中的结构体替换为C++中的类可能需要对原始代码进行大量的修改和调整,这可能会带来一定的工作量和风险。
另外,对于一些简单的C代码,保持其原始的结构体定义可能更为简洁和清晰,而不必引入额外的面向对象的特性。因此,在考虑将C代码迁移到C++时,需要综合考虑代码的复杂度、维护成本以及迁移的可行性,以找到最合适的迁移策略。
我应该在什么情况下使用结构体而不去使用类呢
- 简单数据结构:如果你只需要组织一组简单的数据,而不需要涉及复杂的行为和方法,那么结构体可能更适合。比如,用结构体来表示坐标、日期、时间等简单的数据结构是很常见的。
- 数据封装:如果你只需要简单地将数据封装在一起,并且不需要对数据进行复杂的操作或逻辑,那么结构体可能更合适。结构体的成员变量默认是
public的,可以方便地直接访问。 - C语言兼容性:如果你需要与C代码进行交互或者在已有的C代码基础上进行开发,那么结构体可能是一个更自然的选择,因为在C中结构体是常见的数据组织方式。
- 性能要求:在某些情况下,结构体可能比类更轻量级,因为类通常会引入虚函数表等额外的开销。如果对性能有严格的要求,并且不需要类提供的额外功能,那么结构体可能更适合。
总的来说,当你需要简单地组织数据,并且不需要涉及复杂的行为和逻辑时,结构体可能是一个更合适的选择。但是,如果你需要封装数据并定义复杂的行为和逻辑,或者需要利用类提供的面向对象的特性,那么类可能更适合。在选择时,需要综合考虑代码的需求、复杂度、性能要求以及与其他代码的兼容性等因素。
类设计(log类为例)
log类为例
可以先设想我们会怎么使用这个类,再去具体实现
int main()
{
Log log;
int Level = 1;
log.SetLevel(Level);
log.info("信息日志");
log.warn("警告日志");
log.error("错误日志");
std::cin.get();
}
目前设想的是可以设置日志等级,只输出对应等级的日志到控制台,通过对应的方法输出对应等级的日志;
class Log
{
public://分两个public 是因为把不同部分的代码分开,会比较简介和美观
const int LogLevelError = 0;//因为SetLevel要传人int,所以最好先定义好每个int值的意思,使用魔法值会导致可读性很差
const int LogLevelWarning = 1;
const int LogLevelInfo = 2;
private:
int m_LogLevel = LogLevelInfo;//m_LogLevel加m_前缀通常是表明这是一个私有的类成员变量
public:
void SetLevel(int level)
{
m_LogLevel = level;
}
void info(const char* message) {
if(m_LogLevel>= LogLevelInfo)//判断当前日志等级,符合的才打印出来,进行分级处理
std::cout << "[Info]: " << message << std::endl;//对应等级的信息加上"[Info]: "这样的前缀,会有助于我们调试
}
void warn(const char* message) {
if (m_LogLevel >= LogLevelWarning)
std::cout << "[Warning]: " << message << std::endl;
}
void error(const char* message) {
if (m_LogLevel >= LogLevelError)
std::cout << "[Erroe]: " << message << std::endl;
}
};
这里就是进行对应的实现了
C++的静态(static)
static在C++中有两种意思:
1,在类或结构体外部使用
这意味着你声明为static的符号,链接将只是内部,它只对自己的C++文件(翻译单元)可见
2,在类或结构体内部使用
意味着该变量实际上将与类的所有实例共享内存,也就是说不管你类建了多少个实例,静态变量都只有一个实例
类外部使用用例
static.app
static int s_Variable = 5;
main.cpp
#include <iostream>
int s_Variable = 10;
int main()
{
std::cout << s_Variable << std::endl;
std::cin.get();
}
这是将正常执行,因为static.app的s_Variable变量是静态的,链接阶段不会找到static.app的s_Variable变量,因此能正常运行
当把static.app的s_Variable变量的static删除后,点击生成,在链接阶段发生报错
错误 LNK2005 "int s_Variable" (?s_Variable@@3HA) 已经在 main.obj 中定义
错误 LNK1169 找到一个或多个多重定义的符号
因为在链接阶段找到了两个s_Variable变量,导致了变量的重复定义
使用extern关键字,定义为主外部翻译单元寻找
extern int s_Variable;
运行结果是5,说明成功找到了
比如在头文件中声明了一个静态变量,在分别引用到两个翻译单元中,本质是在两个翻译单元中各建立了一个静态变量,如果没有声明静态就会在链接的时候出现重复定义的错误
s_ 前缀表示静态成员变量或者静态函数
类内部使用用例
#include <iostream>
struct Entity
{
int x, y;
void print()
{
std::cout << x << "," << y << std::endl;
}
};
int main()
{
Entity e;
e.x = 2;
e.y = 3;
Entity e1 = { 5,8 };//{ 5,8 }初始化列表语法
e.print();
e1.print();
std::cin.get();
}
结果为
将Entity的变量变更为静态
#include <iostream>
struct Entity
{
static int x, y;
void print()
{
std::cout << x << "," << y << std::endl;
}
};
int Entity::x;
int Entity::y;
int main()
{
Entity e;
e.x = 2;//变量静态后等同于Entity::x = 2;,推荐这种写法
e.y = 3;
Entity e1;
e1.x = 5;
e1.y = 8;
e.print();
e1.print();
std::cin.get();
}
结果为
设置为静态后对一个类中的静态变量的操作会同步到所有实例上,因为两个实例的静态变量指向的是相同的内存地址
相当于是在Entity的命名空间中创建了两个变量,他们实质上不属于类
疑问点:为什么要单独声明int Entity::x;int Entity::y;
当你在类中声明静态成员变量时,编译器知道这些变量存在,但是不会为它们分配内存空间。因此,在类外部需要提供额外的定义来分配内存空间。这样做是为了确保程序在链接时可以正确找到静态成员变量的内存位置,从而使其在程序运行时能够正常使用。
方法也可以变更为静态
static void print()
{
std::cout << x << "," << y << std::endl;
}
调用方法
Entity::print();//等同于e1.print()
需要注意的是:调用静态方法的时候,如Entity::print;少了(),s生成并不会报错,只会有一个警告,方法还是能执行成功,只是没有调用到print方法
warning C4551: 缺少参数列表的函数调用
方法静态,但变量动态
#include <iostream>
struct Entity
{
int x, y;
static void print()
{
std::cout << x << "," << y << std::endl;
}
};
int main()
{
Entity e;
e.x = 2;//等同于Entity::x = 2;
e.y = 3;
Entity e1;
e1.x = 5;
e1.y = 8;
Entity::print();
Entity::print();
std::cin.get();
}
静态方法是无法访问动态变量的,生成将会报如下错误
局部作用域
static int i = 0;
void Function()
{
i++;
std::cout << i << std::endl;
}
和
void Function()
{
static int i = 0;
i++;
std::cout << i << std::endl;
}
用
int main()
{
Function();
Function();
Function();
Function();
Function();
std::cin.get();
}
运行起来结果一样,不一样的是作用域不一样
第一个函数的 i 是一个全局静态变量。它在程序启动时分配内存,,并且一直存在直到程序结束
第二个函数的 i 是一个局部静态变量。它在程序首次调用函数时分配内存,并且一直存在直到程序结束
但是,全局静态变量的作用域是整个程序,而局部静态变量的作用域仅限于定义它的函数内部。
这是一个让代码更干净的方法之一
单例类
class Singleton
{
private:
static Singleton* s_Instance;
public:
static Singleton& Get() {
return *s_Instance;
}
void Hello() {
std::cout << "Hello" << std::endl;
}
};
Singleton* Singleton::s_Instance = nullptr;
int main()
{
Singleton::Get().Hello();
std::cin.get();
}
这是单列类的一种创建方式,但如果使用局部作用域来定义
class Singleton
{
public:
static Singleton& Get() {
static Singleton* s_Instance;
return *s_Instance;
}
void Hello() {
std::cout << "Hello" << std::endl;
}
};
int main()
{
Singleton::Get().Hello();
std::cin.get();
}
可以看到这段代码简洁了很多,但实现的功能是一样的
通过上面的例子可以看到使用局部作用域可以帮助我们简化很多代码
C++枚举类(enum)
enum Example:char//默认是用32位的int,但可以指定为8位的char,可以减少内存的使用,需要注意必须是整数,不能是float
{
A,B,C//默认是0,1,2,也可以单独设置,设置后后面的会顺延下去
};
本质
枚举类的本质就是给特定值命名的一种方式
在log类的应用
#include <iostream>
class Log
{
public://分两个public 是因为把不同部分的代码分开,会比较简介和美观
enum LogLevel:char
{//使用枚举类将值限制在范围内,编写=0,=1这些东西可以增加代码的可读性,char是定义值的类型,可以减少内存的使用
LevelError = 0,//不能直接使用Error,因为存在同名方法,使用Log::Error会检索到函数
LevelWarning = 1,//加上Level也会一定程度的,增加代码的可读性
LevelInfo = 2
};
private:
LogLevel m_LogLevel = LevelInfo;
public:
void SetLevel(LogLevel level)
{
m_LogLevel = level;
}
void Info(const char* message) {
if (m_LogLevel >= LevelInfo)//再转换成枚举类后,依旧能够进行值的比较
std::cout << "[Info]: " << message << std::endl;
}
void Warn(const char* message) {
if (m_LogLevel >= LevelWarning)
std::cout << "[Warning]: " << message << std::endl;
}
void Error(const char* message) {
if (m_LogLevel >= LevelError)
std::cout << "[Erroe]: " << message << std::endl;
}
};
int main()
{
Log log;
log.SetLevel(Log::LevelError);
log.Info("信息日志");
log.Warn("警告日志");
log.Error("错误日志");
std::cin.get();
}
C++构造函数
未使用构造函数
#include <iostream>
class Entity
{
public:
float X, Y;
void Print() {
std::cout << X << "," << Y << std::endl;
}
};
int main() {
Entity e;//实例化了Entity,分配了内存地址,但没有进行初始化
e.Print();//调用打印方法也能成功,因为虽然没有初始化,但其原本也是有个值的
std::cin.get();
}
运行结果
特别注意问题
int main() {
Entity e;
std::cout << e.X << "," << e.Y << std::endl;
e.Print();
std::cin.get();
}
当在Entity类外部直接访问未初始化的变量时,在编译过程中就直接会报错了
error C4700: 使用了未初始化的局部变量“e”
这就提出个疑问:类内类外都是访问未初始化的变量,为什么产生的结果不一样呢
在C++中,成员函数(如Print()方法)内部可以访问它们所属对象的成员变量,即使这些成员变量未被初始化。这是因为成员函数通过一个隐含的指向对象的指针(称为this指针)来访问对象的成员。这个this指针使得在成员函数内部可以访问对象的成员变量,即使它们未被初始化。但是,在函数外部直接访问对象的成员变量时,编译器无法确定对象的上下文,因此访问未初始化的成员变量可能会导致未定义的行为。
使用构造函数
先使用手动初始化函数
void Init() {
X = 0.0f;
Y = 0.0f;
}
在Entity类中添加Init函数
int main() {
Entity e;
e.Init();
std::cout << e.X << "," << e.Y << std::endl;
e.Print();
std::cin.get();
}
使用的时候,得实例化后,去调用Init函数,这样就很麻烦,同C#的类一样,这里也提供了构造函数,就是无返回值,与类同名的方法,实例化类的时候会自动调用
Entity()
{
X = 0.0f;
Y = 0.0f;
}
在未声明这个构造方法的时候,其实它本身也有个默认的构造方法,但是不同与java和C#的是,int ,float这些值不会自动初始化为0,C++得手动初始化所有基本类型
同样的也可以定义有参构造方法
Entity(float x,float y)
{
X = x;
Y = y;
}
用法
Entity e(10.0f,5.0f);
删除构造方法
class Log {
private:
Log() {};//第一种方式
public:
Log() = delete;//第二种方式
};
C++析构函数(~)
析构函数是在销毁对象时运行,适用于栈和堆分配的对象
如果你使用new分配一个对象,当你调用delete时,析构函数会被调用(堆)
如果只是栈对象的话,当作用域结束,栈对象被自动删除,析构函数会被调用(栈)
注意点:可以看到前期使用类的时候都是没有new就实例化了一个对象的,这个种都是栈对象,而使用new分配的是堆对象
栈和堆对象区别
-
栈上创建对象:
- 当你声明一个类的对象时,例如
Entity e;,对象会被自动分配在程序的栈内存上。 - 对象在其声明的作用域结束时会自动被销毁,不需要手动释放内存。这是因为对象是在栈上分配的,当它们的作用域结束时,栈上的内存会自动被释放。
- 当你声明一个类的对象时,例如
-
使用new动态分配对象:
- 当你使用
new关键字创建对象时,对象会被分配在程序的堆内存上。 - 对象在使用
delete关键字显式释放内存之前,会一直存在于堆上。如果忘记释放对象,可能会导致内存泄漏。 - 动态分配对象允许在程序运行时动态地创建和销毁对象,因为对象的生存期不再受限于其声明的作用域。
- 当你使用
通常情况下,如果对象的生命周期在函数内或作用域内,且对象不是太大,那么直接在栈上创建对象更为方便和高效。而如果对象的生命周期需要跨越多个函数调用,或者对象的大小不确定,那么动态分配对象可能更为适合。
堆对象的创建
Entity *ptr = new Entity();//创建
delete ptr;//删除
栈对象
#include <iostream>
class Entity
{
public:
float X, Y;
Entity()
{
X = 0.0f;
Y = 0.0f;
std::cout << "Created Emtity!" << std::endl;
}
Entity(float x,float y)
{
X = x;
Y = y;
}
~Entity()
{
std::cout << "Destroyed Emtity!" << std::endl;
}
void Print() {
std::cout << X << "," << Y << std::endl;
}
};
void Function() {
Entity e;
e.Print();
}
int main() {
Function();
std::cin.get();
}
在主方法的话,要控制台退出了,才会触发析构函数,因此移到Function中,限制它的作用域,结果如下
Q&A
为什么要写析构函数,其他语言为什么没有
1,因为如果在构造函数中调用了特定的初始化代码,可能会需要在析构函数中,卸载或销毁所有这些东西,如果不这样做可能会照成内存泄露
2,在其他一些编程语言中,如Java或Python,由于其具有自动垃圾回收机制,通常不需要显式编写析构函数。相反,这些语言会在对象不再被引用时自动执行清理操作。
C++继承
class Entity
{
public:
float X, Y;
void move(float xa, float ya) {
X += xa;
Y += ya;
}
};
class Player : public Entity
{
public:
const char* Name;
void PrintName()
{
std::cout << Name << std::endl;
}
};
int main() {
Player player;
player.move(5, 2);
std::cin.get();
}
Player是继承至Entity的,Player是Entity的超集,拥有Entity的一切,加上自己独有的
优点
- 代码重用性: 继承允许子类(派生类)从父类(基类)那里继承属性和行为,这样可以避免在子类中重复编写相同的代码,提高了代码的重用性。
- 可扩展性: 继承允许在现有类的基础上创建新的类,通过添加新的属性和行为来扩展功能,从而使代码更加灵活和可扩展。
- 维护性: 继承使得对基类的修改可以自动地反映在所有派生类中,从而减少了代码的维护成本。这意味着一些通用的变更只需要在基类中进行一次,而不必修改每个派生类。
- 多态性: 继承是实现多态性的基础。通过基类指针或引用指向派生类对象,可以在运行时选择调用相应的函数,从而实现多态性。
- 抽象和封装: 继承允许通过抽象基类来定义通用的接口和行为,而派生类可以根据需要实现具体的细节。这有助于实现抽象和封装的编程原则。
- 代码组织: 使用继承可以将相关的类组织在一起,形成层次化的结构,使得代码更加清晰和易于理解。
Q&A
C++继承为什么还要写public这样的关键字
在C++中,继承时指定基类的访问权限是非常重要的,因为它决定了派生类对基类成员的访问权限。关键字 public、protected 和 private 在这里扮演着重要的角色:
- public 继承: 当使用
public继承时,基类的公有成员和保护成员在派生类中保持相应的访问权限,即公有成员仍然是公有的,而保护成员在派生类中仍然是保护的。这种方式意味着派生类可以访问基类的公有和保护成员,但不能直接访问基类的私有成员。 - protected 继承: 使用
protected继承时,基类的公有和保护成员在派生类中都变为保护的。这意味着派生类可以访问基类的公有和保护成员,但外部类不能访问派生类中继承的公有成员。 - private 继承: 使用
private继承时,基类的公有和保护成员在派生类中都变为私有的。这意味着派生类可以访问基类的公有和保护成员,但外部类不能访问派生类中继承的成员。
因此,指定继承时的访问权限可以控制派生类对基类成员的访问级别,从而实现封装和数据隐藏。这种灵活性使得 C++ 能够更好地支持面向对象编程的概念。
protected 和private 好像在这里没有区别
当使用 protected 或 private 继承时,基类的公有和保护成员都会在派生类中变为相应的访问级别(保护或私有)。这意味着外部类无法访问这些继承的成员,而只有派生类内部可以访问它们。在这一点上,它们的作用相似。
但是,它们在派生类的派生类中的行为上是有区别的
FurtherDerivedProtected 中,派生类的派生类仍然可以访问基类的保护成员。而在 FurtherDerivedPrivate 中,基类的所有成员对派生类的派生类都是不可访问的。这就是 protected 和 private 继承之间的区别。
C++虚函数(virtual)
用例
#include <iostream>
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){}//造函数 Player(const std::string& name) 接受一个字符串参数 name,用于初始化 m_Name 成员变量
std::string GetName() { return m_Name; };
};
void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main() {
Entity* e = new Entity();
PrintName(e);
Player* p = new Player("Player");
PrintName(p);
std::cin.get();
}
运行结果
该代码的问题是,我们期望的是传入Entity,运行entity的方法,传人Player用Player的方法,打印了两次Entity,说明没有运行Player的方法
我需要PrintName方法知道,我传递的Entity其实是Player,这就需要虚函数了,要覆写一个函数,必须将基类中的基函数标记为虚函数
virtual std::string GetName() { return "Entity"; };
代码上就是添加一个virtual关键字,这将会生成v表,为这个函数,如果它被重写,将指向正确的函数
std::string GetName() override{ return m_Name; };
override是C++11引入的,这不是必须的,没有这个也能正常工作,但这是推荐的,这能使代码更具可读性
同时它也能帮我们检验函数,当写了override关键字,但基类中没有可重写的方法时,会报错对我们进行提醒
需要注意,虚函数并不是无额外开销的,有两种与虚函数相关的运行时成本
1.需要额外的内存来存储v表,包括基类中要有一个成员指针来指向v表
2.每次调用虚函数时,需要遍历这个表,确认要映射到哪个函数
但一般情况下,不会有开销特别大的情况,以至于不使用虚函数(部分嵌入式平台,cpu很差的情况下要注意)
Q&A
:m_Name(name)这是什么语法糖,能初始化多个变量吗
对于 m_Name(name) 这样的语法,它表示将成员变量 m_Name 初始化为传入构造函数的参数 name 的值,初始化多个成员变量,只需要用逗号 , 将它们分开即可
Player(const std::string& name, int age)
: m_Name(name), m_Age(age) {}
在添加virtual关键字后,将会生成v表,这个v表是什么
虚函数表(vtable)是用于实现C++中多态性的机制之一。每个含有虚函数的类都会在其对象中包含一个指向虚函数表的指针(通常称为虚函数指针)。虚函数表是一个数组,存储了类中所有虚函数的地址。
当调用一个虚函数时,实际上是通过对象的虚函数指针找到对应的虚函数表,然后根据函数在表中的位置,调用相应的虚函数。这种方式允许在运行时动态地确定应该调用的函数,实现了多态性。
那在其他语言中,有类似的概念吗
-
Java 和 C# 中的虚方法表(vtable) :
- Java 和 C# 中的对象也包含一个指向虚方法表的指针(称为虚方法表指针或类型信息指针)。
- 虚方法表中存储了类中所有虚方法的地址。
- 当调用一个虚方法时,会通过对象的虚方法表指针找到对应的虚方法表,并调用相应的虚方法。
-
Objective-C 中的消息机制:
- Objective-C 使用消息机制来实现类似于虚函数表的多态性。
- 对象中存储了一个指向方法选择器的指针,用于确定应该调用哪个方法。
- 在运行时,根据对象的类和方法选择器,确定应该调用哪个方法。
-
Python 中的动态派发:
- Python 是一种动态类型语言,它不需要显式地声明函数为虚函数,所有方法都是虚方法。
- 在运行时,根据对象的实际类型来确定应该调用的方法。
C#我没有用过指向虚方法表的指针啊
在 C# 中,并没有像 C++ 中那样明确的指向虚函数表的指针
在 C# 中,虚函数的调用是通过 CLR 的虚方法表(Virtual Method Table,VMT)来实现的,但这个表是由 CLR 管理的,对开发者是不可见的。
C++接口(纯虚函数)
本质上等同于其他语言的接口或抽象方法
纯虚函数允许我们在基类中 定义一个没有实现的函数,强制子类去实现它
class Entity
{
public:
virtual std::string GetName() = 0;
};
去掉方法体,再=0,这个方法就是纯虚函数了,含有纯虚函数的类,不在具有实例化的能力,而继承了它的子类,在没有实现所有的纯虚函数的时候,也是不具备实例化的能力的
其他语言有interface关键字,但C++是没有的,接口只是C++的类而已
#include <iostream>
class Printable
{
public:
virtual std::string getClassName() = 0;
};
class Entity : public Printable
{
public:
virtual std::string GetName() { return "Entity"; };
std::string getClassName() override { return "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 m_Name; }
};
void PrintName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main() {
Entity* e = new Entity();
Player* p = new Player("Player");
PrintName(e);
PrintName(p);
std::cin.get();
}
应用场景就和其他语言一样,确保类都有一个特定的方法,将基类作为参数放入一个通用的函数中,然后就可以调用不同方法的实现了
C++可见性(public)
可见性是面向对象编程的概念,指的是类的某些对象实际上有多可见
可见性关键字 private,protected,public
private私有的
”只有“这个类自身可以访问,加引号是因为在C++中 有个叫friend(友元)的东西,可以让类和函数访问类的私有成员
protected受保护的
自身和子类可以访问,外部还是无法访问
public公共的
都可以访问
C++数组(int a[])
int example[5];
example[0]= 1;
C++数组也和前面一样,不需要new,就默认实例化了,但没有初始化,[5]也是在变量名旁边而不是类型旁边
索引超出
int main() {
int exmaple[5];
exmaple[0] = 1;
exmaple[-1] = 1;
std::cin.get();
}
需要注意的是,这里索引超出范围里依旧能正常运行,但实际这已经写入了不属于它的内存,我们得确保总是在数组的边界写东西
指针偏移
int main() {
int exmaple[5];
int* ptr = exmaple;
*(ptr + 2) = 5;
exmaple[2] = 5;
std::cin.get();
}
因为C++指针的特性,我们可以通过指针偏移的方式去访问修改数组的值
这边的位移2,并不是位移两个字节,事实上位移的是8个字节
*(int*)((char*)ptr + 8) = 5;
效果等同于上面,清晰的演示了它的偏移是基于类型的
上面那些都是在堆上建立的,在作用域结束的时候会自动销毁
int main() {
int* another = new int[5];
for (int i = 0;i < 5;i++)
another[i] = 2;
delete[] another;
std::cin.get();
}
这是在栈建立的,需要用new来动态分配,而且生存期在程序销毁前都在,提前释放需要delete[]手动释放
指针跳转
class Entity
{
public:
int* example = new int[5];
Entity() {
for (int i = 0;i < 5;i++)
example[i] = 2;
}
};
int main() {
Entity e;
std::cin.get();
}
Entity e的内存地址的值实际上是example的内存地址,当我们要访问 example的值的时候,要多经过一次内存地址的跳转,我们应该在栈上创建数组,避免不必要的跳转来影响性能、
std::array
C++11新特性
#include <array>
class Entity
{
public:
static const int size = 5;
int example[size];
std::array<int, 5> another;
Entity() {
for (int i = 0;i < another.size();i++)
another[i] = 2;
}
};
新特性的使用要包含,有边界检查,记录数组大小(原始数组无法知道大小)
当然新特性的使用是要有一些开销的,但这个是推荐的
int a[5];
int count = sizeof(a) / sizeof(int);//5
当然可以通过sizeof间接的获取
int* example = new int[5];
int count = sizeof(example);//8
因为example是一个指针,有时是8位有时是4位,建议原始数组不要获取大小
static const int size = 5;
int example[size];
或者通过这种方法去简介设置
C++字符串
C++对待字符默认方式是通过Ascii字符进行文本编码
字符默认指char,但是字符有1个字节也有2个字节3个字节,也有宽字符串
字符也就是char数据类型,字符串实际上是字符数组,而数据就是一组元素的集合
const char*
常用的字符串 const char ,const不是必须的,但通常字符串是不会去变的,因为字符串是不可变的,不能扩展字符串并使它变大,因为这是一个固定分配的内存块,同时它加了,但它不是堆上分配的,不能通过调用delete[]来删除这些东西
一般来讲,没用new,就不要用delete
不是必须是加引号的,char* name = "Cherno"; 曾经在早期 C++ 版本中是可以接受的,但在现代 C++ 中,尤其是遵循 C++11 及其后续标准的编译器中,这样的写法通常会被视为不推荐或产生警告,尽管在某些编译器设置下可能仍能编译通过
空终止字符
const char* name = "Cherno";
可以看到在6个字节后接了一个00的字节,这被称为空终止字符,这样编译器才知道那是字符串结束的地方,比较name是一个指针,不包含数据,也不包含字符串长度,它只是一个地址
如果要手动声明这个
#include <iostream>
int main() {
const char* name = "Cherno";
char name2[6] = { 'C','h','e','r','n','o'};
std::cout << name2 << std::endl;
char name3[7] = { 'C','h','e','r','n','o',0};
std::cout << name3 << std::endl;
std::cin.get();
}
可以看到name2没有加空终止字符,把后面地址的东西也打印出来了,直到遇到了00
而name3加了空终止字符,就正常执行了
数组守卫
去观察内存地址,发现字符串后面很多cc,这个实际上就是数组守卫,让我们知道内存在分配之外
String类
std::string,它是一个char数组和一些函数
鼠标悬停在上面,可以看到它的实现
在教程中std::string要用std::cout打印需要包含#include
但在我实际使用中,却发现不需要
在现代 C++ 编译器(如基于 C++17 或更新标准的编译器)中,即使没有显式包含 <string> 头文件,您的代码也能正常编译和运行。
这是因为 C++ 标准库的设计发生了变化,引入了模块化和依赖项自动引入的机制。
模块化: C++20 引入了模块(Modules)特性,旨在改善编译速度、减少编译时依赖和提高代码封装性。虽然您的代码并未使用模块化语法,但现代编译器在实现 <iostream> 时可能采用了模块化设计,使得包含 <iostream> 会隐式地包含其他相关头文件,如 <string>。
依赖项自动引入: 一些编译器(如 Clang 和 GCC)在实现 C++ 标准库时,即使不使用模块化,也可能采取了自动引入依赖项的策略。当您包含 <iostream> 时,编译器会自动包含 <string>,因为编译器知道 std::string 是 std::cout 输出操作的常见用法之一。
C++是不允许这种其他语言中常见的用法的,这是因为,“"asd"实际上是相当于const char数组,而这相当于把两个const char数组想加,他们并不是真正的字符串
int main() {
std::string name = "Cherno" + std::string("First");
name += "Second";
std::cout << name << std::endl;
std::cin.get();
}
可以通过这两种方式来实现相加的效果
name.find("no") != std::string::npos;
还有这个判断字符串中是否含有”no“,std::string::npos代表一个不存在的位置
void PreingString(std::string string) {
string += "h";
std::cout << string << std::endl;
}
以及这样在函数中传递一个对象,实际上是在复制这个对象,在函数内对string进行的操作不会影响原字符串
如果我们不进行修改的话,这样的复制是耗时的,这意味着我们必须动态的在堆上分配一个全新的char数组来存储一个完全相同的文本
正常如果是只读的情况下,我们应该通过常量引用来传递
void PreingString(const std::string& string) {
std::cout << string << std::endl;
}
const意味着它不会被修改,不加的话,函数内的修改会影响到原本的字符串,&意味着它不会被复制
字符串字面量
空终止字符
"Cherno"
双引号,中间写字符,这就是一个字符串字面量
鼠标悬停,可以看到这个6个字符的字符串字面量,是const char[7],这是因为最后一个是空终止字符
可以看到,我在字符串中插入了一个空终止字符,就只打印了前三个字符,这是因为它只答应,内存地址到空终止字符之间的东西
可以看到,如果设置一个char数组的话是允许修改的,但如果要设置一个字符串的话就必须要声明const,且不允许修改
字符编码
int main() {
const char* name = u8"Cherno";
const wchar_t* name = L"Cherno";
const char16_t* name = u"Cherno";
const char32_t* name = U"Cherno";
std::cin.get();
}
u8指定UTF-8编码,适合跨平台文本处理,尤其是在网络传输和文件I/O中广泛使用。wchar_t在不同平台上的含义不完全相同,但通常用于处理平台相关的宽字符集或Unicode子集。通常是2或4字节char16_t明确表示UTF-16编码,适用于需要固定宽度(每个字符16位)且能覆盖大多数常见字符集的场景。char32_t则是用于存储完整的Unicode字符集,每个字符占用固定的32位空间,确保能表示所有Unicode字符。
字符串拼接
可以看到在引用string_literals后,通过在字面量后加s,来调用函数,这个函数会返回字符串对象,而不是原来的指针,两个指针是没法相加的
int main() {
using namespace std::string_literals;
std::u32string name = U"Cherno"s + U"hello";//定义32bit字符串
const char* example = R"(Line1
Line2
Line3
Line4)";//R就是用来忽略转义字符的
const char* example0 = "Line1\n"
"Line2\n"
"Line3\n"
"Line4\n";//这样就比较麻烦
std::cin.get();
}
C++CONST
从语法上指定一个数是个常量,并且不允许修改,但这个只是一个约定,这个约定帮助我们简化代码
用法
1.可变指针,不可变内容
const int* a = new int; // 指针a指向一个int类型的常量
int const* a = new int; // 等价于上面的
这意味着你不能通过 a来修改字符串的内容,但是 name 本身(即指针地址)是可以改变的,可以指向另一个字符串常量。
2.可变内容,不可变指针
int* const a = new int; // 表示对象自身是常量
这意味着你能通过 a来修改字符串的内容,但是 name 本身(即指针地址)是不可以改变的。
3.指针内容都不可变
const int* const a = new int;
4,类中的使用
class MyClass {
public:
void myFunction() const; // 常量成员函数
};
const关键字放在函数后意味着该成员函数不会修改类的成员变量
不允许修改,所有通常用于GET类方法
class MyClass {
private:
int* X,Y;
public:
const int* const GetX() const {
return X;
}; // 返回一个不允许修改指针,不允许修改内容的指针,函数本身不允许修改成员变量
};
这个是多个条件限制的函数,需要注意的是int* X,Y;声明的,x是指针,但Y依然是int
int* X,*Y;
再加个*,就也会变成指针
class Entity {
private:
int* X,*Y;
public:
const int* const GetX() {
return X;
}; // 返回一个不允许修改指针,不允许修改内容的指针,函数本身不允许修改成员变量
};
void PrintEntity(const Entity& e) {//传入的Entity的引用是const的,不允许修改
std::cout << e.GetX() << std::endl; //在GetX为声明const的时候,方法内部是有可能进行修改成员变量的操作,所以是不允许执行的
}
int main() {
Entity e;
PrintEntity(e);
std::cin.get();
}
E1086 对象含有与成员 函数 "Entity::GetX" 不兼容的类型限定符
const int* const GetX() const {//加上const就可以了
return X;
};
在实际没有修改类或者不应该修改类的时候,要记得总是要标记方法为const,这样在有常量引用的时候,就用不了你的方法了
mutable
但如果我们想要在有常量引用的时候调用的const函数依旧能修改变量
class Entity {
private:
int* X,*Y;
mutable int var;//将一个变量声明为 mutable,在const方法内就能修改了
public:
const int* const GetX() const {
var = 5;
return X;
}; // 返回一个不允许修改指针,不允许修改内容的指针,函数本身不允许修改成员变量
};
mutable允许函数是常亮方法,但可以修改变量
C++mutable
mutable有两个用途,1是在在有常量引用的时候调用的const函数依旧能修改变量
#include <iostream>
using namespace std;
class Entity {
private:
string m_Name;
mutable int DebugCount = 0;//用于记录函数被调用了多少次,演示mutable的用法
public:
const string& GetName() const {
DebugCount++;
return m_Name;
}
};
int main() {
const Entity e;
e.GetName();
std::cin.get();
}
2是在lambda中使用,在实践中几乎不会用到
lambda
lambda基本就项一个一次性的小函数
int main() {
int x = 8;
auto f = [=]() {//=是值传递,不会改变原来的值,&是引用传递
x++;
cout << x << endl;
};
cin.get();
}
因为是值传递,无法直接修改
int main() {
int x = 8;
auto f = [=]() {
int y = x;
y++;
cout << y << endl;
};
f();
cin.get();
}
将值先赋值在修改就不会报错了
int main() {
int x = 8;
auto f = [=]() mutable
{
x++;
cout << x << endl;
};
f();
cin.get();
}
加上mutable后,就能直接进行值修改了
成员初始化列表
class Entity {
private:
string m_Name;
mutable int DebugCount;//用于记录函数被调用了多少次,演示mutable的用法
public:
Entity()
:m_Name("Unknown"), DebugCount(0)//初始化变量要按顺序写
{
}
Entity(const string& name)
:m_Name(name)//这个就是成员初始化列表的写法
{
}
};
可以看到这样初始化变量可以避免在构造函数中写一堆变量赋值的代码
Q&A
我们为什么要使用成员初始化列表,这只是代码风格的问题吗?
首先是这种初始化成员变量的方法确实对我们的代码风格很有帮助,可以避免在构造函数中写一堆变量赋值的代码,使得代码非常干净,易于阅读
其次,在功能上也有区别
class Example {
public:
Example() {
cout << "Created Entity!" << endl;
}
Example(int x) {
cout << "Created Entity with!" << x << endl;
}
};
class Entity {
private:
Example m_Example;
public:
Entity()
{
m_Example = Example(2);
}
};
int main() {
Entity e;
cin.get();
}
可以看到在构造函数内对变量进行初始化,变量的构造方法调用了两次,所以这里实际上创建了两个Example,一次是Example m_Example;的时候就已经创建了个无参的,第二次是Example(2);的时候创建了个有参的,这里多执行了无参的,就照成了性能的浪费
class Entity {
private:
Example m_Example;
public:
Entity()
:m_Example(2)
{
}
};
换成这个就就只会创建一个有参的
原始类型的话是没有区别,但不需要区分,直接全部用成员初始化列表就好
成员初始化列表的作用,一是代码风格的简洁,二是在功能上,可以避免性能上的浪费
三元运算符
和C#一样没什么区别
int main() {
int s_Level = 5;
int s_Speed = s_Level > 5 ? 10 : 5;//常规用法
s_Speed = s_Level > 5 ? s_Level > 10 ? 15 : 10 : 5;//嵌套用法
cin.get();
}
尽量不要使用嵌套用法,这会使得代码很混乱
创建并初始化C++对象
两个方式,栈和堆
这两种方式该怎么选择呢
栈通常比较小,如果对象太大或者太多,或者要显式的控制对象 的生命周期,就使用堆,否则就使用栈,栈会简单很多,自动化,性能更快,堆还要手动delete,如果忘记delete,就会导致内存泄露
两种创建方式,推荐使用栈,除非无法使用
C++ new关键字
new的主要目的是在堆上分配内存
如new int,它将需要4个字节的内存,它需要找到一个包含4个字节内存的连续块,然后返回一个指向这个内存的指针
空闲列表
上面指出必须要寻找4个字节的连续内存,这个是通过空闲列表来完成的,它会维护那些有空闲字节的地址
int main() {
int* b = new int[50];//200 bytes
Entity* e = new Entity[50];//这将会创建50个内存地址连续的Entity
//通常调用new,会调用隐藏在里面的c函数malloc
malloc(50);//传入要的内存大小,返回一个指针
Entity* e1 = new Entity();
Entity* e2 = (Entity*)malloc(sizeof(Entity));
//这两个是等价的,区别是e1会调用构造函数,而e2仅仅是分配内存,然后返回指针,C++中不应该这样分配内存
delete e1;//当使用了new关键字的时候,一定要记得用delete
delete e2;
//这很关键,没有被delete的内存,无法返回到空闲列表中,直到我们调用delete
//后续也有很多c++的策略让这个过程自动化,基于作用域的指针,引用计数
delete[] e;//释放数组要用delete[]
cin.get();
}
隐式转换与explicit关键字
c++运行编译器对代码执行一次隐式转换
class Entity {
private:
string m_Name;
int m_Age;
public:
Entity(const string& name)
:m_Name(name), m_Age(-1) { }
Entity(int age)
:m_Name("UnKnown"), m_Age(age) { }
};
void PrintEntity(const Entity& entity) {
}
int main() {
Entity a("Cherno");
Entity b(22);
//常规方法是这样实例化对象
Entity a1= string("Cherno");
Entity b1= 22;
//这个方法在C#与java中是无法使用的
//这里使用到了隐式构造函数,它将int或string隐式的转换成一个Entity,因为有接收int或string的构造函数
PrintEntity(22);
//甚至还可以对入参是Entity引用的方法直接传入int
//PrintEntity("Cherno");但这个是不允许的,因为这要经过两次转换,const char数组到string,string到Entity
//它只允许做一次隐式转换
cin.get();
}
隐式构造函数应尽量避免使用,这会使代码可读性变差
explicit关键字作用就是对隐式构造函数禁用
class Entity {
private:
string m_Name;
int m_Age;
public:
explicit Entity(const string& name)
:m_Name(name), m_Age(-1) { }
explicit Entity(int age)
:m_Name("UnKnown"), m_Age(age) { }
};
int main() {
Entity a("Cherno");
Entity b(22);
Entity a1= string("Cherno");
//不存在用户定义的从 "std::string" 到 "Entity" 的适当转换
Entity b1= 22;
//不存在从 "int" 转换到 "Entity" 的适当构造函数
cin.get();
}
加上之后再使用隐式构造函数,就会提示报错了,这就是explicit关键字唯一的用处,可以防止做意外转换,导致性能或bug
运算符及其重载
运算符是我们使用的一种符号,通常代替函数来执行一些事情,包括加减乘除,*(逆向引用),箭头运算符,+=,&(地址引用),左移运算符,new,delete
运算符重载是什么意思?
重载运算符本质意思就是给运算符赋予新的含义,添加参数,或创建允许在程序中定义或更改运算符的行为
运算符其实就是函数,不用给出函数名,在大多数情况有助于清理代码,保持整洁,但随意使用也会导致代码混乱
因此对运算符重载的使用,应该要非常少
struct Vector2
{
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);
}//通过重载运算符实现
};
int main() {
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f);
Vector2 result = position.Add(speed.Multiply(powerup));
//可以看到要实现结构体的运算可以通过函数来实现,在java中这是唯一的选择
//但在C++中,我们有运算符重载
Vector2 result = position + speed * powerup;
cin.get();
}
上面演示的就是重载+和*,实现结构体的加法和乘法
struct Vector2
{
float x, y;
Vector2(float x, float y)
:x(x), y(y) {
}
Vector2 Add(const Vector2& other) const
{
return *this+other;
}//扩展写法,重载运算符函数实现,ADD方法使用运算符
Vector2 operator+(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 Multiply(const Vector2& other) const
{
return operator*(other);
}//也可以直接想调用函数一样调用
Vector2 operator*(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
};
当然,这样写起来就会比上面难理解一点
ostream& operator<<(ostream& stream, const Vector2& other)
{
stream << other.x << "," << other.y << endl;
return stream;
}//去重载<<运算符,进行实现
int main() {
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f);
Vector2 result = position.Add(speed.Multiply(powerup));
Vector2 result = position + speed * powerup;
cout << result << endl;//直接用会报,没有与这些操作数匹配的 "<<" 运算符
cin.get();
}
bool operator==(const Vector2& other) const
{
return x == other.x && y == other.y;
}//重载==
bool operator!=(const Vector2& other) const
{
return !(*this==other);
}
一般情况下,ADD方法和重载运算符的方法两个都会保留
this关键字
含义
this关键字是一个指向当前对象实例的指针,通过它可以访问该实例的成员变量,成员函数,是在对象和类的内部使用的
如何使用
void PrintEntity(Entity* e);
class Entity
{
public:
int x, y;
Entity(int x, int y)
{
x = x;//直接这样使用,会出现没有对成员变量赋值的情况因为他们名字一样
this->x = x;
(*this).x = x;
//使用this的话,才可以在同名的情况下对成员函数赋值
const Entity* e = this;
//this实际上就是Entity*,Entity的指针
PrintEntity(this);
//以及在类内部,要传递自身的实例就只能通过this
//如果接收的是引用void PrintEntity(Entity& e)
//那就要用PrintEntity(*this);*表示逆向引用
delete this;
//你甚至可以在类内部释放自己
}
};
void PrintEntity(Entity* e) {
}
对象生存期(栈作用域生存期)
class Entity {
public:
Entity() {
cout << "create Entity!" << endl;
}
~Entity() {
cout << "Destroyed Entity!" << endl;
}
//创建一个类,在构造函数和析构函数中分别打印,这样在它创建和销毁的时候就有提示了
};
int main() {
{
Entity e;//执行完当前行,断点箭头到下一行,控制台打印create Entity!对象被创建了
}//执行完当前行,断点箭头到下一行,离开{}的范围l,控制台打印Destroyed Entity!对象被销毁了
//上面是栈创建对象
{
Entity* e = new Entity();//执行完当前行,断点箭头到下一行,控制台打印create Entity!对象被创建了
}//执行完当前行,断点箭头到下一行,离开{}的范围,控制台没有打印
//上面是堆创建对象
cin.get();
}
这里展示了栈创建对象和堆创建对象的生存期
int* CreateArray()
{
int array[50];
return array;
}
这个就是经典错误案例,返回的指针在方法结束的时候,对应的内存已经被销毁了
作用域指针
作用域指针基本上是一个类,一个指针的包装器,在构造时用堆分配指针,在析构时删除指针
这个可以使用标准库中的unique_ptr,工作原理累死下面
int main() {
{
ScopedPtr e0(new Entity());//最直接的写法是这样
ScopedPtr e = new Entity();//比较习惯的写法,这实际用到了隐式转换
}
//这个是用堆创建的对象,但会想栈一样,超出作用域自动销毁
cin.get();
}
智能指针
智能指针本质上是一个原始指针的包装,它可以实现new和delete的自动化,使用这种编程风格,可以从来都不调用new和delete,智能指针是实现这个的一种方法
作用域指针
unique_ptr
最简单的智能指针,超出作用域时,会被销毁
int main() {
{
unique_ptr<Entity> entity(new Entity());//这个不能= new,因为它的构造函数加上了explicit关键字,禁止隐式转换
}
{
unique_ptr<Entity> entity= make_unique<Entity>(new Entity());
//这种方法对比前者有因其更好的异常安全性、代码可读性和潜在的性能优势,通常被视为更推荐的做法
//如在构造函数时抛出了异常,它会避免你得到一个没有引用的悬空指针,从而造成内存泄露
}
cin.get();
}
这个的开销很小,甚至相当于没有开销,它本质就是个栈分配对象,在被销毁的时候,delete堆指针,释放内存
但这个有个问题,unique_ptr是不能复制的
{
unique_ptr<Entity> entity= make_unique<Entity>(new Entity());
unique_ptr<Entity> e0 = entity;
//这里会得到一个错误信息,这是因为它的拷贝构造函数和拷贝构造操作符被删除,这是专门用来防止拷贝后这个指针被销毁,拷贝的也会跟着销毁,就会出现异常
}
共享指针
shared_ptr
shared_ptr实现的方式取决于编译器和在编译器中使用的标准库
它通过跟踪对动态分配对象的引用数量来管理该对象的内存。当最后一个引用该对象的shared_ptr超出作用域或被重置时,对象会自动删除,从而防止了内存泄漏。
int main() {
shared_ptr<Entity> sharedEntity0(new Entity());
//这种方式进行了两次内存分配,一次new Entity的分配,一次shared_ptr控制内存块的分配
shared_ptr<Entity> sharedEntity = make_shared<Entity>();
//在内部同时执行内存分配和Entity对象的构造,只进行一次内存分配来存放shared_ptr的控制块和Entity对象
//推荐第二种方式,它更有效率
shared_ptr<Entity> e0 = sharedEntity;
//共享指针shared_ptr就能直接进行复制
cin.get();
}
int main() {
{
shared_ptr<Entity> e0;
{
shared_ptr<Entity> sharedEntity = make_shared<Entity>();
//Entity的构造函数被调用了
e0 = sharedEntity;//将一个shared_ptr赋给另一个,不会复制对象,而是共享所有权
}
//执行到这的sharedEntity已经死掉了,但并没有调用析构函数,因为e0有Entity的引用
}
//执行到这,e0也死掉了,析构函数被调用,所有引用都没掉后,内存就被释放了
cin.get();
}
弱指针
weak_ptr
int main() {
{
weak_ptr<Entity> e0;
{
shared_ptr<Entity> sharedEntity = make_shared<Entity>();
e0 = sharedEntity;
//使用弱指针进行复制,和之前的区别就是,不会添加引用计数
}
//执行到这,sharedEntity死了,析构函数就被调用了,现在e0指向的就是一个无效的Entity
//但weak_ptr可以去问它是否过期了
}
cin.get();
}
应用场景
当我们要声明一个堆分配对象,并且不希望自己清理,不想显示的调用delete,就应该使用智能指针
优先使用unique_ptr,因为它有一个较低的开销
如果需要在对象之间共享,无法使用unique_ptr时,就使用shared_ptr
-
std::unique_ptr
- 应用场景:当你需要独占资源的所有权,并且希望在智能指针销毁时自动释放资源时,使用
unique_ptr是最合适的。它保证了资源的唯一性和自动清理,常用于临时对象的管理、函数间资源传递(通过移动语义而非复制)等场景。 - 示例:临时缓冲区的创建、单体模式中的资源管理、RAII(Resource Acquisition Is Initialization)模式的实现。
- 应用场景:当你需要独占资源的所有权,并且希望在智能指针销毁时自动释放资源时,使用
-
std::shared_ptr
- 应用场景:当一个资源需要被多个对象共享时,
shared_ptr能够确保资源的安全共享,并且只有当所有共享该资源的智能指针都销毁时,资源才会被释放。适合于复杂的数据结构(如图、树)中的节点共享、跨模块或跨线程的资源共享。 - 示例:在实现引用计数的资源管理、数据库连接池、对象池等场景中,
shared_ptr可以有效管理资源的生命周期。
- 应用场景:当一个资源需要被多个对象共享时,
-
std::weak_ptr
- 应用场景:
weak_ptr用于解决shared_ptr可能导致的循环引用问题。当需要观察一个由shared_ptr管理的对象,但又不希望增加其引用计数,以免影响其生命周期决策时,应使用weak_ptr。它允许你检查资源是否存在,但不控制资源的生命周期。 - 示例:在观察者模式中,让观察者持有被观察者的弱引用,以避免观察者和被观察者之间的循环依赖;在图形界面编程中,用于表示窗口与控件之间的关系,不希望控件的存在影响窗口的生命周期判断。
- 应用场景:
复制与拷贝构造函数
struct Vector2 {
float x, y;
};
int main() {
Vector2 a = {2,3};
Vector2 b = a;
//这里就是对a的复制,创建了一个a的副本,a和b是两个独立的变量,有不同的内存地址
cin.get();
}
值复制
int main() {
Vector2* a = new Vector2();
Vector2* b = a;
//因为现在a是个指针,进行的复制不是值的复制,复制的是指针,现在有两个指针,他们指向同一个内存地址
b->x = 2;
//访问内存地址,对值进行修改,会同时影响a和b
cin.get();
}
引用复制
class String {
private:
char* m_Buffer;
unsigned int m_Size;
public:
String(const char* string) {
m_Size = strlen(string);
//获取出来的size是不包括空终止字符的那一位的
m_Buffer = new char[m_Size+1];//这里new的要记得delete
memcpy(m_Buffer, string, m_Size+1);//加1后,把空终止字符也跟着一起copy了
/* for (int i = 0; i < m_Size; i++) {
m_Buffer[i] = string[i];
}*/
//功能上等价于上面
}
~String() {
delete[] m_Buffer;
}
char& operator[](unsigned int index) {
return m_Buffer[index];
}
friend ostream& operator<<(ostream& stream, const String& string);
//借助friend关键字,使得外部方法可以访问内部成员变量
};
ostream& operator<<(ostream& stream, const String& string) {
stream << string.m_Buffer;
return stream;
}
int main() {
String string = "Cherno";
cout << string << endl;
cin.get();
}
为了演示复制,这边先创建了个String类
浅拷贝
int main() {
String string = "Cherno";
String second = string;
second[2] = 'a';//这里一改,两个string都变了
cout << string << endl;
cout << second << endl;
//执行到这边都是正常的
cin.get();
//按下任意一键后就报错了
}
这是因为 String的m_Buffer是一个指针,String second = string;这里进行的复制,虽然是值复制,但指针类型的变量复制的是内存地址,现在两个对象的m_Buffer指向的是一个内存了,这种拷贝就是一种浅拷贝,它并不会深入指针的内容
在销毁的时候,因为有两个对象所以销毁了两次,但在第一次中m_Buffer已经被销毁了,第二次已经没了,就没办法再销毁了
深拷贝(拷贝构造函数)
这个时候就要用到深拷贝了,复制整个对象
这里有很多种实现方式,克隆,或者定义方法或函数返回一个新的字符串,但这里不用
这里用拷贝构造函数
String(const String& other)
:m_Buffer(other.m_Buffer),m_Size(other.m_Size)
{
}
//这个拷贝构造函数是C++默认提供的拷贝构造函数
String(const String& other) = delete;
//这样的就是不允许复制
要实现深拷贝,我们得自己实现拷贝构造函数
String(const String& other)
:m_Size(other.m_Size)
{
m_Buffer = new char[m_Size + 1];
memcpy(m_Buffer, other.m_Buffer, m_Size + 1);
}
现在再执行就一切正常了
int main() {
String string = "Cherno";
String second = string;
second[2] = 'a';
cout << string << endl;
cout << second << endl;
cin.get();
}
箭头操作符
int main() {
Entity e;
e.Print();
//e是一个对象,能直接.方法来调用
Entity* ptr = &e;
ptr->Print();
//ptr是一个指针,不能通过.方法来调用只能通过箭头
(*ptr).Print();
//不然就要通过逆向引用转成对象,再来调用
//两个是等价的,显然箭头更好用
cin.get();
}
绝大多数情况箭头操作符都是这样用的
特殊用法
1.重载箭头,直接返回指针,使代码更简洁
class ScopedPtr
{
private:
Entity* m_Obj;
public:
ScopedPtr(Entity* entity)
:m_Obj(entity)
{
}
~ScopedPtr() {
delete m_Obj;
}
Entity* GetObject() { return m_Obj; }
};
//现在我们就是设定一个类,它的成员变量是个指针
int main() {
ScopedPtr entity = new Entity();//这里用到了隐式转换
entity.GetObject()->Print();
//现在我们要通过指针成员变量来访问指针的方法,可以看到有点麻烦
//一是成员变量设置成公共的,二是专门写个方法返回
cin.get();
}
class ScopedPtr
{
private:
Entity* m_Obj;
public:
ScopedPtr(Entity* entity)
:m_Obj(entity)
{
}
~ScopedPtr() {
delete m_Obj;
}
Entity* operator->() { return m_Obj; }
//这里去重载箭头操作符,直接返回指针
const Entity* operator->() const { return m_Obj; }
//以及再声明一个const的同名方法
};
int main() {
ScopedPtr entity = new Entity();
entity->Print();
//这里就能像是直接操作指针一样,直接用箭头调方法了
//代码简洁了很多
const ScopedPtr entity = new Entity();
entity->Print();
//这样在生成的对象是const的时候,也能调用
cin.get();
}
2.获取内存中某个成员变量的偏移量
struct Vector3
{
float x, y, z;
//这里声明的顺序,会影响他们在内存中的偏移量
//已当前顺序的话,x是0,y是4,z是8,因为一个float的大小是4字节
};
int main() {
int offset = (int)&((Vector3*)nullptr)->y;
//创建一个空指针转换为Vector3*类型,然后访问其x成员的地址,最后将这个地址转换为整数形式得到偏移量
//通常认为指针大小等于int的大小是不安全的假设,这种转换依赖于平台和编译器,可能会有问题
cout << offset << endl;
//y,打印出了4
cin.get();
}
当要把数据序列转化为一串字节流时,当要计算某些东西的偏移量时,都会有用
动态数组(vector)
C++标准库,它是一个由C++标准委员会设计并维护的类库和函数的集合。这个库旨在提供一系列通用、高效的工具和组件,以便程序员能够更容易地编写出强大、可靠且高性能的软件
主要讲标准库的vector,vector是一种动态数组容器,它被设计为在内存中连续存储元素,类似于常规数组,但其大小可以在运行时改变。
在C++中,vector容器既可以用来存储对象实例,也可以用来存储指向对象的指针(包括原始指针或智能指针)。这两种方式各有其优缺点:
存储对象实例
优点:
- 内存管理简便:当对象存储在
vector中时,容器会自动管理这些对象的生命周期,包括构造和析构。你无需手动分配和释放内存。 - 数据完整性:所有数据都在一起,易于管理和传递。这有助于提高缓存局部性,可能提升访问速度。
- 使用方便:可以直接使用对象的方法和属性,无需解引用。
缺点:
- 深拷贝问题:如果对象很大或者拷贝成本高(例如含有大型数组或复杂资源),每次向
vector添加对象时都会发生拷贝,可能影响性能。 - 性能开销:对象的构造和析构可能引入额外的性能开销,尤其是在频繁插入和删除操作时。
存储指针(原始指针或智能指针)
优点:
- 减少拷贝成本:存储指针可以避免复制大对象的开销,特别是当使用智能指针(如
std::unique_ptr或std::shared_ptr)时,还能确保资源的安全管理。 - 灵活性:可以通过指针灵活地管理对象的生命周期,例如,可以在
vector外部创建对象,并在不再需要时单独销毁它们。
缺点:
- 内存管理复杂:使用原始指针时,需要手动管理内存,容易引发内存泄漏。即使使用智能指针,也需要正确理解和使用它们的语义。
- 解引用开销:访问对象属性或方法需要通过解引用操作,这可能略微增加运行时开销。
- 不保证对象有效性:如果存储的是裸指针,当对象提前销毁或重分配内存时,可能会导致悬挂指针。
总结来说,选择存储对象实例还是存储指针,取决于具体的应用场景、对象的大小和拷贝成本、以及对性能和内存管理的需求。对于小对象或频繁创建销毁的情况,直接存储对象可能更合适;而对于大对象或需要精细控制对象生命周期的情况,则推荐使用指针(尤其是智能指针)。
常用的操作方法及其示例:
- 创建和初始化
1#include <vector>
2#include <iostream>
3
4int main() {
5 std::vector<int> vec; // 默认构造
6 std::vector<int> vec2(10); // 初始化10个默认值(int类型默认为0)
7 std::vector<int> vec3(10, 42); // 初始化10个值为42的元素
8 std::vector<int> vec4{1, 2, 3, 4, 5}; // 初始化列表
9 return 0;
10}
- 添加元素
push_back(value):在末尾添加元素emplace_back(args...):在末尾原地构造元素
1vec.push_back(42); // 添加一个值为42的元素
2vec.emplace_back(42); // 直接在末尾构造一个值为42的元素
- 访问元素
- 使用下标
operator[] front():获取第一个元素back():获取最后一个元素
1std::cout << vec[0]; // 输出第一个元素
2std::cout << vec.front(); // 同上
3std::cout << vec.back(); // 输出最后一个元素
- 遍历
- 范围for循环
- 迭代器
1for(auto& elem : vec) std::cout << elem << ' ';
2// 或者
3for(auto it = vec.begin(); it != vec.end(); ++it) std::cout << *it << ' ';
- 删除元素
pop_back():删除clear():清空erase*():单独移除某个元素
struct Vertex
{
float x, y, z;
};
ostream& operator<<(ostream& stream, const Vertex& vertex) {
stream << vertex.x << "," << vertex.y << "," << vertex.z;
return stream;
}
int main() {
vector<Vertex> vertices;
vertices.push_back({ 1,2,3 });//末尾添加元素
vertices.push_back({ 4,5,6 });
for (int i = 0; i < vertices.size(); i++) {
cout << vertices[i] << endl;
}//for循环
for (Vertex v : vertices) {//这种方式会复制v,到for循环范围里,这不好
cout << v << endl;
}//迭代器的方式
for (Vertex& v : vertices) {//这样就不会复制
cout << v << endl;
}
vertices.clear();
vertices.erase(vertices.begin()+1);//这里是移除第二个元素
//用于单独移除某个元素,但要通过迭代器iterator,不能直接写数字
cin.get();
}
Vector的使用优化
vector 在 C++ 标准模板库(STL)中是一个动态数组,当向 vector 添加元素导致其当前容量不足以容纳新元素时,它会执行以下操作进行内存重新分配:
- 查找新内存:
vector需要找到一块足够大的新内存来存储现有的元素加上即将添加的元素。这通常意味着寻找一个大小为当前容量乘以某个因子(常见的因子是1.5或2,具体取决于实现)的新内存区域。 - 复制元素:找到新内存后,所有现有的元素需要从旧内存复制到新内存中。这是一个O(n)操作,其中n是当前已存储元素的数量。
- 释放旧内存:完成复制后,旧内存会被释放,以避免内存泄漏。
- 更新迭代器和指针:如果有指向
vector内存的迭代器或指针,它们可能需要被更新或失效,因为vector的内存地址已经改变。
为什么慢:
- 内存分配和释放成本:寻找并分配大块连续内存可能涉及操作系统级别的操作,这是耗时的。
- 数据搬迁:复制所有元素到新位置是一个耗时的操作,特别是当
vector很大时。 - 缓存不友好:频繁的内存重新分配和数据搬迁可能导致缓存未命中,降低程序效率。
struct Vertex
{
float x, y, z;
Vertex(float x, float y, float z)
:x(x), y(y), z(z)
{
}
Vertex(const Vertex& vertex)
:x(vertex.x), y(vertex.y), z(vertex.z)
{
cout << "Copied!!!" << endl;
}
//要用清楚vector,在push_back的时候复制了多少次,就通过拷贝构造函数重写一次,来监控这个过程
};
int main() {
vector<Vertex> vertices;
vertices.push_back(Vertex(1, 2, 3));//这里执行了一次Copy
vertices.push_back(Vertex(4, 5, 6));//这里执行了两次
vertices.push_back(Vertex(7, 8, 9));//这里执行了三次
//一共执行了6次Copy
std::cin.get();
}
这是优化前的
int main() {
vector<Vertex> vertices;
vertices.reserve(3);//3要写在这里,不能写vertices(3),因为这实际上会构造三个对象,而不仅仅是扩容内存
//这里一共执行了3次扩容,一个优化就是,我们在知道会push三次的情况下,直接事先就扩容成3个对象的内存
vertices.emplace_back(1,2,3);
vertices.emplace_back(4, 5, 6);
vertices.emplace_back(7, 8, 9);
//push_back做的事是先在栈上构造了Vertex,再从栈上copy到vector的内存里
//这边能做一个优化,在适当的位置构造Vertex,避免copy
//emplace_back这里就用到了这个
std::cin.get();
}
这是优化后的,一次copied都没打印
C++库(静态链接)
C++中的静态链接库和动态链接库主要在链接时的处理方式、程序运行时的行为、资源消耗、更新便利性等方面存在差异。以下是两者之间的关键区别:
静态链接库
- 链接时期:静态链接发生在编译时。编译器会把静态库中的所有相关代码直接包含进最终的可执行文件中。
- 资源占用:静态链接会导致生成的可执行文件体积较大,因为它包含了库的所有代码,这可能造成磁盘空间和内存的浪费,尤其是在多个程序都使用同一库的情况下。
- 运行时依赖:静态链接的程序在运行时不需要外部库文件,因为所有需要的代码都已经集成在可执行文件内部,这使得分发更为简单。
- 更新和维护:更新静态库时,必须重新编译链接所有使用该库的应用程序。这意味着如果库有安全更新或错误修复,所有依赖它的软件都需要重新构建和分发。
- 性能:静态链接的程序启动速度可能稍快,因为它不需要在运行时解析和加载库文件。
动态链接库
- 链接时期:动态链接发生在程序加载或运行时。程序中只包含对库函数的引用(如DLL或.so文件),而非实际的代码。
- 资源占用:动态链接可以节省磁盘空间和内存,因为多个程序可以共享同一个动态库的实例。
- 运行时依赖:动态链接的程序在运行时需要相应的动态库文件存在,并且路径需正确设置(如通过环境变量或配置)。如果库缺失或版本不兼容,程序可能无法正常运行。
- 更新和维护:动态库可以独立于应用程序更新,只需替换库文件即可,无需重新编译应用程序,便于维护和升级。
- 性能:虽然动态链接的程序在启动时可能需要额外的时间来加载和解析库,但运行时如果库已经被加载,则调用库函数可能更快,因为代码已经在内存中。
使用
是加载GLFW库为例
在解决方案目录下,创建Dependencies文件夹,作为依赖文件的存储地址
再新建GLFW文件夹,存放GLFW库相关的include和lib文件夹
vs设置
点击项目,右键属性,C/C++,常规,附加包含目录,添加
$(SolutionDir)\Dependencies\GLFW\include;
点击项目,右键属性,链接器,常规,附加库目录,添加
$(SolutionDir)\Dependencies\GLFW\lib-vc2022
链接器,输入,附加依赖项,添加
glfw3.lib;
#include <iostream>
#include "GLFW/glfw3.h"
//一般来说""是源文件在解决方案里的,<>代表是完全的外部依赖
using namespace std;
int main() {
int a = glfwInit();
//随便找个方法来调用
cin.get();
}
这边就可以调用了
C++动态库
使用
链接器,输入,附加依赖项,添加
glfw3.lib替换成glfw3dll.lib;
这个直接执行,会报找不到glfw3.dll,这是因为我们没有把glfw3.dll可执行目录下
创建和使用库
现在我们要在解决方案创建一个新的项目作为库,可执行文件将引用它
创建库项目
1.创建一个名为Engine的空C++项目
2.右键项目,属性,配置属性,常规,配置类型改成
静态库(.lib)
3.点击显示所有文件,并创建src文件夹,和.cpp文件,.h文件
4.具体代码
Engine.h
#pragma once
namespace engine {
void PrintMessage();
}//头文件定义函数
Engine.cpp
#include "Engine.h"//""和<>功能上没有区别
#include <iostream>
using namespace std;
namespace engine {
void PrintMessage()
{
cout << "Hello" << endl;
}
}
定义了个非常简单的库,只有一个打印方法
可执行项目
1.右键项目属性,C/C++,常规,附加包含目录
$(SolutionDir)\Engine\src (根据实际的项目结构来)
2.代码
#include "Engine.h"
#include <iostream>
int main() {
engine::PrintMessage();
std::cin.get();
}
有了附加包含目录后直接就能调用了,但目前项目还没有链接到库
因为这两个项目是在一个解决方案下的,所以可以直接使用vs的引用项目来完成
右键项目,添加,引用,项目,解决方案,选择要引用的项目
多返回值处理
首先C++在默认情况下,不能返回两种类型
比较推荐的做法是
使用结构体
#include <iostream>
using namespace std;
struct MyStruct
{
string str1;
string str2;
};
MyStruct GetMyStruct() {
string a = "123";
string b = "456";
return { a,b };
}
其他方法还有
传递引用
void GetMyStruct(string& str1,string& str2) {
str1 = "123";
str2 = "456";
}
int main() {
string str1;
string str2;
GetMyStruct(str1, str2);
cout << str1 << "," << str2 << endl;
std::cin.get();
}
这算是最理想的方法之一,因为全程没有变量的复制,虽然有其他的问题,但在性能上这是最好的
传递指针
void GetMyStruct(string* str1,string* str2) {
if(str1)
*str1 = "123";
if(str2)
*str2 = "456";
}
int main() {
string str1;
GetMyStruct(&str1, nullptr);
cout << str1 << endl;
std::cin.get();
}
这个在灵活性上比较好,允许只传递部分指针,函数内可以做判断避免赋值
返回数组
有两个方式
array
#include <iostream>
#include <array>
using namespace std;
array<string,2> GetMyStruct() {
return array<string, 2>{"123", "456"};
}
int main() {
array<string, 2> array = GetMyStruct();
cout << array[0] << endl;
std::cin.get();
}
vector
#include <iostream>
#include <vector>
using namespace std;
vector<string> GetMyStruct() {
return vector<string>{"123", "456"};
}
int main() {
vector<string> array = GetMyStruct();
cout << array[0] << endl;
std::cin.get();
}
共同点是,返回的多个数据得是同一类型的,需要返回多个不同类型的东西时,不好用
array相较与vector性能上好点,但vector用着方便
元组tuple
一个类,可以包含多个变量,但不关心类型
#include <iostream>
#include <tuple>
using namespace std;
std::tuple<int, std::string> myFunction() {
int intValue = 42;
std::string stringValue = "Hello, world!";
return std::make_tuple(intValue, stringValue);
}
int main() {
auto result = myFunction();
int intValue = std::get<0>(result);
std::string stringValue = std::get<1>(result);
cout << "Int value: " << intValue << endl;
std::cin.get();
}
用起来太麻烦了,取数据也特别繁琐
最后,多返回值的场景,最推荐的还是结构体
C++模板
C++模板提供了一种编写泛型代码的方法,允许你在不了解具体数据类型的情况下编写函数或类。
函数模板
#include <iostream>
// 函数模板定义
template <typename T>
//typename和class在这里都是可以用的,功能上是一样的,但我们还是应该用typename
//typename能更清楚地表明我们是在讨论类型而非其他实体
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
std::cout << "Before swapping: x = " << x << ", y = " << y << std::endl;
swap(x, y); // 调用函数模板
std::cout << "After swapping: x = " << x << ", y = " << y << std::endl;
double m = 2.5, n = 3.5;
std::cout << "Before swapping: m = " << m << ", n = " << n << std::endl;
swap(m, n); // 同一个模板也适用于double类型
std::cout << "After swapping: m = " << m << ", n = " << n << std::endl;
}
类模板
与函数模板相似,类模板允许你定义一个可以处理多种数据类型的类
template <typename T>
class Array {
private:
T* arr;
int size;
public:
Array(int s) : size(s) {
arr = new T[s];
}
~Array() {
delete[] arr;
}
void set(int index, T value) {
if (index >= 0 && index < size)
arr[index] = value;
}
T get(int index) const {
if (index >= 0 && index < size)
return arr[index];
return T(); // 默认构造
}
};
int main() {
Array<int> intArray(5);
for (int i = 0; i < 5; ++i)
intArray.set(i, i * 2);
for (int i = 0; i < 5; ++i)
std::cout << intArray.get(i) << " ";
std::cout << "\n";
Array<double> doubleArray(5);
for (int i = 0; i < 5; ++i)
doubleArray.set(i, 1.0 / (i + 1));
for (int i = 0; i < 5; ++i)
std::cout << doubleArray.get(i) << " ";
return 0;
}
定义的模板其实并不是实际的代码,比如,你在模板中写下错误的代码,如果为进行使用,编译器就不会把模板转换成真正的代码就不会提示报错
堆和栈的内存比较
栈(Stack)和堆(Heap)都是程序在运行时使用的内存区域,它们都位于计算机的物理内存(RAM)中,尽管它们逻辑上分开,物理上都映射到RAM的不同部分,共同支持程序运行时的数据存储需求。
可以看到栈分配的内存地址都是连续的,中间穿插的是内存守卫,
内存分配速度:
- 栈:分配和释放速度快,因为只需要调整栈顶指针即可,不需要复杂的分配算法。
- 堆:分配和释放较慢,因为需要查找未使用的内存块、分配合适的大小并维护内存分配信息。此外,频繁的堆操作可能导致内存碎片。
事实上,应尽量在栈上分配,如果可以的话,在堆上分配的唯一原因,就是不能够在栈上分配
C++宏
C++宏定义是通过预处理器在编译之前对源代码进行文本替换的一种机制
#define PI 3.1415926;//无参宏定义
//定义一个表示圆周率的宏
#define MUL(x, y) (x * y);//带参数的宏定义
//定义一个求两数乘积的宏
无参宏案例
#define WIDTH 10
#define HEIGHT 5
int main() {
int area = WIDTH * HEIGHT;
std::cout << "Area: " << area << std::endl; // 输出 Area: 50
return 0;
}
带参数宏案例
#define SQUARE(x) (x * x)
int main() {
int num = 4;
int result = SQUARE(num);
std::cout << "Square of " << num << " is: " << result << std::endl; // 输出 Square of 4 is: 16
return 0;
}
注意事项
- 类型安全:宏没有类型检查,使用时需确保类型匹配,否则可能导致难以追踪的错误。
- 括号使用:在带参数宏中,为了防止运算优先级引发的问题,建议将整个替换文本包裹在括号内。
- 命名规范:宏名通常全大写,以便与变量区分。
- 避免副作用:宏替换是纯粹的文本替换,应避免在宏体内有改变变量值的操作,以免引起意料之外的结果。
- 作用域:宏定义的作用域通常是整个文件,除非使用
#undef显式取消定义。
C++auto
C++中的auto关键字是一个类型推导关键字,它允许编译器自动根据初始化表达式的类型来推断变量的类型。这在一些复杂类型、提高代码可读性和减少代码重复方面非常有用。
基本用法
基本类型推导
1auto value = 10; // value 的类型为 int
2auto text = "Hello"; // text 的类型为 const char*
复杂类型推导
1std::vector<std::pair<int, std::string>> vec;
2auto iter = vec.begin(); // iter 的类型为 std::vector<std::pair<int, std::string>>::iterator
lambda 表达式中的返回类型
auto add = [](int a, int b) { return a + b; }; // add 是一个 lambda 函数,返回类型由编译器推导为 int
auto与指针和引用
自动推导指针和引用类型
int x = 5;
auto ptr = &x; // ptr 的类型为 int*
auto& ref = x; // ref 的类型为 int&
const 和 decltype 当与const或decltype结合使用时,auto可以更精确地推导出类型:
const auto& cr = x; // cr 是一个对int的const引用
auto func() -> decltype(x + 1) { return x + 1; } // 使用decltype指定返回类型
为什么使用auto
- 简化代码:特别是对于模板和迭代器等复杂类型,
auto可以让代码更加简洁易读。 - 减少错误:手动指定类型可能会出错,而
auto可以避免这种类型的错误。 - 提高维护性:当初始化表达式的类型改变时,使用
auto的变量不需要修改类型声明。
注意事项
- 尽管
auto方便,但在变量的用途不明显时过度使用可能降低代码的可读性。 auto不能用于函数参数的类型声明,因为函数参数的类型需要在调用函数前就明确。
通过合理使用auto,可以在保持代码清晰的同时,减少编写和维护时的工作量。
C++静态数组(array)
静态数组是在编译时分配内存的数据结构,其大小在声明时就必须固定,并在整个程序执行期间保持不变
#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 5> data;
data[0] = 2;
data[1] = 1;
data[5] = 1;//有边界检查,执行到这里会报错
for (auto grade : data) {
}//可以使用迭代器
//模板类的静态数组
int arr[5];
arr[0] = 2;
arr[1] = 1;
arr[5] = 1;//没有边界检查,会成功执行,覆盖未管理的内存
//C的原始静态数组
}
void PrintArray(int* array, unsigned int size) {
for (int i = 0; i > size; i++) {
}
}
void PrintArray(array<int, 5>& array) {
for (int i = 0; i > array.size(); i++) {
}
}//可以直接传递std::array对象,包括其大小信息,无需单独传递大小参数
注意事项
- 固定大小:与C风格数组一样,
std::array的大小在定义时固定,之后不可更改。 - 内存布局:
std::array的元素在内存中连续存储,与C风格数组相同,适合于性能敏感的场景。 - 类型安全:
std::array是类型安全的,其类型包括元素类型和大小,有助于编译时错误检查。 - STL兼容性:
std::array可以像其他STL容器那样使用,与算法和迭代器无缝配合。 - 初始化:虽然可以默认初始化,但建议显式初始化所有元素,以避免未定义行为。
C++函数指针
原始指针函数
void HellpWorld()
{
cout << "He" << endl;
}
int main() {
auto function = HellpWorld;//不带括号,表示不是调用函数,而是要获取函数
function();//直接用函数指针来调用函数
}
这是函数指针的基础用法,通过函数指针调用函数
#include <iostream>
#include <vector>
using namespace std;
void PrintValue(int value)
{
cout << value << endl;
}
void ForEach(const vector<int>& values,void(*func)(int)) {
for (int value : values) {
func(value);
}
}
int main() {
vector<int> values = { 1,2,4,5,6 };
ForEach(values, PrintValue);
}
函数指针最经典的用法,作为其他函数的参数,以实现更高程度的灵活性。
void ForEach(const vector<int>& values,void(*func)(int)) {
for (int value : values) {
func(value);
}
}
int main() {
vector<int> values = { 1,2,4,5,6 };
ForEach(values, [](int value){ cout << value << endl; });
}
lambda表达式
函数指针与现代C++
虽然函数指针在C++中仍然有用,但现代C++编程倾向于使用函数对象(functors)、lambda表达式和std::function,这些提供了更安全、更灵活和类型安全的替代方案,尤其是在涉及到泛型编程和回调函数时。
C++ lambda
C++中的lambda表达式是一种定义匿名函数的方法,它允许你快速创建小型、一次性使用的函数对象,常用于算法、排序、遍历容器、事件处理等场景,为C++代码提供了更高的灵活性和表达力
Lambda表达式的语法
[capture-list] (parameters) -> return-type { function-body }
其中各部分解释如下:
- capture-list(捕获列表):定义了lambda函数体内能够访问的外部变量的方式。可以是按值捕获(
=, 默认),按引用捕获(&),或者明确指定哪些变量按值捕获哪些按引用捕获([this, &var1])。 - parameters(参数列表):类似于常规函数的参数列表,可以为空。
- return-type(返回类型):可选,如果不写,编译器会根据函数体内的return语句自动推导返回类型;如果写了,则明确指定返回类型。
- function-body:lambda表达式的实际执行代码,可以是一条表达式或者用花括号
{}包围的多条语句。
示例
-
简单的Lambda表达式
不带捕获列表,无参数,无返回值的Lambda:
[] { std::cout << "Hello, World!" << std::endl; }()调用时直接加上括号执行。
-
带有参数和返回值
接收两个int参数并返回它们的和:
auto add = [](int a, int b) { return a + b; }; int result = add(3, 4); // result = 7 -
捕获外部变量
按值捕获
x,按引用捕获y:int x = 10, y = 20; auto modify = [x, &y](int increment) { y += increment+x; }; modify(5); // y的值,x不是可修改的左值
不推荐使用using namespace std
主要有以下几个原因:
名称冲突:std命名空间包含了大量标准库的函数和类,使用using namespace std;会将这些名称全部导入到全局命名空间中。如果你的程序或其它引入的库中也有同名的标识符,就可能会引发名称冲突,导致编译错误或者难以预料的行为。
可读性和可维护性:显式地使用std::前缀可以明确指出哪些函数或类型来自于标准库,这提高了代码的可读性和可维护性。当其他开发者阅读你的代码时,他们能立刻识别出哪些功能是标准库的一部分,这对于理解代码逻辑非常重要。
控制导入范围:避免在整个文件或程序中使用using namespace std;允许你更精细地控制哪些名称被导入到作用域中。使用using std::function_name;这样的语句可以仅导入你需要的具体名称,减少潜在的冲突风险。
编译时间和依赖清晰度:虽然现代编译器优化了这一点,但在理论上,导入整个命名空间可能增加编译时间和降低编译器诊断信息的清晰度,特别是当项目变得庞大且包含多个相互依赖的模块时。
未来兼容性:随着C++标准的发展,std命名空间可能会新增成员。如果盲目地使用using namespace std;,将来标准库的更新可能会意外地引入名称冲突,导致原本工作正常的代码需要修改。
最佳实践是只在必要时显式指定std::前缀,或使用特定名称的using声明,以此来保持代码的清晰、安全和可维护性。
C++命名空间
为什么使用命名空间
例如小李和小韩都参与了一个文件管理系统的开发,它们都定义了一个全局变量 fp,用来指明当前打开的文件,将他们的代码整合在一起编译时,很明显编译器会提示 fp 重复定义(Redefinition)错误。
为了解决合作开发时的命名冲突问题,C++ 引入了命名空间(Namespace) 的概念
命名空间的概念
1.命名空间将全局作用域分成不同的部分 2.不同命名空间中的标识符可以同名而不会发生冲突 3.命名空间可以发生嵌套 4.全局作用域也叫默认命名空间
命名空间的定义
namespace 是C++中的关键字,用来定义一个命名空间,语法格式为:
namespace name{
//variables, functions, classes
}
name是命名空间的名字,它里面可以包含变量、函数、类、typedef、#define 等,最后由{ }包围。
命名空间的使用
使用整个命名空间:using namespace name; 使用命名空间中的变量:using name :: variable 使用默认命名空间中的变量: :: variable
C++线程
基础使用案例
#include <iostream>
#include <thread>
static bool s_Finished = false;//定义一个变量用于在线程外部控制线程
void Dowork()
{
using namespace std::literals::chrono_literals;//使用sleep_for需要引入
std::cout << "Started thread id = " << std::this_thread::get_id() << std::endl;
//std::this_thread::get_id() 可以获取当前线程的id
//在局部引用避免全局引用
while (!s_Finished) //变成true的时候线程就结束掉了
{
std::cout << "Working..\n";
std::this_thread::sleep_for(1s);
//睡眠一秒,避免以最快的速度重复,这样会把cpu占满的
}
}
int main()
{
std::thread worker(Dowork);
//这里传入的是函数指针所以没有带()
std::cin.get();
s_Finished = true;
//在当前线程改变
worker.join();
std::cout << "Started thread id = " << std::this_thread::get_id() << std::endl;
//在当前线程等待这个线程完成
}
多线程加互斥
用到了std::lock_guard,专门用于自动管理互斥锁的生命周期,从而确保锁的自动释放
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 互斥锁
int sharedResource = 0;
void incrementResource() {
// 使用std::lock_guard自动管理锁的生命周期
std::lock_guard<std::mutex> lock(mtx); // 在此作用域内自动上锁
++sharedResource; // 保证了对sharedResource的互斥访问
// 当离开此作用域时,lock_guard会自动调用unlock()
}
int main() {
std::thread t1(incrementResource);
std::thread t2(incrementResource);
t1.join();
t2.join();
std::cout << "The final value of the shared resource is: " << sharedResource << std::endl; // 应该是2
return 0;
}
多任务并行执行,能大幅度提升程序执行速度的
C++计时
基础功能代码
#include <iostream>
#include <chrono>
#include <thread>
int main()
{
using namespace std::literals::chrono_literals;
auto start = std::chrono::high_resolution_clock::now(); // 开始计时
std::this_thread::sleep_for(1s); // 执行需要计时的操作
auto end = std::chrono::high_resolution_clock::now(); // 结束计时
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
std::cout << "Work took " << duration << " microseconds" << std::endl;
return 0;
}
利用生存域实现的计时
#include <iostream>
#include <chrono>
#include <thread>
struct Timer
{
std::chrono::steady_clock::time_point start;
Timer()
{
start = std::chrono::high_resolution_clock::now(); // 开始计时
}
~Timer()
{
auto end = std::chrono::high_resolution_clock::now(); // 结束计时
std::chrono::duration<float> duration = end - start;
std::cout << "Work took " << duration.count() << " s" << std::endl;
}
};
void Function()
{
Timer t;
for (int i = 0;i < 100;i++) {
std::cout << "Hello" << std::endl;
std::cout << "Hello\n" ;
//使用std::endl比较慢,如果使用\n的话会快很多
}
}
int main()
{
Function();
return 0;
}
带名称参数的Timer
struct Timer
{
public:
Timer(const char* name)
:m_Name(name)
{
m_StartTimepoint = std::chrono::high_resolution_clock::now(); // 开始计时
}
void stop()
{
auto endTImepoint = std::chrono::high_resolution_clock::now(); // 结束计时
long long start = std::chrono::time_point_cast<std::chrono::milliseconds>(m_StartTimepoint).time_since_epoch().count();
long long end = std::chrono::time_point_cast<std::chrono::milliseconds>(endTImepoint).time_since_epoch().count();
std::cout << m_Name << ":" << (end-start)<<" ms\n";
m_Stopped = true;
}
~Timer()
{
if (!m_Stopped)
{
stop();
}
}
private:
const char* m_Name;
std::chrono::time_point<std::chrono::steady_clock> m_StartTimepoint;
bool m_Stopped;
};
C++多维数组
int指针多维数组
内存存储方式解析,演示内存不连续的问题
int main()
{
int** a3d = new int*[5];
for (int i = 0;i < 5;i++)
{
a3d[i] = new int[5];
for (int j = 0;j < 5;j++)
{
a3d[i][j] = 2;
}
}
a3d[0][0] = 1;
//这样的指针数组会照成内存碎片的问题,它们的内存不是连续的
int* array = new int[5 * 5];
for (int i = 0;i < 5;i++)
{
for (int j = 0;j < 5;j++)
{
array[i * 5 + j] = 2;
}
}
//这样的功能上是等价于上面,但变成一维
return 0;
}
C++排序
C++标准库提供了多种排序算法,它们都位于<algorithm>头文件中
- sort()
sort()是最基本的排序函数,它使用快速排序作为默认算法,适用于随机访问迭代器,如数组和向量。
#include <iostream>
#include <algorithm> // 包含sort函数
#include <vector> // 使用vector容器
int main() {
std::vector<int> vec = {34, 12, 89, 5, 7, 65};
std::sort(vec.begin(), vec.end()); // 对vec中的元素进行排序
for(int num : vec) {
std::cout << num << " ";
}
return 0;
}
解释: sort()的第一个参数是排序范围的开始迭代器,第二个参数是结束迭代器。此例中,对整个vec进行了升序排序。
- stable_sort()
stable_sort()与sort()类似,但它保持相等元素的原有顺序,因此更加稳定。
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {34, 12, 89, 5, 7, 65, 12};
std::stable_sort(vec.begin(), vec.end());
for(int num : vec) {
std::cout << num << " ";
}
return 0;
}
解释: 相较于sort(),stable_sort()在处理有相同值的元素时,能保持它们原来的相对顺序。
- partial_sort()
partial_sort()只对范围内的部分元素进行排序,保证前N个元素是排序好的,而后面的元素则不保证顺序。
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {34, 12, 89, 5, 7, 65};
const size_t n = 3;
std::partial_sort(vec.begin(), vec.begin() + n, vec.end());
for(int num : vec) {
std::cout << num << " ";
}
return 0;
}
解释: 这里只对vec的前3个元素进行了排序,输出结果保证前3个元素是排序好的,后面的元素顺序不确定。
- sort with custom comparator
你还可以自定义比较函数或谓词来改变排序的行为。
#include <iostream>
#include <algorithm>
#include <vector>
bool compareItems(int a, int b) {
return a > b; // 降序排列
}
int main() {
std::vector<int> vec = {34, 12, 89, 5, 7, 65};
std::sort(vec.begin(), vec.end(), compareItems);
for(int num : vec) {
std::cout << num << " ";
}
return 0;
}
解释: 通过传递自定义的比较函数compareItems,实现了降序排列。
这些示例展示了C++标准库中一些基本排序算法的用法,帮助你根据不同的需求选择合适的排序方法。
C++类型双关(std::memcpy)
C++中的类型双关(Type Punning)是指绕过类型系统限制,使同一个内存区域被解释为不同类型的手段。
这在某些情况下是有用的,比如为了高效地处理内存或在网络通信中解析数据,但也可能导致未定义行为,尤其是在涉及严格别名规则时
一个简单的类型双关示例,使用了C++11引入的std::memcpy来安全地进行类型转换,避免未定义行为
#include <iostream>
#include <cstring> // 包含memcpy函数
int main() {
// 假设我们有一个整数
int value = 42;
// 我们想要不通过常规类型转换,而是直接解释这个内存为一个浮点数
float floatValue;
// 使用memcpy来安全地进行类型转换,这是类型双关的一种安全实现方式
std::memcpy(&floatValue, &value, sizeof(value));
std::cout << "Interpreted as int: " << value << std::endl;
std::cout << "Interpreted as float: " << floatValue << std::endl;
return 0;
}
通过std::memcpy将value的二进制表示复制到floatValue的内存空间中,原始数据没有改变,但我们能够以浮点数的形式“解释”这段内存,展示了类型双关的概念。
直接使用指针重叠(例如,将一个int*强制转换为float*并解引用)在C++中通常被视为未定义行为,除非符合特定的例外情况(如字符类型指针可以访问任何类型对象的内存)
C++联合体(union)
C++中的联合体(Union)是另一种实现类型双关的机制,联合体的所有成员共享同一块内存位置,所以改变一个成员的值可能会影响到其他成员的值
union MyUnion {
int asInteger;
float asFloat;
};
C++虚析构函数(virtual)
用法就是在基类的析构函数上声明virtual 关键字
C++中,虚析构函数是一种特殊的成员函数,它允许在通过基类指针或引用来删除派生类对象时,正确调用派生类的析构函数。这在多态性中非常重要,确保了资源的正确释放,避免了内存泄漏或其他资源管理问题。
#include <iostream>
class Base {
public:
Base() { std::cout << "Base constructor called." << std::endl; }
virtual ~Base() { std::cout << "Base destructor called." << std::endl; } // 虚析构函数
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor called." << std::endl; }
~Derived() override { std::cout << "Derived destructor called." << std::endl; } // 重写虚析构函数
};
int main() {
Base* basePtr = new Derived(); // 基类指针指向派生类对象
delete basePtr; // 此处如果没有虚析构函数,只会调用Base的析构函数
return 0;
}
我们定义了一个基类Base和它的派生类Derived。Base类的析构函数被声明为虚拟的(使用virtual关键字),这意味着当通过基类指针删除派生类对象时,会自动调用派生类的析构函数,然后再调用基类的析构函数。如果Base类的析构函数不是虚拟的,那么仅Base的析构函数会被调用,导致派生类特有的资源无法被正确释放。
C++类型转换
C++提供了多种类型转换方式,包括隐式转换、显式转换(类型铸造)、以及使用转换函数。
隐式类型转换(Implicit Conversion)
C++编译器自动执行的类型转换称为隐式转换。例如,小整型向大整型转换、浮点数到整数的转换(截断)、以及算术运算中不同类型的操作数转换等。
int i = 5;
double d = i; // 隐式转换:整数转为浮点数
显式类型转换(Explicit Casts)
显式类型转换(类型铸造)是程序员明确指定的转换,有四种主要形式:
- C风格的类型转换(C-style cast):使用
(type)value语法,但不推荐,因为它不够明确且可能隐藏错误。
int i = 3.14; // 错误:不能直接从浮点转整数
int j = (int)3.14; // 显式转换:正确,但使用C++风格的更好
static_cast
- 静态转换(static_cast()):用于安全的类型转换,如基本类型间的转换、子类到父类的转换等。
double d = 3.14;
int i = static_cast<int>(d); // 明确的浮点数转整数,可能丢失精度
dynamic_cast
- 动态转换(dynamic_cast<T*>):仅用于含有虚函数的类(多态类),用于在运行时检查并转换指针或引用,安全地向下转型。
class Base { virtual ~Base() {} };
class Derived : public Base {};
Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 成功时返回派生类指针,失败返回nullptr
reinterpret_cast
- 重解释指针转换(reinterpret_cast()):用于底层的比特模式重新解释,如指针到整数的转换,或者函数指针的转换,非常危险,应谨慎使用。
void (*funcPtr)() = someFunction;
void* ptrAsInt = reinterpret_cast<void*>(funcPtr); // 将函数指针转换为整数表示
- const_cast() :改变类型的常量性或易变性,但不改变类型的基本含义。
const int ci = 5;
int& mutable_i = const_cast<int&>(ci); // 移除const属性,但修改这样的变量通常是个错误
转换函数(Conversion Functions)
转换函数是类成员函数的一种,可以定义一个类对象如何转换为另一种类型。使用关键字operator后跟目标类型来声明。
class RationalNumber {
public:
operator double() const { return numerator / denominator; } // 类RationalNumber到double的转换
private:
int numerator, denominator;
};
RationalNumber rn{1, 2};
double d = rn; // 通过转换函数,RationalNumber实例rn被转换为double
C++结构化绑定
旧的多返回值方法
#include <iostream>
#include <tuple>
std::tuple<std::string, int> CreatePerson()
{
return { "asd",22 };
}
int main() {
auto person = CreatePerson();
std::string& name=std::get<0>(person);
int& age=std::get<1>(person);
//这种取值很麻烦
std::string name0;
int age0;
std::tie(name, age) = CreatePerson();
//这种简洁了很多,但仍然需要3行
//这些都不如结构体好用
}
C++17引入了一个叫做结构化绑定的新特性,比结构体还要好用
#include <iostream>
#include <tuple>
std::tuple<std::string, int> CreatePerson()
{
return { "asd",22 };
}
int main() {
auto[name,age] = CreatePerson();
//C++17才有的这个特性
std::cout << name;
}
可以看到非常简洁好用
C++字典
在C++中,字典数据结构通常通过标准模板库(STL)中的std::map或std::unordered_map来实现。这两种容器提供了键(key)-值(value)对的存储,允许高效地进行查找、插入和删除操作。
std::map
std::map是一个关联容器,它包含以排序方式存储的唯一键值对。元素默认按照键的升序排序。
#include <map>
#include <iostream>
int main() {
// 创建一个map,键为string类型,值为int类型
std::map<std::string, int> myMap;
// 插入元素
myMap["apple"] = 1;
myMap["banana"] = 2;
myMap.insert(std::make_pair("cherry", 3));
// 查找并输出元素
auto it = myMap.find("banana");
if (it != myMap.end())
std::cout << "Found: " << it->first << " => " << it->second << std::endl;
else
std::cout << "Not found." << std::endl;
// 删除元素
myMap.erase("apple");
// 遍历map
for(const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
std::unordered_map
std::unordered_map与std::map相似,但它是无序的,并且通常提供更快的平均访问时间,因为它使用哈希表实现。
#include <map>
#include <iostream>
int main() {
// 创建一个map,键为string类型,值为int类型
std::map<std::string, int> myMap;
// 插入元素
myMap["apple"] = 1;
myMap["banana"] = 2;
myMap.insert(std::make_pair("cherry", 3));
// 查找并输出元素
auto it = myMap.find("banana");
if (it != myMap.end())
std::cout << "Found: " << it->first << " => " << it->second << std::endl;
else
std::cout << "Not found." << std::endl;
// 删除元素
myMap.erase("apple");
// 遍历map
for(const auto &pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
尽量用unordered_map,速度快很多
C++17特性
std::optional
解释: std::optional是一个可以包含或不包含值的容器。它主要用于表示那些可能没有有效值的场景,替代了过去经常使用nullptr、特殊值或空对象的做法。
用法示例:
#include <optional>
std::optional<int> findValue(const std::vector<int>& vec, int target) {
for(const auto& val : vec) {
if(val == target) return std::optional<int>(val);
}
return {}; // 返回一个空的optional
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto result = findValue(vec, 6);
if(result) {
std::cout << "Found: " << *result << std::endl; // 如果有值则解引用输出
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
应用场景: 查询结果可能不存在的情况,如数据库查询、查找算法等。
std::variant
解释: std::variant是一个类型安全的联合体,可以存储多种不同类型中的一个。与传统联合体不同,variant知道当前存储的是哪种类型,并提供类型安全的访问方式。
用法示例:
#include <variant>
#include <iostream>
int main() {
std::variant<int, std::string, double> var = 10;
var = std::string("Hello");
var = 3.14;
// 访问variant的值,需要使用std::get<T>()
try {
std::string strVal = std::get<std::string>(var);
std::cout << strVal << std::endl;
} catch(const std::bad_variant_access&) {
std::cout << "Error: Variant does not hold a string" << std::endl;
}
return 0;
}
应用场景: 当一个变量可能属于几种不同的类型,且这些类型之间没有共同基类时,例如解析多种格式的数据、设计灵活的回调系统等。
CT
typedef struct _tagCTVariant
{
enum CT_VAR_TYPE vt;
struct
{
int nVal;
float fVal;
char strVal[CT_MAX_STR_LEN];
}val;
}CTVariant;
enum CT_VAR_TYPE
{
TVT_INT = 0,
TVT_FLT = 1,
TVT_STR = 2,
};
void function{
CTVariant attrVariant;
attrVariant.vt = CT_VAR_TYPE::TVT_INT;
attrVariant.val.nVal = nValue;
}
std::any
解释: std::any是一个能够持有任意类型对象的容器。与std::variant相比,any可以存储任何类型,但不提供类型安全的直接访问,需要通过类型检查和转换来获取值。
#include <any>
#include <iostream>
int main() {
std::any anyVal = 42;
anyVal = std::string("Dynamic typing!");
// 获取any中的值需要先检查类型
if(anyVal.type() == typeid(int)) {
std::cout << std::any_cast<int>(anyVal) << std::endl;
} else if(anyVal.type() == typeid(std::string)) {
std::cout << std::any_cast<std::string>(anyVal) << std::endl;
}
return 0;
}
应用场景: 需要存储未知类型或动态类型数据的场合,比如某些配置系统、通用事件处理系统等,但需要注意的是,由于缺乏类型安全,使用时需谨慎处理类型转换。
std::string_view
解释
- 概念:
std::string_view是一个非拥有、不变的字符串引用类型。它实质上是一个指针加上长度,指向现有的字符数组,但并不负责管理这些字符的内存生命周期。这意味着创建或传递string_view对象不会引起内存分配或复制。 - 成员函数:它提供了类似于
std::string的操作接口,如.size(),.length(),.substr(),.find()等,但不包括那些会改变字符串内容的方法,如插入或删除字符。
使用方法
#include <string_view>
void printText(std::string_view text) {
std::cout << text << std::endl;
}
int main() {
std::string str = "Hello, World!";
std::string_view view(str); // 从std::string创建string_view
const char* cstr = "C-style string";
std::string_view viewFromCStr(cstr, strlen(cstr)); // 从C风格字符串创建string_view
printText(view); // 传递string_view给函数
printText("Direct literal"); // 直接使用字符串字面量
}
使用场景
- 性能敏感的字符串处理:在需要频繁操作字符串但又不修改内容的场景下,使用
string_view可以避免不必要的字符串复制,显著提高性能。 - API接口设计:作为函数参数时,使用
string_view可以接受各种字符串类型(如std::string、C风格字符串、字符串字面量)而无需复制,提高灵活性和效率。 - 文本解析:在解析大量文本数据时,
string_view可以用来快速定位和处理子串,而不需要复制整个字符串。 - 日志记录、数据分析:在处理日志记录或数据分析任务中,使用
string_view可以高效地遍历和分析数据,而不需要额外的内存开销。 - 模板元编程:由于
string_view的轻量级特性,它在模板元编程中也很有用,可以作为字符串常量的高效表示。
字符串使用优化
导致多次分配,运行慢的字符串处理
void* operator new(size_t size)
{
s_AllocCount++;
std::cout << "AllocCount" << size << "bytes\n" << std::endl;
return malloc(size);
}//专门用来监控分配行为,实际不能这样用,会导致错误
void PrintName(const std::string name) {
}
int main()
{
std::string name = "asd";//这里会一次分配,
PrintName(name);//值传递也会导致分配
auto firstName = name.substr(0, 2);//取子字符串也会导致分配
auto secondName = name.substr(2, 1);
std::cin.get();
return 0;
}
共导致了4次分配
优化思路
1.指针视图
name.substr(0, 2)
如果我们仅仅是像看里面的东西而不进行再次修改
直接通过指针取读取指定大小的内存,就能知道了
没必要在分配内存给子字符串
C++17的新特性的std::string_view替我们做好了这一切
std::string_view firstName(name.c_str(), 2);//构造里传指针和大小,获取字符串视图
std::string_view secondName(name.c_str()+2, 1);
void PrintName(const std::string_view name) {
}
string_view和string是不同的类型,函数得重载
2,传递引用
还是那个思路,如果不进行改变,就没必要复制
void PrintName(const std::string& name)
#define STRING_VIEW 0
#if STRING_VIEW
void PrintName(const std::string name) {
}
#else
void PrintName(const std::string_view name) {
}
#endif
int main()
{
#if STRING_VIEW
std::string name = "asd";//这里会一次分配,
PrintName(name);//值传递也会导致分配
auto firstName = name.substr(0, 2);//取子字符串也会导致分配
auto secondName = name.substr(2, 1);
#else
std::string name = "asd";//这里会一次分配,
PrintName(name);
std::string_view firstName(name.c_str(), 2);//构造里传const char*和大小,获取字符串视图
std::string_view secondName(name.c_str() +2, 1);
PrintName(firstName);
#endif
std::cin.get();
return 0;
}
可视化基准测试
测试数据可以输出为JSON格式,利用Chrome Tracing 进行可视化
用Chrome进入chrome://tracing/
生成json源码
#include <iostream>
#include <algorithm>
#include <chrono>
#include <fstream>
#include <thread>
#include <thread>
// 结构体ProfileResult,用于存储性能测试结果
struct ProfileResult
{
std::string Name; // 测试名称
long long Start, End; // 测试开始和结束的时间点
uint32_t ThreadID; // 执行测试的线程ID
};
// 结构体InstrumentationSession,用于存储一个测试会话的信息
struct InstrumentationSession
{
std::string Name; // 会话名称
};
// 类Instrumentor,用于进行性能测试和结果输出
class Instrumentor
{
private:
InstrumentationSession* m_CurrentSession; // 当前的测试会话
std::ofstream m_OutputStream; // 输出流,用于写入测试结果
int m_ProfileCount; // 记录当前会话已完成的测试数量
public:
// 构造函数
Instrumentor()
: m_CurrentSession(nullptr), m_ProfileCount(0)
{
}
// 开始一个新的测试会话
void BeginSession(const std::string& name, const std::string& filepath = "results.json")
{
m_OutputStream.open(filepath); // 打开输出文件
WriteHeader(); // 写入测试结果的头部信息
m_CurrentSession = new InstrumentationSession{ name }; // 创建新的会话
}
// 结束当前的测试会话
void EndSession()
{
WriteFooter(); // 写入测试结果的尾部信息
m_OutputStream.close(); // 关闭输出文件
delete m_CurrentSession; // 删除当前会话
m_CurrentSession = nullptr; // 将当前会话指针设置为nullptr
m_ProfileCount = 0; // 重置测试数量
}
// 将一个测试结果写入输出文件
void WriteProfile(const ProfileResult& result)
{
if (m_ProfileCount++ > 0)
m_OutputStream << ","; // 如果已经有测试结果,那么在新的测试结果前添加一个逗号
// 处理测试名称中可能存在的双引号字符
std::string name = result.Name;
std::replace(name.begin(), name.end(), '"', ''');
// 写入测试结果
// 测试结果的格式是JSON,包含测试名称、开始和结束时间、线程ID等信息
m_OutputStream << "{";
m_OutputStream << ""cat":"function",";
m_OutputStream << ""dur":" << (result.End - result.Start) << ',';
m_OutputStream << ""name":"" << name << "",";
m_OutputStream << ""ph":"X",";
m_OutputStream << ""pid":0,";
m_OutputStream << ""tid":" << result.ThreadID << ",";
m_OutputStream << ""ts":" << result.Start;
m_OutputStream << "}";
m_OutputStream.flush(); // 刷新输出流,确保结果被写入文件
}
// 写入测试结果的头部信息
void WriteHeader()
{
m_OutputStream << "{"otherData": {},"traceEvents":[";
m_OutputStream.flush();
}
// 写入测试结果的尾部信息
void WriteFooter()
{
m_OutputStream << "]}";
m_OutputStream.flush();
}
// 获取Instrumentor的单例
// 由于这是一个性能测试工具,我们通常只需要一个实例
static Instrumentor& Get()
{
static Instrumentor instance;
return instance;
}
};
// 类InstrumentationTimer,用于计时和性能测试
class InstrumentationTimer
{
public:
// 构造函数
// 在创建对象时开始计时
InstrumentationTimer(const char* name)
: m_Name(name), m_Stopped(false)
{
m_StartTimepoint = std::chrono::high_resolution_clock::now();
}
// 析构函数
// 如果计时器没有停止,那么在对象被销毁时自动停止计时,并记录测试结果
~InstrumentationTimer()
{
if (!m_Stopped)
Stop();
}
// 停止计时,并记录测试结果
void Stop()
{
// 获取当前时间点
auto endTimepoint = std::chrono::high_resolution_clock::now();
// 计算开始和结束的时间(单位:微秒)
long long start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimepoint).time_since_epoch().count();
long long end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count();
// 获取当前线程的ID
uint32_t threadID = std::hash<std::thread::id>{}(std::this_thread::get_id());
// 将测试结果写入输出文件
Instrumentor::Get().WriteProfile({ m_Name, start, end, threadID });
m_Stopped = true; // 标记计时器已停止
}
private:
const char* m_Name; // 测试名称
std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint; // 计时开始的时间点
bool m_Stopped; // 标记是否已经停止计时
};
void Function1()
{
InstrumentationTimer time("Function1");
for (int i = 0; i < 1000; i++)
std::cout << "Hello World #" << i << std::endl;
}
void Function2()
{
InstrumentationTimer time("Function2");
for (int i = 0; i < 1000; i++)
std::cout << "Hello World #" << sqrt(i) << std::endl;
}
int main()
{
Instrumentor::Get().BeginSession("Profile"); // 启动会话
Function1();
Function2();
Instrumentor::Get().EndSession();
std::cin.get();
return 0;
}
方法套方法,一样
void RunBenchmarks()
{
InstrumentationTimer timer("RunBenchMarks");
std::cout << "Running Benchmarks...\n";
Function1();
Function2();
}
int main()
{
Instrumentor::Get().BeginSession("Profile");
RunBenchmarks();
Instrumentor::Get().EndSession();
std::cin.get();
}
必须复制粘贴我们调用的每个函数的名称,比较麻烦
使用宏定义
可以关闭计时,可以自动添加函数名
#define PROFILING 1
#if PROFILING
#define PROFILE_SCOPE(name) InstrumentationTimer timer##__LINE__(name);
#define PROFILE_FUNCTION() PROFILE_SCOPE(__FUNCTION__)// 预定义的宏,会返回一个包含当前函数名称的字符串。
#else
#define PROFILE_SCOPE(name)
#endif
void Function1()
{
PROFILE_FUNCTION();
for (int i = 0; i < 1000; i++)
std::cout << "Hello World #" << i << std::endl;
}
如果我们想要更多信息,比如说有函数重载的情
#define PROFILE_FUNCTION() PROFILE_SCOPE(__FUNCSIG__)// 预定义的宏,返回一个包含当前函数签名
多线程也能用
void RunBenchmarks()
{
PROFILE_FUNCTION();
std::cout << "Running Benchmarks...\n";
std::thread a([]() {Function1(1); });
std::thread b([]() {Function1(); });
// 最后两个join让这两个线程都完成工作前,不会真正地退出这个Benchmark函数
a.join();
b.join();
}
完整测试代码
#include <iostream>
#include <algorithm>
#include <chrono>
#include <fstream>
#include <thread>
#include <thread>
// 结构体ProfileResult,用于存储性能测试结果
struct ProfileResult
{
std::string Name; // 测试名称
long long Start, End; // 测试开始和结束的时间点
uint32_t ThreadID; // 执行测试的线程ID
};
// 结构体InstrumentationSession,用于存储一个测试会话的信息
struct InstrumentationSession
{
std::string Name; // 会话名称
};
// 类Instrumentor,用于进行性能测试和结果输出
class Instrumentor
{
private:
InstrumentationSession* m_CurrentSession; // 当前的测试会话
std::ofstream m_OutputStream; // 输出流,用于写入测试结果
int m_ProfileCount; // 记录当前会话已完成的测试数量
public:
// 构造函数
Instrumentor()
: m_CurrentSession(nullptr), m_ProfileCount(0)
{
}
// 开始一个新的测试会话
void BeginSession(const std::string& name, const std::string& filepath = "results.json")
{
m_OutputStream.open(filepath); // 打开输出文件
WriteHeader(); // 写入测试结果的头部信息
m_CurrentSession = new InstrumentationSession{ name }; // 创建新的会话
}
// 结束当前的测试会话
void EndSession()
{
WriteFooter(); // 写入测试结果的尾部信息
m_OutputStream.close(); // 关闭输出文件
delete m_CurrentSession; // 删除当前会话
m_CurrentSession = nullptr; // 将当前会话指针设置为nullptr
m_ProfileCount = 0; // 重置测试数量
}
// 将一个测试结果写入输出文件
void WriteProfile(const ProfileResult& result)
{
if (m_ProfileCount++ > 0)
m_OutputStream << ","; // 如果已经有测试结果,那么在新的测试结果前添加一个逗号
// 处理测试名称中可能存在的双引号字符
std::string name = result.Name;
std::replace(name.begin(), name.end(), '"', ''');
// 写入测试结果
// 测试结果的格式是JSON,包含测试名称、开始和结束时间、线程ID等信息
m_OutputStream << "{";
m_OutputStream << ""cat":"function",";
m_OutputStream << ""dur":" << (result.End - result.Start) << ',';
m_OutputStream << ""name":"" << name << "",";
m_OutputStream << ""ph":"X",";
m_OutputStream << ""pid":0,";
m_OutputStream << ""tid":" << result.ThreadID << ",";
m_OutputStream << ""ts":" << result.Start;
m_OutputStream << "}";
m_OutputStream.flush(); // 刷新输出流,确保结果被写入文件
}
// 写入测试结果的头部信息
void WriteHeader()
{
m_OutputStream << "{"otherData": {},"traceEvents":[";
m_OutputStream.flush();
}
// 写入测试结果的尾部信息
void WriteFooter()
{
m_OutputStream << "]}";
m_OutputStream.flush();
}
// 获取Instrumentor的单例
// 由于这是一个性能测试工具,我们通常只需要一个实例
static Instrumentor& Get()
{
static Instrumentor instance;
return instance;
}
};
// 类InstrumentationTimer,用于计时和性能测试
class InstrumentationTimer
{
public:
// 构造函数
// 在创建对象时开始计时
InstrumentationTimer(const char* name)
: m_Name(name), m_Stopped(false)
{
m_StartTimepoint = std::chrono::high_resolution_clock::now();
}
// 析构函数
// 如果计时器没有停止,那么在对象被销毁时自动停止计时,并记录测试结果
~InstrumentationTimer()
{
if (!m_Stopped)
Stop();
}
// 停止计时,并记录测试结果
void Stop()
{
// 获取当前时间点
auto endTimepoint = std::chrono::high_resolution_clock::now();
// 计算开始和结束的时间(单位:微秒)
long long start = std::chrono::time_point_cast<std::chrono::microseconds>(m_StartTimepoint).time_since_epoch().count();
long long end = std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count();
// 获取当前线程的ID
uint32_t threadID = std::hash<std::thread::id>{}(std::this_thread::get_id());
// 将测试结果写入输出文件
Instrumentor::Get().WriteProfile({ m_Name, start, end, threadID });
m_Stopped = true; // 标记计时器已停止
}
private:
const char* m_Name; // 测试名称
std::chrono::time_point<std::chrono::high_resolution_clock> m_StartTimepoint; // 计时开始的时间点
bool m_Stopped; // 标记是否已经停止计时
};
#define PROFILING 1
#if PROFILING
#define PROFILE_SCOPE(name) InstrumentationTimer timer##__LINE__(name);
#define PROFILE_FUNCTION() PROFILE_SCOPE(__FUNCSIG__)// 预定义的宏,会返回一个包含当前函数名称的字符串。
#else
#define PROFILE_SCOPE(name)
#endif
namespace testnamespace {
void Function1(int a)
{
PROFILE_FUNCTION();
for (int i = 0; i < 1000; i++)
std::cout << "Hello World #" << i << std::endl;
}void Function1()
{
PROFILE_FUNCTION();
for (int i = 0; i < 1000; i++)
std::cout << "Hello World #" << i << std::endl;
}
void Function2()
{
PROFILE_FUNCTION();
for (int i = 0; i < 1000; i++)
std::cout << "Hello World #" << sqrt(i) << std::endl;
}
void RunBenchmarks()
{
PROFILE_FUNCTION();
std::cout << "Running Benchmarks...\n";
std::thread a([]() {Function1(1); });
std::thread b([]() {Function1(); });
// 最后两个join让这两个线程都完成工作前,不会真正地退出这个Benchmark函数
a.join();
b.join();
}
}
int main()
{
Instrumentor::Get().BeginSession("Profile");
testnamespace::RunBenchmarks();
Instrumentor::Get().EndSession();
std::cin.get();
return 0;
}
单例模式
代码实现
多线程安全单例模式实现
#include <mutex>
#include <memory>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initInstanceFlag, &Singleton::init);
return *instance;
}
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作符
private:
Singleton() = default; // 私有构造函数
static void init() {
instance.reset(new Singleton());
}
static std::once_flag initInstanceFlag;
static std::unique_ptr<Singleton> instance;
};
// 初始化静态成员 不在方法体内
std::once_flag Singleton::initInstanceFlag;
std::unique_ptr<Singleton> Singleton::instance = nullptr;
int main() {
// 线程安全地获取Singleton实例
Singleton& singleton1 = Singleton::getInstance();
// 再次获取Singleton实例,确保是同一个实例
Singleton& singleton2 = Singleton::getInstance();
}
实际项目的用法
CPostDllIngerface* CPostDllIngerface::getInstance()
{
static CPostDllIngerface instance;
return &instance;
}
这种方式称为"本地静态变量"单例模式,是C++11之后推荐的一种简单而高效的单例实现方法。它的优点包括:
- 简洁性:实现非常简单,易于理解。
- 线程安全:C++11标准保证了局部静态变量的初始化是线程安全的,因此在这个实现中,
instance的构造只会在第一次调用getInstance()时发生,并且这个过程是线程安全的。 - 延迟初始化:直到首次需要时才创建实例,有助于提高程序效率,特别是在实例从未被使用的情况下。
然而,这种实现也有其局限性:
- 控制力较弱:相比于更复杂的单例实现(如上面提到的基于
std::call_once的版本),这种实现方式对于实例的生命周期控制较少,比如你不能显式销毁实例。 - 不支持参数化构造:如果
CPostDllInterface的构造需要参数,这种方法就不太适用了,因为静态局部变量的初始化不能传递参数。
总的来说,如果你的单例不需要复杂的初始化逻辑或参数,且不关心显式的销毁时机,那么CPostDllInterface::getInstance()所示的实现是一个很好的选择,它既简洁又高效。对于更复杂的需求,则可能需要采用更复杂的单例模式实现。
小字符串优化SSO
C++中的小字符串优化(Short String Optimization, SSO)也被称为小对象优化(Small String Optimization),是许多标准库实现(如libstdc++和libc++)对std::string类采用的一种优化策略。这项优化允许短字符串直接存储在std::string对象内部,而不需要动态分配内存。这样可以减少内存分配和释放的开销,提升性能,尤其是在处理短字符串时。
#include <iostream>
#include <string>
void* operator new(size_t size) // 操作符重载
{
std::cout << "Allocating " << size << " bytes\n";
return malloc(size);
}
int main()
{
std::string name = "Cherno"; // 显然小于15个字符
std::cin.get();
}
Debug下打印,release不打印
跟踪内存的简单实现
通过重载new delete,来跟踪堆内存的分配情况
#include <iostream>
struct AllocationMetrics
{
uint32_t TotalAllocated = 0;
uint32_t TotalFreed = 0;
uint32_t CurrentUsage() { return TotalAllocated - TotalFreed; }
};
static AllocationMetrics s_allocation_metrics;
void* operator new(size_t size)
{
s_allocation_metrics.TotalAllocated += size;
return malloc(size);
}
void operator delete(void* memory, size_t size)
{
s_allocation_metrics.TotalFreed += size;
free(memory);
}
struct Object
{
int x, y, z;
};
static void PrintMemoryUsage()
{
std::cout << "Memory Usage: " << s_allocation_metrics.CurrentUsage() << " bytes\n";
}
int main()
{
PrintMemoryUsage();
std::string string = "Cherno";
PrintMemoryUsage();
{
std::unique_ptr<Object> obj2 = std::make_unique<Object>();
PrintMemoryUsage();
}
PrintMemoryUsage();
}
这里也可以印证SSO(小字符串优化),debug和release下占用的堆内存不一样
左值与右值(std::move)
-
左值和右值的概念
- 左值(lvalue):指能够标识一个存储位置的表达式,其值可以被读取和修改。通常对应于具名的变量、被引用的对象以及某些表达式的结果。左值是可以别取地址的,也是可以被修改的(const修饰的左值除外)。
- 右值(rvalue):指不能标识存储位置的表达式,其值只能被读取,不能被修改。右值可以是字面量、临时对象、返回右值引用的函数调用的结果等。右值不能被读取地址,也不能被修改。右值本质就是一个临时变量或常量值。
-
右值引用的概念
- 右值引用(rvalue reference):用于引用右值的一种引用类型。通过右值引用,可以允许开发者对临时对象进行更高效的操作,如移动语义(move semantics)和完美转发(perfect forwarding)。
-
基于右值引用进行的move优化
-
move语义:C++11及之后的标准中引入了“移动语义”以及相关的std::move函数。移动语义允许资源(如动态分配的内存、文件句柄等)从一个对象转移到另一个对象,而不是复制。这样做的主要目的是优化性能和资源管理。
-
std::move:std::move函数并不是真的“移动”对象,它只是将其转换为右值引用。通过std::move,可以将一个左值转换为右值引用,从而触发移动语义。
-
使用场景
- 转移所有权:当需要转移对象的所有权而不需要复制对象时,std::move非常有用。例如,转移大型容器或自定义资源管理对象的所有权时,可以避免昂贵的深度复制操作。
- 优化标准库容器操作:在使用标准库容器(如std::vector、std::string等)时,通过std::move可以避免不必要的拷贝操作,提高性能。
- 资源管理:在处理动态内存分配、文件句柄等资源时,通过移动语义可以更加高效地管理这些资源,减少内存泄漏等问题的发生。
-
std::string str1 = "Hello, world!";
std::string str2 = std::move(str1);
str2的构造不会复制str1的字符串内容。相反,它将"偷走"(即移动)str1的内部指针,现在该指针指向的内存属于str2。str1被置于一个有效但未定义的状态,通常不应再使用。
总结
总之记住:
- 左值是某种存储支持的变量,右值是临时值;
- 左值引用之接受左值,除非用
const,右值引用只接受右值
移动语义std::move
移动语义本质上允许我们移动对象,这在 C++11 之前是不可能的,因为 C++11 才引入了右值引用,这是移动语义所必需的。
使用右值引用将将入参转为临时对象来使用move
Entity(String&& name)
: m_Name((String&&)name) {}
// 在实践中,应该用更优雅的std::move来实现:
Entity(String&& name)
:m_Name(std::move(name)) {} // 本质上和上面作用相同,下节会详细讲述
这里移动构造函数确实“偷取”了other对象的内存。它直接接管了other.m_Data指向的内存,而不是分配新的内存并复制内容。这样,移动操作通常比复制操作更快、更有效率,因为它避免了资源的额外分配和复制。
之后,原始对象other的成员被设置为默认值(例如,nullptr),确保其析构函数不会释放已经转移出去的资源。这是移动语义的典型实现。
std::move本身并不执行任何移动操作;它仅仅重新解释对象,使其可以被当作右值使用
class MyClass {
public:
MyClass(std::vector<int> data) : m_data(std::move(data)) {} // 移动构造函数示例
MyClass& operator=(MyClass&& other) {
if (this != &other) {
m_data = std::move(other.m_data); // 资源从other转移到*this
// 可能还需要清理或置空other的数据,确保其处于有效但未定义状态
}
return *this;
}
private:
std::vector<int> m_data;
};
// 使用示例
MyClass obj1(std::vector<int>{1, 2, 3});
MyClass obj2;
obj2 = std::move(obj1); // 这里std::move使得obj1的数据被移动到obj2,而不是复制
C++核心
内存分区
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
1.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
存放 CPU 执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
全局变量和静态变量存放在此.
全局区还包含了常量区, 字符串常量和其他常量也存放在此.
该区域的数据在程序结束后由操作系统释放.
//全局变量
int g_a = 10;
int g_b = 10;
//全局常量
const int c_g_a = 10;
const int c_g_b = 10;
int main() {
//局部变量
int a = 10;
int b = 10;
//打印地址
cout << "局部变量a地址为: " << (int)&a << endl;
cout << "局部变量b地址为: " << (int)&b << endl;
cout << "全局变量g_a地址为: " << (int)&g_a << endl;
cout << "全局变量g_b地址为: " << (int)&g_b << endl;
//静态变量
static int s_a = 10;
static int s_b = 10;
cout << "静态变量s_a地址为: " << (int)&s_a << endl;
cout << "静态变量s_b地址为: " << (int)&s_b << endl;
cout << "字符串常量地址为: " << (int)&"hello world" << endl;
cout << "字符串常量地址为: " << (int)&"hello world1" << endl;
cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl;
cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;
const int c_l_a = 10;
const int c_l_b = 10;
cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;
system("pause");
return 0;
}
std::thread
场景:多线程任务顺序执行
互斥锁
这边不能用互斥锁,因为由同一个线程来对一个 mutex 对象进行 lock 和 unlock 操作
而这里的需求是,执行任务前要先锁上,执行对应的任务后解锁对应的互斥锁,这样对mutex的操作不在同一个方法内,会造成未定义的行为
常用方法案例
#include <iostream>
#include <mutex>
#include <functional>
std::mutex mtx1;
void first(std::function<void()> printFirst) {
mtx1.lock();
printFirst();
mtx1.unlock();
}
这种方法的缺点是,如果 printFirst() 执行过程中发生异常,mtx1 将不会被解锁,从而导致死锁。
std::mutex mtx1;
void first(std::function<void()> printFirst) {
std::unique_lock<std::mutex> lock_1(mtx1);
printFirst();
}
std::unique_lock 提供了更多的灵活性,例如尝试锁定和锁的所有权转移
std::mutex mtx1;
void first(std::function<void()> printFirst) {
lock_guard<mutex> guard(mtx_1);
if (num > 10) {
num -= 10;
}
}
std::lock_guard 不提供高级特性,如尝试锁定或锁的所有权转移,但它更轻量级,且足够用于大多数简单的锁管理需求
条件变量
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <thread>
#include <functional>
using namespace std;
//`std::condition_variable` 是一种用来同时阻塞多个线程的** 同步原语** (synchronization primitive),** `std::condition_variable` 必须和 `std::unique_lock` 搭配使用** :
class Foo {
condition_variable cv;
mutex mtx;
int k = 0;
public:
void first(function<void()> printFirst) {
printFirst();
k = 1;
cv.notify_all(); // 通知其他所有在等待唤醒队列中的线程
}
void second(function<void()> printSecond) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this]() { return k == 1; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 1 才能继续运行
printSecond();
k = 2;
cv.notify_one(); // 随机通知一个(unspecified)在等待唤醒队列中的线程
}
void third(function<void()> printThird) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this]() { return k == 2; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 2 才能继续运行
printThird();
}
};
int main() {
Foo foo;
thread t1(&Foo::first, std::ref(foo), []() { std::cout << "first" << std::endl; });
thread t2(&Foo::second, std::ref(foo), []() { std::cout << "second" << std::endl; });
thread t3(&Foo::third, std::ref(foo), []() { std::cout << "third" << std::endl; });
t1.join();
t2.join();
t3.join();
std::cin.get();
return 0;
}
原子操作
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <thread>
#include <functional>
using namespace std;
//`std::condition_variable` 是一种用来同时阻塞多个线程的** 同步原语** (synchronization primitive),** `std::condition_variable` 必须和 `std::unique_lock` 搭配使用** :
class Foo {
std::atomic<bool> a{ false };
std::atomic<bool> b{ false };
public:
void first(function<void()> printFirst) {
printFirst();
a = true;
}
void second(function<void()> printSecond) {
while (!a)
this_thread::sleep_for(chrono::milliseconds(1));
printSecond();
b = true;
}
void third(function<void()> printThird) {
while (!b)
this_thread::sleep_for(chrono::milliseconds(1));
printThird();
}
};
int main() {
Foo foo;
thread t1(&Foo::first, std::ref(foo), []() { std::cout << "first" << std::endl; });
thread t2(&Foo::second, std::ref(foo), []() { std::cout << "second" << std::endl; });
thread t3(&Foo::third, std::ref(foo), []() { std::cout << "third" << std::endl; });
t1.join();
t2.join();
t3.join();
std::cin.get();
return 0;
}
this_thread::yield();
可以用于代替 this_thread::sleep_for(chrono::milliseconds(1));
- std::this_thread::yield() 更适用于需要快速响应和高 CPU 利用率的场景,尤其是在自旋锁或高并发环境中。
- std::this_thread::sleep_for() 更适用于不需要立即响应,或者预计等待时间较长的场景,有助于减少 CPU 资源的浪费。
原子操作(自旋锁)
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <thread>
#include <functional>
using namespace std;
//`std::condition_variable` 是一种用来同时阻塞多个线程的** 同步原语** (synchronization primitive),** `std::condition_variable` 必须和 `std::unique_lock` 搭配使用** :
class FooBar {
private:
int n;
std::atomic<bool> a{ false };
public:
FooBar(int n) {
this->n = n;
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
while (a)this_thread::yield();
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
a = true;
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
while (!a)this_thread::yield();
// printBar() outputs "bar". Do not change or remove this line.
printBar();
a = false;
}
}
};
int main() {
FooBar fob(5);
thread t1(&FooBar::foo, std::ref(fob), []() { std::cout << "foo"; });
thread t2(&FooBar::bar, std::ref(fob), []() { std::cout << "bar"; });
t1.join();
t2.join();
std::cin.get();
return 0;
}