文章目录

  • 1 C穿越时空的迷雾
    • 1.1 const类型变量
      • 1.1.1 不可改变的值
      • 1.1.2 const类型匹配
    • 1.2 隐类型隐式转换
      • 1.2.1 unsigned和signed之间的转换
    • 1.3 关于pragma的使用
  • 2 这不是BUG而是语言特性
    • 2.1 switch的标注
    • 2.2 sizeof歧义
    • 2.3 优先级
    • 2.4 gets漏洞
    • 2.5 局部变量返回问题
  • 3 分析C语言的声明
    • 3.1 C语言复杂的声明
  • 4令人震惊的事实:数组和指针并不相同
  • 5 对链接的思考
    • 5.1 链接库
    • 5.2 警惕Interpositioning
  • 6 运动的诗章:运行时数据结构
    • 6.1 a.out的组成结构
    • 6.2 线程控制(setjmp和longjmp的使用)
  • 7 对内存的思考
    • 7.1 内存错误
  • 8 程序员为什么无法愤青万圣节和圣诞节
  • 9 再论数组
  • 10 再论指针
  • 11 附录

  下面整理的是《C专家编程》这本书中提到的一些C语言的缺陷的存在争议的问题。其中罗列的程序都是经过调试运行的,《C专家编程》中提到的一些bug或者缺陷若已经在本人使用的C版本中得到修复将不再重述,只会简略提到。书中提到的很多问题都是在没有一款强的IDE的情况下,现如今我们的IDE足够强大很难产生作者提到的很多问题,但了解这些不无裨益。

环境:windows7旗舰版编译器:gcc (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.2cl 来源于Microsoft Visual Studio 2013

1 C穿越时空的迷雾

1.1 const类型变量

  const变量的声明在C语言中命名为常量,但是实现的功能仅仅是不能被直接的修改,可以通过指针寻址修改const的值。

1.1.1 不可改变的值

  如下代码:

     const int num = 12;int array[num];

  在gcc下编译可以正常运行,而且可以作为正常的数组使用,但是在cl中就会出现如下的错误提示:

  说明const变量作为数组的初始化长度在不同的编译器中有不同的定义。虽然const为不可改变的变量,但是通过读取内存地址可以间接的修改const的值。

    const int num = 12;int* ptr = #printf("const num:%d\n", num);*ptr = 24;printf("const num:%d\n", num);

  代码的编译和执行在cl和gcc下基本相同输出结果为:

  说明const这个类型并不是真正的const,(C++中对const的定义很严格以上情况不会发生)但是庆幸的是在gcc中编译给出了警告:

  这可以让开发人员注意到这一点(突然感觉gcc如此强大)

1.1.2 const类型匹配

  书中提到的类型匹配问题在一级指针和普通变量中不存在,但在多级指针中cl无反应,gcc会警告用户。

    void run(const char **num){printf("run function!\n");}int main(void){char* a[] = { "12" };const int num = 12;run(a);system("pause");return 0;}

  gcc警告:

1.2 隐类型隐式转换

  C语言中存在数据类型的转换例如int转换为float,float转换为double,unsigned 转换为signed,这些数据类型再进行计算是会进行隐式转换,对计算来说是好事,比如:

    int num_rst = 2;double num_snd = 3.0;double result = num_rst / num_snd;

  当进行计算时,num_rst先隐式转换为double再参与计算,省去了程序员的一些工作。但是有些数据类型转换就会产生错误。

1.2.1 unsigned和signed之间的转换

    unsigned short a = -1;if (a < 1){printf("-1 < (unisgned int)1 \n");}else{printf("(unsigned) 1:%d\n", (unsigned)1);printf("(unsigned)-1:%d\n", a);printf("-1 > (unisgned int)1 \n");}

  cl和gcc下输出的结果都是:

  可以看出输出结果和我们想象的有差距。

  并且当short类型换成char类型使输出为255,而当换成int是输出为-1,其实利用负数在内存中的状态来思考就很好理解了,负数在内存中以补码的方式存在,char类型为一个字节八位,-1的补码为1111 1111直接解析就是255;short int为2个字节十六位-1的补码为1111 1111 1111 1111直接解析就是65535 int类型为4个字节32位,-1的补码为0xFFFFFFFF结果是4294967295打打印出来是-1这和printf函数有关,将%d改为%ud即可。可见unsigned 类型对变成有一定的影响,所以尽量不要使用unsigned,除非一些要用二进制位段的地方,比如51单片机等。

1.3 关于pragma的使用

  gcc和cl不会出现异常。

2 这不是BUG而是语言特性

  正如本章的题目所示,文中提到的很多并不是bug而是语言特性,只要程序员稍加注意就没有问题。

2.1 switch的标注

  书中提到的容易出错的问题,我并不认为是问题,但在当case之间不能再break的时候加上/fall through/的注释很有利于检查:

    switch (num){case 1:printf("");/*fall through*/case 2:printf("");case 3:printf("");}

2.2 sizeof歧义

    int *p = 12;int a =  12 * sizeof * p;printf("%d\n", a);

  这种地方sizeof后面的’*’到底解析为乘号还是取值符,这个并不算很有争议,毕竟sizeof必须跟参数但是:

    int apple = sizeof(int)*p

  有两种解释方法:

  • 第一种:sizeof(int) * p中*是一个乘号;
  • 第二种:解析为sizeof((int)p)先对p进行类型转换在计算sizeof。

  可能我们第一眼感觉都是第一种解析,但第二种解析方式并没有出错,并不能以我们看代码的方向来思考编译器解析代码的规则,在我电脑上的编译器会根据p的数据类型选择如何解析,这对程序员来说是好事,但我们还是必须知道这些缺陷。
  解决类似问题也很简单就是多加几个括号即可。

2.3 优先级

  相关优先级如下:

  建议:

  1. 乘法和除法优先级高于加法和减法;
  2. 当设计多种操作符时加上括号绝对不会错。

  赋值符就有右结合性。

    int a, b = 2, c = 4;a = b = c;

  先执行b = c再执行a = b;
  逻辑操作符具有左结合性。

    int a = 1, b = 0, c = 0;printf("%d\n", a | b | c);

  先执行a|b在执行|c。

2.4 gets漏洞

  gets函数可以将用户输入写入对应的堆栈中,但不会对用户输入进行检查,当用户输入的字符数量超过指定区域时他照样会写入,这就导致了内存越界的问题。

    char buf[10];gets(buf);printf("%p\n", buf);printf("%s\n", buf);


  这将导致很严重的内存问题,可以使用gets或者fget(buf,sizeof(buf),stdin)来代替输入,vs中对于使用gets除非定义#define _CRT_SECURE_NO_WARNINGS将会报错,而gcc中仍然可以使用。

2.5 局部变量返回问题

    char buf[12];…return buf;

  这段代码没有语法错误但会导致一个问题buf的内存是一个局部的当函数执行结束时buf也会被自动销毁,所以返回的指针也就没有了意义。
  解决方法:

3 分析C语言的声明

3.1 C语言复杂的声明

 int (*fun())(void)            //函数返回值是一个函数指针int (*fun(void))[]            //函数返回值是一个指向数组的指针int(*fun[])()                 //函数指针数组

  类似的形式判断规则:

  • A. 声明从它的名字开始读取,然后按照优先级顺序依次读取;
  • B. 优先级从高到低依次是:
    • B1:声明中被括号扩住的部分;
    • B2:后缀操作符()表示一个函数 []表示一个数组;
    • B3:前缀操作符,*表示”指向…的指针”;
  • C. 如果const,volatile后面紧跟类型说明符(如int),则他做用于类型说明符,否则作用于它左边紧邻的指针星号。

4令人震惊的事实:数组和指针并不相同

  数组和指针在某些方面很相似但两者不同:

    double* ptr = 1.3;

  ptr需要指向一个内存空间,但1.3没有内存空间 。

5 对链接的思考

5.1 链接库

  静态链接:每个可执行文件都拥有一份对应代码二进制的拷贝,导致可执行文件的大小相对较大。
  动态连接:所有可执行文件都共享同一个链接库,可执行文件相对较小,但是对文件的搜寻会占用一定的时间,但相对于静态链接来说速度上不会太差,而且动态连接不用依赖于操作系统的版本问题,独立性更强。
  以后写相对大一点的项目都建议使用动态连接库。
  注:

  1. 动态链接库的可扩展名(linux下)为”.so”(shared object),静态链接库的扩展名为”.a”(archive);window下动态链接库是”dll”(dynamic link library),静态链接库是”.lib”(libray),
  2. 使用的动态链接库是libname.so,连接时可以使用-lname即可找到对应的库文件
  3. 在使用动态链接库的同时尽量使用确定的目录来通知编译器
  4. C语言中头文件的名称并一定和链接库的名称吻合
  5. 静态链接库和动态链接库符号提取方法不同,静态链接更为蛋疼,连接顺序的不同可能导致程序的错误。(始终将-l函数库选项放在变异命令的最右边是不错的选择)

5.2 警惕Interpositioning

  Interpositioning指的是用户自定义的函数使用的函数名和库函数中的函数名相同导致的bug,但是现如今在gcc和cl中都会提示错误所以基本不存在类似问题。

  Interpositioning补充:
  形成bug的主要原因是用户自定义的一个函数和库函数名称冲突,但是用户调用的另一个库函数中用到了被用户覆盖的库函数,导致函数执行出错。

6 运动的诗章:运行时数据结构

  a.out由来是assember output(汇编程序输出)的缩写Unix中#define FS_MAGIC 0x011954是该文件系统设计者的生日。

6.1 a.out的组成结构

  a.out由三个段组成:文本段,数据段和bss段。
  bss保存未经初始化的变量,经初始化的静态变量和全局变量会存储在数据段,执行代码会存储在文本段。
  图中a.out比exe多一个int类型的全局变量,a比a.out多一个int类型的局部变量,可以看出局部变量并不占用空间,局部变量是在代码运行期间分配内存,局部变量存储在堆栈区。

6.2 线程控制(setjmp和longjmp的使用)

  setjmp和longjmp类似于C++中的try和catch,头文件是setjmp.h。

  longjmp可以从函数中跳出而goto不能。

    #define _CRT_SECURE_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>#include <setjmp.h>jmp_buf jmp;void fun(){printf("in the function before jmp\n");longjmp(jmp, 2);printf("in the function after jmp\n");}int main(int argc,char* argv[]){printf("before set jmp\n");//第一次初始化完成的返回值为0,跳转成功后的返回值为longjmp的第二个参数int ret = setjmp(jmp);printf("%d\n", ret);printf("after set jmp\n");if (2 == ret){printf("跳转成功\n");system("pause");return;}fun();system("pause");return 0;}

  linux下C语言工具。
  之前是查看源代码的程序,dis之后是检查可执行文件的程序,truss之后是帮助调试的程序,collector之后是性能优化的辅助工具。

7 对内存的思考

  MS-DOS总共1MB的内存但却限制任何应用程序使用内存不能大于640KB,原因是部分作保留由系统使用。

  可用near,_far,huge修饰指针来控制指针长度,但vs貌似不支持。

7.1 内存错误

  内存损坏:释放或者改写正在使用的内存块。
  内存泄露:未释放不再使用的内存块。
  检测内存泄露:

  1. 使用swap命令检测内存系统使用情况;
  2. 使用``ps -lu [用户名]```检查用户进程内存分配。

  总线错误(bus error(core dumped)):多产生于未对齐的读或者写。


  注:但是在vs和ubuntu中均未出现总线错误的提示。
  段错误(segmentation fault(core dumped))
  引起段错误的直接原因:

  1. 解除引用一个包含非法值的指针;
  2. 解除引用一个空指针(常常为系统返回,未做检查);
  3. 未得到正确权限进行访问。如:向制度文本存储值;
  4. 用完了堆栈或者堆。

  显示出现段错误,0地址无法写:

8 程序员为什么无法愤青万圣节和圣诞节

  万圣节:11月1日,万圣节前夜是10月30日,十月的英文是October(Oct),八进制。
  圣诞节是在12月25日,12月的英文是(Dec),十进制。
  八进制的30刚好是十进制的25,这里作者提到的更像是一个冷笑话!

9 再论数组

  记住:

  • 数组是一种类型;
  • 数组名并不是指针;
  • 当数组作为函数参数时,数组会退化为指针;
  • C语言不存在多维数组,C语言支持的只是数组的数组;
  • C语言中任何数组在内存中的映射都是线性的。

10 再论指针

  多维数组的存储结构都是线性,只是解析方式不同而已:

  所有有效的传递:

    void fun_rst(int buf[2][2][2]){}void fun_snd(int buf[][2][2]){}void fun_thd(int (*buf)[2][2]){}int main(int argc,char* argv[]){int buf[2][2][2];int(*p)[2][2][2] = &buf;fun_rst(buf);fun_snd(buf);fun_thd(buf);fun_rst(*p);fun_snd(*p);fun_thd(*p);system("pause");return 0;}

11 附录

  1、判断一个链表中是否存在环:

  1. 对访问过的每个元素进行标记,之后若标记重复则有环;
  2. 将访问过的数据存入一个数组,若之后访问的数据在数组中则有环(数据可以重复啊!!!);
  3. 创建一个指针指向链表头,然后连续访问n个元素并与指针所指向的元素进行比较,比较结束后将指针向后移动,若出现相等则有环。
      答案:快慢指针。

  2、库函数和系统函数的区别

  3、如何确定一个变量是有符号还是无符号:

  分析:决不能使用函数调用,函数调用就会发生类型转换

  1. 若判断的是一个变量:
#define GO(a)  (a >= 0 && ~a>=0)
  1. 若判断的是一个类型:
#define GO(type)((type)0 - 1 > 0)

《C专家编程》阅读笔记相关推荐

  1. trainer setup_Detectron2源码阅读笔记-(一)Configamp;Trainer

    一.代码结构概览 1.核心部分 configs:储存各种网络的yaml配置文件 datasets:存放数据集的地方 detectron2:运行代码的核心组件 tools:提供了运行代码的入口以及一切可 ...

  2. VoxelNet阅读笔记

    作者:Tom Hardy Date:2020-02-11 来源:VoxelNet阅读笔记

  3. Transformers包tokenizer.encode()方法源码阅读笔记

    Transformers包tokenizer.encode()方法源码阅读笔记_天才小呵呵的博客-CSDN博客_tokenizer.encode

  4. 源码阅读笔记 BiLSTM+CRF做NER任务 流程图

    源码阅读笔记 BiLSTM+CRF做NER任务(二) 源码地址:https://github.com/ZhixiuYe/NER-pytorch 本篇正式进入源码的阅读,按照流程顺序,一一解剖. 一.流 ...

  5. Mina源码阅读笔记(一)-整体解读

    2019独角兽企业重金招聘Python工程师标准>>> 今天的这一节,将从整体上对mina的源代码进行把握,网上已经有好多关于mina源码的阅读笔记,但好多都是列举了一下每个接口或者 ...

  6. “CoreCLR is now Open Source”阅读笔记

    英文原文:CoreCLR is now Open Source 阅读笔记如下: CoreCLR是.NET Core的执行引擎,功能包括GC(Garbage Collection), JIT(将CIL代 ...

  7. QCon 2015 阅读笔记 - 团队建设

    QCon 2015阅读笔记 QCon 2015 阅读笔记 - 移动开发最佳实践 QCon 2015 阅读笔记 - 团队建设 中西对话:团队管理的五项理论和实战 - 谢欣.董飞(今日头条,LinkedI ...

  8. 05《软件需求模式》阅读笔记

    剩下的两个阅读笔记写第二部分.各类需求模式,共八个领域和它的需求模式,这一次写前四个. 基础需求模式,它是所有种类的系统都可能需要的一些东西.系统间接口需求模式使用系统间接口需求模式定义被定义的系统和 ...

  9. [置顶] Linux协议栈代码阅读笔记(一)

    Linux协议栈代码阅读笔记(一) (基于linux-2.6.21.7) (一)用户态通过诸如下面的C库函数访问协议栈服务 int socket(int domain, int type, int p ...

  10. 大型网站技术架构:核心原理与案例分析阅读笔记二

    大型网站技术架构:核心原理与案例分析阅读笔记二 网站架构设计时可能会存在误区,其实不必一味追随大公司的解决方案,也不必为了技术而技术,要根据本公司的实际情况,制定适合本公司发展的网站架构设计,否则会变 ...

最新文章

  1. RCNN 目标识别基本原理
  2. 【bzoj3309】DZY Loves Math 莫比乌斯反演+线性筛
  3. A JWT old for new exchange schema
  4. 人工智能热门图书(深度学习、TensorFlow)免费送!
  5. mysql索引原理剖析
  6. 计算机培训三下乡实践报告,暑期“三下乡”社会实践心得体会范文
  7. ProFile配置节属serializeAs
  8. 金叉成功率_曝光MACD零轴上的秘密:“0线下方金叉买入”千万别小看,成功率达100%!...
  9. 字母组合---排列组合问题
  10. could not initialize javavm mysql_Could not initialize JavaVM
  11. 2022芒果TV算法赛_用户下一个观看视频预测_baseline_CF召回YoutubeDNN
  12. python数据按照分组进行频率分布_3.2.1 分布分析
  13. 二阶常系数齐次线性微分方程通解的求取
  14. 学士峡谷风景Mac壁纸分享
  15. 16、持续集成流水线实践:流水线上的AI单元测试(MAVEN)
  16. ASP调用存储过程中与SQL对应的数据类型
  17. C语言 | 输入一些字符,直到输入“#”为止
  18. ubuntu20.04 安装 WPS 2019
  19. Python使用Cv2模块识别验证码
  20. PimaIndiansdiabetes-数据预处理实验(一)

热门文章

  1. rtl8187L驱动在linux2.6.35上的编译
  2. 信息系统项目管理师考试难?一次过备考经验分享给大家
  3. 模拟退火算法(SA)
  4. MAPGIS67默认打开方式失败
  5. 数据分析进阶 - 评分模型权重计算方法
  6. 百度cpc联盟的漏洞?还是内鬼?
  7. 637道Java面试题(含答案)
  8. 云原生时代——投资人视角下的云原生趋势思考
  9. CDN学习笔记二(技术详解)
  10. 类似铸剑物语的java游戏_怀旧向:GBA上的10款经典RPG游戏推荐,这些你都玩过吗?...