C语言面试通关秘籍:高频考点深度解析与实战

C语言作为编程界的常青树,其面试考点往往集中在内存管理、指针操作、底层原理等方面。根据这张思维导图,我们将面试内容划分为六大板块。让我们逐一击破。

一、数组和指针

这部分是C语言面试的“重灾区”,特别是复杂的类型声明。

1、指针数组

指针数组即存放指针的数组。例如:

int* p1,*p2,*p3; //声明指针变量
int* arr = {p1,p2,p3};//arr是指针数组,存放3个int*
2、数组指针

数组指针即指向数组的指针变量。例如:

int arr = {1,2,3,4,5};
int (*p)[5] = &arr;//p是一个指针变量,指向的数组存放5个int
int (*p)[5] = arr;//是错的!!!

很多人误以为数组名就是首元素地址,这个说法并不严谨。C 语言里数组名在大多数表达式会隐式转换成首元素地址,但sizeof(arr)和&arr是两个例外。arr和&arr打印出来地址数值一样,但是类型不同:arr隐转后是int*,代表首元素地址;&arr是int (*)[5]数组指针,代表整个数组的地址。数组指针赋值时,不能直接写p=arr,必须写p=&arr。指针加减的偏移量,由指针的类型决定。

3、函数指针

函数指针即指向函数的指针变量。例如:

int Add(int a,int b)
{
    return a+b;
}

int (*P)(int,int) = &Add;//*p是一个指向函数的指针变量,函数的返回值是int,参数是(int,int)
4、函数指针数组

函数指针数组即数组里存的是函数指针。(C++虚函数表即函数指针数组)。例如:

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    // 函数指针数组
    int (*ops[2])(int, int) = {add, sub};
    printf("%d\n", ops[0](10, 5)); // 输出 15
    return 0;
}

代码解析:

int (*ops[2])(int, int) = {add, sub};

*ops是一个函数指针数组,数组的容量是2,存放的函数的返回值是int,需要接收的参数是:(int,int)。

printf("%d\n", ops[0](10, 5)); // 输出 15

ops[0](10, 5):拿到ops第0号元素,即int add(int a,int b),传递参数(10,5)。

5、const和指针
  1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变。例如:const int * p;
  2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。例如:int* const p;

例如:
在这里插入图片描述
在这里插入图片描述

从图中可以看出结果和我们得出的结论一致。所以在项目中,某些不会改变的参数就可以使用const修饰来防止该变量被误改,提升程序的健壮性。

6、sizeof 和指针和数组

sizeof:操作符,计算变量/类型所占内存大小(包括\0),编译时确定。

  1. 数组名在 sizeof(数组名)中代表整个数组,算出数组总字节大小。
  2. 指针变量,sizeof得到的是指针本身的大小(32 位机器 = 4 字节,64 位机器 = 8 字节)
  3. 数组名传参到函数里会退化成为指针,在函数内部 sizeof就变成指针大小。

例如:
在这里插入图片描述

图中,arr数组一共有5个int类型数据,一共20字节,一个指针大小为8字节。

7、strlen 和指针和数组
  1. strlen接收的是地址,不管你传的是字符数组名,还是char*指针,都会当成地址去遍历。

  2. sizeof编译期求内存大小;strlen 运行期扫描内存。

  3. 扫描时遇到'\0'就停止。

例如:

在这里插入图片描述

8、回调函数

通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

二、库函数的模拟实现

1. memcpy

memcpy 不处理内存重叠,按字节从前往后拷贝。

#include <stddef.h>

void *my_memcpy(void *dest, const void *src, size_t n)
{
    if (dest == NULL || src == NULL || n == 0)
        return dest;

    char *d = (char *)dest;
    const char *s = (const char *)src;

    while (n--) 
    {
        *d++ = *s++;
    }
    return dest;
}

要点:

  • 参数用 void *,内部转成 char * 按字节拷贝。
  • 返回 dest,支持链式调用。
  • 不处理重叠内存,重叠时行为未定义。

2. memmove

memmove 需要处理内存重叠。判断 dest 和 src 的相对位置:

  • 若 dest < src 或 dest >= src + n:不重叠或 dest 在前,从前往后拷。
  • 否则:dest 在 src 后面且重叠,从后往前拷,避免覆盖。
#include <stddef.h>

void *my_memmove(void *dest, const void *src, size_t n)
{
    if (dest == NULL || src == NULL || n == 0)
        return dest;

    char *d = (char *)dest;
    const char *s = (const char *)src;

    if (d < s || d >= s + n) 
    {
        // 从前往后拷贝
        while (n--) 
        {
            *d++ = *s++;
        }
    } 
    else 
    {
        // 从后往前拷贝
        d += n - 1;
        s += n - 1;
        while (n--) 
        {
            *d-- = *s--;
        }
    }
    return dest;
}

要点:

  • 核心是判断重叠方向,决定拷贝顺序。
  • 比较 d < s 时,严格来说跨对象指针比较是 UB,但工程实践中常见;更严谨可用 uintptr_t 比较。

更严谨的写法:

#include <stdint.h>

void *my_memmove(void *dest, const void *src, size_t n)
{
    if (dest == NULL || src == NULL || n == 0)
        return dest;

    char *d = (char *)dest;
    const char *s = (const char *)src;

    if ((uintptr_t)d < (uintptr_t)s || (uintptr_t)d >= (uintptr_t)s + n) 
    {
        while (n--) *d++ = *s++;
    } 
    else 
    {
        d += n - 1;
        s += n - 1;
        while (n--) *d-- = *s--;
    }
    return dest;
}

3. strstr

在 haystack 中查找首次出现 needle 的位置,返回该位置指针;找不到返回 NULL。

暴力实现

#include <stddef.h>

char *my_strstr(const char *haystack, const char *needle)
{
    if (haystack == NULL || needle == NULL)
        return NULL;

    if (*needle == '\0')
        return (char *)haystack;

    const char *h = haystack;

    while (*h) 
    {
        const char *p1 = h;
        const char *p2 = needle;

        while (*p1 && *p2 && *p1 == *p2) 
        {
            p1++;
            p2++;
        }

        if (*p2 == '\0')
            return (char *)h;

        h++;
    }
    return NULL;
}

要点:

  • needle 为空串时返回 haystack(C 标准规定)。
  • 外层遍历 haystack 每个起点,内层逐字符比较。
  • 时间复杂度 O(m*n),m 为 haystack 长度,n 为 needle 长度。

KMP 实现(进阶)

如果面试要求优化,可以手写 KMP:

#include <stdlib.h>
#include <string.h>

static void build_next(const char *pat, int *next, int len)
{
    next[0] = -1;
    int i = 0, j = -1;
    while (i < len - 1) 
    {
        if (j == -1 || pat[i] == pat[j]) 
        {
            i++;
            j++;
            next[i] = j;
        } 
        else 
        {
            j = next[j];
        }
    }
}

char *my_strstr_kmp(const char *haystack, const char *needle)
{
    if (haystack == NULL || needle == NULL)
        return NULL;

    int n = (int)strlen(haystack);
    int m = (int)strlen(needle);

    if (m == 0)
        return (char *)haystack;
    if (n < m)
        return NULL;

    int *next = (int *)malloc(sizeof(int) * m);
    if (!next)
        return NULL;

    build_next(needle, next, m);

    int i = 0, j = 0;
    while (i < n && j < m) 
    {
        if (j == -1 || haystack[i] == needle[j]) 
        {
            i++;
            j++;
        } 
        else 
        {
            j = next[j];
        }
    }

    free(next);

    if (j == m)
        return (char *)(haystack + i - m);
    return NULL;
}

要点:

  • next 数组记录最长公共前后缀。
  • 匹配失败时 j = next[j],主串指针 i 不回退。
  • 时间复杂度 O(n+m)。

三、自定义类型

1、内存对齐
  • 规则:

    1. 第一个成员在偏移量为0处。
    2. 其他成员要对齐到对齐数(编译器默认对齐数与成员自身大小的较小值)的整数倍地址。
    3. 结构体总大小为最大对齐数的整数倍。
    4. 嵌套结构体对齐到自己的最大对齐数整数倍。
  • 示例:

    struct S 
    {
        char c;   // 偏移0,大小1
        int i;    // 对齐数4,偏移4,大小4 (浪费3字节)
        double d; // 对齐数8,偏移8,大小8 (浪费0字节)
    };
    // 总大小:8的倍数,故为16字节
    

2. 联合体、共用体

  • 概念:所有成员共用同一块内存空间,大小等于最大成员的大小。

  • 应用:判断大小端。

    union U 
    {
        int i;
        char c;
    };
    // 如果u.i=1,u.c为1则是小端,为0则是大端
    

3. 位段(网络报文的报头不常考)

  • 概念:结构体成员后面加冒号和数字,指定占用的比特位数。

    struct S 
    {
        int a : 2; // a占2个比特位
        int b : 5; // b占5个比特位
    };
    
  • 注意:位段的内存分配依赖编译器,可移植性差。


四、数据存储

1. 整形的存储规则(原反补)

  • 原码:直接二进制表示。
  • 反码:符号位不变,其余取反。
  • 补码:反码+1。
  • 内存存储:计算机内存中一律存储补码。

2. 为什么要存补码?

  • 统一了0的表示(+0和-0在补码中都是全0)。
  • 正数的原反、补、码一样,而负数的补码是自己的反码+1,而计算机又只有加法器,那么就可以利用加法来实现减法。例如:a-b,可以写成a+(-b),也就是a的反码加上b的反码。

3. 大小端如何判断?

  • 大端:低位字节存高地址。

  • 小端:低位字节存低地址。

  • 代码判断:

    int check_sys() 
    {
        int i = 1;
        return *(char *)&i; // 返回1是小端,0是大端
    }
    

4. 浮点数的存储

  • 遵循IEEE 754标准。
  • 公式:(-1)^S * M * 2^E。
  • 32位float:1位符号,8位指数,23位尾数。
  • 64位double:1位符号,11位指数,52位尾数。

5. 类型提升和截断

  • 提升:参与运算时,短整型(char, short)会自动提升为int。

  • 截断:将长类型赋值给短类型时,直接截取低位。

    char c = 300; // 300的二进制是 1 0010 1100,截断后为 0010 1100,即44
    

五、编译、链接

1. 宏(考察很多,轻松实现宏函数)

  • 概念:预处理阶段的文本替换。

  • 示例:写一个宏求两个数的最大值。

    #define MAX(a, b) ((a) > (b) ? (a) : (b))
    // 必须加括号,否则会有优先级问题
    

2. 编译链接的过程

  1. 预处理:展开头文件、宏替换、去注释、条件编译。生成 .i 文件。

  2. 编译:语法分析、词法分析、语义分析、生成汇编代码。生成 .s 文件。

  3. 汇编:将汇编代码转为机器码。生成 .o 文件(目标文件)。

  4. 链接:合并段表、符号表合并与重定位。生成可执行文件。

    在这里插入图片描述

3. 条件编译

  • 常用指令:#ifdef, #ifndef, #endif, #if, #elif, #else。

  • 应用:防止头文件重复包含。

    #ifndef __TEST_H__
    #define __TEST_H__
    // 头文件内容
    #endif
    

2. 编译链接的过程

  1. 预处理:展开头文件、宏替换、去注释、条件编译。生成 .i 文件。

  2. 编译:语法分析、词法分析、语义分析、生成汇编代码。生成 .s 文件。

  3. 汇编:将汇编代码转为机器码。生成 .o 文件(目标文件)。

  4. 链接:合并段表、符号表合并与重定位。生成可执行文件。

    [外链图片转存中…(img-fSQLKLTT-1790504346561)]

3. 条件编译

  • 常用指令:#ifdef, #ifndef, #endif, #if, #elif, #else。

  • 应用:防止头文件重复包含。

    #ifndef __TEST_H__
    #define __TEST_H__
    // 头文件内容
    #endif
    

如有错误,恳请指出。

Logo

一站式 AI 云服务平台

更多推荐