嵌入式软件笔试题考点整理(C语言篇)

也可通过 notion 或者 我的博客 访问

1. 字,字节,位的关系

  1. bit) 来自英文bit,音译为“比特”,表示二进制位。位是计算机内部数据储存的最小单位。
  2. 字节byte) 字节来自英文Byte,音译为“拜特”,习惯上用大写的“B”表示。 字节是计算机中数据处理的基本单位,内存编址以字节为单位
  3. word)计算机进行数据处理时,一次存取、加工和传送的数据长度称为字。一个字通常由一个或多个(一般是字节的整数位)字节构成。

1.1 相互转换

  1. 1字节(byte) = 8位(bit)
  2. 在16位的系统中(比如8086微机) 1字(word)= 2字节(byte)= 16(bit)
    在32位的系统中(比如win32)  1字(word)= 4字节(byte)= 32(bit)
    在64位的系统中(比如win64)  1字(word)= 8字节(byte)= 64(bit)

1.2 常用的变量类型所占字节

C语言中的基本数据类型有: char、short、int、long、float、double。

char:1个字节、8位;

short:2个字节、16位;

int:8/16位通常是2字节、16位;GCC编译器下32/64位的CPU为4字节、32位;

char*:指针类型,所有指针类型均与CPU本身的数据位宽一致,如:32位机器为4字节、32bit,而64位机器为8字节、64bit。

整型这个整,就体现在它和CPU本身的数据位宽是一样的,例如32位的CPU,int 就是32位。

不同变量在不同位数的处理器下所占的字节

变量类型8/16位处理器32位处理器64位处理器
char111
short int-22
int244
long int448
long long int-88
char*1/248
float444
double888

2. 变量的作用域及生命周期

局部变量:作用域及生命期为当前函数;

静态局部变量:作用域为当前函数,生命期为整个源程序

全局变量:作用域及生命期为整个源程序

静态局部变量:作用域为当前文件,生命期为整个源程序

3. 内存相关

3.1 内存编址:

内存由一个个内存单元组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和内存单元格式唯一对应且永久绑定。
在程序运行时,CPU实际上只认识内存地址,不关心这个地址所代表的的内存空间在哪里,如何分布的这些问题,因为硬件设计保证了按照这个地址就一定能找到这个内存空间,所以内存单元的两个概念:地址和空间是两个方面的问题。

内存编址是以字节(8bit)为单位。

3.2 内存和数据类型:

数据类型是用来定义变量的,而变量需要存储、运行在内存中的,所以数据类型和内存相匹配才能获得最好的性能。
例如:在32位系统中定义变量最好int,因为这样效率最高,原因就是32位系统本身配合内存也是32位,虽然也能定义8位的 char,或者是16位的short类型,但实际访问效率没有int高,但如果都用int,会导致内存浪费,所以实际开发中,需要考虑需要的是省内存还是运行效率。

3.3 内存对齐:

内存对齐不是逻辑上的问题,是硬件的问题。
对齐访问配合硬件,所以效率很高,非对齐访问因为和硬件本身不搭配,所以效率不高,但由于兼容性的问题,一般硬件也都提供非对齐访问,但效率很低

3.4 text段 data段 bss段

  • 程序执行前

    一个程序本质上都是由 bss段、data段、text段三个段组成。[1、5]

    • text段:代码段。存放代码和字符串,编译时确定,只读。
    • data段:数据段。已初始化的(非 0)全局变量 、常量 、全局或局部静态变量,存放在编译阶段(而非运行时)就能确定的数据,可读可写。
    • bss段:BSS段,存未初始化的静态变量 、 未初始化的全局变量 和 初始化为0的全局变量。BSS是Unix链接器产生的未初始化数据段,不包含任何数据,只是简单的维护开始和结束的地址[2]。一般在初始化时bss段变量将会清零(bss段属于静态内存分配,即程序一开始就将其清零了),可读可写。

    C/C++程序经编译器编译后产生的可执行文件,其大小由text段和data段决定。[3、5]

    原因:从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是 .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间。[3]

  • 程序执行时

    程序在执行时,会产生临时变量或者函数返回值,还会有函数中的动态分配地址空间(如 malloc、new),此时才会出现堆(heap)和栈(stack)[4]。

    • 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等,用来函数切换时保存现场。其操作方式类似于数据结构中的栈。栈地址是向下增长。

      满增栈 满减栈 空增栈 空减栈

    • 堆区(heap): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。堆地址一般是向上增长。

参照[2]绘制参照[2]绘制

引用:
[1] 浅谈text段、data段和bss段
[2] 终于知道什么叫BSS段
[3] 基础知识——嵌入式内存使用分析(text data bss及堆栈)
[4] 程序各个段text,data,bss,stack,heap
[5] (深入理解计算机系统) bss段,data段、text段、堆(heap)和栈(stack)

3.5 C/C++ 内存分区

程序执行时,内存也可按如下分区:

  • 动态存储区

    • 堆区(heap):由程序猿手动申请,手动释放。使用malloc或者new进行堆的申请。
    • 栈区(stack):由编译器自动分配释放。存放函数的参数、局部变量、局部常量(const 变量);
  • 静态存储区(全局区 static):静态存储区内的变量在程序编译阶段已经分配好内存空间并初始化。这块内存在程序的整个运行期间都存在。存放:字符串常量、全局常量(const 变量)、静态变量、全局变量等;

    可分全局已初始化区全局未初始化区(即上文BSS段中的变量),这里未做区分。静态存储区内的变量若未初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。

    • 常量存储区(const):常量占用内存,只读状态,不可修改。存放字符串常量和全局常量。
  • 程序代码区:存放程序编译后的二进制代码,不可寻址区。

int a = 0;//静态全局变量区 全局初始化区
char *p1; //静态全局变量区 中的 全局未初始化区,编译器默认初始化为 NULL
void main()
{
    int b; //栈
    char s[] = "abc";//栈
    char *p2 = "123456";//p2在栈上,123456在字符串常量区
    static int c = 0; //c在静态变量区,0为文字常量,在代码区
    const int d = 0; //栈
    static const int d;//静态常量区
    p1 = (char *)malloc(10);//分配得来得10字节在堆区。
    strcpy(p1, "123456"); //123456放在字符串常量区,编译器可能会将它与p2所指向的"123456"优化成一个地方
}

引用:
C/C++的四大内存分区和常量的存储位置
内存分区

4. 处理器大小端

大端(存储)模式: 是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中;
小端(存储)模式: 是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

因为在计算机系统中,我们以字节为存储单元,每个地址单元都对应着一个字节,一个字节为8bit。而在C语言中,不仅仅是一个字节来存储一个数据,除了一个字节的char,还有两个字节的short,四个字节的int等等(看具体编译器)。另外,对于位数大于8位的处理器,例如32位的处理器,由于寄存器的宽度大于一个字节,那么就有如何将多个字节进行排布的问题,于是就出现了大小端的问题。下面举个栗子:

Untitled.png

判断方法:

  1. 通过联合体判断

    定义联合体,一个成员是多字节,一个是单字节,给多字节的成员赋一个最低一个字节不为0,其他字节为0 的值,再用第二个成员来判断,如果第二个字节不为0,就是小端,若为0,就是大端。

    void judge_bigend_littleend2()
    {
        union
        {
            int i;
            char c;
        }un;
        un.i = 1;
    
        if (un.c == 1)
            printf("小端\n");
        else
            printf("大端\n");
    }
    
  2. 通过强制类型转换判断

    将int 48存起来,然后取得其地址,再将这个地址转为char* 这时候,如果是小端存储,那么char*指针就指向48;48对应的ASCII码为字符‘0’;

    void judge_bigend_littleend3()
    {
        int i = 48;
        int* p = &i;
        char c = 0;
        c = *((char*)p);
    
        if (c == '0')
            printf("小端\n");
        else
            printf("大端\n");
    }
    

引用:
大端 / 小端,三种判断方法
C++怎么判断大小端模式

5. 结构体

5.1 结构体的对齐

对内向上对齐,整体4字节对齐

  1. 结构体(struct)的数据成员,第一个数据成员存放的地址为结构体变量偏移量为0的地址处。

  2. 其他结构体成员自身对齐时,存放的地址为min{有效对齐值为自身对齐值, 指定对齐值}最小整数倍的地址处。

    自身对齐值:结构体变量里每个成员的自身大小
    指定对齐值:有宏 #pragma pack(N) 指定的值,这里面的 N一定是2的幂次方。如1,2,4,8,16等。如果没有通过宏那么在32位Linux主机上默认指定对齐值为4,64位的默认对齐值为8,AMR CPU默认指定对齐值为8;
    有效对齐值:结构体成员自身对齐时
    有效对齐值为自身对齐值
    指定对齐值中较小的一个。

  3. 总体对齐时,字节大小是min{所有成员中自身对齐值最大的,指定对齐值} 的整数倍。

5.2 对齐系数

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n), n = 1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。

32位系统默认4字节对齐,64位系统默认8字节对齐。

5.3 结构体大小计算

//此代码在64位Linux下编写
typedef struct _st_struct2
{
		char a;
		int c;
		short b;
}st_struct2;

printf("%ld\n",sizeof(st_struct2));

打印结果为:12

  • a是char类型,占1个字节。第一个数据成员,放在结构体变量偏移量为0 的地址处。
  • c是int类型,占4个字节,我们根据结构体对齐规则可知,c的有效对齐值为4。对齐到4的整数倍地址,即地址偏移量为4处。在内存中存放的位置为4、5、6、7。
  • b是short类型,占2个字节,我们根据结构体对齐规则可知,b的有效对齐值为2。对齐到2的整数倍地址,即地址偏移量为8处。在内存中存放的位置为8、9。
  • 结构体总对齐字节大小为min{4, 8}=4 的整数倍。此时内存中共占10个字节,又要求是4的整数倍,所以sizeof(st_struct1)=12

20210117212221200.png

5.4 结构体的位域

该程序未指定对齐系数,因此为系统默认对齐系数。

struct ftl_block_status
{
		zx_uint32_t erase_times : 28;
		zx_uint32_t block_status: 3;
		zx_uint32_t reserv : 1;
		struct ftl_pagestatus page_status;  /*status bitmap for each page inside of block */
};

结构体里的这三个变量总体只占4个字节,冒号后面的数字为指定这个变量占几位。

6. static修饰符

  • static修饰局部变量:将局部变量转换成静态局部变量,在全局数据区分配内存空间,编译器自动对其初始化,即使函数返回,它的值也会保持不变。其作用域为局部作用域。
  • static修饰全局变量:将其转换成静态全局变量,仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。
  • static修饰函数:将其转换成静态函数,只能在声明它的文件中可见,其他文件不能引用该函数。不同的文件可以使用相同名字的静态函数,互不影响。
  • static修饰成员变量:将其转换成静态成员变量
    • 在编译阶段分配内存。静态成员变量存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中初始化。
    • 所有对象共享同一份数据。静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝。
    • 由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作。
    • 和普通数据成员一样,静态数据成员也遵从public, protected, private访问规则。
    • 类内声明,类外初始化。静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员名>=<值>
    • 类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
  • static修饰成员函数:将其转换成静态成员函数
    • 所有对象共享同一个函数。静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。
    • 出现在类体外的函数定义不能指定关键字static。
    • 静态成员函数只能访问静态成员变量。非静态成员函数可以任意地访问静态成员函数和静态数据成员。

7. const 修饰

  • const 修饰变量:该变量为常量,不可修改,代表 只读。必须要给变量初始化。

  • const 修饰指针

    • const修饰指针:常量指针。指针指向可以改,指针指向的值不可以更改,但是还是可以通过其他的引用来改变变量的值的

      int a = 5;
      const int* p1 = &a;
      a = 6;
      
    • const修饰常量:指针常量。指针指向不可以改,指针指向的值可以更改。

      int* const p2 = &a;
      

      区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果const在星号的左边,则为常量指针,如果const在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const读作‘常量’的话,内容正好符合。

    • const即修饰指针,又修饰常量。指针指向不可以改,指针指向的值也不可以更改。

      const int* const p3 = &a;
      
  • const 修饰形参

    根据常量指针与指针常量,const修饰函数的参数也是分为三种情况

    • 防止修改指针指向的值

      void StringCopy(char *strDestination, const char *strSource);
      

      其中 strSource 是输入参数,strDestination 是输出参数。给 strSource 加上 const 修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。

    • 防止修改指针指向的地址

      void swap ( int * const p1 , int * const p2 )
      

      指针p1和指针p2指向的地址都不能修改。

    • 以上两种的结合

  • const 修饰函数的返回值:函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
    例如函数

    const char * GetString(void);
    

    如下语句将出现编译错误:

    char *str = GetString();
    

    正确的用法是

    const char *str = GetString();
    
  • const 修饰类成员函数

    • 常函数:const 修饰的是 this 指针指向空间的内容。

      • 成员函数后加const后我们称为这个函数为常函数
      • 常函数内不可以修改成员属性。
      • 成员属性声明时加关键字mutable后,在常函数中依然可以修改。
      // this指针的本质 是指针常量  也就是指针的指向是不可以修改的
      // this指针相当于Person * const this
      // 在成员函数后面加const,修饰的是this指向的内容,让指针指向的值也不可以修改,相当于const Person * const this
      void showPerson() const
      {
      	this->m_B = 100;
      	//m_A = 100; //相当于  this->m_A = 100;
      	//this = NULL; //this指针不可以修改指针的指向的
      }
      int m_A;
      mutable int m_B; //特殊变量,即使在常函数中,也可以修改这个值,加关键字 mutable
      
    • 常对象:

      • 声明对象前加const称该对象为常对象。
      • 常对象只能调用常函数。
      const Person p; //在对象前加入const,变为常对象
      //p.m_A = 100; // 常对象不能修改成员变量的值,但是可以访问
      p.m_B = 100; // 但是常对象可以修改mutable修饰成员变量
      
      //常对象只能调用常函数
      p.showPerson();
      //p.func(); //常对象 不可以调用普通成员函数,因为普通成员函数可以修改属性
      

8. volatile 修饰

提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

在编译阶段起作用。

详细解析

9. inline 修饰函数:内联函数

在函数声明或定义中,函数返回类型前加上关键字inline,即可以把函数指定为内联函数。这样可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。

内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在内联函数的位置上,而主函数在调用一般函数时,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码。

详细解析

10. 编译的四个步骤

预处理 编译 汇编 链接

11. 字符串拷贝函数

strcpy函数的缺陷:可能会内存溢出,strncpy 不会,最安全的是 strncpy_s

12. 指针函数和函数指针

12.1 指针函数

函数的返回值为指针,其声明的形式如下:

ret * func(args, ...);

其中,func是一个函数,args是形参列表,ret *作为一个整体,是 func函数的返回值,是一个指针的形式。

下面举一个具体的实例来做说明:

# include <stdio.h>

int * func_sum(int n)
{
    static int sum = n; // 必须加 static
    int *p = &sum;
    return p;
}

int main(void)
{
    int num = 0;
    scanf("%d", &num);
    int *p = func_sum(num);
    printf("sum:%d\n", *p);
    return 0;
}

⚠️ 指针函数返回局部变量地址时,必须使用 静态局部变量。因为局部变量储存在 栈区,函数执行完后,该局部变量地址会被释放,在执行后面程序时,该地址可能被其他变量占用,地址里的值就会被修改。而静态局部变量储存在全局区(static data),程序执行完后不会被释放。

12.2 函数指针

储存函数入口地址的指针。声明形式如下:

ret (*p)(args, ...);

其中,ret为返回值,*p作为一个整体,代表的是指向该函数的指针,args为形参列表。其中p被称为函数指针变量

函数指针所占用字节与CPU本身的数据位宽一致,如:32位机器为32bit,而64位机器为64bit。

定义及初始化:

#include <stdio.h>

int max(int a, int b)
{
    return a > b ? a : b;
}

int callback(int a, int b, int (*p)(int, int))
{
    return p(a, b);
}

int main()
{
    int (*p)(int, int); //函数指针的定义
    //int (*p)();       //函数指针的另一种定义方式,不过不建议使用
    //int (*p)(int a, int b);   //也可以使用这种方式定义函数指针

    p = max;    //函数指针初始化

    int ret = p(10, 15);    //函数指针的调用
    //int ret = (*max)(10,15);
    //int ret = (*p)(10,15);
    //以上两种写法与第一种写法是等价的,不过建议使用第一种方式

    //也可以作为函数的形参进行传递
    int ret1 = callback(10, 15, max);

    printf("max = %d \n", ret);
    printf("max = %d \n", ret1);

    return 0;
}

13. typedef关键字

  1. 为基本数据类型定义新的类型名

    typedef unsigned int COUNT;
    
  2. 为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称

    typedef struct tagPoint
    {
        double x;
        double y;
        double z;
    } Point;
    
  3. 为数组定义简洁的类型名称

    typedef int INT_ARRAY_100[100];
    INT_ARRAY_100 arr;
    
  4. 为指针定义简洁的名称

    typedef char* PCHAR;
    PCHAR pa;
    

详见:typedef的用法,C语言typedef详解

14. sizeof和strlen

首先,strlen 是函数,sizeof 是运算操作符,二者得到的结果类型为 size_t,即 unsigned int 类型。大部分编译程序在编译的时候就把 sizeof 计算过了,而 strlen 的结果要在运行的时候才能计算出来。

  • sizeof 获得变量或数据类型的字节大小,可用于类、结构、共用体和其他用户自定义数据类型;
  • strlen 返回的是该字符串的长度,遇到 \0 结束, \0 本身不计算在内。

**例:**对于以下语句:

char *str1 = "asdfgh";
char str2[] = "asdfgh";
char str3[8] = {'a', 's', 'd'};
char str4[] = "as\0df";

执行结果是:

sizeof(str1) = 4;  strlen(str1) = 6;
sizeof(str2) = 7;  strlen(str2) = 6;
sizeof(str3) = 8;  strlen(str3) = 3;
sizeof(str4) = 6;  strlen(str4) = 2;

15. 数组

15.1 数组指针和指针数组

  • 指针数组 char* arr[4] :指针数组可以说成是”指针组成的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型。
  • 数组指针 char (*pa)[4] :数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。

详见:指针数组与数组指针详解

15.2 一维数组的访问方式

一维数组的10种访问方式:

#include<stdio.h>
int main()
{
		int a[10]={0,1,2,3,4,5,6,7,8,9};
		int *p=a;
		printf("%d %d %d %d %d %d %d %d %d %d ",
					 0[a],*(p+1),*(a+2),a[3],p[4],5[p],(&a[5])[1],1[(&a[6])],(&a[9])[-1],9[&a[0]]);
		
		return 0;
}

输出:0 1 2 3 4 5 6 7 8 9

数组的地址:

int main(){    
	int a[5]={1,2,3,4,5};    
	**int *ptr=(int*)(&a+1); //相当于int *ptr=*(&a+1); a指向int类型,&a指向数组类型 
	printf("%d,%d",*(a+1),*(ptr-1));
}

输出 2,5

💡 不可使用数组名自加,如 a++ 会报错。

15.2 二维数组的访问方式

二维数组:

int arr[4][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}};
  1. 当成一维数组来访问二维数组

    int  *p = arr[0];
    for(int i = 0 ;i<16;i++)
            cout << *(p + i) << ' ';
    
  2. 使用数组指针的方式访问二维数组

    int (*p1)[4] = arr; //指向含有四个元素一维数组的首地址
    for(int i = 0;i<4;i++)
        for(int j = 0;j<4;j++)
            cout << *(*(p1 + i)+j) << ' ';
    
  3. 使用指针数组的方式访问二维数组

    int  *p2[4];		//定义指针数组
    for(int k = 0 ;k<4;k++)
        p2[k] = arr[k];  //每个指针指向行元素,存储每行首地址
    for(int i = 0;i<4;i++)
        for(int j = 0;j<4;j++)
            cout << *(p2[i]+j) << ' '; //p2[i]已经存储并指向每行的首地址了
    
  4. 使用指针的指针&指针数组

    int **pointer; //指向指针的指针
    int *pp[4];    //指针数组
    for(int i = 0; i < 4; i++)
        pp[i] = arr[i]; //每个指针指向行元素,存储每行首地址
    pointer = pp;
    for(int i = 0; i < 4; i++)
        for(int j = 0; j < 4; j++)
            cout << *(*(pointer + i) + j) << ' ';
    
  • 二维数组的第 i 行起始地址的表示方法

    arr+i
    *(arr+i)
    arr[i]
    &arr[i]
    

16. 运算符

16.1 不能重载的运算符

  1. .”(类成员访问运算符)
  2. " .*"(类成员指针访问运算符)
  3. ::”(域运算符)
  4. siezof”(长度运算符)
  5. ?:”(条件运算符)

16.2 运算对象必须是整型的运算符

% 取余运算符

  • 13
    点赞
  • 116
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值