在C语言中,整数类型有很多类似UINT32这样的指代类型,以便在不同的字长的硬件里,确定使用特定长度(如32比特)的整数。在C中,从整形往字符串的转换一般是调用一些函数,如printf, 并人为指定一个类型说明符,比如 %d 来完成。若迁移C的工程到C++,则要额外注意类型的敏感性。在产生字符串时,不指定类型的C++调用,会把char作为字符处理,导致诡异的问题。
下面这个例子就是最近协助解决的一个非常典型的类型问题,在一个生产系统里存在多年,直到传感器扩容后才爆发,造成了经济损失。
1. C示意程序老版软件,使用一个结构体存储传感器的电压(代表温度),并插入到数据库里。由于是控制台程序,采用的是管道重定向,即直接生成INSERT语句,而后定向到数据库的控制台客户端中执行。
#includetypedef struct tag_item{ unsigned int timestamp; int vol; char machine_id; } ITEM; int main() { ITEM item; item.timestamp = 1646587837; item.machine_id = 66; item.vol = 12; printf("INSERT INTO mlog(tmstmp,vol,machine) VALUES (%u, %d, 'M_%d');n" , item.timestamp , item.vol , item.machine_id ); return 0; }
此时,会输出:
INSERT INTO mlog(tmstmp,vol,machine) VALUES (1646587837, 12, 'M_66');
这个例子里,machine字段在数据库里是字符串,因此采用引号包裹。但这种字符串类型的字段,是不会检查“66”这个整形的有效性的。
2. C++Qt示意程序新版软件,是这样处理的:
#include#include struct tag_item{ long long timestamp; int vol; char machine_id; }; int main(int argc, char * * argv) { QCoreApplication a(argc,argv); tag_item item; item.timestamp = 1646587837; item.vol = 12; item.machine_id = 66; QString sql = QString("INSERT INTO mlog(tmstmp,vol,machine) VALUES (%1,%2,'M_%3');n") .arg(item.timestamp) .arg(item.vol) .arg(item.machine_id); QTextStream stm(stdout); stm << sql; return 0; }
程序输出为:
INSERT INTO mlog(tmstmp,vol,machine) VALUES (1646587837,12,'M_B');
注意到了吧?66被作为char类型的ascii码,转换为了"B"
3. 问题的隐藏这个系统部署了3年了,产生无数的记录,但是一直都很正常。
用户不知道正常的ID应该是M66,M67,负责的工人在界面记录的是 M_B,M_C。工人知道M_B在电镀车间,M_X在制氧车间的液箱里。如果当时安装传感器的师傅知道内部规则,就会发现水箱上贴的标记是诡异的。由于工程一期的传感器不多,传感器的取值恰好位于字母区,这个问题就一直隐藏在这里。
此外,上位机上的程序过于简单,传感器首次上线,只是检查传感器的ID有无重复,并不检查取值。如果发现了新的传感器,则直接插入,并提示车间助理录入位置、温度报警范围等参数。系统部署以来,在界面上展示的始终就是 M_B这样的字母ID!就连公司的客服也以为这是正常的。
4. 问题爆发和修复直到今年夏天疫情结束,生产线重启前,趁机批量更换并扩容,导致出现了反斜杠(92号传感器的ASCII)转译问题:
INSERT INTO mlog(tmstmp,vol,machine) VALUES (1646587837,12,'M_');
而这些有问题的语句导致事务的失败和回滚,同一个事务内的所有INSERT全部丢失。由此带来的温控系统得不到及时、正确的反馈,液体温度过热,生产线停机。解决的方法很简单, 强制转换为qint8:
QString sql = QString("INSERT INTO mlog(tmstmp,vol,machine) VALUES (%1,%2,'M_%3');n")
.arg(item.timestamp)
.arg(item.vol)
.arg((qint8)item.machine_id);
但是损失是很大的,造成了液料损失,和管道清洗损失共计20多万元。
5. 问题测试问题根源是没有正确估计到C++的自动类型判断带来的潜在问题。C++自动通过类型来决定策略,可以用下面的例子具体观察:
#include#include #include struct tag_item{ long long timestamp; int vol; char machine_id; }; template void test(T v) { QTextStream stm(stdout); stm << "type << typeid(v).name() <<"n"; stm << "tTest QTextStream : " << v << "n"; stm << "tTest QString() : " << QString("%1").arg(v) << "n"; stm.flush(); std::cout << "tTest iostream : " << v << "n"; } int main(int argc, char * * argv) { QCoreApplication a(argc,argv); tag_item item; item.timestamp = 1646587837; item.vol = 12; item.machine_id = 66; test(item.machine_id); test((char) item.machine_id); test((qint8) item.machine_id); test((__int8_t) item.machine_id); test((unsigned char) item.machine_id); test((quint8) item.machine_id); test((int) item.machine_id); return 0; }
输出为:
typeid=c Test QTextStream : B Test QString() : B Test iostream : B typeid=c Test QTextStream : B Test QString() : B Test iostream : B typeid=a Test QTextStream : 66 Test QString() : 66 Test iostream : B typeid=a Test QTextStream : 66 Test QString() : 66 Test iostream : B typeid=h Test QTextStream : 66 Test QString() : 66 Test iostream : B typeid=h Test QTextStream : 66 Test QString() : 66 Test iostream : B typeid=i Test QTextStream : 66 Test QString() : 66 Test iostream : 66
可见,无论是用标准C++或者Qt,都存在char类型的自动转换注意事项。
(生产环境为传感器嵌入式Linux系统+Linux上位机)



