编写合格的C代码(1):通过编译选项将特定警告视为错误

目录

  • 快速设定
  • 向错误的执念开炮,向C编译器开炮
    • 编译警告应当被忽略吗?warning不重要吗?

      • (1)编库时调用了未定义函数(非编译器内置函数),编译只报warning;链接该库时报error
      • (2)编库时调用了未定义函数(编译器内置同名函数),编译只报warning;链接该库时报error
      • (3)编可执行时.c代码中使用了未定义的函数(编译器内置同名函数)
      • (4)编可执行时.c代码中使用了未定义的函数(编译器无内置同名函数)
  • 个人总结的应当视作error的warning
    • 1. 函数没有声明就使用
    • 2. 函数虽然有声明,但是声明不完整,没有写出返回值类型。
    • 3. 指针类型不兼容
    • 4. 函数应该有返回值但是没有return返回值
    • 5. 使用了影子变量(shadow variable)
    • 6. 函数返回局部变量的地址
    • 7. 变量没有初始化就使用
    • 8. printf等语句中的格式串和实参类型不匹配
    • 9. 把unsigned int和int类型的两个变量比较
    • 10. 把int指针和int相互赋值
  • 在开发环境中配置上述CFLAGS
  • TODO
  • 实录
  • 参考

快速设定

如果你没兴趣/没时间看具体解释、只想快速排错,请明确:这里列出了个人认为应当当作error但被C编译器(少量情况是C++编译器)默认设定为warning的编译选项(CFLAGS/CXXFLAGS),比“忽略所有warning”要更安全,比开启“视所有warning为error”要宽松精准。支持包括主流的Visual Studio和GCC这两个编译器。

  1. CMakeLists.txt中的设定
if (CMAKE_SYSTEM_NAME MATCHES "Windows")set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /we4013 /we4431 /we4133 /we4716 /we6244 /we6246 /we4457 /we4456 /we4172 /we4700 /we4477 /we4018 /we4047")set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} /we4013 /we4431 /we4133 /we4716 /we6244 /we6246 /we4457 /we4456 /we4172 /we4700 /we4477 /we4018 /we4047")
elseif (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Darwin")set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion")set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion")
endif()
  1. Visual Studio中的设定

项目属性->配置属性->C/C++->高级->将特定的警告视为错误,填入相应的警告、错误代号:

4013;4431;4133;4716;6244;6246;4457;4456;4172;4700;4477;4018;4047;4013;4431;4133;4716;6244;6246;4457;4456;4172;4700;4477;4018;4047

  1. 基于Makefile
CFLAGS += -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion
  1. 直接调用gcc/clang
gcc xxx.c -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=return-type -Werror=shadow -Werror=return-local-addr -Werror=uninitialized -Werror=format -Werror=sign-compare -Werror=int-conversion

向错误的执念开炮,向C编译器开炮

说说为什么要定制上面一大串CFLAGS/CXXFLAGS:默认的CFLAGS/CXXFLAGS过分相信程序员,而小白则无法驾驭。问题比较严重的是纯C的代码,C++稍微好一些,因此这里主要说C特有的,剩余少量的是C/C++共有的问题。

编译警告应当被忽略吗?warning不重要吗?

很多程序小白(甚至工作多年的老鸟)认为:

C代码报error需要消灭掉,报warning没啥事儿的赶紧提交版本/给QA测试/上线,PM或老板等着呢/别浪费我不必要时间/warning都是鸡毛蒜皮问题...

遗憾的是这种想法并不罕见,似乎觉得“不crash就没问题”的心态,一旦出问题查起来很可能手忙脚乱,因为crash/bug很可能不好重现(血泪教训:移植ncnn为纯C代码,忘记include相应头文件,手机上运行出现难复现的crash)

.c文件被C编译(而不是C++编译器)编译。最常见的case是(纯C代码,C++没有这个问题):没有找到函数声明的情况下调用函数。也就是,没有实现函数xx(),或者实现了函数但是没有#include头文件,然后调用xx()。细分下来又有这几种情况:

  • (1)编译目标是库文件(而不是可执行文件),xx()不是编译器内置函数;编译阶段仅仅报warning,运行时结果不对/不稳定
  • (2)编译目标是可执行文件,xx()不是编译器内置函数;链接阶段报错说找不到符号(函数定义)
  • (3)编译目标是库文件(而不是可执行文件),xx()是编译器内置函数;编译阶段仅仅报warning,运行时结果正确
  • (4)编译目标是可执行文件,xx()是编译器内置函数;编译阶段报warning;运行时结果正确

上述四种情况我们一一举例说明。每个例子都基于CMake构建。

(1)编库时调用了未定义函数(非编译器内置函数),编译只报warning;链接该库时报error

CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(hoho)
add_library(hoholib src/hoholib.c)
add_executable(hohoexe src/hohoexe.c)
target_link_libraries(hohoexe hoholib)

hoholib.c

void hello() {const char* name = "Chris";print_hello(name);
}

hohoexe.c

#include <stdio.h>int main() {hello();return 0;
}

VS2017编译输出:

GCC编译输出:

可以看到,问题在链接阶段才会报error,编译阶段仅报warning。编库是不需要链接的,只需要编译。如果忽略编库阶段的上述warning那就是埋雷。

(2)编库时调用了未定义函数(编译器内置同名函数),编译只报warning;链接该库时报error

首先明确下什么是编译器内置函数:对于gcc而言,定义了printf、fabs等函数,而这些函数是在C标准库、math库中定义的,gcc为了优化而提供了自己的实现,而如果用户没有链接相应的库、没有包含相应的头文件,则链接阶段找不到对应的符号表,但能找到built-in函数,因而直接调用built-in函数。这就是为什么“把(1)中调用的未定义函数换成fabs、printf等函数,gcc下链接阶段也不会报错反而能正确输出结果”的原因。参考:关于gcc内置函数和c隐式函数声明的认识以及一些推测

遗憾的是,这种取巧的做法对于Visual Studio行不通,因为cl.exe并没有和gcc完全相同的编译器内置函数。cl.exe的编译器内置函数叫做Compiler Intrinsics,并没有定义printf、fabs等函数。这就解释了“为什么调用了printf、fabs等gcc内置同名函数的代码,gcc下链接正常运行正确但在VS下链接出错”。

还是上面的CMake配置,C代码为:

hoholib.c

void hello() {const char* name = "Chris";printf("hello, %s\n", name);
}

hohoexe.c

#include <stdio.h>int main() {hello();return 0;
}

VS下编译报错,gcc下则编译链接都无error,可以运行并得到预期结果。

(3)编可执行时.c代码中使用了未定义的函数(编译器内置同名函数)

这种情况下,gcc编译链接无error且结果正确,VS则可能编译就报错,也可能编译链接通过但结果不对。

cmake_minimum_required(VERSION 3.14)
project(hoho)
add_executable(hohoexe src/hohoexe.c)

如果hohoexe.c的代码是这样:

int main() {const char* name = "Chris";printf("hello, %s\n", name);return 0;
}

则,VS下编译报错,gcc下编译链接无error且结果符合预期。

如果hohoexe.c的代码是这样:

#include <stdio.h>int main() {double x = -3.3;double y = fabs(x);printf("fabs(%lf)=%lf\n", x, y);return 0;
}

则VS下编译链接无error但结果不对:

fabs(-3.300000)=-858993460.000000

(4)编可执行时.c代码中使用了未定义的函数(编译器无内置同名函数)

这种情况下,VS和gcc都直接编译报错,没什么好说的:

#include <stdio.h>int main() {const char* name = "Chris";print_hello(name);return 0;
}

简单总结一下上述(1)~(4):对于printf、fabs、sin等常见函数,gcc有内置函数的实现使得一些代码尽管报warning但也能运行;同样的代码在Visual Studio下没法编译链接;对于用户自定义的函数,如果是编库,则编译阶段只报warning不报error,如果是可执行程序则会报error。对于小白和老菜鸟们,应该无论如何都把“未声明函数就使用”强制作为error,绝对不亏。C编译器的这个现象不免让人疑惑:你这该报错的不报错,误导人啊!然而有种说法是为了兼容老版本代码。嗯,简直无语的C编译器默认编译选项!

被C编译器默认报为warning而不是error、但实际上又很重要的编译选项,还有很多,而其中很多编译选项在C++中是默认为error的。如果项目允许,不妨使用C++编译器。而对于必须使用纯C的项目,就需要把C编译器中的这些严重warning都设定为error,提前发现问题解决问题。

个人总结的应当视作error的warning

下列警告应当视作错误(血泪教训):

1. 函数没有声明就使用

VS下为/we4013。gcc下用-Werror=implicit-function-declaration

2. 函数虽然有声明,但是声明不完整,没有写出返回值类型。

VS下开关为/we4431。gcc下用-Werror=implicit-int。注:其实implicit-function-declaration和implicit-int可以用一个implicit来替代。

3. 指针类型不兼容

VS下为/we4133。gcc下用-Werror=incompatible-pointer-types

4. 函数应该有返回值但是没有return返回值

VS下为/we4716。gcc下用-Werror=return-type

5. 使用了影子变量(shadow variable)

内层作用域重新声明/定义了与外层作用域中同名的变量。举一个例子说明shadow变量的危害:

void set_value(int* val) {double r = 0.0;if(isRandom) {double r = this->generateRandomNumber();}*val = r;
}

上述代码运行后,val的值始终是0而不可能被改成随机值。

VS下有好几个开关:/we6244 /we6246 /we4457 /we4456(MSDN上还有个 /we2082但实际用的时候提示无效: 命令行 warning D9014: 值“2082”对于“/we”无效;假定为“5999”)。gcc下用-Werror=shadow

6. 函数返回局部变量的地址

VS下的开关:/we4172。gcc下用-Werror=shadow -Werror=return-local-addr

7. 变量没有初始化就使用

函数调用完毕,无法保证用过的栈帧空间后续被如何使用(编译器是否开启优化、栈帧布局结构都有影响),不可侥幸。

VS下的开关:/we4700。gcc下用-Werror=uninitialized

8. printf等语句中的格式串和实参类型不匹配

例如%d匹配到了double,结果肯定不对,应当提前检查出来。

VS下的开关:/we4477。gcc下用-Werror=format

9. 把unsigned int和int类型的两个变量比较

有符号数可能在比较之前被转换为无符号数而导致结果错误。

VS下的开关:/we4018。gcc下用-Werror=sign-compare

10. 把int指针和int相互赋值

虽说可以把指针的值(一个地址)当做一个int(其实是unsigned int)来理解,但考虑这种情况:int a=*p被写成int a=p而引发错误。

VS下的开关:/we4047。gcc下用-Werror=int-conversion

因为上述N条规则是我自行制定的,有些是C++下默认视为错误,有些则是C++下也为警告。因此不妨把CFLAGS和CXXFLAGS都添加这些检查规则。

在开发环境中配置上述CFLAGS

建议基于CMakeLists.txt,现有Visual Studio工程也可配置,具体见文章第一部分“快速配置”。

其他配置方式说明:

  1. .c代码中使用#pragma warning (error: xxxx)。缺点:只有visual studio工程能用;不能确保所有文件有效
  2. Visual Studio工程属性中配置->配置属性->C/C++->高级->将特定的警告视为错误,填写"xxxx"。缺点:只适合Visual Studio;优点:非CMake生成的VS工程,适合。
  3. gcc编译时指定flags,例如gcc gcc xxx.c -Werror=implicit-function-declaration

CMakeLists.txt中配置说明:set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /weXXXX")(windows)或set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=xxxx")
其中,windows格式中XXXX为警告编号;gcc下xxxx为警告对应的字符串。这种方式个人推荐。

TODO

C++编译器默认链接C++标准库,C++标准库包含了math库;C编译器默认链接C标准库,C标准库不包含math库(参考:Why do you have to link the math library in C?)。问题来了:对于gcc,如果纯C代码调用了math函数而没有设定链接选项-lm,会使用gcc的built-in函数;同样的代码,VS2017并没有内置math库的函数,没有链接数学库的秦广下,为什么也能正确运行?

#include <stdio.h>
#include <math.h>int main() {double x = -3.3;double y = fabs(3.3);printf("fabs(%lf)=%lf\n", x, y);return 0;
}

实录

  • 2020-10-15 14:12:58 同事ZYC因为没有#include <stdlib.h>,导致malloc时出现分配非法地址,查bug查了大半天。包含utils.cmake后快速报错定位问题。

参考

/w, /W0, /W1, /W2, /W3, /W4, /w1, /w2, /w3, /w4, /Wall, /wd, /we, /wo, /Wv, /WX (Warning Level)

How to set compiler options with CMake in Visual Studio 2017

Make one gcc warning an error?

Can I treat a specific warning as an error?

Is there an equivalent of gcc's -Wshadow in visual C++

已释放的栈内存

GCC编译选项

关于gcc内置函数和c隐式函数声明的认识以及一些推测

Why do you have to link the math library in C?

编写合格的C代码(1):通过编译选项将特定警告视为错误相关推荐

  1. 用html编写一个贪吃蛇,HTML编写的贪吃蛇代码

    <HTML编写的贪吃蛇代码>由会员分享,可在线阅读,更多相关<HTML编写的贪吃蛇代码(14页珍藏版)>请在人人文库网上搜索. 1.使用表格 var fangxiang;fan ...

  2. 如何编写可测试的代码 哈利勒的方法论

    Understanding how to write testable code is one of the biggest frustrations I had when I finished sc ...

  3. html5转apicloud,使用APICloud编写优雅的HTML5代码

    使用APICloud编写优雅的HTML5代码<一>一.实现下拉刷新: 默认样式>代码清晰简洁明了,符合ECMA262规范的callback,最少只需5行代码: apiready = ...

  4. 《SQL与关系数据库理论——如何编写健壮的SQL代码》一第3章

    本节书摘来华章计算机<SQL与关系数据库理论--如何编写健壮的SQL代码>一书中的第3章 ,第3.1节 C. J. Date 著 单世民 何英昊 许侃 译 更多章节内容可以访问云栖社区&q ...

  5. 《编写高质量Python代码的59个有效方法》——第10条:尽量用enumerate取代range

    本节书摘来自华章社区<编写高质量Python代码的59个有效方法>一书中的第10条:尽量用enumerate取代range,作者[美]布雷特·斯拉特金(Brett Slatkin),更多章 ...

  6. 编写高效Excel VBA代码的最佳实践(一)

    很多Excel VBA文章和图书都介绍过如何优化VBA代码,使代码运行得更快.下面搜集了一些使Excel VBA代码运行更快的技术和技巧,基本上都是实践经验的总结.如果您还有其它优化Excel VBA ...

  7. 编写高性能的 Lua 代码

    原文出处: wuzhiwei 的博客   欢迎分享原创到伯乐头条 前言 Lua是一门以其性能著称的脚本语言,被广泛应用在很多方面,尤其是游戏.像<魔兽世界>的插件,手机游戏<大掌门& ...

  8. iOS 11开发教程(七)编写第一个iOS11代码Hello,World

    iOS 11开发教程(七)编写第一个iOS11代码Hello,World 代码就是用来实现某一特定的功能,而用计算机语言编写的命令序列的集合.现在就来通过代码在文本框中实现显示"Hello, ...

  9. 表示python代码块的是_编写高质量Python代码的59个有效方法,你用过几个

    欢迎点击右上角关注小编,除了分享技术文章之外还有很多福利,私信学习资料可以领取包括不限于Python实战演练.PDF电子文档.面试集锦.学习资料等. 这个周末断断续续的阅读完了<Effectiv ...

  10. 用python编写一个高效搜索代码工具

    用python编写一个高效搜索代码工具 大多码农在linux环境下使用grep+关键词的命令搜索自己想要的代码或者log文件.今天介绍用python如何编写一个更强大的搜索工具,windows下也适用 ...

最新文章

  1. <<温泉屋的小老板娘>>观后感
  2. 三因素三水平正交表l9_影响多腔导管挤出机头设计的关键因素
  3. Source Insight编辑器配置
  4. 检测到python编程环境中存在多个版本_linux下多个python版本怎么管理?
  5. Java 删除集合中指定的元素
  6. Nginx整合tomcat,实现反向代理和负载均衡
  7. 问题记录:EntityFramework 一对一关系映射
  8. java如何去掉文件后缀名_JAVA 递归批量更改文件后缀名 删除后缀
  9. 网狐6603全部架设过程棋牌源码下载搭建教程
  10. 站长 给windows7的系统保留分区设置驱动器号
  11. getch()函数怎么用
  12. 中国朝代历史,名字的简介
  13. Python实现自动群发自定义QQ消息
  14. java语言签到定位系统_百度地图定位签到功能
  15. 米转经纬度_经纬度换算米(经纬度精度换算米数)
  16. 网络系统管理 - C模块 - Centos7.9 - Iptables
  17. Java实现递归 斐波那契数列 阶乘
  18. Docker 入门学习
  19. 第5次作业+001+陈定国
  20. div固定在浏览器顶部

热门文章

  1. js开源框架最新版下载
  2. Java多线程学习(三)volatile关键字
  3. 如何在swift中实现数组的深拷贝
  4. MySQL类型float double decimal的区别
  5. oracle rac的特征
  6. css 3 制作水波状进度条
  7. Ext.js4 的Store携带参数加载中文,后台出现乱码解决办法
  8. Apache创建虚拟目录绑定域名
  9. web controls归档
  10. 处理MySql连接超时引起的错误