
人再笨还能学不会超出存储期的对象的指针不能解引用?
人再笨还能学不会 alloc 出来的指针需要 free 一次?
人再笨还能学不会 alloc 出来的指针不能越界读取和写入?
人再笨还能学不会 malloc 出来的内存需要初始化才能读取?
人再笨还能学不会 free 只能用在 alloc 出来的指针上?
人再笨还能学不会 free'ed / realloc'ed 的指针不能再次 free ?
人再笨还能学不会重用 free'ed / realloc'ed 的存储是未定义行为?
人再笨还能学不会 realloc 返回 NULL 时原有指针仍需 free 一次?
人再笨还能学不会 memcpy memmove 的目标缓冲区的大小需要足够大?
人再笨还能学不会 memcpy 的两段内存区域不能重叠?
人再笨还能学不会 strcpy strlen puts printf("%s"...) 只能用在带 '\0' 终结符的字符串上?
...
]]>想起它骨子里的自由,放荡,甚至危险
我害怕却又兴奋
它没有边界感,但它的指针能带我去别人根本想象不了的环境
它给不了安全感,但它跑起来永远是最快最轻松的
它的世界里没有规矩,没有限制,更没有随波逐流的浮华
它能随时能做它想做的,要它想要的
C ,我想你了
]]>举个简单的例子:数文件层级。
在 Linux 上,我们数斜杠数量就好。
在 Windows 上,再加上反斜杠,应该就好了。——我是这样想的。
#include <stdio.h> int main(int argc, char *argv[]) { int level; const char *p; if (argc < 2) { return 1; } for (level = 0, p = argv[1]; *p; p++) { if (*p == '/' || *p == '\\') { level++; } } printf("%d\n", level); return 0; } 用 MinGW 的 GCC 编译一下,然后跑几个用例:
gcc -o getlevel.exe getlevel.c C:\>getlevel C:\浙江省\宁波市\北仑区\小港街道.txt 4 C:\>getlevel C:\浙江省\宁波市\北仑区\大碶街道.txt 5 天塌了,这么简单的代码竟然出了 bug 。
原来 碶 的 编码是 {0xb4, 0x5c},其中 0x5c 和反斜杠的 ASCII 编码一模一样。
GBK 的第一字节兼容 ASCII ,但第二字节的范围是 0x40 ~ 0xfe,与 ASCII 的 0x00 ~ 0x7f 重叠。BUG 就这么诞生了。
UTF-8 没有这个问题的原因是:只要字节范围在 0x00 ~ 0x7f,那么就一定是 ASCII ,因为后续字节都避开了这个范围。虽然中文编码比 GB 系列长了,但是这个设计确实省了很多事。包括 strstr() strcmp() 之类的都不会出现奇奇怪怪的 bug 。
或许我应该使用 wmain() 然后获取 wchar_t,但是 wmain() 是 Windows 特有的东西,这样做就没法和 Linux 公用同一套代码了。目前加上了 mbtowc() 作为修复。原本简洁的代码变得十分复杂:(
说到这又不得不吐槽下 Windows 的各种奇怪 API 了,不知道它是如何存活到现在的...
]]>我要为 C 语言的 qsort 编写一个比较函数,比较两个 uint64_t ,考虑以下两种实现:
int qcmp_uint64(const void* pa, const void* pb) { uint64_t a = *(uint64_t*)pa; uint64_t b = *(uint64_t*)pb; if (a < b) return -1; if (a > b) return 1; return 0; } int qcmp_uint64_2(const void* pa, const void* pb) { uint64_t a = *(uint64_t*)pa; uint64_t b = *(uint64_t*)pb; return a - b; } 从直觉上看,似乎第二种实现效率更高,但由于整数相减可能溢出,我对第二种方案的正确性产生了怀疑
第二种方案是否正确?如何高效实现这一比较函数?
对于第二种实现 qcmp_uint64_2,直接返回 a - b 的确存在隐患。原因如下:
无符号整数减法不会溢出,但结果始终为非负数。
转换为 int 可能导致符号错误:
例如:
高效且安全的实现 利用 比较操作的布尔值隐式转换为 0/1 ,可写出无分支的高效代码:
int qcmp_uint64(const void* pa, const void* pb) { uint64_t a = *(const uint64_t*)pa; uint64_t b = *(const uint64_t*)pb; return (a > b) - (a < b); // 若 a > b 则 1-0=1 ; a < b 则 0-1=-1 ;相等则为 0 } 优点:
性能验证
在 x86-64 架构下,上述代码的典型汇编输出为:
mov rax, [rdi] cmp [rsi], rax setb al seta cl movzx eax, al movzx ecx, cl sub eax, ecx ret 完全无分支,效率与减法方案相当,且绝对安全。
结论:不要使用 a - b 方案,优先选择无分支的布尔比较实现。
这个 (a > b) - (a < b) 的写法太巧妙了!
感觉这类惯用法( idiom )很难通过看书或网络学习,正是 AI 的强项
只搜到一个相关的: https://stackoverflow.com/questions/3886446/problem-trying-to-use-the-c-qsort-function
]]>#include <stdio.h> void assign_value(int *array, int index, int value); int main() { printf("Hello, World!\n"); int array[10]; assign_value(array, 16, 131); printf("%d\n", array[16]); return 0; } void assign_value(int *array, int index, int value) { array[index] = value; printf("done\n"); } 编译:$ gcc -g -Wall -std=c18 -o hello_world hello_world.c 运行输出:
Hello, World! done 131 [1] 3719 segmentation fault (core dumped) ./hello_world 但是如果把 index 从 16 改成 12, 则不会出现最后的 segmentation fault. 如果 C 不处理越界的话,为什么 16 会报错,如果处理越界为什么 12 不报错?
]]>#include <stdio.h> struct X { int a; char c[10]; short d; }; char checker(int a) { (void)a; return '1'; } #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) #define TEST(x) \ do \ { \ char checkSizeOfInt[sizeof(x)] = {checker(&checkSizeOfInt)}; \ _Pragma(TOSTRING(message("Value: " #x))) \ } while (0); int main() { TEST(struct X); return 0; } ]]>

然后我找了下在 stackexchange 的真实提问:
从回答来看,C 和 Python 的两种做法在数值计算上都是成立的。两种做法的区别在于是否允许余数为负数,或者说,符号该不该与原数值相同。
不允许余数出现负数的,是目前广泛使用的欧几里得除法。
所以“数学洁癖”会认为负值余数是错的?
]]>/** * @brief interface print format data * @param[in] fmt is the format data * @note none */ void ov2640_interface_debug_print(const char *const fmt, ...) { } 这个函数的函数原型与 printf 完全一致,,所以我想像下面这样直接把它的参数传递给 printf ,,可是编译器报错,,请问应该怎么改??
void ov2640_interface_debug_print(const char *const fmt, ...) { printf(fmt, ...); } ]]> 1000: f3 0f 1e fa endbr64 1004: 48 83 ec 08 sub $0x8,%rsp 1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__@Base> 100f: 48 85 c0 test %rax,%rax 1012: 74 02 je 1016 <_init+0x16> void myprint1(uint8_t *pdata, size_t len, char *fmt, ...) { myprint2(fmt,...); // 这里参数怎么写 } void mprint2(char *fmt, ...) { printf(fmt,...); // 如何调用 } 谢谢
]]>背景是这样的:最近闲着报了个深圳人社的 c 语言培训班。 完了之后想要把之前自己写的 python 图片处理脚本用 C 写一遍。 然后就发现没法引用第三方库(也许是自己哪里做错了)。
以使用第三方库 fmt 为例,按照下述操作之后,在测试文件 test.c 里面输入 #include <f vscode 能自动联想出#include <fmt/format.h> ,但是在 debug 或 run without debug 时终端会报错以下内容 :
#include errors detected. Please update your includePath. Squiggles are disabled for this translation unit (C:\Users\xxx\Desktop\C\test.c).
cannot open source file "cmath" (dependency of "C:\Users\xxx.vscode\extensions\ms-vscode.cpptools-1.20.5-win32-x64\bin\fmt\format.h"). Please run the 'Select IntelliSense Configuration...' command to locate your system headers.
fmt/format.h: No such file or directory
gcc/gdb 都已经正常安装并且可以在终端正常使用; 其中,gcc 路径为:C:\MinGW\bin\gcc.exe ; gdb 版本为 GNU gdb (GDB) 7.6.1 ;
包管理用的是 vcpkg; 安装到包的路径是:C:\vcpkg\installed\x64-windows\include
vscode 装了插件:C/C++ v1.20.5
在 google 操作之后下来,之后相关文件内容如下:
c_cpp_properties.json:
{ "configurations": [ { "name": "Win32", "includePath": [ "${workspaceFolder}/**", "C:/vcpkg/installed/x64-windows/include", "C:/MinGW/include" ], "defines": [], "windowsSdkVersion": "10.0.18362.0", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "windows-gcc-x64" } ], "version": 4 } tasks.json:
{ "version": "2.0.0", "tasks": [ { "type": "shell", "label": "gcc build active file", "command": "gcc", "args": [ "-g", "${file}", "-I", "C:/vcpkg/installed/x64-windows/include", "-L", "C:/vcpkg/installed/x64-windows/lib", "-lfmt", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ], "group": "build" }, { "type": "cppbuild", "label": "C/C++: gcc.exe build active file", "command": "C:\\MinGW\\bin\\gcc.exe", "args": [ "-fdiagnostics-color=always", "-g", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build", "isDefault": true }, "detail": "Task generated by Debugger." } ] } launch.json:
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "(gdb) Attach", "type": "cppdbg", "request": "attach", "program": "${workspaceRoot}/${fileBasenameNoExtension}.exe", "MIMode": "gdb", "miDebuggerPath": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true }, { "description": "Set Disassembly Flavor to Intel", "text": "-gdb-set disassembly-flavor intel", "ignoreFailures": true } ] } ] } 测试脚本
#include <fmt/format.h> int main(void){ int a = 1; fmt::print("a is {}", a); return 0; } ]]>chmod +x embed_file_c.sh ./embed_file_c.sh dir2c_test # or ./embed_file_c.sh dir2c /path/to/resource > _data_enc.c gcc embed_file.c -o embed_file ./embed_file #include <stdio.h> #include <stdint.h> int main(int argc, char *argv[]) { #define _DATA_ENC_INC 1 #include "_data_enc.c" for (int i = 0; i < sizeof(_data_enc_lst)/sizeof(_data_enc_lst[0]); ++i) { printf("%3u, %3u, %s\n", _data_enc_lst[i].off, _data_enc_lst[i].len, _data_enc_lst[i].path); // &_data_enc[_data_enc_lst[i].off] is the data ptr, use it by need } return 0; } 代码 https://github.com/webd90kb/webd/tree/master/codes/scripts/embed_file_c
]]>EXE=\ exe1\ a/exe2\ a/exe3\ OBJ=\ mod1\ liba/mod2\ liba/mod3\ include inc.mak 工程源码 https://github.com/webd90kb/webd/tree/master/codes/c_project_template
]]>一万年前写了个C 语言跨平台小工具库,提到一开始是用 C++写的,后来改成了 C99 。结果现在又搞了一个 C++版本( C 版本还保留且两个版本同步)。代码更新了不少,也重新组织了,文档从 README 改成了 doxygen (但是不知道要怎么导入 github wiki )。主要功能有:
其实是一个很个人的工具库,平时工作用到什么就加什么进去。近期在玩 cuda ,所以也加了点 cuda 的东西。
]]>unsigned int x1 = 0xaeb1c2aa; unsigned int x2 = 0xaeb1c2aa; long long r_whole = (signed long long)x1 * (signed long long)x2; printf("r_whole = %lld\n",r_whole); 输出的结果是无符号乘法的结果:
r_whole = 8590088583138384100 而下面的程序
unsigned int x1 = 0xaeb1c2aa; unsigned int x2 = 0xaeb1c2aa; int xx1 = x1; int xx2 = x2; long long r_whole = (signed long long)xx1 * (signed long long)xx2; printf("r_whole = %lld\n",r_whole); 输出的事有符号乘法的结果:
r_whole = 1860719719092984036 第一段程序里我不是对 x1 进行强制类型转换了吗
]]>#include <stdio.h> int main() { printf("Hello, World\n"); } 使用如下命令编译:
riscv64-linux-gnu-gcc --static hello.c -o hello_riscv64 居然可以直接运行
> ./hello_riscv64 Hello, World 使用 qemu 也能运行:
> qemu-riscv64-static hello_riscv64 Hello, World readelf 结果:
> readelf -h hello_riscv64 ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: RISC-V Version: 0x1 Entry point address: 0x1054c Start of program headers: 64 (bytes into file) Start of section headers: 501880 (bytes into file) Flags: 0x5, RVC, double-float ABI Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 7 Size of section headers: 64 (bytes) Number of section headers: 28 Section header string table index: 27 ]]>MinGW 11.0 w64 下面的代码使用 chatgpt 加了注释,主要的问题是:
在直接给定输入
8 1 9 2 6 0 8 1 7 时,结果为 0.
而再次给定输入
8 1 9 2 6 90 8 1 7 时,结果还是 0.
而手动一个一个输入则正常显示 1.
虽然描述的很离奇,但是确实是这样,即:第一次的结果会影响第二次的结果,使之变成第一次的结果
我问了我们教 C 语言的老师,连他也不知道怎么回事,说可能是 CLion 的问题,但是这个答案并不能令我信服,
故来寻求各位 V 友帮助
求各位 V 友解答 QAQ
代码如下:
#include <stdio.h> int main() { int n; scanf("%d", &n); // 从标准输入中读取一个整数,存储到变量 n 中 int min; scanf("%d", &min); // 假设输入的第一个数为最小值,存储到变量 min 中 int num; for (int i = 1; i < n; i++) { // 循环读取剩余的 n-1 个整数 scanf("%d", &num); // 从标准输入中读取一个整数,存储到变量 num 中 printf("%d\n", num); // 将读取的整数打印到标准输出,以换行符结束 if (num < min) { // 检查当前读取的整数是否比最小值小 min = num; // 更新最小值为当前读取的整数 } } printf("%d", min); // 打印最小值到标准输出 return 0; // 返回 0 表示程序正常结束 } ]]>#include<stdio.h> int main(){ int *p = (int *)0x1; printf("%p\n", p); *p = 1; // segmentation fault printf("%d", *p); // segmentation fault } ]]>int main() { system("echo hello world"); } 使用 gcc 编译时会警告找不到 system 的声明:
DoubleHash.c: In function ‘main’: DoubleHash.c:3:5: warning: implicit declaration of function ‘system’ [-Wimplicit-function-declaration] 3 | system("echo hello world"); | ^~~~~~ 但是程序仍然可以运行,我想知道编译器是怎么找到 system 的定义的?
]]>下面是该组件支持的语法:
abc --这是一个变量 "abc" --这是一个字符串常量 'abc' --这也是字符串常量 1 --整数 1.2 --浮点数 0xa --十六进制整数 0311 --八进制整数 concat(abc, bcd) --这是一个函数,参数有两个,都是变量 concat(abc, "bcd") --这是一个函数,参数有两个,一个是变量,一个是常量 concat(1, "bcd") --两个参数都是常量 concat("abc", concat(bcd, "efg")) --这个例子展示了函数嵌套调用 concat("abc", concat(bcd, "efg")) aaa concat("bcd", concat(efg, "hij")) --这个例子展示运行多个表达式 简单来说,表达式语法支持三种类型内容:
并且函数支持嵌套调用。
此外,可以一次执行多个表达式,所有表达式的执行结果为最后一个表达式的结果。
我们分别对着三种类型分别说明:
这个比较好理解,就是字面量,主要支持:字符串、整数和浮点数。其中,整数支持十进制写法、八进制写法和十六进制写法。
变量顾名思义,就是其值可变。但由于表达式比较简单,且应用场景与常规编程语言不同,因此不是通过=来进行赋值的,而是通过回调函数,由使用者决定该返回何值作为该变量的值。
与变量一样,函数的行为完全由回调函数决定,也就是说由使用者自行定制。
我们看一个示例
#include "mln_expr.h" #include "mln_log.h" #include <stdio.h> static mln_expr_val_t *func_expr_handler(mln_string_t *name, int is_func, mln_array_t *args, void *data) { mln_expr_val_t *v, *p; int i; mln_string_t *s1 = NULL, *s2, *s3; if (!is_func) return mln_expr_val_new(mln_expr_type_string, name, NULL); for (i = 0, v = p = mln_array_elts(args); i < mln_array_nelts(args); v = p + (++i)) { if (s1 == NULL) { s1 = mln_string_ref(v->data.s); continue; } s2 = v->data.s; s3 = mln_string_strcat(s1, s2); mln_string_free(s1); s1 = s3; } v = mln_expr_val_new(mln_expr_type_string, s1, NULL); mln_string_free(s1); return v; } int main(void) { mln_string_t func_exp = mln_string("concat('abc', concat(aaa, 'bbb')) ccc concat('eee', concat(bbb, 'fff'))"); mln_expr_val_t *v; v = mln_expr_run(&func_exp, func_expr_handler, NULL); if (v == NULL) { mln_log(error, "run failed\n"); return -1; } mln_log(debug, "%d %S\n", v->type, v->data.s); mln_expr_val_free(v); return 0; } 这是一个综合一点的例子,这个例子中包含了函数嵌套调用、变量、多表达式执行。
表达式中的变量和函数都由func_expr_handler这个回调函数来解析。对于变量,回调函数直接返回变量的名字作为变量的值。而对于函数,回调函数则是将函数参数拼接成一个字符串作为函数的返回值。
由于本例中存在三个表达式:
concat('abc', concat(aaa, 'bbb'))cccconcat('eee', concat(bbb, 'fff'))前面我们说到过,mln_expr_run的返回值是最后一个表达式的值,所以最终终端的输出就是:
eeebbbfff 也正如这个例子所示,表达式组件只是提供了一种对文本格式的规范,而具体有哪些函数和变量都完全交给回调函数来决定,也就是交给了使用者决定。并且这个组件并不像完整的编程语言那样功能繁重,因此比较适合一些小型功能整合或者模板替换之类的场景。
模板替换可以参考 web 前端的那些模板,例如:twig 、mustache 等。
小功能整合举个例子,例如在对某种网络通信中,我们需要对报文提取某些字段,然后对字段处理,然后再做验证。那么提取、处理、验证就可以被封装成三个函数。这三个函数是三种行为,而不是策略。我们可以将这三种行为应设成表达式组件中的三个函数,然后我们就可以通过对这三个函数的组合应用来实现策略。对于策略的改变,我们并不需要修改 C 代码,只需要将策略的文本内容(也就是这些表达式)做一些修改即可。
感谢阅读!
]]>#include <stdio.h> int main() { char s[7] = "七 123"; int i; for (i = 0; i < 7; i++) { printf("%d,", s[i]); } } 打印出来的结果是:-28,-72,-125,49,50,51,0,
前面的-28,-72,-125 正是七的 utf8 编码
要是我想打印出七的 gbk 编码应该怎么办?
它这里的程序是指单个应用还是指单个 C 语言文件呀?如果是单个应用,怎么说多个程序共享呢;如果是说单个 C 文件,那所谓的共享又是指啥呢;反正我横竖都弄不清,只知道都能用。(我个人背景是上层 Android 开发,使用的时候感觉没啥太大区别,当然了静态库快、动态库慢这个我能理解,像空间节省这个我没能理解)
]]>关于 bfdev 库,这是一个开源的 C 语言算法库, 它具有:良好的可移植性,面向对象的方法设计、安装部署简单等等优势。
MPI 即 Multi precision integer (多精度整数),就是对很大的数进行一系列的运算。在数学中,数的大小是没有上限的,但是在计算机中,由于受 ALU 字长的限制,处理器无法对其进行直接计算,我们就需要用到多精度整数算法。
我们先给出两个平台基于 MPI 使用 machin 公式 计算圆周率的测试代码,再进行说明。
#define MODULE_NAME "mpi-machin" #define bfdev_log_fmt(fmt) MODULE_NAME ": " fmt #include <stdio.h> #include <bfdev/mpi.h> #include <bfdev/log.h> #include "../time.h" #include "helper.h" #define TEST_LEN 10000 #define TEST_SIZE (TEST_LEN / 4 + 1) #define TEST_LOOP (TEST_LEN / 1.39793 + 1) #define PRINT_RESULT 1 int main(int argc, const char *argv[]) { bfdev_mpi_t *vw, *vs, *vv, *vq; unsigned int k; int retval; if (!((vw = bfdev_mpi_create(NULL)) && (vs = bfdev_mpi_create(NULL)) && (vv = bfdev_mpi_create(NULL)) && (vq = bfdev_mpi_create(NULL)))) return 1; /** * Machin-like formula: * PI = 16arctan(1/5) - 4arctan(1/239) * * These formulas are used in conjunction with Gregory's * series, the Taylor series expansion for arctangent: * arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 + ... */ if ((retval = bfdev_mpi_seti(vw, 16 * 5)) || (retval = bfdev_mpi_seti(vv, 4 * 239)) || (retval = bfdev_mpi_seti(vq, 10000))) return retval; for (k = 0; k < TEST_SIZE; ++k) { if ((retval = bfdev_mpi_mul(vw, vw, vq)) || (retval = bfdev_mpi_mul(vv, vv, vq))) return retval; } bfdev_log_info("Convergence Machin %d:\n", TEST_LEN); EXAMPLE_TIME_STATISTICAL( for (k = 1; k <= TEST_LOOP; ++k) { if ((retval = bfdev_mpi_divi(vw, vw, vw, 5 * 5)) || (retval = bfdev_mpi_divi(vv, vv, vv, 239 * 239)) || (retval = bfdev_mpi_sub(vq, vw, vv)) || (retval = bfdev_mpi_divi(vq, vq, vq, 2 * k - 1))) return retval; if (k & 1) retval = bfdev_mpi_add(vs, vs, vq); else retval = bfdev_mpi_sub(vs, vs, vq); if (retval) return retval; } 0; ); #if PRINT_RESULT char *result; result = print_num(vs, 10); if (!result) return 1; printf("%c.", *result); puts(result + 1); free(result); #endif bfdev_mpi_destory(vw); bfdev_mpi_destory(vs); bfdev_mpi_destory(vv); bfdev_mpi_destory(vq); return 0; } 以下是运算 pi 后 10000 位的结果,共使用了 0.36 秒。
❯ ./build/examples/mpi/mpi-machin [info] mpi-machin: Convergence Machin 10000: [info] mpi-machin: real time: 0.360000 [info] mpi-machin: user time: 0.350000 [info] mpi-machin: kern time: 0.000000 3.14159 ... #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <errno.h> #include <bfdev.h> #include "main.h" #include "py32f0xx_hal.h" #define TEST_LEN 100 #define TEST_SIZE (TEST_LEN / 4 + 1) #define TEST_LOOP (TEST_LEN / 1.39793 + 1) int mpi_benchmark(void) { uint32_t start, time; bfdev_mpi_t *vw, *vs, *vv, *vq; unsigned int k; int retval; if (!((vw = bfdev_mpi_create(NULL)) && (vs = bfdev_mpi_create(NULL)) && (vv = bfdev_mpi_create(NULL)) && (vq = bfdev_mpi_create(NULL)))) return 1; if ((retval = bfdev_mpi_seti(vw, 16 * 5)) || (retval = bfdev_mpi_seti(vv, 4 * 239)) || (retval = bfdev_mpi_seti(vq, 10000))) return retval; for (k = 0; k < TEST_SIZE; ++k) { if ((retval = bfdev_mpi_mul(vw, vw, vq)) || (retval = bfdev_mpi_mul(vv, vv, vq))) return retval; } bfdev_log_info("Convergence Machin %d:\n", TEST_LEN); start = HAL_GetTick(); for (k = 1; k <= TEST_LOOP; ++k) { if ((retval = bfdev_mpi_divi(vw, vw, vw, 5 * 5)) || (retval = bfdev_mpi_divi(vv, vv, vv, 239 * 239)) || (retval = bfdev_mpi_sub(vq, vw, vv)) || (retval = bfdev_mpi_divi(vq, vq, vq, 2 * k - 1))) return retval; if (k & 1) retval = bfdev_mpi_add(vs, vs, vq); else retval = bfdev_mpi_sub(vs, vs, vq); if (retval) return retval; iwdg_touch(); } bfdev_mpi_destory(vw); bfdev_mpi_destory(vs); bfdev_mpi_destory(vv); bfdev_mpi_destory(vq); iwdg_touch(); time = HAL_GetTick() - start; bfdev_log_notice("Total time: %lu.%lus\n", time / 1000, time % 1000); return 0; } 以下是运算结果,由于 py32f030 只有 4KiB RAM ,我们这里只计算 100 位。
[info] benchmark: Benchmark for PY32F0xx. [info] benchmark: Bfdev version: 1.0.1-devel [info] benchmark: This may take a few minutes... [info] Convergence Machin 100: [notice] Total time: 0.21s ... 我们计算的大体流程如下:
bfdev 中,使用 MPI 需要引入 <bfdev/mpi.h> 头文件,或者引入 <bfdev.h> 顶层头文件也可以。
这里要重点介绍一下 bfdev_mpi_create(alloc) 函数,它使用了 bfdev 的 allocator 兼容层,传入 NULL 将使用系统默认内存分配器,用户也可以使用自己的内存池为 MPI 分配内存。
欢迎各位感兴趣的读者去访问 bfdev 仓库。
]]>首先给出一段由 ChatGPT 给出的简短的 AOP 概念:
AOP 是一种编程方法,用来将在程序中多处重复出现的代码(比如日志、权限控制)从主要业务逻辑中抽取出来,提高代码的模块化和可维护性。
抽取后的代码会在原始的业务逻辑代码中特定的位置执行,这些位置由切点( Pointcut )定义。通常会在方法执行前、执行后、抛出异常时等特定点执行抽取出的代码,这些点被称为连接点( Join Point )。
在 C 语言中,编译器所提供的编译期和执行期的能力相较于 java 或者其他语言来说会弱一些,这也许就是可能很少听到在 C 语言中搞面向切面编程的原因之一吧。
从上面的概念上来看,AOP 一般是在一些函数(或类方法)执行前后做一些额外处理,例如调用前增加一些权限控制,调用后增加一些日志记录。从这些行为上来说,任何语言其实都可以做到。我们可以简单的在一个函数的开始加一段逻辑或调用某个函数来实现权限验证,在函数返回前调用某个函数添加日志等等。类似如下代码:
void foo(void) { if (!verify_identity()) return; //... log("end"); } 但很显然,这么做会在程序的很多个函数中添加很多重复的代码(例如本例的verify_identity和log),以至于代码变得比较臃肿。
那么有没有什么办法来瘦身呢?
这就是 AOP 擅长的领域了。
C 语言编译器没有提供很完整的 AOP 支持,因此我们需要自行手动实现,或者使用一些现有的库来实现。
本文将使用开源 C 语言库 Melon 的函数模板来实现上面的效果。
在 Melon 提供的函数模板组件中,实现了若干宏函数,这些宏函数都是用来定义不同类型的函数的。这些用宏来定义的函数和我们原生 C 语言中的函数的区别,简单来说就是,在我们实际要执行的函数逻辑外,再封装一个函数,这个函数会在我们指定的函数逻辑开始前和结束后调用一个回调函数(即函数的入口回调函数和出口回调函数)。
基于函数模板的这一特性,Melon 中实现了一个 span 组件,用来度量使用函数模板定义的函数的时间开销。
但如果事情仅限于此,那么这种 AOP 很显然能做到的事情也基本仅限于此了。
因此,Melon 支持了 c99 ,并利用 c99 提供的宏特性,实现了将函数模板定义的函数的实参以可变参数的形式传递到入口和出口回调函数中。这就意味着,入口和出口回调函数可以访问函数的参数,并对参数的内容作出修改(主要针对指针指向的内存中的数据)。
这样,就给我们在回调函数中提供了更多的可操作空间。我们可以针对不同的函数,修改其参数值,从而来影响后续函数调用中的执行逻辑。例如前面的权限验证,我们可以将其大致简化为如下形式:
void entry_callback(char *file, char *func, int line, ...) { va_list args; va_start(args, line); int *a = (int *)va_arg(args, int *); va_end(args); if (!verify_identity()) *a = 0; } void exit_callback(char *file, char *func, int line, ...) { va_list args; va_start(args, line); int *a = (int *)va_arg(args, int *); va_end(args); log("%d\n", *a); } void foo(int *a) { if (!*a) return; //... } int bar(int *d, int e) { if (!*d) return -1; //... return 0; } 这里的代码只是一个示意,后面会给出一个实际可用的示例。
我们可以随意增加函数,这些函数都会利用同一对入口和出口函数来实现身份验证。
下面就给出一个可用的使用函数模板实现 AOP 的 C 语言代码。
//a.c #include "mln_func.h" #include <stdio.h> #include <string.h> #include <stdarg.h> MLN_FUNC_VOID(static, void, foo, (int *a, int b), (a, b), { printf("in %s: %d\n", __FUNCTION__, *a); *a += b; }) MLN_FUNC(static, int, bar, (void), (), { printf("%s\n", __FUNCTION__); return 0; }) static void my_entry(const char *file, const char *func, int line, ...) { if (strcmp(func, "foo")) return; va_list args; va_start(args, line); int *a = (int *)va_arg(args, int *); va_end(args); printf("entry %s %s %d %d\n", file, func, line, *a); ++(*a); } static void my_exit(const char *file, const char *func, int line, ...) { if (strcmp(func, "foo")) return; va_list args; va_start(args, line); int *a = (int *)va_arg(args, int *); va_end(args); printf("exit %s %s %d %d\n", file, func, line, *a); } int main(void) { int a = 1; mln_func_entry_callback_set(my_entry); mln_func_exit_callback_set(my_exit); foo(&a, 2); return bar(); } 这段函数中,我们使用MLN_FUNC和MLN_FUNC_VOID来定义了两个函数,即foo和bar。两个函数的逻辑很简单,就是 printf 输出当前函数名以及参数值(如果有参数的话)。同时,我们也使用了mln_func_entry_callback_set和mln_func_exit_callback_set定义了两个全局回调函数,用来在函数调用开始和结束时调用。
我们可以看到,回调函数中使用strcmp对进入回调的函数做了过滤,仅对foo函数做额外处理。在入口回调中输出函数信息及第一个参数的值,随后修改参数指针指向的内存中的值。在出口回调中输出函数信息和参数值。
我们来编译一下(我们假定这个代码文件名为a.c):
cc -o a a.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -std=c99 -DMLN_C99 -DMLN_FUNC_FLAG 这里:
/usr/local/melon是 Melon 库的默认安装路径。-std=c99是启用 c99 。-DMLN_C99是定义一个名为MLN_C99的宏,这个宏用来启用函数模板组件中 C99 下才有的特性。-DMLN_FUNC_FLAG用来定义一个名为MLN_FUNC_FLAG的宏,这个宏用来启用函数模板功能。是的,如果没有这个宏,上面的那些使用MLN_FUNC定义的函数就是普通的 C 语言函数,也不会触发入口和出口回调函数的调用。执行一下看看效果:
entry a.c foo 6 1 in __mln_func_foo: 2 exit a.c foo 6 4 __mln_func_bar 可以看到:
1。foo的实际函数逻辑中,printf 输出当前的函数名为__mln_func_foo,以及此时看到的第一个参数的值为2,不再是1了,因为在入口回调函数中被修改了。__mln_func_foo这个函数执行的才是我们定义的逻辑,而foo是对__mln_func_foo的一个封装。4,因为它在我们给出的函数逻辑中做了修改。bar的实际执行逻辑所在的函数名,与foo的形式一致。最后,我们去掉MLN_FUNC_FLAG这个宏再次编译一次:
cc -o a a.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -std=c99 -DMLN_C99 然后执行一下看看输出结果:
in foo: 1 bar 可以看得出,此时foo和bar不再是封装函数,而是我们定义的函数逻辑的函数名,即普通的 C 语言函数。
读到这里的都是真爱,感谢阅读!
]]>一开始我实在是莫名其妙,位运算这么简单具体的操作,哪来的“抽象”?
然后再看评论区、回答区,才明白原来提问人的“抽象”竟然是网络语言的语境,而不是专业术语的语境。
这次“抽象”二字的用法可真够抽象的。
(严谨的备注:最后一个“抽象”是网络用语)
今天,我们介绍 Melon 中的span组件,使用它来轻松监控函数的调用耗时情况。
Melon 中的资源开销( span )组件是用来测量 C 语言函数开销的,这个模块需要配合函数模板模块一同使用,因此也需要定义MLN_FUNC_FLAG才会使得函数模板功能启用,进而实现函数开销的跟踪。
目前支持的开销如下:
#include "mln_span.h" span
在 Melon 中支持模块选择性编译,因此可以选择指定的模块进行编译,脚本将自行计算该模块所依赖的 Melon 中的其他模块并一同进行编译。
下面我们一起看一个多线程的示例,用来展示mln_span接口的使用以及在多线程环境下的效果。
//a.c #include <pthread.h> #include "mln_span.h" #include "mln_func.h" MLN_FUNC(int, abc, (int a, int b), (a, b), { return a + b; }) MLN_FUNC(static int, bcd, (int a, int b), (a, b), { return abc(a, b) + abc(a, b); }) MLN_FUNC(static int, cde, (int a, int b), (a, b), { return bcd(a, b) + bcd(a, b); }) void *pentry(void *args) { int i; mln_span_start(); for (i = 0; i < 10; ++i) { cde(i, i + 1); } mln_span_stop(); mln_span_dump(); mln_span_release(); return NULL; } int main(void) { int i; pthread_t pth; pthread_create(&pth, NULL, pentry, NULL); for (i = 0; i < 10; ++i) { bcd(i, i + 1); } pthread_join(pth, NULL); return 0; } 前面我们说过,span 组件需要配合函数模板组件一同使用。这里我们使用函数模板组件定义了三个函数abc, bcd, cde。
然后我们在main函数中启动一个线程,并在线程入口函数内,调用mln_span_start开启资源消耗跟踪。然后调用cde函数十次。而在main函数中,在创建线程后,循环调用bcd函数十次,最后等待线程退出,程序结束。
我们对这段程序进行编译:
cc -o a a.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -DMLN_FUNC_FLAG -lpthread 注意,这里-DMLN_FUNC_FLAG是用来启用函数模板模块的功能。如果不定义这个宏,那么使用MLN_FUNC定义的函数就是普通 C 语言函数,不会启用任何跟踪能力。
编译好后运行程序,可以看到类似如下的输出:
| pentry at a.c:20 takes 92 (us) | cde at a.c:13 takes 4 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 5 (us) | bcd at a.c:9 takes 0 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 2 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 24 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 21 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 5 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 3 (us) | bcd at a.c:9 takes 2 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 30 (us) | bcd at a.c:9 takes 24 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 1 (us) | bcd at a.c:9 takes 6 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 3 (us) | bcd at a.c:9 takes 2 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 3 (us) | bcd at a.c:9 takes 2 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 1 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | cde at a.c:13 takes 7 (us) | bcd at a.c:9 takes 1 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 2 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 1 (us) | cde at a.c:13 takes 3 (us) | bcd at a.c:9 takes 2 (us) | abc at a.c:5 takes 1 (us) | abc at a.c:5 takes 0 (us) | bcd at a.c:9 takes 0 (us) | abc at a.c:5 takes 0 (us) | abc at a.c:5 takes 0 (us) 感兴趣的小伙伴欢迎访问Melon 的 Github进行试用。Melon 是一个跨平台的 C 语言库,内含多种数据结构、算法和常用组件。Melon 并不是一个纯粹的数据结构和算法库,而是致力于提供开发中常用的组件,如:内存池、线程池、多进程框架、可观测性等。Melon 提供中英文文档,内含各模块接口说明和示例代码,便于开发者快速上手。
感谢阅读!
]]>本文主要介绍的是 Melon 库中的func模块,之所以没有给这个模块起名叫可观测性或者span,原因是这是一个更为通用的模块,不仅限于可观测性的需求。
func模块实现的功能与 GCC 的 constructor 和 destructor 特性十分相似,就是在 C 语言函数的入口和出口增加用户自定义回调函数,在调用函数时自行调用这些函数。
我们先看一个简单的例子:
// a.c #include "mln_func.h" MLN_FUNC(int, abc, (int a, int b), (a, b), { printf("in %s\n", __FUNCTION__); return a + b; }) MLN_FUNC(static int, bcd, (int a, int b), (a, b), { printf("in %s\n", __FUNCTION__); return abc(a, b) + abc(a, b); }) static void my_entry(const char *file, const char *func, int line) { printf("entry %s %s %d\n", file, func, line); } static void my_exit(const char *file, const char *func, int line) { printf("exit %s %s %d\n", file, func, line); } int main(void) { mln_func_entry_callback_set(my_entry); mln_func_exit_callback_set(my_exit); printf("%d\n", bcd(1, 2)); return 0; } 这段代码中,使用MLN_FUNC定义了两个函数,分别为abc和bcd,且在bcd中会调用abc。其实这个模板宏相对比较容易理解,其宏函数参数顺序如下:
static)()扩住)()扩住)这里唯一有些困惑的是实参列表,这与宏的实现有关。我们以abc为例,简述一下实现原理。
原理:这个宏会定义两个函数,一个名为abc,一个名为__abc。函数体其实对应的是__abc,也就是说__abc才是真正我们期望调用的那个函数,而abc是对__abc的一个封装,会在__abc的调用前后调用自定义回调函数。
而实参列表就是在函数abc中调用__abc时需要给__abc传递的参数,所以这个参数列表其实就是形参列表去掉类型之后的名字和顺序。
这个实参列表无法忽略,是因为
__abc不能省略,而__abc不能省略是因为函数体中可能包含 return 语句,因此我们无法完全隐式地在 return 前,甚至是在 return 的表达式计算后真正的返回前调用回调函数。所以必须单独定义成一个函数也就是__abc。
下面我们来编译这个程序:
cc -o a a.c -I /path/to/melon/include -L /path/to/melon/lib -lmelon 其中/path/to/melon的部分是 Melon 的安装路径,默认一般是/usr/local/melon。
然后运行一下
./a in bcd in abc in abc 6 你会发现回调函数完全没被调用。这不是我们的代码有问题,而是我们并未启用模板功能。模板启用需要编译时存在MLN_FUNC_FLAG的宏定义,我们既可以将它定义在源文件中,也可以在编译时作为命令行参数给出。下面我以后者为例展示:
cc -o a a.c -I /path/to/melon/include -L /path/to/melon/lib -lmelon -DMLN_FUNC_FLAG 再次运行
./a entry a.c bcd 10 in __bcd entry a.c abc 5 in __abc exit a.c abc 5 entry a.c abc 5 in __abc exit a.c abc 5 exit a.c bcd 10 6 可以看到,回调函数都被正常调用了。
利用这个开关宏,我们可以在不修改任何代码的情况下,轻松切换是否需要开启这项功能。
前面给出的例子比较简单,那么下面就来看一个实现测量函数调用耗时的例子吧。
这里我将给出三个文件:
span.h:这是为测量耗时所定义的数据结构和函数声明等内容。span.c:这是为测量耗时定义的相关函数。a.c:这是我们自定义的一些函数以及在main函数中调用这些函数。其中,span.h和span.c可以随意复制粘贴使用,这是一个独立的模块,当然,你还需要先安装好 Melon 库。
#include <sys/time.h> #include "mln_array.h" typedef struct mln_span_s { struct timeval begin; struct timeval end; const char *file; const char *func; int line; mln_array_t subspans; struct mln_span_s *parent; } mln_span_t; extern int mln_span_start(void); extern void mln_span_stop(void); extern void mln_span_dump(void); extern void mln_span_release(void); 这里定义了一个数据结构mln_span_t,用来存放函数调用的起始和结束时的时间戳,以及函数所在源文件的信息。还包含了这个函数中调用的其他函数的调用时长信息,以及一个指向上一级调用(也就是调用当前函数的函数)信息的指针。
也就是说,当我们的函数执行完毕后,我们遍历这个结构就能拿到完整的调用关系及其调用细节。
#include <stdlib.h> #include <string.h> #include "span.h" #include "mln_stack.h" #include "mln_func.h" static mln_stack_t *callstack = NULL; static mln_span_t *root = NULL; static void mln_span_entry(const char *file, const char *func, int line); static void mln_span_exit(const char *file, const char *func, int line); static mln_span_t *mln_span_new(mln_span_t *parent, const char *file, const char *func, int line); static void mln_span_free(mln_span_t *s); static mln_span_t *mln_span_new(mln_span_t *parent, const char *file, const char *func, int line) { mln_span_t *s; struct mln_array_attr attr; if (parent != NULL) { s = (mln_span_t *)mln_array_push(&parent->subspans); } else { s = (mln_span_t *)malloc(sizeof(mln_span_t)); } if (s == NULL) return NULL; memset(&s->begin, 0, sizeof(struct timeval)); memset(&s->end, 0, sizeof(struct timeval)); s->file = file; s->func = func; s->line = line; attr.pool = NULL; attr.pool_alloc = NULL; attr.pool_free = NULL; attr.free = (array_free)mln_span_free; attr.size = sizeof(mln_span_t); attr.nalloc = 7; if (mln_array_init(&s->subspans, &attr) < 0) { if (parent == NULL) free(s); return NULL; } s->parent = parent; return s; } static void mln_span_free(mln_span_t *s) { if (s == NULL) return; mln_array_destroy(&s->subspans); if (s->parent == NULL) free(s); } int mln_span_start(void) { struct mln_stack_attr sattr; mln_func_entry_callback_set(mln_span_entry); mln_func_exit_callback_set(mln_span_exit); sattr.free_handler = NULL; sattr.copy_handler = NULL; if ((callstack = mln_stack_init(&sattr)) == NULL) return -1; return 0; } void mln_span_stop(void) { mln_func_entry_callback_set(NULL); mln_func_exit_callback_set(NULL); mln_stack_destroy(callstack); } void mln_span_release(void) { mln_span_free(root); } static void mln_span_format_dump(mln_span_t *span, int blanks) { int i; mln_span_t *sub; for (i = 0; i < blanks; ++i) printf(" "); printf("| %s at %s:%d takes %lu (us)\n", \ span->func, span->file, span->line, \ (span->end.tv_sec * 1000000 + span->end.tv_usec) - (span->begin.tv_sec * 1000000 + span->begin.tv_usec)); for (i = 0; i < mln_array_nelts(&(span->subspans)); ++i) { sub = ((mln_span_t *)mln_array_elts(&(span->subspans))) + i; mln_span_format_dump(sub, blanks + 2); } } void mln_span_dump(void) { if (root != NULL) mln_span_format_dump(root, 0); } static void mln_span_entry(const char *file, const char *func, int line) { mln_span_t *span; if ((span = mln_span_new(mln_stack_top(callstack), file, func, line)) == NULL) { fprintf(stderr, "new span failed\n"); exit(1); } if (mln_stack_push(callstack, span) < 0) { fprintf(stderr, "push span failed\n"); exit(1); } if (root == NULL) root = span; gettimeofday(&span->begin, NULL); } static void mln_span_exit(const char *file, const char *func, int line) { mln_span_t *span = mln_stack_pop(callstack); if (span == NULL) { fprintf(stderr, "call stack crashed\n"); exit(1); } gettimeofday(&span->end, NULL); } 这里就是耗时统计所需要的所有函数定义。利用一个栈数据结构来保证函数的调用关系,然后在函数的入口回调处创建mln_span_t结点记录起始时间和函数信息并入栈,在出口回调处记录结束时间并出栈。
#include "span.h" #include "mln_func.h" MLN_FUNC(int, abc, (int a, int b), (a, b), { return a + b; }) MLN_FUNC(static int, bcd, (int a, int b), (a, b), { return abc(a, b) + abc(a, b); }) int main(void) { mln_span_start(); bcd(1, 2); mln_span_stop(); mln_span_dump(); mln_span_release(); return 0; } 这里还是那个配方,就是调用bcd,然后bcd调用abc。我们这次在main函数中使用span.h中声明的函数。
一起来简单编译一下:
cc -o a span.c a.c -I /usr/local/melon/include -L /usr/local/melon/lib -lmelon -DMLN_FUNC_FLAG 然后运行一下:
./a | bcd at a.c:8 takes 2 (us) | abc at a.c:4 takes 0 (us) | abc at a.c:4 takes 0 (us) Melon的函数模板其实设计之初也是为了可观测性,因为 GCC 仅支持了 constructor 和 destructor 。如果显式地在代码中加入各种跟踪函数调用,就会让整个函数定义看着非常不连贯和杂乱。因此选择了当前的这个使用方式,但也不可避免的引入了看似没什么用途的实参部分。
另外,Melon 库支持模块选择性编译,因此函数模版模块可以单独编译成库,换言之,这个模块是完全无操作系统依赖的,单片机的小伙伴们可以随意取用。
感谢阅读!
]]>typedef void (*myown_call)(void); #define func_init(func) \ __attribute__((section("myown"), aligned(__alignof__(myown_call)))) \ myown_call _fn_##func = func extern myown_call __start_myown; extern myown_call __stop_myown; void do_initcalls(void) { myown_call *pfun = &__start_myown; do { (*pfun)(); ++pfun; } while (pfun < &__stop_myown); } 上面的代码是实现初始化逻辑的,我在网上看到要想实现自定义的段数据加载,需要在链接脚本中指定__start_myown 和__stop_myown 的地址位置,但是在代码的编译过程中没有看到相关的设置,不太清楚__start_myown 、__stop_myown 的地址编译器是如何分配的,又懂得大佬可以给解释下么?
]]>意思是最好在
CURLOPT_WRITEFUNCTION CURLOPT_READFUNCTION CURLOPT_PROGRESSFUNCTION
使用么?有没有大佬用过,或者有相关例子的(搜索了很多,没找到)
]]>PROJECT-ROOT │ CMakeLists.txt │ ├─app │ │ CMakeLists.txt │ │ │ ├─include │ │ misc.h │ │ │ └─src │ main.cpp │ └─lib-example │ CMakeLists.txt │ ├─include │ lib.h │ └─src lib.cpp app 是可执行文件,lib-example 是动态库。app 这个项目需要动态链接到 lib-example,并且使用 lib-example 的头文件。网上有资料介绍类似的用法,我拿过来改了一下,没搞成功。
]]>ps: 1.如果有点汇编也行,我也想再学学汇编 2.物联网相关的也行,单片机方向 3.nginx 就算了,一口吃不了个胖子,希望能有个总体的代码量少点的。
]]>typedef struct { int width; int height; int depth; } INFO_T; int Send(int infoNum, const INFO_T *pInfo[]) { INFO_T tInfo; for (int m = 0; m < infoNum; m++) { memcpy(&tInfo, &pInfo[m], sizeof(INFO_T)); printf("info=%d \n", tInfo.width); //这条语句是正常的 printf("info=%d \n", pInfo[m]->width); //按理说这条和上面一条是等价的,但是加入这条语句后就会 core 崩溃 } } ]]>但有时候一个函数可能出现多种错误,所以单纯返回-1 也无法满足需要。有些人的处理方式是增加一个 errno 来标记错误。
但个人感觉还是有些麻烦,而且需要考虑多线程问题。
所以实现了这样一个返回值管理的组件放到了 Melon 库中。
先给出一个例子,大家看看效果吧。
#include "mln_error.h" #define OK 0 #define NMEM 1 int main(void) { char msg[1024]; mln_string_t files[] = { mln_string("a.c"), }; mln_string_t errs[] = { mln_string("Success"), mln_string("No memory"), }; mln_error_init(files, errs, sizeof(files)/sizeof(mln_string_t), sizeof(errs)/sizeof(mln_string_t)); printf("%x %d [%s]\n", RET(OK), CODE(RET(OK)), mln_error_string(RET(OK), msg, sizeof(msg))); printf("%x %d [%s]\n", RET(NMEM), CODE(RET(NMEM)), mln_error_string(RET(NMEM), msg, sizeof(msg))); printf("%x %d [%s]\n", RET(2), CODE(RET(2)), mln_error_string(RET(2), msg, sizeof(msg))); printf("%x %d [%s]\n", RET(0xff), CODE(RET(0xff)), mln_error_string(RET(0xff), msg, sizeof(msg))); return 0; } 这个程序的执行结果如下:
0 0 [Success] ffffedff 1 [a.c:18:No memory] ffffec01 255 [a.c:19:Unknown Code] ffffeb01 255 [a.c:20:Unknown Code] 可以看到,通过一个 int 值,我可以获取到错误码、文件、行号甚至是错误码对应的描述。
当然这些内容需要在返回值管理组件初始化的时候给出。
这样,每一份代码的每一个使用RET生成的错误码将是全项目中独一无二的一个负整数。只要有了这个负整数,同时知道了代码的版本,就可以轻松获取到错误的位置和错误内容。
感兴趣的可以看一下Melon 的官方文档了解详情。
]]>这么多年没再碰 C 语言,刚刚发现,其实指针除了是一个地址,应该还包含“如何去读取这个地址的值”这个东西?
同样的地址,赋值给 int 指针和 char 指针,读取来的东西肯定是不一样的,感觉很有意思。
]]>正因为 Melon 提供了非常多的组件,才有了今天这个主题——模块选择性编译。所谓的选择性编译,简单来说就是:想要用什么组件就编译什么组件,无关组件不会编译成库。
这样做有两个目的:
下面我们就演示一下如何进行模块选择性编译。
第一步:获取 Melon 仓库
git clone https://github.com/Water-Melon/Melon.git 第二步:进入仓库
cd Melon 第三步:执行 configure ,我们的选择性编译也是在这一步
./configure --select=rbtree,array 这里假设我们仅编译红黑树和数组两个组件。
第四步:执行 make
make 第五步:执行 make install
make install 如果安装的位置权限不够,则需要
sudo make install 第三步中--select的参数是以逗号分隔的模块名称,注意不要有空格。
在 Melon 官方文档中,每个组件的文档内,都给出了对应的模块名称。
还有一种简单的办法去获取模块名。在 Melon 中,几乎每个模块都有一对.c 和.h 文件。当我们需要用到某个模块,一般都是 include 那个模块的头文件。而头文件的命名一般是mln_<module>.h,这里的module部分就是模块名称(<>只是用来突出模块名称)。
下面内容为笔者在虚机上执行上述命令进行选择性编译安装的终端输出内容。
$ git clone https://github.com/Water-Melon/Melon.git Cloning into 'Melon'... remote: Enumerating objects: 6095, done. remote: Counting objects: 100% (1281/1281), done. remote: Compressing objects: 100% (480/480), done. remote: Total 6095 (delta 909), reused 1131 (delta 793), pack-reused 4814 Receiving objects: 100% (6095/6095), 3.22 MiB | 1.02 MiB/s, done. Resolving deltas: 100% (4417/4417), done. $ cd Melon nik@nik:~/test/Melon$ ./configure --select=rbtree,array Installation Path [/usr/local/melon] Melang script Path [/usr/local/lib/melang] Melang dylib Path [/usr/local/lib/melang_dynamic] Configure done! Melon$ make test -d objs || mkdir objs test -d lib || mkdir lib cc -Iinclude -c -Wall -Werror -O3 -fPIC -o objs/mln_rbtree.o src/mln_rbtree.c cc -Iinclude -c -Wall -Werror -O3 -fPIC -o objs/mln_array.o src/mln_array.c cc -o lib/libmelon.so objs/mln_rbtree.o objs/mln_array.o -Wall -lpthread -Llib/ -ldl -shared -fPIC ar -r lib/libmelon.a objs/mln_rbtree.o objs/mln_array.o ar: creating lib/libmelon.a Melon$ sudo make install test -d /usr/local/lib/melang || mkdir -p /usr/local/lib/melang test -d /usr/local/melon || mkdir -p /usr/local/melon cp -fr lib /usr/local/melon cp -fr include /usr/local/melon test -d /usr/local/melon/conf || cp -fr conf /usr/local/melon test -d /usr/local/lib/melang/trace || cp -fr trace /usr/local/lib/melang 可以看到,只有红黑树和数组的源文件被编译和打包成库了。
]]>#include "stdio.h" #include "stdlib.h" #include "string.h" char * reverseWords(char * s){ char * start, * start2; start = s; while(*start != '\0'){ start ++; } -- start; char s2[strlen(s)]; char * s2p = s2; while(start > s){ while(*start == ' ') start --; while(*start != ' ' && start >= s) start --; start2 = start; ++start2; while(*start2 != ' '){ *s2p++ = *start2++; } *s2p++ = ' ' ; } *s2p = '\0'; start = s; s2p = s2; while(*s2p!='\0') *start++ = *s2p++; *start = '\0'; return s; } int main(int argc, char ** argv){ char s[] = "abc edf ghi lkm opq rst "; printf("%s\n",reverseWords(s)); } 在 linux 机器上能编译运行正确 leetcode 提示==20==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000020 at pc 0x563392652f73 bp 0x7ffefb8e0c40 sp 0x7ffefb8e0c30 READ of size 1 at 0x602000000020 thread T0
我是不是太贱了?
]]>Melon 是一个包含了开发中常用的各类组件的开源 C 语言库,支持 Linux 、MacOS 、Windows 系统,可用于服务器开发亦可用于嵌入式开发,无第三方软件依赖,安装简单,开箱即用,中英文文档配套齐全。
日志是 Melon 库提供的通用组建之一,在介绍其特性前,我们先来看一下它的简单使用示例。
#include "mln_log.h" int main(int argc, char *argv[]) { mln_log(debug, "This will be outputted to stderr\n"); mln_log_init(NULL); mln_log(debug, "This will be outputted to stderr and log file\n"); return 0; } 可以看到,使用与我们常用的printf或fprintf很相似。
同时,从这个例子中也可以看到这个日志模块允许在未初始化情况下使用。在未初始化时,这些日志都将被输出至stderr中。
当然,我们也可以使用mln_log_init对日志模块进行初始化。初始化后将获得两种能力:
显然,我们的日志不仅仅可以做这些事情。
下一步我们将讨论如何将日志内容做自定义处理,我们将介绍日志回调函数。
这里涉及到两个日志模块的函数:
mln_log_logger_get 获取当前日志处理函数mln_log_logger_set 设置日志处理函数为某个指定的函数我们看一下简单的例子,我们对上面的例子进行一顿改造:
#include <stdio.h> #include "mln_log.h" mln_logger_t old = NULL; static void mylogger(mln_log_t *log, mln_log_level_t level, const char *file, const char *func, int line, char *fmt, va_list args) { printf("%s:%s:%d: %s\n", file, func, line, fmt); if (old != NULL) old(log, level, file, func, line, fmt, args); } int main(int argc, char *argv[]) { mln_log(debug, "This will be outputted to stderr\n"); mln_log_init(NULL); old = mln_log_logger_get(); mln_log_logger_set(mylogger); mln_log(debug, "This will be outputted to stderr and log file\n"); return 0; } 我们增加了一个自己的日志处理函数名为mylogger,它将截获日志的内容及其关联信息,使用printf将这些信息输出到标准输出(stdout)中。然后再使用原有的日志处理函数将日志输出到终端的stderr和日志文件中。
这样,我们就可以随意地对日志内容进行处理了。
Melon 的日志模块允许开发者对其进行任意扩展来满足自己的需求。
另外,要补充一点,这个mln_log是允许多线程使用的。并且对于x86 架构,在不调用mln_log_init对日志进行初始化的情况下,这个函数就是多线程可用的。
感谢阅读!
]]>相信很多读者都听说过甚至使用过 cJSON 开源库。那么本文就拿 cJSON 与 Melon 的 JSON 组件进行对比。
下面我们就来一起看一看。
假设我们要构建如下 JSON:
{ "name": "Awesome 4K", "resolutions": [ { "width": 1280, "height": 720 }, { "width": 1920, "height": 1080 }, { "width": 3840, "height": 2160 } ] } 那么,我们先看一下cJSON的构建代码:
#include <stdio.h> #include <cjson/cJSON.h> //NOTE: Returns a heap allocated string, you are required to free it after use. char *create_monitor_with_helpers(void) { const unsigned int resolution_numbers[3][2] = { {1280, 720}, {1920, 1080}, {3840, 2160} }; char *string = NULL; cJSON *resolutiOns= NULL; size_t index = 0; cJSON *mOnitor= cJSON_CreateObject(); if (cJSON_AddStringToObject(monitor, "name", "Awesome 4K") == NULL) { goto end; } resolutiOns= cJSON_AddArrayToObject(monitor, "resolutions"); if (resolutiOns== NULL) { goto end; } for (index = 0; index < (sizeof(resolution_numbers) / (2 * sizeof(int))); ++index) { cJSON *resolution = cJSON_CreateObject(); if (cJSON_AddNumberToObject(resolution, "width", resolution_numbers[index][0]) == NULL) { goto end; } if (cJSON_AddNumberToObject(resolution, "height", resolution_numbers[index][1]) == NULL) { goto end; } cJSON_AddItemToArray(resolutions, resolution); } string = cJSON_Print(monitor); if (string == NULL) { fprintf(stderr, "Failed to print monitor.\n"); } end: cJSON_Delete(monitor); return string; } int main(void) { char *p; p = create_monitor_with_helpers(); printf("%s\n", p); return 0; } 下面,我们一起看下使用Melon 的 JSON 组件的代码:
#include <stdio.h> #include "mln_json.h" #include "mln_log.h" static mln_string_t *generate(void) { mln_json_t j; mln_string_t *ret; mln_json_init(&j); mln_json_generate(&j, "{s:s,s:[{s:d,s:d},{s:d,s:d},{s:d,s:d}]}", \ "name", "Awesome 4K", "resolutions", "width", 1280, "height", 720, \ "width", 1920, "height", 1080, "width", 3840, "height", 2160); ret = mln_json_encode(&j); mln_json_destroy(&j); return ret; } int main(void) { mln_string_t *p; p = generate(); mln_log(none, "%S\n", p); return 0; } 假设我们有如下 JSON:
{ "name": "Awesome 4K", "resolutions": [ { "width": 1280, "height": 720 } ] } 下面一起来看下解码,还是先上cJSON的代码:
#include <stdio.h> #include <cjson/cJSON.h> /* return 1 if the monitor supports full hd, 0 otherwise */ int supports_full_hd(const char * const monitor) { const cJSON *resolution = NULL; const cJSON *resolutiOns= NULL; cJSON *monitor_json = cJSON_Parse(monitor); if (monitor_json == NULL) return -1; resolutiOns= cJSON_GetObjectItemCaseSensitive(monitor_json, "resolutions"); cJSON_ArrayForEach(resolution, resolutions) { cJSON *width = cJSON_GetObjectItemCaseSensitive(resolution, "width"); return width->valuedouble; } cJSON_Delete(monitor_json); return -1; } int main(void) { char p[] = "{\"name\":\"Awesome 4K\",\"resolutions\":[{\"width\":1280,\"height\":720}]}"; int i = supports_full_hd(p); printf("%d\n", i); return 0; } 接下来是Melon 的 JSON 组件的解码代码:
#include <stdio.h> #include "mln_json.h" #include "mln_log.h" static int handler(mln_json_t *j, void *data) { return (int)M_JSON_GET_DATA_NUMBER(j); } static int parse(mln_string_t *p) { mln_json_t j; mln_string_t exp = mln_string("resolutions.0.width"); mln_json_decode(p, &j); return mln_json_parse(&j, &exp, handler, NULL); } int main(void) { mln_string_t p = mln_string("{\"name\":\"Awesome 4K\",\"resolutions\":[{\"width\":1280,\"height\":720}]}"); int i = parse(&p); mln_log(none, "%d\n", i); return 0; } Melon 的 JSON 组件主要提供了如下四个函数来便于使用者构建和解析 JSON:
mln_json_decode解码 JSON 字符串为 JSON 结构体结点mln_json_parse从解码的 JSON 结构体中,根据给定表达式,获取对应的 JSON 子结点mln_json_generate根据给定的格式信息构建 JSON 结构体mln_json_encode根据生成的 JSON 结构体生成 JSON 字符串Melon 的 JSON 组件提供了易于阅读和使用的函数接口,更易于开发者对项目的维护。
欢迎大家来试用开源 C 语言库 Melon 。
]]> #include <stdio.h> #include <pthread.h> #include <semaphore.h> sem_t mutex; void *my_thread() { while (1) { //判断信号量是否大于 0 ,如果大于 0 ,执行减一操作,并继续 sem_wait 之后的代码;如果当前 sem 等于 0 ,阻塞等待直至大于零 // (大于零之后执行减一操作,继续 sem_wait 之后的代码) printf("测试数据 1"); sem_wait(&mutex); //信号量值加一,直接返回; printf("测试数据 2"); sem_post(&mutex); printf("测试数据 3"); break; } return 0; } int main() { pthread_t thread_1, thread_2; //声明线程 ID // sem_open()创建一个新的 POSIX 信号量或打开一个现有的信号量。信号灯由名称标识。有关名称构造的详细信息,请参见 sem_overview(7)。 // oflag 参数指定用于控制调用操作的标志。 (可以通过包含来获得标志值的定义。)如果在 oflag 中指定 O_CREAT ,则如果信号灯尚不存在,则会创建该信号灯。 // 信号量的所有者(用户 ID)设置为调用过程的有效用户 ID 。组所有权(组 ID)设置为调用过程的有效组 ID 。如果在 oflag 中同时指定了 O_CREAT 和 O_EXCL , // 则如果已经存在具有给定名称的信号灯,则将返回错误。 // 如果在 oflag 中指定 O_CREAT ,则必须提供两个附加参数。mode 参数指定要放置在新信号量上的权限,就像 open(2)一样。 // (可以通过包含。获得权限位的符号定义。)权限设置针对进程 umask 进行掩码。应该向将访问信号量的每个用户类别都授予读取和写入权限。 // value 参数指定新信号量的初始值。如果指定了 O_CREAT ,并且已经存在具有给定名称的信号灯,则将忽略 mode 和 value mutex = *sem_open("mutex", O_CREAT, S_IRWXU, 1); // Name 用于标识信号量的名字 // Oflag 被设置为 O_CREAT 用来创建一个信号量(如果和 0_EXCL 一起,当这个信号量已经存在时候这个调用将会失败) // mode_t 控制新的信号量的访问权限 // Value 指定信号量的初始化值 // ( 1 ) tidp:事先创建好的 pthread_t 类型的参数。成功时 tidp 指向的内存单元被设置为新创建线程的线程 ID 。 // ( 2 ) attr:用于定制各种不同的线程属性。APUE 的 12.3 节讨论了线程属性。通常直接设为 NULL 。 // ( 3 ) start_rtn:新创建线程从此函数开始运行。无参数是 arg 设为 NULL 即可。 // ( 4 ) arg:start_rtn 函数的参数。无参数时设为 NULL 即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。(以下举例) //向线程函数传递参数 pthread_create(&thread_1, NULL, &my_thread, NULL); pthread_create(&thread_2, NULL, &my_thread, NULL); //以阻塞的方式等待 thread 指定的线程结束。当函数返回时,被等待线程的资源被收回。 // 如果线程已经结束,那么该函数会立即返回。并且 thread 指定的线程必须是 joinable 的 pthread_join(thread_1, NULL); pthread_join(thread_2, NULL); sem_unlink("mutex"); return 0; } ``` 报错是这个 zsh: segmentation fault ./a.out ]]>给大家推荐一个自己开发和维护至今 9 年的 C 语言开源项目——Melon 。
这个项目中实现了各种我以往工作或者私人项目中用到的一些公共组件,包含但不限于:数据结构、算法,还包含各类常用组件,如:内存池,各类线程模型、多进程模型等等。
这个库没有第三方依赖,所以安装简单,开箱即用,中英文文档齐全,每个组件均配有可执行的示例。
操作系统环境:完整支持 UNIX/Linux 环境,Windows 下除框架功能暂不可用,其他组件均可用。
Docker:目前由于无法访问 docker hub ,所以 docker 镜像尚无法跟随 master 分支最新代码。
Webassembly:支持使用 emcc ,将 Melon 编译为 wasm 静态库。
支持服务器开发,也可以支持嵌入式交叉编译。
在 V2EX 也发过很多篇关于这个库的使用文章,感兴趣的朋友可以翻阅一下本号历史。最近也发布过一个短视频来介绍其中的组件。
GitHub: https://github.com/Water-Melon/Melon
Gitee 镜像站: https://gitee.com/melon-c/Melon
B 站视频: [ C 语言必备开源库——红黑树使用]
]]>