### 重要知识点解析 #### 1. 声明和初始化 **1.1 我如何决定使用那种整数类型?** 在C语言中选择合适的整数类型取决于多个因素,包括但不限于所需数值范围、存储效率以及跨平台兼容性。通常情况下,`int`是最常用的类型,因为它提供了一个合理的范围(通常是-32768到32767),并且在大多数平台上都具有良好的性能。然而,在需要更精确控制的情况下,比如确保一个整数能够存储超过32767的值时,可以选择`long int`或`long long int`。另一方面,如果程序对空间非常敏感,则可以考虑使用`short int`或`char`。此外,C99标准引入了一系列固定宽度的整数类型,如`int32_t`、`uint32_t`等,这些类型提供了跨平台的一致性。 **1.2 64位机上的64位类型是什么样的?** 在64位机器上,64位类型指的是那些使用64位(即8字节)存储的数据类型。常见的64位类型包括`long long int`和`unsigned long long int`,它们分别用于表示有符号和无符号的64位整数。此外,C99标准还定义了专门的64位整数类型,如`int64_t`和`uint64_t`。这些类型的引入使得开发者能够在不同平台上编写一致且高效的代码。 **1.3 怎样定义和声明全局变量和函数最好?** 定义和声明全局变量及函数时,应当遵循一些最佳实践: - **命名规范**:使用有意义的名称,并遵循一致的命名约定。 - **作用域限制**:尽可能地将变量的作用域限制在最小范围内,以减少潜在的冲突和提高代码的可维护性。 - **初始化**:总是初始化全局变量,尤其是在它们被用于多线程环境时尤为重要。 - **避免过度使用**:过多的全局变量可能导致代码变得难以理解和调试。尽可能使用局部变量,并通过函数参数传递必要的数据。 **1.4 extern在函数声明中是什么意思?** `extern`关键字用于声明一个变量或函数是在其他文件中定义的。当在一个源文件中声明另一个源文件中定义的变量或函数时,通常会使用`extern`。例如,如果变量`x`在文件A中定义,而在文件B中需要使用这个变量,则可以在文件B中声明`extern int x;`。这表明`x`是在外部定义的,并且其实际定义位于另一个地方。 **1.5 关键字auto到底有什么用途?** `auto`关键字在C语言中有两个主要用途: - **声明变量时**:`auto`关键字默认用于所有局部变量的声明。因此,声明如`auto int x;`等同于`int x;`。 - **类型推断**:在C99及以后的标准中,`auto`也可以用于声明变量并根据初始化表达式的类型推断出变量的类型,如`auto x = 10;`这里`x`将被推断为`int`类型。 **1.6 我似乎不能成功定义一个链表。** 在定义链表时,常见的错误之一是在结构体中尝试定义指向该结构体本身的指针类型。正确的做法是先定义一个通用的指针类型,再在结构体中使用该类型。例如: ```c typedef struct Node { char *item; struct Node *next; } Node; ``` **1.7 怎样建立和理解非常复杂的声明?** 理解复杂的声明通常需要分解每个部分,并了解它们之间的关系。例如,要理解`int (*ptr)[N]`这样的声明,我们可以将其分解为: - `(*ptr)` 表示`ptr`是一个指向某种类型的指针。 - `[N]` 表示这是一个数组。 - `int` 表示数组的元素类型。 因此,整体来看,`ptr`是一个指向含有`N`个整数的数组的指针。 **1.8 函数只定义了一次,调用了一次,但编译器提示非法重定义了。** 这种错误通常是由于多个源文件中出现了相同的函数定义所导致的。为了避免此类问题,可以采用以下几种方法: - **头文件和源文件分离**:将函数声明放在头文件中,并在需要使用的地方通过`#include`指令引入,而函数的定义则单独放在源文件中。 - **使用`static`关键字**:如果函数仅在一个源文件内使用,可以将其定义为静态函数,即使用`static`关键字修饰。 **1.9 main()的正确定义是什么?void main()正确吗?** `main()`函数是C程序的入口点。其标准定义形式为`int main(void)`或`int main(int argc, char *argv[])`。后者允许从命令行接收参数。`void main()`不是标准定义,虽然某些编译器可能支持这种方式,但在正式编程实践中应避免使用。 **1.10 对于没有初始化的变量的初始值可以作怎样的假定?** 未初始化的局部变量的值是未定义的,因此不能假设任何特定的值。全局变量和静态变量如果没有显式初始化,则会被自动初始化为零值(对于数字类型来说是0,对于指针类型来说是`NULL`)。 **1.11 代码int f(){char a[]="Hello, world!";}不能编译。** 这段代码之所以不能编译,是因为`char a[]="Hello, world!";`声明了一个数组,并对其进行了字符串字面量初始化。然而,在`int f()`函数体内声明的数组是动态大小的,不能用字符串字面量初始化。正确的做法是使用`const char *a = "Hello, world!";`。 **1.12 这样的初始化有什么问题?char *p = malloc(10);编译器提示“非法初始式”。** `malloc`是一个库函数,用于动态分配内存。`char *p = malloc(10);`这样的初始化是不合法的,因为`malloc`的返回值不是一个初始值表达式。正确的做法是先分配内存,然后将指针赋值给`p`: ```c char *p; p = (char *)malloc(10); ``` 需要注意的是,使用`malloc`分配的内存需要手动释放,通常使用`free`函数。 **1.13 以下的初始化有什么区别?char a[]="string literal"; char *p="string literal";** 这两种初始化方式的区别在于: - `char a[]="string literal";`创建了一个字符数组,并将其初始化为字符串字面量。 - `char *p="string literal";`定义了一个指向字符串字面量的字符指针。 当试图修改通过`char *p="string literal";`初始化的字符串时,可能会导致程序崩溃,因为字符串字面量通常是在只读内存区域中,不允许修改。 **1.14 我总算弄清楚函数指针的声明方法了,但怎样才能初始化呢?** 函数指针的初始化可以通过直接赋值的方式完成。例如,假设有一个函数`int add(int a, int b)`,则可以这样初始化函数指针: ```c int (*func_ptr)(int, int) = add; ``` 这里的`int (*func_ptr)(int, int)`声明了一个指向接受两个`int`参数并返回`int`值的函数的指针,然后将其初始化为`add`函数的地址。 #### 2. 结构、联合和枚举 **2.1 声明struct x1{};和typedef struct{} x2;有什么不同?** 这两种声明的主要区别在于: - `struct x1{};`声明了一个名为`x1`的结构体类型。 - `typedef struct{} x2;`定义了一个匿名结构体,并通过`typedef`关键字为其指定了别名`x2`。 这意味着你可以直接声明`struct x1`类型的变量,但只能通过`x2`来声明`typedef struct{} x2;`类型的变量。 **2.2 为什么struct x{}; x thestruct;不对?** `struct x{}; x thestruct;`这样的声明是不合法的,因为`struct x`类型声明后,必须使用`struct x`的形式来声明变量,而不是直接使用类型名`x`。正确的声明方式是: ```c struct x thestruct; ``` **2.3 一个结构可以包含指向自己的指针吗?** 结构体可以包含指向自身的指针成员。例如: ```c struct node { int data; struct node *next; }; ``` **2.4 在C语言中实现抽象数据类型什么方法最好?** 在C语言中实现抽象数据类型的一种常用方法是使用结构体和函数指针结合。例如,可以定义一个结构体,其中包含数据成员和指向操作这些数据成员的函数的指针。这样可以封装数据和相关的操作,提高代码的可读性和可维护性。 **2.5 在C中是否有模拟继承等面向对象程序设计特性的好方法?** C语言本身并不支持传统的面向对象编程特性,如类和继承。但是,可以通过使用结构体和函数指针等机制来模拟面向对象编程的一些概念。例如,可以定义一系列结构体,每个结构体包含指向相同接口函数的指针,从而实现类似于继承的效果。 **2.6 我遇到这样声明结构的代码: struct name { int namelen; char namestr[1]; };然后又使用一些内存分配技巧使namestr数组用起来好像有多个元素。这样合法和可移植吗?** 这种声明方式被称为“flexible array member”,是一种在C99标准中引入的特性。它允许在结构体的末尾定义一个变长数组成员,从而可以在运行时决定数组的实际大小。这种方式在很多情况下是合法且可移植的,但要注意不同编译器的实现细节可能有所不同。 **2.7 是否有自动比较结构的方法?** 比较结构体内容通常需要手动实现,遍历每个成员并逐一比较。C语言并没有内置的自动比较结构体的方法。一种常见的做法是编写一个函数,该函数遍历结构体的所有成员,并逐个比较它们的值。 **2.8 如何向接受结构参数的函数传入常数值?** 向接受结构参数的函数传入常数值可以通过初始化一个结构体变量,然后将该变量作为参数传递。如果需要传入不可修改的常数值,可以使用`const`关键字。 **2.9 怎样从/向数据文件读/写结构?** 读写结构体到文件通常涉及到序列化过程。可以使用`fwrite`和`fread`函数来读写结构体。需要注意的是,直接读写结构体可能会遇到对齐问题,导致跨平台问题。为了保证可移植性,可以手动序列化结构体成员。 **2.10 我的编译器在结构中留下了空洞,这导致空间浪费而且无法与外部数据文件进行”二进制”读写。能否关掉填充,或者控制结构域的对齐方式?** 大多数编译器提供了控制结构体成员对齐方式的方法。例如,在GCC中可以使用`#pragma pack`来控制结构体的填充行为。这有助于减少空间浪费,并确保与外部数据文件的兼容性。 **2.11 为什么sizeof返回的值大于结构的期望值,是不是尾部有填充?** `sizeof`运算符返回的是对象或类型的字节大小。如果`sizeof`返回的值大于预期,通常是因为编译器为了优化内存访问速度而添加了填充字节。可以通过查看编译器文档或使用`#pragma pack`来控制填充行为。 **2.12 如何确定域在结构中的字节偏移?** 确定结构体中域的字节偏移可以通过编写简单的测试程序来实现。例如,可以定义一个结构体,并使用`offsetof`宏来获取特定成员相对于结构体开头的偏移量。 **2.13 怎样在运行时用名字访问结构中的域?** 在C语言中,直接通过名字访问结构体成员通常需要在编译时已知成员的名字。如果需要在运行时根据名字访问成员,可以考虑使用反射机制或元数据,但这通常需要额外的库支持。 **2.14 程序运行正确,但退出时却“core dump”了,怎么回事?** “core dump”通常发生在程序尝试访问无效内存或违反了某些内存访问规则时。在涉及结构体的操作中,常见的原因包括: - 使用了已经被释放的内存。 - 越界访问结构体成员。 - 指针解引用错误。 为了解决这类问题,可以使用调试工具如GDB来检查程序的状态,并查找可能的内存访问违规。 **2.15 可以初始化一个联合吗?** 联合可以像结构体一样初始化,但是由于联合的所有成员共享同一段内存,所以在初始化时需要注意不要同时初始化多个成员。一般的做法是初始化联合的第一个成员,之后通过成员访问来更新其他成员。 **2.16 枚举和一组预处理的#define有什么不同?** 枚举(`enum`)和预处理器宏(`#define`)都可以用来定义符号常量,但它们之间存在一些关键区别: - 枚举提供了类型安全的符号常量,而预处理器宏只是简单的文本替换。 - 枚举成员在编译时被赋予唯一的整数值,而预处理器宏没有类型限制,可以是任意表达式。 - 枚举提供了更好的可读性和错误检测能力。 **2.17 有什么容易的显示枚举值符号的方法?** 显示枚举值的符号通常需要维护一个映射表或查找表,将整数值映射回相应的枚举符号。这可以通过定义一个函数来实现,该函数接受枚举类型的值,并返回对应的符号字符串。 #### 3. 表达式 **3.1 为什么这样的代码: a[i]=i++;不能工作?** 这段代码的问题在于,`i++`会导致`i`的值在赋值操作之后递增,这意味着`a[i]`和`i`可能不会同步更新。为了避免这种问题,建议将操作分解为多个步骤,例如先递增`i`,然后再进行赋值。 **3.2 使用我的编译器,下面的代码int i=7; printf("%d\n", i++ * i++); 返回49?不管按什么顺序计算,难道不该打印出56吗?** 这段代码的行为取决于表达式的求值顺序,而C语言规范并未明确规定具体的顺序。不同的编译器可能会有不同的实现。在这种情况下,建议避免在同一表达式中多次修改同一个变量,以确保代码的可预测性。 **3.3 对于代码int i=3; i=i++;不同编译器给出不同的结果,有的为3,有的为4,哪个是正确?** 这种行为同样取决于表达式的求值顺序。理论上,`i=i++;`的结果是未定义的,因为`i`在同一个表达式中被修改了两次。不同的编译器可能会给出不同的结果。正确的做法是避免在同一表达式中多次修改同一个变量。 **3.4 这是个巧妙的表达式: a^=b^=a^=b 它不需要临时变量就可以交换a和b的值。** 这个表达式确实可以用于交换两个变量的值,但它依赖于异或(XOR)操作的性质。具体来说,`a ^= b ^= a ^= b`等价于`a = a ^ b; b = a ^ b; a = a ^ b;`。这种做法虽然巧妙,但由于其背后的原理并不直观,可能会降低代码的可读性和可维护性。 **3.5 我可否用括号来强制执行我所需要的计算顺序?** 是的,可以使用括号来明确指定计算顺序。括号在C语言中的优先级最高,因此可以用来控制表达式的求值顺序。例如,`(a + b) * c`将首先执行加法运算,然后乘法运算。 **3.6 可是&&和||运算符呢?我看到过类似while ((c=getchar()) != EOF && c != '\n')的代码……** `&&`和`||`运算符都是短路运算符,这意味着如果第一个操作数足以确定整个表达式的真假,那么第二个操作数就不会被评估。在`while ((c=getchar()) != EOF && c != '\n')`这样的表达式中,如果`(c=getchar()) != EOF`为假(即`c`等于`EOF`),那么整个表达式的结果就是假,`c != '\n'`这部分就不会被评估。 **3.7 我怎样才能理解复杂表达式?“序列点”是什么?** 理解复杂表达式的关键是识别序列点(sequence point)。序列点是C语言中的概念,它标志着所有之前发生的副作用(如变量的修改)都被完成的时间点。在序列点之后,所有之前的副作用都被认为已经完成,新的副作用可以从那一点开始发生。例如,在`a = b, c = d`中,逗号运算符``,`是序列点,意味着`a = b`和`c = d`之间有明确的顺序。 **3.8 那么,对于a[i]=i++;我们不知道a[]的哪一个分量会被改写,但i的确会增加1,对吗?** 是的,`a[i]=i++;`这种表达式的行为是未定义的,因为我们不能确定`a[i]`和`i++`之间的执行顺序。然而,`i`的值确实会在表达式执行完成后增加1。为了避免这种不确定的行为,建议将表达式分解为多个独立的操作。 **3.9 ++i和i++有什么区别?** `++i`和`i++`都是自增运算符,但它们在使用上有区别: - `++i`先将`i`的值加1,然后使用新值。 - `i++`先使用`i`的当前值,然后将其加1。 这种差异在复合表达式中尤其明显,例如`a = ++i + i++;`这样的表达式。 **3.10 如果我不使用表达式的值,我应该用++i或i++来自增一个变量吗?** 如果不使用表达式的值,那么使用`++i`或`i++`在功能上是相同的,因为最终都会使变量增加1。然而,出于习惯和可读性的考虑,通常推荐使用`++i`,因为它更简洁,且通常被视为“更自然”的方式。 **3.11 为什么如下的代码int a=100, b=100; long int c=a*b;不能工作?** 这段代码的问题在于`a`和`b`都是`int`类型,它们的乘积可能会超出`int`的最大表示范围。尽管`c`是`long int`类型,但在赋值给`c`之前,乘积已经在`int`范围内计算。为了避免溢出问题,可以显式转换类型: ```c long int c = (long int)a * (long int)b; ``` **3.12 我需要根据条件把一个复杂的表达式赋值给两个变量中的一个。可以用下边这样的代码吗?((condition) ? a : b) = complicated_expression;** 这种方式是不合法的,因为在C语言中,三元运算符`?:`的左边必须是一个变量。正确的做法是先计算复杂的表达式,然后根据条件选择其中一个变量进行赋值。例如: ```c int result = complicated_expression; (condition) ? (a = result) : (b = result); ``` 以上是对《你必须知道的495个C语言问题》中提到的部分知识点的详细解释和扩展。希望这些内容能够帮助读者更好地理解和掌握C语言中的这些概念。
剩余152页未读,继续阅读
- 粉丝: 0
- 资源: 1
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助