本篇文章作为《必须弄懂的495个C语言问题》的学习笔记,进行查漏补缺,文章中列出部分经典问题,方便后续查看
1.定义一个包含 N 个指向返回 指向字符的指针的函数的指针的数组
1.char * (*(*a[N])())();
2. 用 typedef 逐步完成声明:
typedef char *pc; /* 字符指针*/
typedef pc fpc(); /* 返回字符指针的函数 */
typedef fpc *pfpc; /* 上面函数的指针*/
typedef pfpc fpfpc(); /* 返回函数指针的函数 */
typedef fpfpc *pfpfpc; /* 上面函数的指针*/
pfpfpc a[N]; /* 上面指针的数组*/
- 使用 cdecl 程序, 它可以把英文翻译成 C 或者把 C 翻译成英文:
cdecl> declare a as array of pointer to function returning
pointer to function returning pointer to char
char *(*(*a[])())()
通过类型转换, cdecl 也可以用于解释复杂的声明, 指出参数应该进入哪一对括号 (如同在上述的复杂函数定义中)
2.如何确定域在结构中的字节偏移?
ANSI C 在 <stddef.h> 中定义了 offsetof() 宏, 用 offsetof(struct s, f) 可以计算出域 f 在结构 s 中的偏移量。如果出于某种原因, 需要自己实现这个功能, 可以使用下边这样的代码:
#define offsetof(type, f) ((size_t) \
((char *)&((type *)0)->f - (char *)(type *)0))
这种实现不是 100% 的可移植; 某些编译器可能会合法地拒绝接受。
3.为什么这样的代码: a[i] = i++; 不能工作?
子表达式 i++ 有一个副作用 —– 它会改变 i 的值 —– 由于 i 在同一表达式的其它地方被引用, 这会导致无定义的结果, 无从判断引用(左边的 a[i] 中)是旧值还是新值。
4.++i 和 i++ 有什么区别?
简单而言: ++i在 i 存储的值上增加一并向使用它的表达式 “返回” 新的, 增加后的值; 而 i++ 对 i增加一, 但返回原来的是未增加值。
5.根据条件把一个复杂的表达式赋值给两个变量中的一个,可以用((condition) ? a : b) = compli-cated expression;吗?
不能。? : 操作符, 跟多数操作符一样, 生成一个值, 而不能被赋值。换言之, ?: 不能生成一个 “左值”。如果你真的需要, 你可以试试下面这样的代码:
*((condition) ? &a : &b) = complicated_expression;
6.*p++ 自增 p 还是 p 所指向的变量?
后缀 ++ 和 – 操作符本质上比前缀一目操作的优先级高, 因此 p++ 和(p++) 等价, 它自增 p 并返回 p 自增之前所指向的值。要自增 p 指向的值, 使用(*p)++, 如果副作用的顺序无关紧要也可以使用 ++*p。
7.C 语言中 “指针和数组等价” 到底是什么意思?
在 C 语言中对数组和指针的困惑多数都来自这句话。说数组和指针 “等价”不表示它们相同, 甚至也不能互换。它的意思是说数组和指针的算法定义可以用指针方便的访问数组或者模拟数组。
特别地, 等价的基础来自这个关键定义:一个 T 的数组类型的左值如果出现在表达式中会蜕变为一个指向数组第一个成员的指针(除了三种例外情况); 结果指针的类型是 T 的指针。
这就是说, 一旦数组出现在表达式中, 编译器会隐式地生成一个指向数组第一个成员地指针, 就像程序员写出了 &a[0] 一样。例外的情况是, 数组为 sizeof 或 &操作符的操作数, 或者为字符数组的字符串初始值。
作为这个这个定义的后果, 编译器并那么不严格区分数组下标操作符和指针。在形如 a[i] 的表达式中, 根据上边的规则, 数组蜕化为指针然后按照指针变量的方式如 p[i] 那样寻址, 如问题 6.2 所述, 尽管最终的内存访问并不一样。如果你把数组地址赋给指针:
p = a;
那么 p[3] 和 a[3] 将会访问同样的成员。
8.现实地讲, 数组和指针地区别是什么?
数组自动分配空间, 但是不能重分配或改变大小。指针必须明确赋值以指向分配的空间 (可能使用 malloc), 但是可以随意重新赋值(即, 指向不同的对象), 同时除了表示一个内存块的基址之外, 还有许多其它的用途。由于数组和指针所谓的等价性, 数组和指针经常看起来可以互换, 而事实上指向 malloc 分配的内存块的指针通常被看作一个真正的数组(也可以用 [ ] 引用)。
9.包含 5[“abcdef”] 这样的 “表达式”。这为什么是合法的 C 表达式呢?
数组和下标在 C 语言中可以互换。这个奇怪的事实来自数组下标的指针定义, 即对于任何两个表达式 a 和 e, 只要其中一个是指针表达式而另一个为整数, 则 a[e] 和 *((a)+(e)) 完全一样。
10.当我 malloc() 为一个函数的局部指针分配内存时, 我还需要用free() 明确的释放吗?
是的。记住指针和它所指向的东西是完全不同的。局部变量在函数返回时就会释放, 但是在指针变量这个问题上, 这表示指针被释放, 而不是它所指向的对象。用 malloc() 分配的内存直到你明确释放它之前都会保留在那里。一般地, 对于每一个 malloc() 都必须有个对应的 free() 调用。
11.分配一些结构, 它们包含指向其它动态分配的对象的指针。释放结构的时候, 还需要释放每一个下级指针吗?
是的。一般地, 你必须分别向 free() 传入 malloc() 返回的每一个指针, 仅仅一次 (如果它的确要被释放的话)。一个好的经验法则是对于程序中的每一个malloc() 调用, 你都可以找到一个对应的 free() 调用以释放 malloc() 分配的内存。
12.calloc() 和 malloc() 有什么区别?利用 calloc 的零填充功能安全吗?free() 可以释放 calloc() 分配的内存吗, 还是需要一个cfree()?
calloc(m, n) /*本质上等价于*/
p = malloc(m * n);
memset(p, 0, m * n);
填充的零是全零, 因此不能确保生成有用的空指针值或浮点零值。free() 可以安全地用来释放 calloc() 分配的内存。
13.sizeof(’a’) 是 2 而不是 1 (即,不是 sizeof(char))。
C 语言中的字符常数是 int 型, 因此 sizeof(’a’) 是 sizeof(int),这是另一个与 C++ 不同的地方。
14.书写多语句宏的最好方法是什么?
#define MACRO(arg1, arg2) do {\
/* declarations */\
stmt1;\
stmt2;\
/* ... */\
} while(0)
/* 没有结尾的 ; */
15.把什么放到 .h 文件(头文件)
作为一般规则, 你应该把这些东西放入头 (.h) 文件中:
• 宏定义 (预处理 #defines)
• 结构、联合和枚举声明
• typedef 声明
• 外部函数声明
• 全局变量声明
当声明或定义需要在多个文件中共享时, 尤其需要把它们放入头文件中。特别是, 永远不要把外部函数原型放到 .c 文件中。另一方面, 如果定义或声明为一个 .c 文件私有, 则最好留在 .c 文件中。
16.一种流行的头文件定义技巧
#ifndef HFILENAME_USED
#define HFILENAME_USED
/*... 头文件内容 ...*/
#endif
每一个头文件都使用了一个独一无二的宏名。这令头文件可自我识别,以便可以安全的多次包含; 而自动 Makefile 维护工具 (无论如何, 在大型项目中都是必不可少的) 可以很容易的处理嵌套包含文件的依赖问题。
17.#include <> 和 #include “” 有什么区别?
<> 语法通常用于标准或系统提供的头文件, 而 “” 通常用于程序自己的头文件。
18.完整的头文件搜索规则是怎样的?
准确的的行为是由实现定义的,这就是应该有文档说明; 通常, 用 <> 括起来的头文件会先在一个或多个标准位置搜索。用 “” 括起来的头文件会首先在 “当前目录” 中搜索, 然后 (如果没有找到) 再在标准位置搜索。
19.ANSI 引入了一个明确定义的标识符粘结操作符 ——
#define Paste(a, b) a##b
20.不能象这样在初始化和数组维度中使用常量:const int n = 5; int a[n];
const 限定词真正的含义是 “只读的”; 用它限定的对象是运行时 (同常) 不能被赋值的对象。因此用 const 限定的对象的值并不完全是一个真正的常量。在这点上 C 和 C++ 不一样。如果你需要真正的运行时常量, 使用预定义宏 #define(或enum)。
21.“const char *p” 和 “char * const p” 有何区别?
“const char *p” (也可以写成 “char const *p”) 声明了一个指向字符常量的指针, 因此不能改变它所指向的字符; “char * const p” 声明一个指向 (可变) 字符的指针常量, 就是说, 你不能修改指针。
22.能否把 main() 定义为 void, 以避免扰人的 “main无返回值”警告?
不能。main() 必须声明为返回 int, 且没有参数或者接受适当类型的两个参数。如果你调用了 exit() 但还是有警告信息, 你可能需要插入一条冗余的 return语句 (或者使用某种 “未到达” 指令, 如果有的话)。
把函数声明为 void 并不仅仅关掉了警告信息:它可能导致与调用者(对于main(), 就是 C 运行期初始代码) 期待的不同的函数调用/返回顺序。
23.#pragma 是什么, 有什么用?
#pragam 指令提供了一种单一的明确定义的 “救生舱”, 可以用作各种 (不可移植的) 实现相关的控制和扩展: 源码表控制、结构压缩、警告去除 (就像 lint 的老 /* NOTREACHED */ 注释), 等等。
24.memcpy() 和 memmove() 有什么区别?
如果源和目的参数有重叠, memmove() 提供有保证的行为。而 memcpy()则不能提供这样的保证, 因此可以实现得更加有效率。如果有疑问, 最好使用memmove()。
25.用 printf 实现可变的域宽度?
printf("%*d", width, x);
26.一个 float 变量赋值为 3.1 时, 为什么 printf 输出的值为 3.0999999?
大多数电脑都是用二进制来表示浮点和整数的。在十进制里, 0.1 是个简单、精确的小数, 但是用二进制表示起来却是个循环小数 0.0001100110011 . . . 。所以3.1 在十进制内可以准确地表达, 而在二进制下不能。
在对一些二进制中无法精确表示的小数进行赋值或读入再输出时, 也就是从十进制转成二进制再转回十进制, 你会观察到数值的不一致. 这是由于编译器二进制/十进制转换例程的精确度引起的, 这些例程也用在 printf 中。
27.怎样写类似 printf() 的函数, 再把参数转传给 printf() 去完成大部分工作?
用 vprintf(), vfprintf() 或 vsprintf()。
下面是一个 error() 函数, 它列印一个出错信息, 在信息前加入字符串 “error: ”
和在信息后加入换行符:
#include <stdio.h>
#include <stdarg.h>
void error(const char *fmt, ...)
{
va_list argp;
fprintf(stderr, "error: ");
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr, "\n");
}
28.怎样判断机器的字节顺序是高字节在前还是低字节在前?
int x = 1;
if(*(char *)&x == 1)
printf("little-endian\n");
else
printf("big-endian\n");
以上就是我认为C语言学习中比较经典的问题,感谢大家阅读。