三个一项目的有趣问题汇总
Near far 近远指针。*(char far *) *(int *)0x2000=0xf; *(char *)0x2000=’a’; ((char *)*(int far *)0x02000000) malloc(20)是开辟20内存空间的字节。
以下地址的初衷是指内存地址。否则,如果你真的想说,一切都必须有一个地址。只要它依赖于硬件,我们就可以看到、触摸和测试它。只有没有地址,才会有鬼魂。现在我只比较最常用的区别。 1>.变量有名字吗?变量有地址吗? 变量有变量名;变量有地址。 2>.寄存器有名字吗?寄存器有地址吗? 寄存器有寄存器的名字;寄存器没有地址(记住在这里arm单片机等区别,现在谈谈简单的C语言)。 3>.内存有名字吗?内存有地址吗? 内存没有名字;内存有内存地址。 4>.端口有名字吗?端口有地址吗? 就目前而言,地址似乎很常用;名字?有吗?
1>.没有变量可以用C语言编程吗? 可以,综合研究二得出其中一种额外选择:可以使用寄存器替换变量的使用。 2>.函数名和变量,这些不同于汇编的新名词,以什么熟悉的形式在C语言反汇编成汇编语言? 与函数相比,很容易想到子程序,包括子程序的标签、呼叫和返回,这与函数感觉有一定的近亲关系。如果是变量,存在的形式是唯一的形式。 3>. 用debug函数的偏移地址怎么找? 参考语句“printf(“%x\n”,main);最好举一反三。
1>.c0s.obj文件的作用 c0s.obj文件的作用:程序开始运行,相关初始化,然后调用main函数,返回后释放相关资源,恢复环境,然后返回程序。 2>.可以不用main函数编程吗? 是的。只需修改程序开始运行时相应的原始操作,并负责相关的初始化操作和调用main函数、相关资源关资源释放、环境恢复等工作文件。在本研究中使用c0s.obj文件,同理,以后要学会举一反三。
1>.头文件的作用和生成的四个阶段? 1.预处理(include在源程序中生成该库搜索定义头文件的内容.i文件) 2.编译(将.i文件翻译成生成.s汇编后缀语言程序) 3.汇编 (将.s将文件翻译成机器语言,生成可重定位的目标程序,包装生成.o文件) 4.连接(将各种代码和数据整合成单个可执行文件)
3>.编译程序的过程?
虽然最终会编译成机器代码,但有些程序不需要编译成汇编语言,c语言必须转化为汇编语言,因为C语言是汇编和编译。之后,我们将讨论汇编器的相关知识。
4>.Main与Main()的区别? 在用printf()打印main时间,打印出来main()偏移地址;打印main(),得到的数字是main()返回值。但是,这个返回值的选择涉及到之前函数保存返回值的问题,因为原来写的main()保存返回值ax当我们在寄存器中编写另一个函数时,函数在作用过程中没有修改ax寄存器值。我们得到的函数的返回值仍然是上次保存ax寄存器中的值。
5>.Sp、bp问题? 作用一:与SP联合使用,SP不能轻易改变,所以赋值BP,用BP实现我们的要求; 功能二:栈帧,相当于链表,bp,sp代表栈帧序列,为方便调用其他函数,返回后恢复调用前状态。
在这里,让我们回顾一下在这个过程中指出迷津和重要的句子printf(“%x\n”,main);格式是这样的。这里的函数名可以理解为变量,但区别是什么?联系呢?函数名和变量,这些不同于汇编的新名词,以什么熟悉的形式进行C语言反汇编? 与函数相比,很容易想到子程序,包括子程序的标号、调用、返回等,与函数感觉有一定的近亲关系。
1>.既然每个人都使用变量进行存储,为什么位置不同?或者为什么整体变量不需要堆栈?为什么不选择局部变量的数据段?
这是肯定的。首先,整体变量是让它从定义开始就可以在程序的任何位置使用。如果选择栈,栈就用完了pop其他地方怎么用?这就是全局变量不选栈; 局部变量呢?如果所有数据都直接放在数据段中,局部变量直到整个程序完成才会释放,那么如果我们定义的变量越来越多。。。一方面增加了内存的成本,另一方面,局部变量可以在任何地方修改,函数的独立性难以保证。 2>.在程序1中,全局变量n,是由“unsigned int n这句话的定义还是由这句话定义的main函数中的“n=这句话的定义? 函数外定义的变量应由前者定义,无论是否添加static,如果没有初始化,默认情况下始化为0。n=在0句之前打印n,是能够打印出它的值的。
搜索到的网上资料: 当启动连接程序时,它会寻找“未定义的外部函数”,也就是说,它将在每一个库文件中查找源代码文件中未定义的函数。当它找到未定义的外部函数时,它会引入包含函数定义的目标代码(obj)。不幸的是,如果这个函数是在一个包含其他函数定义的源文件中编译的,那么这些函数也含在你的可执行代码中,包含一些不必要的代码。因此,将库函数放入各自的源文件中是非常重要的——否则会浪费宝贵的程序空间。一些编译程序包含特殊的精明连接程序,它们可以找到不必要的函数并删除它们,使它们不再进入您的程序。
第一,内存对齐:节省时间,用空间换时间。资料自己推演。vc下面可以写代码规定内存对齐。(希望大家的研究要有深度)
同样的观察汇编代码可以发现(a n)等价于(&a[0] n),即a与&a[0]等价。这是因为a[0]是数组的第一项,所以它的地址是数组a的地址。 因此,数组名称是指针,[]操作是在指针值上添加偏移量,使其指向下一个存储单元的数据。
结合之前的学习汇编,数字是计算机中最常见的。这些数字在c语言中有不同的使命。数据、指令和地址。但这三者是相互关联的。啊,拍你的额头。这不是对应计算机组成中三条主要总线的数据总线、控制总线和地址总线吗?是的,这是非常有针对性的。
你能自己写一个功能吗?sizeof类似关键字的函数呢?如果你打算写,你会从什么角度开始? Sizeof这是为了字节数,字节是内存中的绝对单元长度。例如,我认为他就像尺子里的厘米单元。当然,它比他小。相应的位置可以填充。同样sizeof它似乎是一个测量尺子。它是一个短而精致的工具。cm-字节、mm-位等存在于内存中,但这并不意味着内存更接近尺子。sizeof与此相比。内存是一个容器,一个字节,一个位置,一个他们的世界,一个我们的世界,一个我们的容器。如果我想测量一段字节有多长,我会怎么测量?当我在现实生活中使用尺子时,我想到了这一点: 第一种:我会将尺子的0刻度对齐,测量字节段的头部或尾部,然后检查相应尾部或头部读出的尺子刻度是多少厘米,即字节、毫米或位置。 第二:有时尺子的0刻度可能会断裂,并被墨水污染。。。。。。这这个时候,我会随便找一个刻度,当然,在现实生活中一般选择整厘米,内存选择整个字节。然后写下刻度值,将尺子的刻度与要测量字节段的首端或尾端对齐,然后检查相应尾端或首端读取的尺子刻度,然后将两个值相减去绝对值。也就是多少厘米,也就是多少字节,多少毫米,也就是多少位置。。。。。。 如果你写程序,你会选择以上两个。但还有两个问题。 1>.sizeof是关键字,不是函数; 2>.sizeof能够正确计算结构体内存对齐。
在生成可执行文件的同时,链接器将生成一个工程map从而大致查看可执行文件中符号的内存布局,以及从哪里引入可执行文件。这通常对小型工程没有多大影响,因为代码太多了grep知道符号定义的位置。但对于一些大型项目或涉及更多的第三方库,或涉及更多的功能模块,你需要知道这些符号在哪里定义,或者如果引用符号但不知道函数定义,也需要找到哪个模块引入符号,为什么需要,所以需要一些通用(正式)方法来搜索这些符号,map文件是更好的切入点。
1.对于静态库和动态库占用内存资源的问题。比较两者的实现。 静态动态的关键区别(学长):加载时间不同。 还有其他异同:存储在静态库和动态库中的文件。静态库是多个*.obj一些学生发现动态库是可执行程序文件的集合。 动态库和静态库都是模块。
动态库在调用时加载,因为如果一运行就加载,动态库和静态库有什么区别? 库: 指由标准常用函数编译的文件,旨在提高常用函数的可重用性,减轻开发人员的负担。sdtio.h,math.h等等。库是C函数库冰山一角。 .静态库:指编译链接阶段将整个库复制到可执行文件 1.1优点:静态链接的程序不依赖外界库支持,具有良好的可移植性。 1.2缺点: 每次库更新都需要重新编译程序,即使更新很小或只是局部。 1.3缺点:每个静态链接的程序都有一份库文件,存储时增加了硬盘空间消耗,运行时则增加了内存消耗。 2.动态库:指直到运行时才将库链接到可执行程序 1.1优点: 动态链接方式的程序不需要包含库(编辑链接时节省时间),占用的空间小很多。 1.2优点: 运行时系统内存只需提供一个共享库给所有程序动态链接,内存消耗减少。 1.3缺点: 需要系统中动态库支持才可运行,可能有动态库不兼容问题
1.(学长的)指令“ c = *c + 1;”c为int类型,这条指令读写了几次内存? 1>.读c的内容; 2>.读*c的内容; *3>.读c的内容; 4>.写*c。 这里注意一下:*c在算过一次地址后要再算一遍。
1.(学长的)指令“c = &ch;”c为int*类型,ch为int类型,这条指令读写了几次内存? ch的地址并没有存放在内存中,所以访问&ch并不需要访问内存)写一次。 相关汇编代码: Mov ax,offset ch Mov [c],ax
3.(学长的)Push [bx] 运行这一句,访问了读指令、算地址、取一次数据、入栈4次指令。
对于强类型语言c语言中,当然不止是c语言,这些语言中都有很多的描述性语言,但是有时候当一件拥有大量陌生名词的学习事物充斥在初学者身边时,很多时候,其实是排斥的。而且有很多概念如果拿以前已经学习吸收的知识组装成新知识的理解框架之后,相比于直接抛出这个陌生的概念,学习效果可能也会很不错。“=”右边的表达式中,放在变量、数字之前的,目前来看大部分看作“强制类型转换”来理解,特例如果有可以举出来,这样对于大家理解指针函数、函数指针、指向指针的指针等较复杂概念与一般化的(int)、(char)等大家熟悉的强制类型转化性质进行统一和类比,更容易消化吸收。“b =(int) a”、“ b =(int ) a”、“ b =(int *) a”等等,甚至将“b =& a”,也就是“b =(&)a”,也归入“强制转换类型”,好像也说的通。还有“b =* a”,也就是“b =()a”,从这个角度,我自己觉得也是。然后如果p是一个指向字符的指针,“b = p[3]”,这个呢?好像有点不像了,可是之前说的等价式:“b = (p + 3)”,也就是“b = ()(p + 3)”,这样好像和前面我认为的同样属于强制类型转换应用的“b =()a”有点像呀,只不过这里(p+3),抽象化,两者都是指针,属性不变呀,所以,这种也算吗?嗯,有点不确定。学长,您说呢?还有其他的什么情况可以归到这里呢?还需要再想想,从长计议,好好总结。
1.打印char类型,要以无符号形式打印16进制。因为char类型是1个字节。打印%x是压入2个字节,这时要进行扩展,如果是以有符号形式打印的话,则扩展后为ff80,(假设打印80h),这样就不合适,因为前面的bit位都变成了1,而用无符号的话,扩展出来就成了0x0080,这样就很合适。
这样,也就涉及到快速数数的方面。我们对计算机存取数值的灵敏感知,其实没有必要非得看什么原码、反码、补码。想出这些只顾着理论性强的繁琐词汇,并想要学生们非得记住的,一定一定不是一个好主意,而很多学校老师们就很是这样,不仅喜欢教,还喜欢这样考,真的受不了。其实数一直是连续的,至少,是线性排列的。就好像最经典的一维坐标轴:
——————————————————————————————————>
对于无符号数,比如无符号整型,数值范围是(0~255)同样模仿一维坐标轴的形式列出来:
————————————————————————————————–> 增大 0 1 2 …… 127 128 129 …… 253 254 255 00h 01h 02h …… 7fh 80h 81h …… Fdh feh ffh 补码
其中从始至终,箭头方向,就是数值单调递增的方向。
对于有符号数,比如有符号整型,数值范围是(-128~127)同样模仿一维坐标轴的形式列出来:
——————————————>|———————————————————>增大 0 1 2 …… 127 | -128 -127 …… -3 -2 -1 00h 01h 02h …… 7fh | 80h 81h …… Fdh feh ffh 补码
其中从始至终,箭头方向,就是数值递增的方向,但是并不是单调递增,分为了两大部分:“0~127”和“-128~-1”。接着中间这个划分两部分,也就是划分“正负”的界限,就更熟悉了,此处的“127”和“-128”之间,也就是正好二分处。
接着考虑进来之前所说的数是线性排列的概念,思考学长提出的扩展成有符号四字节之后,“-128”又是怎样表示的,数值范围是(-32768~32767)同样模仿一维坐标轴:
——————————————>|——————————————————>增大 0 1 …… 32767 | -32768 -32767 …… -2 -1 0000h 0001h …… 7fffh | 8000h 80011h …… fffeh ffffh 补码
其中,同样-32768 到 -1也是线性增大的,而在之前的有符号整型,-128距离 -1是80h 到ffh 的距离,这样推算下来,-128扩展之后的表示,就是ff80h。
而在这个过程中,补码是一直线性连续的,这样就比较直观,好记忆理解了。
在不同的系统中, NULL并非总是和0等同,NULL仅仅代表空值,也就是指向一个不被使用的地址,在大多数系统中,都将0作为不被使用的地址,所以就有了类似这样的定义
#define NULL 0
但并非总是如此,也有些系统不将0地址作为NULL,而是用其他的地址,所以说,千万别将NULL和0等价起来,特别是在一些跨平台的代码中,这更是将给你带来灾难。
结果表明用函数指针p和用变量b强转,都实现了调用函数的效果。
这里最令我感兴趣的就是“a = ( (int ()(char,char) )b )(3,4);”这一句,因为他又让我想起了自己之前那段逻辑不清的表述,这里也是可以归类于其中的另一种“强制类型转换”,尽管书上没有教过我们这个道理,但它的确是存在的,这个程序恰恰验证了。“(int ()(char,char) )b”这里就是将整型变量b强制类型转换为“返回值为int,参数为char、char的函数的函数指针变量类型”。而之所以可以实现f()函数的功能,是因为在这之前b的值已经通过“b =(int)f”赋值为f()函数的首地址了。
看到这里,感觉之前自己认为的“(&)a”,这里a是一个普通变量,假设是整型吧,可以有另一种自己觉得更好的解释,本来a的地址与变量a以及变量a的值就是一体的,但是我们平时默认的就是正面的角色-》a的值,而(&)a自然就是反面角色了。但是正反从来都是一体的。看过不少武侠书籍之中,总会提到一种常见的机关门,按下机关,门就会翻转,主人公就能抵达暗室了。在这里,a的值,以及(&)a的值,就像是这扇会翻转的门的正反两面,而他们从来都不是分割的,因为他们都在a这扇门上。嘿嘿,感觉好像是在“翻牌子”;那么“*p”呢?其实我更喜欢“->”的标记来描述。这里p是一个普通的指针变量,假设是整型指针吧,它又有什么自己的解释呢?它更像个穿越门,穿越空间,不穿越时间。到了链表中就更好说了,通过p可以穿越到哪里,制定目的地的主人,就是我们了。多个穿越门可以到达同一个地方,一个穿越门可以修改到达的目的地,穿越了之后可以再次穿越。。。。。。我去 ,怎么有点哈利波特的感觉了?哈哈。希望以后现实中也可以有这种超级黑科技。应该,可以实现吧?
1.sizeof和strlen的区别
Sizeof是预处理时候得出的,strlen是库函数,运行的时候根据“\0”进行计算
粗浅的理解: Continue
#include <stdio.h>
void printWelcome(int len)
{
printf("welcome -- %d\n", len);
}
void printGoodbye(int len)
{
printf("goodbye -- %d\n", len);
}
void callback(int times, void (* print)(int))
{
int i;
for (i = 0; i < times; ++i)
{
print(i);
}
printf("\nwhat am i doing!\n\n");
}
void main(void)
{
callback(3, printWelcome);
callback(3, printGoodbye);
printWelcome(3);
printGoodbye(3);
}
这里,callback()是可以调用两个回调函数( printWelcome()、printGoodbye() )的函数,这两个回调函数“类型”是一样的。Callback()这里就像普通的整型变量一样用“这两个回调函数的类型”定义了一个形参变量给自己,另外还有一个变量,是在main()函数中可以提供的。因为打印时候的i本质上是受传入的参数times影响和控制的。 所以这个有什么用呢?网上对这个讲的真的是五花八门,看着都能睡着了。我就觉得他让我最感兴趣的就俩点儿: 1.把函数像变量来使用,通过使用函数的地址、函数指针等; 2.在最外面的函数,这里就是main(),通过在main()函数里面进行操作,也能很轻松按照我们想要的方式控制和影响最里面的函数的变化,从而实现最终结果变量化的功能。
表面上看,中间函数callback()可以通过函数指针形参来选择回调函数,最外层函数又可以通过times传参影响callback函数,但是这个times参数还会对回调函数产生影响,而且其实最终目的就是为了影响最内层的回调函数。 我该不会越讲越乱了吧。
变量:
变量是一种存放数据方式,与常量不同,它的内容是可以改变的。它可以分为全局变量和局部变量,它们的本质区别是存储的位置不同,全局变量是在内存中存储的,而局部变量是在栈段中存储的,这个差别导致了它们的一系列区别:全局变量存储的内存空间是没有内存对齐的情况的,而局部变量有;全局变量作为参数传递是直接用地址调用,而局部变量是入栈的方式;全局变量的生命周期是整个程序,而局部变量的生命周期是当前函数;全局变量的段地址在ds寄存器里,局部变量的段地址在ss寄存器里;全局变量定义是自动清零的,而局部变量定义时在栈中的空间还是原来的数据。比较特别的静态局部变量的存储位置和生命周期都和全局变量一样,只是静态局部变量只能在定义的函数中使用。
比较重要的变量类型有char、int、long、double和结构体,它们分别占的大小为1字节、2字节、4字节、8字节,结构体的大小是结构体中数据项之和。结构体也存在内存对齐的情况,结构体中各数据项存储位置是相邻的。结构体作为参数传递和返回比一般变量要复杂,一般变量都是直接入栈,而结构体必须创建一个临时变量,用块搬移函数将结构体的各数据项复制到临时变量里,在子函数里再将临时变量的值搬移到栈段里面,返回的原理也是相同的。
数组:
数组是利用一段连续的内存空间存放一系列相同类型的数据。一维数组是存储的数据按照线性的顺序来排列,二维数组是存储的数据按照类似围棋棋盘的顺序来排列,多维数组是存储的数据以多维的形式存储,我们可以通过当前一组不断向下查找到某一个元素。数组也可以根据存放的元素类型不同来分类:整型数组的元素是int型数据、指针型数组的元素是指针型数据、结构体数组的元素是结构体数据。数组中的元素是连续存放的。数组名相当于数组的首地址,也是数组第一个元素的地址,它的使用和指针有相似之处,如果p是一个指针,那么p[n]等同于*(p+n),即跳到下一个元素就相当于在当前地址上加上数组元素的类型大小。数组还有函数指针数组,存放的元素是函数指针,指向函数的指针,函数指针数组可以将要运行的程序以数据的形式写入并对函数进行调用。
函数:
函数是一段指令的集合。函数名相当于一个函数指针,存储函数的入口地址,程序由这个入口地址跳转到当前函数。函数的参数是局部变量,在调用该函数的函数中将参数压入栈中,在子函数里用bp寄存器找到参数的地址进行调用。函数可以有返回值,void函数没有返回值,函数的返回值一般是存储在寄存器中,如果返回值为结构体,则将结构体的内容传递到一个临时变量里。函数是一段数据,它同样存储在内存空间里,这样我们可以以数据的形式将一个函数写到内存中执行。
指针:
指针存储的数据是一个地址,我们可以通过“*”来取得指针存储的这个地址处的内容,通过“&”来取得一个内存空间的地址赋给指针。指针加减一个数并不是以它的值加减一个数字,而是加减它所指向的存储空间的数据类型的大小,即如果它指向的是int型数据,那么加1就是在当前地址上加上2个字节。我们可以将一个地址赋给一个整形变量,但我们不能对一个整形变量使用“*”取得它所存储的地址处的值,因为它不是一个指针,同时如果一个指针是一级指针,即定义成*p,那么只能用“*”对它取一次值,如果一个指针是二级指针,可以用“*”对它取两次值,总之,一个指针是几级指针,就可以对它取几次值。对于指针的使用我们一定要注意它和其他的变量的类型匹配问题,近指针占2个字节,存储偏移地址,远指针占4个字节,存储段地址加偏移地址。虽然指针的值的大小是固定的,但是指针指向的值的大小和指针的定义有关。指针在地址和内容之间建立了一条联系,这种联系是c语言最重要的基础,我们可以用它来实现多种数据结构。指针可以指向任意的数据类型,结构体指针指向的是一个结构体,它可以以->符号调用结构体的数据项。函数指针是指向一个函数入口的指针,当定义一个函数指针时要指明函数的类型和参数类型和个数,通过函数指针可以调用指定位置的函数。
1、重新总结针对12的关于共性和个性的问题
研究12中b.c程序的共性和个性的思考可以分成几个部分。 第一部分:通过函数指针数组func[n]中选择n的值来实现调用不同的函数,其中func[]是共性,通过变量n来调用不同的add(),sub()等函数,参数不同,这是个性; 第二部分:几个个性函数的接收参数都是两个,这是共性,但是对于两个参数的操作是不同的,这是个性; 第三部分:字符串code中的“+-*/”等都是可以增减变化的,而且各自的意义不同,这是个性,但是函数中对于code[n]数组元素的遍历及判断选择操作并不会因为code中元素内容的改变而更改,这是共性。
1.对数组和指针一定要有关键的区分点。
计算机为数组分配存储空间,但没有为数组变量分配空间; 计算机为指针是分配了空间的。
由于数组变量没有分配空间,而且它其实在编译的时候就被替换为具体的地址,所以它也无法像一个正经的指针那样指向任何地方。
但是应用方面我觉得最经典的就是,对于字符串常量了: 如果“char* f = ”hello”;”之后像“f[1] = f[0];”这样想要改变值的情况时,就会出现错误; 如果 “char f1[] = ”hello”;”,之后再改变,就安然无恙。 因为这里位于栈区的f想要改变位于常量区字符串常量,而位于栈区f1想要改变的是位于栈区的字符串常量的副本。
map文件的作用是,我们可以根据程序产生错误地址在map文件中找到相应的函数、变量地址。
比如程序有“除0错误”,在 Debug 方式下编译的话,运行时肯定会产生“非法操作”。好,让我们运行它,果然,“非法操作”对话框出现了,这时我们点击“详细信息”按钮,记录下产生崩溃的地址。比如:0x0040104a
再看看它的 MAP 文件:
仔细浏览 Rva+Base 这列,会发现第一个比崩溃地址 0x0040104a 大的函数地址是 0x00401070 ,所以说在 0x00401070 这个地址之前的那个入口就是产生崩溃的函数,也就是这行:
0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj
发生崩溃的函数就是 ?Crash@@YAXXZ ,也就是 Crash() 这个子函数。
不仅如此,根据*.map文件还可以找到具体的行号。不过得先生成代码行信息,
2.为什么要使用一个头结点?
这里我觉得就是一个统一的问题,比较一下按位计数法,最常见的10进制计数法,0的作用是什么呢?举个数字“2503”,0在这里的作用是什么?对的,脱口而出,就是“没有”,0在这里就是表示没有,但是,可不可以没有“没有”呢?那不就是“253”了吗?明显不对了。所以这里的0表示“没有”但是不可或缺,也就是起了“占位”的作用。同样的,乐谱中的休止符也是可以由此联想的。头结点就像0一样,它有具体的空间,它占了一个位。而在数学中10的幂次从0…5…100…依次增长,这里为什么 有0呢?因为还有…-100…-5…0 的次幂,如果我们把10的0次只作为1来特殊化处理,就好像人为地建立了一个隔断,所以0还有统一标准的作用。头结点同样如此,统一标准,就像是程序员的一枚小细作,程序员控制头结点,头结点像领头羊一样带领着屁股后面的一串结点。
3.void *memcpy( void *dest, const void *src, size_t count);
Memcpy函数的原型如上,即从指针src指向的空间拷贝count个字节到指针dest指向的空间里。
4.对void* 指针强制类型转换时的体会
就像是用基本类型(系统地默认模板)或自定义数据类型(自定义模板)对一块已经密密麻麻摆满的数据的文本进行了相应的数据类型的格式化排版,这也就解决了相应的打印输出问题。