10-20

《程序员面试宝典(第三版)》第12.2节问题9。经常出现这本书,有空可以看看。

在这篇文章中经常出现。

B代表二进制,D是十进制,H是16进制

汇编语句1

下面的这个参考:https://zhidao.baidu.com/question/331105575.html 

特殊数据寄存器:DPTR

mov DPTR,#4000H;  //表示将数据指针 指向 片外RAM地址为4000H的位置

接下来的就可以执行读取操作

movx A,@DPTR; //将 片外RAM地址为4000H位置 中的数据 读入到CPU的寄存器A(累加器A)中   

说明:dptr是特殊的数据寄存器
注意:如果硬件没有在CPU的外部连接RAM芯片,那么这样的操作将毫无意义。

汇编语句2  mov

mov,movx,movc三者的区别
(1)mov,用于 片内 数据存储器 中的数据传递指令中。
(2)movx,用于 片外 数据存储器 中的数据传递指令中。
且所有外部RAM中的数据必须通过累加器A读入;
并且所有需要送入RAM中的数据也必须通过A送入。
累加器A与片外RAM之间的数据传递指令:
movx A,@RI;
movx @RI,A;
movx A,@DPTR;
movx @DPTR,A;
(3)movc,用于 程序存储器 向累加器A 传送指令
语句:movc A,@A+DPTR
说明:此条指令引出一个新的寻址方法:变址寻址。
本指令是要在ROM的一个地址单元中找出数据,显然必须知道这个单元的地址,这个单元的地址是这样确定的:在执行本指令 立脚点DPTR中有一个数,累加器A中有一个数,执行指令时,将A和DPTR中的数累加成为,要查找的单元(数)的地址。查找到的结果被放在累加器A中,因此,本条指令执行前后,累加器A中的值不一定相同。

汇编语句3

摘抄自:http://www.eeworld.com.cn/mcu/article_2018030638051.html  (电子工程世界  记住了)

#include<reg52.h>
unsigned char x,y,z;
unsigned char xdata d;
void main()
{
	x=0xaa;
	y=0xbb;
	z=0xcc;
	d=0xdd;
	while(1)
	{
		x=d;
		y=d;
		z=d;
	}
}

汇编

main
mov x,#0AAH
mov y,#0BBH
mov z,#0CCH
mov DPTR,#d   //将DPTR指向d的地址
mov A,#0DDH   //这里我的理解是将0DDH放在寄存器A中
movx @DPTR,A  //将累加器A中的数据传送给外部RAM 特殊数据寄存器DPTR中
?C0001:
mov DPTR,#d
movx A,@DPTR
mov x,A
mov y,A
mov z,A
SJMP ?C0001
END

两个放在一起

#include<reg52.h>
unsigned char x,y,z;
unsigned char xdata d;
void main()
{
	x=0xaa;  //mov x,#0AAH
	y=0xbb;  //mov y,#0BBH
	z=0xcc;  //mov z,#0CCH
			 //mov DPTR,#d
			 //mov A,#0DDH
			 //movx @DPTR,A	
	d=0xdd;

	while(1)
	{
			 //mov DPTR,#d
			 //movx A,@DPTR	
			 //mov x,A		
		x=d; 
		y=d; //mov y,A
		z=d; //mov z,A
	}
}

从上面可以看到,变量d赋值给x,y,z的时候,只有x中的值是直接读取内存中d的值,而y=d,z=d则都是直接将寄存器A中的数值赋值给y,z。
若是在此过程中,变量d的值被改变(比如d是一个硬件寄存器),则y,z变量中得到的数据将是错误的。

访问内部寄存器要比访问RAM的速度要快。

 

当加了

#include<reg52.h>
unsigned char x,y,z;
volatile unsigned char xdata d;
void main()
{
	x=0xaa;  //mov x,#0AAH
	y=0xbb;  //mov y,#0BBH
	z=0xcc;  //mov z,#0CCH
			 //mov DPTR,#d
			 //mov A,#0DDH
			 //movx @DPTR,A	
	d=0xdd;			 
	while(1)
	{
			 //mov DPTR,#d
			 //movx A,@DPTR	
			 //mov x,A				 
		x=d; 
			 //mov A,@DPTR
			 //mov y,A
		y=d; 
			 //mov A,@DPTR
			 //mov z,A
		z=d; 
	}
}

 

一般来说,volatile关键字用在如下的几个地方。

(1)中断服务程序中修改的供其他程序检测的变量需要加volatile。

(2)多任务环境下,各任务间共享的标志应该加volatile。

(3)存储器映射的硬件寄存器通常也要加volatile说明, 因为每次对它的读写都可能有不同的意义。

10、c关键字

c的关键字共32个
 *数据类型关键字(12)
 char,short,int,long,float,double,unsigned,signed,union,enum,void,struct
 *控制语句关键字(12)
 if,else,switch,case,default,for,do,while,break,continue,goto,return
 *存储类关键字(5)
 auto,extern,register,static,const
 *其他关键字(3)
 sizeof,typedef,volatile

union关键字

union 关键字的用法与struct 的用法非常类似。
union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。例子如下:

union StateMachine
{
   char character;
   int number;
   char *str;
   double exp;
};

一个union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大长度是double 型态,所以StateMachine 的空间大小就是double 数据类型的大小——8。

union
 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址

union
{
    int a;
    long long b;
    unsigned char c[20];
} myUnion;

关于union占用的的内存大小:
1)大小足够容纳最宽的成员;2)大小能被其包含的所有基本数据类型的大小所整除。
所以对于sizeof(myUnion),结果为24,而不是20.
原文:https://blog.csdn.net/xaiojiang/article/details/51585072 
 

大端是正常顺序,小端是正常逻辑。 

#include <stdio.h>
int checkSystem()
{
	union check
	{
		int i;
		char ch;
	} c;
	c.i = 1;
	return (c.ch == 1);
}
int main()
{
	int aa;
	aa = 1;
	int bb=checkSystem();
	printf("%d",bb);
}
//bb=1表示的是小端存储

提示:C语言中的char占1个字节,而int占4字节,因此如果某个int变量被赋值为1,则大端模式内存布局(由低到高,下同)应该为0x00,0x00,0x00,0x01,小端模式为0x01,0x00,0x00,0x00。因此可以利用union结构的特性测试大小端。

变量i 占4 个字节,但只有一个字节的值为1,另外三个字节的值都为0。如果取出低地址上的值为0,毫无疑问,这是大端模式;如果取出低地址上的值为1,毫无疑问,这是小端模式。既然如此,我们完全可以利用union 类型数据的特点:所有成员的起始地址一致。

static关键字

的作用:面试问到的时候总是一知半解:

第一:static 方法:

(1)用static修饰的方法,调用的时候,直接是类+方法名称。(静态方法不需要依赖任何人对象进行访问)

(2)用static修饰的方法,里面不能访问非静态成员变量和非静态方法。

第二:static变量(静态变量)

静态变量和非静态变量的区别:体现在初始化的时候

静态成员变量仅有在类加载的时候会进行初始化,静态成员变量在内存中仅存在一个副本;

非静态成员在这个类创建对象的时候就被初始化,而且会有多个副本,个不影响。

有几个读程序题目,觉得可能不会用;

public class Test extends Base
{
    static
    {
        System.out.println("test static");
    }
     
    public Test()
    {
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) 
    {
        new Test();
    }
}
 
class Base
{  
    static
    {
        System.out.println("base static");
    }
     
    public Base()
    {
        System.out.println("base constructor");
    }
}

答案是

base static
test static
base constructor
test constructor

至于为什么是这个结果,我们先不讨论,先来想一下这段代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。

第二道

public class Test 
{
    Person person = new Person("Test");
    static
    {
        System.out.println("test static");
    }
     
    public Test() 
    {
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) 
    {
        new MyClass();
    }
}
 
class Person
{
    static
    {
        System.out.println("person static");
    }
    public Person(String str) 
    {
        System.out.println("person "+str);
    }
}
 
 
class MyClass extends Test 
{
    Person person = new Person("MyClass");
    static
    {
        System.out.println("myclass static");
    }
     
    public MyClass() 
    {
        System.out.println("myclass constructor");
    }
}

答案是

test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

类似地,我们还是来想一下这段代码的具体执行过程。首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。

第三道

public class Test 
{     
    static
    {
        System.out.println("test static 1");
    }
    public static void main(String[] args)
    {
         
    }  
    static
    {
        System.out.println("test static 2");
    }
}

答案是

test static 1
test static 2

虽然在main方法中没有任何语句,但是还是会输出,原因上面已经讲述过了。另外,static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。

这个static讲解也甚是清晰:https://www.cnblogs.com/heyonggang/p/3198431.html

转载至(甚是彻底):https://www.cnblogs.com/dolphin0520/p/3799052.html  

extern变量

extern是计算机语言中的一个关键字,可置于变量或者函数前,以表示变量或者函数的定义在别的文件中。提示编译器遇到此变量或函数时,在其它模块中寻找其定义,另外,extern也可用来进行链接指定。

C里面的堆和栈的划分

https://www.cnblogs.com/fenghuan/p/4778050.html

关键字volatile的用法

https://blog.csdn.net/yang1393214887/article/details/80840016  可以参考看看

例1

简单地说就是防止编译器对代码进行优化。比如如下程序:

XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;

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

精确地说就是,编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

例2

下面是volatile变量的几个例子:
1)并行设备的硬件寄存器(如:状态寄存器)
2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3)多线程应用中被几个任务共享的变量
这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。

再来一个例子

1.volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据, 即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳 定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用 volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

例3

告诉compiler不能做任何优化
比如要往某一地址送两指令:

int *ip =...; //设备地址
*ip = 1; //第一个指令
*ip = 2; //第二个指令

以上程序compiler可能做优化而成:

int *ip = ...;
*ip = 2;

结果第一个指令丢失。如果用volatile, compiler就不允许做任何的优化,从而保证程序的原意:

volatile int *ip = ...;
*ip = 1;
*ip = 2;

即使你要compiler做优化,它也不会把两次付值语句间化为一。它只能做其它的优化。

例4

用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能重复使用放在cache或寄存器中的备份。
例如:

volatile char a;
a=0;
while(!a)
{
    //do some things;
}
doother();

如果没有 volatile  doother()不会被执行。

例5-1

下面是使用volatile变量的几个场景:
1>中断服务程序中修改的供其它程序检测的变量需要加volatile;
例如:

static int i=0;
int main(void)
{
    ...
    while (1)
    {
        if (i) 
            dosomething();
    }
}
/* Interrupt service routine. */
void ISR_2(void)
{
    i=1;
}

程 序的本意是希望ISR_2中断产生时,在main函数中调用dosomething函数。但是,由于编译器判断在main函数里面没有修改过i,因此可能 只执行一次对从i(内存)到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将变量加 上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。

例5-2

2>多任务环境下各任务间共享的标志应该加volatile

例5-3


3>存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。
例如:
假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。

int *output = (unsigned int *)0xff800000;//定义一个IO端口;
int init(void)
{
    int i;
    for(i=0;i< 10;i++)
    {
        *output = i;
    }
}

经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译编译的代码结果相当于:

int init(void)
{
    *output = 9;
}

如 果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。

反之如果你不是对此端口反复写操作,而是反复读操 作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。

然而从代码角度看是没有任何问题的。这时候就该使用volatile通知 编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。

例5-4

volatile int *output=(volatile unsigned int *)0xff800000;//定义一个I/O端口

另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),

在1中可以通过关中断来实现,2中禁止任务调度,3中则只能依靠硬件的良好设计。

例6

几个问题
1)一个参数既可以是const还可以是volatile吗?
可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

解答:

volatile修饰符告诉complier变量值可以以任何不被程序明确指明的方式改变,最常见的例子就是外部端口的值,它的变化可以不用程序内的任何赋值语句就有可能改变的,这种变量就可以用volatile来修饰,complier不会优化掉它。

const修饰的变量在程序里面是不能改变的,但是可以被程序外的东西修改,就象上面说的外部端口的值,如果仅仅使用const,有可能complier会优化掉这些变量,加上volatile就万无一失了。

所以使用const volatile修饰变量  表明变量值只由外部条件改变,不会出现其它的副作用
例如

const volatile char *port  = (const volatile char *)0x00ff;

讲的甚得我心:原文:https://blog.csdn.net/yangzheng_yz/article/details/10226619 

2) 一个指针可以是volatile 吗?
可以,当一个中服务子程序修该一个指向一个名称为buffer的指针时。

buffer指向一个地址被A线程使用,B线程修改了buffer所指的地址,同时希望A线程使用新地址,设置volatile。

例7.volatile的本质:


1> 编译器的优化
在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。
2>volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人。

例8.下面的函数有什么错误:

int square(volatile int *ptr)
{
    return *ptr * *ptr;
}

该程序的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)
{
    int a;
    a = *ptr;
    return a * a;
}

注意:频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。

例9

#include "stdio.h"
int main(void)
{
    const char i = 1;
    char * j = (char *)&i;
    printf("%d,%d,%p,%p\n",i,*j,&i,j);//1,1
    *j = 2;
    printf("%d,%d,%p,%p\n",i,*j,&i,j);//1,2
    *j = 3;
    printf("%d,%d,%p,%p\n",i,*j,&i,j);//1,3
}

https://blog.csdn.net/race604/article/details/6725475  ----    char * j = (char *)&i;

《程序员面试宝典》中的一个错误(有时间看一下,搞清楚)

define关键字

1.定义常量,比如#define max 10此时就是一个文本的替换

2.宏定义#define后不需要加;(不加分号)

3.宏函数不能出现递归

4.比如 #define MAX(a,b) ((a) > (b) ? (a) : (b)) 此时就和定义了比大小的函数一样

注意:define是简单的文本替换,也就是说这并没有你想的那么好用比如说

例1

#include<stdio.h>
#define add(a,b) a*b
#define add1(a,b) (a)*(b) 
int main (void)
{
	printf ("%d\n",add(1,2));	//2
	printf ("%d\n",add(1+2,2+3));	//8
	printf ("%d\n",add1(1+2,2+3));	//15
} 

 就是因为它只是单纯的文本调换,所以要记得加括号例2

例2

#define MAX(a,b) ((a) > (b) ? (a) : (b))这一条也比较常用,总之要在描述作用时把括号带上

例3

另外有人会这么写

#define pin int *
pin a,b;

本意其实都想让a和b成为int型指针,但是实际上却变成了

int *a,b;

a是int型指针,b是int型变量。咋处理捏?这个时候typedef就出来了,它说我可以满 足define满足不了的要求,所以改成

typedef pin (int *)

原文:https://blog.csdn.net/kevinashen/article/details/80039433 

_interrupt关键字

1、ISR不能有返回值,必须用void;
2、ISR不能传递参数,必须用void;
3、ISR应该是短而高效的,所以不推荐在ISR中做浮点运算,应该只由中断发出消息或置位标志然后由应用层去处理其他工作;
4、ISR中不应该有重入和性能上的问题,因此使用pintf()函数也是不好的。

11、有符号数和无符号数相运算,会自动变成无符号数

int main(void) 
{  
	unsigned int a = 6;  
	int b = -20;  
	char c;  
	(a+b>6)?(c=1):(c=0); 
}
//则c=1,但a+b=-14;如果a为int类型则c=0。

原来有符号数和无符号数进行比较运算时(==,<,>,<=,>=),有符号数隐式转换成了无符号数(即底层的补码不变,但是此数从有符号数变成了无符号数),
 比如上面 (a+b)>6这个比较运算,a+b=-14,-14的补码为1111111111110010。此数进行比较运算时,
 被当成了无符号数,它远远大于6,所以得到上述结果。

12、设置某个变量的位

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

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

13、要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。

(建议用这种)

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;

一个较晦涩的方法是:

 *(int * const)(0x67a9) = 0xaa66;

14、中断服务例程ISR

中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。
 具代表性的是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius) 
{
    double area = PI * radius * radius;
    printf("/nArea = %f", area);
    return area;
}

1)ISR 不能返回一个值(都应该为void类型)。如果你不懂这个,那么你不会被雇用的。

2)ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。

3)在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额外的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。另外中断服务程序是运行在内核态的(linux),内核通常是不支持浮点运算的。

 ISR尽量不要使用浮点数处理程序,浮点数的处理程序一般来说是不可重入的,而且是消耗大量CPU时间的!!

 printf函数一般也是不可重入的,UART属于低速设备,printf函数同样面临大量消耗CPU时间的问题!

参考:https://blog.csdn.net/weibo1230123/article/details/82632233

了解中断服务程序

与每类I/O设备相关的进程都有一个靠近内存底部的地址,称作中断向量。 它包括中断服务程序的入口地址。

中央处理器正在处理内部数据时,外界发生了紧急情况,要求CPU暂停当前的工作转去处理这个紧急事件。处理完毕后,再回到原来被中断的地址,继续原来的工作,这样的过程称为中断。

中断处理过程:

(1)保护被中断进程现场。为了在中断处理结束后能够使进程准确地返回到中断点,系统必须保存当前处理机程序状态字PSW和程序计数器PC等的值。(压栈)

(2)分析中断原因,转去执行相应的中断处理程序。在多个中断请求同时发生时,处理优先级最高的中断源发出的中断请求。

(3)恢复被中断进程的现场,CPU继续执行原来被中断的进程。(出栈)

栈的相关知识

参考:https://blog.csdn.net/u014421422/article/details/79471396

main() 
{
    //执行test前
    print(int p1,int p2);
    //执行test后
}
分析下上面程序的调用原理,假设执行print前esp=Q:
push p2; //函数参数p2入栈,esp=Q-4H
push p1; //函数参数p1入栈,esp=Q-8H
call print; //函数返回地址入栈,esp=Q-0CH
//现在进入print内,做些准备工作:
push ebp; //保护先前ebp指针,ebp入栈,esp=Q-10H
mov ebp,esp; //设置ebp等于当前的esp
// 此时,ebp+0CH=Q-4H,即p2的位置
// 同样,ebp+08H=Q-8H,即p1的位置
// 下面是print内的一些操作:
//sub esp,20H; //设置长度为10H大小的局部变量空间,esp=Q-20H(觉得理解是错误的)
sub esp,20H; //设置长度为20H大小的局部变量空间,esp=Q-30H(esp向低地址方向移动20个字节)
// ... ...// 一系列操作// ... ...
add esp,20H; //释放局部变量空间,esp=Q-10H
pop ebp; //出栈,恢复原先的ebp的值,esp=Q-0CH
ret 8; //ret返回,弹出先前入栈的返回地址,esp=Q-08H,后面加操作数8H为平衡堆栈
// 之后,弹出函数参数,esp=Q,恢复执行print函数前的堆栈;

再来一个栗子,忘记从哪里复制的了。

#include <stdio.h>
#include <malloc.h>
int main(void)
{
    /*在栈上分配*/
    int  i1=0;
    int  i2=0;
    int  i3=0;
    int  i4=0;
    printf("栈:向下\n");
    printf("i1=0x%08x\n",&i1);
    printf("i2=0x%08x\n",&i2);
    printf("i3=0x%08x\n",&i3);
    printf("i4=0x%08x\n\n",&i4);
     printf("--------------------\n\n");
    /*在堆上分配*/
    char  *p1 = (char *)malloc(4);
    char  *p2 = (char *)malloc(4);
    char  *p3 = (char *)malloc(4);
    char  *p4 = (char *)malloc(4);
    printf("p1=0x%08x\n",p1);
    printf("p2=0x%08x\n",p2);
    printf("p3=0x%08x\n",p3);
    printf("p4=0x%08x\n",p4);
    printf("堆:向上\n\n");
    /*释放堆内存*/
    free(p1);
    p1=NULL;
    free(p2);
    p2=NULL;
    free(p3);
    p3=NULL;
    free(p4);
    p4=NULL;
    return 0;
}

该示例代码主要演示了在内存分配中的堆和栈的区别,其运行结果为:
栈:向下
i1=0x0060fefc
i2=0x0060fef8
i3=0x0060fef4
i4=0x0060fef0
--------------------
p1=0x00bd14e0
p2=0x00bd3148
p3=0x00bd3158
p4=0x00bd3168
堆:向上


从运行结果中不难发现,内存中的栈区主要用于分配局部变量空间,处于相对较高的地址,其栈地址是向下增长的;而堆区则主要用于分配程序员申请的内存空间,堆地址是向上增长的。

堆与栈的区别(面试):https://blog.csdn.net/bxyill/article/details/8681140

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"优化成一个地方。 
} 

15、16位与32位的区别

评价下面的代码片断:

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;

下面参考:https://blog.csdn.net/weibo1230123/article/details/82822479?utm_source=blogxgwz2

unsigned int compzero = 0xFFFF;  即1111 1111 1111 1111只写了2个字节(16位),16位的才符合 。

32位的可以写: 

unsigned int compzero = 0xFFFFFFFF; 即1111 1111 1111 1111 1111 1111 1111 1111 (32位)

但unsigned int compzero = ~0;更安全,不管有多少位,直接取反,把所有的0都变成1了。

16、malloc(0)

第一个栗子

main()
{
     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';不出现段错误。

解析参考:https://www.cnblogs.com/wuyuegb2312/p/3219659.html

 解析:......故意把0值传给了函数malloc,得到了一个合法的指针,这就是上面的代码,该代码的输出是"Got a valid pointer"。

这个“解析”根本就没有解析嘛。好在查资料很方便,《C语言参考手册》上说“如果请求的长度为0,则标准C语言函数返回一个null指针或不能用于访问对象的非null指针。”或者你也可以直接在linux里man malloc来查阅手册:

void *malloc(size_t size);

...

malloc() allocates size bytes and returns a pointer to the allocated memory. The memory is not cleared. If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().

  可见,原题的if是为了鉴别malloc()返回值是NULL,还是一个唯一的待释放指针;而不是“解析”中的必然是非NULL的“合法指针”,因此输出也不是确定的,尽管我用gcc和clang多次编译运行,输出都是"Got a valid pointer"。

综上所述: (char *)malloc(0)) 返回的应该是一个唯一的待释放的指针

第二个例子

char *ptr;
if(int pp = (strlen(ptr=(char *)malloc(0))) == 0)
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

第一段程序的分析和上面一样,如果不幸返回了一个唯一的待释放非NULL指针,行为不可预测;只不过这个if判断写的有些繁琐:注意到“==”优先级高于"=",而赋值语句的值是其左值。

  此时malloc(0)返回了一个可用于free()释放的唯一指针(非NULL),而且将它传给strlen(),返回值为0,这样看来,它用'\0'进行填充的(即内容是NULL而非指针指向NULL)。但这一点并没有在man中提到,个人猜测是和实现有关的。
  除此以外,顺便考察了strlen((char*)NULL)的行为:会导致段错误。

第三个例子

char *ptr;
if(int pp = (sizeof(ptr=(char *)malloc(0))) == 4)
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

第二段程序呢,sizeof()里写了一大堆,其实只是计算了sizeof(char *),在32位机上结果当然是4,而sizeof()里面的malloc()根本没有执行。和前面两段代码不同,关键点不在malloc而是sizeof。 

这个小伙子还是小姑娘知道的太多了:https://www.cnblogs.com/wuyuegb2312/p/3219659.html

17、typedef关键字===同义字

Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。
    例如,思考一下下面的例子:

#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:

dPS p1,p2;
tPS p3,p4;

第一个扩展为

struct s * p1, p2;

上面的代码定义p1为一个指向结构的指针p2为一个实际的结构,这也许不是你想要的。

第二个例子正确地定义了p3 和p4 两个指针

下方结构体的相关知识:https://blog.csdn.net/u014421520/article/details/80641046

结构体的声明和定义:

方法1:一个结构体变量XiaoMing。这个变量包含两个成员,一个是char型的名字,一个是float型的分数

struct 
{ 
    char name[20]; 
    float score; 
}XiaoMing;

方法2:给这个结构体加上一个“_Student”的标签后,我们在需要定义的结构体变量的地方可以这样来定义:struct _Student XiaoMing; 。 这样我们就一目了然,原来这个结构体定义的是关于一个学生。

struct _Student
{
	char name[20];
	float score;
};
struct _Student XiaoMing;

方法3:用typedef 来给结构体起一个别名

typedef struct _Student
{
	char name[20];
	float score;
} Student;
Student XiaoMing;

18、a++与++a

int a = 5, b = 7, c;
c = a+++b;

就近原则,与编译器无关,很容易记的。
相当于(a++)+b;
但是太多+号就不行了,
比如a+++++b,虽然一看就知道意思是(a++)+(++b);但是编译就通不过,出错信息大概是:迷失在加号空间?
 则c=12。猜想a=6。

19、???有待确认

int main()
{
    int j=2;
    int i=1;
    if(i = 1) 
        j=3;
    if(i = 2) 
        j=5;
    printf("%d",j);
} 

输出为5;如果再加上if(i=3)j=6;则输出6。

20、宏定义是在预编译阶段被处理的。

#include <stdio.h>
void fun()
{
    #define M 100
}
int main()
{
    printf("%d\n",M,N);
    #define N 10
    return 0;
}

经过预处理之后

#include <stdio.h>
void fun()
{
	//之前定义的宏代码在预处理的时候被删除
}
int main()
{
	//这里只替换了M为100,N依旧是原来的样子
	//说明宏只对定义在后面的语句有效,与定义在哪里无关,及时函数不被调用,也可以使用宏
	printf("%d\n",100,N);
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值