“filename.h”是从本项目里搜索filename.h,<filename.h> 是从标准库里搜索filename.h文件
问题:“静态全局变量”和“非静态全局变量”有什么区别?“静态局部变量”和“非静态局部变量”有什么区别?“静态函数”和“非静态函数”有什么区别? 静态全局变量只在本文件中定义,其他文件不能引用. 局部变量所在函数每次调用的时候都会被重新分配存储空间,函数结束后,就会回收该存储空间。静态局部变量不会,始终保持当前值。
calloc在动态分配完内存后,将内存空间置为零。malloc不初始化,里边数据是随机的脏数据。
静态全局变量:在全局变量前,加上关键字static,该变量就被定义成为一个静态全局变量。静态变量在应用层面上主要是限定作用域。 静态全局变量有以下特点:
代码区 | low address |
---|---|
全局数据区堆区栈区 | high address |
一般程序把新产生的动态数据存放在堆区,函数内部的自动变量存放在栈区。自动变量一般会随着函数的退出而释放空间,静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间。 定义全局变量就可以实现变量在文件中的共享,但定义静态全局变量还有以下好处:
void staticFun(void)
{
static uint8_t data = 0;
data++;
printf("static function data = %d\r\n",data);
}
void NostaticFun(void)
{
uint8_t data = 0;
data++;
printf("no static function data = %d\r\n",data);
}
int main()
{
staticFun();
staticFun();
staticFun();
NostaticFun();
NostaticFun();
NostaticFun();
return 0;
}
执行此程序,主函数会先调用三次staticFun();函数,再调用三次NostaticFun();函数。最后的输出结果为:
1
2
3
1
1
1
因为每次NostaticFun中的data 都会被重新定义,而staticFun中的data不会重复定义。
用来修饰不可赋值的变量,如果一个变量在声明初始化之后不希望被修改,可以声明为const; const修饰的变量应该进行初始化; const修饰的变量有可能改变,部分编译器可用scanf修改; const常用来修饰函数的形参,保证该参数在函数内部不会被修改。
extern表明变量或者函数是定义在其他其他文件中的。用来修饰外部变量(全局),表示该变量在其他文件中定义。首先讲一下声明与定义, 声明不等于定义,声明只是指出了变量的名字,并没有为其分配存储空间;定义指出变量名字同时为变量分配存储空间,定义包含了声明。extern是用来声明全局变量的。注意:在程序中一个变量可以声明多次,但只能定义一次。
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)。
1.全局变量
从静态存储区域分配,其作用域是全局作用域,也就是整个程序的生命周期内都可以使用。如果程序是由多个源文件构成的,那么全局变量只要在一个文件中定义,就可以在其他所有的文件中使用,但必须在其他文件中通过使用extern关键字来声明该全局变量。
2.全局静态变量
从静态存储区域分配,其生命周期也是与整个程序同在的,从程序开始到结束一直起作用。与全局变量不同的是,全局静态变量作用域只在定义它的一个源文件内,其他源文件不能使用。
3.局部变量
从栈上分配,其作用域只是在局部函数内,在定义该变量的函数内,只要出了该函数,该局部变量就不再起作用,也即该变量的生命周期和该函数同在。
4.局部静态变量
从静态存储区域分配,其在第一次初始化后就一直存在直到程序结束。该变量的特点是其作用域只在定义它的函数内可见,出了该函数就不可见了。
在 C/C++ 中,结构体/类是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。编译器为每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。如果在 32 位的机器下,一个int类型的地址为0x00000004,那么它就是自然对齐的。同理,short 类型的地址为0x00000002,那么它就是自然对齐的。char 类型就比较 “随意” 了,因为它本身长度就是 1 个字节。自然对其的前提下:
char 偏移量为sizeof(char) 即 1 的倍数
short 偏移量为sizeof(short) 即 2 的倍数
int 偏移量为sizeof(int) 即 4 的倍数
float 偏移量为sizeof(float) 即 4 的倍数
double 偏移量为sizeof(double) 即 8 的倍数
结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
struct asd1{
char a;
int b;
short c;
};//12字节
struct asd2{
char a;
short b;
int c;
};//8字节
上面两个结构体拥有相同的数据成员 char、short 和 int,但由于各个成员按照它们被声明的顺序在内存中顺序存储,所以不同的声明顺序导致了结构体所占空间的不同。具体如下图:
2. 看到上面的第二张图,有的人可能会有疑问,为什么 short 不是紧挨着 char 呢?其实这个原因在上面已经给出了答案——自然对齐。为此,我们可以创建结构体验证自然对齐的规则。实验很简单,在原本 short 类型变量前后添加 char 类型,看结果是怎样的。实验二:
struct asd3{
char a;
char b;
short c;
int d;
};//8字节
struct asd4{
char a;
short b;
char c
int d;
};//12字节
3. 当数据成员中有 double 和 long 时,情况又会有一点变化。还是以上面的结构体 asd1 和 asd2 为基础,都添加 double 型数据成员。来看看结果是什么,实验三:
struct asd1{
char a;
int b;
short c;
double d;
};//24个字节
struct asd2{
char a;
short b;
int c;
double d;
};//16个字节
只添加了一个 double,但 struct asd1 的大小从 12 变到了 24。而 struct asd2 的大小从 8 变到了 16。不需要迷惑,因为这和 double 的自然对其有关(需要注意)。原本的 asd1 占 12 个字节大小,但是 double 对齐需要是 8 的倍数,所以在 short 后面又填充了 4 个字节。此时,asd1 的占 16 个字节,再加上 double 的 8 个字节就成了 24 个字节。而 asd2 没有这个问题,它原本占 8 个字节。因为正好能对齐,所以添加 double 后占 16 个字节。具体情况如下图所示:
4. 指定对齐值 在缺省情况下,C 编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件: 使用伪指令 #pragma pack (n),C 编译器将按照 n 个字节对齐。 使用伪指令 #pragma pack (),取消自定义字节对齐方式。
实验四:
#pragma pack(4)
struct asd5{
char a;
int b;
short c;
float d;
char e;
};//20
#pragma pack()
#pragma pack(1)
struct asd6{
char a;
int b;
short c;
float d;
char e;
};//12
#pragma pack()
使用 #pragma pack (value) 指令将结构体按相应的值进行对齐。两个结构体包含同样的成员,但是却相差 8 个字节。难道我们只需要通过简单的指令就能完成内存对齐的工作吗?其实不是的。上面的对齐结果如下:
以 32 位机器为例,CPU 取的字长是 32 位。所以上面的对齐结果会这样带来的问题是:访问未对齐的内存,处理器需要作两次内存访问。如果我要获取 int 和 float 的数据,处理器需要访问两次内存,一次获取 “前一部分” 的值,一次获取 “后一部分” 的值。这样做虽然减少了空间,但是增加访问时间的消耗。其实最理想的对齐结果应该是:
ps.使用 #pragma pack(4) 可以让前面的实验三中的 asd1 少占用 4 字节。
对齐原则
根据实际情况,有时需要把几种类型不同的数据,如一个整型变量、一个字符变量、一个实型变量存放在起始地址相同的同一段存储单元种。这三个变量在内存种所占的字节数不同,但都从同一个地址开始存放。这种几个类型不同的变量共同占用同一段内存的结构,称为“共用体”类型结构。共用体,也称为联合体。
union 共用体名
{
成员表列
};
共用体变量的所有成员共享同一段存储空间,这个存储空间等于共用体变量种占据内存字节数最多的成员的字节数。
##用来连接前后两个参数,把它们变成一个字符串。 例子如下:
#define main(x,y) x##y
int xy=1;
cout < < main(x,y) < < endl;
将会使编译器把 cout < < main(x,y) < < endl; 解释为 cout < < xy < < endl; 理所当然,将会在标准输出处显示’1’。 从此可以看出,x##y的效果就是将x和y连在一起了。 而#define main(x,y) x##y 则相当于把main(x,y)等价于x##y
我们在调试程序时,经常会遇到某段功能的实现,写了两种版本的程序,但调试时又不想来回切换。,这时候我们可以使用条件变量。 比如:想测试__set_FAULTMASK(1);和__disable_fault_irq();的区别,就可以使用如下方式,只需要更改#if后面是1还是0就可以选择是使用哪段程序。
#if 1
__set_FAULTMASK(1);
NVIC_SystemReset();
#else
__disable_irq();
delay_ms(1000);
__disable_fault_irq();
NVIC_SystemReset();
#endif
C库函数是我们开发过程中必不可少的,其中面试中突出考察的大多为string.h中的库函数。 memcpy函数的用法,memcpy (void* _Dst,void const* _Src,size_t _Size) memcpy函数是将后面地址的内容一个数据一个数据放在前面的地址,注意,是先放低位。 _Size是字节数,也就是说如果是32位数组,两个数组值就应该是_Size就应该是4。 例子:
char a[8]={0x12,0x34,0x56,0x78,0x90,0x14,0x52,0x46 };
short b=0;
memcpy(&b,a+1,2);
printf("b=%x", b);
此段代码的作用是把0x34和0x56拼接起来送到b,输出的最终结果是:0x5634。
void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针对 void 指针赋值。如果要将 void 指针 p 赋给其他类型的指针,则需要强制类型转换,就本例而言:a=(int *)p。在内存的分配中我们可以见到 void 指针使用:内存分配函数 malloc 函数返回的指针就是 void * 型,用户在使用这个指针的时候,要进行强制类型转换,也就是显式说明该指针指向的内存中是存放的什么类型的数据 (int )malloc(1024) 表示强制规定 malloc 返回的 void 指针指向的内存中存放的是一个个的 int 型数据。
int *Q;
void *P;
P=Q;
我们可以看到void指针类型是可以指向int *指针类型的。
在64位系统中,不管什么样的基类型,系统指针给指针变量分配的内存空间都是8字节,在C语言中,指针变量的“基类型”仅用来指定该指针变量可以指向的变量类型,并没有其他意思。不管基类型是什么类型的指针变量,他仍然是指针变量,所以仍然占用 8 字节。
%*c表示忽略一个字符
此函数在嵌入式的日常开发中使用频繁,功能为:在字符串 A中查找第一次出现字符串B的位置。 C 标准库 - <string.h> C 库函数 char *strstr(const char *haystack, const char *needle) 在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 ‘\0’。
C 标准库 - <stdlib.h 描述 C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。
描述 C 库函数 int rename(const char *old_filename, const char *new_filename) 把 old_filename 所指向的文件名改为 new_filename。 参数 old_filename – 这是 C 字符串,包含了要被重命名/移动的文件名称。 new_filename – 这是 C 字符串,包含了文件的新名称。
O(n^2):b、e O(n*logn):a、c、d
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
解答;会崩溃:因为GetMemory 并不能传递动态内存,Test 函数中的 str 一直都是 NULL。strcpy(str, “hello world”);将使程序崩溃;动态申请内存有可能失败,所以应该增加判断; 执行GetMemory之后,p得到新分配的空间地址,str依然为NULL; 没有对内存进行回收free(),局部变量存在栈区,malloc()在堆区; 局部变量在函数执行完毕之后回收栈空间; 调用时传递进去的参数是str的一份备份。既然是备份,那么无论函数内部如何操作,都只是操作它的备份,与原本的str值没有关系。所以Test里的str经过GetMemory之后仍然是原本定义时的NULL,使用strcpy字符串到NULL时自然就会发生段错误。另外,如果第7行不初始化为NULL,编译时不会报错,但是,它就成了野指针野指针野指针啊,操作野指针是很危险的; 一级指针传递的函数内部更改不影响实参一级指针的值,所以此处要么使用二级指针,要么使用引用。
(2)
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
解答:乱码:char p[ ]是函数内部局部自动变量,存在于“栈内存(stack)”中,当GetMemory函数运行结束之后p的内存释放了,将它返回给调用者去操作就自然出错; 有些初学者可能会好奇,明明以前开发时,经常会用到函数返回值,没出现过乱码?造成这种不同的原因,其实是因为返回类型的不一样,函数返回的是返回值副本,如果你返回的是一个值那无所谓,这个值本身就是你需要的,但如果像上述程序一样返回的是一个指针地址,因为此地址的指向的栈内存已经释放了,那你读取的就是脏数据了。也就是乱码。 (3)
Void GetMemory2(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
解答:正常输出hello (4)
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, “hello”);
free(str);
if(str != NULL)
{
strcpy(str, “world”);
printf(str);
}
}
解答:str的动态内存已经被释放
char *revstr(char *str, size_t len)
{
char *start = str;
char *end = str + len - 1;
char ch;
if (str != NULL)
{
while (start < end)
{
ch = start;
end=ch;
start=end;
*start++;
*end--;
}
}
return str;
}
//将字符串中的小写字母转换为大写
//str:要转换的字符串
//len:字符串长度
void litterTobig(u8 *str,u8 len)
{
u8 i;
for(i=0;i<len;i++)
{
if((96<str[i])&&(str[i]<123)) //小写字母
str[i]=str[i]-32; //转换为大写
}
}
int8_t* CapToLow(int8_t* str)
{
int i;
for (i = 0; i < sizeof(str); i++)
{
if ((64 < str[i]) && (str[i] < 91)) //大写
str[i] = str[i] + 32; //小写
}
return str;
}
uint32_t ultmp;
ultmp=0x12345678;
ultmp&= ~(0XFFFF0000);
printf("ultmp=0x%d\n",ultmp);
上述程序的作用就是将ultmp的高16位 置0,低16位保留. 最后输出的结果是0x00005678。
这里知识帮大家总结一下,有什么错误的地方欢迎指正。
编写不易,感谢支持。