【嵌入式笔/面试】嵌入式软件基础题和真题总结——C/C++

在学习的时候找到几个十分好的工程和个人博客,先码一下,内容都摘自其中,有些重难点做了补充!
才鲸 / 嵌入式软件笔试题汇总
嵌入式与Linux那些事
阿秀的学习笔记
小林coding
百问网linux
嵌入式软件面试合集
2022年春招实习十四面(嵌入式面经)

说明:C++更多的内容后面再补充,平时还是主攻C比较多,等系统学完再补充。 还有很多面试具体真题,待把其它基础点更完再一一补充。力争做到一步到位!

文章目录

  • 一、关键字
    • 1.1 C语言宏中"#“和”##"的用法
      • 1.1.1 (#)字符串化操作符
      • 1.1.2 (##)符号连接操作符
    • 1.2 volatile
      • 1.2.1 定义与含义
      • 1.2.2 使用举例
    • 1.3 const和static
      • 1.3.1 C中定义和作用
      • 1.3.2 C++中定义和作用
      • 1.3.3 一些常见的面试提问
        • 1.3.3.1 解释下面几个表达式的意思(从右往左)
        • 1.3.3.2 const常量和#define的区别(编译阶段、安全性、内存占用等)
    • 1.4 extern
      • 1.4.1 语法和作用
      • 1.4.2 extern"C"的用法
    • 1.5 new/delete与malloc/free
      • 1.5.1 区别
    • 1.6 sizeof 与strlen
      • 1.6.1 区别
    • 1.7 struct和union
      • 1.7.1 区别
      • 1.7.2 内存对齐
        • 1.7.2.1 作用
        • 1.7.2.2 结构体struct内存对齐的3大规则
        • 1.7.2.3 联合体union内存对齐的2大规则:
      • 1.7.3 引申:位域
    • 1.8 左值和右值
    • 1.9 短路求值
    • 1.10 ++a和a++
  • 二、内存
    • 2.1 C语言内存四区
      • 2.1.1 概念
      • 2.1.2 C语言中内存分配的方式
      • 2.1.3 堆与栈的区别
    • 2.2 C++的内存管理
      • 2.2.1 虚拟内存
    • 2.3 内存泄漏
      • 2.3.1 概念
      • 2.3.2 防止与定位方法
    • 2.4 大小端
      • 2.4.1 概念
      • 2.4.2 判断方式
    • 2.5 memcpy(), strcpy(), strcmp(), strcat()有什么区别,如何避免内存越界?
      • 2.5.1 安全函数memcpy_s(), strcpy_s(), strcmp_s()的使用
  • 三、指针和数组
    • 3.1 指针和数组的区别
    • 3.2 数组指针和指针数组的区别
    • 3.3 函数指针和指针函数的区别
      • 3.3.1 函数指针
      • 3.3.2 指针函数
    • 3.4 数组名和指针的区别与联系
    • 3.5 指针和引用
      • 3.5.1 异同点
      • 3.5.2 转换
    • 3.6 野指针
      • 3.6.1 定义
      • 3.6.2 如何避免野指针
    • 3.7 数组和链表
      • 3.7.1数组和链表的优点
  • 四、预处理
    • 4.1 typedef和 define
      • 4.1.1 二者区别
        • 4.1.1.1 原理不同
        • 4.1.1.2 功能不同
        • 4.1.1.3 作用域不同
        • 4.1.1.4 对指针的操作不同
    • 4.2 # include< filename. h>和# include" filename. h"有什么区别?
    • 4.3 不使用流程控制语句,如何打印出1~1000的整数?
    • 4.4 从代码编译到可执行文件的流程?
  • 五、函数
    • 5.1 attribute的用法
      • 5.1.1 请写个函数在main函数执行前先运行
    • 5.2 C语言是怎么进行函数调用的?
    • 5.3 inline函数
    • 5.4 析构函数和虚函数
      • 5.4.1 为什么析构函数必须是虚函数?
      • 5.4.2 为什么C++默认的析构函数不是虚函数?
      • 5.4.3 C++中析构函数的作用?
      • 5.4.4 静态函数和虚函数的区别?
      • 5.4.5 虚函数表具体是怎样实现运行时多态的?
    • 5.5 重载和覆盖
  • 六、 位操作
    • 6.1 如何求解整型数的二进制表示中1的个数?
    • 6.2 如何求解二进制中0的个数?
    • 6.3 交换两个变量的值,不使用第三个变量?
  • 七、待补充

一、关键字

1.1 C语言宏中"#“和”##"的用法

1.1.1 (#)字符串化操作符

作用:将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。其只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。
举例:

#define example( instr ) printf( "the input string is:\t%s\n", #instr )
#define example1( instr ) #instr当使用该宏定义时:

example( abc ); // 在编译时将会展开成:printf("the input string is:\t%s\n","abc")
string str = example1( abc );  // 将会展成:string str="abc"

1.1.2 (##)符号连接操作符

作用:将宏定义的多个形参转换成一个实际参数名。
举例:

#define exampleNum( n ) num##n

int num9 = 9;
int num = exampleNum( 9 ); // 将会扩展成 int num = num9
  1. 当用##连接形参时,##前后的空格可有可无。
#define exampleNum( n )       num ## n                 
// 相当于 #define exampleNum( n )     num##n
  1. 连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。
  2. 如果##后的参数本身也是一个宏的话,##会阻止这个宏的展开。
    举例:
#include 
#include 
#define STRCPY(a, b)   strcpy(a ## _p, #b)
int main()
{
    char var1_p[20];
    char var2_p[30];
    strcpy(var1_p, "aaaa");
    strcpy(var2_p, "bbbb");
    STRCPY(var1, var2);
    STRCPY(var2, var1);
    printf("var1 = %s\n", var1_p);
    printf("var2 = %s\n", var2_p);
    //STRCPY(STRCPY(var1,var2),var2);
    //这里是否会展开为: strcpy(strcpy(var1_p,"var2")_p,"var2“)?答案是否定的:
    //展开结果将是: strcpy(STRCPY(var1,var2)_p,"var2")
    //## 阻止了参数的宏展开!如果宏定义里没有用到 # 和 ##, 宏将会完全展开
    // 把注释打开的话,会报错:implicit declaration of function 'STRCPY'
    return 0;
}

结果:

var1 = var2
var2 = var1

1.2 volatile

1.2.1 定义与含义

(1)volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
(2)volatile 指针
volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念修饰由指针指向的对象、数据是 const 或 volatile 的:

const char* cpch;volatile char* vpch;

针自身的值——一个代表地址的整数变量,是 const 或 volatile 的:

char* const pchc;char* volatile pchv;

注意:

  1. 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
  2. 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
  3. C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

(3)多线程下的volatile
有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,**该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。**如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值

1.2.2 使用举例

  1. 并行设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
  2. 一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
  3. 多线程应用中被几个任务共享的变量。单地说就是防止编译器对代码进行优化.比如如下程序:
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;

对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,编译器会逐一的进行编译并产生相应的机器代码(产生四条代码)。

1.3 const和static

1.3.1 C中定义和作用

  • static
    1)用static修饰局部变量:使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
    2)用static修饰全局变量:使其只在本文件内部有效,而其他文件不可连接或引用该变量。
    3)用static修饰函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的(这一点在大工程中很重要很重要,避免很多麻烦,很常见)。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。
  • const
    const主要用来修饰变量、函数形参和类成员函数:
    1)用const修饰常量:定义时就初始化,以后不能更改。
    2)用const修饰形参:func(const int a){};该形参在函数里不能改变
    3)用const修饰类成员函数:该函数对成员变量只能进行只读操作,就是const类成员函数是不能修改
    成员变量的数值的。
    被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

1.3.2 C++中定义和作用

  • static

    • 不考虑类的情况
      • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用;
      • 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区;
      • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。
    • 考虑类的情况
      • static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问;
      • static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问。
  • const

    • 不考虑类的情况

      • const常量在定义时必须初始化,之后无法更改;
      • const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}。
    • 考虑类的情况

      • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化;
      • const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值。

补充一点const相关:const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。

1.3.3 一些常见的面试提问

1.3.3.1 解释下面几个表达式的意思(从右往左)
const int a;//a是一个常整型数
int const a;//a是一个常整型数

const int*a; //常量指针,指向常量的指针。即p指向的内存可以变,p指向的数值内容不可变
int const*a; //同上

int *const a; //a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。

int const *const a ;  //a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)
const int *const a;   //a是一个常量指针,且指向整型常量 ---> p是一个指向整型常量的常量指针(同上)

指针常量和常量指针

1.3.3.2 const常量和#define的区别(编译阶段、安全性、内存占用等)
  • 用#define max 100 ; 定义的常量是没有类型的(不进行类型安全检查,可能会产生意想不到的错误),所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,define所定义的宏变量在预处理阶段的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换;
  • 用const int max = 255 ; 定义的常量有类型(编译时会进行类型检查)名字,存放在内存的静态区域中,在编译时确定其值。在程序运行过程中const变量只有一个拷贝,而#define所定义的宏变量却有多个拷贝,所以宏定义在程序运行过程中所消耗的内存要比const变量的大得多

1.4 extern

1.4.1 语法和作用

extern放在变量和函数声明之前,表示该变量或者函数在别的文件中已经定义,提示编译器在编译时要从别的文件中寻找。除此之外,extern还可以用来进行链接指定。
extern在链接阶段起作用(四大阶段:预处理–编译–汇编–链接)。
C语言extern关键字用法和理解

1.4.2 extern"C"的用法

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
用法举例:
(1)C++调用C函数:

//xx.h
extern int add(...)
//xx.c
int add(){
    
}
//xx.cpp
extern "C" {
    #include "xx.h"
}
 

(2)C调用C++函数

//xx.h
extern "C"{
    int add();
}
//xx.cpp
int add(){    
}
//xx.c
extern int add();

1.5 new/delete与malloc/free

1.5.1 区别

  1. new、delete是C++中的操作符,而malloc和free是标准库函数。
  2. 对于非内部数据对象来说,只使用malloc是无法完成动态对象要求的,一般在创建对象时需要调用构造函数,对象消亡时,自动的调用析构函数。而malloc free是库函数而不是运算符,不在编译器控制范围之内,不能够自动调用构造函数和析构函数。而NEW在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对象的初始化。同理,delete也可以自动调用析构函数。而mallloc只是做一件事,只是为变量分配了内存,同理,free也只是释放变量的内存。
  3. new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而 malloc需要我们计算申请内存的大小,并且在返回时强行转换为实际类型的指针。

1.6 sizeof 与strlen

1.6.1 区别

strlen与 sizeof的差别表现在以下5个方面。

  1. sizeof是运算符(是不是被弄糊涂了?事实上, sizeof既是关键字,也是运算符,但不是函数),而strlen是函数。 sizeof后如果是类型,则必须加括弧,如果是变量名,则可以不加括弧。
  2. sizeof运算符的结果类型是 size_t,它在头文件中 typedef为 unsigned int类型。该类型保证能够容纳实现所建立的最大对象的字节大小
  3. sizeof可以用类型作为参数, strlen只能用char*作参数,而且必须是以“0结尾的。 sizeof还可以以函数作为参数,如int g(),则 sizeof(g())的值等于 sizeof( int)的值,在32位计算机下,该值为4。
  4. 大部分编译程序的 sizeof都是在编译的时候计算的,所以可以通过 sizeof(x)来定义数组维数。而 strlen则是在运行期计算的,用来计算字符串的实际长度,不是类型占内存的大小。例如, char str[20] = "0123456789”,字符数组str是编译期大小已经固定的数组,在32位机器下,为 sizeof(char)*20=20,而其 strlen大小则是在运行期确定的,所以其值为字符串的实际长度10。
  5. 当数组作为参数传给函数时,传递的是指针,而不是数组,即传递的是数组的首地址。

1.7 struct和union

1.7.1 区别

struct(结构体)与 union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区别主要表现在以下两个方面。

  1. 结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的,其所有成员都存在,不同成员会存放在不同的地址。在计算一个结构型变量的总长度时,其内存空间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度。
  2. 对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了,而对结构体的不同成员赋值是互不影响的。

1.7.2 内存对齐

1.7.2.1 作用

内存对齐作用:
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

1.7.2.2 结构体struct内存对齐的3大规则

1.对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍;
2.结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍;
3.如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。

举例

#pragma pack(1)
	struct fun{
	int i;
	double d;
	char c;
};

sizeof(fun) = 13
struct CAT_s
{
	int ld;
	char Color;
	unsigned short Age;
	char *Name;
	void(*Jump)(void);
}Garfield;

1.使用32位编译,int占4, char 占1, unsigned short 占2,char* 占4,函数指针占4个,由于是32位编译是4字节对齐,所以该结构体占16个字节。(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是int是最长的字节,所以按4字节对齐);
2.使用64位编译 ,int占4, char 占1, unsigned short 占2,char* 占8,函数指针占8个,由于是64位编译是8字节对齐(说明:按几字节对齐,是根据结构体的最长类型决定的,这里是函数指针是最长的字节,所以按8字节对齐)所以该结构体占24个字节。

//64位
struct C
{
	double t; //8 1111 1111
	char b; //1 1
	int a; //4 0001111
	short c; //2 11000000
};
sizeof(C) = 24; //注意:1 4 2 不能拼在一起

char是1,然后在int之前,地址偏移量得是4的倍数,所以char后面补三个字节,也就是char占了4个字
节,然后int四个字节,最后是short,只占两个字节,但是总的偏移量得是double的倍数,也就是8的倍
数,所以short后面补六个字节

1.7.2.3 联合体union内存对齐的2大规则:

1.找到占用字节最多的成员;
2.union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员。

//x64
typedef union {
	long i;
	int k[5];
	char c;
}D

要计算union的大小,首先要找到占用字节最多的成员,本例中是long,占用8个字节,int k[5]中都是int类型,
仍然是占用4个字节的,然后union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳
其他的成员,为了要容纳k(20个字节),就必须要保证是8的倍数的同时还要大于20个字节,所以是24个字
节。

1.7.3 引申:位域

C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。一个位段必须存储在同一存储单元中,不能跨两个单元。如果第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。
1.位段声明和结构体类似;
2.位段的成员必须是int、unsigned int、signed int;
3.位段的成员名后边有一个冒号和一个数字。

typedef struct_data{
	char m:3;
	char n:5;
	short s;
	union{
		int a;
		char b;
	};
	int h;
}_attribute_((packed)) data_t;

答案12
m和n一起,刚好占用一个字节内存,因为后面是short类型变量,所以在short s之前,应该补一个字节。所以m和n其实是占了两个字节的,然后是short两个个字节,加起来就4个字节,然后联合体占了四个字节,总共8个字节了,最后int h占了四个字节,就是12个字节了

attribute((packed)) 取消对齐
GNU C的一大特色就是attribute机制。attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
attribute书写特征是:attribute前后都有两个下划线,并且后面会紧跟一对括弧,括弧里面是相应的attribute参数。
跨平台通信时用到。不同平台内存对齐方式不同。如果使用结构体进行平台间的通信,会有问题。例如,发送消息的平台上,结构体为24字节,接受消息的平台上,此结构体为32字节(只是随便举个例子),那么每个变量对应的值就不对了。
不同框架的处理器对齐方式会有不同,这个时候不指定对齐的话,会产生错误结果

1.8 左值和右值

左值是指可以出现在等号左边的变量或表达式(例如变量、数组元素、结构体成员等),它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。
右值是指只可以出现在等号右边的变量或表达式(例如一个常量、字面量、表达式的计算结果等)。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。
通常,左值可以作为右值,但是右值不一定是左值。

在 C 语言中,左值和右值在语法上有一些差别。例如,左值可以出现在地址运算符 & 的操作数中,而右值不能。例如:

int a = 1;
int *p = &a;  // &a 是一个左值,可以取地址
int *q = &1;  // 错误:&1 是一个右值,不能取地址

需要注意的是,有些表达式既可以是左值也可以是右值,这取决于上下文。例如,数组名可以被用作左值(例如 a[0] = 1;),也可以被用作右值(例如 int *p = a;)。

1.9 短路求值

下面的输出结果是多少:

#include 
int main()
{
	int i = 6;
	int j = 1;
	if(i>0||(j++)>0);
	printf("%D\r\n",j);
	return 0;
}

参考答案:输出1
说明:

  • 输出为什么不是2,而是1呢?其实,这里就涉及一个短路计算的问题。由于i语句是个条件判断语句,里面是有两个简单语句进行或运算组合的复合语句,因为或运算中,只要参与或运算的两个表达式的值都为真,则整个运算结果为真,而由于变量i的值为6,已经大于0了,而该语句已经为true,则不需要执行后续的j+操作来判断真假,所以后续的j++操作不需要执行,j的值仍然为1。
  • 因为短路计算的问题,对于&&操作,由于在两个表达式的返回值中,如果有一个为假则整个表达式的值都为假,如果前一个语句的返回值为 false,则无论后一个语句的返回值是真是假,整个条件判断都为假,不用执行后一个语句,而a>b的返回值为 false,程序不执行表达式n=c>d,所以,n的值保持为初值2。

1.10 ++a和a++

a++和++a 都是自增运算符,俩者的区别在于对变量a的值进行自增的时机不同==> a++是先取值后自增; ++a是先自增后取值。
后置自增运算符需要把原来变量的值复制到一个临时的存储空间,等运算结束后才会返回这个临时变量的值。所以前置自增运算符效率比后置自增要高
举例如下:

1:var a=10 ; b=20; c=4;++b+c+a++的值
++b=21;a++=10;c=4=> ++b+c+a++=21+4+10=35;2:var a=10, b=20 , c=30;
    ++a;//①
    a++;//②
    e=++a+(++b)+(c++)+a++;//③
    alert(e);=> a=11(自增后取值为11;=> a=11(直接取值,此时a在下次运算时自增值为12);=> ++a=13 ++b=21 c++ = 30 a++=13
所以e=77

二、内存

2.1 C语言内存四区

2.1.1 概念

代码区:也称为文本区,存储程序的可执行代码,通常是只读的。在程序运行时,代码区的内容被加载到内存中,并且只能被执行,不能被修改。

数据区:也称为静态区,存储程序中已经初始化的全局变量、静态变量、常量等。数据区在程序启动时被分配,并且在程序结束时被释放。数据区分为只读数据区和可读写数据区。只读数据区存储不能被修改的常量和字符串,而可读写数据区存储可以被修改的全局变量、静态变量等。

堆区:是由程序员自行申请和释放的内存空间。在程序运行时,堆区的内存空间是动态分配的,可以通过 malloc、calloc、realloc 等函数进行分配。堆区的内存空间需要手动释放,否则会导致内存泄漏。

栈区:存储程序的局部变量、函数参数等。在函数调用时,栈区会自动分配一定的内存空间,当函数返回时,这些内存空间会被自动释放。栈区的大小是固定的,通常比堆区小得多。

2.1.2 C语言中内存分配的方式

静态分配:在程序编译时就分配好内存,通常用于定义全局变量、静态变量、常量等。静态分配的内存在程序启动时就已经分配好,直到程序结束时才会释放。

栈上分配:在函数调用时分配内存,通常用于定义局部变量、函数参数等。栈上分配的内存在函数执行完毕时就会自动释放,因此不需要手动释放。

堆上分配:在程序运行时根据需要动态分配内存,通常使用 malloc、calloc、realloc 等函数进行内存分配。堆上分配的内存需要手动释放,否则会导致内存泄漏。

2.1.3 堆与栈的区别

堆(Heap)和栈(Stack)是计算机内存中两种不同的内存管理方式,在内存分配、使用、释放等方面存在以下几点区别:

  1. 内存分配方式:栈采用先进后出的方式进行内存分配,而堆采用动态分配的方式进行内存分配。
  2. 内存分配效率:栈的内存分配效率高于堆,因为栈分配内存只需要移动栈顶指针,而堆的内存分配需要进行搜索、分配等复杂的操作。同时,栈的内存分配方式也更容易实现内存的空间管理和回收。
  3. 内存空间大小:栈的内存空间大小是固定的,通常比堆小得多,而堆的内存空间则可以动态扩展,因此堆的内存空间通常比栈大。
  4. 内存分配方式:栈的内存分配是由编译器自动完成的,而堆的内存分配则需要程序员手动进行操作,使用动态内存分配函数(如malloccallocrealloc等)进行内存分配。
  5. 内存访问方式:栈的内存访问速度快于堆,因为栈的内存地址是连续的,可以直接访问,而堆的内存地址则是分散的,需要进行搜索和解析。

2.2 C++的内存管理

2.2.1 虚拟内存

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
BSS 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态
变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

2.3 内存泄漏

2.3.1 概念

简单地说就是申请了一块内存空间,使用完毕后没有释放掉。
它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

2.3.2 防止与定位方法

避免内存泄露的几种方式:

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露;
  • 一定要将基类的析构函数声明为虚函数;
  • 对象数组的释放一定要用delete [];
  • 有new就有delete,有malloc就有free,保证它们一定成对出现。
    检测工具
  • Linux下可以使用Valgrind工具;
  • Windows下可以使用CRT库。

2.4 大小端

2.4.1 概念

  • 大端存储:字数据的高字节存储在低地址中
  • 小端存储:字数据的低字节存储在低地址中
    例如:32bit的数字0x12345678

小端:

内存地址 0x4000 0x4001 0x4002 0x4003
存放内容 0x78 0x56 0x34 0x12

大端:

内存地址 0x4000 0x4001 0x4002 0x4003
存放内容 0x12 0x34 0x56 0x78

2.4.2 判断方式

方式一:使用强制类型转换

#include 
using namespace std;
int main()
{
    int a = 0x1234;
    //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)
        cout << "big endian" << endl;
    else if(c == 0x34)
        cout << "little endian" << endl;
}

方式二:巧用union联合体

#include 
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
    int a;
    char ch;
};
int main()
{
    endian value;
    value.a = 0x1234;
    //a和ch共用4字节的内存空间
    if (value.ch == 0x12)
        cout << "big endian"<<endl;
    else if (value.ch == 0x34)
        cout << "little endian"<<endl;
}

2.5 memcpy(), strcpy(), strcmp(), strcat()有什么区别,如何避免内存越界?

memcpy(), strcpy(), strcmp(), strcat()都是C语言中常用的字符串处理函数,它们之间的区别如下:

  1. memcpy()函数:用于复制一段内存区域的数据。它的函数原型为void *memcpy(void *dest, const void *src, size_t n),其中dest表示目标内存地址,src表示源内存地址,n表示需要复制的字节数。
  2. strcpy()函数:用于将一个字符串复制到另一个字符串中。它的函数原型为char *strcpy(char *dest, const char *src),其中dest表示目标字符串地址,src表示源字符串地址,它会将源字符串中的字符复制到目标字符串中,直到遇到’\0’结束。
  3. strcmp()函数:用于比较两个字符串是否相同。它的函数原型为int strcmp(const char *s1, const char *s2),其中s1和s2分别表示要比较的两个字符串,如果s1s2返回正数。
  4. strcat()函数:用于将一个字符串连接到另一个字符串的末尾。它的函数原型为char *strcat(char *dest, const char *src),其中dest表示目标字符串地址,src表示源字符串地址,它会将源字符串中的字符连接到目标字符串的末尾,直到遇到’\0’结束。

memcpy(), strcpy()的区别

  1. 功能不同:memcpy()函数用于复制一段内存区域的数据,而strcpy()函数用于将一个字符串复制到另一个字符串中

  2. 参数不同:memcpy()函数需要指定要复制的内存区域的起始地址和长度,而strcpy()函数需要指定源字符串的地址和目标字符串的地址

  3. 复制方式不同:memcpy()函数是按字节复制,没有字符串的结束符’\0’的限制,可以复制任意长度的内存区域。而strcpy()函数是按字符复制,需要遇到字符串结束符’\0’才会停止复制。

  4. 安全性不同:由于strcpy()函数没有指定目标字符串的大小,容易发生缓冲区溢出等安全问题。而memcpy()函数需要指定要复制的内存区域的长度,可以避免缓冲区溢出的问题。

总之,memcpy()和strcpy()是两个不同的函数,主要用途也不同。在实际的程序开发中,应根据具体的需求选择合适的函数,以保证代码的安全性和可靠性。如果需要复制一个字符串,应该选择strcpy()函数,如果需要复制一段内存区域,应该选择memcpy()函数。同时,为了保证代码的安全性,可以使用一些安全函数,如memcpy_s()和strcpy_s()等。

注意1:
strncpy拷贝函数并不算安全(总结:可能出现目标字符串小于源字符串的情况,会被截断,同时这种情况下不会自动添加“\0”)
需要手动添加字符串结束符’\0’:由于strncpy函数在复制过程中可能不会自动添加字符串结束符’\0’,因此需要手动在目标字符串的末尾添加’\0’,否则可能会导致目标字符串没有结束符,影响后续的字符串处理。

需要保证目标字符串的大小:strncpy函数在复制过程中会考虑目标字符串的大小,但需要保证目标字符串的大小足够大,以避免缓冲区溢出等问题。

需要考虑源字符串的长度:如果源字符串的长度小于n,则strncpy函数会在目标字符串的末尾添加’\0’以补齐剩余的空间。但如果源字符串的长度大于等于n,则strncpy函数只会复制前n个字符,后面的字符会被忽略。

注意2
memcpy函数并不会在复制的数据结尾处添加任何特殊的结尾标志。所以不建议使用其复制字符串。

为了避免内存越界,需要注意以下几点:

  1. 保证内存空间足够:在使用这些函数时,需要确保目标内存空间足够大,以避免越界访问。
  2. 确保源字符串以’\0’结尾:源字符串必须以’\0’结尾,否则这些函数将会继续读取内存中的数据,可能导致内存越界。
  3. 使用函数的返回值:这些函数都返回了一个指向目标内存地址的指针或者整数值,可以用来检查是否发生了内存越界。
  4. 使用安全函数:C11标准中提供了一些安全函数,如memcpy_s(), strcpy_s(), strcmp_s()等,它们在传递参数时需要指定缓冲区的大小,可以防止内存越界。

2.5.1 安全函数memcpy_s(), strcpy_s(), strcmp_s()的使用

安全函数要求在传递参数时需要指定缓冲区的大小,以防止缓冲区溢出等问题。以下是几个常见的安全函数的使用示例:

  1. memcpy_s()函数:

memcpy_s()函数用于复制一段内存区域的数据,并且在参数传递时需要指定复制的字节数。它的函数原型为:errno_t memcpy_s(void* dest, size_t destSize, const void* src, size_t count)。其中,dest表示目标内存地址,destSize表示目标内存地址的大小,src表示源内存地址,count表示需要复制的字节数。

使用示例:

char str1[20] = "Hello World!";
char str2[20];
errno_t err = memcpy_s(str2, sizeof(str2), str1, sizeof(str1));
if (err == 0) {
    printf("复制成功: %s\n", str2);
} else {
    printf("复制失败\n");
}
  1. strcpy_s()函数:

strcpy_s()函数用于将一个字符串复制到另一个字符串中,并且在参数传递时需要指定目标字符串的大小。它的函数原型为:errno_t strcpy_s(char* dest, size_t destSize, const char* src)。其中,dest表示目标字符串地址,destSize表示目标字符串的大小,src表示源字符串地址。

使用示例:

char str1[] = "Hello World!";
char str2[20];
errno_t err = strcpy_s(str2, sizeof(str2), str1);
if (err == 0) {
    printf("复制成功: %s\n", str2);
} else {
    printf("复制失败\n");
}
  1. strcmp_s()函数:

strcmp_s()函数用于比较两个字符串是否相同,并且在参数传递时需要指定比较的字节数。它的函数原型为:errno_t strcmp_s(const char* str1, rsize_t str1max, const char* str2, int* result)。其中,str1和str2分别表示要比较的两个字符串,str1max表示比较的最大字节数,result表示比较的结果。

使用示例:

char str1[] = "Hello World!";
char str2[] = "Hello";
int result;
errno_t err = strcmp_s(str1, sizeof(str1), str2, &result);
if (err == 0) {
    if (result == 0) {
        printf("两个字符串相同\n");
    } else {
        printf("两个字符串不同\n");
    }
} else {
    printf("比较失败\n");
}

三、指针和数组

3.1 指针和数组的区别

  1. 数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);
  2. 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。
  3. 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。
  4. 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;
  5. 在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的。

3.2 数组指针和指针数组的区别

数组指针就是指向数组的指针,它表示的是一个指针,这个指针指向的是一个数组,它的重点是指针。例如, int(*pa)[8] 声明了一个指针,该指针指向了一个有8个int型元素的数组。下面给出一个数组指针的示例。

#include 
#include 
void main()
{
	int b[12]={1,2,3,4,5,6,7,8,9,10,11,12};
	int (*p)[4];
	p = b;
	printf("%d\n"**(++p);
}

程序的输出结果为 5。
上例中,p是一个数组指针,它指向一个包含有4个int类型数组的指针,刚开始p被初始化为指向数组b的首地址,++p相当于把p所指向的地址向后移动4个int所占用的空间,此时p指向数组{5,6,7,8},语句 *(++p); 表示的是这个数组中第一个元素的地址(可以理解p为指向二维数组的指针,{1,2,3,4},{5,6,7,8},{9,10,11,12}。p指向的就是{1,2,3,4}的地址, *p 就是指向元素{1,2,3,4}, ** p指向的就是1,语句 **(++p)会输出这个数组的第一个元素5。

指针数组表示的是一个数组,而数组中的元素是指针。下面给出另外一个指针数组的示例:

#include 
int main()
{
	int i;
	int *p[4];
	int a[4]={1,2,3,4};
	p[0] = &a[0];
	p[1] = &a[1];
	p[2] = &a[2];
	p[3] = &a[3];
	for(i=0;i<4;i++)
		printf("%d",*p[i]);
	printf("\n"); 
	return 0;
}

程序的输出结果为1234

3.3 函数指针和指针函数的区别

3.3.1 函数指针

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

int(*p)(int, int);

这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即 (*p) ;其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为int(*)(int,int)
我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(指针变量名)”。但是这里需要注意的是:“(指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
最后需要注意的是,指向函数的指针变量没有 ++ 和 - - 运算。

# include 
int Max(int, int);  //函数声明
int main(void)
{
    int(*p)(int, int);  //定义一个函数指针
    int a, b, c;
    p = Max;  //把函数Max赋给指针变量p, 使p指向Max函数
    printf("please enter a and b:");
    scanf("%d%d", &a, &b);
    c = (*p)(a, b);  //通过函数指针调用Max函数
    printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
    return 0;
}
int Max(int x, int y)  //定义Max函数
{
    int z;
    if (x > y)
   {
        z = x;
   }
    else
   {
        z = y;
   }
	return z;
}

3.3.2 指针函数

首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有“函数返回值”,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
类型名 *函数名(函数参数列表);
其中,后缀运算符括号“()”表示这是一个函数,其前缀运算符星号“*”表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个“指向返回值为…的指针(地址),“类型名”表示函数返回的指针指向的类型”。

“(函数参数列表)”中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下:

int *pfun(int, int);

由于“*”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。
即:

int *(pfun(int, int));

接着再和前面的“*”结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。

#include 
float *find(float(*pionter)[4],int n);//函数声明
int main(void)
 {
     static float score[][4]={{60,70,80,90},{56,89,34,45},{34,23,56,45}};
     float *p;
     int i,m;
     printf("Enter the number to be found:");
     scanf("%d",&m);
     printf("the score of NO.%d are:\n",m);
     p=find(score,m-1);
     for(i=0;i<4;i++)
         printf("%5.2f\t",*(p+i));
  
     return 0;
 }
float *find(float(*pionter)[4],int n)/*定义指针函数*/
 {
     float *pt;
     pt=*(pionter+n);
     return(pt);
 }

共有三个学生的成绩,函数find()被定义为指针函数,其形参pointer是指针指向包含4个元素的一维数组的指针变量。pointer+n指向score的第n+1行。*(pointer+1)指向第一行的第0个元素。pt是一个指针变量,它指向浮点型变量。main()函数中调用find()函数,将score数组的首地址传给pointer。

3.4 数组名和指针的区别与联系

  1. 数据保存方面
    指针保存的是地址(保存目标数据地址,自身地址由编译器分配),内存访问偏移量为4个字节,无论其中保存的是何种数据均已地址类型进行解析。
    数组保存的数据。数组名表示的是第一个元素的地址,内存偏移量是保存数据类型的内存偏移量;只有对数组名取地址(&数组名)时数组名才表示整个数组,内存偏移量是整个数组的大小(sizeof(数组名))。
  2. 数据访问方面
    指针对数据的访问方式是间接访问,需要用到解引用符号(*数组名)。
    数组对数据的访问则是直接访问,可通过下标访问或数组名+元素偏移量的方式
  3. 使用环境
    指针多用于动态数据结构(如链表,等等)和动态内存开辟。
    数组多用于存储固定个数且类型统一的数据结构(如线性表等等)和隐式分配。

3.5 指针和引用

3.5.1 异同点

相同

  1. 都是地址的概念,指针指向某一内存、它的内容是所指内存的地址;引用则是某块内存的别名。
  2. 从内存分配上看:两者都占内存,程序为指针会分配内存,一般是4个字节;而引用的本质是指针
    区别
  3. 指针是实体,而引用是别名。
  4. 指针和引用的自增(++)运算符意义不同,指针是对内存地址自增,而引用是对值的自增。
  5. 引用使用时无需解引用(*),指针需要解引用;
  6. 引用只能在定义时被初始化一次,之后不可变;指针可变。
  7. 引用不能为空,指针可以为空。
  8. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32
    位系统指针变量一般占用4字节内存。
#include "stdio.h"
int main(){
	 int x = 5;
	 int *p = &x;
	 int &q = x;
	 
	 printf("%d %d\n",*p,sizeof(p)); 
	 printf("%d %d\n",q,sizeof(q));
}

//结果
5  8
5  4

由结果可知,引用使用时无需解引用(*),指针需要解引用;用的是64位操作系统,“sizeof 指针”得到的是指针本身的大小,即8个字节。而“sizeof 引用”得到的是的对象本身的大小及int的大小,4个字节。

3.5.2 转换

  1. 指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。
  2. 引用转指针:把引用类型的对象用&取地址就获得指针了。
int a = 5;
int *p = &a;
void fun(int &x){}//此时调用fun可使用 : fun(*p);
//p是指针,加个*号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,
//p指针指向的对象会转换成引用X。

3.6 野指针

3.6.1 定义

  1. 野指针是指向不可用内存的指针,当指针被创建时,指针不可能自动指向NULL,这时,默认值是随机的,此时的指针成为野指针。
  2. 当指针被free或delete释放掉时,如果没有把指针设置为NULL,则会产生野指针,因为释放掉的仅仅是指针指向的内存,并没有把指针本身释放掉。
  3. 第三个造成野指针的原因是指针操作超越了变量的作用范围。

3.6.2 如何避免野指针

  1. 对指针进行初始化。
//将指针初始化为NULL。
char *   p  = NULL;
//用malloc分配内存
char * p = (char * )malloc(sizeof(char));
//用已有合法的可访问的内存地址对指针初始化
char num[ 30] = {0};
char *p = num;
  1. 指针用完后释放内存,将指针赋NULL。
delete(p);
p = NULL;

注:malloc函数分配完内存后需注意:
a. 检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL。可以通过if语句来判断)
b. 清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)

//s是需要置零的空间的起始地址; n是需要置零的数据字节个数。
void bzero(void *s, int n);
// 如果要清空空间的首地址为p,value为值,size为字节数。
void memset(void *start, int value, int size);

为了避免野指针的出现,我们可以采取以下几个方法:

  1. 使用智能指针:智能指针是 C++11 引入的一种 RAII 技术,可以自动管理内存资源,避免手动管理内存带来的风险。C++ STL 中提供了三种智能指针:unique_ptrshared_ptrweak_ptr。其中,unique_ptr 用于管理独占的资源,shared_ptr 用于管理共享的资源,weak_ptr 用于解决循环引用问题。

  2. 使用容器类:C++ STL 中提供了各种容器类,例如 vectorlistmap 等,可以避免手动管理内存带来的风险。容器类可以自动管理内存,提供了安全的访问和操作接口。

  3. 避免手动管理内存:手动管理内存容易出现各种错误,例如忘记释放、重复释放、野指针等。我们可以使用 C++ STL 中的容器类、智能指针等 RAII 技术来管理内存,从而避免手动管理内存带来的风险。

  4. 使用工具检测野指针:可以使用一些工具来检测野指针的存在,例如内存泄漏检测工具、静态代码分析工具等。

3.7 数组和链表

3.7.1数组和链表的优点

数组的优点:

  1. 随机访问:由于数组中的元素在内存中是连续存储的,因此可以通过下标直接访问任何一个元素,这使得随机访问非常快速。
  2. 缓存友好:由于数组中的元素在内存中是连续存储的,因此当访问一个元素时,与它相邻的元素通常也会被缓存,这可以提高访问效率。
  3. 大小固定:数组在创建时需要指定大小,一旦创建,其大小就固定不变。这使得数组在内存管理方面更加简单,也不会发生内存分配和释放的开销。

链表的优点:

  1. 动态大小:链表的大小可以动态增长或缩小,这使得链表在需要动态管理数据大小的情况下非常有用。
  2. 插入和删除元素快速:由于链表中的元素在内存中是不连续存储的,因此插入和删除元素非常快速,只需要修改指针即可。
  3. 空间利用率高:由于链表中的元素可以灵活分配内存,因此可以使用较少的内存来存储相同数量的元素。

四、预处理

4.1 typedef和 define

4.1.1 二者区别

4.1.1.1 原理不同
  • #define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检査,不管含义是否正确照样代入,只有在编译已被展开的源程序时,才会发现可能的错误并报错。
  • 例如, # define Pl3.1415926 ,当程序执行 area=Pr*r 语句时,PI会被替换为3.1415926。于是该语句被替area=3.1415926*r*r 。如果把# define语句中的数字9写成了g,预处理也照样代入,而不去检查其是否合理、合法。
  • typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef。例如, typedef int INTEGER ,这以后就可用 INTEGER来代替int作整型变量的类型说明了,例如: INTEGER a,b;
  • 用 typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如: typedef int a[10]; 表示a是整型数组类型,数组长度为10。然后就可用a说明变量,例如:语句a s1,s2;完全等效于语句 int s1[10],s2[10].同理, typedef void(*p)(void)表示p是一种指向void型的指针类型。
4.1.1.2 功能不同
  • typedef用来定义类型的别名,这些类型不仅包含内部类型(int、char等),还包括自定义类型(如struct),可以起到使类型易于记忆的功能。
  • 例如: typedef int (*PF)(const char *, const char*)
    定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为 const char*。typedef还有另外一个重要的用途,那就是定义机器无关的类型。例如,可以定义一个叫REAL的浮点类型,在目标机器上它可以获得最高的精度: typedef long double REAL ,在不支持 long double的机器上,该 typedef看起来会是下面这样: typedef double real ,在 double都不支持的机器上,该 typedef看起来会是这样: typedef float REAL 。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
4.1.1.3 作用域不同

#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。

void fun()
{
	#define A int
}
void gun()
{
	//这里也可以使用A,因为宏替换没有作用域,但如果上面用的是 typedef,那这里就不能用
	//A,不过,一般不在函数内使用 typedef
}
4.1.1.4 对指针的操作不同

两者修饰指针类型时,作用不同。

#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1  pl, p2;
INTPTR2 p3, p4;

INTPTR1 pl, p2和INTPTR2 p3, p4的效果截然不同。 INTPTR1 pl, p2进行字符串替换后变成 int*p1,p2 ,要表达的意义是声明一个指针变量p1和一个整型变量p2.而 INTPTR2 p3, p4,由于INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于int*pl,*p2 .从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用 typedef为一种数据类型起的别名是带有一定含义的。

程序示例如下

#define INTPTR1 int*
typedef int* INTPTR2  
int a=1;
int b=2;
int c=3;
const INTPTR1  p1=&a; 
const INTPTR2 p2=&b;
INTPTR2 const p3=&c;

上述代码中, const INTPTR1 p1表示p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是p1可以指向其他内容。而对于 const INTPTR2 p2,由于 INTPTR2表示的是个指针类型,因此用 const去限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他内容,但可以通过p2修改其当前指向的内容。 INTPTR2 const p3同样声明的是一个指针常量。

4.2 # include< filename. h>和# include" filename. h"有什么区别?

对于 include< filename. h>,编译器先从标准库路径开始搜索 filename.h,使得系统文件调用较快。
而对于# include“ filename.h"”,编译器先从用户的工作路径开始搜索 filename.h,然后去寻找系统路径,使得自定义文件较快。

4.3 不使用流程控制语句,如何打印出1~1000的整数?

//宏定义多层嵌套(10 * 10 * 10),printf多次输出。
#include 
#define B P,P,P,P,P,P,P,P,P,P
#define P L,L,L,L,L,L,L,L,L,L
#define L I,I,I,I,I,I,I,I,I,I,N
#define I printf("%3d",i++
#define N printf("n"
int main()
{
	int i = 1;
	B;
	return 0;
}
//简便写法,同样使用多层嵌套
#include
#define A(x)x;x;x;x;x;x;x;x;x;
int main ()
{
 int n=1;
 A(A(A(printf("%d", n++);
 return 0;
}

4.4 从代码编译到可执行文件的流程?

一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接

五、函数

5.1 attribute的用法

5.1.1 请写个函数在main函数执行前先运行

attribute可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
gnu对于函数属性主要设置的关键字如下:

alias:     设置函数别名。
aligned:   设置函数对齐方式。
always_inline/gnu_inline: 
	函数是否是内联函数。
constructor/destructor:
	主函数执行之前、之后执行的函数。
format:
	指定变参函数的格式输入字符串所在函数位置以及对应格式输出的位置。
noreturn:
	指定这个函数没有返回值。
	请注意,这里的没有返回值,并不是返回值是void。而是像_exit/exit/abord那样执行完函数之后进程就结束的函数。
weak:指定函数属性为弱属性,而不是全局属性,一旦全局函数名称和指定的函数名称命名有冲突,使用全局函数名称。

示例代码如下:

#include 
void before() __attribute__((constructor));
void after() __attribute__((destructor));
void before() {
    printf("this is function %s\n",__func__);
    return;
}
void after(){
    printf("this is function %s\n",__func__);
    return;
}
int main(){
    printf("this is function %s\n",__func__);
    return 0;
}
// 输出结果
// this is function before
// this is function main
// this is function after

5.2 C语言是怎么进行函数调用的?

大多数CPU上的程序实现使用栈来支持函数调用操作,栈被用来传递函数参数、存储返回信息、临时保存寄存器原有的值以备恢复以及用来存储局部变量。
函数调用操作所使用的栈部分叫做栈帧结构,每个函数调用都有属于自己的栈帧结构,栈帧结构由两个指针指定,帧指针(指向起始),栈指针(指向栈顶),函数对大多数数据的访问都是基于帧指针。下面是结构图:
【嵌入式笔/面试】嵌入式软件基础题和真题总结——C/C++_第1张图片
栈指针和帧指针一般都有专门的寄存器,通常使用ebp寄存器作为帧指针,使用esp寄存器做栈指针。
帧指针指向栈帧结构的头,存放着上一个栈帧的头部地址,栈指针指向栈顶。

5.3 inline函数

在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。为了解决这个问题,特别的引入了inline修饰符,表示为内联函数
大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline放在函数定义(注意是定义而非声明)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率
内联函数的主要特点包括:

  1. 内联函数的定义通常放在头文件中,以便在多个源文件中使用。
  2. 内联函数的函数体通常比较简单,以便在函数调用处直接展开。
  3. 内联函数的展开是由编译器在编译时完成的,而不是在运行时完成的。
  4. 内联函数的展开可能会导致代码膨胀,增加可执行文件的大小。
  5. 内联函数的展开可以减少函数调用的开销,提高程序的运行速度。
    内联函数的使用方式如下:
inline int add(int a, int b) {
    return a + b;
}

在上面的代码中,add 函数被定义为内联函数,这意味着在函数调用处,编译器会将函数体直接展开,而不是通过函数调用的方式执行。这样可以避免函数调用的开销,提高程序的运行速度。

5.4 析构函数和虚函数

5.4.1 为什么析构函数必须是虚函数?

析构函数必须是虚函数的原因是为了正确地销毁对象和避免内存泄漏。
当一个对象被销毁时,会先调用它的析构函数,然后再释放它所占用的内存。如果一个类有继承关系,而它的析构函数不是虚函数,当使用基类指针指向派生类对象并销毁这个对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类对象中的资源无法正确释放,从而引发内存泄漏。
例如,假设有一个基类 Animal 和它的一个派生类 Cat,并且它们都有一个需要释放的资源 m_pData,代码如下所示:

class Animal {
public:
    Animal() { m_pData = new int(0); }
    ~Animal() { delete m_pData; }
private:
    int* m_pData;
};

class Cat : public Animal {
public:
    Cat() { m_pData = new int(1); }
    ~Cat() { delete m_pData; }
private:
    int* m_pData;
};

如果将析构函数改为非虚函数,那么当使用基类指针指向派生类对象并销毁这个对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类对象中的 m_pData 资源无法正确释放,从而引发内存泄漏。

5.4.2 为什么C++默认的析构函数不是虚函数?

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

5.4.3 C++中析构函数的作用?

如果构造函数打开了一个文件,最后不需要使用时文件就要被关闭。析构函数允许类自动完成类似清理工作,不必调用其他成员函数。
析构函数也是特殊的类成员函数。简单来说,析构函数与构造函数的作用正好相反,它用来完成对象被删除前的一些清理工作,也就是专门的扫尾工作。

5.4.4 静态函数和虚函数的区别?

静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

5.4.5 虚函数表具体是怎样实现运行时多态的?

原理:
虚函数表是一个类的虚函数的地址表,每个对象在创建时,都会有一个指针指向该类虚函数表,每一个类的虚函数表,按照函数声明的顺序,会将函数地址存在虚函数表中,当子类对象重写父类的虚函数的时候,父类的虚函数表中对应的位置会被子类的虚函数地址覆盖。
作用:
在用父类的指针调用子类对象成员函数时,虚函数表会指明要调用的具体函数是哪个。

5.5 重载和覆盖

重载(Overload)和覆盖(Override)是两个不同的概念,它们的区别主要体现在以下几个方面:

  1. 定义位置不同。重载是在同一个作用域内,定义了多个同名函数,它们的参数列表不同;而覆盖是在派生类中,重新定义了基类中已有的同名虚函数。
  2. 参数列表不同。重载函数的参数列表必须不同,可以是参数个数、类型或顺序的不同;而覆盖函数的参数列表必须与被覆盖函数完全相同。
  3. 静态绑定和动态绑定。重载函数是静态绑定,即在编译时根据函数名和参数列表确定调用哪个函数;而覆盖函数是动态绑定,即在运行时根据对象的实际类型确定调用哪个函数。
  4. 返回值类型不同。重载函数的返回值类型可以不同;而覆盖函数的返回值类型必须相同或是其派生类型。
  5. const 成员函数不同。重载函数可以将成员函数声明为 const;而覆盖函数必须与基类中的虚函数的 const 属性相同,即如果基类中的虚函数是 const 成员函数,则派生类中的覆盖函数也必须是 const 成员函数。

六、 位操作

6.1 如何求解整型数的二进制表示中1的个数?

程序代码如下:

#include 
int func(int x)
{
	int countx = 0;
	while(x)
	{
		countx++;
		x = x&(x-1);
	}
	return countx;
}
int main()
{
	printf("%d\n",func(9999));
	return 0;
}

程序输出的结果为8。
在上例中,函数func()的功能是将x转化为二进制数,然后计算该二进制数中含有的1的个数。首先以9为例来分析,9的二进制表示为1001,8的二进制表示为1000,两者执行&操作之后结果为1000,此时1000再与0111(7的二进制位)执行&操作之后结果为0。

为了理解这个算法的核心,需要理解以下两个操作:
1)当一个数被减1时,它最右边的那个值为1的bit将变为0,同时其右边的所有的bit都会变成1。
2)每次执行x&(x-1)的作用是把ⅹ对应的二进制数中的最后一位1去掉。因此,循环执行这个操作直到ⅹ等于0的时候,循环的次数就是x对应的二进制数中1的个数。

6.2 如何求解二进制中0的个数?

以25为例:
【嵌入式笔/面试】嵌入式软件基础题和真题总结——C/C++_第2张图片

int CountZeroBit(int num)
{
	int count = 0;
	while (num + 1)
	{
		count++;
		num |= (num + 1); //算法转换
	}
	return count;
}
int main()
{
	int value = 25;
	int ret = CountZeroBit(value);
	printf("%d的二进制位中0的个数为%d\n",value, ret);
	system("pause");
	return 0;
}

6.3 交换两个变量的值,不使用第三个变量?

有两种解法, 一种用算术算法, 一种用^(异或)。

方法一:
a = a + b;
b = a - b;
a = a - b;

方法二:
a = a^b;// 只能对int,char..
b = a^b;
a = a^b;
or
a ^= b ^= a;

给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在
以上两个操作中,要保持其它位不变。

#define BIT3 (0x1<<3) 
static int a; 
void set_bit3(void) 
{ 
	a |= BIT3; 
} 
void clear_bit3(void) 
{ 
	a &= ~BIT3; 
}

七、待补充

你可能感兴趣的:(嵌入式开发面试笔试总结笔记,面试,嵌入式,C/C++)