在日常 PHP 编程中,我们经常使用 unset() 删除数组中的元素,例如:
$arr = [ 'a', 'b', 'c' ];
unset($arr[1]);
print_r($arr);
很多人以为执行后数组会变成:
['a', 'c']
但实际上输出结果是:
Array
(
[0] => a
[2] => c
)
可以看到索引 没有重新排列,原来的下标 [1] 被删除后,PHP 并不会自动调整后续元素的索引。
PHP 的数组是 有序哈希表(Ordered Hash Table),
既可以用作索引数组,也可以用作关联数组。
当你执行:
unset($arr[1]);
PHP 实际上只是从哈希表中 移除了该键对应的项,
但不会重新分配其它键(即不会重排)。
这样设计的好处是:
删除元素开销小;
维持键的稳定性;
保证关联数组不被破坏。
这种行为在某些场景下会导致逻辑错误:
❌ 示例 1:使用 for 循环遍历
$arr = [ 'a', 'b', 'c' ];
for ($i = 0; $i < count($arr); $i++) {
if ($arr[$i] === 'b') unset($arr[$i]);
}
print_r($arr);
输出结果:
Array
(
[0] => a
[2] => c
)
由于索引跳跃,$arr[1] 被删除后,count() 返回 3,但 $arr[1] 不存在。
如果后续逻辑依赖索引顺序(比如 $arr[$i+1]),就会出错。
✅ 方案 1:使用 array_values() 重排索引
$arr = [ 'a', 'b', 'c' ];
unset($arr[1]);
$arr = array_values($arr);
print_r($arr);
输出:
Array
(
[0] => a
[1] => c
)
array_values() 会重新生成从 0 开始的连续索引,非常适合处理纯数字索引数组。
✅ 方案 2:使用 foreach 遍历 + unset
$arr = [ 'a', 'b', 'c' ];
foreach ($arr as $key => $value) {
if ($value === 'b') unset($arr[$key]);
}
print_r($arr);
PHP 的 foreach 遍历使用内部指针,即使当前元素被删除,也不会影响循环行为。
但仍然不会重排索引。
如果后续需要连续索引,可再执行:
$arr = array_values($arr);
✅ 方案 3:反向遍历删除(for 循环)
$arr = [ 'a', 'b', 'c' ];
for ($i = count($arr) - 1; $i >= 0; $i--) {
if ($arr[$i] === 'b') unset($arr[$i]);
}
print_r($arr);
这种写法在删除多个元素时能保证不会“跳过”元素。
✅ 方案 4:用关联键(推荐用于复杂数据结构)
如果你的数据是关联数组(如数据库记录),
可以直接用键值操作,避免索引问题:
$db = [
['id' => 1, 'name' => 'A'],
['id' => 2, 'name' => 'B'],
['id' => 3, 'name' => 'C']
];
// 重新索引成 id=>item
$map = [];
foreach ($db as $item) {
$map[$item['id']] = $item;
}
// 删除指定 id
unset($map[2]);
print_r(array_values($map));
输出:
Array
(
[0] => Array ( [id] => 1 [name] => A )
[1] => Array ( [id] => 3 [name] => C )
)
场景 是否重排 适用情况 推荐度
unset() 直接删除 ❌ 否 临时删除,不依赖索引 ⭐
array_values() 重排 ✅ 是 删除后需保持连续索引 ⭐⭐⭐⭐
foreach + unset() ❌ 否 逻辑删除但不重排 ⭐⭐⭐
反向 for 删除 ❌ 否 控制删除顺序 ⭐⭐
关联键删除 ✅ 无影响 有唯一 ID 键的数组
POINT pt; GetCursorPos(&pt);
SetForegroundWindow(m_hWnd); //右击后点别地可以清除“右击出来的菜单”
HMENU hMenu; //托盘菜单 win32程序使用的是HMENU,如果是MFC程序可以使用CMenu
hMenu = CreatePopupMenu();//生成托盘菜单
AppendMenu(hMenu, MF_STRING, WM_ONCLOSE, _T("退出"));
int cmd = TrackPopupMenu(hMenu, TPM_RETURNCMD, pt.x, pt.y, NULL, m_hWnd, NULL);
if(cmd == WM_ONCLOSE)
{
//退出程序
m_nid.hIcon = NULL;
Shell_NotifyIcon(NIM_DELETE, &m_nid);
::PostQuitMessage(0);
}$a['modulesn'] = "0542509004725";
$b = "00542509004725";
一个是 以 0 开头,另一个是 以 00 开头。
按理说用 != 应该返回 true(不相等),但你说结果是 false(相等)。
原因:PHP 的弱类型比较(类型转换问题)
在 PHP 里,!=、== 会触发 弱类型比较:
如果两个字符串看起来都是 数字字符串,PHP 会把它们转换成数字再比较。
"0542509004725" 和 "00542509004725" 都会被转换成 542509004725(整数)。
转换后它们相等,所以 != 返回 false。
验证
var_dump((int)"0542509004725"); // int(542509004725)
var_dump((int)"00542509004725"); // int(542509004725)
结果一样。
解决办法
如果你想比较字符串本身,而不是数值:
用 全等比较 !== 或 ===
var_dump($a['modulesn'] !== $b); // true
或者强制用 字符串比较函数
var_dump(strcmp($a['modulesn'], $b) !== 0); // true
建议:凡是涉及可能带前导 0 的数据(比如条码、序列号、工号),一定要用 === 来比较,不要用 == 或 !=。
今天在工程中使用jsoncpp时,发现一个问题。
现在把问题用一个demo重现出来。如下:
#ifndef _USER_STORE_H_
#define _USER_STORE_H_
class UserStore
{
public:
UserStore();
virtual ~UserStore();
private:
Json::Value m_jvData;
public:
void Login();
};
#endif
//cpp文件
#include "pch.h"
#include "UserStore.h"
UserStore::UserStore()
{
}
UserStore::~UserStore()
{
}
void UserStore::Login()
{
m_jvData["a"] = "123123";
return;
}
调用如下:
...
// 全局变量:
UserStore theStore;
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
....
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
theStore.Login();
break;
...
退出程序后,显示

问了deepseek和chatgpt都没啥用,后百度搜索如下内容:
发现在json_value.cpp调用下面语句时崩溃。
{
value_.string_ = valueAllocator()->duplicateStringValue( value );
}
查看 valueAllocator() ,在src\lib_json\json_value.cpp里,他是一个函数,如下:
static ValueAllocator *&valueAllocator()
{
static DefaultValueAllocator defaultAllocator;
static ValueAllocator *valueAllocator = &defaultAllocator;
return valueAllocator;
}
它是取得一个静态变量的针指。
调试发现在使用valueAllocator()时,即DefaultValueAllocator 对象指针时。DefaultValueAllocator 对象已经被析构了。
因为c++中不同的cpp文件中,全局对象和静态对象 构造和析构 顺序是不确定的。
son_value.cpp再看往下看。可以看到。
static struct DummyValueAllocatorInitializer
{
DummyValueAllocatorInitializer()
{
valueAllocator(); // ensure valueAllocator() statics are initialized before main().
}
} dummyValueAllocatorInitializer;
它的作用是确保valueAllocator()在main()函数前被调用。(注意:先构造的后析构,后构造的先析构)
但其实这样还不能确保在比其它全局对象的构造函数先调用,比其它全局对象晚析构。问题就出在这里了。
解决方案1:
不在全局对象析构函数中使用jsoncpp字符串。就没问题了。
但有时候会在全局对象析构函数保存一些数据,把它转成json格式后再存盘。所以这个解决方案,治标不治本。
解决方案2:
提前对 DefaultValueAllocator 类对象进行构造,比其它【全部对象】或【静态对象】更前构造,这样DefaultValueAllocator也会比他们更晚析构。
可以在 DummyValueAllocatorInitializer 前面加上一个编译指令 #pragma init_seg(lib) 如下:
#pragma init_seg(lib) // add by fangyukuan 2012.5.6
static struct DummyValueAllocatorInitializer
{
DummyValueAllocatorInitializer()
{
valueAllocator(); // ensure valueAllocator() statics are initialized before main().
}
} dummyValueAllocatorInitializer;
这个方案,不好的地方就是修改了第三方库。一般我们是不会去修改第三方库的。
你有更好方案吗?有,请告诉我。
其它:
我们再来看看 DefaultValueAllocator 类,都做了些什么?代码如下:
class DefaultValueAllocator : public ValueAllocator
{
public:
virtual ~DefaultValueAllocator()
{
}
virtual char *makeMemberName( const char *memberName )
{
return duplicateStringValue( memberName );
}
virtual void releaseMemberName( char *memberName )
{
releaseStringValue( memberName );
}
virtual char *duplicateStringValue( const char *value,
unsigned int length = unknown )
{
//@todo invesgate this old optimization
//if ( !value || value[0] == 0 )
// return 0;
if ( length == unknown )
length = (unsigned int)strlen(value);
char *newString = static_cast<char *>( malloc( length + 1 ) );
memcpy( newString, value, length );
newString[length] = 0;
return newString;
}
virtual void releaseStringValue( char *value )
{
if ( value )
free( value );
}
};
上面是方法一和方法二实验后均可以解决问题,我的实验方法三也能解决,随便写个值:
UserStore::UserStore()
{
m_jvData["xxxx"] = 1111;
}运行如下代码
void main() {
// 临时future对象将在语句结束时销毁
std::async(std::launch::async, []{
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Async task completed\n";
});
std::cout << "Function continues\n";
}
在这个代码里,你会惊讶地发现 "Function continues" 会在2秒后才输出,而不是立即输出,为什么?
标准规定 (C++11及以上),当从 std::async 返回的 future 对象被销毁时:如果这是最后一个引用该共享状态的 future,且异步任务还未完成,则析构函数会阻塞等待任务完成。
设计成这样主要是为了以下的思考
异常安全:确保异步任务中的异常不会被无声丢弃
资源管理:防止任务未完成时资源提前释放
确定性:避免程序退出时仍有后台线程运行
保存future对象
std::future<void> taskFuture; // 成员变量或长期存在的对象
void startAsyncTask() {
taskFuture = std::async(std::launch::async, []{
// 长时间运行的任务
});
// future生命周期延长,不会立即阻塞
}memset 的具体实现的代码大概如下:
//实现方式是逐字节写入
void* memset(void* dest, int ch, size_t count) {
unsigned char* p = (unsigned char*)dest;
while (count--) {
*p++ = (unsigned char)ch;
}
return dest;
}
memset 是按字节填充的,所以:
memset(a, 0, sizeof(a)) ✅(0 的字节模式是 0x00)
memset(a, -1, sizeof(a)) ✅(-1 的字节模式是 0xFF)
memset(a, 20, sizeof(a)) ❌(20 的字节模式是 0x14,但 int 可能是 0x14141414,不符合预期)
memset 会破坏 C++ 对象的内部结构(如虚表指针),导致未定义行为(UB)。
for(int i 0; i < 100; i++)
a[i] = 20;
int a[100];
std::fill(std::begin(a), std::end(a), 20); // 全部填充为 20
或者
std::fill_n(a, 100, 20); // 填充前 100 个元素为 20
#include <array>
#include <algorithm>
std::array<int, 100> a;
a.fill(20); // 全部填充为 20
或者
std::fill(a.begin(), a.end(), 20);执行如下三个命令即可。
reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve
结束explorer.exe
taskkill /f /im explorer.exe
启动explorer.exe
start explorer.exemax_input_vars 表示接受多少 输入的变量(限制分别应用于 $_GET、$_POST 和 $_COOKIE 超全局变量) 指令的使用减轻了以哈希碰撞来进行拒绝服务攻击的可能性。 如有超过指令指定数量的输入变量,将会导致 E_WARNING 的产生,更多的输入变量将会从请求中截断。
; How many GET/POST/COOKIE input variables may be accepted
; max_input_vars = 1000
max_input_vars = 10000在axios文件中导入router,console输出为undefined
(js/ts中无法使用vue声明的文件/不能使用useRouter ,useRoute,他们需要在setup中调用执行后才能用)
解决方法(推荐)
导入项目中已配置好的router
import router from '@/router'
其他方法
window.location.href = "/login"VSCODE 配置用户代码片段:通过header添加避免重复包含问题。
{
"c c++ Header": {
"scope": "c,cpp",
"prefix": "header",
"body": [
"#ifndef __${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H__",
"#define __${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H__",
"",
"$0",
"",
"#endif /* __${TM_FILENAME_BASE/(.*)/${1:/upcase}/}_H__ */"
],
"description": "Add #ifndef,#define and #endif"
}
}两年前,曾经用过 STC的单片机,当时对他的那个ISP下载功能很是感兴趣,且当时也想实现一个IAP 升级办法,又不想占用他现有的fash 空间,毕竟还是有点小。就想办法着手研究了一下,就写了一段代码在8051内核的64K空间依次读取所有的数据,最后得到了一个 2k 多一点的 ISP 所用的 bin 文件,反汇编得到汇编文件,就在那花功夫细细地看了看实现方法,分析得出了基本的下载协议,两年后,我觉得这些东西可以考虑公开了。阅读全文→
通过第三方程序实现对STC单片机的程序下载,可以方便进行现场的调试和更新。特别是对于设计远程程序更新、无线程序下载与调试等功能有帮助。阅读全文→
C++11引入了=default和=delete关键字,用于控制类的特殊函数(如构造函数、赋值运算符等)的行为。=default允许显式要求编译器提供默认实现,提高代码可读性和性能;=delete则阻止这些函数的使用,增强类型安全。阅读全文→
std::promise 提供了一种灵活的方式来在不同线程之间传递几乎任何类型的数据。阅读全文→
C++11引入了一个新的较实用的模板类型,std::tuple,也即是元组。元组是一个固定大小的不同类型(异质,heterogeneous)值的集合,也即它可以同时存放不同类型的数据。C++已有的std::pair类型类似于一个二元组,可看作是std::tuple的一个特例,std::tuple也可看作是std::pair的泛化。std::pair的长度限制为2,而std::tuple的元素个数为0~任意个。阅读全文→
COMMTIMEOUTS:COMMTIMEOUTS主要用于串口超时参数设置。COMMTIMEOUTS结构如下:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;
DWORD ReadTotalTimeoutMultiplier;
DWORD ReadTotalTimeoutConstant;
DWORD WriteTotalTimeoutMultiplier;
DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
间隔超时=ReadIntervalTimeout
总超时 = ReadTotalTimeoutMultiplier * 字节数 + ReadTotalTimeoutConstant
串口读取事件分为两个阶段(我以Win32 API函数ReadFile读取串口过程来说明一下)
第一个阶段是:串口执行到ReadFile()函数时,串口还没有开始传输数据,所以串口缓冲区的第一个字节是没有装数据的,这时候总超时起作用,如果在总超时时间内没有进行串口数据的传输,ReadFile()函数就返回,当然 没有读取到任何数据。而且,间隔超时并没有起作用。
第二阶段:假设总超时为20秒,程序运行到ReadFile(),总超时开始从0 计时,如果在计时到达10秒时,串口开始了数据的传输,那么从接收的第一个字节开始,间隔超时就开始计时,假如间隔超时为1ms,那么在读取完第一个字节后,串口开始等待1ms,如果1ms之内接收到了第二个字节,就读取第二个字节,间隔超时重置为0并计时,等待第三个字节的到来,如果第三个字节到来的时间超过了1ms,那么ReadFile()函数立即返回,这时候总超时计时是没到20秒的。如果在20秒总计时时间结束之前,所有的数据都遵守数据间隔为1ms的约定并陆陆续续的到达串口缓冲区,那么就成功进行了一次串口传输和读取;如果20秒总计时时间到,串口还陆陆续续的有数据到达,即使遵守字节间隔为1ms的约定,ReadFile()函数也会立即返回,这时候总超时就起作用了。
总结起来,总超时在两种情况下起作用
第一:串口没进行数据传输,等待总超时时间那么长ReadFile()才返回。非正常数据传输
第二:数据太长,总超时设置太短,数据还没读取完就返回了。读取的数据是不全的
间隔超时触发是有条件的
第一:在总超时时间内。
第二:串口进行了数据的传输。
成功的进行一次串口数据的传输和读取,只有总超时和间隔超时相互参与配合才能完成