C语言代码学习笔记

简介: <编程精粹:编写高质量C语言代码> 读书笔记

0.规则

1.假想的编译程序
(1)使用编译器提供的所有的可选警告设施
//代码效果参考:http://www.zidongmutanji.com/bxxx/175038.html

增强类型静态检查的能力
eg: void memchr(const void str, int ch, int size);
那个调用该函数时,即使互换其字符ch和大小size参数,编译器也不会发出警告

但是如果在函数原型中使用更加精确的类型,就可以增强原型提供的错误检查能力
void memchr(const char str, unsigned char ch, size_t size);

注:引入无符号数可以增强类型检查能力,但是也导致 无符号数带来的隐式转换错误(有符号数必须转为无符号数)

(2)使用lint等静态检查工具来检查编译器漏掉的错误
(3)如果有单元测试,就进行单元测试

2.自己设计并使用断言

复制代码
/ memcpy v1: 拷贝不重叠的内存块 /
void memcpy(void to, const void from, size_t size)
{
void
pto = (char)to;
const void
pfrom = (const char)from;
if (pto == NULL || pfrom == NULL) {
fprintf(stderr, "Bad args in memcpy\n");
abort();
}
while (size-- > 0)
pto++ = pfrom++;
return pto;
}
复制代码
复制代码
/
memcpy v2: 拷贝不重叠的内存块 /
void
memcpy(void to, const void from, size_t size)
{
void pto = (char)to;
const void pfrom = (const char)from;

ifdef DEBUG

if (pto == NULL || pfrom == NULL) {
    fprintf(stderr, "Bad args in memcpy\n");
    abort();
}

endif

while (size-- > 0)
    *pto++ = *pfrom++;
return pto;

}
复制代码
既要维护程序的 release 版本,又要维护程序的 debug 版本
利用#ifdef DEBUG 调试宏这种方法的关键是保证调试代码不会在最终产品中出现

复制代码
/ memcpy v3: 拷贝不重叠的内存块 /
void memcpy(void to, void from, size_t size)
{
void
pto = (char)to;
const void
pfrom = (const char)from;
assert(pto != NULL && pfrom != NULL);
while (size-- > 0)
pto+= = *pfrom++;
return pto;
}
复制代码

尽可能多的使用断言,及早发现错误:
必须使用断言对函数的每个指针参数进行检查
必须立即使用断言对获取的资源(malloc获取的指针, fopen获取的FILE指针, 打开的数据库连接,获取的文件描述符等等)进行安全检查

复制代码
/ memcpy v4: 拷贝不重叠的内存块 /
void memcpy(void to, const void from, size_t size)
{
void
pto = (char)to;
const void
pfrom = (const char)from;
assert(pto != NULL && pfrom != NULL);
assert(pto >= pfrom + size || pfrom >= pto + size); /
检查是否重叠 /
while (size-- > 0)
pto++ = *pfrom++;
return pto;
}
复制代码
在程序中使用断言检查语法中未定义行为特性的非法使用

3.为子系统设防
内存管理程序,可能犯的错误:
a.分配一个内存块并使用未经初始化的内容
b.释放一个内存块但继续引用其中的内容
c.调用realloc对一个内存块进行扩展,因此原来的内容发生了存储位置的变化,但程序引用的仍是原来存储位置的内容
d.分配一个内存块后立即"失去"了它,因为没有保存指向所分配内存块的指针
e.读写操作越过了所分配内存块的边界
f.没有对错误情况进行检查

复制代码
/ new_memory v1 : 分配一个内存块 /
int new_memory(void ptr, size_t size)
{
unsigned char
p = (unsigned char*)ptr; p = (unsigned char)malloc(size);
return (
p != NULL);
}
复制代码
然后如下调用:
if (new_memory(&block, 32))
成功,block指向所分配的内存块
else
不成功,block等于NULL

根据ANSI标准,调用malloc存在两处未定义行为,必须加以处理:
a.分配长度为0时,结果未定义
b.malloc分配成功,返回的内存块的内容未定义,可以是0,也可以是随意的信息

复制代码
/ new_memory v2: 分配一个内存块 /
/ 加上内存块大小的检查和内存块的填充初始化 /

define INIT_VALUE 0xA3

int new_memory(void ptr, size_t size)
{
unsigned char
p = (unsigned char*)ptr;
assert(ptr != NULL && size != 0);
p = (unsigned char*)malloc(size);

ifdef DEBUG

{
    if (*p != NULL)
        memset(*p, INIT_VALUE, size);
}

endif

return (*p != NULL);

}
复制代码
在程序的调试版本中保存额外的信息,就可以提供更强的错误检查
只要相应的 release 版本能够满足要求,就可以在debug版本加入尽可能多的调试代码来检查错误

4.对程序进行逐条跟踪
5.糖果机界面

//代码效果参考:http://www.zidongmutanji.com/bxxx/133256.html

复制代码
/ strdup: 为一个字符串建立副本 /
char strdup(const char str)
{
char newstr = (char)malloc(strlen(str) + 1);
assert(newstr);
strcpy(newstr, str);
retrun newstr;
}
复制代码
不要把错误标志和有效数据混杂在一起返回

复制代码
int resize_memory(void ptr, size_t newsize)
{
unsigned char
p = (unsigned char*)ptr;
unsigned char
presize = (unsigned char)realloc(p, newsize);
if (presize != NULL)
*p = presize;
return (presize != NULL);
}
复制代码

一个函数只干一件事,编写功能单一的函数,而不是多功能集一身的函数
反面教材: void realloc(void* ptr, size_t size);
该函数改变先前已分配的内存块大小:
a.如果新请求大小小于原来长度,realloc释放该块尾部多余的内存空间,返回的ptr不变
b.如果新请求大小大于原来长度,扩大后的内存块有可能被分配到新地址处,该块的原有内容被拷贝到新的位置,返回的指针指向扩大后的内存首地址,并且新内存块扩大部分未经初始化
c.如果满足不了扩大内存块的请求,realloc返回NULL,当缩小内存块时,总是成功的
d.如果ptr == NULL 则realloc的作用相当于调用 malloc(size);
e.如果ptr != NULL 且 size == 0 则realloc的作用相当于调用 free(ptr);
f.如果ptr == NULL 且 size == 0 则realloc结果未定义

在允许大小为0的参数时要特别小心,一开始就要为函数的输入选择严格的定义,并最大限度地利用断言
为了程序的易读性和扩充性,不要使用布尔类型作为函数的参数类型

6.风险事业
ANSI并没有标准化 char,int,long 这样的基本数据类型
ANSI没有标准化 基本数据类型的原因:C语言产生于70年代,等到标准化时已经有了20多年写出来的代码基,定义严格的标准将会使大量现存代码无效

复制代码
char strcpy(char pto, const char pfrom)
{
char
ptr = pto;
while ((pto ++ = pfrom++) != '\0')
NULL;
return ptr;
}
复制代码
上述代码在任何编译系统上都可以正确工作

复制代码
int strcmp(const char left, const char right)
{
for (NULL; left = right; left++, right++) {
if (left == '\0')
return 0;
}
return ((left < right) ? -1 : 1);
}
复制代码
上述代码由于最后一行的比较操作而失去了可移植性。修改strcmp,只需声明 left 和 right 为 unsigned char 指针,或者直接在比较中先使用强制转型

((unsigned char)left < (unsigned char)right)
for (unsigned char ch = 0; ch <= UCHAR_MAX; ch++)
array[ch] = ch;
如果 ch = UCHAR_MAX 时,执行最后一次循环,循环之后,ch增加为 UCHAR_MAX + 1, 这将引起ch上溢为0,因此该循环变成了无限循环

if (n < 0)
n = -n;
这段代码可能会出现bug. 在二进制补码系统中,数据类型的表达范围不是对称的,例如 char [-128, 127) 如果n正好为最小负数,则 n = -n 则会上溢

7.编码中的假象
不要引用不属于你的未知存储区,"引用"意味着不仅读而且要写,这样可能会和别的进程产生不可思议的相互作用

//代码效果参考:http://www.zidongmutanji.com/zsjx/531929.html

复制代码
/ unsign_to_str: 将无符号数转换为字符串 /
void unsign_to_str(unsigned u, char str)
{
char
start = str;
while (u > 0) {
str++ = (u % 10) + '\0';
u /= 10;
}
str = '\0';
reverse_string(start);
}
复制代码
上述代码 是反向顺序导出数字,确正向顺序建立字符串,所以需要 reverse_string 来重排数字顺序

复制代码
void unsign_to_str(unsigned u, char str)
{
assert(u < UMAX);
/
将每一位数字从后往前存储, 字符串足够大以便能存储 u 的最大可能值 /
char
ptr = &str[5]; / 假设 u <= 65536 /
ptr = '\0';
while (u > 0) {
(--ptr) = (u % 10) + '\0';
u /= 10;
}
strcpy(str, ptr);

}
复制代码
函数能正确工作是不够的,还必须能够防范程序员产生明显的错误
尽量慎用静态(或全局)存储区传递数据

紧凑的C代码并不能保证得到高效的机器代码,首先应该考虑的是代码的正确性和可读性

8.剩下来的就是态度问题

相关文章
|
1月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
91 1
|
1月前
|
存储 C语言
【C语言】基础刷题训练4(含全面分析和代码改进示例)
【C语言】基础刷题训练4(含全面分析和代码改进示例)
|
2天前
|
安全 C语言
在C语言中,正确使用运算符能提升代码的可读性和效率
在C语言中,运算符的使用需要注意优先级、结合性、自增自减的形式、逻辑运算的短路特性、位运算的类型、条件运算的可读性、类型转换以及使用括号来明确运算顺序。掌握这些注意事项可以帮助编写出更安全和高效的代码。
15 4
|
19天前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
|
19天前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
|
19天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
123 3
|
19天前
|
存储 算法 C语言
C语言手撕实战代码_二叉排序树(二叉搜索树)_构建_删除_插入操作详解
这份二叉排序树习题集涵盖了二叉搜索树(BST)的基本操作,包括构建、查找、删除等核心功能。通过多个具体示例,如构建BST、查找节点所在层数、删除特定节点及查找小于某个关键字的所有节点等,帮助读者深入理解二叉排序树的工作原理与应用技巧。此外,还介绍了如何将一棵二叉树分解为两棵满足特定条件的BST,以及删除所有关键字小于指定值的节点等高级操作。每个题目均配有详细解释与代码实现,便于学习与实践。
|
19天前
|
存储 算法 C语言
C语言手撕实战代码_二叉树_构造二叉树_层序遍历二叉树_二叉树深度的超详细代码实现
这段代码和文本介绍了一系列二叉树相关的问题及其解决方案。其中包括根据前序和中序序列构建二叉树、通过层次遍历序列和中序序列创建二叉树、计算二叉树节点数量、叶子节点数量、度为1的节点数量、二叉树高度、特定节点子树深度、判断两棵树是否相似、将叶子节点链接成双向链表、计算算术表达式的值、判断是否为完全二叉树以及求二叉树的最大宽度等。每道题目均提供了详细的算法思路及相应的C/C++代码实现,帮助读者理解和掌握二叉树的基本操作与应用。
|
19天前
|
存储 算法 C语言
C语言手撕实战代码_循环单链表和循环双链表
本文档详细介绍了用C语言实现循环单链表和循环双链表的相关算法。包括循环单链表的建立、逆转、左移、拆分及合并等操作;以及双链表的建立、遍历、排序和循环双链表的重组。通过具体示例和代码片段,展示了每种算法的实现思路与步骤,帮助读者深入理解并掌握这些数据结构的基本操作方法。
|
19天前
|
算法 C语言 开发者
C语言手撕实战代码_单链表
本文档详细介绍了使用C语言实现单链表的各种基本操作和经典算法。内容涵盖单链表的构建、插入、查找、合并及特殊操作,如头插法和尾插法构建单链表、插入元素、查找倒数第m个节点、合并两个有序链表等。每部分均配有详细的代码示例和注释,帮助读者更好地理解和掌握单链表的编程技巧。此外,还提供了判断子链、查找公共后缀等进阶题目,适合初学者和有一定基础的开发者学习参考。