嵌入式八股-C语言基础30题

1. static关键字

  • 静态变量:声明静态变量使其生命周期延长,作用域仅限于当前文件内。
  • 静态函数:声明静态函数使其作用域限定在当前文件内,不可在其他文件中访问。
  • 静态成员变量:声明静态成员变量使其属于类本身而非对象,多个对象共享同一份内存。
  • 静态限定符:控制变量的初始化和生命周期。

示例:在函数内部使用 static

#include <stdio.h>

void increment() {
  static int count = 0;
  count++;
  printf("调用次数:%d\n", count);
}

int main() {
  for (int i = 0; i < 5; i++) {
    increment();
  }
  return 0;
}

在每次调用 increment 函数时,count 的值会持续增加,而不会被重置。因为 count 被声明为 static,其生命周期跨越了函数调用。

2. 在文件作用域使用 static

示例:防止外部访问文件内静态全局变量

// File1.c
static int globalVar = 10; // 变量只可在 file1.c 中使用

// File2.c
extern int globalVar;
int main() {
  printf("globalVar 的值:%d\n", globalVar);
  return 0;
}

File1.c 中,声明了一个具有文件作用域的静态全局变量 globalVar。在 File2.c 中试图 extern 该变量,但会失败。

3. 全局变量和局部变量的区别

  • 全局变量

    • 在函数外部声明,整个程序都可以访问。
    • 默认初始化,生命周期长,存储在全局数据区(data)。
  • 局部变量

    • 在函数或代码块内部声明,仅在所属范围内有效。
    • 无默认初始化,需手动赋值,生命周期短,存储在栈区(stack)。

4. 普通函数与宏函数的区别

  1. 编译时机:普通函数在编译时处理,宏函数在预处理阶段展开。
  2. 参数处理:普通函数参数会被求值且类型检查;宏函数参数在展开时直接替换,不检查类型。
  3. 代码大小:普通函数生成独立代码,宏函数展开后直接插入调用点,可能导致代码膨胀。
  4. 调试:普通函数有明确的调用栈,宏函数展开后调试信息可能不匹配源代码。
  5. 作用域:普通函数有自己的作用域,宏函数在展开时直接替换,可能影响代码作用域。

5. int main(int argc, char **argv) 函数参数含义

  • argc:整型,用于统计命令行参数的个数。
  • argv:字符串数组,存放指向字符串的指针。

成员含义

  • argv[0]:指向程序运行的全路径名。
  • argv[1]:指向命令行中第一个字符串参数。
  • argv[argc-1]:指向最后一个字符串参数。
  • argv[argc]:为 NULL

6. #include<> 和 #include"" 的区别

  • #include<>

    • 用于包含系统标准库头文件。
    • 在编译器的搜索路径中寻找头文件。
  • #include""

    • 用于包含用户自定义或项目中使用的非系统头文件。
    • 在当前源文件的相对路径或指定的绝对路径中寻找头文件。

7. C语言的基本类型(32位系统)及其占用字节数

  • char:1字节
  • short int:2字节
  • int/long int:4字节
  • char * / int * / 任意指针:4字节
  • float:4字节
  • double:8字节

8. 头文件 #ifndef/#define/#endif 的作用

  • #ifndef:判断当前头文件是否已被包含。
  • #define:定义一个宏,标识头文件已被包含。
  • #endif:结束条件编译,标记头文件的结束位置。

示例

#ifndef MYHEADER_H     
#define MYHEADER_H     

void sayHello();       
const int MAX_VALUE = 100;  

#endif               

9. 数组和指针的区别

  • 概念

    • 数组:用于储存多个相同类型数据的集合,数组名是首元素的地址。
    • 指针:存放其他变量在内存中的地址,指向内存的首地址。
  • 区别

    • 赋值:同类型指针变量可相互赋值,数组不能。
    • 存储方式:数组在内存中连续存放,指针是变量本身。
    • sizeof:数组表示总大小,指针表示地址大小。

10. 如何判断大小端?

示例代码

#include <stdio.h>

int check_endianness() {
    unsigned int num = 1;
    char *ptr = (char *)&num;
    
    if (*ptr) {
        return 1; // 小端序
    } else {
        return 0; // 大端序
    }
}

int main() {
    if (check_endianness()) {
        printf("This system is little-endian.\n");
    } else {
        printf("This system is big-endian.\n");
    }

    return 0;
}

通过检查指针 ptr 指向的第一个字节来判断系统的字节序。

11. 内存泄漏和内存溢出

  • 内存溢出:申请内存时,内存不足或类型不匹配导致的错误。
  • 内存泄漏:申请内存后,未释放导致内存堆积。

12. strcpy 和 memcpy 的区别

  1. 复制内容strcpy 只能复制字符串,memcpy 可复制任意数据。
  2. 复制方式strcpy 遇到 \0 结束,memcpy 根据指定长度复制。
  3. 用途strcpy 用于字符串,memcpy 用于复制其他类型数据。

13. sizeof与strlen的区别

  • sizeof

    • 用于获取数据类型或变量的字节大小。
    • 可接受多种参数,包括数据类型、变量名、数组名等。
    • 返回的是整个数据类型或变量占用的内存空间大小。
  • strlen

    • 用于获取以 \0 结尾的字符串的实际长度。
    • 运行时计算,需要遍历字符串的内容来确定长度。
    • 返回的是字符串中的字符个数,不包括字符串结束符 \0

示例

char str[] = "Hello";
size_t size_str = sizeof(str);  // size_str 的值为 6
size_t length_str = strlen(str); // length_str 的值为 5

注意事项

  • sizeof 返回的是静态的大小,而 strlen 返回的是实际的字符串长度。
  • 使用 strlen 时,要确保操作对象是以 \0 结尾的字符串,否则可能出现不确定结果。
  • sizeof 可用于任何数据类型或变量,而 strlen 仅适用于字符串。

14. 结构体和共用体的区别

  • 结构体(struct)

    • 是一种用户自定义的数据类型,包含多个不同类型的成员变量。
    • 各个成员在内存中按照定义的顺序依次存储,每个成员有自己的内存空间。
    • 大小等于所有成员变量大小之和,可能会有内存对齐问题。
    • 各个成员可以同时被访问和操作。
  • 共用体(union)

    • 是一种特殊的数据结构,所有成员共享同一块内存空间。
    • 大小等于其最大成员的大小,因为只有一个成员可以同时被使用。
    • 当一个成员被赋值后,其他成员的值会被覆盖。
    • 共用体通常用于节省内存空间,或在不同类型的数据之间进行转换时使用。

15. 一个指针可以是volatile吗?

  • volatile 指针
    • 是指一个指针所指向的数据可能会在程序的执行过程中被外部因素(如硬件中断、多线程操作)改变。
    • 当一个指针被声明为 volatile 时,编译器不会对该指针所指向的数据进行优化,每次访问该数据时都从内存中读取。

示例

volatile int *ptr;
int main() {
    int a = 10;
    ptr = &a;

    int b = *ptr; // 每次访问都从内存中读取 a 的值
}

16. 数组名与指针的区别

  • 数组名

    • 是一个常量指针,指向数组的首元素的地址。
    • 大小固定为整个数组的大小,因为数组名代表整个数组。
    • 不能被改变或重新赋值。
    • 不能进行指针运算,因为数组名是常量指针。
  • 指针

    • 是一个变量,存储一个内存地址,可以指向任意类型的对象。
    • 大小固定为指针类型的大小,通常与机器的字长相关。
    • 可以被改变或重新赋值,可以指向不同的内存地址。
    • 可以进行指针运算,如加法、减法等,用来移动指针指向的位置。

17. 数组指针与指针数组的区别

  • 数组指针(Pointer to an Array)
    • 定义:指向数组的指针,它指向整个数组。
    • 声明:int (*ptr)[size]; 这里 ptr 是一个指针,指向一个大小为 size 的数组。
    • 用法:通过数组指针可以访问整个数组,也可以使用指针算术运算访问数组中的元素。

示例

int arr[3] = {1, 2, 3};
int (*ptr)[3] = &arr; // 定义一个指向大小为3的数组的指针
  • 指针数组(Array of Pointers)
    • 定义:指针数组是一个数组,其中的每个元素都是一个指针。
    • 声明:int *arr[size]; 这里 arr 是一个包含 size 个指针的数组。
    • 用法:指针数组中的每个元素都可以指向不同的内存位置,每个指针可以指向不同类型或大小的数据。

示例

int a = 1, b = 2, c = 3;
int *ptrArr[3] = {&a, &b, &c}; // 定义一个包含3个指针的数组

18. 常见变量定义及解释

int a;                // 定义一个变量 a,类型为 int
int *ptr_a;           // 定义一个指针 ptr_a,指向 int 类型的变量
int **ptr_ptr_a;      // 定义一个指针 ptr_ptr_a,指向一个指向 int 类型的指针
int arr[10];          // 定义一个数组 arr,有 10 个元素,每个元素是 int 类型
int *ptr_arr[10];     // 定义一个数组 ptr_arr,有 10 个元素,每个元素是 int 类型的指针
int (*ptr_to_arr)[10];// 定义一个指针 ptr_to_arr,指向一个数组,该数组有 10 个元素,每个元素是 int 类型
int (*ptr_to_func)(int);// 定义一个指针 ptr_to_func,指向一个参数是 int,返回值是 int 的函数
int (*arr_of_func[10])(int);// 定义一个数组 arr_of_func,每个元素是一个指向参数是 int,返回值是 int 的函数指针

19. malloc和calloc的区别

  1. 参数不同

    • malloc(size_t size):接受一个参数,表示需要分配的内存大小(字节为单位)。
    • calloc(size_t num, size_t size):接受两个参数,第一个是需要分配的元素个数,第二个是每个元素的大小。
  2. 内存内容

    • malloc:分配的内存未初始化,可能包含任意值。
    • calloc:分配的内存初始化为零(所有位都是 0)。
  3. 性能

    • calloc 在某些系统上可能比 malloc 稍慢,因为 calloc 在分配内存后会将内存初始化为零。

示例

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr_malloc, *ptr_calloc;
    int size = 5;

    // 使用 malloc 分配内存
    ptr_malloc = (int *)malloc(size * sizeof(int));
    if (ptr_malloc == NULL) {
        printf("Memory allocation failed with malloc.\n");
        return 1;
    }

    // 使用 calloc 分配内存
    ptr_calloc = (int *)calloc(size, sizeof(int));
    if (ptr_calloc == NULL) {
        printf("Memory allocation failed with calloc.\n");
        return 1;
    }

    // 输出 malloc 分配的内存内容
    printf("Memory allocated with malloc:\n");
    for (int i = 0; i < size; i++) {
        printf("%d ", ptr_malloc[i]);
    }
    printf("\n");

    // 输出 calloc 分配的内存内容
    printf("Memory allocated with calloc:\n");
    for (int i = 0; i < size; i++) {
        printf("%d ", ptr_calloc[i]);
    }
    printf("\n");

    // 释放内存
    free(ptr_malloc);
    free(ptr_calloc);

    return 0;
}

20. 表达式的区别

  • const int* p;:指针 p 指向一个常量整数,不能通过 p 修改所指向的值,但可以改变 p 的指向。
  • int* const p;p 是一个常量指针,不能改变 p 的指向,但可以通过 p 修改所指向的值。
  • int const *p;:与 const int* p; 等价,表示指针 p 指向一个常量整数。
  • const int* const p;p 是一个指向常量整数的常量指针,既不能修改所指向的值,也不能改变 p 的指向。

21. 如何在指定地址赋值?

示例

int *ptr = (int *)0x5566; // 将指针 ptr 指向地址 0x5566
*ptr = 10; // 在地址 0x5566 处赋值为 10

22. malloc的底层原理

  1. 结论
    • 小于 128K 的内存分配

调用 brk(),通过移动 _enddata 指针分配。

  • 大于 128K 的内存分配调用 mmap(),在虚拟地址空间中找一块空间分配。
  1. 具体实现
    • malloc(size) 首先计算需要分配的内存块大小。
    • 遍历数据结构(如空闲链表)查找合适大小的空闲内存块。
    • 如果找到合适内存块,标记为已分配,并返回指针。
    • 如果没有足够大的空闲内存块,调用 brkmmap 请求更多内存。
    • 当内存块被释放时,通过 free 函数将其标记为未分配,加入空闲内存块列表。

简易代码实现

#include <unistd.h>   // 包含系统调用相关的头文件

typedef struct Block {
    size_t size;       // 内存块的大小
    struct Block* next; // 指向下一个内存块的指针
} Block;

Block* freeList = NULL;   // 空闲链表的头指针

void* malloc(size_t size) {
    // 检查参数是否合法
    if (size <= 0) {
        return NULL;
    }
    
    // 计算需要分配的内存大小
    size_t blockSize = sizeof(Block) + size;
    
    // 在空闲链表中查找符合要求的内存块
    Block* prevBlock = NULL;
    Block* currBlock = freeList;
    while (currBlock != NULL) {
        if (currBlock->size >= blockSize) {
            // 找到合适大小的空闲块
            if (prevBlock != NULL) {
                // 删除这个空闲块
                prevBlock->next = currBlock->next;
            } else {
                // 这个空闲块是链表的头节点
                freeList = currBlock->next;
            }
            
            // 返回指向内存块的指针
            return (void*)(currBlock + 1);
        }
        prevBlock = currBlock;
        currBlock = currBlock->next;
    }
    
    // 没有找到可用的内存块,请求更多内存空间
    Block* newBlock = sbrk(blockSize);
    if (newBlock == (void*)-1) {
        return NULL;   // 请求失败,返回 NULL
    }
    
    // 返回指向新内存块的指针
    return (void*)(newBlock + 1);
}

void free(void* ptr) {
    // 检查参数是否合法
    if (ptr == NULL) {
        return;
    }
    
    // 获取指向内存块起始位置的指针
    Block* block = ((Block*)ptr) - 1;
    
    // 将内存块标记为未分配状态,然后将其添加到空闲链表中
    block->next = freeList;
    freeList = block;
}

23. volatile声明的作用

volatile 关键字用于声明可能会被意外改变的变量,这样编译器就不会对该变量进行优化。它主要用于多线程编程中,以确保共享变量的内存可见性。指针也可以使用 volatile

常见应用场景

  1. 多线程中的共享变量
  2. 中断处理程序中访问到的非自动变量
  3. 并行设备的硬件寄存器

24. extern关键字

extern 关键字用于声明一个在其他文件中定义的外部变量或函数,告诉编译器在链接过程中需要找到对应的定义,并允许在当前文件中使用这些外部变量或函数而不需要重新定义。

示例

// File1.c
extern int globalVar = 10;

// File2.c
#include <stdio.h>
extern int globalVar;

int main() {
   printf("globalVar 的值:%d\n", globalVar);
   return 0;
}

在这个例子中,File1.c 中定义了全局变量 globalVar,而 File2.c 中通过 extern 声明了它,以便在 File2.c 中使用该变量。

25. 什么是内存池?

定义
内存池(Memory Pool)是一种数据结构,用于管理动态分配的内存块。通过预先分配一定数量的内存块,并在程序运行期间重复使用这些内存块,内存池能够减少内存碎片化并提高内存分配效率。内存池通常用于需要频繁分配和释放内存的场景,如嵌入式系统、网络编程或图形处理等。

原理

  • 初始化:内存池在初始化阶段预先分配一定数量的内存块,并建立一个空闲内存块的列表。
  • 分配:当程序需要内存时,内存池从空闲列表中取出一个内存块并分配给程序。
  • 释放:当程序释放内存时,内存池将该内存块重新加入空闲列表,供后续使用。

内存池通过重复利用预分配的内存块,避免频繁向操作系统请求内存,减少内存碎片化,提高分配效率。

26. 指针使用的注意事项

  1. 空指针检查:在使用指针之前,始终检查指针是否为空(NULL),避免空指针操作导致程序崩溃或不可预测的行为。
  2. 避免野指针:避免使用未初始化的指针或已释放的指针,这些指针称为野指针,使用它们会导致不可预测的行为。
  3. 指针算术:进行指针算术操作时,确保不会越界访问内存区域。
  4. 指针类型转换:在进行指针类型转换时,确保转换是安全的,避免数据损坏或未定义行为。
  5. 动态内存管理:使用动态内存分配时,及时释放内存,避免内存泄漏。
  6. 指针传递:在函数间传递指针时,确保指针所指向的内存在函数调用期间保持有效。
  7. 指针的生命周期:确保指针的生命周期不超过其所指向对象的生命周期,以避免悬空指针问题。
  8. 指针的安全性:在多线程环境下,确保对指针的访问是线程安全的。

27. 在函数中申请堆内存需要注意什么?

  1. 避免返回栈内存的指针:栈内存在函数结束时会自动释放,返回指向栈内存的指针是无效的。
  2. 避免返回常量区的内存:常量字符串存放在代码段的常量区,无法修改,返回其指针没有意义。
  3. 使用二级指针或指针函数:通过使用二级指针或返回 malloc 申请的堆内存的地址,可以解决指针指向的问题,确保内存内容在函数外仍然有效。

28. 什么是内存碎片?

内存碎片 是指内存中未被有效利用的小块零散内存空间。它可以分为两种类型:外部碎片和内部碎片。

  1. 外部碎片

    • 外部碎片是指内存中未被使用的零散内存块,虽然总大小足够容纳需要分配的内存,但由于分散,无法满足连续内存分配的需求。
    • 外部碎片通常由于频繁的内存分配和释放操作引起。
  2. 内部碎片

    • 内部碎片是指已分配给程序的内存块中未被程序有效利用的部分。
    • 内部碎片通常由于内存分配器为了对齐或其他目的而分配了比程序请求更大的内存块。

29. 堆和栈的区别

  1. 空间分配不同

    • :由操作系统自动分配和释放,存放函数的参数值、局部变量等,效率高。
    • :由程序员手动分配和释放,效率较低。
  2. 缓存方式不同

    • :使用一级缓存,存储在处理器核心中,调用完成后立即释放,速度快。
    • :存储在二级缓存或主存中,速度相对较慢。
  3. 生长方向

    • :向地址较大的方向分配。
    • :向地址较小的方向分配。
  4. 生命周期

    • :内存分配后不会自动释放,需要手动释放。
    • :内存分配和释放是自动进行的,数据仅在特定作用域内有效。
  5. 空间大小

    • :空间较小,通常最多为2MB,超过则报溢出错误。
    • :空间较大,理论上可接近3GB(对于32位程序)。
  6. 是否产生碎片

    • :遵循"后进先出"原则,不会产生碎片。
    • :通过动态分配内存,频繁申请和释放内存可能引发内存碎片问题。

30. 初始化为0的全局变量在BSS还是Data段?

初始化为0的全局变量通常会被分配到程序的 BSS(Block Started by Symbol)段。BSS段用于存放未初始化或初始化为0的全局变量和静态变量。在程序加载时,系统会自动将BSS段中的变量初始化为0。

已经明确初始化为非零值的全局变量会被分配到程序的 Data段,Data段用于存放已经初始化的全局变量和静态变量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sagima_sdu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值