【Linux】C语言补充知识
有一些Linux常见的C语言用法需要哈好复习一下。
部分图片和经验来源于网络,若有侵权麻烦联系我删除,主要是做笔记的时候忘记写来源了,做完笔记很久才写博客。
专栏目录:记录自己的嵌入式学习之路-CSDN博客
1 结构体
1.1 结构体的定义
(1) 最常用方法:先定义结构体后定义结构体变量
(2) 定义结构体同时初始化结构体变量
(3) 直接初始化匿名结构体变量(尽量别用,能看懂就行)
1.2 结构体的初始化
(1) 顺序初始化
(2) 引用成员初始化
该方法也可以只初始化部分结构体成员,如不初始化name也可以。
(3) 定义结构体变量时顺便初始
1.3 结构体数组的初始化
除了与结构体初始化相似的方法外,结构体数组还有一种看起来较为特别的初始化方式:
该方式在初始化时使用“[索引]=”指定初始化的结构体,好像是较新版本的C语言规范和编译器支持的。
2 常用C函数
2.1 strcmp
函数原型:
int strcmp ( const char * str1, const char * str2 );
函数原理:
逐字符比较两个字符串,若两个字符相等,则继续比较下一个字符,若两个字符不相等,则返回它们的ASCII码差值(ASCII码值大的字符串大)。
若两字符串完全相同,则返回0。
3 指针
3.1 指针的初始化
如果一个指针变量被声明后没有被赋予任何值,它将包含一个垃圾值(一个随机的内存地址),这个值是不可预测的。因此对其进行操作就是不合法的,需要先初始化其值。
- 指针的初始化并不是总是简单地指向一个变量的地址就可以了,下面的代码可行:
int * ptr = NULL;
int a = 100;
ptr = &a;
这样可行的前提是,指针ptr的作用域和局部变量a的作用域相同。
- 要是局部变量的作用域和指针无交叉,就无效了,如:
int* init()
{int tmp = 100;return &tmp;
}
int* ptr = init();
这种情况下ptr指向了一个已经被释放的地址,成为了悬挂指针,不合法。
在指针的作用域比较广的时候,最好使用malloc/kmalloc等内存分配函数进行内存分配后或将指针指向静态变量的地址来给指针初始化。
3.2 指针使用的注意事项
- 声明时赋NULL:int* ptr = NULL;
由于没有途径知道指针是否已经被初始化,因此,声明时对其赋值NULL是一个很好的标志,后续看指针是否为NULL就知道其是否已经初始化了。
- 释放后赋NULL:free(ptr); ptr = NULL;
同上,只有释放后赋NULL,才能在别的地方知道这个指针是否被分配了内存。
- 手动分配内存后一定要记得释放:如果使用了像是malloc的内存分配函数,就一定要记得自己free这个指针变量,否则会造成内存泄漏。
3.3 指针的指针
如果需要将一个指针传入函数,并修改这个指针的地址值(注意,不是修改这个指针解引用的那个值,而是指针自身的值)。那么,根据函数传参都是值传递这个说法(“传指针是引用传递”只是针对要传数值而言的,但如果我们要传的本来就是指针),传入的就应该是指针的指针,才能达到修改指针值的作用。
错误的例子:
void host_dev_delete(host_dev* host){if (host){kfree(host);host = NULL;}else {DRIVER_INFO_L("Trying to delete a NULL host_dev!");}}
该例子中,内存是释放掉了,但是函数外部host的实参依然指向已经被释放的内存空间,即host = NULL;没有发挥作用。
正确的例子:
void host_dev_delete(host_dev** host)
{if (host && *host){kfree(*host);*host = NULL;}else if (!host){DRIVER_INFO_L("Trying to delete a NULL host_dev pointer!");}else {DRIVER_INFO_L("Trying to delete a NULL host_dev!");}
}
在正确的例子中,传入的是指针的指针,使得NULL赋值有效。同时,通过判断host是否为NULL,可以防止重复释放以及释放NULL的发生。
3.4 内存泄漏
在用户空间中,要是手动分配了内存但没有释放,那么在用户程序结束后,系统会去回收这部分没被用户释放的内存。
在内核空间中,要是没有进行手动释放,系统也无法回收,只能等重启或关机的时候统一释放。而哪怕内核空间的程序写了释放,但是运行到释放前程序崩溃了,那就基本只能等重启和关机来释放了。别的方法的话,还可以考虑使用以下思路:
(1)notifier机制:
内核提供了notifier机制,允许模块在特定的系统事件发生时注册回调函数。这些回调函数可以在系统halt、restart、power off、oops、panic等事件发生时被调用,用于执行必要的清理操作。
(2)panic和oops处理:
在内核发生oops或panic时,内核会尝试执行一些标准的处理流程,包括打印堆栈跟踪、保存日志等。在某些情况下,内核允许注册回调函数来处理这些异常事件,例如通过aee框架保存关键信息。
3.5 将一个内存释放过程封装成函数的注意事项
- 传入host_dev**,为了将host_dev*的内容赋值为NULL,只传入host_dev*释放内容是没问题的,但赋值NULL的操作会失效,具体原因看前面的解释。
- 检查host_dev和*host_dev是否为空指针的原因:
- 检查host_dev:确保函数内部不会对NULL指针进行解引用操作,这是未定义行为,可能导致程序崩溃;
- 检查*host_dev:确保不会释放已经被释放的内存或未初始化的内存。这同样会导致未定义行为,包括程序崩溃、数据损坏等。
4 static修饰的意义
4.1 封装和隐藏实现细节
被static修饰过的变量只能于本文件访问,因此可以避免被别的文件中的程序肆意修改。因此就可以将一些只在本文件使用的变量用static修饰,防止外部访问。
函数也是如此,对于一些只是在本文件中使用的函数,使用static修饰,可以限制外部程序的权限,避免一些底层的函数被外部调用。对于外部访问,只暴露一些必要的接口就行,即接口函数不使用static修饰。
4.2 避免命名空间的污染
不使用static修饰的函数和变量可被全局访问(include后),从而造成全局命名空间的污染。多使用static修饰,可以降低污染,并有利于后续代码模块的增加,避免命名冲突。
4.3 链接优化
如果一个static函数在其文件内未被使用,链接器可以优化掉这部分代码,减少最终二进制文件的大小。