前言
- 1 你所能用的正与你手写的效率相同
- 2 你不需要为你没有用到的特性付出
(无脑的调用函数or公式的空壳人类请出门右转)
c
001 scanf and strcpy "_s"bug?
- 微软官方说明
- 1 Visual Studio 库中的许多函数、成员函数、函数模板和全局变量已弃用,改用微软新增的强化函数(在原有基础加_s,但是伴随着参数发生变化需要点开观察)
- debug
//二选一 写在主函数所在文件第一行
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
#pragma warning(disable : 4996) //关闭之下所有警告
#pragma warning(suppress : 4996)、、关闭下一条警告
scanf("%d",num);
–>
003 内存四区
- c语言实际测试中堆区申请的内存并不能达到理论上的3gb,甚至2gb也会申请失败?
- 1 内存碎片:内存碎片是已经指定的内存快之间随机分散在堆空间内,分配的区块空间的间隙是空闲内存块,当进行3GB(或大块)内存申请,因为内存碎片导致剩余空间不足,或其他部分的代码申请的内存没有释放,导致
- 2 操作系统限制:不同操作系统会对堆内存的最大申请量设置一定的限制。设计者为了防止应用程序占用过多的系统资源。
- 1 初步设计:减少内存碎片的产生,在释放内存时,尽量将内存块回收到内存池中,而不是直接释放。
// 创建内存池
// 参数 size 是内存池的大小
struct mp_pool_s *mp_create_pool(size_t size)
{
struct mp_pool_s *pool;
// 如果 size 小于 PAGE_SIZE 或者 size 不是 PAGE_SIZE 的倍数,则将 size 设置为 PAGE_SIZE
if (size < PAGE_SIZE || size % PAGE_SIZE != 0) {
size = PAGE_SIZE;
}
// 使用 posix_memalign 函数分配对齐的内存
int ret = posix_memalign((void **) &pool,MP_ALIGNMENT, size);
// 如果分配失败,返回 NULL
if (ret)
return NULL;
// 初始化内存池
pool->large = NULL;
pool->current = pool->head = (unsigned char *) pool + sizeof(struct mp_pool_s);
pool->head->last = (unsigned char *) pool + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s);
pool->head->end = (unsigned char *) pool + PAGE_SIZE;
pool->head->failed = 0;
// 返回创建的内存池
return pool;
}
// 销毁内存池
// 参数 pool 是要销毁的内存池
void mp_destroy_pool(struct mp_pool_s *pool)
{
struct mp_large_s *large;
// 遍历大块内存,释放已分配的内存
for (large = pool->large; large; large = large->next)
{
if (large->alloc)
free(large->alloc);
}
struct mp_node_s *cur, *next;
// 遍历内存池,释放已分配的内存
cur = pool->head->next;
while (cur)
{
next = cur->next;
free(cur);
cur = next;
}
// 释放内存池
free(pool);
}
// 释放内存
// 参数 pool 是内存池,p 是要释放的内存地址
void mp_free(struct mp_pool_s *pool, void *p)
{
struct mp_large_s *large;
// 遍历大块内存,如果 p 是大块内存的一部分,则释放该内存
for (large = pool->large; large; large = large->next)
{
if (p == large->alloc)
{
free(large->alloc);
large->size = 0;
large->alloc = NULL;
return;
}
}
struct mp_node_s *cur = NULL;
// 遍历内存池,如果 p 是内存池的一部分,则释放该内存
for (cur = pool->head; cur; cur = cur->next)
{
if ((unsigned char *) cur <= (unsigned char *) p && (unsigned char *) p <= (unsigned char *) cur->end)
{
cur->quote--;
if (cur->quote == 0)
{
if (cur == pool->head)
pool->head->last = (unsigned char *) pool + sizeof(struct mp_pool_s) + sizeof(struct mp_node_s);
else
cur->last = (unsigned char *) cur + sizeof(struct mp_node_s);
cur->failed = 0;
pool->current = pool->head;
}
return;
}
}
}
- 缺陷和bug1:posix_memalign函数来分配对齐的内存,如果posix_memalign函数调用失败,它会返回一个非零值,并且errno将被设置为一个表示错误的值.但是没有对返回值进行检查,也没有对errno进行检查。这可能会导致在内存分配失败时,程序无法正确处理.
int ret = posix_memalign((void **) &pool,MP_ALIGNMENT, size);//
if (ret)
{
perror("posix_memalign failed");
return NULL;
}
- 缺陷和bug2:内存泄漏:内存释放:在mp_free函数中,我们可以看到在释放内存之前,没有检查pool或p是否为NULL。如果pool或p为NULL,
if (pool == NULL || p == NULL)
{
return;
}
- 缺陷和bug3内存泄漏:在mp_destroy_pool函数中,我们可以看到在释放内存池之前,没有检查pool是否为NULL。如果pool为NULL,
if (pool == NULL)
{
return;
}
- 缺陷和bug2:内存池大小:在mp_create_pool函数中,我们可以看到在分配内存池之前,没有检查size是否为0。如果size为0,
if (size == 0)
{
return NULL;
}
- 2 操作系统和内存管理器的设置,在系统级别进行,而不是在程序中进行的,获取操作系统管理员或者系统级别的开发者权限尝试调整这些限制,或者尝试使用不同的操作系统或内存管理器。
- 检查程序的内存使用情况,尽量减少不必要的内存使用,释放不再需要的内存。
- 如果可能,尝试使用其他的内存分配策略,例如使用连续的内存块,或者使用更高效的内存管理器。
vs2022使用
-
1 代码类型补全提示,工具->文本编辑器->c/c++ lintellisense->启动内联(全选)
-
枚举
/*先switch然后两下tab
会补完到default,光标显示在switch后的变量
这时输入枚举,输完后回车,补完所有枚举的case */
预处理
#define 定义一个预处理宏
#undef 取消宏的定义
#if 编译预处理中的条件命令,相当于C语法中的if语句
#ifdef 判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef 与#ifdef相反,判断某个宏是否未被定义
#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
defined 与#if, #elif配合使用,判断某个宏是否被定义
#if 表达式
程序段1
#else
程序段2
#endif
//表示:如果表达式为真,则编译程序段1,否则编译程序段2.
#include <iostream>
//宏定义注释
int main(void)
{
int a = 0;
#if 0
a = 1;
#endif
printf("%d\n",a);
return 0;
}
#define qwer int//start
/*各种函数*/
#undef//end
#defind 判断宏名是否被定义 //#ifdef和#ifndef仅能一次判断一个宏名,而defined能做到一次判断多个宏名
typedef Data int //可以控制一个类型或者一个数据,更好的调整防止漏掉
- 实例
#include <stdio.h>
#define MAX 10
#define MIN 2
void test()
{
#if defined(MAX) && define (MIN) && //...
printf(“三个宏已全部定义\n”);
#elif MAX==10
printf(“三个宏未全部定义\n”);
#endif
}
糟粕/旧的设计模式(一个操作的淘汰不代表完全不能用)
-
1 类和面向对象弃用
- (1)单例模式(Singleton Pattern):单例模式被广泛使用,但也容易被滥用。它将一个类限制为只能创建一个对象,并提供全局访问点。然而,单例模式经常引入全局状态和紧密耦合的依赖关系,使得代码难以测试和扩展。
- (2)多重继承(Multiple Inheritance):多重继承允许一个类从多个基类派生,但它可能导致继承图的复杂性增加。多重继承可能引入菱形继承问题(Diamond Inheritance Problem),使得代码难以理解和维护。此外,多重继承还可能导致命名冲突和二义性。
- (4)巨大的继承层级(Deep Inheritance Hierarchy):当类的继承层级非常深时,代码的可读性和可维护性可能会下降。巨大的继承层级使得代码的行为和依赖关系变得复杂,同时也增加了代码的耦合性。
- (5)大量的getter和setter方法:在某些情况下,类中存在大量的getter和setter方法,这破坏了封装性,也使代码变得冗长。过多的getter和setter方法可能暴露了过多的类内部细节,增加了代码的耦合性。
- (6)巨大的类(God Object):巨大的类承担了太多的责任和功能,而没有很好地分割成更小的、可管理的部分。这种设计可能导致代码的可读性和可维护性降低,同时也使得代码难以进行单元测试和重用。
-
1 过度使用全局状态(Global State):过度依赖全局状态会导致代码的可读性和可维护性降低。全局状态使得代码的行为变得不可预测,并增加了代码的耦合性。过度依赖全局状态和全局函数会导致代码的可读性和可维护性下降。全局变量和函数使得代码的依赖关系变得复杂,而且难以进行单元测试和重用。
-
2 副作用(Side Effects):副作用指的是对于给定输入,函数或方法除了返回一个结果之外,还会对系统状态或外部资源进行修改。过度依赖副作用可能导致代码的行为不可预测,并增加代码的复杂性和维护难度。
-
3 魔术数字(Magic Numbers):魔术数字是指在代码中出现的没有解释或命名的硬编码常量。魔术数字使得代码难以理解和维护,并且容易引入错误。
-
4 过度使用设计模式:虽然设计模式是一种有用的工具,但过度使用某些设计模式可能导致代码的复杂性增加。在某些情况下,使用设计模式可能会使代码变得冗长、难以理解和维护。
-
5 过度使用注释(Overuse of Comments):注释是一种有用的工具,但过度使用注释可能说明代码本身不够清晰和自解释。过多的注释会导致代码冗长,并且容易出现注释与实际代码不一致的情况。
-
6 过度复杂的条件逻辑(Complex Conditional Logic):过度复杂的条件逻辑使得代码难以理解和维护。复杂的条件语句和嵌套关系容易引入错误,并且使得代码更加脆弱。
数据结构
数组
- 线性搜索
- 二分查找
- 快速排序
- 归并排序
- 堆排序
链表
- 双向链表
- 单向链表
- 循环链表
- 链表反转
- 链表合并
- 判断链表是否有环
- 找到链表中的中间节点
- 删除链表中指定元素
栈
- 栈的实现
- 中缀表达式转后缀表达式
- 后缀表达式求值
- 括号匹配问题
- N皇后问题
队列
- 队列的实现
- 循环队列
- 双端队列
- 优先队列
- 迷宫问题
- 广度优先搜索
树
- 二叉树的遍历(前序、中序、后序)
- 平衡二叉树(AVL树)
- 红黑树
- 堆(最大堆、最小堆)
- 字典树(Trie树)
- Huffman树
- B树和B+树
图
- 邻接表和邻接矩阵的表示
- 深度优先搜索
- 广度优先搜索
- 最短路径问题(Dijkstra算法、Floyd算法)
- 最小生成树问题(Prim算法、Kruskal算法)
哈希表
- 哈希表的实现
- 冲突处理方法(链表法、开放寻址法)
- 一致性哈希
无名结构体和有名结构体
- 有名结构体的用途较为广泛,因为它定义了一种新的数据类型,可以在多个地方重复使用。适合于那些需要在程序中多次使用或者作为函数参数、返回值等场合的数据结构。
- 无名结构体通常用于定义单个复杂数据项或者当做特定作用域内部的一次性使用的数据结构
有名 | 无名 |
---|---|
可重用性高,便于维护和扩展; | 可以直接在定义时初始化,减少代码量。 |
易于理解和交流,提高代码的可读性; | 不便于重复使用,每次使用都需要重新定义; |
可以被用作函数的参数或返回类型,增强了代码的模块化。 | 在复杂程序中,过多使用无名结构体可能会降低代码的可维护性和可读性。 |
在某些仅需一次使用的场景中可能显得稍微繁琐 | 简化了代码,适用于只需要一次性使用的场景; |
开发一个需要处理多个员工信息的系统,使用有名结构体来定义员工信息 | 某个函数内部需要临时组织一些数据,而这组数据在函数外部不再使用 |
- typedef 和无名结构体的组合在游戏中开发
- 1 定义简洁的数据类型
- 1.1 游戏开发涉及大量的数据结构来表示游戏世界中的元素,如角色属性、坐标位置、游戏状态等。使用typedef配合无名结构体
typedef struct { float x, y, z; } Position; typedef struct { int health; int mana; } Stats;
- 2 封装组件数据
- 2.1 游戏通常由多个系统组成,每个系统可能需要处理特定的数据组件
typedef struct { unsigned int id; char name[50]; Position position; Stats stats; } Entity;//Entity封装了一个游戏实体的基本数据,包括位置和状态,使得在处理游戏逻辑时更加方便。
- 3 创建灵活的接口参数
- 3.1 函数或方法需要接受多种类型的数据
typedef struct { int type; union { int intValue; float floatValue; char* stringValue; }data }EventParam;//这个结构体可以用于事件系统,允许发送和接收多种类型的数据,而不必为每种数据类型定义单独的接口。
- 4 优化内存布局
- 4.1
typedef struct { Vector3D position; Vector3D velocity; float mass; unsigned char isActive : 1; // 使用位字段节省空间 } PhysicsComponent;//PhysicsComponent用于存储物理系统中对象的数据
深拷贝/浅拷贝
- 浅拷贝:包含指针的数据结构,浅拷贝仅仅复制指针本身,而不复制指针所指向的数据。这意味着原始数据和复制后的数据会共享同一块内存地址中的数据。
typedef struct
{
int *ptr;
} Example;
Example original, copy;
original.ptr = (int*)malloc(sizeof(int)); // 分配内存
*original.ptr = 10; // 赋值
// 浅拷贝
copy.ptr = original.ptr;//original和copy共享相同的内存地址,因此对copy.ptr或original.ptr的任何修改都会影响到另一个
- 深拷贝不仅复制数据结构的表面层级,还包括复制指针所指向的实际数据。这意味着创建了原始数据的一个完整副本,原始数据和复制后的数据不会共享任何内存地址。
Example deep_copy(Example src)
{
Example dest;
dest.ptr = (int*)malloc(sizeof(int)); // 为dest分配新的内存
if (dest.ptr != NULL)
{
*dest.ptr = *src.ptr; // 复制实际的数据
}
return dest;
}
Example copy = deep_copy(original);
->next
数据类型
-
1 整数类型:使用场景:一般用于存储和操作整数。
- int:用于表示整数值,通常占用机器字长大小。
- short:用于表示较小范围的整数值,通常占用 2 字节。
- long:用于表示较大范围的整数值,通常占用 4 字节或 8 字节。
- long long:用于表示非常大范围的整数值,通常占用 8 字节。
-
固定大小整数类型:不受环境影响的准确字节大小
无符号:size_t uint8_t、int16_t、uint32_t uint64_t uintptr_t
int8_t、int16_t、int32_t、int64_t intptr_t -
2 字符类型:使用场景:处理文本和字符串数据。
- char:用于表示字符。
- signed char:用于表示有符号字符。
- unsigned char:用于表示无符号字符。
-
3 浮点数类型:使用场景:处理实数和十进制数据。
- float:用于表示单精度浮点数,通常占用 4 字节。
- double:用于表示双精度浮点数,通常占用 8 字节。
- long double:用于表示更高精度的浮点数,占用字节大小因平台而异。
-
4 bool:用于表示真(true)或假(false)值。
-
5 指针类型:使用场景:动态内存分配、数组操作、函数参数传递等。
int*、char* 等:用于表示指向不同类型的指针。 -
复合类型:使用场景:组织相关数据和内存优化。
- 枚举类型:enum:用于定义一组命名的常量。使用场景:限制变量取值范围,增加代码可读性。
- struct:用于自定义数据结构。
- union:用于共享内存空间,不同成员使用相同的内存。
-
void:无类型或无返回值。使用场景:函数返回类型、空指针等。
数据结构_算法
- 顺序表和链表 不适用高数量场景
存储方式:
顺序表:使用一段连续的存储空间来存储数据元素,可以通过索引直接访问任何位置的元素。因此,顺序表支持随机访问,时间复杂度为O(1)。
链表:链表则是由一系列的节点组成,每个节点包含数据元素和指向下一个节点的指针。节点在内存中不一定是连续存储的,通过指针将它们串联起来。链表通常只能通过头节点开始顺序访问,因此不支持随机访问,时间复杂度为O(n),其中n为链表的长度。
插入和删除操作:
顺序表:插入和删除可能造成数据移动,特别是在中间或者开头插入/删除元素时,需要移动后续元素,平均时间复杂度为O(n)。
链表:由于链表节点可以通过指针直接链接,插入和删除操作相对简单,并且不需要移动大量数据。在已知位置的情况下,插入和删除操作的时间复杂度为O(1)。
空间复杂度:
顺序表:需要预先分配一定大小的内存空间,可能会浪费部分空间,尤其是当数据量变化不定时。
链表:空间利用率高,可以动态分配内存空间,每个节点的大小可以根据需要而分配。
适用场景:
顺序表:频繁随机访问元素的场景,例如索引表、稀疏矩阵等。
链表: 插入和删除操作频繁,且规模不确定的场景,例如队列、堆栈、大整数的表示等。
- such as : pvz game
顺序表的适用场景
植物列表:
游戏中的植物可以使用顺序表来进行管理。顺序表适合于频繁的随机访问操作,例如通过植物的编号或者位置索引快速访问特定的植物数据,如生命值、攻击力等。
僵尸列表:
类似地,游戏中的僵尸也可以使用顺序表来管理。这样可以快速地遍历和访问不同的僵尸实例,例如在战斗中检查僵尸的位置和状态。
链表的适用场景:
子弹列表:链表的插入和删除操作适合处理不断产生和消失的子弹实例,
动态事件队列:
游戏中的动态事件,例如僵尸的出现或特殊技能的施放,可以使用链表来管理。链表的动态插入和删除特性适合处理随机事件的触发和管理,例如随机生成的奖励物品或者特殊天气效果等。
- 顺序表
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int DataType;
typedef struct list
{
DataType *data;//数据数组 value表示其中一个的具体数值
int maxCapacity;//capacity容量
int count;//当前位置数 index
} list, *LPSQ;
LPSQ create_list()
{
LPSQ list = (LPSQ)calloc(1, sizeof(*list));
//sizeof(list)list是指针类型,而不是结构体本身
//应该写为sizeof(struct list)或sizeof(*list)
assert(list);
return list;
}
void destroy_list(LPSQ list) // destory 销毁
{
free(list->data);
list->data = NULL;
list->maxCapacity = 0;
list->count = 0;
free(list);
}
void checkFullAndExpand(LPSQ list) // check检查 enpand 扩大
{
if (list->count >= list->maxCapacity)
{
int newCapacity = list->maxCapacity * 2;
int *newData = (int *)realloc(list->data, newCapacity * sizeof(int));
if (newData == NULL)
{
fprintf(stderr, "Error: Memory reallocation failed.\n");
exit(EXIT_FAILURE);
}
list->data = newData;
list->maxCapacity = newCapacity;
}
}
bool get_value_sqlist(LPSQ list, int index, int *value) // value数值
{
if (index < 0 || index >= list->count)
{
fprintf(stderr, "Error: Get index out of bounds.\n");
return false;
}
*value = list->data[index];
return true;
}
bool insert_list(LPSQ list, int index, int value)
{
if (index < 0 || index >= list->count)
{
fprintf(stderr, "ERROOR:Insert index out of bounds.\n");
return false;
}
checkFullAndExpand(list);
// 将index及其之后的元素后移一位
for (int i = list->count; i > index; --i)
{
list->data[i] = list->data[i - 1];
}
list->data[index] = value;
list->count++;
return true;
}
void insert_list_end(LPSQ list, DataType data)
{
assert(list);
if (list->count == list->maxCapacity)
{
list->maxCapacity += 10;
DataType *temp = (DataType *)realloc(list->data, sizeof(DataType) * list->maxCapacity);
assert(temp);
list->data = temp;
}
list->data[list->count++] = data;
}
bool delete_sqlist(LPSQ list, int index)
{
if (index < 0 || index >= list->count)
{
fprintf(stderr, "Error: Delete index out of bounds.\n");
return false;
}
for (int i = index; i < list->count-1; i++)
{
list->data[i]=list->data[i+1];
}
list->count--;
return true;
}
void print_sqlpst(LPSQ list)
{
for (int i = 0; i < list->count; i++)
{
printf("%d ", list->data[i]);
}
printf("\n");
}
- 单向链表
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义链表节点结构体
typedef struct ListNode
{
int data;
struct ListNode *next;
} ListNode, *LPLN;
// 定义链表结构体
typedef struct
{
LPLN head; // 头节点指针
LPLN tail; //尾节点指针
int size; // 链表当前大小
} LinkedList, *LPLINK;
LPLINK create_linkedlist()
{
LPLINK list = (LPLINK)malloc(sizeof(LinkedList));
if (list)
{
list->head = NULL;
list->size = 0;
}
return 0;
}
// 销毁链表
void destroy_linkedlist(LinkedList *list)
{
if (list)
{
LPLN current = list->head;
while (current)
{
LPLN next = current->next;
free(current);
current = next;
}
free(list);
}
}
// 在链表末尾插入节点
void insert_end(LPLINK list, int value)
{
LPLN newNode = (ListNode *)malloc(sizeof(ListNode));
if (newNode)
{
newNode->data = value;
newNode->next = NULL;
if (list->head == NULL)
list->head = newNode;
else
{
LPLN current = list->head;
while (current)
{
current = current->next;
}
current->next = newNode;
}
list->size++;
}
else fprintf(stderr, "Error: Memory allocation failed.\n");
}
//在链表末尾插入节点优化思想
- 优化单向链表
-
创建链表函数 create_linkedlist:
- 哨兵节点作为头节点:list->sentinel = (LPLN)malloc(sizeof(ListNode));
- 尾指针初始化:list->tail = list->sentinel;
- 大小初始化:list->size = 0;
-
销毁链表函数 destroy_linkedlist:
- 释放节点内存:free(current);
- 释放哨兵节点内存:free(list->sentinel);
- 释放链表结构内存:free(list);
-
在链表尾部插入元素函数 insert_end:
- 链接新节点到尾部:list->tail->next = newNode;
- 更新尾指针为新节点:list->tail = newNode;
- 增加链表大小:list->size++;
-
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode {
int data;
struct ListNode *next;
} ListNode, *LPLN;
typedef struct {
ListNode *sentinel; // 哨兵节点作为头节点
ListNode *tail; // 尾节点指针
int size; // 链表大小
} LinkedList, *LPLINK;
// 创建链表
LPLINK create_linkedlist() {
LPLINK list = (LPLINK)malloc(sizeof(LinkedList));
if (list) {
list->sentinel = (LPLN)malloc(sizeof(ListNode)); // 创建哨兵节点
list->sentinel->next = NULL;
list->tail = list->sentinel; // 初始时尾指针指向哨兵节点
list->size = 0; // 初始大小为0
}
return list;
}
// 销毁链表
void destroy_linkedlist(LPLINK list) {
if (list) {
LPLN current = list->sentinel->next; // 跳过哨兵节点
while (current) {
LPLN next = current->next;
free(current);
current = next;
}
free(list->sentinel); // 释放哨兵节点
free(list);
}
}
// 在链表尾部插入元素
void insert_end(LPLINK list, int value) {
LPLN newNode = (LPLN)malloc(sizeof(ListNode));
if (newNode) {
newNode->data = value;
newNode->next = NULL;
list->tail->next = newNode; // 将新节点链接到尾部节点的后面
list->tail = newNode; // 更新尾部节点为新节点
list->size++; // 链表大小增加
} else {
fprintf(stderr, "Error: Memory allocation failed.\n");
}
}
// 打印链表
void print_list(LPLINK list) {
LPLN current = list->sentinel->next; // 跳过哨兵节点
while (current) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
// 主函数测试链表功能
int main() {
LPLINK list = create_linkedlist();
insert_end(list, 1);
insert_end(list, 2);
insert_end(list, 3);
print_list(list);
destroy_linkedlist(list);
return 0;
}
- quque
#include "list_node.c"
// 入队操作
void enqueue(LPLINK list, int value)
{
LPLN newNode = (ListNode *)malloc(sizeof(ListNode));
if (newNode)
{
newNode->data = value;
newNode->next = NULL;
list->tail->next = newNode;
list->tail = newNode;
list->size++;
}
else
{
fprintf(stderr, "Error: Memory allocation failed.\n");
}
}
// 出队操作(dequeue)
int dequeue(LPLINK list)
{
if (list->size == 0)
{
fprintf(stderr, "Error: Queue is empty.\n");
return -1; // 代表错误的标志值
}
LPLN nodeToDequeue = list->sentinel->next; // 获取队列头部节点
int value = nodeToDequeue->data; // 保存数据
list->sentinel->next = nodeToDequeue->next; // 更新哨兵节点的下一个指针
if (list->tail == nodeToDequeue) // 如果队列为空
{
list->tail = list->sentinel; // 更新尾指针指向哨兵节点
}
free(nodeToDequeue); // 释放被删除的节点
list->size--; // 链表大小减少
return value;
}
// 打印链表
void print_list(LPLINK list)
{
LPLN current = list->sentinel->next; // 跳过哨兵节点
while (current)
{
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
// 示例主函数
int main()
{
LPLINK list = create_linkedlist();
if (!list)
return 1; // 如果链表创建失败,退出程序
// 入队操作
enqueue(list, 10);
enqueue(list, 20);
enqueue(list, 30);
// 打印链表
printf("Queue after enqueues:\n");
print_list(list);
// 出队操作
printf("Dequeued value: %d\n", dequeue(list));
printf("Dequeued value: %d\n", dequeue(list));
// 打印链表
printf("Queue after dequeues:\n");
print_list(list);
// 销毁链表
destroy_linkedlist(list);
return 0;
}
- 双向链表
- 总结修改思路
typedef struct ListNode
{
int data;
struct ListNode *prev; // 增加一个指向前一个节点的指针
struct ListNode *next;
} ListNode, *LPLN;
LPLINK create_linkedlist()
{
LPLINK list = (LPLINK)malloc(sizeof(LinkedList));
if (list)
{
list->sentinel = (LPLN)malloc(sizeof(ListNode)); // 创建哨兵节点
list->sentinel->next = NULL;
list->tail = list->sentinel; // 初始时尾指针指向哨兵节点
list->size = 0; // 初始大小为0
}
return list;
}
修改在链表尾部插入元素的函数
if (newNode)
{
newNode->data = value;
newNode->next = NULL;
newNode->prev = list->tail; // 新节点的前指针指向当前尾节点
list->tail->next = newNode; // 将新节点链接到尾部节点的后面
list->tail = newNode; // 更新尾部节点为新节点
list->size++; // 链表大小增加
}
这个链表实现基本可以满足大多数常见的需求和环境下的使用,但是还有一些情况和考虑因素可以进一步讨论和优化:
内存管理效率:
当链表非常大时,频繁的内存分配和释放可能会影响性能。可以考虑实现一个对象池来管理节点的内存,减少频繁的malloc/free操作,从而提高效率。
线程安全性:
如果在多线程环境中使用这个链表,当前的实现并没有考虑线程安全。可以通过添加互斥锁或者使用无锁数据结构来保证线程安全。
错误处理:
目前的实现在节点分配内存失败时,只是简单地输出错误信息到标准错误流。更健壮的实现应该考虑如何优雅地处理内存分配失败的情况,例如通过返回错误码或者抛出异常。
功能扩展:
如果需要支持更多的操作,例如删除节点、在指定位置插入节点等,可以进一步扩展链表的功能。
性能优化:
尽管尾指针可以提高插入操作的效率,但是对于其他操作(如查找、删除等),链表的性能可能较低。在需要频繁进行这些操作的情况下,可能需要考虑使用其他数据结构,如平衡树或者哈希表。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 双向链表节点结构体定义
typedef struct DoubleListNode
{
int data;
struct DoubleListNode *prev;
struct DoubleListNode *next;
} DoubleListNode, *DoubleListNodePtr;
// 双向链表结构体定义
typedef struct
{
DoubleListNodePtr sentinel; // 哨兵节点作为头节点
DoubleListNodePtr tail; // 尾节点指针
int size; // 链表大小
} DoubleLinkedList, *DoubleLinkedListPtr;
// 创建双向链表
DoubleLinkedListPtr create_double_linkedlist()
{
DoubleLinkedListPtr list = (DoubleLinkedListPtr)malloc(sizeof(DoubleLinkedList));
if (!list)
{
fprintf(stderr, "Error: Memory allocation failed.\n");
return NULL;
}
list->sentinel = (DoubleListNodePtr)malloc(sizeof(DoubleListNode));
if (!list->sentinel)
{
fprintf(stderr, "Error: Memory allocation failed.\n");
free(list);
return NULL;
}
list->sentinel->next = NULL;
list->sentinel->prev = NULL; // 初始化哨兵节点的前后指针为NULL
list->tail = list->sentinel; // 初始时尾指针指向哨兵节点
list->size = 0; // 初始大小为0
return list;
}
// 销毁双向链表
void destroy_double_linkedlist(DoubleLinkedListPtr list)
{
if (!list)
return;
DoubleListNodePtr current = list->sentinel->next;
while (current != NULL)
{
DoubleListNodePtr temp = current;
current = current->next;
free(temp);
}
free(list->sentinel);
free(list);
}
// 迭代查询指定位置的节点
DoubleListNodePtr find_node_position(DoubleLinkedListPtr list, int position)
{
assert(list != NULL);
assert(position >= 0 && position<list->size);
DoubleListNodePtr current=list->sentinel->next;
for (int i = 0; i < position; i++)
{
current=current->next;
}
return current;
}
// head
void insert_head(DoubleLinkedListPtr list, int value)
{
assert(list != NULL);
DoubleListNodePtr newNode = (DoubleListNodePtr)malloc(sizeof(DoubleListNode));
if (!newNode)
{
fprintf(stderr, "Error: Memory allocation failed.\n");
return;
}
newNode->data = value;
newNode->prev = list->sentinel;
newNode->next = list->sentinel->next;
if (list->sentinel->next)
{
list->sentinel->next->prev = newNode;
}
list->sentinel->next = newNode;
if (list->size == 0)
{
list->tail = newNode;
}
list->size++;
}
// 在双向链表尾部插入节点
void insert_end(DoubleLinkedListPtr list, int value)
{
assert(list != NULL);
DoubleListNodePtr newNode = (DoubleListNodePtr)malloc(sizeof(DoubleListNode));
if (!newNode)
{
fprintf(stderr, "Error: Memory allocation failed.\n");
return;
}
newNode->data = value;
newNode->next = NULL;
newNode->prev = list->tail; // 新节点的前指针指向当前尾节点
list->tail->next = newNode; // 将新节点链接到尾部节点的后面
list->tail = newNode; // 更新尾部节点为新节点
if (list->size == 0)
list->sentinel->next = newNode;
list->size++; // 链表大小增加
}
void insert_position(DoubleLinkedListPtr list, int value, int position)
{
assert(list != NULL);
assert(position >= 0 && position <= list->size);
if (position == 0)
{
insert_head(list,value);
return;
}
if(position==list->size)
{
insert_end(list,value);
return;
}
DoubleListNodePtr current=find_node_position(list,position-1);
DoubleListNodePtr newNode=(DoubleListNodePtr)malloc(sizeof(DoubleListNode));
if(!newNode)
{
fprintf(stderr, "Error: Memory allocation failed.\n");
return;
}
newNode->data=value;
newNode->next=current->next;
newNode->prev=current;
current->next->prev=newNode;
current->next=newNode;
list->size++;
}
//
void delete_position_optimized(DoubleLinkedListPtr list, int position)
{
assert(list!=NULL);
assert(position>=0&&position<list->size);
DoubleListNodePtr curernt=find_node_position(list,position);
// 删除头节点
if(curernt==list->sentinel->next)
{
list->sentinel->next=curernt->next;
if(curernt->next) curernt->next->prev=NULL;
}
// 删除尾节点
else if (curernt==list->tail)
{
list->tail=curernt->prev;
curernt->prev->next=NULL;
}
// 删除中间节点
else
{
curernt->prev->next=curernt->next;
curernt->next->prev=curernt->prev;
}
free(curernt);
list--;
}
// 打印双向链表
void print_double_list(DoubleLinkedListPtr list)
{
DoubleListNodePtr current = list->sentinel->next; // 跳过哨兵节点
printf("List: ");
while (current)
{
printf("%d <-> ", current->data);
current = current->next;
}
printf("NULL\n");
}
/* main test
DoubleLinkedListPtr list = create_double_linkedlist();
insert_end(list, 1);
insert_end(list, 2);
insert_end(list, 3);
print_double_list(list);
destroy_double_linkedlist(list);
*/
- 广义表
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h> // 引入stdbool.h头文件,用于bool类型
// 定义GeneralizedList节点结构体
typedef struct GeneralizedList
{
bool tag; // 使用bool类型来区分原子和子表
union
{
char data; // 原子节点数据
struct GeneralizedList *sublist; // 子表节点指针
} atom_or_sublist;
struct GeneralizedList *next; // 指向下一个节点
} GeneralizedList;
// 函数原型声明
void free_generalized_list(GeneralizedList *list);
//用于释放GeneralizedList结构体的内存。由于这个函数在delete_atom和print_generalized_list函数中不会被直接调用,但它在main函数中被调用
void delete_atom(GeneralizedList **list, char data);
//用于删除特定数据的原子节点。delete_atom函数会被main函数调用,且可能会使用到free_generalized_list
// 创建原子节点
GeneralizedList *create_atom(char data)
{
GeneralizedList *node = (GeneralizedList *)malloc(sizeof(GeneralizedList));
if (node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
node->tag = true; // 原子节点的tag为true
node->atom_or_sublist.data = data;
node->next = NULL;
return node;
}
// 创建子表节点
GeneralizedList *create_sublist(GeneralizedList *sublist)
{
GeneralizedList *node = (GeneralizedList *)malloc(sizeof(GeneralizedList));
if (node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
node->tag = false; // 子表节点的tag为false
node->atom_or_sublist.sublist = sublist;
node->next = NULL;
return node;
}
// 打印Generalized List
void print_generalized_list(GeneralizedList *list)
{
if (list == NULL)
{
printf("()");
return;
}
if (list->tag)
{
printf("%c", list->atom_or_sublist.data);
}
else
{
printf("(");
print_generalized_list(list->atom_or_sublist.sublist);
printf(")");
}
if (list->next != NULL)
{
printf(",");
print_generalized_list(list->next);
}
}
// 释放Generalized List内存
void free_generalized_list(GeneralizedList *list)
{
if (list == NULL)
return;
if (!list->tag)
{
free_generalized_list(list->atom_or_sublist.sublist);
}
free_generalized_list(list->next);
free(list);
}
//如果广义表特别深或节点特别多,可能会导致栈溢出或效率问题
// 打印Generalized List
void print_generalized_list(GeneralizedList *list)
{
if (list == NULL)
{
printf("()");
return;
}
if (list->tag)
{
printf("%c", list->atom_or_sublist.data);
}
else
{
printf("(");
print_generalized_list(list->atom_or_sublist.sublist);
printf(")");
}
if (list->next != NULL)
{
printf(",");
print_generalized_list(list->next);
}
}
// 释放Generalized List内存
void free_generalized_list(GeneralizedList *list)
{
if (list == NULL)
return;
if (!list->tag)
{
free_generalized_list(list->atom_or_sublist.sublist);
}
free_generalized_list(list->next);
free(list);
}
int main()
{
// 创建Generalized List (a, ((b), c), d)
GeneralizedList *sublist_b = create_atom('b');
GeneralizedList *sublist_bc = create_sublist(sublist_b);
sublist_bc->next = create_atom('c');
GeneralizedList *generalized_list = create_atom('a');
generalized_list->next = create_sublist(sublist_bc);
generalized_list->next->next = create_atom('d');
// 打印Generalized List
printf("Generalized List: ");
print_generalized_list(generalized_list);
printf("\n");
// 打印深度和节点数量
int depth = calculate_depth(generalized_list);
int count = count_nodes(generalized_list);
printf("深度: %d\n", depth);
printf("节点数量: %d\n", count);
// 删除节点
char data_to_delete = 'b';
printf("删除节点 '%c'\n", data_to_delete);
delete_atom(&generalized_list, data_to_delete);
// 打印更新后的Generalized List
printf("更新后的Generalized List: ");
print_generalized_list(generalized_list);
printf("\n");
// 释放Generalized List内存
free_generalized_list(generalized_list);
return 0;
}
- 完善广义表
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h> // 引入stdbool.h头文件,用于bool类型
// 定义GeneralizedList节点结构体
typedef struct GeneralizedList
{
bool tag; // 使用bool类型来区分原子和子表
union
{
char data; // 原子节点数据
struct GeneralizedList *sublist; // 子表节点指针
} atom_or_sublist;
struct GeneralizedList *next; // 指向下一个节点
} GeneralizedList;
// 函数原型声明
void free_generalized_list(GeneralizedList *list);
void delete_atom(GeneralizedList **list, char data);
// 创建原子节点
GeneralizedList *create_atom(char data)
{
GeneralizedList *node = (GeneralizedList *)malloc(sizeof(GeneralizedList));
if (node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
node->tag = true; // 原子节点的tag为true
node->atom_or_sublist.data = data;
node->next = NULL;
return node;
}
// 创建子表节点
GeneralizedList *create_sublist(GeneralizedList *sublist)
{
GeneralizedList *node = (GeneralizedList *)malloc(sizeof(GeneralizedList));
if (node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
node->tag = false; // 子表节点的tag为false
node->atom_or_sublist.sublist = sublist;
node->next = NULL;
return node;
}
// 如果广义表特别深或节点特别多,可能会导致栈溢出或效率问题
// 打印Generalized List
void print_generalized_list(GeneralizedList *list)
{
if (list == NULL)
{
printf("()");
return;
}
if (list->tag)
{
printf("%c", list->atom_or_sublist.data);
}
else
{
printf("(");
print_generalized_list(list->atom_or_sublist.sublist);
printf(")");
}
if (list->next != NULL)
{
printf(",");
print_generalized_list(list->next);
}
}
// 释放Generalized List内存
void free_generalized_list(GeneralizedList *list)
{
if (list == NULL)
return;
if (!list->tag)
{
free_generalized_list(list->atom_or_sublist.sublist);
}
free_generalized_list(list->next);
free(list);
}
int calculate_depth(GeneralizedList *list)
{
if (list == NULL)
return 0;
typedef struct StackNode
{
GeneralizedList *node;
int depth;
} StackNode;
StackNode *stack = (StackNode *)malloc(sizeof(StackNode) * 1000); // 使用静态分配的栈
int top = -1;
int max_depth = 0;
stack[++top] = (StackNode){list, 1};
while (top >= 0)
{
StackNode current = stack[top--];
GeneralizedList *node = current.node;
int current_depth = current.depth;
if (node->tag == false)
{
if (current_depth > max_depth)
{
max_depth = current_depth;
}
stack[++top] = (StackNode){node->atom_or_sublist.sublist, current_depth + 1};
}
if (node->next != NULL)
{
stack[++top] = (StackNode){node->next, current_depth};
}
}
free(stack);
return max_depth;
}
// 计算节点数量 (使用显式栈)
int count_nodes(GeneralizedList *list)
{
if (list == NULL)
return 0;
typedef struct StackNode
{
GeneralizedList *node;
} StackNode;
StackNode *stack = (StackNode *)malloc(sizeof(StackNode) * 1000); // 使用静态分配的栈
int top = -1;
int count = 0;
stack[++top] = (StackNode){list};
while (top >= 0)
{
StackNode current = stack[top--];
GeneralizedList *node = current.node;
count++;
if (node->tag == false)
{
stack[++top] = (StackNode){node->atom_or_sublist.sublist};
}
if (node->next != NULL)
{
stack[++top] = (StackNode){node->next};
}
}
free(stack);
return count;
}
int main()
{
// 创建Generalized List (a, ((b), c), d)
GeneralizedList *sublist_b = create_atom('b');
GeneralizedList *sublist_bc = create_sublist(sublist_b);
sublist_bc->next = create_atom('c');
GeneralizedList *generalized_list = create_atom('a');
generalized_list->next = create_sublist(sublist_bc);
generalized_list->next->next = create_atom('d');
// 打印Generalized List
printf("Generalized List: ");
print_generalized_list(generalized_list);
printf("\n");
// 打印深度和节点数量
int depth = calculate_depth(generalized_list);
int count = count_nodes(generalized_list);
printf("深度: %d\n", depth);
printf("节点数量: %d\n", count);
// 删除节点
char data_to_delete = 'b';
printf("删除节点 '%c'\n", data_to_delete);
delete_atom(&generalized_list, data_to_delete);
// 打印更新后的Generalized List
printf("更新后的Generalized List: ");
print_generalized_list(generalized_list);
printf("\n");
// 释放Generalized List内存
free_generalized_list(generalized_list);
return 0;
}
- hash
#include <stdio.h>
#include <stdlib.h>
typedef struct HashNode
{
int key;
int value;
struct HashNode *next; // 处理冲突的链表
} HashNode, *LPHASHNODE;
typedef struct
{
LPHASHNODE *buckets; // 指向数组的指针
int size; // 哈希表的大小
int count; // 当前元素数量
int (*default_hash)(int, int); // 当前使用的哈希函数
} HashTalbe, *LPHASHTABLE;
int default_hash(int key, int size)
{
return key & (size - 1); // 位运算
}
int unversal_hash(int key, int size)
{
int a = 1 + rand() % (size - 1);
int b = rand() % size;
return ((a * key + b) % size);
}
void check_ptr(void *ptr, const char *errMsg)
{
if (ptr == NULL)
{
fprintf(stderr, "%s\n", errMsg);
exit(EXIT_FAILURE); // 退出程序
}
}
LPHASHNODE create_node(int key, int value)
{
LPHASHNODE newNode = (LPHASHNODE)malloc(sizeof(HashNode));
check_ptr(newNode, "Memory allocation failed for new hash node "); // 检查内存分配
newNode->key = key;
newNode->value = value;
newNode->next = NULL;
return newNode;
}
LPHASHTABLE create_table(int size)
{
// 使用 malloc 分配足够存储一个 HashTable 结构体的内存。sizeof(HashTable) 计算出该结构体的字节大小。分配的内存地址被强制转换为 LPHASHTABLE 类型,并赋值给指针变量 table
LPHASHTABLE table = (LPHASHTABLE)malloc(sizeof(HashTalbe));
// 空指针检查
check_ptr(table, "Memory allocation failed for hash table");
// 设置大小:将传入的 size 值赋给哈希表结构中的 size 字段。这一行设置了哈希表的桶(buckets)的数量。
table->size = size;
// 将 count 字段初始化为 0,表示当前哈希表中没有存储的元素。这是用于跟踪哈希表中元素数量的字段。
table->count = 0;
// calloc申请数组指针,大小是size,其中每个元素的大小是sizeof(LPHASHNODE)calloc 会将分配的内存初始化为零,这样每个桶指针初始为 NULL。buckets 字段指向这个数组,准备存储哈希节点的指针。
table->buckets = (LPHASHNODE *)calloc(size, sizeof(LPHASHNODE));
check_ptr(table->buckets, "Memory allocation failed for hash table buckets");
return table;
}
void insert(LPHASHTABLE table, int key, int value)
{
// 计算哈希值并取模,得到桶的索引
int index = default_hash(key) % table->size;
LPHASHNODE newNode = create_node(key, value);
// 插入到对应索引位置的链表头部
newNode->next = table->buckets[index];
table->buckets[index] = newNode;
table++;
}
int search(LPHASHTABLE table, int key)
{
int index = default_hash(key, table->size);
LPHASHNODE current = table->buckets[index];
while (current != NULL)
{
if (current->key == key) // 发现已有相同key,更新value并返回
{
return current->value;
}
current = current->next;
}
return -1;
}
void delete(LPHASHTABLE table, int key)
{
int index = default_hash(key, table->size);
LPHASHNODE current = table->buckets[index];
LPHASHNODE prev = NULL;
while (current != NULL)
{
if (current->key == key)
{
if (prev == NULL)
{
table->buckets[index] = current->next;
}
else
{
prev->next = current->next;
}
free(current);
table->count--;
return;
}
prev = current;
current = current->next;
}
printf("Key not found for deletion.\n");
}
void printf_table(LPHASHTABLE table)
{
for (int i = 0; i < table->size; i++)
{
printf("Bucket %d: ", i);
LPHASHNODE current = table->buckets[i];
while (current != NULL)
{
printf("(%d, %d) -> ", current->key, current->value);
current = current->next;
}
printf("NULL\n");
}
}
void free_table(LPHASHTABLE table)
{
for (int i = 0; i < table->size; i++)
{
LPHASHNODE current = table->buckets[i];
while (current != NULL)
{
LPHASHNODE newNode = current->next;
free(current);
current = newNode;
}
}
free(table->buckets); // 释放桶数组的内存
free(table);
}
void table_test()
{
LPHASHTABLE table = create_table(65536); // 2^16
insert(table, 1, 100); // 插入元素
insert(table, 2, 200);
insert(table, 11, 300);
insert(table, 21, 400);
print_table(table); // 打印哈希表内容
printf("Search key 2: %d\n", search(table, 2)); // 查找键为2的元素
printf("Search key 11: %d\n", search(table, 11));
delete (table, 2); // 删除键为2的元素
printf("After deletion of key 2:\n");
print_table(table); // 打印哈希表内容
free_table(table); // 释放内存
}
c++
- 特性最终目的是简化频繁的操作
- 而不是为了特性而使用特性
- 避免无用的优化或者在彻底完成之前不要优化。(禁止艺术式,折腾内存造成资源浪费,c/c++默认的内存管理可以解决掉大多数场景)计算机没有你想象中的脆弱
- 1 包含头文件
//c
#include <stdio.h>
#include <string.h>
//cpp
#include<iostream>//c++标准库
#include<cstring>//包含c的标准库
#include<string>//c++的string
- c++中运行c : extern关键字:用于声明外部变量或函数。它告诉编译器,被声明的变量或函数是在其他文件中定义的,当前文件中只是引用了它而已。这样可以解决多文件编译链接时的符号重定义错误,同时也能够方便地实现模块化编程。
//外部变量声明
extern int x; // 声明一个外部变量x,x在其他文件中定义
//外部函数声明:
extern void foo(); // 声明一个外部函数foo,foo在其他文件中定义
//引用外部C语言函数:
extern "C"
{
#include <header.h> // 引用C语言的头文件
}
- 2 内存申请z
-
C语言中,堆区储存
- malloc:分配内存
- calloc:分配内存并初始化为0
- realloc:重新分配内存大小
- free:释放内存
-
C++ 增加了第五区: 自由储存区(也是在这里申请内存的) c++类的对象的内存不能使用malloc申请
- new:用于分配内存。比如int* p = new int;用于分配一个整数大小的内存。
- 申请单个变量内存
- 申请一段内存
- 申请内存可以手动初始化
- 申请内存后再分配
- delete:用于释放内存。比如delete p;用于释放p指向的内存。
- new[]:用于分配数组。比如int* arr = new int[10];用于分配10个整数大小的连续内存。
- delete[]:用于释放数组。比如delete[] arr;用于释放arr指向的数组内存。
- new:用于分配内存。比如int* p = new int;用于分配一个整数大小的内存。
-
动态内存分配
-
int* ptr = new int(0);
delete ptr;
//list
int* ptr = new int{ 5 };
//array _lsit
int* ptr = new int[5] { 1, 2, 3, 4, 5 };
int size=10;
int *arr=new int [size];
vector<int> vec(size,0);
delete[] arr;
int a = 1, b = 2, c = 3;
vector<int*> arr_ptr = {&a, &b, &c};
for (int i = 0; i < size; i++)
{
arr[i] += i * 2;
}
for (int i = 0; i < size; ++i)
{
std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
}
delete[] arr;
// 动态内存与异常处理
try
{
int* myArray = new int[100000000];
}
catch (std::bad_alloc& e)
{
std::cout << "Memory allocation failed: " << e.what() << '\n';
}
- 当c++一个完整的数据结构的链表的所有node的指针全部存入vector然而vector后续存在扩容操作会可能会更改内存
#include <iostream>
#include <vector>
#include <memory>
#include <cstdlib>
#include <ctime>
struct Node
{
int data;
std::shared_ptr<Node> next;
Node(int val):data(val),next(nullptr){}
};
void testVectorResize()
{
std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
std::shared_ptr<Node> node2 = std::make_shared<Node>(2);
std::shared_ptr<Node> node3 = std::make_shared<Node>(3);
node1->next = node2;
node2->next = node3;
std::vector<std::shared_ptr<Node>> nodePointers;
nodePointers.push_back(node1);
nodePointers.push_back(node2);
nodePointers.push_back(node3);
srand(static_cast<unsigned int>(time(nullptr)));
for (int i = 0; i < 3; i++)
{
int newSize = rand();
std::cout << "newSize " << i + 1 << ": " << newSize << std::endl;
nodePointers.reserve(newSize);
nodePointers.push_back(std::make_shared<Node>(i + 4));
}
// 打印 vector 中节点的数据
for (const auto& nodePtr : nodePointers)
{
std::cout << nodePtr->data << " ";
}
std::cout << std::endl;
}
默认 && 缺省 参数
- 默认参数必须从右向左连续设置,即默认参数只能出现在参数列表的最右边。
- 一旦为某个参数提供了默认值,则其后的所有参数都必须有默认值。换句话说,不能只为参数列表中间的某个参数提供默认值。
- 默认参数通常在函数声明或定义中给出,而不是在函数的实现部分。
void test(int num =1024){};
//当项目需要让这个函数预留一个参数位置可以缺
void test(int num2=2048,int*){};
//切勿使用不必要的重名,缺省的参数在以下这种情况就可以造成定义不明确,
void test(int num1){};
void test(int num1,int num2=0){};//调用函数时因默认而参数-1,和第一个调用形式一样无法识别
void test(int ){};
友元
class MyClass
{
private:
int data;
public:
MyClass(int d) : data(d) {}
friend void display(const MyClass &obj);
friend void display(int num);
};
void display(const MyClass &obj)
{
std::cout << "Data in MyClass object: " << obj.data << std::endl;
}
void display(int num) {
std::cout << "Number: " << num << std::endl;
}
int main()
{
MyClass obj(42);
display(obj); // 调用友元函数 display(const MyClass &obj)
display(123); // 调用友元函数 display(int num)
return 0;
}
重载
- 1 函数重载
- 函数名相同: 相同操作 参数不同 : 减少了不必要的命名,减少某次调用不必要的传参
- 2 运算符重载
- 当重载一个运算符时,实际上是在定义一个函数,其名称是 operator 加上想要重载的运算符符号。例如,如果你想重载加法运算符 +,你需要定义一个名为 operator+ 的函数。
- 结合template ,支持运算符连续运算例如a+b+c ,abc,a&&b&&c
#include <iostream>
template <typename T>
class ContinuousOperations
{
private:
T value;
public:
ContinuousOperations(const T& val) : value(val) {}
ContinuousOperations<T>& operator+(const T& other)
{
value += other;
return *this;
}
ContinuousOperations<T>& operator*(const T& other)
{
value *= other;
return *this;
}
ContinuousOperations<T>& operator&(const T& other)
{
value = value && other;
return *this;
}
// 重载输出运算符
friend std::ostream& operator<<(std::ostream& os, const ContinuousOperations<T>& obj)
{
os << obj.value;
return os;
}
};
int main()
{
ContinuousOperations<int> a(5), b(10), c(15);
// 加法链式运算
std::cout << (a + b + c) << std::endl; // 输出: 30
// 乘法链式运算
std::cout << (a * b * c) << std::endl; // 输出: 750
// 逻辑与操作(用函数而非运算符)
std::cout << std::boolalpha << a.logicalAnd(b).logicalAnd(c) << std::endl; // 输出: true (如果 a、b 和 c 的值都为非零)
return 0;
}
构造 析构
- 1 构造 : (1)提供默认值 (2)对象初始化:成员变量的赋值 (3)动态内存分配
#include <iostream>
#include <string>
class Person
{
private:
std::string name;
int age;
public:
// 默认构造函数,提供默认值
Person() : name("Unknown"), age(0) {
std::cout << "Default constructor called." << std::endl;
}
// 构造函数,初始化成员变量
Person(const std::string& name, int age) : name(name), age(age) {
std::cout << "Parameterized constructor called." << std::endl;
}
// 析构函数,用于释放动态分配的内存
~Person() {
std::cout << "Destructor called for " << name << std::endl;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
int main()
{
// 使用默认构造函数创建对象
Person person1;
person1.display();
// 使用参数化构造函数创建对象
Person person2("Alice", 30);
person2.display();
// 使用动态内存分配创建对象
Person* person3 = new Person("Bob", 25);
person3->display();
// 释放动态分配的内存
delete person3;
return 0;
}
继承
静态成员&&静态对象
- 附加全局状态->确认了全程的固定内存,在一定情况下节约创建释放内存操作
- 静态成员的用途:
- 全局游戏状态: 可以使用静态成员变量来保存全局游戏状态,如当前关卡、玩家得分、游戏难度等信息。
- 共享资源管理: 静态成员变量可以用于管理共享资源,如纹理、模型、音频等,确保这些资源在整个游戏生命周期内只加载一次。
- 全局事件处理: 可以使用静态成员变量来注册全局事件处理程序,处理游戏中的重要事件,如碰撞检测、输入处理等。
- 配置参数: 静态成员变量可以用于存储游戏的配置参数,如画面分辨率、音效音量、控制设置等,以便在整个游戏中使用。
- 静态对象的用途:
- 单例模式: 静态对象可以用于实现单例模式,确保在整个程序中只有一个实例。例如,可以使用静态对象来表示游戏管理器、音频管理器、日志记录器等。
- 全局事件分发器: 可以使用静态对象来实现全局事件分发器,用于在游戏中发送和接收事件,实现不同游戏对象之间的通信。
- 游戏设置: 静态对象可以用于存储游戏的设置信息,如画面设置、音效设置、控制设置等,以便在整个游戏中访问和修改。
#include <iostream>
class Singleton {
private:
static Singleton* instance; // 静态成员变量,用于保存单例实例
Singleton() {} // 私有构造函数,防止外部实例化
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
void showMessage() {
std::cout << "Hello, I am a Singleton!" << std::endl;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
class EventDispatcher {
private:
static EventDispatcher* instance; // 静态成员变量,用于保存单例实例
EventDispatcher() {} // 私有构造函数,防止外部实例化
public:
static EventDispatcher* getInstance() {
if (instance == nullptr) {
instance = new EventDispatcher();
}
return instance;
}
void dispatchEvent(const std::string& event) {
std::cout << "Event dispatched: " << event << std::endl;
// 在这里可以添加处理事件的逻辑
}
};
// 初始化静态成员变量
EventDispatcher* EventDispatcher::instance = nullptr;
int main() {
// 获取单例实例并调用方法
Singleton* singleton = Singleton::getInstance();
singleton->showMessage();
// 获取事件分发器实例并发送事件
EventDispatcher* dispatcher = EventDispatcher::getInstance();
dispatcher->dispatchEvent("PlayerDeath");
return 0;
}
- 更好的例子
#ifndef __RESOURCEMANAGER_H__
#define __RESOURCEMANAGER_H__
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <mutex>
#include <shared_mutex>
#include <optional>
using namespace std;
class ResourceManager
{
private:
static shared_ptr<ResourceManager> instance; // 使用 shared_ptr 管理实例,避免使用原始指针
static shared_mutex mtx;
// 资源:例如纹理、音效等
vector<string> textures;
vector<string> sounds;
ResourceManager()
{
std::cout << "ResourceManager created\n";
textures.push_back("plant_texture.png");
textures.push_back("zombie_texture.png");
sounds.push_back("background_music.mp3");
}
public:
// 懒加载单例获取方法
static shared_ptr<ResourceManager> getInstance()
{
{
// 加锁只保护实例初始化部分
unique_lock<shared_mutex> lock(mtx);
if (!instance)
{
instance = make_shared<ResourceManager>();
}
}
return instance;
}
void loadResources()
{
std::cout << "Loading resources...\n";
// 模拟加载资源
}
void releaseResources()
{
std::cout << "Releasing resources...\n";
textures.clear();
sounds.clear();
}
void printResources()
{
std::cout << "Textures:\n";
for (const auto &texture : textures)
{
std::cout << "- " << texture << "\n";
}
std::cout << "Sounds:\n";
for (const auto &sound : sounds)
{
std::cout << "- " << sound << "\n";
}
}
// 获取资源列表
optional<vector<string>> getTextures() {
return textures.empty() ? std::nullopt : std::make_optional(textures);
}
};
shared_ptr<ResourceManager> ResourceManager::instance = nullptr;
shared_mutex ResourceManager::mtx;
#endif //__RESOURCEMANAGER_H__
template
- 诞生场景:不同类型拥有共同大量重复的操作
//算法部分中 排序可能存在需要中间变量 如果同类型的方法大量使用就需要定义一个函数
void swap(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//但是如果需要交换其他数据类型怎么办 void Swap(float *a, float *b)
//如果是两个类型可以再写一次,现在你需要让这个算法兼容所有数据类型,例如long,short,char,double 这个时候诞生了一个数据类型的通用类型
template<typename T>
//方法1简单 对于基本数据类型来说,效率高,且不需要额外的模板参数推导。缺点:对于大型对象,需要进行完整的拷贝,可能会产生额外的开销,特别是在拷贝构造函数和析构函数执行复杂操作的情况下。
/*
void Swap(T *a,T *b)
{
T temp=*a;
*a=*b;
*b=temp;
}
*/
//移动可以减少拷贝,适用于各种数据类型,包括自定义类型和标准库类型,且在交换大型对象时可以提高性能。
void swap(T& a, T& b) {
T temp = std::move(a); // 使用 std::move 将 a 转移给临时变量 temp
a = std::move(b); // 使用 std::move 将 b 转移给 a
b = std::move(temp); // 使用 std::move 将 temp 转移给 b
}
{
int a=10,b=20;
double c=10,d=20;
Swap(&a,&b);
Swap(&c,&d);
}
-
注:增长编译时间:当模板被实例化时,编译器需要生成相应的代码。尽可能地使用前置声明来减少头文件的依赖关系,实现细节放在cpp中进行编译,避免.h中使用。
-
c++标准已经存在swap,只能交换两个相同类型,包含
-
模板特化
- 以一个简单加法为例,实现计算器逻辑
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {return a + b;}
// 特化版本,处理 char 类型
template<>
auto add<char, char>(char a, char b) -> std::enable_if_t<std::is_same_v<char, decltype(a + b)>, decltype(a + b)> {return a + b;}
// 特化版本,处理 char 类型和 const char* 类型
template<>
auto add<char, const char*>(char a, const char* b) -> std::enable_if_t<std::is_same_v<std::string, decltype(std::string(a) + b)>, std::string> {return std::string(a) + b;}
// 特化版本,处理 const char* 类型和 char 类型
template<>
auto add<const char*, char>(const char* a, char b) -> std::enable_if_t<std::is_same_v<std::string, decltype(std::string(a) + b)>, std::string> {return std::string(a) + b;}
- 适用场景分析
- 按照刚才的思路完全可以写一个add构造函数,写法相同而匹配逻辑更简单,同时存在时优先匹配更简单的构造函数
auto add<char, char>(char a, char b) -> std::enable_if_t<std::is_same_v<char, decltype(a + b)>, decltype(a + b)> {return a + b;}
- 模板类(math通用向量)
#include <iostream>
#include <vector>
template <typename T>
class MyVector
{
public:
std::vector<T> data;
MyVector(std::initializer_list<T> values) : data(values) {}
MyVector operator+(T value) const
{
MyVector result = *this;
for (auto &d : result.data) {d += value; }
return result;
}
MyVector operator*(T value) const
{
MyVector result = *this;
for (auto &d : result.data) {d *= value; }
return result;
}
void print() const
{
std::cout << "[ ";
for (const auto &d : data) {std::cout << d << " ";}
std::cout << "]" << std::endl;
}
};
int main()
{
//模板类调用创建时需要指定类型
MyVector<int> vec({1, 2, 3, 4, 5});
std::cout << "Original vector: ";
vec.print();
MyVector<int> vec_plus = vec + 10;
std::cout << "After adding 10: ";
vec_plus.print();
MyVector<int> vec_multiplied = vec * 2;
std::cout << "After multiplying by 2: ";
vec_multiplied.print();
return 0;
}
- 模拟手写通用顺序表
#include <iostream>
#include <vector>
#include <string>
#include <memory>
using namespace std;
template <typename T>
class Vector
{
private:
T *array{}; // 存储数组元素的指针,最初是空指针t
size_t v_size{}; // 当前数组中实际存储的元素数量
size_t capacity{}; // 数组的当前容量or分配的内存大小
void _grow(size_t size) // 内部函数,用于扩容数组
{
if (size <= capacity)
return;
T *ptr = new T[size];
if (!empty())
{
memcpy(ptr, array, v_size * sizeof(T));// 将旧数据拷贝到新内存
delete[] array;
}
array = ptr;
capacity = size;
static int i = 0;
std::cout << __FUNCTION__ << i++ << " capacity:" << capacity << std::endl; // 输出函数名和当前容量
}
public:
Vector() { _grow(15); }// 初始化时默认容量为15
//向数组尾部添加元素
void push_back(const T &&v)
{
if (v_size == capacity)
_grow(capacity * 2); //容量不足时扩大为原容量的两倍
array[v_size++] = v;
}
//改用右值移动语义
void push_back(T &&v)
{
if (v_size == capacity)
_grow(capacity * 2);
array[v_size++] = v;
}
const T &front() const { return array[0]; } // 获取数组首元素
const T &back() const { return array[v_size - 1]; } // 获取数组尾元素
T& operator[](size_t index) noexcept// 重载[]运算符,返回可修改的引用// noexcept标记该函数中不需要执行额外的异常处理如果在 noexcept 函数中抛出了异常,程序会立即调用 std::terminate(),终止程序的执行。
{
if (index < 0 || index >= v_size) // 下标越界检查
abort(); // 终止程序
return array[index]; // 返回对应下标的元素
}
const T& operator[](size_t index)const noexcept // 重载[]运算符,返回常量引用
{
if (index < 0 || index >= v_size) // 下标越界检查
abort(); // 终止程序
return array[index]; // 返回对应下标的元素
}
const T& at(size_t index)const // 返回常量引用,带下标越界检查
{
if (index < 0 || index >= v_size) // 下标越界检查
throw std::out_of_range("index " + std::to_string(index) + " out of range"); // 抛出越界异常
return array[index]; // 返回对应下标的元素
}
T& at(size_t index) // 返回可修改的引用,带下标越界检查
{
if (index < 0 || index >= v_size) // 下标越界检查
throw std::out_of_range("index " + std::to_string(index) + " out of range"); // 抛出越界异常
return array[index]; // 返回对应下标的元素
}
void resize(size_t size) // 调整数组大小
{
if (size <= capacity) // 如果请求的大小小于等于当前容量,直接返回
return;
_grow(size); // 否则扩容
v_size = size; // 更新数组大小
}
const T* data()const { return array; } // 返回数组首地址
T* data() { return array; } // 返回数组首地址(可修改版本)
size_t size()const { return v_size; } // 返回数组大小
size_t v_capacity()const { return capacity; } // 返回数组容量
bool empty()const { return v_size == 0; } // 判断数组是否为空
};
- 可变参数模板
inline (优化buff)
- (建议(只是一个请求)编译器对函数进行内联展开,以减少函数调用的开销,可提高程序的执行效率)
- buff触发条件:
- 1 频繁调用函数 eg:循环中调用函数
- 2 避免内联复杂函数 eg内部包含循环、递归、复杂条件判断等复杂操作
- 3 多文件操作需要 inline函数定义放在头文件中
- 4 编译器的决定权且项目完成后优化:项目中这一部分彻底完成之后再使用linux操作,编译器最终决定如何运行,故而需要前后对比是否需要优化。
- eg 衔接template
template<typename T>
inline void Swap(T *a,T *b)
{
T temp=*a;
*a=*b;
*b=temp;
}
while(10000)
{
//多次循环可提高程序的执行效率
}
类型转化
#include <iostream>
class Base
{
public:
virtual void print() const
{
std::cout << "Base\n";
}
};
class Derived : public Base
{
public:
void print() const override
{
std::cout << "Derived\n";
}
};
- static_cast:
//编译时类型转换
//1 常用于显式的类型转换。也可隐式,父子
//2不失去const属性的情况下进行基本类型的转换,以及在继承关系中进行指针和引用类型的转换。
//3不能进行动态类型检查,需要自行确保类型安全。
void staticCastExample()
{
// 基本类型转换
int x = 10;
double y = static_cast<double>(x);
// const 转换
const int z = 20;
int w = static_cast<int>(z);
// 继承关系转换
Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Base*>(basePtr);
derivedPtr->print(); // 输出: Derived
}
- const_cast:
//指针或引用类型中移除const属性或将其添加。
void constCastExample()
{
const int x = 10;
int* y = const_cast<int*>(&x);
*y = 20;
std::cout << "x: " << x << ", *y: " << *y << std::endl; // 输出: x: 10, *y: 20
}
- reinterpret_cast:
//1指针或引用转换为不同类型的指针或引用
//2 整型转换为指针类型
//3 指针类型转换为整类:这是低级操作,需要附带重新的语义,不同操作系统的整型字节可能不同,需要使用准确类型(uint_8,int_32)(reinterpret_cast强大,危险,可绕过编译器的类型检查。)
void reinterpretCastExample()
{
// 1. 指针或引用转换为不同类型的指针或引用
int value = 65;
char* ptr = reinterpret_cast<char*>(&value);
std::cout << *ptr << std::endl; // 输出: A
// 2. 整型转换为指针类型
int* intValue = reinterpret_cast<int*>(0x12345678);
std::cout << "Integer Value: " << *intValue << std::endl; // 输出: Integer Value: 305419896
// 3. 指针类型转换为整型
uintptr_t intValue2 = reinterpret_cast<uintptr_t>(ptr);
std::cout << "Pointer Value: " << intValue2 << std::endl; // 输出: Pointer Value: 140721106775400
}
- dynamic_cast:
//(安全)继承(父子)的动态类型转换。只用于指针或引用类型(如果转换不安全dynamic_cast将返回nullptr(对于指针类型)或抛出std::bad_cast异常(对于引用类型)。)
void dynamicCastExample()
{
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Base*>(basePtr);
if (derivedPtr)
derivedPtr->print(); // 输出: Derived
else
std::cout << "Dynamic cast failed\n";
delete basePtr;
}
STL
1 序列容器:存储元素的序列,允许双向遍历。
std::vector:动态数组,支持快速随机访问。
std::deque:双端队列,支持快速插入和删除。
std::list:链表,支持快速插入和删除,但不支持随机访问。
2、关联容器:存储键值对,每个元素都有一个键(key)和一个值(value),并且通过键来组织元素。
std::set:集合,不允许重复元素。
std::multiset:多重集合,允许多个元素具有相同的键。
std::map:映射,每个键映射到一个值。
std::multimap:多重映射,允许多个键映射到相同的值。
3、无序容器(C++11 引入):哈希表,支持快速的查找、插入和删除。
std::unordered_set:无序集合。
std::unordered_multiset:无序多重集合。
std::unordered_map:无序映射。
std::unordered_multimap:无序多重映射。
- lower_bound 返回的是第一个大于或等于给定值的元素
- upper_bound 返回的是第一个严格大于给定值的元素
- 可以通过组合使用 lower_bound 和 upper_bound 来确定容器中某个值的范围,
- [lower_bound, upper_bound)
STL函数适配器
//c++20之后没有unary_function和binary_function
STL 关联式容器
- map
C++11
C++11 chrono
- 新增的日期和时间库 提供了一组类型和函数,用于表示和操作时间间隔、时间点和时钟。引入了四个概念
- 1 精度和可移植性 std::chrono 提供了更高的精度,不同平台实现一样。
- 2 单位 clock_t 通常以时钟滴答数(clock ticks)为单位表示时间间隔,不同系统实现不同大小定义不同。
#include <iostream>
#include <chrono>
int main() {
// 时间点(time point)
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
std::cout << "Current time: " << std::chrono::system_clock::to_time_t(now) << std::endl;
// 时间间隔(duration)
std::chrono::milliseconds duration(2000);
std::cout << "Duration: " << duration.count() << " milliseconds" << std::endl;
// 时钟(clock)
typedef std::chrono::steady_clock MyClock;
MyClock::time_point start = MyClock::now();
// 执行需要计时的代码
MyClock::time_point end = MyClock::now();
MyClock::duration elapsed = end - start;
std::cout << "Elapsed time: " << std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() << " milliseconds" << std::endl;
// 时钟类型(clock type)
std::cout << "Is steady clock: " << std::chrono::steady_clock::is_steady << std::endl;
return 0;
}
C++11 右值引用
https://zhuanlan.zhihu.com/p/335994370
https://zhuanlan.zhihu.com/p/107445960
- std::move只是类型转换工具
- 引用是一个对象的别名,引用修改变量的值时,在修改变量本身,当函数参数为引用时,实际上传递的是对原始数据的直接访问权,而不是数据的副本,这样就避免了数据的拷贝。
#include<iostream>
#include<utility>//std::move
class Example
{
public:
Example() { std::cout << "构造\n"; }
~Example() { std::cout << "析构\n"; }
Example(const Example&) { std::cout << "拷贝构造\n"; }
Example(Example&&) { std::cout << "移动构造\n"; }
Example& operator=(Example&&)
{
std::cout << "移动赋值\n";
return *this;
}
void use() const { std::cout << "使用\n"; }
};
Example createExample()
{
Example e;
return e;
}
int main()
{
Example e = createExample();
e.use(); // 在这里使用e是安全的。
// 使用std::move(e)将e标记为可移动,之后e将进入一个有效但未指定的状态。
Example moved = std::move(e);
// 注意:在此之后尝试使用e将是不安全的,因为e的状态是未定义的。
// e.use(); // 不安全:使用e将导致未定义行为。
// 可以安全地给e赋予新的值,这使得e重新进入一个确定的状态。
e = createExample();
e.use(); // 现在再次使用e是安全的。
return 0;
}
- 左值引用:能指向左值,不能指向右值,但是,const左值引用是可以指向右值
- 右值引用能指向右值,本质上也是把右值提升为一个左值
int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败
const int &ref_a = 5; // 编译通过
int &&ref_a = 5;
ref_a = 6;
等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
// 形参是个右值引用
void change(int&& right_value)
{
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}
- std::move和移动语义:通过std::move(e),我们将e转换为右值引用,从而允许将其移动构造到新对象moved中。e处于一个有效但未指定的状态,这意味着其值不再可靠,尽管对象本身仍然存在,直到离开作用域被析构。
- 补:move单词理解:v.(使)改变位置,(使)移动;搬家,搬迁;(使)变换,调动;(使)改变观点(或做法);前进,进展;采取行动;使感动,打动;促使,驱使;更改(时间或日期);改变话题;走棋,移动棋子;n.措施,行动;移动,活动;转变,改变;一步棋,走棋;搬家,迁移
在这里我们不妨猜测中文翻译出现了问题,因为move并非移动,而是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)。单纯move不会提升性能,这里我建议大家翻译成:(使)变换或者搬家
- 补:move单词理解:v.(使)改变位置,(使)移动;搬家,搬迁;(使)变换,调动;(使)改变观点(或做法);前进,进展;采取行动;使感动,打动;促使,驱使;更改(时间或日期);改变话题;走棋,移动棋子;n.措施,行动;移动,活动;转变,改变;一步棋,走棋;搬家,迁移
c++11 decltype关键字
int x = 10;
// 在不求值上下文中使用decltype获取变量x的类型
decltype(x) y; // y的类型为int,但不会实际创建一个实例
static_assert(std::is_same<decltype(y), int>::value, "Error: y is not of type int");
内存泄漏检查
c++智能指针
- unique_ptr (c++11)
#include <memory>
#include <iostream>
int main()
{
std::unique_ptr<int> p1(new int(5));
std::cout << *p1 << std::endl; // 输出 5
// 不需要手动释放内存
return 0;
}
- 应用场景:在需要确保资源独占所有权的情况下使用,例如管理文件句柄或数据库连接。作为容器中的元素,以确保元素的所有权唯一。
- 不可使用的场景:不可用于需要多个指针共享同一块内存的情况。不能进行拷贝或赋值操作,因为它会导致资源所有权的转移。
- make_unique(c++14)
void test_make()
{
{
unique_ptr<int> ptr_int(new int(32));
*ptr_int = 520;
cout << *ptr_int << endl;
unique_ptr<char[]> ptr_char(new char[50] {"hello world" });
strncpy(ptr_char.get(), "heiheihelloe", 30);
cout << ptr_char << endl;
}
auto p1 = make_unique<int>();
auto parr = make_unique<double[]>(10);
for (int i = 0; i < 10; i++)
{
parr[i] = i;
cout << parr[i];
}
}
- 用于创建 unique_ptr 对象并初始化,节约new,减少重复代码,接受构造函数参数,返回一个初始化的 unique_ptr 对象
- shared_ptr:
#include <memory>
#include <iostream>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1;
std::cout << *p1 << " " << *p2 << std::endl; // 输出 10 10
return 0;
}
-
应用场景:当需要多个指针共享相同的资源时使用,如在多线程环境下共享数据。在循环引用的情况下,可以通过 weak_ptr 来解决。
-
不可使用的场景:被频繁创建和销毁的对象,因为 shared_ptr 的引用计数会带来额外的开销。不适合在性能要求极高的场景中使用。
-
weak_ptr:
#include <memory>
#include <iostream>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(15);
std::weak_ptr<int> wp = p1;
if (auto locked = wp.lock())
std::cout << *locked << std::endl; // 输出 15
else
std::cout << "资源已释放" << std::endl;
return 0;
}
-
应用场景:用于打破 shared_ptr 的循环引用,防止内存泄漏。作为 shared_ptr 的临时观察者,检查资源是否存在。
-
不可使用的场景:不能直接访问资源,需要先通过 lock() 转换成 shared_ptr。
-
删除器
- 在某些时候,默认的释放操作不能满足咱们的需要,这个时候就需要自定义删除器。
void del_global(Test* ptr){delete ptr;} //全局函数
auto del_lambda = [](Test* ptr) {delete ptr; }; //lambda
std::function<void(Test*)> del_function = del_lambda; //函数包装器
struct Del_Object //仿函数
{
void operator()(Test* ptr)
{
delete ptr;
}
};
int main()
{
std::unique_ptr<Test, decltype(del_global)*> p2(new Test, del_global);
std::unique_ptr<Test, decltype(del_lambda)> p3(new Test, del_lambda);
std::unique_ptr<Test, decltype(del_function)> p4(new Test, del_function);
std::unique_ptr<Test, decltype(Del_Object())> p5(new Test, Del_Object());
return 0;
}
auto关键字
- auto 可以让编译器根据初始化表达式的类型来确定变量的类型,从而减少代码中的类型重复和冗长
//1 类型复杂或难以记忆
std::map<std::string, std::vector<int>> myMap;
// 使用auto简化变量类型的声明
auto it = myMap.begin(); // it的类型为std::map<std::string, std::vector<int>>::iterator
//2 迭代器类型推断 简化类型声明
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用auto推断迭代器类型
for (auto it = vec.begin(); it != vec.end(); ++it)
{
std::cout << *it << " ";
}
// 3 模板编程 :难以事先确定类型
template<typename T,typename U>
auto add(T t,U u)-> decltype(t + u){ return t+u; }
// 4 复杂类型的简化
std::unique_ptr<std::map<std::string, std::vector<int>>> ptr(new std::map<std::string, std::vector<int>>());
// 使用auto简化变量类型的声明
auto ptr = std::make_unique<std::map<std::string, std::vector<int>>>();
–>
type_index
- 运行时处理类型信息
#include <typeindex>
//确定对象的实际类型
#include <iostream>
#include <typeinfo>
#include <memory>
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {};
int main() {
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// 使用 typeid 和 std::type_index 获取运行时类型
std::type_index ti = std::type_index(typeid(*ptr));
if (ti == std::type_index(typeid(Derived))) {
std::cout << "ptr is pointing to a Derived object." << std::endl;
} else {
std::cout << "ptr is not pointing to a Derived object." << std::endl;
}
return 0;
}
c++17
强制消除拷贝
–>
c++20
consteval关键字
- consteval关键字在C++中的主要用途是强制函数在编译时求值,并产生常量表达式。它的引入使得编译器能够在编译期间进行更多的优化和静态分析,进而提高程序的性能和可靠性。[cpp20]
- 1 常量表达式计算:通过将函数声明为consteval,可以确保函数在编译时被求值,得到常量表达式结果。这对于需要在编译期间进行计算的常量表达式非常有用,例如数学运算、字符串操作等。
consteval int square(int x) { return x * x; } 编译时错误检查:将函数声明为consteval可以帮助在编译期间捕获一些错误,例如类型不匹配、越界访问等。通过在编译时进行静态分析,可以尽早发现潜在的问题,从而减少运行时错误。 constexpr int result = square(5); // 在编译时求值,result将成为常量 static_assert(result == 25);
- 2 编译时错误检查:将函数声明为consteval可以帮助在编译期间捕获一些错误,例如类型不匹配、越界访问等。通过在编译时进行静态分析,可以尽早发现潜在的问题,从而减少运行时错误。
consteval int divide(int x, int y) { if (y == 0) { static_assert(false, "Divide by zero"); // 在编译时抛出错误 } return x / y; } constexpr int result = divide(10, 0); // 编译时错误,除以零
- 3 优化编译器生成的代码:consteval函数要求在编译时产生常量表达式,这使得编译器能够进行更多的优化。通过帮助编译器减少运行时的计算和判断,可以提高程序的性能和效率。
lambda
-
C++11:
- Lambda表达式的基本语法被引入,允许使用
[capture](parameters) -> return_type { body }
的形式定义匿名函数。 - 捕获列表中可以使用值传递和引用传递捕获外部变量。
- Lambda表达式可以作为函数对象传递给标准算法或其他函数。
- Lambda表达式内部可以访问外部变量,但是默认情况下不能修改外部变量的值。
- Lambda表达式的基本语法被引入,允许使用
-
C++14:
- 允许省略捕获列表中的捕获符号
&
和=
,使得捕获列表更加简洁,如[=]
和[&]
。 - 允许在Lambda表达式中使用泛型的
auto
参数,使得Lambda表达式更加灵活。
- 允许省略捕获列表中的捕获符号
-
C++17:
- 允许在Lambda表达式的参数列表中使用初始化语句,如
int x = 10
。 - 允许在Lambda表达式的参数列表中使用折叠表达式。
- 允许在Lambda表达式中使用
constexpr
修饰符声明常量表达式。 - 允许在Lambda表达式中使用
if constexpr
语句,实现编译时条件分支。
- 允许在Lambda表达式的参数列表中使用初始化语句,如
-
C++20:
- 允许在Lambda表达式中使用
constexpr
修饰符声明函数对象为常量表达式。 - 允许在Lambda表达式中使用
constexpr
修饰符声明Lambda表达式本身为常量表达式。 - 允许在Lambda表达式中使用
template
参数,支持模板化Lambda表达式。 - 引入了
[=, this]
捕获列表,允许以值传递方式捕获当前对象的指针。
- 允许在Lambda表达式中使用
设计模式
- change
临时使用:
对于短期使用或者小规模的任务,单纯的函数可能是最合适的选择。这种情况下,不需要引入复杂的设计模式,简单的函数能够满足需求,并且易于理解和维护。
短期使用:
如果任务规模稍大,但是时间较短,可能会选择一些轻量级的设计模式,如策略模式、工厂模式等。这些模式能够提高代码的可扩展性和可维护性,但不会引入过多的复杂性。
中期使用:
对于中等规模的项目,面向对象的设计模式通常更合适,如单例模式、观察者模式、装饰器模式等。这些模式能够更好地组织代码结构,提高代码的重用性和可维护性。
长期或永久使用:
长期或永久使用的项目需要更加健壮和可扩展的架构。这种情况下,通常会采用更多的设计模式,如MVC模式、MVVM模式、领域驱动设计(DDD)等。这些模式能够更好地分离关注点、降低耦合度,并且适应长期的变化和需求扩展。
ECS适合长期或永久使用的项目,尤其是对性能要求较高、需要处理大量实体和组件的应用。它能够有效地管理复杂的游戏对象,提高代码的可维护性和可扩展性。但是,ECS并不是适合所有类型的项目,它更适合那些需要处理大量实体和组件的场景,对于小规模项目或者简单的应用,引入ECS可能会增加不必要的复杂性。
ECS开发
小故事:LOLbug之王-佛耶戈
- bug_list
1. 破败之王佛耶戈在击杀敌人之后,在拾取变身成为敌人的过程之中,敌人可以卖掉装备导致佛耶戈变身之后无任何装备可用,在被敌人队友发现之后极其容易阵亡!
2. 破败之王佛耶戈在击杀敌人之后,变身成为该英雄之后佛耶戈可以快速回城卖掉自己原有的装备且不可恢复!
3. 破败之王佛耶戈在击杀祖安狂人蒙多之后,变身成为蒙多,即使变身时间结束,佛耶戈也可以一直保持蒙多二技能释放时的特效,国服玩家戏称为获得了无线魂环!
4. 破败之王佛耶戈在中路一塔阵亡之后,在该地区永久无法隐身!
5. 用Q攻击敌方英雄或敌方小兵或野怪 在夺取敌方英雄控制权时(右键幽魂)用鼠标右键被Q攻击的英雄 这时破败之王的平A会变成某个技能(目前已知日女E,沙皇被动,酒桶R,盲僧R,薇恩W)
6. 每击杀变身成为大嘴,就会永久获得+Q被动攻速!
7. 即将死亡时放E技能,该技能的隐身区域会永久存在(再次释放E技能会刷新)!
8. 变成成为敌方英雄会清空杀人书,补满药水!
9. 击杀地方挖掘机,只要不钻出地面就会一直显示遁地状态!
10. 如果在变身成为蛇女的过程中被击杀,那么模型将永远显示石头!
11. 变身成为卡沙时,每次变身都将重新读条!
12. 占据妖姬时,使用QWE技能将无法使用R技能!
-
bug出现的原因
- 1动态调用不同英雄技能时,多线程中非常容易出现bug
- 条件优先级判断,如果没有u区分两个技能的优先而是同时,那么当他们的效果冲突时可能内存出现问题
- 存在继承和持续效果的问题,导致在控制时间结束后仍然会出现异常状态(尽管回收了对象)
-
构想解决方案
- 思考:佛耶戈技能的挑战
- 佛耶戈的被动技能允许他在击杀敌方英雄后,暂时控制被击杀英雄并使用他们的技能。从技术角度看,这要求系统能够在运行时动态地调用不同英雄的技能。
- 多态性和重载:游戏中的英雄技能可能需要通过多态性来实现,使得不同英雄的相同技能(如Q技能)可以有不同的实现。同时,考虑到技能的不同效果(如加强版),可能还需要重载技能方法以适应不同情况。
- 构造和状态管理:当佛耶戈控制另一个英雄时,需要正确地构造那个英雄的实例,并管理其状态(如技能冷却、当前生命值等),这可能需要复杂的状态管理和错误处理机制。
- 代码复用与依赖管理:为了避免重复代码,可能会倾向于使用继承或组合来复用英雄技能的实现。然而,这也可能导致复杂的依赖关系和难以预料的交互效果,特别是在一个拥有上百个英雄和技能的游戏中。
- 佛耶戈的被动技能允许他在击杀敌方英雄后,暂时控制被击杀英雄并使用他们的技能。从技术角度看,这要求系统能够在运行时动态地调用不同英雄的技能。
- 软件工程的实践
- 使用设计模式,如命令模式(Command Pattern)来封装执行操作的请求,或者访问者模式(Visitor Pattern)来在不修改现有类的情况下增加新的操作。
- 实施严格的测试,包括单元测试、集成测试和端到端测试,以确保新引入的功能不会破坏现有的系统。
- 模块化和解耦,通过设计清晰的接口和抽象层,减少不同系统组件之间的直接依赖,提高系统的灵活性和可维护性。
- 思考:佛耶戈技能的挑战
-
面向对象编程(OOP)的适用性(这里涉及到之前面向对象的严重缺点问题)
- 面向对象编程是一种广泛使用的编程范式,它通过封装、继承和多态等概念来提高代码的重用性、灵活性和可维护性。然而,OOP并非万能的,它也有可能在某些情况下导致过度复杂化或性能问题。对于《英雄联盟》这样的实时、多玩家在线游戏,性能优化、代码的可维护性和可扩展性尤其重要。然而随着多年的不断更新,继承沉余严重,会造成更严重甚至无法修复的问题
-
函数式编程(FP)
利用FP处理游戏逻辑:对于游戏中的状态计算、AI决策树、路径查找等逻辑,可以采用函数式编程的方法。这些逻辑通常涉及到大量的数据处理和转换,而且更容易并行化处理,利用FP的不可变性和纯函数特性可以提高这部分代码的清晰度和效率。
使用OOP管理游戏状态和实体:对于游戏中的角色、物品、环境等实体的管理,可以采用面向对象的方法。这些实体通常具有复杂的状态和行为,利用OOP的封装和继承特性可以有效地组织和管理这些复杂性。
模块化设计:将系统分解为可以独立开发和测试的模块,每个模块根据其特性选择最适合的编程范式。例如,游戏引擎底层可能更适合面向对象的方法,而游戏逻辑处理可能更适合函数式编程。
组件系统与实体组件系统(ECS):在游戏开发中,ECS是一种常用的架构模式,它将实体(Entity)、组件(Component)和系统(System)分离开来。实体是游戏中的任何对象,组件定义了实体的属性,系统则负责处理具有特定组件的实体。ECS模式倾向于数据驱动和组合优于继承的原则,这与函数式编程的理念相符,有助于提高游戏性能和可扩展性。
ECS基础概念
-
实体(Entity):
- 实体是游戏中的基本对象,可以是角色、物体或其他可操作对象。
- 每个实体都有一个唯一的标识符。
- 实体本身并不包含任何行为或数据,只是一种标识符。
-
组件(Component):
- 组件是实体的属性或特征,描述了实体的外观、行为或状态。
- 组件是可复用的,可以附加到多个实体上。
- 通常有位置组件、碰撞组件、渲染组件等。
-
系统(System):
- 系统处理实体和组件的逻辑。
- 每个系统处理一类特定的组件或实体。
- 系统通常是独立的,通过查询和操作实体和组件的数据来实现其功能。
-
管理器(Manager):
- 管理器用于管理实体和组件的创建、销毁和存储。
- 通常有实体管理器、组件管理器等。
-
游戏循环(Game Loop):
- 游戏循环是游戏的核心执行部分,包括输入处理、更新游戏状态和渲染画面等步骤。
- 在ECS中,游戏循环会依次调用各个系统来处理实体和组件。
-
实体-组件-系统的交互:
- 实体由组件构成,系统根据组件的存在与否来处理实体。
- 系统通过查询组件或实体来获取需要处理的对象。
- 系统通常会遍历所有符合条件的实体并执行相应的逻辑。
-
性能优化:
- 在处理大量实体和组件时,需要考虑性能优化的问题,如数据布局优化、批处理处理等。
-
回收机制:
- 回收机制用于释放不再使用的实体和组件,以减少内存占用和提高性能。
- 可以采用对象池等技术来管理实体和组件的回收和重用。
-
事件系统(Event System):
- 事件系统用于实现实体和组件之间的通信和交互。
- 可以用于实现触发器、消息传递等功能。
-
序列化与反序列化:
- 序列化是将实体和组件转换为可存储或传输的格式,反序列化是将其转换回原始格式。
- 用于保存和加载游戏场景、存档等功能。
-
插件化与扩展:
- 可以通过插件化和扩展机制来增加新的组件和系统,以实现更丰富的功能和游戏机制。
-
测试:
- 编写单元测试和集成测试来验证系统的正确性和稳定性。
#include <iostream>
#include <vector>
// 实体
struct Entity
{
int id;
};
// 组件
struct PositionComponent
{
float x, y;
};
struct RenderComponent
{
std::string sprite;
};
// 系统
struct RenderSystem
{
void render(const std::vector<Entity>& entities, const std::vector<RenderComponent>& renderComponents)
{
for (size_t i = 0; i < entities.size(); ++i)
{
std::cout << "Rendering entity with id " << entities[i].id << " and sprite " << renderComponents[i].sprite << std::endl;
}
}
};
// 管理器
struct EntityManager
{
std::vector<Entity> entities;
Entity createEntity()
{
static int nextId = 1;
Entity entity;
entity.id = nextId++;
entities.push_back(entity);
return entity;
}
void destroyEntity(Entity entity)
{
// 实现删除实体的逻辑
}
};
// 游戏循环
void gameLoop(EntityManager& entityManager, RenderSystem& renderSystem)
{
// 创建实体
Entity entity1 = entityManager.createEntity();
Entity entity2 = entityManager.createEntity();
// 添加组件
PositionComponent position1 = {10.0f, 20.0f};
RenderComponent render1 = {"sprite1.png"};
entityManager.addComponent(entity1, position1);
entityManager.addComponent(entity1, render1);
PositionComponent position2 = {30.0f, 40.0f};
RenderComponent render2 = {"sprite2.png"};
entityManager.addComponent(entity2, position2);
entityManager.addComponent(entity2, render2);
// 游戏循环
std::vector<Entity> entities = entityManager.getEntitiesWithComponent<RenderComponent>();
std::vector<RenderComponent> renderComponents = entityManager.getComponents<RenderComponent>();
renderSystem.render(entities, renderComponents);
}
int main()
{
EntityManager entityManager;
RenderSystem renderSystem;
gameLoop(entityManager, renderSystem);
return 0;
}
ECS开发游戏适配的算法
对象池(Object Pooling):
对象池是一种管理可重复使用对象实例的技术。在游戏中,炮灰小兵经常需要频繁创建和销毁,使用对象池可以避免频繁的内存分配和释放操作,从而提高性能并减少内存碎片化。
空间分区技术(Spatial Partitioning):
对于需要频繁进行空间查询的场景,如检测碰撞或寻找附近的实体,空间分区技术(如网格、四叉树或八叉树)可以有效地减少遍历的实体数量,提高查询效率。
碰撞检测优化:
对于需要进行大量碰撞检测的游戏,使用高效的碰撞检测算法如包围盒检测(AABB)、凸多边形检测(Convex Polygon)、甚至使用空间分区技术来减少检测的实体数量,以提高性能。
批处理和数据紧凑性:
ECS 架构本身就支持批处理操作,可以利用这一特性对大量实体进行高效地更新和处理。确保组件数据在内存中的布局紧凑,以提高缓存命中率和数据访问效率。
多线程处理:
如果游戏引擎支持多线程,可以考虑在系统执行阶段并行化处理多个实体的更新。这可以显著提高处理大量实体时的性能表现。
延迟更新和可视化剪裁:
对于不需要即时更新的实体,可以考虑延迟它们的更新,或者在视野之外的实体暂时停止更新,以减轻系统的负载和提高效率。
linux c-api
进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
// fork(): 用于创建一个新的进程,新进程是调用进程的副本。
void fork_example() {
pid_t pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(1);
} else if (pid == 0) {
printf("子进程:PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
} else {
printf("父进程:PID=%d, 子进程PID=%d\n", getpid(), pid);
}
}
// exec(): 用于在当前进程中执行新的程序,常见的函数有execvp()、execlp()等。
void exec_example() {
char *args[] = {"ls", "-l", NULL};
execvp(args[0], args);
perror("exec 失败");
exit(1);
}
// wait()和waitpid(): 用于父进程等待子进程的终止,并获取子进程的状态信息。
void wait_example() {
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出状态:%d\n", WEXITSTATUS(status));
}
}
// exit(): 用于终止当前进程。
void exit_example() {
printf("父进程终止\n");
exit(0);
}
// getpid()和getppid(): 分别用于获取当前进程的ID和父进程的ID。
void getpid_example() {
printf("当前进程:PID=%d, 父进程PID=%d\n", getpid(), getppid());
}
// kill(): 用于向指定进程发送信号。
void kill_example(pid_t pid) {
kill(pid, SIGUSR1);
}
// signal()和sigaction(): 用于处理信号,例如捕捉、忽略或者执行默认操作。
void sig_handler(int signo) {
if (signo == SIGUSR1) {
printf("接收到SIGUSR1信号\n");
}
}
void signal_example() {
if (signal(SIGUSR1, sig_handler) == SIG_ERR) {
perror("注册信号处理函数失败");
exit(1);
}
}
// pipe(): 用于创建管道,实现进程间通信。
void pipe_example() {
int fd[2];
if (pipe(fd) < 0) {
perror("创建管道失败");
exit(1);
}
printf("管道已创建,文件描述符:%d, %d\n", fd[0], fd[1]);
}
// dup()和dup2(): 用于复制文件描述符,通常用于重定向文件描述符。
void dup_example() {
int fd[2];
if (pipe(fd) < 0) {
perror("创建管道失败");
exit(1);
}
int new_fd = dup(fd[1]);
printf("新的文件描述符:%d\n", new_fd);
}
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork 失败");
exit(1);
} else if (pid == 0) {
// 子进程
fork_example();
exec_example();
exit_example();
} else {
// 父进程
wait_example();
getpid_example();
kill_example(pid);
signal_example();
pipe_example();
dup_example();
}
return 0;
}
c/c++ linux
- gcc g++ (c/c++ 在各种系统运行的基本环境)gdb (调试环境)
- 环境检查配置
gcc --version
g++ --version
which gcc
which g++
sudo apt-get install gdb
whitch gdb
- 创建项目目录
- 项目目录下->创建 .vscode ->下创建c_cpp_properties.json(这个配置文件指定了使用gcc和g++作为编译器,并设置了C和C++的标准。)
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "gnu++14",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
- .vscode下创建launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/<your_executable>",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
- .vscode目录下创建tasks.json(这个配置将会在vscode中添加一个任务,用于构建项目。)
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cmake --build .",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$gcc"
}
]
}
- cmake (介绍在上一个标题中)
sudo apt-get install cmake
- .vscode文件夹下创建一个CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(<your_project_name>)
set(CMAKE_CXX_STANDARD 11)
add_executable(<your_executable> <your_source_files>)
- SDL2
sudo apt-get install libsdl2-dev
- 在CMakeLists.txt中添加
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
target_link_libraries(<your_executable> ${SDL2_LIBRARIES})
- try(使用SDL2库创建一个窗口,并在窗口中显示一个红色的背景)
#include <iostream>
#include <SDL2/SDL.h>
using namespace std;
int main()
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
cout << "SDL_Init Error: " << SDL_GetError() << endl;
return 1;
}
SDL_Window *window = SDL_CreateWindow("Hello World!", 100, 100, 640, 480, SDL_WINDOW_SHOWN);
if (window == nullptr) {
cout << "SDL_CreateWindow Error: " << SDL_GetError() << endl;
SDL_Quit();
return 1;
}
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr) {
SDL_DestroyWindow(window);
cout << "SDL_CreateRenderer Error: " << SDL_GetError() << endl;
SDL_Quit();
return 1;
}
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
SDL_Delay(2000);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
数据传输
关闭写端时,读取端仍然可以读取已经在管道中的数据。这是因为关闭写端后,读取端仍然可以继续读取管道中的数据,直到所有的数据都被读取完毕,此时读取端的read()函数会返回0,表示已经读取到文件末尾(EOF)。关闭读端时,写入端仍然可以将数据写入管道,但是如果写入端尝试写入的数据超过了管道的容量,或者所有读取端都已经关闭,则写入操作会失败,通常会导致写入端接收到信号(SIGPIPE)或者write()函数返回-1并设置errno为EPIPE。
第三方库(third-party library)
RDKit(化学计算函数cpp)
https://github.com/rdkit/rdkit
Mannich反应是一种重要的有机化学反应,通常用于合成含有β-胺基酮结构的化合物。下面是Mannich反应的一般条件:
反应物:通常包括两个或更多的底物,其中包括一个醛或酮、一种胺和一个活性甲基化试剂(如甲醛)。
溶剂:常用的溶剂包括惰性溶剂(如二甲基甲酰胺、二甲基亚砜等)或极性溶剂(如乙腈、二氯甲烷等)。
温度:通常在室温下至中等温度(20-60摄氏度)进行。
催化剂:有时需要添加酸性或碱性催化剂来促进反应速率和选择性。
反应时间:通常需要几小时到几天的时间。
#include <iostream>
#include <RDGeneral/Invariant.h>
#include <GraphMol/SmilesParse/SmilesParse.h>
#include <GraphMol/ChemReactions/ReactionParser.h>
#include <GraphMol/ChemReactions/ReactionRunner.h>
#include <GraphMol/FileParsers/FileParsers.h>
#include <GraphMol/FileParsers/MolSupplier.h>
#include <GraphMol/FileParsers/MolWriter.h>
using namespace RDKit;
int main() {
// 创建反应物的分子对象
ROMol* reactant1 = SmilesToMol("NC(N)=S");
ROMol* reactant2 = SmilesToMol("O=Cc1ccccc1");
// 创建反应对象
ChemicalReaction reaction;
reaction.addReactantTemplate(reactant1);
reaction.addReactantTemplate(reactant2);
// 加载反应条件(水加热)
std::string reactionSmarts = ">[H]O>[O][H]";
RxnSmartsToChemicalReaction(reactionSmarts, reaction);
// 运行反应
ReactionRunner runner;
std::vector<MOL_SPTR> products = runner.runReaction(reaction);
// 输出产物SMILES字符串
for (const auto& product : products) {
std::string productSmiles = MolToSmiles(*product);
std::cout << "Possible product: " << productSmiles << std::endl;
}
return 0;
}
vscode
- 重新启动窗口
#Ctrl + Shift + P 打开命令面板
Reload Window #重新启动编辑器
- 智能选择gcc版本
#linux终端 安装CMake Tools扩展 完成之后确认 vscode拓展CMake Tools是否安装
code --install-extension ms-vscode.cmake-tools
#Ctrl + Shift + P 打开命令面板
## 选择你想要的GCC版本对应的kit
CMake: Select a kit
## 自定义GCC版本
CMake: Edit User-Local CMake Kits
#重新启动VSCode,CMake将会根据你的选择来调整GCC版本。
Reload Window
-
.vscode (配置文件) (特定的编译器会支持json使用//进行注释)
- tasks.json: 定义任务和构建过程,例如编译器选项、任务运行命令等
- settings.json:项目的全局设置,包括编辑器行为、插件设置和文件关联等。快捷打开: 在用户设置中,通过 File > Preferences > Settings
- launch.json: 配置调试器的启动方式和调试会话参数,包括断点设置、环境变量等。
- extensions.json: 定义推荐的扩展列表和建议的配置
- keybindings.json: 自定义的键盘快捷键映射。
- jsconfig.json / tsconfig.json : 配置 JavaScript / TypeScript 项目的语法检查、路径映射和编译选项。
位置: 在项目根目录下的 jsconfig.json 或 tsconfig.json 中配置。 - c_cpp_properties.json:配置 C/C++ 项目的编译器路径、标准库路径和头文件路径。
- gitignore: Git 版本控制系统忽略的文件和文件夹。位置: 在项目根目录下的 。gitignore 中配置。
- npmrc: 配置 npm 包管理器的参数,例如代理设置和存储库地址。在项目根目录下的 .npmrc 中配置。
- babel.config.json :作用: 配置 Babel 转换器的转换规则和插件。位置: 在项目根目录下的 babel.config.json 中配置。
-
settings.json
{
"explorer.confirmDelete": false,//禁止在资源管理器中删除文件或文件夹时的确认提示。
"C_Cpp.default.compilerPath": "/usr/bin/clang",
"workbench.colorTheme": "Visual Studio Dark - C++",//设置 Visual Studio Code 的颜色主题
"files.autoSave": "afterDelay",//设置文件自动保存策略为延时自动保存。
"task.allowAutomaticTasks": "on",//允许 Visual Studio Code 自动检测和运行任务。
"cmake.configureOnOpen": true,//当打开 CMake 项目时自动执行配置。
"debug.javascript.resourceRequestOptions": {
"url": "http://localhost:3000/debugger",//可加载调试器所需的web JavaScript 脚本
"headers": {//允许设置请求的自定义 HTTP 标头
"Authorization": "Bearer myAccessToken",
"User-Agent": "MyDebugClient/1.0"
},
"followRedirects": true,//指示调试器是否应跟随重定向加载资源
"timeout": 5000,//设置请求超时时间,以毫秒为单位。
"skipContent": true,//布尔值,指示是否跳过资源的内容下载,只获取资源的元数据。
"ignoreResponseBody": true//布尔值,指示是否忽略资源的响应体内容。
},
//固定在编辑器顶部工具栏中的 CMake 相关命令,包括配置任务运行器和运行任务。
"cmake.pinnedCommands": [
"workbench.action.tasks.configureTaskRunner",
"workbench.action.tasks.runTask"
],
"explorer.confirmPasteNative": false,//禁止在资源管理器中粘贴文件时的确认提示。
"explorer.confirmDragAndDrop": false,//禁止在资源管理器中拖放文件时的确认提示。
//高级的 CMake 配置选项,包含了针对构建(build)、启动(launch)和调试(debug)阶段的状态栏可见性设置,指定为继承默认可见状态。
//设置为 "inherit",表示状态栏可见性继承默认值。
//设置为 "visible",表示在构建阶段继承默认的可见状态。
//hidden: 表示状态栏不可见,隐藏状态。
//collapsed: 在某些 UI 设计中,可能表示状态栏被折叠起来,部分可见或占用最小空间。
//auto: 自动决定状态栏的可见性,通常是根据上下文或用户配置自动调整。
//off: 关闭状态栏的显示,完全隐藏并不占用界面空间。
"cmake.options.advanced": {
"build": {
"statusBarVisibility": "inherit",
"inheritDefault": "visible"
},
"launch": {
"statusBarVisibility": "inherit",
"inheritDefault": "visible"
},
"debug": {
"statusBarVisibility": "inherit",
"inheritDefault": "visible"
}
}
}
cmake && SDL2
- 简介:
Simple DirectMedia Layer(官网:SDL2) 是一个跨平台开发库(SDL 正式兼容 Windows、Mac OS X、Linux、iOS 和Android。),旨在通过 OpenGL 和 Direct3D 提供对音频、键盘、鼠标、游戏杆和图形硬件的低级访问。它被视频播放软件、模拟器和流行游戏使用,包括Valve的获奖目录和许多Humble Bundle游戏。 - linux环境可用apt,yum,dnf直接下载(window,mac需要去官网github上下载)
#debain系
sudo apt update && sudo apt install gcc g++ gdb build-essential cmake
sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-net-dev libsdl2-ttf-dev
#apt下载的第三方库自带环境变量可直接使用
- SDL2的证书(开发的软件也需u遵循证书协议):zlib 许可下分发。(媒体层库 (SDL) 是一个通用 API,它提供对音频、键盘、鼠标、游戏杆、通过 OpenGL 的 3D 硬件以及跨多个平台的 2D 帧缓冲区的低级别访问。)
- 配置环境变量(因win环境变量不能保证绝对的稳定,日常犯病无法识别环境变量信息非常正常,但是vs2022拥有内存观察和强大的分步debug观察状态功能)
- vs2022 空项目SDL2
- 新项目->空项目->项目属性->
(所有配置 x64)vs++目录->包含目录(path=SDL2include) 库目录(path=(SDL2lib))->链接器下->输入->附加依赖项->(SDL2.libSDL2main.lib)
- 新项目->空项目->项目属性->
- vs2022 空项目SDL2
cmake_minimum_required(VERSION 3.28.3)
# 项目信息
project(SDL2PVZ)
# 添加包含了一个叫include的目录,使其自动检测到include下的所有被包含文件或者可执行文件
include_directories(include include1 include2)#可包含多个目录
# # 使用 file(GLOB ...) 匹配所有满足 *.cpp 模式的文件 并且可以选择多个目录
file(GLOB SOURCES
"plantsAndZombie/src/*.c"
"anotherDirectory/*.c"
# 添加更多的目录路径
)
# # 添加可执行文件并指定源文件变量
# add_executable(MyExecutable ${SOURCES})
# 添加可执行文件
add_executable(SDL2PVZ main.cpp ${SOURCES})
# 查找SDL2库
find_package(SDL2 REQUIRED)
# 链接SDL2库
target_link_libraries(SDL2PVZ ${SDL2_LIBRARIES})
# 查找SDL2_image库
find_package(SDL2_image REQUIRED)
# 链接SDL2_image库
target_link_libraries(SDL2PVZ ${SDL2_IMAGE_LIBRARIES})
# 查找SDL2_mixer库
find_package(SDL2_mixer REQUIRED)
# 链接SDL2_mixer库
target_link_libraries(SDL2PVZ ${SDL2_MIXER_LIBRARIES})
# 查找SDL2_net库
find_package(SDL2_net REQUIRED)
# 链接SDL2_net库
target_link_libraries(SDL2PVZ ${SDL2_NET_LIBRARIES})
# 查找SDL2_ttf库
find_package(SDL2_ttf REQUIRED)
# 链接SDL2_ttf库
target_link_libraries(SDL2PVZ ${SDL2_TTF_LIBRARIES})
# 设置编译器选项
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
- 参考项目结构
project/
│
├── CMakeLists.txt
├── include/
│ └── header.h
│ └── header2.h
└── src/
└── main.cpp
├── file1.cpp
├── file2.cpp
├── file3.cpp
└── ...
#include <SDL.h>
using namespace std;
//SDL2期望程序的入口点是SDL_main而不是标准的main函数。当SDL_main未定义时,链接器会报错。
int SDL_main(int argc, char* argv[])
{
// 初始化SDL
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;
return 1;
}
// 创建窗口
SDL_Window* window = SDL_CreateWindow("SDL2 窗口示例", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN);
if (window == nullptr) {
std::cerr << "窗口创建失败: " << SDL_GetError() << std::endl;
return 1;
}
// 等待窗口关闭
SDL_Event event;
bool running = true;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
}
}
// 清理SDL
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
cout << "Hello CMake." << endl;
return 0;
}
- cmake配置
# cmake .执行 CMake 命令来生成新的构建系统文件
#以下方法为指定位置构建防止混乱
# cmake -B <build-dir> -S <source-dir> : <source-dir> 表示你的项目源代码所在的目录,而 <build-dir> 则表示你希望生成构建文件的位置
cmake -B build -S ./
# 第二次调用
rm -rf build && rm CMakeCache.txt
- setting.json
{
"files.associations": {
"iostream": "cpp"
},
"cmake.configureSettings": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "YES"
}
}
RDKit(有机化学cpp库)
微软写的c++库和一些功能
libm
- 数学库(libm)并不是第三方库,而是C标准库的一部分。它提供了各种基本的数学函数,如三角函数 (sin, cos, tan)、指数函数 (exp, log) 等。在Unix-like系统(如Linux和macOS)上,数学库通常是作为一个独立的库文件 libm.so 提供的,因此在编译和链接时需要显式地链接这个库。
- MSVC(Windows系统):不需要显式链接数学库,因为MSVC会自动链接所需的系统库。
- GCC(Linux/Unix-like系统):你通常需要显式地指定链接数学库,例如使用 -lm 选项。
- cmake
# 链接 SDL2 库
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS})
target_link_libraries(PVZ_C_SDL2 ${SDL2_LIBRARIES})
# 链接数学库
if (UNIX)
target_link_libraries(PVZ_C_SDL2 m)
endif()
#重新生成构建文件并构建项目
cd ???????/build
cmake ..
cmake --build . --config Debug --target all -j 10
Boost.Math vs2022已内置
- c++ Boost.Math实现三重积分
#include <boost/math/quadrature/gauss.hpp>
#include <iostream>
// 被积函数,这里是三个变量的函数
double f(double x, double y, double z) {
return x + y + z;
}
// 对最内层变量的积分
double inner_integral(double x, double y) {
auto g = [x, y](double z) { return f(x, y, z); };
double a = 0, b = 1; // 积分区间
auto [result, error] = boost::math::quadrature::gauss<double, 15>::integrate(g, a, b);
return result;
}
// 对中间变量的积分
double middle_integral(double x) {
auto h = [x](double y) { return inner_integral(x, y); };
double a = 0, b = 1; // 积分区间
auto [result, error] = boost::math::quadrature::gauss<double, 15>::integrate(h, a, b);
return result;
}
// 对最外层变量的积分
double outer_integral() {
auto i = [](double x) { return middle_integral(x); };
double a = 0, b = 1; // 积分区间
auto [result, error] = boost::math::quadrature::gauss<double, 15>::integrate(i, a, b);
return result;
}
int main() {
double result = outer_integral();
std::cout << "The result of the triple integral is: " << result << std::endl;
return 0;
}
MSVC 内置内存泄漏检查
- _CrtDumpMemoryLeaks() 不是标准 C++ 库函数,而是 Microsoft Visual Studio 提供的一组调试工具中的一部分,用于帮助开发者检测内存泄漏。它通常用于 Windows 平台上的 C++ 开发。
//Vs中检测内存泄漏代码,C/C++通用
#ifdef _DEBUG
#ifdef __cplusplus
#include<iostream>
#define new new( _CLIENT_BLOCK, __FILE__, __LINE__)
#else
#define _CRTDBG_MAP_ALLOC
#include<malloc.h>
#include<crtdbg.h>
#include<stdlib.h>
#endif // __cplusplus
#else
#include<malloc.h>
#endif
//用于atexit注册,会在程序退出时自动调用
win32开发
QT
- Android
安装必要的工具:
安装 Qt for Android。确保你下载了包含 Android 支持的 Qt 版本。
安装 Android Studio,并确保安装了 NDK(Native Development Kit)和 SDK(Software Development Kit)。
配置 Qt 环境:
打开 Qt Creator,进入 Tools -> Options -> Kits,确保已经配置了 Android Kit。你需要指定 SDK 和 NDK 的路径。
创建 Android 项目:
在 Qt Creator 中创建一个新的项目,选择 Qt Quick Application 或 Qt Widgets Application,然后选择 Android Kit。
配置项目设置:
在项目的 .pro 文件中,可以添加 Android 特有的设置,如权限、包名等。
你可以使用 android:permissions 标签来声明需要的权限。
构建和运行:
选择 Android Kit 作为构建目标,使用 Qt Creator 的构建和运行功能。确保你的 Android 设备或模拟器已连接并可以识别。
生成 APK:
使用 Qt Creator 中的构建选项生成 APK 文件,通常会在项目的 build 目录下找到。
调试和测试:
使用 Android Studio 或者 Qt Creator 中的调试工具,进行应用的测试和调试。
发布:
在准备好后,可以使用 Android Studio 的工具进行签名和发布。