批判性接受,很多观点个人理解和思考,不一定正确,大家一起探讨交流。
使用断言记录内部假设。 不能用断言来检查运行时错误。 断言是用来处理内部编程或设计是否符合假设; 不能处理对于可能会发生的且必须处理的情况要写防错程序,而不是断言。 如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查, 此过程为正常的错误检查,不能用断言来实现。
C/C++ 里面断言是 宏定义,只在 Debug 版本生效,Release 版本是一个空宏。 如果程序在 assert 处终止了,并不是说含有该 assert 的函数有错误,而是调用者出了差错,assert 可以帮助我们找到发生错误的原因。 不能使用改变环境的语句。不应该存在副作用,童畜无害。
assert(i++ < 100); // i++ 这个逻辑就丢了。Release 和 Debug 版本就存在不一致性。
断言有点类似单元测试。CRT 断言:
char* strdup(char* str) {
int length = strlen(str);
char* strnew = memcpy(malloc(length), str, length);
return strnew;
}
上面这段代码存在问题,当 str 输入为 NULL,会发生什么?当 malloc 失败会发生什么? 改进版本 1:
// 申请创建 str 的拷贝。当 str == NULL,程序无定义。
char* strdup(char* str) {
// 因为它被用来检查在该程序正常工作时绝不应该发生的非法情况。
assert(str != NULL); // 正确的 断言 使用。
char* strnew = (char*) malloc(strlen(str) + 1);
// 最终产品中肯定会出现并且必须对其进行处理的错误情况。
assert(strnew != NULL); // 错误的断言使用,因为内存申请真的可能不成功。
strcpy(strnew, str);
return strnew;
}
assert(strnew != NULL);
错误的断言使用,因为内存申请真的可能不成功。
改进版本 2:
// 申请创建 str 的拷贝。当 str == NULL,程序无定义。
char* strdup(char* str) {
assert(str != NULL);
char* strnew = (char*) malloc(strlen(str) + 1);
if (strnew != NULL) {
strcpy(strnew, str);
assert(strcmp(strnew, str) == 0); // 断言一样
}
return strnew;
}
程序错误一般会在运行一段时间后才因为异常退出。这时候触发错误导致进程退出的代码位置往往不是“案发的第一现场”,给调试工作带来了更大的难度。
如果错误概率发生,就让其稳定必现。
Win32 堆和 CRT 堆的常用字节模式和它们的含义 @156@liebao
字节模式 | 堆管理器 | 用途 | 长度 |
---|---|---|---|
0xFEEEFEEE | Win32 堆 | 填充空闲块的数据区 | 块数据区大小 |
0xBAADF00D | Win32 堆 | 填充新分配块的数据区 | 块数据区大小 |
0xAB | Win32 堆 | 填充在堆块的用户数据之后,用于检测堆溢出 | 不确定 |
0xFD | CRT 调试堆 | 填充用户数据区前后的隔离区(no-man’s land) | 各 4 个字节 |
0xDD | CRT 调试堆 | 填充释放的堆块(dead land) | 整个堆块大小 |
0xCD | CRT 调试堆 | 填充新分配的堆块(clean land) | 用户数据区大小 |
各种运行时检查:
多用断言,用对断言。 一种程序,突然崩溃了,闷声不啃声,也不说啥原因,耽搁调试半天。 另外一种程序,自己发现错误了,会说话,能主动报告错误。无数个断言就形成了一个哨兵网络。
assert 语句,你可以把错误原因放到 assert 的参数中,这样不仅能保护你的程序不往下走,而且还能把错误原因返回给调用方。
假如你受雇为核反应堆编写软件,就必须对堆芯过热这一情况进行处理。
// const 保证参数不会搞反,想用错都难。
char* strcpy(char* dest, const char* src) {
// 断言,Debug 版本生效。
assert((dest != NULL) && (src != NULL));
// 入参检查,特殊情况处理。
if (src == NULL || dest == NULL) { // 还能再抢救一下?
// Release 极端断言情况发生了,蓝屏。
// 特殊处理一下,让问题转移?
return dest; // 建议做法
}
char* address = dest; // 功能逻辑
while ((*dest++ = *src++) != '\0')
NULL;
return address;
}
int resetBufferSize(int nNewSize) {
// 在函数开始处检验传入参数的合法性
// 异常输入,程序的逻辑未定义。
// 说明这个函数的使用者用错函数了。
assert(nNewSize >= 0);
assert(nNewSize <= MAX_BUFFER_SIZE);
...
}
void* memcpy(void* dst, const void* src, size_t count) {
// 安全检查
assert(dst != NULL && src != NULL);
unsigned char* pdst = (unsigned char*)dst;
const unsigned char* psrc = (const unsigned char*)src;
bool overlap = (psrc <= pdst && pdst < psrc + count) ||
(pdst <= psrc && psrc < pdst + count);
// 防止内存重复。
assert(!overlap); // Debug 版本。
if (overlap) { // 覆盖处理,早期暴露,有一个 DirectX API 就是这样做的。
// 让 Release 不稳定的 bug,稳定出现。
// 将错误早点暴露出来
while (count--) {
*pdst++ = 0;
}
return dst;
}
while (count--) {
*pdst++ = *psrc++;
}
return dst;
}
函数的参数,特别是指针参数必须利用断言来进行确认。 利用断言检查程序中的各种假设的正确性,任何函数都存在一些定义域,存在各种假定。 在程序设计中不要轻易认为某种情况不可能发生,对你认为不可能发生的情况必须用断言来证实。
利用不同的算法对同一个东西进行检验,即使会降低程序运行速度。 非常复杂的情况,可以采用代码块来验证。
#ifdef _DEBUG // IDE 内置的一个宏,不同 IDE 存在差异。
... // 代码块,各种检查。
#endif
在写 视频信息解析 代码的时候,采用 ffmpeg.exe 命令行调用。实在搞不清楚,就写了两个版本,一个正则解析命令行输出,一个解析 JSON 数据, 两份都可能存在错误,两份实现相互校验,发布版本采用 JSON 版本。
bool VInfoEngine::runProbe(const QString& filename, int timeout) {
bool temp = m_probe->runx(filename, timeout); // JSON 解析
#if _DEBUG
m_probe_regex->runx(filename, timeout); // 命令行正则解析
m_probe_regex->assertEqual(m_probe);
#endif
return temp;
}
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 128 kb/s
Stream #0:1: Video: flv1, yuv420p, 1120x800, 23.98 fps, 23.98 tbr, 23.97 tbn
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 640,
"height": 480,
"coded_width": 640,
"coded_height": 480,
"closed_captions": 0,
"has_b_frames": 2,
"pix_fmt": "yuvj420p",
"level": 30,
"color_range": "pc",
"color_space": "bt470bg",
"chroma_location": "left",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"r_frame_rate": "30/1",
"avg_frame_rate": "1387907242/46242737",
"time_base": "1/90000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 660344400,
"duration": "7337.160000",
"bit_rate": "299079",
"bits_per_raw_sample": "8",
"nb_frames": "220214"
}
在后继的各种输入断言下,其实两份实现多多少少都存在 bug,不过最终都得以修复。
通过正则版本,才搞清楚 Json 里面各种数字的单位;通过 Json 版本才知道 视频存在 SAR 4:3 DAR 16:9
和 旋转的问题。
断言旋转角度 只有 0°、90°、-90°、-180°。
// bitrate 以为是 1024,其实是 1000,不知道正确与否,反正正则版本是这样算的。
bitrate = (qjson.value("bit_rate").toDouble() / 1000);
图像处理 API:
// 返回 0 表示成功。
// 外面需要把结果内存申请好,要和输入图片一样即可,并合理释放。
int getGrayBitmap(FastImage fimage, FastImage& result, bool clearBackgroud) {
assert(fimage.format == FastImageType::FastImageRGBA8888);
assert(fimage.width > 0 && fimage.height > 0 && fimage.pixels != nullptr);
assert(result.format == FastImageType::FastImageRGBA8888);
assert(fimage.width == result.width && fimage.height == result.height && result.pixels != nullptr);
cv::Mat mrgba(fimage.height, fimage.width, CV_8UC4, fimage.pixels);
cv::Mat dst = mrgba.clone();
cv::cvtColor(mrgba, dst, CV_RGB2GRAY);
return matToBitmap(dst, false, result);
}
Android 甚至断言 网络访问在主线程崩溃,界面修改不在主线程 也崩溃。☭
base::ReadFileToString 此行代码在 UI 线程中会触发 __debugbreak();
如果是加载界面库或必须的配置可以通过声明 base::ThreadRestrictions::ScopedAllowIO allowio; 禁止 __debugbreak();
UI 线程开始
TlsSetValue(key, GetCurrentThreadID());
bool Utils::IsVip() {
ASSERT(TlsGetValue(key) != GetCurrentThreadID());
}
void* realloc( void* pv, size_t size );
还有一个悲剧函数。
int ch = getchar();
还有一个神奇函数,有点像状态机。
#include <string.h>
#include <stdio.h>
int main() {
char str[80] = "This is - test # website";
const char s[] = "-#";
char* token;
/* 获取第一个子字符串 */
token = strtok(str, s);
/* 继续获取其他的子字符串 */
while (token != NULL) {
printf("\"%s\"\n", token);
token = strtok(NULL, s);
}
// 注意分割处理后原字符串 str 会变,变成第一个子字符串:混蛋。
printf("\n\"%s\"\n", str);
return(0);
}
"This is "
" test "
" website"
"This is "
真不敢相信,这些都是 C 语言大师设计的接口标准函数。