读 《C Traps and Pitfalls》Record

@(C 语言)[基础, 编程]

薄薄一本书, 却记录了c 编程经常犯下的错误,再读,记录下。

词法

  • 词法分析 : 大嘴法 编译器分解符号的方法是从左到右读入, 判断可能组成的最大的一个符号
a---b // (a--) - b

别复杂化, 使用括号,清晰直观

  • 字符和字符串
char* pStr = "YES" // 'Y', 'E', 'S', 0 , 4 char
char ch = 'y'// a char

单引号实际代表一个整数 双引号代表指向无名数组的起始字符的指针(字符结尾 0) 使用库函数计算得到的字符串长度不包括结尾的0!

语法

  • 理解函数声明 调用首地址为0的子例程 (void fun()),
(* (void(*)())0 )();

// call void fun()
// 声明指向上述fun的指针
void (*pfun)();  
// 注意括号,void *rf(); 声明了返回void* 指针的函数。
pfun = &fun;// pfun = fun; 也可以
(*pfun)();
pfun(); // 简写

// 对0地址进行类型转换
pfun = (void (*)())0;
没必要多声明一个‘哑’变量pfun,因此如开端所写,对0进行类型转换,再解引用调用

// 直观的话
typedef void (*pfun)();
(* (pfun)0 )();

准则 : 按照使用的方式来声明

  • 运算符优先级问题 对于这个问题,优先级记归记,不熟悉建议使用括号来显示保证。 记录下,吃过这个亏不止一次:
if (flag & 0x01 != 0) {}...
//want 
if ((flag & 0x01) != 0) {}...
//actualy 
if (flag & (0x01 != 0)) {}...

r = hi << 4 + low;
//watnt 
r = (hi << 4) + low;
// actually 
r = hi << (4 + low);

a = *p++;
// == *(p++)  <——
a = *p; p++;

记录优先级可以写出更加优雅的代码, 不装X。

  • 语句结束分号
//少写出错
if (i < 2)
    return
i = 2;
//->
if (i < 2) return (i = 2);

//多写出错
if (i < 2);
    a = 1;
//->
if (i < 2);
a = 1; // every time
  • switch 别漏 break !!!!
  • if else 的对应关系 else 始终与同一对括号内最近的未匹配if 结合
if (x == 0) 
    if (y == 0 ) {..}
else {
    ..
}
//  不同所缩进, 实际是
if (x == 0) {
    if (y == 0) {..}
    else {..}
}

避免悬挂式else, 使用{}进行匹配

if (x == 0) {
    if (y == 0 ) {..}
} else {
    ..
}

语义

  • 指针和数组
  1. C语言中只有一维数组
int a[12]       // 12个 int
int b[12][31]   // 12个 int[31]类型数组的元素
// sizeof(a) == 12 * sizeof(int);
// sizeof(b) == 12 * (31 * sizeof(int))

// attention 
/*
* 数组名是数组的首地址(符号表中对应地址)
* 数组操作 :数组地址 + 偏移地址 --> 内容
* 指针操作 : 指针 --> 数组地址 + 偏移 --> 内容
*/
int *p  = a; 
//sizeof(p) == sizeof(int*) // sizeof of pointer(32位 4, 64位 8)
//sizeof(*p) ==  sizeof(int)

对数组取sizeof可以得到数组的大小,但是对其他指针取sizeof取到的是平台地址的长度

2.数组根据他自身的类型,决定其在加减时,实际增减的内存地址值

int *p0 = (int*)0x0000;
char *p1 = (char*)0x0000;
++p0; ++p1;  // p0 += 1; p1 += 1;
//p0 == 0x0004; p1 == 0x0001;  // if sizeof int == 4

指向数组的的指针

char array[12][31];
char (*p)[31];
char **pp = array;  //注意这里
p = array;
p[0] -->array[0];
++p  -->array[1];

假设array在地址0x0000,那么p 也是地址0x0000, ++p后,p指到array[1],所以地址就是array + sizeof(char)*31; 但是,pp,指向指针的指针,一开始也是指向array[0], 但是,++pp后,指向的和p不一样,而是加了一个sizeof(char*)的长度,因为他的单位是一个指针大小,而p的单位是一个char charxx[31]的大小。

所以,请告诉指针,我指的到底是谁?

  • 非数组的指针
char *s = "jj";
char *t = "xx";
//1 :strlen 不包括字符串结束字符
char *r = (char*)malloc(strlen(s) + strlen(t) + 1); 
if (!r) {
    //2 : malloc 可能申请失败
} else {
    strcpy(r, s);
    strcpy(r, t);
    //..
}

//3 : 记得释放!
free(r);
  • 数组作为参数传递给函数,已经转换为指针。
  • 复制指针不等于复制指针指向的对象,东西只有一份,只是多了一个别称。
  • 数组边界问题(左闭右开, 在一些用变量去索引的情况下,没有处理好,导致访问读取未位置的内容带来的错误。) 提到这种不对称数学角度来说不优美,却给程序设计带来了一些简化。
for (int i = 0; i < 6; ++i) {
...
}
1, 上下边界差就是元素数目
2, 上下相等,范围为空
3, 即使取值为空,上边界永远不小于下边界
  • main 函数返回0代表执行成功,其他地方也可以默认这种操作,返回0代表成功。

连接

  • 定义一处, 拿到内存; 其他地方使用, 声明
  • 避免冲突,限定作用域,使用static

库函数

...

预处理器

  • 注意宏定义错误空格
#define f (x) ((x) -1)
// #define f   ((x) ((x) - 1)) 

宏定义中恰当使用括号,避免实际使用展开后由于优先级而带来的错误。

  • 宏不是语句
#define assert(e) if (!e) assert_error(__FILE__, __LINE__)
if (..)
    assert(..);
else
    assert(..);

// 展开
if (..)
    if (..) assert_error(__FILE__, __LINE__)
else
    if (..) assert_error(__FILE__, __LINE__)
    
// 参考前面悬挂式else, 整理后看到实际效果不一致
if (..)
    if (..) 
        assert_error(__FILE__, __LINE__)
    else
        if (..) 
            assert_error(__FILE__, __LINE__)

可移植性缺陷

其他

  • static 1,函数内的变量,静态变量, 作用域限定在该函数的“全局变量”, 函数退出也保存在内存,下次调用仍能使用该值 2,模块内的变量,限定该模块内使用 3,模块内的函数,限定该模块内使用
  • const 不可改,保护,避免意外修改不想被改变的数据
  • volatile 易变的, 避免被优化 如果一个变量存在在程序流程外被改变(多线程,中断等)声明为volatile保证每次都从内存读取,避免编译器优化,带来数据更新不一致等问题,保证访问的稳定。
char fifo[SIZE];
// 指针指向,不能通过指针修改fifo内容
char const *pff = fifo;
// 指针指向,不能修改指针内容, 即保证pff == fifo 永远
char * const pff = fifo;
// 修饰谁,靠近谁。

// pff 不可变,*(pff + xx) 随时可能改变。
static volatile char* const pff = fifo;
  • 符号数扩展带来的问题
char a = 0x03;
char b = 0x81;
uint16_t c = a <<8 | b;    
// c = 0xFF81 --> 
// b 变成16bit 的时候,char b是有符号数,高位填充1(填充符号位)
unsigned char a = 0x03;
unsigned char b = 0x81;
uint16_t c = a <<8 | b;    //  c = 0x0381  

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术小站

c++(二)

算数运算符:+,-,*,/,%,++,--  进行算数运算时,如果存在溢出,则把溢出的部分拿掉(浮点型的难以预测),如 int i=0xffffffff,j;j...

11810
来自专栏开发与安全

从零开始学C++之从C到C++(一):const与#define、结构体对齐、函数重载name mangling、new/delete 等

一、bool 类型 逻辑型也称布尔型,其取值为true(逻辑真)和false(逻辑假),存储字节数在不同编译系统中可能有所不同,VC++中为1个字节。 声明方式...

20200
来自专栏前端布道

JavaScript之正则表达式

正则表达式 (regular expression) 描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某...

29560
来自专栏Laoqi's Linux运维专列

python3–函数

62950
来自专栏java一日一条

(转)Java正则表达式入门

众所周知,在程序开发中,难免会遇到需要匹配、查找、替换、判断字符串的情况发生,而这些情况有时又比较复杂,如果用纯编码方式解决,往往会浪费程序员的时间及精力。因此...

10810
来自专栏CDA数据分析师

超能教程 十分钟学会 Python!

假设你希望学习Python这门语言,却苦于找不到一个简短而全面的入门教程。那么本教程将花费十分钟的时间带你走入Python的大门。本文的内容介于教程(Totur...

29460
来自专栏Java编程

Java动态代理机制详解

在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来...

48510
来自专栏微信公众号:Java团长

Java动态代理机制详解

在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来...

14610
来自专栏C/C++基础

多字节与宽字符串的相互转换

说到多字节字符串与宽字符串,不得不说一下多字节字符与宽字符。多字节字符实际上是由多个字节来表示一个字符,在各个国家和地区采用不同的编码方案,不同编码方案字符码值...

17920
来自专栏一枝花算不算浪漫

[C#基础]基础知识一: 面向对象的基本知识.

439170

扫码关注云+社区

领取腾讯云代金券