1、 各个类型在X位系统中占字节数
为考虑效率,在32位平台中(最大内存空间是2^32),内存是以4字节对齐(不够4字节填充为4字节)
在64位平台中(最大内存空间是2的64次方),内存是以8字节对齐(不够8字节填充为8字节)
16位系统 | 32位系统 | 64微系统 | |
---|---|---|---|
char | 1 | 1 | 1 |
指针变量 | 2 | 4 | 8 |
short int | 2 | 2 | 2 |
int | 4 | 4 | 4 |
unsigned int | 4 | 4 | |
float | 4 | 4 | |
double | 8 | 8 | |
long | 4 | 4 | 8 |
long long | 8 | 8 | |
unsigned long | 4 | 8 | |
word | 2 | 4 | 8 |
2、sizeof与strlen的区别
1)sizeof是什么?
- sizeof是关键字,这一点毋庸置疑。你不能将sizeof定义为任何标识符。
- sizeof是运算符(操作符),而且是唯一一个以单词形式出现的运算符,它用来计算存放某一个量需要占用多少字节,它的结合性是从右到左。
- sizeof不是函数,产生这样的疑问主要是因为有时候sizeof的外在表现确实有点类似函数,比如:i = sizeof(int);这样的式子,就很容易让人误以为sizeof是一个函数呢。但如果sizeof是函数,那么sizeof i;(假设i为int变量)这种式子一定不能成立,因为函数的参数不可能不用括号括起来。事实上这个sizeof i;可以正常运行,这就说明sizeof绝对不是函数。
2) strlen是什么?
strlen=string length,顾名思义,该函数是用来求解字符串的长度的。
我们都知道在编译器中输入printf(”Hello World!”),就会输出”Hello World!”,这就是一个字符串,类似这种由双引号引起来的一串字符称为字符串面值,或者简称字符串。接着我们就需要了解一下“\0”这个转义字符了,记住任何一个字符串的结尾都会隐藏一个“\0”,他是字符串的结束标志,所以我们在用strlen函数读取字符串的时候,当我们遇到“\0”时我们就要停止读取,此时“\0”前字符的个数就是字符串的长度,注意:这里的“\0”只是结束标志,仅仅告诉我们strlen函数读取到这里就要停止了,“\0”不算做一个字符!
例子1:
例子2:
众所周知,C语言中没有字符串对应的类型,它不像整型用int存储,浮点型用float或者double来存储。
arr1数组只是单纯把字符串“abcdef”的每一个字符用数组存储起来,而arr2数组则是多存储了一个“\0",究竟有什么不同呢?我们一起来看运行及如果。
我们可以看到arr1数组的长度为38,arr2数组的长度为6,为什么呢?同样我们需要探讨两个数组在内存中的存储状况。
arr2数组的存储情况和示例1字符串的存储情况相同,而arr1却不同,所以arr2的长度为6我们就不再赘述,主要研究一下arr1的长度为什么是38。我们都知道字符串的结束标志为”\0”,对于arr1数组来说,没有在数组中额外存储”\0”,所以编译器在读取时,并不会像我们所期望的那样停止读取,故长度当然不会时6,至于为什么是38,是因为在读取时,编译器读取完arr1时会继续往后读取,直到读取到”\0”,arr1在读取完第38个字符后才会遇到”\0”;由于每个人的电脑和编译器不同,读取的长度也不一样,所以arr1这种情况一般我们认为它读取的结果为随机值!
3) Strlen与Sizeof比较
所以我在这里只介绍一下用sizeof来计算一般情况,同样拿”abcdef“来举例吧。
例子:
此时的结果是7,比strlen的结果多1,那是因为sizeof计算的是所占字节大小,所以字符串后面隐藏的”\0"也要计算进去,我们可以调试监视一下arr这个数组。
我们可以清楚的看见arr[6] = ‘\0’,故sizeof(arr)= 7。
3、 结构体和联合体的区别
- 结构体和联合体都是由不同的数据类型组成,但在任何时刻,联合体只存在一个被选中的成员,结构体所有成员都存在。
- 在结构体中,各成员占有自己的储存空间,总大小等于各成员的大小之和。
- 在联合体中,所有成员公用一块储存空间,其大小等于联合体中最大成员的大小。
typedef union
{
uint8 Byte;
struct
{
uint8 BIT0 :1; // Bit 0
uint8 BIT1 :1; // Bit 1
uint8 BIT2 :1; // Bit 2
uint8 BIT3 :1; // Bit 3
uint8 BIT4 :1; // Bit 4
uint8 BIT5 :1; // Bit 5
uint8 BIT6 :1; // Bit 6
uint8 BIT7 :1; // Bit 7
}Bits;
}BitType;
typedef union
{
signed int Word;
struct
{
unsigned int ByteH:8;
unsigned int ByteL:8;
} WordHL;
} MyWord;
4、 指针和数组的区别
数组要么在静态存储区,要么在栈上被创建。数组名对应着一块内存,其容量与地址在生命周期内保持不变。
指针可以随时指向任意类型的内存块,他的特征是可变。比数组灵活,但也危险。
1) 概念
数组:数组就是存储多个相同类型数据的集合
指针:指针相当于一个变量,但是它和变量不同,它存放的是变量在内存空间中的位置
2) 赋值、存储方式、求sizeof、初始化
-
赋值:同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或copy
-
存储方式:
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组下标进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的
数组存储位置:数组的存储空间,不是在静态区就是在栈上。
指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
指针存储方式:指针的存储空间不能确定,由于指针本身就是一个变量,再加上它所存放的也是变量,所以确定不了。
3) 所占内存大小不同
**数组:**数组所占存储空间的内存:sizeof(数组名)、数组的大小:sizeof(数组名)/ sizeof(数据类型)
指针:在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
5、指针函数与函数指针的区别
指针函数是一个函数,它的返回值是一个指针。
函数指针是一个指针,这个指针指向的对象是一个数组。
待补充…
6、 #include <stdio.h>与#include "stdio.h"的区别
-
<stdio.h>表示的是该文件存在编译器指定的标准头文件存放处。
-
"stdio.h"表示的是该文件在用户当前的工作目录下
7、头文件中的ifdef/define endif的作用是什么?
防止头文件被重复引用。
8、用#define实现宏并求最大值与最小值
#define MAX(A,B) ((A)>(B)?(A):(B))
#define MIN(A,B) ((A)<(B)?(A):(B))
9、break语句与continue语句的区别
- continue语句只能出现在循环语句内,表示结束本次循环(跳过当前循环中的代码,强迫开始下一次循环),对于 for 循环,continue 语句执行后自增语句仍然会执行。对于 while 和 do…while 循环,continue 语句会重新执行条件判断语句。
- break语句还能出现在switch语句中,表示结束switch语句,出现在循环语句中,表示结束整个循环。
1) continue
for (1; 2; 3)
{
A;
B;
continue; //如果执行该语句的话,执行完之后,会执行语句3,C和D都会被跳过去,C和D不会被执行
C;
D;
}
while(表达式)
{
A;
B;
continue; //如果执行该语句的话,执行完之后,会执行表达式,C和D都会被跳过去,C和D不会被执行
C;
D;
}
#include <stdio.h>
#include <string.h>
int main()
{
int i;
char ch;
scanf_s("%d", &i);
printf("i = %d\n", i);
while ((ch = getchar()) != '\n')
{
continue;// 如果接收到的字符一直不是换行符的话,就会一直循环while,直到输入换行符
}
int j;
scanf_s("%d", &j);
printf("i = %d\n", j);
return 0;
}
例:continue
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
int main()
{
int a = 10;
do
{
if (15 == a)
{
// 跳过迭代
a = a + 1;
continue;
}
printf("a 的值: %d\n", a);
a++;
} while (a < 20);
return 0;
}
2) break
例:break
static RoomTemp_em Calc_RoomTempRange(uint16 u16_roomTemp, bool b_errState)
{
unsigned char u8_loop = 0;
static RoomTemp_em em_roomRange = RT_BELOW32;
if (b_errState != 0)
{
em_roomRange = RT_BELOW32;
return em_roomRange;
}
if (u16_roomTemp > U16_50P0_DEGREE)
{
u16_roomTemp = U16_50P0_DEGREE;
}
for (u8_loop = 0; u8_loop < RT_MAXSIZE; u8_loop++)
{
if (u16_roomTemp < ARY_RoomRange[u8_loop].u16_LowerLimit)
{
em_roomRange = ARY_RoomRange[u8_loop].em_RoomRange;
break;
}
else if (u16_roomTemp < ARY_RoomRange[u8_loop].u16_UpperLimit)
{
if (em_roomRange <= ARY_RoomRange[u8_loop].em_RoomRange)
{
em_roomRange = ARY_RoomRange[u8_loop].em_RoomRange;
}
else
{
em_roomRange = ARY_RoomRange[u8_loop + 1].em_RoomRange;
}
break;
}
}
return em_roomRange;
}
10、static与const的区别
1) static关键字
- static用于全局变量:表示该变量是静态全局变量
- 作用域为当前文件用于函数:该函数为静态函数,只能在本文件中调用;静态函数在内存中只有一份,普通函数在内存中只有一份拷贝;
- 作用于局部变量:为静态局部变量,只初始化一次,之后调用函数都是上次函数退出的值。即改变变量的生存周期为整个程序运行时间段内。
- static成员函数:表示这个函数属于此而不属于此类的任何。
- static成员变量:表示该变量属于此类而不属于此类的任何对象,该变量的初始化在此类体外。
2)const关键字
被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
- 修饰一般常量:修饰符可在类型说明符前也可以在类型说明符后;
- 修饰数组:修饰符const可以用在类型说明符前,也可以用在类型说明符后;例如:int const a[5]={1,2,3};或 const int a[5]={1,2,3};
- 修饰常指针:const int *A; //const修饰指针指向的对象,指针可变,指针指向的对象不可变;
- 修饰形参:func(const int a){};该形参在函数里不能改变。
const int a; // a是一个常整型数。
int const a; // a是一个常整型数。
const int *a; // a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。
int * const a; // a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。
int const * a const; // a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。
11、const与宏的区别
- 编译检查: 宏不会编译检查,const有编译检查
- 宏的好处: 宏可以定义函数、方法等,const不可以
- 宏的缺点: 大量使用宏,会导致预编译的时间过长
12、引用与指针的区别
- 非空区别: 指针可以指向NULL,引用必须指向某个对象
- 可修改区别:指针可以指向不同的对象,引用总是指向初始化的对象
- 合法性区别:在使用指针之前要判断是否为NULL,引用不需要判断
13、malloc()与calloc()的区别
- malloc与calloc都是从堆上动态申请内存空间。
- malloc只有一个参数,即要分配的内存大小。
- calloc函数有两个参数,分别是元素的个数与元素的大小。
- malloc不能对内存初始化,calloc堆内存的每一位初始化为零。
14、strcpy、sprintf、memset、memcpy的区别
1)strcpy
1)) 描述
C 库函数 char *strcpy(char *dest, const char *src) 把 src 所指向的字符串复制到 dest。
需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。
2)) 声明
char *strcpy(char *dest, const char *src)
3)) 参数
- dest – 指向用于存储复制内容的目标数组。
- src – 要复制的字符串。
4)) 返回值
该函数返回一个指向最终的目标字符串 dest 的指针。
5)) 例子
#include <stdio.h>
#include <string.h>
int main()
{
char src[40];
char dest[100];
memset(dest, '\0', sizeof(dest));
strcpy_s(src, "This is runoob.com");
strcpy_s(dest, src);
printf("最终的目标字符串: %s\n", dest);
return(0);
}
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Sample string";
char str2[40];
char str3[40];
strcpy_s(str2, str1);
strcpy_s(str3, "copy successful");
printf("str1: %s\nstr2: %s\nstr3: %s\n", str1, str2, str3);
return 0;
}
2)sprintf
1)) 描述
C 库函数 int sprintf(char *str, const char *format, …) 发送格式化输出到 str 所指向的字符串。
2)) 声明
int sprintf(char *str, const char *format, ...)
3)) 参数
- str – 这是指向一个字符数组的指针,该数组存储了 C 字符串。
- format – 这是字符串,包含了要被写入到字符串 str 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier,具体讲解如下:https://www.runoob.com/cprogramming/c-function-sprintf.html
4)) 返回值
如果成功,则返回写入的字符总数,不包括字符串追加在字符串末尾的空字符。如果失败,则返回一个负数。
5)) 例子
#include <stdio.h>
#include <math.h>
int main()
{
char str[80];
sprintf_s(str, "Pi 的值 = %f", M_PI);
puts(str);
return(0);
}
3) memset
1)) 描述
C 库函数 void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。
2)) 声明
void *memset(void *str, int c, size_t n)
3)) 参数
- str – 指向要填充的内存块。
- c – 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
- n – 要被设置为该值的字符数。
4)) 返回值
该值返回一个指向存储区 str 的指针。
5)) 例子
#include <stdio.h>
#include <string.h>
int main()
{
char str[50];
strcpy_s(str, "This is string.h library function");
puts(str);
memset(str, '$', 7);
puts(str);
return(0);
}
4) memcpy
1)) 描述
C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。
2)) 声明
void *memcpy(void *str1, const void *str2, size_t n)
3)) 参数
- str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
- str2 – 指向要复制的数据源,类型强制转换为 void* 指针。
- n – 要被复制的字节数。
4)) 返回值
该函数返回一个指向目标存储区 str1 的指针。
5)) 例子
// 将字符串复制到数组 dest 中
#include <stdio.h>
#include <string.h>
int main ()
{
const char src[50] = "http://www.runoob.com";
char dest[50];
memcpy(dest, src, strlen(src)+1);
printf("dest = %s\n", dest);
return(0);
}
#include<stdio.h>
#include<string.h>
int main(void)
{
char src[] = "***";
char dest[] = "abcdefg";
printf("使用 memcpy 前: %s\n", dest);
memcpy(dest, src, strlen(src));
printf("使用 memcpy 后: %s\n", dest);
return 0;
}
15、指针数组 和 数组指针 的区别
对指针数组和数组指针的概念,相信很多C程序员都会混淆。下面通过两个简单的语句来分析一下二者之间的区别,示例代码如下所示:
int *p1[5];
int (*p2)[5];
首先,对于语句“intp1[5]”,因为“[]”的优先级要比“”要高,所以 p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,如图 1 所示,因此,它是一个指针数组。
其次,对于语句“int(p2)[5]”,“()”的优先级比“[]”高,“”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图 2 所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。
(指针数组:装指针的数组,数组指针:指向数组的指针)
了解指针数组和数组指针二者之间的区别之后,继续来看下面的示例代码:
int arr[5]={1,2,3,4,5};
int (*p1)[5] = &arr;
/*下面是错误的*/
int (*p2)[5] = arr;
不难看出,在上面的示例代码中,&arr 是指整个数组的首地址,而 arr 是指数组首元素的首地址,虽然所表示的意义不同,但二者之间的值却是相同的。那么问题出来了,既然值是相同的,为什么语句“int(p1)[5]=&arr”是正确的,而语句“int(p2)[5]=arr”却在有些编译器下运行时会提示错误信息呢(如在 Microsoft Visual Studio 2010 中提示的错误信息为“a value of type"int"cannot be used to initialize an entity of type"int()[5]"”)?
其实原因很简单,在 C 语言中,赋值符号“=”号两边的数据类型必须是相同的,如果不同,则需要显示或隐式类型转换。在这里,p1 和 p2 都是数组指针,指向的是整个数组。p1 这个定义的“=”号两边的数据类型完全一致,而 p2 这个定义的“=”号两边的数据类型就不一致了(左边的类型是指向整个数组的指针,而右边的数据类型是指向单个字符的指针),因此会提示错误信息。
16、volatile的作用和用法
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量在内存中的值,而不是使用保存在寄存器里的备份(虽然读写寄存器比读写内存快)。
这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。
以下几种情况都会用到volatile:
1、并行设备的硬件寄存器(如:状态寄存器)
2、一个中断服务子程序中会访问到的非自动变量
3、多线程应用中被几个任务共享的变量
17、sizeof(struct)和sizeof(union)内存对齐
内存对齐作用:
1)、平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
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后面补六个字节
联合体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个字节。
18、位域
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字节(只是随便举个例子),那么每个变量对应的值就不对了。
不同框架的处理器对齐方式会有不同,这个时候不指定对齐的话,会产生错误结果。
19、局部变量能否和全局变量重名?
能,局部会屏蔽全局。
局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。
对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。
20、extern作用
C语言中extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义 。 这里面要注意,对于extern申明变量可以多次,但定义只有一次。
21、指针初始化
1)什么是野指针?
就是指针指向的位置是不可知(随机性,初始化,不正确,没有明确限制),指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。
2)野指针产生的原因
1、指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitialized in the function ”。
2、指针释放后之后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
3、指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
3)指针赋值
int *pi;
pi = 0; // √
pi = NULL; // √
pi = 100; // ×
pi = num; // ×
可以测试指针是否被设置成了NULL
if(pi)
{
// 不是NULL;
}
else
{
// 是NULL;
}
22、Const与指针
- 指向常量的指针
// 指向常量的指针
const int limit = 500;
const int * pci;
pci = &limit;
- 指向非常量的常量指针
// 指向非常量的常量指针
int num;
int *const pci;
pci = #
23、malloc()怎么用
int *pi = (int *)malloc(sizeof(int));
*pi = 5;
printf("*pi: %d\n", *pi);
free(pi);
malloc函数的参数指定要分配的字节数。如果成功,它会返回从堆上分配的内存的指针,如果失败,则会返回空指针。
使用sizeof更有利于在不同平台间的移植。
24、内存泄漏
-
什么叫内存泄漏
如果不再使用已分配的内存却没有将其释放就会发生内存泄漏。
-
内存泄漏的危害
无法回收内存并重复利用。
-
什么会导致内存泄漏
1、丢失地址
int *pi = (int *)malloc(sizeof(int));
*pi = 5;
...
pi = (int*)malloc(sizeof(int));
堆上开辟了存数据“5”的内存空间,但是没有指向了
2、隐式内存泄漏
程序应该释放的内存而实际却没有释放,一般是程序员的忽视导致的。
25、动态内存分配的函数
-
malloc
-
realloc
会重新分配内存
-
calloc
会在分配的同时清空内存
-
free
26、什么是迷途指针?
free之后,堆上的内存释放,但是指针还指向该内存,这时指向的是垃圾数据。
为避免出现迷途指针,应该将已释放的指针赋值为NULL。
27、free重复释放会怎样?
堆管理器很难判断一个内存块是否已经被释放,它不会试图去检测是否两次释放了同一块内存。重复释放会导致运行时异常。
28、野指针和迷途指针是一个东西吗?
野指针就是声明而没有初始化的指针,指针变量中存的地址可能是任何垃圾数值,这就叫野指针。
迷途指针是正常的指针指向的对象被释放了,比如malloc分配的堆中的内存,或者栈中函数分配的临时变量,一旦你的指针与这些对象断连,这时这个就叫迷途指针。
29、函数的本质是什么?
函数的本质是一段可以重复使用的代码,这段代码被提前编写好了,放到了指定的文件中,使用时直接调取即可。函数名就是这段代码的地址。
30、函数传递参数和函数传递参数的指针的区别
-
传递参数(包括指针)
此时,传递的是他们的值,也就是说,传递给函数的是参数值的一个副本,当涉及到大型数据结构时,传递参数的指针会更高效。
-
传递参数的指针
比如说一个表示雇员的大型结构体,如果我们把整个结构体传递给函数,那么需要复制结构体所有的字节,这样会导致程序运行变慢,栈帧也会占用过多的内存,传递对象的指针意味着不需要复制对象,但可以通过指针访问对象。
31、形参和实参的区别
-
形参(形式参数)
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
-
实参(实际参数)
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。
-
区别
1、形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
2、实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。
3、实参和形参在数量上、类型上、顺序上必须严格一致。
4、形参和实参虽然可以同名,但它们之间是相互独立的,互不影响,因为实参在函数外部有效,而形参在函数内部有效。
5、函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参。
修改形参的值不会影响实参
例如:传递两个整数来交换两个的值,不能实现交换,但传递指针可以。
32、指针和函数
- int *f4(); // 返回指针的函数
- int (*f5)(); // 返回int类型的函数指针
- int* (*f6)();// 返回指针的函数指针
1、声明函数指针
void (* foo)();
2、使用函数指针
#include<stdio.h>
#include<string.h>
#include<malloc.h>
int (*fptr1)(int);
int square(int num);
int main()
{
int n = 5;
fptr1 = square;
printf("%d square is %d\n", n, fptr1(n));
return 0;
}
int square(int num)
{
return num * num;
}
3、传递函数指针
#include <stdio.h>
int add (int num1,int num2)
{
return num1 + num2;
}
int substract(int num1, int num2)
{
return num1 - num2;
}
typedef int (*fptrOperation)(int, int);
int compute(fptrOperation operation,int num1,int num2)
{
return operation(num1 ,num2);
}
int main()
{
printf("%d\n", evaluate('+',5,6));
printf("%d\n", evaluate('-',5,6));
}
4、返回函数指针
#include <stdio.h>
int add (int num1,int num2)
{
return num1 + num2;
}
int substract(int num1, int num2)
{
return num1 - num2;
}
typedef int (*fptrOperation)(int, int);
// 返回函数指针
fptrOperation slect(char optcode)
{
switch (optcode) {
case '+':
return add;
break;
case '-':
return substract;
break;
default:
break;
}
}
int evaluate(char optcode,int num1, int num2)
{
fptrOperation operation = slect(optcode);
return operation(num1,num2);
}
int main()
{
printf("%d\n", evaluate('+',5,6));
printf("%d\n", evaluate('-',5,6));
}
5、使用函数指针数组
#include <stdio.h>
int add (int num1,int num2)
{
return num1 + num2;
}
int substract(int num1, int num2)
{
return num1 - num2;
}
typedef int (*fptrOperation)(int, int);
fptrOperation operations[125] = {NULL};
void initOperationsArray(){
operations['-'] = substract;
operations['+'] = add;
}
int evaluate(char optcode,int num1, int num2)
{
fptrOperation operation;
operation = operations[optcode];
return operation(num1,num2);
}
int main()
{
initOperationsArray();
printf("%d\n", evaluate('+',5,6));
printf("%d\n", evaluate('-',5,6));
}
33、
34、整个数组的地址和数组首元素的地址有何不同?
以数组 int arr[10] = { 0 }; 为例
数组首元素的地址:arr 或者 &arr[0]
整个数组的地址:&arr
他们的地址是相同的,但由于他们是不同的类型,所以当他们加减整数时得到的结果不同。
35、二维数组的行数和列数怎么求得?
36、
37、
38、
39、
40、
二、单片机相关
1、通信基础
1)传输方向分类
从传输方向上分单工通信、半双工通信、全双工通信三类。
- 单工通信就是指只允许一方向另外一方传送信息,而另一方不能回传信息。比如电视遥控器、收音机广播等,都是单工通信技术。
- 半双工通信是指数据可以在双方之间相互传播,但是同一时刻只能其中一方发给另外一方,比如我们的对讲机就是典型的半双工。
- 全双工通信就发送数据的同时也能够接收数据,两者同步进行,就如同我们的电话一样,我们说话的同时也可以听到对方的声音。
2)传输方式分类
串行通信:
定义:串行通信是指利用一条传输线将数据一位位地顺序传送。
传输方式:传输一个字节(8个位)的数据时,串口是将8个位排好队,逐个地在1条 连接线上传输。
特点:通信线路简单,利用电话或电报线就可以实现通信,降低成本,适用于远距离通信,但传输速度慢。
并行通信:
定义:并行通信是指利用多条传输线将一个数据的各位同时传送。
传输方式:传输一个字节(8个位)的数据时,并口是将8个位一字排开,分别在8条 连接线上同时传输。
特点:传输速度快,适用于短距离通信。
3)数据同步方式分类
可以根据通讯过程中是否有使用到时钟信号进行简单的区分。
异步通讯:
在异步通讯中,不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧的格式传输数据。例如规定由起始位、数据位、奇偶校验位、停止位等。某些通讯中还需要双方约定数据的传输速率,以便更好地同步 。波特率(bps)是衡量数据传送速率的指标。
同步通讯:
在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调,同步数据。通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。
2、常用串行通信接口
1)UART
2)单总线
3)SPI
4)I2C
3、RS232、RS485的区别
概念:
RS-232和RS-485 均属于UART是通用异步收发传输器,仅用两根信号线(Rx 和Tx)就可以完成通信过程;而由于各自使用的电平有所不同,因此由UART转换为RS-232、RS-485时,需要经过一个转换芯片SP3232E、SP3485。
RS-232 一般只使用RXD、TXD、GND三条线;首先涉及到了电平的变化,UART使用的芯片自身输出的电压;然后由UART的两条信号线 TX和RX转换为RX-232的 TX和RX;传输速率较低,在异步传输时,波特率为20Kbps。
接口使用一根信号线和一根信号返回线而构成共地的传输形式,这种共地传输容易产生共模干扰,所以抗噪声干扰性弱。
RS-232接口可以实现点对点的通信方式,但这种方式不能实现联网功能。
于是,为了解决这个问题,一个新的标准RS-485产生了。
RS-485的数据信号采用差分传输方式,它使用一对双绞线;RS-485 只有2 根信号线,所以只能工作在半双工模式,常用于总线网。
另外补充:
RS-422 的电气性能与RS-485完全一样。主要的区别在于:RS-422 有4 根信号线:两根发送、两根接收。由于RS-422 的收与发是分开的所以可以同时收和发(全双工),也正因为全双工要求收发要有单独的信道,所以RS-422适用于两个站之间通信,星型网、环网,不可用于总线网;
区别:
-
传输方式不同:RS232采用不平衡传输方式,即所谓单端通信。RS485采用平衡传输,即差分传输方式。
-
传输距离不同:RS232适合本地设备之间通信,传输距离一般不超过20m。RS484传输距离为几十米到上千米。
-
设备数量:RS232只允许一对一通信,而RS485接口在总线上是允许多达128个收发器。
-
连接方式:RS232规定用电平表示数据,因此线路就是单线路的,用两根线才能达到全双工的目的;RS485,使用差分电平表示数据,因此,必须用两根线才能达到传输数据的基本要求,要实现全双工,必须用4根线。
总结:线路上存在的仅仅是电流,RS232/ RS485规定了这些电流在什么样的线路上流动和流动的样式。
4、单片机中断流程
1)中断概念:
CPU在正常执行程序的过程中,由于内部/外部事件的触发或程序的预先安排引起CPU暂时中断当前正在运行的程序,而转去执行中断服务子程序,待中断服务子程序执行完毕后,CPU继续执行原来的程序,这一过程称为中断;
2)中断处理过程
第一步:保护现场,将当前位置的PC地址压栈;
第二步:跳转到中断服务程序,执行中断服务程序;
第三步:恢复现场,将栈顶的值回送给PC;
第四步:跳转到被中断的位置开始执行下一个指令
3)中断服务函数
相对于正常子函数,中断服务函数有以下需要注意的地方:
-
中断服务函数不能传入参数;
-
中断服务函数不能有返回值;
-
中断服务函数应该做到短小精悍;
-
不要在中断函数中使用printf函数,会带来重入和性能问题。
(自己思考:项目中采用定时器中断产生毫秒时钟,为10ms、20ms、100ms、500ms时间片任务提供节拍)
5、单片机外设
单片机的外设可分为两类:1.片内外设; 2.片外外设
- 单片机内部的外设一般包括:串口控制模块,SPI模块,I2C模块,A/D模块,PWM模块,CAN模块,EEPROM,比较器模块,等等,它们都集成在单片机内部,有相对应的内部控制寄存器,可通过单片机指令直接控制。
- 外设指的是单片机外部的外围功能模块,比如键盘控制芯片,液晶,A/D转换芯片,等等。外设可通过单片机的I/O,SPI,I2C等总线控制。
6、PWM
1)概念
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。
(一句话:把数字信号转变为模拟电压信号)
2)PWM原理
以单片机为例,我们知道,单片机的IO口输出的是数字信号,IO口只能输出高电平和低电平,假设高电平为5V 低电平则为0V 那么我们要输出不同的模拟电压,就要用到PWM,通过改变IO口输出的方波的占空比从而获得使用数字信号模拟成的模拟电压信号。
3)PWM参数
-
频率
1秒内PWM有多少个周期,单位Hz。
-
周期
T = 1/f,50Hz = 20ms一个周期,如果频率为50Hz,也就是说一个周期是20ms,那么1秒钟就有50次PWM周期。
-
占空比
是一个脉冲周期内,高电平的时间与整个周期时间的比例,脉宽就是高电平的时间
4)例子
7、IO口工作方式
上拉输入、下拉输入、推挽输出、开漏输出。
8、DMA有什么用
直接存储器存取(Direct Memory Access,DMA)可以让某些电脑内部的硬体子系统(电脑外设),可以独立地直接读写系统存储器,而不需绕道 CPU。在同等程度的CPU负担下,DMA是一种快速的数据传送方式。它允许不同速度的硬件装置来沟通,而不需要依于 CPU的大量中断请求。大大提高了访问效率,减少访问时间,降低CPU资源的消耗。
9、请解释名词:时钟周期、状态周期、机器周期和指令周期?
-
时钟周期:也称为震荡周期,定义为时钟频率的倒数(时钟周期是单片机外接晶振的倒数,如12Mhz的晶振,它的时钟周期就是1/12us),它是单片机中最基本的、最小的时间单位。
-
状态周期:它是时钟周期的两倍
-
机器周期:单片机的基本操作周期,在一个操作周期内,单片机完成一项基本操作,如取指令、存储器读写等。它由12个时钟周期(6个状态周期)组成。
-
指令周期:他是指CPU执行一条指令所需要的时间。一般一个指令周期含有1~4个机器周期。
10、比特率、波特率、通信速度
1)比特率
概念:1字节(Byte)等于8比特(bit)。自然,比特率就是每秒钟传送的比特数。
2)波特率
概念:在电子通信领域,波特(Baud)即调制速率,指的是有效数据信号调制载波的速率,即单位时间内载波调制状态变化的次数。它是对符号传输速率的一种度量,1波特即指每秒传输1个符号,而通过不同的调制方式,可以在一个码元符号上负载多个bit位信息。
和比特率类似,你只需要把波特率中的“波特”(也就是码元符号)理解为一个传输单元即可。
3)比特率与波特率的关系
比特率=波特率x单个调制状态对应的二进制位数。
4)通信速度
I²C通信速度100KHz是什么意思?
I²C属于同步通信,有一根时钟线(SCL),我们说的100KHz一般指的就是这个时钟线的频率。
5)例子
串口传输速率为9600bps,每秒可传输多少字节?起始位:1数据位:8停止位:1校验位:0
传输1字节数据,需要传输10bit,因此:9600 ÷ 10 = 960Byte ,即(常规)串口9600波特率每秒传输960字节。
11、单片机FLASH和ROM是一个东西吗
FLASH**又称闪存,快闪。**它是EEPROM的一种。它结合了ROM和RAM的长处。不仅具备电子可擦除可编辑(EEPROM)的性能,还不会断电丢失数据同时可以快速读取数据。它于EEPROM的最大区别是,FLASH按扇区(block)操作,而EEPROM按照字节操作。FLASH的电路结构较简单,同样容量占芯片面积较小,成本自然比EEPROM低,因此适合用于做程序存储器。
12、Stm32时钟系统
1)简述
Stm32共5个时钟源
- HSI——高速内部时钟,RC 振荡器, 频率 为 8M Hz 。
- HSE——高速外部时钟,可接石英 陶瓷谐振器,或者接外部时钟源,频率范围4MHz~16MHz。
- LSI——低速内部时钟,RC 振荡 器,频率为 40kHz ,独立看门狗的时钟源只能是 LSI。
- LSE——低速外部时钟,是低速外部时钟,接频率为 32.768kHz 的 石英晶体,一般给RTC用。
- PLL——锁相环倍频输出
2) 为什么采用多个时钟源
STM32 本身非常复杂,外设非常的多,但是 并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的 方法来解决这些问题。
3)几个重要的时钟
- 系统时钟SYSCLK
可来源于三个时钟源:1.HSI振荡器时钟 2.HSE振荡器时钟 3.PLL时钟
-
APB1总线时钟(低速):速度最高36MHz,来自系统时钟
-
APB2总线时钟(高速):速度最高72MHz,来自系统时钟
13、Stm32 I/O口
首先STM32 的IO 口可以由软件配置成如下8 种模式:
1、输入浮空
2、输入上拉
3、输入下拉
4、模拟输入
5、开漏输出
6、推挽输出
7、推挽式复用功能
8、开漏复用功能
14、独立看门狗是什么
单片机系统在外界的干扰下会出现程序跑飞的现象导致出现死循环, 看门狗电路就是为了避免这种情况的发生 。 看门狗的作用就是在 一定时间内(通过定时计数器实现) 没有接收喂 信号(表示 MCU 已经挂了),便实现处理器的自动复位重启 (发送复位信号 )。
根据内部看门狗时钟频率,装载寄存器定一个时间值,比如是1000,那么独立看门狗就会按照时钟频率,从1000开始向下每隔一个时钟周期减1,如果在减到0之前,你用程序代码重新向向下计数器里面写1000(喂狗),那么定时器会重新从1000开始向下递减。如果在减到0的时候,你还没有喂狗(用新的数值覆盖计数器),就会产生复位信号。
15、窗口看门狗是什么
根据系统时钟频率,装载一个初始值到向下计数器(假设还是1000),并且设置一个窗口值(小于装载到计数器的初始值,假设是500),窗口看门狗一般会定死窗口下线值是64。计数器从1000开始向下减,在减到500之前(1000到500间),是不允许你去喂狗的,一旦喂狗,就会产生复位信号。只有计数器值减到上限值之后(500到64),才允许你去喂狗。当计数器减到下限值(64到0之间),如果喂狗,也会产生复位信号,当减到0之后,自动产生复位信号。
所以窗口看门狗实际上就是设置一个窗口(上下限),在这个范围内,你才允许你去喂狗,只要不在这个范围之内,都会复位。
16、独立看门狗和窗口看门狗的相同点与区别
-
相同点
都是为了防止程序跑飞。
-
区别
1、窗口看门狗计时时间比独立看门狗精准,窗口看门狗使用的是系统时钟源,独立看门狗用的是LSI。
2、窗口看门狗严格限定喂狗时间段,独立看门狗则是只要没有到时间,都能喂狗
17、中断
1) 概念
- 中断
1:在单片机系统中,如果遇到需要紧急处理的突发事件时,CPU需要迅速的作出反应,暂停正在运行的程序来处理突发事件,这时就需要中断。
2:中断是指单片机正在执行程序的时,发生突发事件从而打断当前程序,转而去处理这一事件,当处理完成后再回到原来被打断出继续执行原程序的过程。
-
异常
异常是导致程序流更改的事件。当发生异常时,处理器暂停当前正在执行的任务,并执行程序中称为异常处理程序的一部分。异常处理程序的执行完成后,处理器将恢复正常的程序执行。在ARM体系结构中,中断是一种异常类型。中断通常由外设或外部输入产生,在某些情况下,它们可以由软件触发。中断的异常处理程序也称为中断服务例程(ISR)。
每个异常源都有一个异常编号。异常编号1至15为系统异常,异常编号16及以上为中断。Cortex-M3和Cortex-M4处理器中的NVIC设计可支持多达240个中断输入。然而,在实践中,设计中实现的中断输入数量要少得多,通常在16到100之间。通过这种方式,设计的硅尺寸可以减小,这也降低了功耗。
18、
19、
20、
三、计算机原理
1、程序的内存分配
1)基础
一个由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区, 程序结束后由系统释放。
4、文字常量区—常量字符串就是放在这里的。程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。
//main.cpp
int a = 0; //全局初始化区
int a = 0; //全局初始化区
char* p1; //全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //栈
char* p2; //栈
char* p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c = 0; //全局(静态)初始化区
p1 = (char*)malloc(10);
p2 = (char*)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
2)堆和栈理论知识
1)) 申请方式
stack:
由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数,如p1 = (char *)malloc(10);
在C++中用new运算符,如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
2)) 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3)) 申请的大小限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4)) 申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便,另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
5)) 堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
3)在单片机中怎么理解
1)) 何为变量?
2)) 如何判定全局变量和局部变量?
简单直观的来说,全局变量就是在函数外面定义的变量,局部变量就是在函数内部定义的变量
3)) 全局变量和局部变量的内存模型
单片机内存包括ROM
和RAM
两部分,ROM
存储的是单片机程序中的指令和一些不可更改的常量数据,而 RAM
存放的是可以被更改的变量数据;也就是说,全局变量和局部变量都是存放在RAM
,但是,虽然都是存放在 RAM
,全局变量和局部变量之间的内存模型还是有明显的区别的。因此,分了两个不同的RAM
区,全局变量占用的 RAM
区称为全局数据区, 局部变量占用的 RAM
区称为栈。
4)) 它们的内存模型到底有什么本质的区别呢?
全局数据区就像你自己家的房间,是唯一的,一个房间的地址只能你一个人住(假设你还是单身狗的时候),而且是永久的(sorry),所以说每个全局变量都有唯一对应的 RAM 地址, 不可能重复的。
栈就像客栈, 一年下来每天晚上住的人不一样,每个人在里面居住的时间是有期限的,不是长久的,一个房间的地址一年下来每天可能住进不同的人,不是唯一的。
全局数据区的全局变量拥有永久产权,栈区的局部变量只能临时居住在宾馆客栈, 地址不是唯一的, 有期限的。栈是给程序里所有函数内部的局部变量共用的,函数被调用的时候,该函数内部的每个局部变量就会被分配对应到栈的某个RAM
地址,函数调用结束后,该局部变量就失效。
因此它对应的栈的RAM
空间就被收回,以便给下一个被调用的函数的局部变量占用。
5)) 举例借用“宾馆客栈”来比喻局部变量所在的“栈”
void function(void); //子函数的声明
void function(void) //子函数的定义
{
unsigned char a; //局部变量
a=1;
}
void main() //主函数
{
function() ; //子函数的调用
}
我们看到单片机从主函数 main 往下执行, 首先遇到function()
子函数的调用, 所以就跳到function()
函数的定义那里开始执行, 此时的局部变量 a 开始被分配在 RAM
的“栈区” 的某个地址, 相当于你入住宾馆被分配到某个房间。
单片机执行完子函数function()
后,局部变量 a 在 RAM
的栈区
所分配的地址被收回, 局部变量a 消失,被收回的RAM
地址可能会被系统重新分配给其它被调用的函数的局部变量。
此时相当于你离开宾馆,从此你跟那个宾馆的房间没有啥关系, 你原来在宾馆入住的那个房间会被宾馆老板重新分配给其他的客人入住。
全局变量的作用域是永久性不受范围限制的,而局部变量的作用域就是它所在函数的内部范围。全局变量的全局数据区
是永久的私人房子,局部变量的栈
是临时居住的客栈。
6)) 一些问题
1.全局数据区和栈区是谁在幕后分配的, 怎么分配的?
是C编译器自动分配的, 至于怎么分配,谁分配多一点,谁分配少一点,C 编译器会有一个默认的比例分配, 我们一般都不用管。
2.栈区是临时借用的,子函数被调用的时候,它内部的局部变量才会“临时” 被分配到“栈” 区的某个地址,那么问题来了,谁在幕后主持“栈区” 这些分配的工作?
单片机已经上电开始运行程序的时候,编译器已经不起作用,“栈区” 分配给函数内部局部变量的工作,确实是 C 编译器做的,但这是在单片机上电前。C 编译器就把所有函数内部的局部变量的分配工作就规划好了,都指定了如果某个函数一旦被调用,该函数内部的哪个局部变量应该分到“栈区” 的哪个地址,C 编译器都是事先把这些“后事” 都交代完毕了才结束自己的生命。等单片机上电开始工作的时候,虽然C编译器此时不在了,但是单片机都是严格按照C编译器交代的遗嘱开始工作和分配“栈区”的。因此,“栈区” 的“临时分配” 非真正严格意义上的“临时分配”。
3.函数内部所定义的局部变量总数不超过单片机的“栈” 区的 RAM 数量, 那, 万一超过了“栈” 区的 RAM数量, 后果严重吗?
这种情况专业术语叫爆栈。程序会出现莫名其妙的异常,后果特别严重。为了避免这种情况, 一般在编写程序的时候, 函数内部都不能定义大数组的局部变量, 局部变量的数量不能定义太多太大,尤其要避免刚才所说的定义开辟大数组局部变量这种情况。
大数组的定义应该定义成全局变量,或者定义成 静态的局部变量。有一些C编译器,遇到“爆栈” 的情况,会好心跟你提醒让你编译不过去,但是也有一些 C 编译器可能就不会给你提醒,所以大家以后做项目写函数的时候,要对爆栈心存敬畏。
4.全局变量和局部变量的优先级
刚才说到,全局变量的作用域是永久性并且不受范围限制的,而局部变量的作用域就是它所在函数的内部范围。那么问题来了,假如局部变量和全局变量的名字重名了,此时函数内部执行的变量到底是局部变量还是全局变量?
这个问题就涉及到优先级。注意,当面对同名的局部变量和全局变量时,函数内部执行的变量是局部变量,也就是局部变量在函数内部要比全局变量的优先级高。
7)) 总结
- 每定义一个新的全局变量,就意味着多开销一个新的
RAM
内存。而每定义一个局部变量,只要在函数内部所定义的局部变量总数不超过单片机的栈
区,此时的局部变量不开销新的RAM
内存, 因为局部变量是临时借用栈
的, 使用后就还给栈
,栈
是公共区, 可以重复利用,可以服务若干个不同的函数内部的局部变量。 - 单片机每次进入执行函数时,局部变量都会被初始化改变,而全局变量则不会被初始化, 全局变量是一直保存之前最后一次更改的值。
2、单片机存储相关
1)两种存储器
1)) FLASH
Flash Memory(闪速存储器)是一种安全、快速的存储体,具有体积小、容量大、成本低、掉电不丢失等一系列优点,已成为嵌入式系统中数据和程序最主要的载体。
Flash是区块结构
,即在物理结构上分成若干个物理块,区块之间相互独立。
Flash写操作必须先擦后写
,Flash只能将数据位由1写成0,不能从0写成1,所以在对存储器写之前必须先执行擦除操作,擦操作的最小单位是一个区块,而不是一个字节。
2)) RAM
RAM(Random Access Memory)又称随机存取存储器,也叫内存,是与CPU直接交换数据的内部存储器。速度很快,断电RAM不保留数据。
RAM主要用来存储程序中用到的全局变量、堆栈等。
2)三种存储区
编译完工程会生成一个.map 的文件,该文件的最后说明了ROM和RAM占用空间大小,如下图所示:
其中ROM就是程序烧录到FLASH中的大小,RW就是占用RAM大小。
-
RO (Read Only ): 只读区域, 需要长久保存,烧录到Flash中,下文的text段和constdata段属于此属性区
-
RW (Read Write): 可读可写,通常为全局变量和静态变量,下文中的.data段和.bss属于RW区
-
ZI (Zero Init): 没有进行初始化或者初始化为0,系统上电时会主动把此区域数据进行0初始化,下文的.bss段就是. 另外, 可翻看Keil工具编译的map文件,Heap和Stack区也进行了Zero的属性标注, 因此,Heap和Stack也可认为是ZI区域。
-
数据段 说明 RAM ROM .bss – true false .data true true RO-data 常量 false true .text – false true stack 局部变量等 true false heap malloc true false
3)程序运行时的5段
- .text代码段: 用来放程序代码(code), 在代码编译完成后, 长久只读存放于此,属于图中的
代码段
- .constdata只读常量数据段: const限定的数据类型存放在此,属于图中的
常量存储区
- .data用来存放初始化不为0的全局变量(global)和静态变量(static),它是可读可写的,属于图中的
静态存储区
- .bss的英文是
Block Started by Symbol
,翻译过来就是由符号开始的块
。此部分类似于数据部分,只是它不占用可执行文件中的空间。bss通常是指用来存放程序中没有初始化或初始化为0的全局变量和静态变量
的一块内存区域,可读可写,属于图中的静态存储区
,如果变量未初始化,系统会将变量初始化为0。 - heap堆区: 通常我们说的动态内存分配,使用malloc/free进行申请和释放,属于动态存储区。
- stack栈区: 在代码执行时用来保存函数的局部变量和参数,属于动态存储区。