目录
宏定义
@ 预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
@ 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
@ 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
变量定义
@ 用变量a给出下面的定义
@ 关键字static的作用是什么?
@ 关键字const有什么含意?
@ 关键字volatile有什么含意?并给出三个不同的例子。
递归
@ 递归举例:倒叙打印字符串
应用
@ 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。
@ 下面的代码就使用了 __interrupt 关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
@ 强制类型转换题,下面的代码输出是什么,为什么?
@ 处理器字长考察, 评价下面的代码片断
@ typedef 和 #define 的区别
@ 计算优先级考查,下边代码执行后各变量的值
@ 内存管理MMU的作用
@ ROM 和 RAM 的区别
@ signed char 的取值范围
@ 编译和链接有什么不同?(如外部符号的处理)
@ 不调用C++/C的字符串库函数,请编写函数strcpy
@ strcpy能把strSrc的内容复制到strDest,为什么还要char *类型的返回值?
@ 什么是堆栈,简述为什么需要堆栈?
@ 请列举常用的串行通信方式(两种以上),并简述串行通信和并行通信不同之处、优缺点。
@ 列举一下你熟悉7层OSI协议中的几层。说说你最熟悉的一层协议的功能。
@ 字符串倒序代码
@ 引用和指针的区别
@ 队列和栈的区别
@ 代码输出什么?考察 malloc(0))
@ 获取动态内存分配大小
@ 关键字static的作用是什么
@ static考察,说出输出结果,并说明每个static的作用
@ 全局变量和局部变量的区别。
@ 数组与链表的区别。
@ 死锁的四个条件及处理方法。
@ 进程调度策略。
@ Linux驱动程序流程及功能。
@ 时间换空间、空间换时间的例子。
@ MAC层通信协议有哪些?
@ 参数的传递方式有几种?
@ 局部变量能否和全局变量重名?
@ 如何引用一个已经定义过的全局变量?
@ 全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
@ 语句for( ;1 ;)有什么问题?它是什么意思?
@ static全局变量与普通的全局变量有什么区别?static函数与普通函数有什么区别?
@ 代码储存位置
@ -1,2,7,28,,126请问28和126中间那个数是什么?为什么?
@ 用两个栈实现一个队列的功能?要求给出算法和思路!
@ .软件测试都有那些种类?
@ 堆栈溢出一般是由什么原因导致的?
@ 写出float x 与“零值”比较的if语句。
@ P地址的编码分为哪俩部分?
@ 写一个程序, 要求功能:求出用1,2,5这三个数不同个数组合的和为100的组合个数。
@ 内存对齐问题的原因?
@ 比较一下进程和线程的区别?
@ 数组指针题,回答运行结果?
@ 参数作用域及malloc问题考察1?
@ 参数作用域及malloc问题考察2?
@ 参数作用域及malloc问题考察3?
@ 参数作用域及malloc问题考察4?
@ 参数作用域、指针及malloc问题考察5?
@ 进程间通信方式:管道、命名管道、消息队列、共享内存、信号、信号量、套接字。
@ 宏和函数的优缺点?
@ c 和 c++的一些不同点(从语言本身的角度):
@ 大小端格式问题。
@ 由C/C++编译的程序占用的内存分为以下几个部分
@ 内存问题综合考察,分析下边代码的 cmd 和 dat 的值变化过程
@ for 语句训练
@ for语句训练
@ 5["abcdef"]能够编译通过,请问编译后的结果是什么?
@ [] ==> 下标运算符,数组的新玩法
@ C语言:char *a =”abcdef”; char a[]=”abcdef”; 的区别
@ 线程同步的方法:
@ 物理地址,虚拟地址,逻辑地址和总线地址的区别
@ 编写内核程序中申请内存和编写应用程序时申请内存有什么区别
@ 如何用C语言实现读写寄存器变量
@ sscanf 格式化字符串
@ 静态字符串考察,下边代码有什么错?
@ c语言位域
@ 结构体数据对齐
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
#define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
预处理器将为你计算常数表达式的值,因此直接写出你如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
标识 #define 在宏中应用的基本知识。这是很重要的。因为在 嵌入(inline)操作符 变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优的代码,了解这个用法是很重要的。
懂得在宏中小心地把参数用括号括起来
用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
推荐写法
while(1) {
}
一些程序员更喜欢如下方案:
for (;;) { }
因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。
第三个方案是用 goto
Loop:
...
goto Loop;
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。
int a; // 一个整型数
int *a; // 一个指向整型数的指针
int **a; // 一个指向指针的的指针,它指向的指针是指向一个整型数
int a[10]; // 一个有10个整型数的数组
int *a[10]; // 一个有10个指针的数组,该指针是指向一个整型数的
int (*a)[10]; // 一个指向有10个整型数数组的指针
int (*a)(int); // 一个指向函数的指针,该函数有一个整型参数并返回一个整型数
int (*a[10])(int); // 一个有10个指针的数组,该指针指向一个函数,
// 该函数有一个整型参数并返回一个整型数
在C语言中,关键字static有三个明显的作用:
在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
面试只能答出 " const 意味着常数" 还比较初级,需要理解具体的含义
const int a; // a 是一个常整型数。
int const a; // a 是一个常整型数。
const int *a; // a 是一个指向常整型数的指针
// 指向的整型数是不可修改的,但指针的值可以
int *const a; // 不能赋给a其他的地址值
// 可以改变a 指向的值
const int *const a; // a 是一个指向常整型数的常指针
// 指针指向的整型数是不可修改的,同时指针也是不可修改的
即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const 呢?我也如下的几下理由:
关键字 const 的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。
通过给优化器一些附加的信息,使用关键字 const 也许能产生更紧凑的代码。
合理地使用关键字 const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
关于const的补充:
const 对象的地址只能赋值给指向const 对象的指针
指向const 对象的指针可以 被赋 以 一个非const 对象的地址
指向const 得指针常被用作函数的形式参数,保证被传递给函数的实际对象在函数得实际对象在函数中不会被修改
常量在定义后就不能被修改,所以它必须被初始化。未初始化的常量定义将导致编译错误(上面都是在说明const得问题,所以没有赋值,实际语句中要赋值的)
这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
并行设备的硬件寄存器(如:状态寄存器)
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
多线程应用中被几个任务共享的变量
使用经验
调试的时候会发现有时变量值看不到,就需要用 volatile 定义变量,告诉编译器不要优化掉。
程序举例
// 错误的用法
int square(volatile int *ptr) { // 返回平方值
int a,b;
a = *ptr;
b = *ptr;
return a * b; // 因为 ptr 可能随时改变,所以 a 不一定等于 b
}
// 正确的写法
int square(volatile int *ptr) {
int a,b;
a = *ptr;
return a * a;
}
void print_str_reverse(char *str) {
if (!*str)
return;
print_str_reverse(str + 1);
putchar(*str);
}
解释:
函数知识:在函数中进入下一个函数,上一个函数的状态还会保存
函数中的退出条件是*str 不为‘\0’,也就是字符串结尾
行5 打印在 行4进入递归之后,所以只有行4的函数退出才能执行行5的打印
因此每一个字符都在行4递归进入下一个函数
当字符结尾的时候,触发递归结束。执行 return
当函数返回时,从最后一次进入的函数开始。行4结束,开始执行行5打印
本层的行5打印一个字符后退出,跳到上层的行4结束,依次类推,直到第一层结束
由于return在打印函数的前边,因此不会打印结束符
c语言机制里关于递归深度有规定,因此容易发生栈溢出,所以不明确递归层数不建议使用
在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
// 方式一:(面试建议用这种)
int *ptr;
ptr = (int *)0x67a9;
* ptr = 0xaa55;
// 方式二
*(int * const)(0x67a9) = 0xaa55;
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
}
这个函数有太多的错误了:
ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
与第三点一脉相承,printf()经常有重入和性能上的问题。
void foo(void) {
unsigned int a = 6;
int b = -20;
(a + b > 6) ? puts("> 6") : puts("<= 6");
}
答案 输出:"> 6" ,理由如下:
若参与运算量的类型不同,则先转换成同一类型,然后进行运算
转换按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int量转成long型后再进行运算
若两种类型的字节数不同,转换成字节数高的类型
若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型
所有的浮点运算都是以双精度进行的,即使是两个 float 单精度量运算的表达式,也要先转换成double 型,再作运算
char 型和 short 型参与运算时,必须先转换成 int 型
在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型
如果右边量的数据类型长度左边长时,将丢失一部分数据,这样会降低精度,丢失的部分按四舍五入向前舍入
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1''s complement of zero */
以上代码应该改成
unsigned int compzero = ~0;
理由
代码的目的是求补码,0xFFFF只能用于16位的处理器。如32位的是 0xFFFF FFFF
嵌入式程序员处应理解理器字长的重要性,且非常准确地明白硬件的细节和它的局限
#define dPS struct s *
typedef struct s * tPS;
两种定义方式哪个更好?
使用 typedef 方式更好
两种方式对比
typedef 方式
定义: tPS p3,p4;
p3,p4都是 tPS 类型的
#define 方式
定义:dPS p1,p2;
实际相当于 struct s * p1, p2;
由于P1是指针,P2 是变量,两种类型不同,可能不是想要的结果
int a = 5, b = 7, c;
c = a+++b;
答案:a = 6, b = 7, c = 12。
分析:
换个形式写好理解: c = (a++) + (b)
a++ 先赋值 a ,再将 a+1,所以就变成 c = a + b; a += 1;
结果就是:c = 5+7=12, a=5+1=6, b=7
内存分配和回收
内存保护
内存扩充
地址映射
ROM是只读存储器,掉电不丢失
RAM是读写存储器,掉电丢失
SRAM:CPU的缓存就是SRAM,静态的随机存取存储器,
加电情况下,不需要刷新,数据不会丢失
DRAM,动态随机存取存储器最为常见的系统内存,需要不断刷新,才能保存数据
SDRAM:同步动态随机存储器,即数据的读取需要时钟来同步。
-128~127.
编译生成的是目标文件(object *.o);
编译过程中对于外部符号不做任何解释和处理。外部符号对应的就是“符号”
链接生成的是可执行程序
链接将会解释和处理外部符号。外部符号对应的是地址
已知strcpy函数的函数原型是:char *strcpy(char *strDest, const char *strSrc)。
其中,strDest是目的字符串,strSrc是源字符串。
char *strcpy(char *strDest, const char *strSrc) {
int i=0;
if(!(strDest && strSrc))
return NULL;
while(strDest[i++] = *strSrc++);
return strDest;
}
为了实现链式表达式
int len = strlen(strcpy(strDest, strSrc));
堆栈是计算机中最常用的一种数据结构,保存数据的一块连续内存;
比如函数的调用是用堆栈实现的。
异步通信和同步通信;
并行速度快,串行口线间干扰小
应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。
inverted_order(char *p) {
char *s1,*s2,tem;
s1=p;
s2=s1+strlen(p)-1;
while(s1
inverted_order(char *p)
{
int len = strlen(src);
char *des = (char *)malloc(len + 1);
char *s = &src[len -1];
char *d = des;
while(len-- != 0)
*d++ = *s--;
*d = 0;
free(des);
}
(1). 指针是一个实体,而引用仅是个别名;
(2). 引用使用时无需解引用(*),指针需要解引用;
(3). 引用只能在定义时被初始化一次,之后不可变;指针可变;
(4). 引用没有 const,指针有 const,const 的指针不可变;
(5). 引用不能为空,指针可以为空;
(6). “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
(7). 指针和引用的自增(++)运算意义不一样;
队列是先进先出,只能在一端插入另一端删除,可以从头或尾进行遍历(但不能同时遍历)
栈是先进后出,只能在同一端插入和删除,只能从头部取数据
void fun() {
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
puts("Got a null pointer");
else
puts("Got a valid pointer");
}
该代码的输出是“Got a valid pointer”。还可以*ptr='a';不出现段错误
为什么不出错?
当 malloc 分配内存时它除了分配我们指定 SIZE 的内存块,还会分配额外的内存来存储我们的内存块信息,用于维护该内存块
因此,malloc(0) 返回一个合法的指针并指向存储内存块信息的额外内存,我们当然可以在该内存上进行读写操作,但是这样做了会破坏该内存块的维护信息,因此当我们调用free(ptr)时就会出现错误
char *ptr = (char *)malloc(0);
_msize(ptr) // win 下调用
malloc_usable_size(ptr); // linux 下调用
static用来修饰一个局部的变量的时候,
生命域是全局的,作用域是局部的
static用来修饰一个模块内的(某一个C的源程序文件)全局变量的时候
生命域不变,作用域减小,只在本模块内有效
static用来修饰一个函数的时候
作用域减小,只在本模块内有效
#include
static int g = 0;
static int fun() {
static int a = 0;
g++;
return a++;
}
int main() {
int a = 0;
for (int i = 0; i < 3; i++) {
a = fun();
}
printf("g = %d, a = %d\n", g, a);
return 0;
}
全局变量,储存在静态区.进入main函数之前就被创建.生命周期为整个源程序;
局部变量,在栈中分配.在函数被调用时才被创建.生命周期为函数内。
数组中的数据在内存中的按顺序存储的,而链表是随机存储的!
要访问数组中的元素可以按下标索引来访问,速度比较快,如果对他进行插入操作的话, 就得移动很多元素,所以对数组进行插入操作效率很低!
由于连表是随机存储的,链表在插入,删除操作上有很高的效率(相对数组)
如果要访问链表中的某个元素的话,那就得从链表的头逐个遍历,直到找到所需要的元素为止,所以链表的随机访问的效率就比数组要低
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决死锁的方法分为死锁的预防,避免,检测与恢复三种
先进先出算法,最短CPU运行期优先调度算法,轮转法,多级队列方法
设备驱动程序的功能:
对设备初始化和释放
把数据从内核传送到硬件和从硬件读取数据
读取应用程序传送给设备文件的数据和回送应用程序请求的数据
检测和处理设备出现的错误
冒泡排序 --- 时间换空间
快速排序,堆排序 --- 空间换时间
ISO2110,IEEE802,IEEE802.2
值传递
指针传递
严格来看,只有一种传递,值传递,指针传递也是按值传递的,复制的是地址。
能,局部会屏蔽全局。要用全局变量,c++需要使用 ":: " 局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。
对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。
extern 可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,
假定你将那个变量写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。
可以,在不同的C文件中以static形式来声明同名全局变量。 相当于每个文件中都定义自己的变量,本文件中改变值不影响其他文件。
可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错
和while(1)相同。
全局变量(外部变量)的说明之前再加static 就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效。 从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。
static函数与普通函数作用域不同。仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static) ,内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件
static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用
static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;
static函数与普通函数有什么区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝
程序的局部变量存在于(堆栈)中,全局变量存在于(静态区 )中,动态申请数据存在于(堆)中。
答案应该是4^3-1=63 规律是n^3-1(当n为偶数0,2,4)n^3+1(当n为奇数1,3,5)
设2个栈为A,B, 一开始均为空.
入队: 将新元素push入栈A;
出队:
判断栈B是否为空;
如果不为空,则将栈A中所有元素依次pop出并push到栈B;
将栈B的栈顶元素pop出;
人工测试:个人复查、抽查和会审
机器测试:黑盒测试(针对系统功能的测试 )
白盒测试 (测试函数功能,各函数接口)
没有回收垃圾资源。
if(x>0.000001&&x<-0.000001)
IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区分哪些是网络位哪些是主机位
如:100个1是一个组合,5个1加19个5是一个组合。。。。 请用C++语言写。
答案:最容易想到的算法是:
设x是1的个数,y是2的个数,z是5的个数,number是组合数
注意到0<=x<=100,0<=y<=50,0<=z=20,所以可以编程为:
number=0;
for (x=0; x<=100; x++)
for (y=0; y<=50; y++)
for (z=0; z<=20; z++)
if ((x+2*y+5*z)==100)
number++;
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据;
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,因为为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次。
(1)、调度:线程是CPU调度和分派的基本单位
(2)、拥有资源:
进程是系统中程序执行和资源分配的基本单位
线程自己一般不拥有资源(除了必不可少的程序计数器,一组寄存器和栈),但他可以去访问其所属进程的资源,
如进程代码,数据段以及系统资源(已打开的文件,I/O设备等)。
(3)系统开销:
同一进程中的多个线程可以共享同一地址空间,因此它们之间的同步和通信的实现也比较简单
在进程切换的时候,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置;
而线程切换只需要保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作,从而能更有效地使用系统资源和提高系统吞吐量。
void main() {
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
}
*(a+1)就是a[1],*(ptr-1)就是a[4],执行结果是2,5
void getmemory(char *p) {
p = (char *) malloc(100);
strcpy(p, "hello world");
}
int main( ){
char *str = NULL;
getmemory(str);
printf("%s\n", str);
free(str);
return 0;
}
// 程序崩溃,getmemory中的malloc 不能返回动态内存, free()对str操作很危险
// 改正后
void getmemory(char **p) {
*p = (char *) malloc(100);
strcpy(*p, "hello world");
}
int main( ){
char *str = NULL;
getmemory(&str);
printf("%s\n", str);
free(str);
return 0;
}
void GetMemory(char *p){
p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
请问运行Test函数会有什么样的结果?
程序崩溃。因为GetMemory并不能传递动态内存,Test函数中的 str一直都是 NULL。strcpy(str, "hello world");将使程序崩溃。
void GetMemory2(char **p, int num) {
*p = (char *)malloc(num);
}
void Test(void) {
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
请问运行Test函数会有什么样的结果?
答:(1)能够输出hello
(2)内存泄漏
请问运行Test函数会有什么样的结果?
char *GetMemory(void) {
char p[] = "hello world";
return p;
}
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
可能是乱码。因为GetMemory返回的是指向“栈内存”的指针,该指针的地址不是 NULL,
但其原现的内容已经被清除,新内容不可知。
请问运行Test函数会有什么样的结果?
void Test(void) {
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) {
strcpy(str, "world");
printf(str);
}
}
篡改动态内存区的内容,后果难以预料,非常危险。因为free(str);之后,str成为野指针,if(str != NULL)语句不起作用。
野指针不是NULL指针,是指向被释放的或者访问受限内存指针。
造成原因:指针变量没有被初始化任何刚创建的指针不会自动成为NULL;
指针被 free 或 delete 之后,没有置NULL;
指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
free后应将指出赋值 NULL
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (named pipe) :有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量( semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字( socket ) : 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
(1)、函数调用时,先求出实参表达式的值,然后带入形参。而使用带参数的宏只是进行简单的字符替换。
(2)、函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
(3)、对函数中的实参和形参都要定义类型,二者的类型要求一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也是无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
(4)、调用函数只可得到一个返回值,而宏定义可以设法得到几个结果。
(5)、使用宏次数多时,宏展开后源程序长,因为每次展开一次都使程序增长,而函数调用不使源程序变长。
(6)、宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)
1)c++源于c,c++最重要的特性就是引入了面向对象机制,class关键字。
2)c++中,变量可以再任何地方声明;c中,局部变量只能在函数开头声明。
3)c++中,const型常量是编译时常量;c中,const常量只是只读的变量。
4)c++有&引用;c没有
5)c++的struct声明自动将结构类型名typedef;c中struct的名字只在结构标签名字空间中,不是作为一种类型出现
6)c语言的main函数可以递归调用;c++中则不可以
7)c中,void *可以隐式转换成其他指针类型;c++中要求现实转换,否则编译通不过
// 方法一:
void checkCpuMode(void){
int i = 0x12345678;
char *cp = (char *)&i;
if(*cp == 0x78)
printf("little endian");
else
printf("big endian\n");
}
// 方法二:
void checkCpuMode(void) {
int a = 0x12345678;
if((char)a == 0x12)
printf("big endian\n");
else
printf("little endian\n");
}
1、栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap): 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(static): 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放
4、文字常量区: 常量字符串就是放在这里的, 程序结束后由系统释放。
5、程序代码区: 存放函数体的二进制代码。
typedef unsigned int uint32_t;
typedef unsigned short int uint16_t;
typedef unsigned char uint8_t;
typedef struct {
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
uint8_t data[];
} test_t;
static test_t t;
static uint8_t cmd = 0x00;
static uint8_t dat = 0x00;
int main( ){
uint32_t packet[] = {0x0a0b0c0d, 0x05060708};
printf("cmd=0x%x dat=0x%x\n", cmd, dat);
cmd = 0xdd;
printf("cmd=0x%x dat=0x%x\n", cmd, dat);
*(uint32_t *)&((uint8_t * )&t)[0] = 0x01020304;
*(uint16_t *)&((uint8_t * )&t)[4] = 0xaabb;
printf("cmd=0x%x dat=0x%x\n", cmd, dat);
memcpy(&t, packet, sizeof(packet));
printf("cmd=0x%x dat=0x%x\n", cmd, dat);
return 0;
}
按执行顺序分析
行9 uint8_t data[]; 看似定义有问题,但是能编译过,因为编译器并没有给它分配空间,因此这个结构体占用4个字节。(.data虽然地址能访问,但是没有空间,因此不能直接使用,这么定义的用处后边给例子)
变量储存地址分析:cmd、dat初始化为0,因此和未初始化的变量空间在一起(指的PC端,MCU没有验证),也就是 t、cmd、dat三个地址连续
行18 因为cmd, dat没变,所以输出: cmd=0x0 dat=0x0
行19 cmd重新赋值0xdd,所以行20输出:cmd=0xdd dat=0x0
行21分解: (uint8_t * )&t 把t首地址强转成 uint8_t *型指针,此时 &t[0] 就是 uint8_t * 数组的首地址,再将数组首地址转成(uint32_t *),再用 (* + 地址) 的方式获取地址的内容。一顿折腾之后实际就是将结构体中 abcd四个char变量的内存组合成了一个 4个字节的 uint32_t 型变量。
小端模式:整数 0x01020304,小端模式储存的顺序是 0x(04 03 02 01),大端相反。已知MCU和PC是小端模式。
行21结果就是相当于 a,b,c,d = 0x(04,03,02,01),按照地址顺序依次赋值
行22 实际就是t.data指向的地址,因为这个地址不在结构体中。所以内存就会溢出到结构体下个变量,也就是cmd,因为赋值两个字节,所以溢出两个字节,导致cmd、dat都被覆盖掉了。
行22赋值相当于 cmd,dat = 0x(0xbb,0xaa),按照地址依次赋值,所以行23输出: cmd=0xbb dat=0xaa
行24 因为t占4个字节,packet是8个字节,因此直接memcpy导致溢出四个字节。按照内存顺序赋值 t.a, t.b, t.c, t.d, cmd, dat, x, x =0x(0d, 0c, 0b, 0a, 08, 07, 06, 05)因此cmd=08,dat=07,所以行25输出cmd=0x8 dat=0x7。(注意还多溢出两个字节呢,都不知道那里有啥,未知更危险)
这个举例很好的解释了内存溢出的现象,以及可怕的后果。希望弄懂以后不要出现这样的问题
补充扩展
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
PC上未初始化的变量默认是0,所以猜测初始化0与未初始化变量放在同一区域。如果初始赋值非0,则会储存在另一块区域。因此cmd和dat如果不赋值0,以上的分析结果则会变化,内存溢出后不会再覆盖掉cmd,dat,因为他们的内存不是连续的。
MCU的内存没有实验验证是否存在两块全局空间。单片机内存默认是0xFF,所以我猜测初始化为0xFF属于未初始化的空间,因此希望有人测试一下两全局变量一个=0,另一个=0xFF,看两个地址是否连续,连续说明只有一个储存空间,不连续非0xff代表初始化了。
结构体结尾的 data[]; 应用举例
typedef struct {
int a;
uint8_t data[]; //不确定用多少
} test_t;
int main( ){
test_t *t = (test_t *)malloc(104);
if(!t) return -1;
t->a = 1;
strcpy(t->data, "hello world");
printf("a=%d, data=%s\n",t->a, t->data);
free(t);
t = NULL;
return 0;
}
uint8_t data[]这种定义在数据内容已知的情况下,可以使用这种类型进行强制类型转换,因为不占空间,所以能够将数据内容按字段划分,例如用在协议包解析中,只是要慎重,确保所操控的空间存在。若是定义为uint8_t *data,则会多占用几个字节空间,强制类型转换有几个字节异常
行7申请空间,因为data没有空间且还是地址,因此正好相当于数组名,指向剩下的100个字节空间,这样一次性就申请了内存。
如果定义成 char * data,则需要将data单独赋值了,而且多占用4个字节。
for(int m = 5; m--; m < 0) {
printf("m=%d\n",m);
}
输出:4、3、2、1、0
伪代码:for(1; 2; 3) {4}
执行顺序 1,2,4,3。 3啥都没做,可以不看,2先判断然后--,到4那就是 5-1了
for(i = 0; i < 2, i < 3, i < 4; i++)
printf("%d \n",i);
输出:0,1,2,3。
printf("%d\n",5["abcdef"]);输出'f'的ACSII值,如果是4["abcdef"]则输出'e'的ACSII的值。
获取数组指定的数据方式 a[b]
根据以往经验,a为数组名,b是数组下标。简单易懂。
实际含义:a和b必须一个是地址,一个是常数。计算方式是 *(a + b)
没错,就是a[b] = *(a + b), a和b可以任意一个是地址,另一个是下标(常数)
扩展一下,地址的表示方式
> a, 数组名就是代表数组的首地址
> "abc":平时直接定义的字符串,实际只是字符串的首地址
> (int *) 0xXX.. :强制类型转换,将一个常数转换成地址
知道以上知识点,来输出字符串 "abc" 的第二个字符
char a[] = "abc";
printf("%c\n", a[1]);
printf("%c\n", 1[a]);
printf("%c\n", "abc"[1]);
printf("%c\n", 1["abc"]);
printf("%c\n", *(a + 1));
printf("%c\n", *("abc" + 1));
// 以上都输出 b
char *a 指向的是常量,内容不能更改
char a[] 表示的数组,内容可以更改
信号量、条件变量、互斥锁。
逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分。
例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制, Cpu不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
物理地址(Physical Address) 是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。在x86下,外设的i/o地址是独立的,即有专门的指令访问外设i/o,i/o地址就是你所说的“总线地址”。而“物理地址”就是ram地址。在arm中,i/o和ram统一编址,但linux为了统一各个平台,仍然保留这个概念,其实就是物理地址。
应用程序使用C函数库中的内存分配函数malloc()申请内存内核会为进程使用的代码和数据空间维护一个当前位置的值brk,这个值保存在每个进程的数据结构中。它指出了进程代码和数据(包括动态分配的数据空间)在进程地址空间中的末端位置。
当malloc()函数为程序分配内存时,它会通过系统调用brk()把程序要求新增的空间长度通知内核,内核代码从而可以根据malloc()所提供的信息来更新brk的值,但此时并不为新申请的空间映射物理内存页面。只有当程序寻址到某个不存在对应物理页面的地址时,内核才会进行相关物理内存页面的映射操作。当用户使用内存释放函数free()动态释放已申请的内存块时,c库中的内存管理函数就会把所释放的内存块标记为空闲,以备程序再次申请内存时使用。在这个过程中内核为该进程所分配的这个物理页面并不会被释放掉。只有当进程最终结束时内核才会全面收回已分配和映射到该进程地址空间范围内的所有物理内存页面。
#define rBANKCON0 (*(volatile unsigned long *)0x48000004)
rBankCON0 = 0x12;
sscanf("123456 ", "%4s", buf); // 1234
sscanf("123456 asdfga","%[^ ]", buf); // 123456
sscanf("123456aafsdfADDEFF", "%[1-9a-z]", buf); // 123456aafsdf
sscanf("123afsdfADJKLJ", "%[^A-Z]", buf); // 123afsdf
char* s = "AAA"; s[0]='B'; printf("%s",s);
"AAA"是字符串常量。s是指针,指向这个字符串常量,所以声明s的时候就有问题。
cosnt char* s="AAA";
然后又因为是常量,所以对是s[0]的赋值操作是不合法的。
位域的具体存储规则如下:
当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;
如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
对齐原因
平台原因(移植原因)
不是所有的硬件平台都能访问任意地址的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据的,否则抛出硬件异常。
性能原因
数据结构(尤其是栈),应该尽可能在自然边界上对齐,因为在访问为对齐的内存时,处理器需要访问两次,而对齐的内存处理器只需要访问一次。
结构体对齐规则
第一个成员在与结构体变量偏移量(offset)为0的地址处。
其他成员变量要对齐到对齐系数的整数倍的地址处。
使用 #pragma pack(n) (中的n就是对齐系数)设置对期数后,对齐系数 = n
未指定对齐数则:对齐系数 = 结构体中类型最大的字节数
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
向最大字节对齐
typedef struct {
char a;
double b;
} test_t; // double是8字节,所以对齐8+8=16
向最大字节对齐
typedef struct {
char a;
short b;
short c;
} test_t; // 因为最大类型两个字节,所以对齐2+2+2=6
向设置对齐系数
#pragma pack(1)
typedef struct {
char a;
double b;
} test_t; // 设置了对齐系数1,所以对齐1+8=9
// 待更新