- C语言存储类别
存储类别指定了变量的范围(可见性)和生命周期。以下是几种存储类别:
auto
: 默认存储类别,用于局部变量。static
: 修改变量的存储周期,使其在程序执行期间保持存在。extern
: 表明变量的定义在另一个文件中,用于引用全局变量。register
: 提示编译器尝试将变量存储在CPU寄存器中以快速访问。
示例 - static变量如何使函数状态持久:
void Counter() {
static int count = 0; // 'count'的值在函数调用之间将持续存在
printf("%d ", ++count); // 输出被调用的次数
}
// 每次调用这个函数,count都会增加,而不是重置为0。
- 指针和数组的关系
指针是一个变量,其值为另一个变量的地址,而数组是一系列相同类型元素的集合,它的名字可以当成指向第一个元素的指针。
示例 - 使用指针访问数组元素:
void printArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i)); // 使用指针访问数组元素
}
}
// 这里,'arr'被用作指向整型数组的指针,在函数体内,我们通过加上偏移量'i'来访问元素。
- 结构体与联合体的区别
结构体和联合体都是自定义数据类型,用于存储多个变量,但在结构体中每个成员都有自己的存储空间,而联合体的所有成员共享同一块存储空间。
示例 - 结构体与联合体的定义:
struct StructExample {
int a;
char b;
float c;
}; // 结构体成员各占独立的内存空间
union UnionExample {
int a;
char b;
float c;
}; // 联合体的成员共享内存空间,任一时刻只能存储一个成员的值
- 动态内存分配
动态内存分配是程序运行时从堆上分配内存的过程。以下是相关函数的用法:
malloc
: 分配指定大小的内存块。calloc
: 分配指定数量的数组元素,并初始化为零。realloc
: 改变已经分配的内存块的大小。free
: 释放已经分配的内存块。
int* allocateArray(int size, int value) {
int *arr = malloc(size * sizeof(int)); // 分配内存
if (arr != NULL) {
for (int i = 0; i < size; i++) {
arr[i] = value; // 初始化内存
}
}
return arr; // 返回指向已分配内存的指针
}
// 需要注意的是使用完毕后要调用free(arr)来释放内存
- 链表的插入与删除
链表是一种数据结构,由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针部分。插入或删除节点需要改变这些指针的链接。
typedef struct Node {
int data;
struct Node* next;
} Node;
void insertNode(Node** head, int data) {
Node* newNode = (Node*) malloc(sizeof(Node)); // 创建新节点
newNode->data = data; // 分配数据
newNode->next = *head; // 新节点的下一个是当前的头节点
*head = newNode; // 头指针指向新节点
}
void deleteNode(Node** head, int key) {
// 实现此功能的详细代码会包括查找具有指定键的节点,然后改变节点指针以移除它。
}
// 这些函数通过指向头节点指针的指针来操作链表,确保所有的修改都会保持。
- 字符串反转
字符串反转是将字符串中的字符顺序倒置。这通过交换字符串的前端和后端字符直至中心点来实现。
void reverseString(char *str) {
int len = strlen(str); // 获取字符串长度
for (int i = 0; i < len / 2; i++) {
char temp = str[i]; // 交换字符
str[i] = str[len - i - 1];
str[len - i - 1] = temp;
}
}
// 这个简单的翻转函数通过交换开始和结束字符来工作,直到它到达字符串的中心。
- 文件操作
使用C语言的标准库函数进行文件读写涉及FILE指针以及各种I/O函数,如fopen, fclose, fprintf, fscanf, fputs, fgets等。
void writeFile(const char *filename, const char *content) {
FILE *fp = fopen(filename, "w"); // 以写模式打开文件
if (fp != NULL) {
fputs(content, fp); // 将内容写入文件
fclose(fp); // 关闭文件
}
}
// 这段代码演示了如何打开一个文件,写入字符串,然后关闭文件。如果文件打开失败,不会尝试写入。
- 宏的用法和优点
宏是预处理器指令,提供了代码复用和条件编译的能力。使用宏可以避免魔法数字,提高代码可读性,并允许编译器在编译前对源代码进行修改。
#define MAX(a,b) ((a) > (b) ? (a) : (b)) // 宏定义来计算两个值的最大值
// 这个宏接受两个参数,并使用三元运算符返回较大者。在编译之前,所有MAX的实例会被展开。
- 递归与迭代
递归是函数调用自己的过程,通常用于解决分治问题,如汉诺塔问题、快速排序等。迭代是使用循环结构重复计算。
计算阶乘的两种方法:
// 递归
int factorial(int n) {
if (n <= 1) return 1; // 基础情况:1的阶乘是1
else return n * factorial(n - 1); // 递归步:n的阶乘是n乘以(n-1)的阶乘
}
// 迭代
int iterativeFactorial(int n) {
int result = 1;
while (n > 1) {
result *= n--; // 乘以当前的n,然后对n递减
}
return result; // 返回最终结果
}
// 递归函数很简洁,但对于大数可能导致栈溢出。迭代函数较为稳妥,但代码比递归版本更长。
- 条件预处理器指令
条件预处理器允许在代码编译之前进行条件编译。这对于只在调试时包含特定代码块特别有用。
#define DEBUG 1
#if DEBUG
printf("Debug information\n"); // 如果定义了DEBUG且值为非零,则包含此行
#endif
// 这种方法可以方便地开启或关闭调试输出,而不需要手动注释或取消注释代码。
以上是详细解释和带有注释的C语言面试题的示例代码。了解这些概念对于熟练使用C语言至关重要。在实际面试中,不仅要能够编写正确的代码,还要能清晰地解释编写代码的原因和背后的理论。