1.  如何生成 "半全局变量", 就是那种只能被部分源文件中的部分函数访问变量?

答:

这在C语言中办不到. 如果不能或不方便在一个源文件中放下所有的函数, 那么有三种的解决方案 :

(1) 为一个库或相关函数的包中所有函数的包中的所有函数和全局变量增加一个唯一的前缀, 并警

告包的用户不能定义和使用文档中列出的公有符号意外的任何带有相同前缀的其它符号. (换言之,

文档中没有提及的带有相同前缀的全局变量被约定为 "私有")

(2) 使用以下划线开头的名称, 因为这样的名称普通代码不能使用. (下划线开头表示"私有", 是一

种约束和建议)

(3) 通过连接器操作, 例如

- piyo.c
int love = ; - hoge.c
int like_you(void) {
extern int love;
return love + ;
}

在链接 hoge.o 的时候, 也需要 piyo.o 确定最终"半全局变量"地址.

2.  如何判断哪些标识符可以使用, 那些被保留了 ?

答:

(1) 标识符的3个属性: 作用域, 命名空间和链接类型.

  [] C 语言有4种作用域(标识符声明的有效区域): 函数, 文件, 块和原型. (第4种类型仅仅存在于函

数原型声明的参数列表中)

  [] C 语言有4种命名空间: 行标(label, 即 goto 的目的地), 标签(tag, 结构, 联合和枚举名称), 结构

联合成员, 以及标准所谓的其它"普通标识符"(函数, 变量, 类型定义名称和枚举常量). 另一个名称集(

虽然标准并没有称其为"命名空间")包括了预处理宏.这些宏在编译器开始考虑上述4种命名空间之前

就会被扩展.

  [] 标准定义了3中"链接类型": 外部链接, 内部链接, 无链接. 对我们来说, 外部链接就是指全部变量,

非静态变量和函数(在所有的源文件中有效); 内部链接就是指限于文件作用域内的静态函数和变量; 而

"无链接"则是指局部变量及类型定义(typedef)名称和枚举常量.

(2) ANSI/ISO C标准标识符标准建议规则:

  规则1: 所有下划线大头, 后跟一个大写字母或另一个下划线的标识符永远保留(所有的作用域, 所

有的命名空间).

  规则2: 所有以下划线打头的标识符作为文件作用域的普通标识符(函数, 变量, 类型定义和枚举常

量)保留(为编译器后续实现保留).

  规则3: 被包含的标准头文件中的宏名称的所有用法保留.

  规则4: 标准中所有具有外部链接属性的标识符(即函数名)永远保留用作外部链接标识符.

  规则5: 在标准头文件中定义的类型定义和标签名称, 如果对应的头文件被包含, 则在(同一个命名

空间中的)文件作用域内保留.(事实上, 标准声称"所有作用于文件作用域的标识符", 但规则4没有包含

标识符只剩下类型定义和标签名称了.)

3. char a{[3]} = "abc"; 是否合法 ?

答:

也许远古时期这样的表达式是合法的, 但现在(2018-08-14)是不合法的!

> error C2143: 语法错误: 缺少“;”(在“{”的前面)
> error C2143: 语法错误: 缺少“;”(在“[”的前面)
> error C2109: 下标要求数组或指针类型
> fatal error C1004: 发现意外的文件尾

char a[3] = "abc"; 是合法的. 最后的 '\0' 没有填充进去.

4. 程序运行正确, 但退出却 "core dump"(核心转存)了, 怎么回事?

struct list {
struct list * next;
char * item;
} /* Here is the main program */
main(argc, argv) {
puts("Hello, 世界");
}

答:

!!! 不要和写出上面的格式的人说代码, 怕你沟通能力有问题 ~

也许以前是崩溃, 但现在并没有, 可以正常运行.  书中对于崩溃给出理由是, 一般而言, 返回结构的函数

编译器在实现时,会加入一个隐含的返回指针, 这样产生的 main 函数试图接受 3 个参数, 而实际上只有

两个传入(这里,由C的启动代码传入).

他说的很有挖掘价值, 但先进一点编译器兼容性很好. 它也为 main 函数在启动函数栈中构建了返回结

构的实体, 所以没有崩溃.

我们来看这样一段代码

 #include <stdio.h>

 struct list {
struct list * next; double number;
char * item;
int piyo;
}; struct list list_get(void) {
return (struct list){ NULL, , "Hello, 世界", };
} /* Here is the main program */
int main(int argc, char * argv[]) {
struct list node = list_get();
puts(node.item); return ;
}

运行到 17 行调试看反汇编代码

    struct list node = list_get();
002444EE lea eax,[ebp-0FCh]
002444F4 push eax
002444F5 call _list_get (024137Ah)

上面 push eax 表示传入了隐式地址. 这里有个有意思的现象, 如果我们返回的结构体很小例如

struct list {
struct list * next;
char * item;
};

x64 是 16 字节, 编译器直接通过两个寄存器搞定, 来帮我们优化代码. 也不会在调用局部栈中构

造一个对象, 传入地址.

在我们返回结构体时候, 编译器帮我们"隐含" 传入结构体指针(寄存器)参数. 目前不推荐这样的做

法, 因为存在浅拷贝性能浪费. 这也是 C++ 引入移动复制的原因. 但没有屌用, 因为这本身就应该

编译器去做.而不是让程序员和编译器双宿双飞, 可能下一代智能编译器会优化的更好. 标准应该

推荐采用下面做法. 从这细节也可以看出, C 系列程序员对操作系统有种天然亲和力, 这种亲和力

也是把双刃剑, 让他太过于着魔, 落叶缤纷, 而败北在杨柳树下 ~

// 显示声明
void list_get(struct list * const out) {
out->next = NULL;
out->item = "Hello, 世界";
} // 调用
struct list node; list_get(&node);

5. 可否用显式括号来强制执行我所需要的计算顺序并控制相关副作用? 就算括号不行, 操作符优先

 级是否能够计算顺序呢?

答:

一般来说, 不行. 操作符优先级和显示括号对表达式的计算顺序只有部分影响. 在如下的代码中

  f() + g() * h()

尽管我们知道乘法运算在加法之前, 但这并不能说明这3个函数哪个会被调用. 换言之, 操作符优先

级只是 "部分" 地决定了表达式的求值顺序. 这里的 "部分" 并不包括对操作数的求值.

  括号告诉编译器那个操作数和那个操作数结合, 但并不要求编译器先对括号内的表达式求值 .

在上面表达式中再加括号

  f() + ( g() * h() )

也无助于改变函数调用的顺序. 同样, 对 i++ * i++ 的表达式加括号也毫无帮助, 因为 ++ 比 * 的优

先级高:

  (i++) * (i++)       /* WRONG */

这个表达式有没有括号都是未定义的.

  如果需要确保子表达式的计算顺序, 可能需要使用显式的临时变量和独立语句.

6.  我有些代码包含这样的表达式.

  a ? b = c : d

 有些编译器可以接受, 有些却不能. 为什么 ?

答:

在 C 语言原来的定义中, = 的优先级是低于 ? : 的, 因此早期的编译器倾向于这样解释这个表达式:

  (a ? b) : (c : d)

然而, 因为这样没什么意义, 后来编译器都接受了这种表达式, 并用这样的方式解释(就像里面暗含

了一对括号);

  a ? (b = c) : d

这里, = 号的左操作数只是 b, 而不是非法的 a ? b. 实际上 ANSI/ISO C 标准中指定的语法就要求这

样的解释. (标准中关于这个的语法不是基于优先级的, 且指出了在 ? 和 : 符号之间可以出现任何表

达式).

  问题中这样的表达式可以毫无问题地被 ANSI 编译器接受. 如果需要在较老的编译器上编译, 总

可以增加一对内部括号.

  历史总是那么有意思, 现在编译器都已经支持这个跳过优先级的而存在的表达式了 ~ 毕竟那是

标准.

7.  我有一个 char * 型指针碰巧指向一些 int 型变量, 我想跳过它们. 为什么 ((int *)p)++; 这样的代码

 不行?

答:

在 C 语言中, 类型转换操作符并不意味者 "把这些二进制位看作另一种类型, 并作相应的处理". 这是一个

转换操作符, 根据定义它只能生成一个右值(rvalue). 而右值即不能赋值, 也不能用 ++ 自增. (如果编译器

接受这样的表达式, 那要么是一个错误, 要么是有意做出非标准扩展.) 要达到你的目的可以用

  p = (char *)((int *)p + 1);

或者, 因为 p 是 char * 型, 直接用

  p += sizeof(int);

要想真正明白无误, 你得用

  int * ip = (int *)p;

  p = (char *)(ip + 1);

但是, 可能的话, 你还是应该一开始就选择适当的指针类型, 而不是一味地试图桃僵李代.

 8.  我看到下面这样的代码:

  char * p = malloc(strlen(s) + 1);

  strcpy(p, s);

 难道不应该是 malloc((strlen(s) + 1) * sizeof(char)) 吗?

答:

永远不必乘上 sizeof(char), 因为根据定义, sizeof(char) 严格为 1. 另一方面, 乘上 sizeof(char) 也没有

害处, 有时候还可以帮忙为表达式引入 size_t 类型.

而且就算 char 类型定义为 16 位, sizeof (char) 依然是1, 而 <limits.h> 中 CHAR_BIT 会被定义为 16. 届

时将不能声明 (或用 malloc 分配) 一个 8位的对象.

  传统上, 一个字节不一定是8位, 它不过是一小段内存, 通常适于存储一个字符. C 标准遵循了这种用

法, 因此 malloc 和 sizeof 所使用的字节可以是 8 位以上(8位字节正式称为八位字节 octet, 标准不允许

低于 8 位).

  为了不用扩展 char 类型就能操作多语言字符集, ANSI/ISO C 定义了 "宽"字符类型 wchar_t 以及对

应的宽字符常量和宽字符串字面量, 同时也提供了操作和转换宽字符串函数.

可能有些令人惊讶, 在C语言中字符字面量是 int 类型, 因此 sizeof ('a') 是 sizeof (int) 而不是 sizeof (char)

这是和 C++ 中不同地方, C++ 'a' 被当作 char 类型字符字面量.

9.  我很吃惊, ANSI 标准竟然有那么多未定义的东西. 标准的唯一任务不就是让这些东西标准化吗?

答: 

某些构造随编译器和硬件的实现而变化吗这一直是C语言的一个特点. 这种有意的不严格可以让编译器

生成效率更高的代码, 而不必让所有程序为了不合理的情况承担额外的负担. 因此, 标准只是把现存的实

践整理成文.

  编程语言标准可以看作是语言使用者和编译器实现者之间的协议. 协议的一部分是编译器实现者同

意提供, 用户可以使用的功能. 而其它部分则包括用户同意遵守和编译器实现者认为会被遵守的规则. 只

要双方都恪守自己的保证, 程序就可以正确运行. 如果任何一方违反它的诺言, 则结果肯定失败.

  面对未定义行为的时候(包括范围内的实现定义行为和不确定行为), 编译器可能做任何实现, 其中也

包括你所期望的结果. 但是依赖这个实现却不明智.

Roger Miller 提供了看待这个问题另一个角度:

 "有人告诉我打篮球的时候不能抱着球跑. 我拿个篮球, 抱着就跑, 一点问题都没有. 显然他并不懂篮球."

10. 有什么好的方法来检查浮点数在 "足够接近" 情况下相等?

答: 

浮点数的定义决定它的绝对精度会随着其量级而变化, 所以比较两个浮点数的最好方法就要利用一个浮

点数的量级相关的精确阈值. 不要用下面这样的代码:

double a, b;
...
if (a == b) /* WRONG */

要用类似这样的方法(相对因子):

#include <math.h>
#include <float.h> if (fabs(a - b) <= fabs(a) * DBL_EPSILON) #define DBL_EPSILON      2.2204460492503131e-016 // smallest such that 1.0+DBL_EPSILON != 1.0

DBL_EPSILON 是 float.h 中一个特定极小的值来控制"接近度".

if (fabs(a - b) < 0.001) /* POOR */

对于上面 0.001 这样的绝对模糊因子恐怕难以持续有. 随着被比较的数不断变化, 很有可能两个较小的, 本

不应该看作不相等的数正好相差小于 0.001, 而两个本应看作相等的两个大数相差大于 0.001. (显然, 模糊

因子修改为0.0005或者0.00001或者其它任何绝对数都无助于解决这个问题.)

  Doug Gwyn 推荐用下面的 "相对差" 函数. 它返回两个实数的相对差值, 如果两个数完全相同, 则返回

0.0, 否则, 返回差值和较大数的比值:

#include <math.h>
#include <float.h>
#include <stdlib.h> inline double reldif(double a, double b) {
double c = fabs(a), d = fabs(b);
d = max(c, d);
return d == ? : fabs(a-b)/d;
}

典型的用法是:

if (reldif(a, b) < DBL_EPSILON) ...

11. 我有个接受 float 型的变参函数, 为什么 va_arg(arg, float) 却不行 ?

答:

"默认参数提升" 规则适用于可变参数中可变部分: 参数类型为 float的总是提升到 double, char 和 short

提升到 int (无符号 unsigned). 所以 va_arg(arg, float) 是错误用法. 应该使用 va_arg(arg, double). 同理,

要用 va_arg(arg, int) 来取得原来类型是 char, short 或 int 的参数. 基于同样的理由, 传给 va_start 的最

后一个"固定"参数类型不会被提升. (printf(char const* const fmt, ...) 类比 fmt 参数一定不会被提升.)

12. 用什么方法计算整数中为1的位的个数最高效? 

答:

许多像这样的位操作可以使用查找表格来提高效率和速度. 这段代码是以每次4位的方式计算数值中为1

的位个数的小函数:

int bitcnt(unsigned u) {
static int bitc[] = {, , , , , , , , , , , , , , , }; int n = ;
while (u) {
n += bitc[u & 0xFF];
u >>= ;
}
return n;
}

这个查表思路极快, 突破在于更大的表, 空间换时间. 还有一种微软面试题套路是

int count(unsigned u) {
int n = ;
while (u) {
++n;
u = (u-) & u;
}
return n;
}

二者对比一下, 最坏情况 32 个 1 第一个好, 最好情况 0 个 1 两个一样. 但对于 0xF000 情况前者是4次循环,

后者 1次. 但拍脑门还是前者好. 预计 32位无符号数出现 1 的期望是 16, 前者最坏执行 8 次, 后者一定要执

行16次.从数学期望上面而言前者占优势, 毕竟算法 1 后续还可以构建一字节表更迅速 ~ 权当一乐.

13. 什么是 "达夫设备" (Duff's Device)

答: 

这是个很棒的迂回循环展开法, 由 Tom Duff 在 Lucasfilm 时设计. 它的 "传统" 形态是用来复制多个字节

void copy(int * to, int from[], int count) {
register n = (count + ) / ; /* count > 0 assumed */ switch(count % ) {
case :
do {
*to = *from++;
case :
*to = *from++;
case :
*to = *from++;
case :
*to = *from++;
case :
*to = *from++;
case :
*to = *from++;
case :
*to = *from++;
case :
*to = *from++;
} while (--n > );
}
}

这里 count 个字节从 from 指向的数组复制到 to 指向的内存地址(这是个内存映射的输出寄存器, 这也是

为什么它没有被增加). 它把 switch 语句和复制 8 个字节的循环交织在一起, 从而解决了剩余字节的处理

问题(当 count 不是 8 的倍数时). 信不信由你, 像这样的 case 标志放在嵌套在 switch 语句内的模块中是

合法的. 当他向 C 的开发者和世界公布这个技巧时. Diff 注意到 C 的 switch 语法, 特别时"跌落"行为, 一

直是备受争论的, 而 "这段代码在争论中形成了某种论据, 但我不清楚是赞成还是反对".

后记 - 引述

- 错误是难免, 欢迎指正交流提升 ~

13 年刚工作的时候在地铁上看完 <<C语言问题>>, 随后就扔掉了. 过去好久,  18 年有幸又买了一本

<<C语言问题>> 看完后被其中好多段子说的心痒难耐. 所以就记录一些很经典的讨论, 供大家一块开

怀. 了解那些尘封在历史长河中, 问题由来的真相 ~

-------: ( :--

- https://y.qq.com/n/yqq/song/003K5qlb0r7BDb.html?ADTAG=baiduald&play=1

我们都会上岸,阳光万里,去哪里都是鲜花开放。

C 语言问题的更多相关文章

  1. C语言 · 高精度加法

    问题描述 输入两个整数a和b,输出这两个整数的和.a和b都不超过100位. 算法描述 由于a和b都比较大,所以不能直接使用语言中的标准数据类型来存储.对于这种问题,一般使用数组来处理. 定义一个数组A ...

  2. Windows server 2012 添加中文语言包(英文转为中文)(离线)

    Windows server 2012 添加中文语言包(英文转为中文)(离线) 相关资料: 公司环境:亚马孙aws虚拟机 英文版Windows2012 中文SQL Server2012安装包,需要安装 ...

  3. iOS开发系列--Swift语言

    概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...

  4. C语言 · Anagrams问题

    问题描述 Anagrams指的是具有如下特性的两个单词:在这两个单词当中,每一个英文字母(不区分大小写)所出现的次数都是相同的.例如,"Unclear"和"Nuclear ...

  5. C语言 · 字符转对比

    问题描述 给定两个仅由大写字母或小写字母组成的字符串(长度介于1到10之间),它们之间的关系是以下4中情况之一: 1:两个字符串长度不等.比如 Beijing 和 Hebei 2:两个字符串不仅长度相 ...

  6. JAVA语言中的修饰符

    JAVA语言中的修饰符 -----------------------------------------------01--------------------------------------- ...

  7. Atitit 项目语言的选择 java c#.net  php??

    Atitit 项目语言的选择 java c#.net  php?? 1.1. 编程语言与技术,应该使用开放式的目前流行的语言趋势1 1.2. 从个人职业生涯考虑,java优先1 1.3. 从项目实际来 ...

  8. 【开源】简单4步搞定QQ登录,无需什么代码功底【无语言界限】

    说17号发超简单的教程就17号,qq核审通过后就封装了这个,现在放出来~~ 这个是我封装的一个开源项目:https://github.com/dunitian/LoTQQLogin ————————— ...

  9. InstallShield 脚本语言学习笔记

    InstallShield脚本语言是类似C语言,利用InstallShield的向导或模板都可以生成基本的脚本程序框架,可以在此基础上按自己的意愿进行修改和添加.     一.基本语法规则      ...

  10. 用C语言封装OC对象(耐心阅读,非常重要)

    用C语言封装OC对象(耐心阅读,非常重要) 本文的主要内容来自这里 前言 做iOS开发的朋友,对OC肯定非常了解,那么大家有没有想过OC中NSInteger,NSObject,NSString这些对象 ...

随机推荐

  1. Udp广播的发送与接收(C#+UdpClient) 上篇

    简介: Udp广播消息用在局域网的消息传递很方便.本文使用UdpClient类在WPF下实现Udp广播收发 发送: void MainWindow_Loaded(object sender, Rout ...

  2. [UVALive 3902] Network

    图片加载可能有点慢,请跳过题面先看题解,谢谢 一道简单的贪心题,而且根节点已经给你了(\(S\)),这就很好做了. 显然,深度小于等于 \(k\) 的都不用管了(\(S\) 深度为0),那么我们只需要 ...

  3. Alpha 冲刺 —— 十分之五

    队名 火箭少男100 组长博客 林燊大哥 作业博客 Alpha 冲鸭鸭鸭鸭鸭! 成员冲刺阶段情况 林燊(组长) 过去两天完成了哪些任务 协调各成员之间的工作 协助测试的进行 测试项目运行的服务器环境 ...

  4. 【XSY2753】LCM

    Description 给你\(n,k\),要你选一些互不相同的正整数,满足这些数的lcm为\(n\),且这些数的和为\(k\)的倍数. 求选择的方案数.对\(232792561\)取模. \(n\l ...

  5. linux内核分析 第七周 Linux内核如何装载和启动一个可执行程序

    一.编译链接的过程和ELF可执行文件格式 vi hello.c gcc -E -o hello.cpp hello.c -m32 //预处理.c文件,预处理包括把include的文件包含进来以及宏替换 ...

  6. FastDFS分布式存储

    分布式存储分类 通用分布式存储:mogilefs, fastdfs, ...(无文件系统接口, 通过API访问) 专用分布式存储:即分布式文件系统, moosefs, ...(有文件系统接口) GFS ...

  7. java堆溢出

    java堆是用来存储对象实例的:只要不断创建对象,GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,当对象数量达到最大堆的容量限制的时候就会产生内存溢出异常.异常对战信息为OutOf ...

  8. Android中的Surface, SurfaceHolder, SurfaceHolder.Callback, SurfaceView

    传入一个surface,然后让openGL在surface上画图 window->view hierachy(DecorView是tree的root)->ViewRoot->Surf ...

  9. Socket通信的简单例子

    客户端代码: package com.bobohe.socket; import java.io.*; import java.net.*; public class TalkClient { pub ...

  10. c++ 顶层const与底层const

    底层const是代表对象本身是一个常量(不可改变):      顶层const是代表指针的值是一个常量,而指针的值(即对象的地址)的内容可以改变(指向的不可改变): #include <iost ...