### 《你必须知道的495个C语言问题》知识点总结
#### 一、声明和初始化
**1.1 如何决定使用哪种整数类型?**
- 在选择整数类型时,需考虑数据范围、存储需求以及是否需要固定长度(如`short`、`int`、`long`)。此外,应考虑标准库`<stdint.h>`中的固定宽度整数类型(如`int32_t`),这些类型跨平台表现一致。
**1.2 64位机上的64位类型是什么样的?**
- 在64位系统上,`long`通常为64位,但在某些系统中也可能保持32位。`long long`则通常为64位。使用`<stdint.h>`中的`int64_t`类型可以确保64位整数的使用。
**1.3 怎样定义和声明全局变量和函数最好?**
- 全局变量应在尽可能少的文件中声明,并且使用`static`关键字限制其作用域。函数声明应放在头文件中以便其他源文件可以使用。
**1.4 `extern`在函数声明中是什么意思?**
- `extern`用于声明一个已在其他地方定义的对象(变量或函数)。在C语言中,大多数函数和外部变量的声明默认都是`extern`的,因此该关键字通常可省略。
**1.5 关键字`auto`到底有什么用途?**
- `auto`关键字用于声明局部变量,默认情况下所有局部变量都具有`auto`属性。在现代C中,`auto`的作用不大,因为局部变量默认就是`auto`的。
**1.6 C语言中一个结构不能包含指向自己的指针吗?**
- C语言支持结构体包含指向自身的指针。示例代码中的问题可能在于指针类型的不一致或声明语法错误。正确的做法应该是:
```c
typedef struct Node {
char *item;
struct Node *next;
} NODEPTR;
```
**1.7 怎样建立和理解非常复杂的声明?**
- 复杂声明可通过逐步分解来理解。例如,一个包含N个指向返回指向字符的指针的函数的指针的数组可以这样声明:
```c
int (*ptr_array[N])(char **);
```
这里`ptr_array`是一个包含N个元素的数组,每个元素都是指向返回指向字符的指针的函数的指针。
**1.8 函数只定义了一次,调用了一次,但编译器提示非法重定义了。**
- 这种情况通常是由于多个头文件包含相同函数的声明导致的。解决方法是在头文件中使用`#ifndef`/`#define`/`#endif`预处理器指令来避免重复声明。
**1.9 `main()`的正确定义是什么?`void main()`正确吗?**
- 标准C语言规范中,`main()`函数应返回一个整型值,表示程序执行的状态。正确的定义是`int main(void)`或`int main(int argc, char *argv[])`。`void main()`不是标准的,尽管有些编译器支持。
**1.10 对于没有初始化的变量的初始值可以作怎样的假定?**
- 未初始化的局部变量具有不确定的值。未初始化的全局变量在C99及以后版本中默认初始化为零值。零值可以被解释为空指针或浮点零,具体取决于变量类型。
**1.11 代码`int f(){char a[]="Hello,world!";}`不能编译。**
- 这是因为字符串字面量不能直接赋给数组。正确的做法是:
```c
int f() {
char a[] = "Hello,world!";
// 使用a
}
```
**1.12 这样的初始化有什么问题?`char *p = malloc(10);`**
- `malloc`返回的是`void *`类型,需要显式转换为相应指针类型。此外,应检查`malloc`是否成功分配内存,正确做法:
```c
char *p = (char *)malloc(10);
if (p == NULL) {
// 错误处理
}
```
**1.13 以下的初始化有什么区别?**
- `char a[]="stringliteral";`创建了一个数组,内容是字符串字面量;而`char *p="stringliteral";`创建了一个指向字符串字面量的指针。修改指针所指向的字符会导致未定义行为。
**1.14 我总算弄清楚函数指针的声明方法了,但怎样才能初始化呢?**
- 函数指针可以通过将其绑定到函数名上来初始化。例如:
```c
int add(int x, int y) { return x + y; }
int (*func_ptr)(int, int) = add;
```
#### 二、结构、联合和枚举
**2.1 声明`struct x1 {};`和`typedef struct {} x2;`有什么不同?**
- 前者声明了一个名为`x1`的结构体类型,后者声明了一个名为`x2`的结构体类型并定义了一个同名的别名。
**2.2 为什么`struct x {}; x the_struct;`不对?**
- 这样声明会导致编译器错误,因为`struct x {};`声明了类型`x`,但没有为其提供任何名称。正确做法:
```c
struct x {
// 成员
} the_struct;
```
**2.3 一个结构可以包含指向自己的指针吗?**
- 结构体可以包含指向自身的指针。这种结构体可用于实现链表、树等数据结构。
**2.4 在C语言中实现抽象数据类型什么方法最好?**
- 可以通过结构体、枚举和函数组合来实现抽象数据类型。将数据隐藏在结构体内部并通过公共接口函数来访问这些数据可以提高封装性和安全性。
**2.5 在C中是否有模拟继承等面向对象程序设计特性的好方法?**
- C语言本身不支持面向对象编程特性,如继承。但可以通过结构体嵌套和函数指针等方式模拟出面向对象的特性。
**2.6 是否有自动比较结构的方法?**
- 没有内置的自动比较结构的方法,但可以通过循环遍历结构体成员或使用专门的函数来实现比较。
**2.7 如何向接受结构参数的函数传入常数值?**
- 可以通过传递结构体实例或指向结构体的指针来实现。如果结构体很大,传递指针更高效。
**2.8 怎样从/向数据文件读/写结构?**
- 通常使用`fwrite`和`fread`函数来读写结构体数据到二进制文件。为了兼容性和可移植性,考虑使用文本格式如JSON或XML。
**2.9 我的编译器在结构中留下了空洞,这导致空间浪费。能否关掉填充,或者控制结构域的对齐方式?**
- 编译器可能会在结构体中添加填充以优化内存对齐。可以通过编译器特定的选项来关闭填充或控制对齐方式。例如,在GCC中使用`__attribute__((packed))`。
**2.10 为什么`sizeof`返回的值大于结构的期望值,是不是尾部有填充?**
- `sizeof`操作符返回的是对象或类型的字节数,包括编译器可能添加的填充。这是因为硬件性能要求内存对齐。
**2.11 如何确定域在结构中的字节偏移?**
- 可以通过遍历结构体成员并计算它们的地址来确定偏移量。也可以使用编译器提供的特性,如GCC中的`offsetof`宏。
**2.12 怎样在运行时用名字访问结构中的域?**
- C语言标准中没有直接支持这种方式。可以通过构建映射表或使用反射机制来间接实现。
**2.13 程序运行正确,但退出时却“core dump”了,怎么回事?**
- “core dump”通常表示程序尝试访问无效内存。检查指针是否为空或超出范围,以及是否有内存泄漏。
**2.14 可以初始化一个联合吗?**
- 联合中的第一个成员会被初始化。可以通过初始化第一个成员来间接初始化联合。
**2.15 枚举和一组预处理的`#define`有什么不同?**
- 枚举提供了类型安全的整数集合,而`#define`只是简单的宏替换。枚举还支持更多的操作,如比较和打印。
**2.16 有什么容易的显示枚举值符号的方法?**
- 可以创建一个枚举值到字符串的映射表来实现。例如,定义一个数组或哈希表,其中键是枚举值,值是对应的字符串。
#### 三、表达式
**3.1 为什么这样的代码:`a[i]=i++;`不能工作?**
- 这是因为表达式中包含了副作用。`i++`在修改`i`的同时,也改变了表达式的结果。这可能导致未定义行为。
**3.2 使用我的编译器,下面的代码`int i=7; printf("%d\n", i++ * i++);`返回49?**
- 这是因为表达式中的顺序点规则。在这个例子中,`i`的值在乘法运算完成之前不会改变,因此输出是49。顺序点规则决定了表达式中操作发生的顺序。
**3.3 对于代码`int i=3; i=i++;`不同编译器给出不同的结果,有的为3,有的为4,哪个是正确的?**
- 这段代码的行为是未定义的。`i++`先返回旧值,再修改`i`,但这之后的赋值结果取决于编译器的具体实现。
**3.4 这是个巧妙的表达式:`a ^= b ^= a ^= b`它不需要临时变量就可以交换a和b的值。**
- 这个表达式利用异或运算的性质来交换两个变量的值。首先`a ^= b`相当于`a = a ^ b`,然后`b ^= a`相当于`b = b ^ (a ^ b)`,即`b = a`。最后`a ^= b`相当于`a = a ^ a`,即`a = b`。
**3.5 我可否用括号来强制执行我所需要的计算顺序?**
- 可以。括号可以用来明确指定运算的优先级,避免顺序点规则引起的未定义行为。
**3.6 可是`&&`和`||`运算符呢?我看到过类似`while ((c = getchar()) != EOF && c != '\n')`的代码...**
- `&&`和`||`运算符具有短路行为,这意味着在某些情况下可以提前终止计算。在上述例子中,如果`c`不等于`EOF`,则会继续计算`c != '\n'`。
**3.7 我怎样才能理解复杂表达式?“序列点”是什么?**
- 序列点是一个概念,用于定义表达式中操作的顺序。在序列点之后,所有之前的副作用都会完成。例如,逗号运算符`a, b`中的逗号就是一个序列点。
**3.8 那么,对于`a[i]=i++;`我们不知道`a[]`的哪一个分量会被改写,但`i`的确会增加1,对吗?**
- 正确。`i++`在修改`i`之前返回其原始值,但这个原始值被用于数组索引,导致数组元素的写入位置不可预测。
**3.9 `++i`和`i++`有什么区别?**
- `++i`是前置自增,它先增加`i`的值,然后返回新的值;`i++`是后置自增,它先返回`i`的当前值,然后再增加`i`。
**3.10 如果我不使用表达式的值,我应该用`++i`或`i++`来自增一个变量吗?**
- 如果不关心表达式的值,使用`++i`更为推荐,因为它效率更高。
**3.11 为什么如下的代码`int a = 100, b = 100; long int c = a * b;`不能工作?**
- 这段代码可以工作,但是要注意`a * b`的结果默认为`int`类型,如果超过了`int`的最大值,会导致溢出。为了避免溢出,可以将其中一个变量强制转换为`long int`:
```c
long int c = (long int)a * b;
```
**3.12 我需要根据条件把一个复杂的表达式赋值给两个变量中的一个。可以用下边这样的代码吗?`((condition) ? a : b) = complicated_expression;`**
- 不行。条件运算符`? :`的返回类型必须是一个具体的类型,而不能用于赋值给左侧的操作数。正确的做法是分别根据条件赋值给变量:
```c
int result;
if (condition) {
result = a;
} else {
result = b;
}
result = complicated_expression;
```
以上是对《你必须知道的495个C语言问题》中部分知识点的总结和解释,希望能帮助读者更好地理解和掌握C语言的关键概念和技术细节。