信息来源于网络 一、内存大话题 1.0、内存就是程序的立足之地,体现内存重要性。 1.1、内存理解: 内存物理看是有很多个Bank(就是行列阵式的存储芯片),每一个Bank的列就是位宽 ,每一行就是Words,则存储单元数量=行数(words)×列数(位宽)×Bank的数量;通常也用M×W的方式来表示芯片的容量(或者说是芯片的规格/组织结构)。 M是以位宽为单位的总容量,单位是兆 ,W代表位宽, 单位是bit。计算出来的芯片容量也是以bit为单位,但用户可以采用除以8的方法换算为字节(Byte)。比如8M×8,这是一个8bit位宽芯片,有8M个存储单元,总容量是64Mbit(8MB)。 1.4、char/int/short/long/float/double型:放在内存的长度和解析作用。(int *)0,使0地址指向一个int型。又比如0000111010101可以解析成int型也可以解析成float型。
1.6、栈的理解: (1) 运行时自动分配&自动回收:栈是自动管理的,程序员不需要手工干预。方便简单。(表现在汇编代码,编译时,会自动编译成汇编码实现函数调用完立即改变栈顶) (2) 反复使用:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。(硬件上有个寄存器,用来存放栈的栈顶地址,栈是有大小的空间) (3) 脏内存:栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。 (4) 临时性:(函数不能返回栈变量的指针,因为这个空间是临时的) (5) 栈会溢出:因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完。栈的操作(怎么出栈怎么入栈)是由具体硬件来干预,程序员只要明白原理就可以了,但是要给相应的栈寄存器赋值。当调用函数时,变量会自动放在栈中(入栈)当函数调用完后,栈会自动出栈. ( 6 ) 栈的 '发展'有四种情况,满增栈,满减栈,空增栈,空减栈,至于是那种要根据编译器决定,而s5pv21 是满减栈。 (1)操作系统堆管理器管理:堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配。 (2)大块内存:堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放。 (3)脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的。 (4)临时性:堆内存只在malloc和free之间属于我这个进程,而可以访问。在malloc之前和free之后都不能再访问,否则会有不可预料的后果。 (5)程序手动申请&释放:手工意思是需要写代码去申请malloc和释放free。(记住:不要把申请的地址给搞丢了, 不然自己用不了,也释放不了) 申请一段内存,可以是:
原型:
//指针函数 size_t是宏定义int 都是便于可移植性 ,返回一个内存地址,void *可以看出,希望申请的内存用来存放什么就强制类型什么。 calloc( 10,sizeof ( int ) ); 原型:void *calloc(size_t nmemb, size_t size);// nmemb个单元,每个单元size字节void *realloc(void *ptr, size_t size);// 改变原来申请的空间的大小的ptr是原来申请内存的指针,size是想要重新申请内存的大小使用就是*(p+1)=12 ; *(P+3)=110; 申请失败返回NULL,申请成功返回一个地址,申请之后一定要检验(NULL!=p)用完一定要 free ( p ) ;释放后不是不能用,是不应该使用了。可以给它“洗盘子‘,p=NULL; 其实申请的内存并不能真正改变大小,原理是先重新申请一段内存,然后把原来申请的内存上的内容复制到新的内存上,然后释放掉原来的内存,返回新的指针。 (6) 在申请内存时,malloc(0)其实也是成功的,因为系统规定少于一定数目的大小,都申请规定的大小,如在win32系统下申请少于32字节的地址,最后申请到的空间是32字节,在朱老师视频中申请少于16字节的地址,最后申请到的是16字节,至于规定多少字节,由具体的系统而言。 (1)代码段:存放代码二进制、常量(char *p='linux',则”linux“存放在代码段,是不可更改的) (2) 数据段: 存放非0全局变量、静态局部变量(局部只属于函数的,不是整个程序的) (3) bss : 存放为0的全局变量/为0的静态局部变量、存放未初始化全局变量/静态局部变量 注意:const int a=9; 有两种存放方式:第一种确实存放在代码段,让a不能修改,第二种是仍然存放在数据段中,让编译器来判断,如果有改变的代码就会报错。 至于那种,是不确定的,像单片机就属于第一种。 《2》内存实际上被划分了两大区域,一个是系统区域,另一个是用户区域,而每一个区域又被划分成了几个小区域,有堆,栈,代码区,.bbs区,数据区(存放的是非0全局变量)。 《3》对于有操作系统而言, 当我们在执行a.out可执行文件时,执行这个文件的那套程序会帮我们把杂段清掉,然后把相应的段加载到内存对应的段。对于裸机程序而言,我们是使用一套工具将a.elf的可执行程序给清掉了所有段的符号信息,把纯净的二进制做成.bin格式的烧录文件。所以我们加载到内存的程序是连续的,也就是说代码段和数据段、.bbs段都是连续的。当然,栈空间是我们自己设置的。而且在裸机中我们不能使用malloc函数,因为我们使用的只是编译器、连接器工具没有集成库函数,没有定义堆空间区。 《4》大总结多程序运行情况: 在Linux系统中运行cdw1.out时,运行这个文件的那套程序会帮我们把相应的段加载到内存对应的段。然后操作系统会把下载到内存的具体物理地址与每条命令(32位)的链接地址映射到TTB中(一段内存空间),当我们又运行cdw2.out时,同样也像cdw1.out一样加载进去,并映射到TTB表中。而且这两个.out文件默认都是链接0地址(逻辑),当cpu发出一个虚拟地址(Linux中程序逻辑地址)通过TTB查找的物理地址是不一样的。所以对于每一个程序而言,它独占4G的内存空间,看不到其他程序。 二、位操作 2.1 ~(0u)是全1; 位与:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0 三、指针—精髓 3.1 printf('%p \n'); 其中%p表示输出一个指针,就是指针变量(其存放的那个地址),可以理解为输出一个地址。
3.5、修饰词:const (修饰变量为常量,应该理解为不应该去变它,当作常量,而并非永远不能改变,当然要看具体运行环境,在gcc,const 这种就可以采用指针方式修改,但是在在VC6.6++中就不可以修改):其虽然是当作常数,但是仍然存放在数据段中,用指针仍然可以改变值。 第一种:const int *p; 3.6、 数组 int a[2]; 其中a是指首元素的首地址,&a是整个数组的收地址(数组指针,其这个指针指向一个数组),他们的值是一样的,但意义不一样,可以参照 int a; int *p=&a; 来理解。数组和指针天生姻缘在于数组名; int a[3]; int* p=a;是可以的,但是 int *p=&a;就会报错,尽管他们的值是一样的,但意义不一样,所以是不允许的,除非强制类型转换。在访问时是a[0],其实编译器会把它变成*(a+0)的方式,只是用a[0]看起来更方便,封装了一下而已,实质还是指针。 与strlen( )要有所区别,他是测字符串实际长度的,不包括‘\0‘,如果给strlen传的参数不是一个字符串,则它会一直去找,直到 找到第一个 ‘\0’,然后再计算其长度。 如 char a[]='chen'; char *p=a; 则strlen(p)=4; 实参的大小会丢失,所以往往会传一个int num 大小进去。 四、C语言复杂表达式 4.1、在表达式中,要看符号的优先级和结合性。 函数指针必须和原函数的类型一样。 4.5 函数指针其实就是为了做结构体内嵌指针的,这样就构成了高级语言中的类。再一个就是上述4.4中p=&max;也是可以的,它和p=max,值和意义都是一样的,这个和数组有所区别,数组的a和&a的值虽然一样,但是意义完全不一样。int a[4];a有两层意思,第一层是数组名,&a表示整个数组的地址,第二层表示首元素的首地址。 '\r'是回车,'\n'是换行,前者使光标到行首,后者使光标下移一格,通常敲一个回车键,即是回车,又是换行(\r\n)。Unix中每行结尾只有“<换行>,即“\n”;Windows中每行结尾是“<换行><回车>”,即“\r\n”;Mac中每行结尾是“<回车>”。scanf('');里面不要加\n符。
4.9、typedef int *intType; const intType p,其意思是指针p为const; 所以只需要修改int32_t就可以了,我可以让他typedef short int32_t;这样我只更改一次,其余的都改了,做到一改全改。 这样收纳后就变成了一个新的数组a[2],每一个格子存放的是它收纳的第一个元素的地址,如a[0]存放的是第一行第一列元素的地址,“a”[1]存放的是第二行第一列的地址; 再与一维数组相联系,一维数组名即表示数组名又表示数组第一个元素的地址,所以a[2][5]中的a表示“a'[2]数组第一个元素的地址;那么再把p=a;层层推递,(p+i)表示指向第几行的地址,*(p+i)表示取第几行的值(而这个值存放的是第几行一列元素的首地址),*(p+i)+j 表示指向第几行第几列的地址,最后在引用这个地址,*(*(p+i)+j)就表示第几行第几列的值了。 一重指针----------->一维数组 五、数组&字符串&结构体&共用体&枚举(5.6?) 5.1、c语言中定义一个字符串: char a[6]={'l','i','n','u','x','\0'}; '\0'的字符编码为0就是NULL;也就是说内存中遇到0,翻译成字符是就是'\0',或这是NULL;
5.2、 sizeof(a)=6是运算符,其意思是所占空间大小,包括字符串后面的‘\0',strlen(a)=5是一个函数,其意思是字符串的长度。strlen( p);其中p只有是字符指针变量才有意义,它的形参是数组变量是没有意义的,因为strlen是根据什么时候遇到 '\0',才结束测试字符串的长度,就算数组不初始化也是有长度的。 char *p='linux'; sizeof(p)永远等于4,因为p是指针变量,存的是地址。所以总结:sizeof()是拿来测数组的大小,strlen()是拿来测试字符串的长度。
则s1.a =12;实质就是int *p=(int *) &s1;*p=12 首先a是int 型,所以是强制类型 int * ,其次是就是算地址,然后强制类型,地址应该是int 型然后加减,不然的话,系统s1.b=12.2;实质就是double *p= (double *) ((int)&s1+4),*p=12.2; 不知道是以int 型加减还是以float型加减,还是以char型加减,所以 应当 (int)&s1; 而且因为地址是s1.c=c;实质就是 char *p=(char *) ((int)&s1+12); *p=c; 4字节的,所以必须是int型。 (1)猜测如果是32位系统,那么编译器默认是4字节对齐,64位系统,那么编译器默认是8字节对齐,因为32位或64位一次性访问效率是最高的。 (2) <1>结构体首地址对齐(编译器自身帮我们保证,会给它分配一个对齐的地址,因为结构体自身已经对齐了,那么第一个变量也就自然对齐,所以我们才可以想象成第一个变量从0地址存放); <2>结构体内部的各个变量要对齐。 <3>整个结构体要对齐,因为定义结构体变量s1时,再定义变量s2时,如果s1没有对齐,就坑了s2,所以也要保证整个结构体对齐。 无论是按照几字节对齐,我们都可以联想到内存实际的安排。1字节对齐那么不管int float double 类型,在每4个格子的内存挨着存放。2字节对齐,也是一样的想法,举一个列子,如果第一个变量是char 型,第二个变量是int型,那么0地址存放char型,1地址空着,2地址存放int型地址部分,3地址存放int型地址部分,然后上排最右4、5地址存放int型高址部分。4字节对齐,如果第一个变量是char型,第二个变量是int型,那么0地址存放char型,1,2,3地址空着,从4地址开始存放int,最后给变量分配完内存空间后,必须要保证整个结构体对齐,下一个结构体的存放起始地址是n字节对齐整数倍,如是4字节对齐,那么最后short算成4字节 以保证整个结构体对齐。 整个结构体对齐,如2字节对齐(2的整数倍),只要是0、2、4地址就行了,如果是4字节对齐(4的整数倍),就必须是0、4地址。8字节对齐(8的整数倍) (3)猜测4字节/8字节其实是针对int型/double型的,比如0地址是char型,那么4字节对齐,int型、float型就必须从4地址开始存放,那么8字节对齐,int型就必须从4地址存放,double型就必须从8地址开始存放.小于几字节对齐的那些,如char型和short型只要能按照规则存放就行了。 (4)对齐命令:<1>需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。(不建议使用) 如:s1占5个字节,s2占8字节(默认)
(结构体本身以及变量) 对齐规则:2字节对齐(2的整数倍),只要是0、2、4地址就行了,4字节对齐(4的整数倍),就必须是0、4地址,
<2>gcc推荐的对齐指令__attribute__((packed)) __attribute__((aligned(n))),在VC中就不行,没有定义这个命令 (1)__attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。 (2)__attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐,内部元素按照默认对齐方式) 例子:
5.5、offsetof宏:#define offsetof( TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER) (1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。 (2)offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。 (3)学习思路:第一步先学会用offsetof宏,第二步再去理解这个宏的实现原理。 (TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。 ((TYPE *)0)->MEMBER(TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素,然后对这个元素取地址,又因为改地址是从0地址开始算的,所以这个地址就是相对起始地址的偏移量。 (2)typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。 (3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了, 再把这个地址强制类型转换为type *即可。 测试:有用共用体 union 和指针方式来测试,基本思路是让 int a=1; 看低地址 char 是0还是1 ;变量都是从地址开始存放,只是变量的高地址和低地址先存放谁不确定。 不能用位与来测,因为存放和读取都是按照某一个方式来的,结果永远都是一样的。int a=1; char b=(char)a;这种方式不可以测试,因为不管大小端,它都以变量a的低地址部分赋给b;
5.9、枚举类型(int型): 这样写默认从第一个常量开始是0,1,2,3,4.........也可以自己赋值,但是每一个值是不一样的,否则逻辑上出错。
六、C语言宏定义与预处理、函数和函数库(看博客strcyp原函数) 6.1、源码.c->(预处理)->预处理过的 .i 文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。 (1)预处理器帮编译器做一些编译前的杂事。如:(1)#include(#include <>和#include ''的区别) (2)注释 (3)#if #elif #endif#ifdef (4)宏定义 备注: gcc中只预处理不编译的方法 -o生成可执行文件名 -c只编译不链接 -E 只预处理不编译 -I ( 是大i,不是L )编译时从某个路径下寻找头文件 . /当前 (1)gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。 (2)gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。 (3)链接器:链接的时候是把目标文件(二进制)通过有序的排列组合起来,如 star.s main.c led.c 这三个源文件,分别被编译成三个目标文件 ,每个目标文件有很多函数集合。链接的时候会根据运行思路把这些杂乱的函数给排列组合起来,不是把目标文件简单的排列组合。 (4)当生成可执行程序之后,这个可执行程序里面有很多符号信息,有个符号表,里面的符号与一个地址相对应,如 函数名max对应一个地址,虽然这个程序有符号信息,但是为什么还是可以执行呢?因为如windows的exe程序,有专门的一套程序来执行这个.exe 文件,就好比压缩文件,就有一套 “好压”的软件,然后去压缩(执行).rar .zip的文件,而这套程序就把这些符号信息给过滤掉,然后得到纯净的二进制代码,最后把他们加载到内存中去。 (5) debug版本就是有符号信息,而Release版本就是纯净版本的。可用strip工具: strip是把可执行程序中的符号信息给拿掉,以节省空间。(Debug版本和Release版本)objcopy:由可执行程序生成可烧录的镜像bin文件。 6.2、预处理: <1>头文件有”“是本目录去找,找不到就去库头文件找,和< > 只到库头文件去找,库头文件可以自己制作,用 -I ( 是大i,不是L )参数去寻找路径。 头文件在预处理时,会把文件的内容原封不动的赋值到 c 文件里面。 <2>注释:在预处理时,把注释全部拿掉。 注意:#define zf 1 再判断 #undef zf 2 时,也是通过的。其意思是有没有定义过zf. <3>条件编译:当作一个配置开关 #define NUM 表示定义了NUM,则执行下一条语句,且NUM用空格替代,而且预处理会删掉条件编译,留下正确的执行语句。 <4>宏定义:#define cdw 1 在预处理阶段,会替代那些宏,可以多重替代宏;也可以表示多个语句,如 #define cdw printf('cdw\n') ; printf('zf\n'); cdw;这条语句会直接展开 (1)整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。 (2)函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。 (3)函数的目的就是实现模块化编程。说白了就是为了提供程序的可移植性。 <1>函数书写的一般原则: 第一:遵循一定格式。函数的返回类型、函数名(男女厕所)、参数列表(太多用结构体)等。 第二:一个函数只做一件事:函数不能太长也不宜太短(一个屏幕的大小),原则是一个函数只做一件事情。 第三:传参不宜过多:在ARM体系下,传参不宜超过4个。如果传参确实需要多则考构体打包考虑。 第四:尽量少碰全局变量:函数最好用传参返回值来和外部交换数据,不要用全局变量。 <2> 之所以函数能被调用,根本实质是在编译时,检查到了该函数的声明,不是因为函数定义了(当然也要定义才行,只是不是本质)。 { int he(int n) 注意: if(n<1) if(3==n||4==n) 栈溢出:递归函数会不停的耗费栈空间 { {
6.7、函数库:<1>静态链接库其实就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫静态链接库文件)。 商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用;客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在连接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段链接进去形成最终的可执行程序。 <2>动态链接库本身不将库函数的代码段链接入可执行程序,只是做个 也就是在运行时,会把库函数代码放入内存中,然后多个程序要用到库函数时,就从这段内存去找,而静态链接对于多程序就是重复使用库函数,比较占内存。 (1) gcc中编译链接程序默认是使用动态库的,要想静态链接需要显式用-static来强制静态链接。 (2) 库函数的使用需要注意3点:第一,包含相应的头文件;第二,调用库函数时注意函数原型;第三,有些库函数链接时需要额外用-lxxx来指定链接;第四,如果是动态库,要注意-L指定动态库的地址。 <2> 数学函数:math.h 需要加 -lm 就是告诉链接器到libm中去查找用到的函数。 C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。 链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。 (1)第一步:自己制作静态链接库,首先使用gcc -c只编译不连接,生成.o文件;然后使用ar工具进行打包成.a归档文件库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件 (2)第二步:使用静态链接库,把.a和.h都放在我引用的文件夹下,然后在.c文件中包含库的.h,然后直接使用库函数。 备注: <1>.a 文件,前缀一定要加lib ,如 libzf.a ; 链接属性 -l(小L),表示库名,属性-L表示库的路径。所以:gcc cdw.c -o cdw -lzf -L ./include -I(大i) ./include <2> 头文件“ ”表示外部自定义,如果没加路径属性,默认当前路径找,如果在其他文件夹下,必须用 -I(大i) 路径。用<>表示的头文件一种是在编译器下的库文件找,第二种是自己定义的库文件找,但是要定义其路径。 <3> 在makefile文件中用到gcc/arm-gcc 那么在shell中就用相应的编译器 gcc/arm-gcc . <4> nm ./include/libmax.a 查看max库的信息,有哪些 .o 文件 .o文件有哪些函数。 举例:makefile: arm-gcc aston.c -o aston.o -c <1>动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a . <2>第一步:创建一个动态链接库。 gcc aston.c -o aston.o -c -fPIC (-fPIC表示设置位置无关码) 第二步:使用自己创建的共享库。gcc cdw.c -o cdw -lmax.so -L ./动态链接库 -I ./动态链接库 第三步:上述编译成功了,但是在 ./cdw 执行时会报错,原因是采用动态链接,在可执行文件只是做了一个标记,标记是使用了哪个函数库的哪个函数。 并没有将库函数加载到源文件中,所以可执行文件很小,在执行时,需要立即从系统里面找到使用到的函数库,然后加载到内存中,在linux系统中 第四步:将动态库 libmax.so 复制到 /usr/lib 下面,但是如果以后所有的库都这样放的话,会越来越臃肿,导致运行速度变慢(一个一个查找);或者是新添加一个环境变量 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include (将库 libmax.so 复制到这个路径下面)这样就可以运行了。 <3>使用 ldd 命令判断一个可执行文件是否能运行成功; ldd cdw 七、存储类&作用域&生命周期&链接属性 7.1、概念词:存储类(栈、堆、数据区、.bss段、.text段) 问题:虚拟地址技术? 解决:后期在Linux应用/网络编程会讲。 OS下和裸机下C程序加载执行的差异? 解决:在arm裸机第十六部分有介绍。 <1> auto 自动的(一个用法:修饰局部变量,在定义变量时可以省略) 【外链接:与第二个c文件链接】【内链接:只与本c文件链接】【无连接:就是无链接】 <2> static 静态的(有两个用法,第一个是修饰局部变量,意思是当作全局变量,存放在数据区,作用域只是定义的那个函数范围,生命周期和整个程序一样,属于无连接 第二个是修改全局变量/函数,意思是这个全局变量/函数只在当前c文件有效,其他c文件是不能使用它的,属于内链接,普通全局变量属于外连接) <4>extern (一个用法,修饰全局变量,表示该文件要使用的这个变量,在另外一个c文件中已经定义了,其一个声明的作用,不能初始化) <5>volatile (一个用法,修饰变量,表示对这个变量的语句不要去优化) (1) volatile的字面意思:可变的、易变的。C语言中volatile用来修饰一个变量,表示这个变量可以被编译器之外的东西改变。编译器之内的意思是变量的值的改变是代码的作用,编译器之外的改变就是这个改变不是代码造成的,或者不是当前代码造成的,编译器在编译当前代码时无法预知。譬如在中断处理程序isr中更改了这个变量的值,譬如多线程中在别的线程更改了这个变量的值,譬如硬件自动更改了这个变量的值(一般这个变量是存在寄存器,或许当其他进程要用到这个寄存器时,就把这个寄存器的变量给改变了,同时也就改变了这个变量) (2) 以上说的三种情况(中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量)都是编译器在编译时无法预知的更改,此时应用使用volatile告诉编译器这个变量属于这种(可变的、易变的)情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。 (3) 编译器的优化在一般情况下非常好,可以帮助提升程序效率。但是在特殊情况(volatile)下,变量会被编译器想象之外的力量所改变,此时如果编译器没有意识到而去优化则就会造成优化错误,优化错误就会带来执行时错误。 而且这种错误很难被发现。 (4) volatile是程序员意识到需要volatile然后在定义变量时加上volatile,如果你遇到了应该加volatile的情况而没有加程序可能会被错误的优化。如果在不应该加volatile而加了的情况程序不会出错只是会降低效率。 所以我们对于volatile的态度应该是:正确区分,该加的时候加不该加的时候不加,如果不能确定该不该加为了保险起见就加上。 举例子1: int a=3 ,b,c; 那么编译器会优化成 c=b=a=3; 如果此时遇到上述三种情况,突然改变了a的值,那么,对于没有优化前是对的,但是对于优化后,那么c仍然是3,就会出错。 所以当我们程序员知道这个变量会发生改变时,就应该加 volatile,提示编译器不要帮我们做优化。
这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
<6> restrict (1)c99中才支持的,所以很多延续c89的编译器是不支持restrict关键字,gcc支持的。 (2)restrict 作用是让编译器对这个变量做一些优化,让它提高效率。下面的网站有列子。 (3)restrict只用来修饰指针,不能修饰普通变量,它表示只能该指针才能修改它的内容。如用memcpy时,两个内存存在重叠的现象。 (4)https://blog./uid-22197900-id-359209.html (这个网站里面有详细的例子) (5)memcpy和memmove的区别 void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n);这样它可以优化成memmove原理的方式(当存在重叠时,先复制到一段内存空间,然后再把它复制到目的空间) (1)全局变量起名字一般是 g_a; (2)名字加前缀表示 <2>为什么要分为数据段和.bbs段?因为当加载到内存重定位时,如果这些数据(包括0)一个一个的复制,会降低效率,为0的变量,直接清内存就可以了,这样提高效率。 <3>在头文件声明全局变量时, extern int a; 声明函数就是 void int max(int a, int b); <4>写程序尽量避免使用全局变量,尤其是非static类型的全局变量。能确定不会被其他文件引用的全局变量一定要static修饰。(因为全局变量占内存的时间是最长的,要看你的变量是不是需要这么长的时间,这样可以节约内存空) 八、一些杂散但值得讨论的问题 8.1、操作系统的理解: <1>它是一个管理阶级者,管理所有资源,负责调配优化等操作。这样想象,就像裸机一样的话,要实现LED闪烁的进程、串口传输的进程、蜂鸣器等这些,他们都要抢占一些资源,这个时候没有操作系统,就乱成一锅粥,当有了OS的时候,它就专门负责资源的调配,让各个任务都能很好的实施,起一个决策者的作用。 <2>如果我们要做一个产品,软件系统到底应该是裸机还是基于操作系统呢?本质上取决于产品本身的复杂度。只有极简单的功能、使用极简单的CPU(譬如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于操作系统来开发。 <3>操作系统负责管理和资源调配,应用程序负责具体的直接劳动,他们之间的接口就是API函数。当应用程序需要使用系统资源(譬如内存、譬如CPU、譬如硬件操作)时就通过API向操作系统发出申请,然后操作系统响应申请帮助应用程序执行功能。 <1>从内核的角度看,需要考虑提供哪些有用的API函数,并不需要关注它们如何被使用。故编程时用API函数会感觉到不好用,没有做优化。系统只负责做出一个可以用的API,没有考虑到用户使用的方便性。所以c库函数对API做了一些优化,让用户使用库函数更容易达到我们想要的目的。 <2>库函数实质还是用的API,或者调用了一个API,也或者调用了更多的API,只不过是做了更多的优化。比如 库函数fopen ,而API是open. <3>有的库函数没有用到API,比如strcpy函数(复制字符串)和atoi函数(转换ASCII为整数),因为它们并不需要向内核请求任何服务。 (1)不同操作系统API是不同的,但是都能完成所有的任务,只是完成一个任务所调用的API不同。 (2)库函数在不同操作系统下也不同,但是相似性要更高一些。这是人为的,因为人下意识想要屏蔽不同操作系统的差异,因此在封装API成库函数的时候,尽量使用了同一套接口,所以封装出来的库函数挺像的。 但是还是有差异,所以在一个操作系统上写的应用程序不可能直接在另一个操作系统上面编译运行。于是乎就有个可移植性出来了。 (3)跨操作系统可移植平台,譬如QT、譬如Java语言。 <1>main()函数的写法:int main(int argc,char **argv) ; int main(int argc ,char *argv[ ] ); 这两种写法是一样的。二重指针等同于指针数组。 <2>不管是主函数还是功能函数,它都应该有一个返回值,而主函数的返回值是给调用的那个人的/main函数从某种角度来讲代表了我当前这个程序,或者说代表了整个程序。main函数的开始意味着整个程序开始执行, <3>inux下一个新程序执行的本质 (1)表面来看,linux中在命令行中去./xx执行一个可执行程序 (2)我们还可以通过shell脚本来调用执行一个程序 (3)我们还可以在程序中去调用执行一个程序(fork exec) 总结:我们有多种方法都可以执行一个程序,但是本质上是相同的。linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程中去执行直到结束。 新进程是被谁开启?在linux中进程都是被它的父进程fork出来的。 分析:命令行本身就是一个进程,在命令行底下去./xx执行一个程序,其实这个新程序是作为命令行进程的一个字进程去执行的。 总之一句话:一个程序被它的父进程所调用。 结论:main函数返回给调用这个函数的父进程。父进程要这个返回值干嘛?父进程调用子进程来执行一个任务,然后字进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。 (0表示执行成功,负数表示失败,正规的要求返回失败的原因,返回-1表示什么,返回-2又表示什么,然后父进程好做相应的处理) (4) main函数的返回值应当是int 型。父进程要求是int型的,如果写成 float 型,则返回就为0,这样是要出错的。 如: ./a.out boy girl ;则 argv=3; argc[0]='./a.out'; argc[1]='boy'; argc[2]='girl' ; printf('%s\n',argc[0]); 解释:argv表示传了多少个参数,argc实质是存的一个指针,也就是一个地址,只是没有一个被绑定的变量名而已。这个地址指向一个字符串,一般字符串都和指针相关。所以可以称之为字符串数组,每一个都存了一个字符串。 在程序内部如果要使用argc,那么一定要先检验argv,先检验个数,然后使用。 (1)编程语言分2种:强类型语言和弱类型语言。强类型语言中所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法;弱类型语言中没有类型的概念,所有变量全都是一个类型(一般都是字符串的),程序在用的时候再根据需要来处理变量。就如:makefile、html语言。 (2)C语言就是典型的强类型语言,C语言中所有的变量都有明确的类型。因为C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法。 (3)void类型的正确的含义是:不知道类型,不确定类型,还没确定类型、未知类型,但是将来一定有类型。 (4)void *a;(编译器可以通过)定义了一个void类型的变量,含义就是说a是一个指针,而且a肯定有确定的类型,只是目前我还不知道a的类型,还不确定,所以标记为void。 void “修饰”的是指针,因为指针就是内存地址,它不知道指向的另一个变量是哪一种类型,而变量一定是确定的,void a;就是错误的。 NULL在C/C++中的标准定义 (1)NULL不是C语言关键字,本质上是一个宏定义,其保护指针的作用,不要让他乱开枪。 (2)NULL的标准定义:
解释:C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。 NULL的本质解析:NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000,代表内存的0地址。(void *)0这个整体表达式表示一个指针,这个指针变量本身占4字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际是0地址开始的一段内存)。如 char *p=NULL; 因为0地址本身就不是我们来访问的,所以 *p时是不可访问的。在程序运行的逻辑上就不会出错。 正规写:
(1)'\0'是一个转义字符,他对应的ASCII编码值是0,内存值是0,一个char空间。 (2)'0'是一个字符,他对应的ASCII编码值是48,内存值是int型48,一个char空间。 (3)0是一个数字,没有ASCll编码, 内存值是int型0,一个int空间。 (4)NULL是一个表达式,是强制类型转换为void *类型的0,内存值是0(内存地址),一个int空间。 <1>“小动作”:高级语言在运算中允许我们大跨度的运算。意思就是低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。譬如C语言中一个变量i要加1,在C中只需要i++即可,看起来只有一句代码。但实际上翻译到汇编阶段需要3步才能完成:第1步从内存中读取i到寄存器, 第2步对寄存器中的i进行加1,第3步将加1后的i写回内存中的i。 <2> float a=12.3; int b=(int)a; (int )a 就是匿名变量;先找一个内存空间,里面存(int)a; 然后把这个值赋值给b;最后匿名值销毁。 float a; int b=10; a=b/3; 左边是3.00000; 右边是3;其中有个匿名变量,先找一个内存空间,里面存 b/3; 然后把它再转换成float型,再赋值个a;最后匿名值销毁。
应用级:
解 释:<1>...表示变参,提示编译器不要对参数个数斤斤计较,不要报错; 其实完全可以把 ...换成 cdw 也是可以的,只是非要装一下而已。 <2> _FILE_ 和 _FUNCTION_和 _LINE_ 都是c库函数的宏定义,分别表示要输出的这句话属于哪个文件名、属于哪个函数名、在第几行。 <3> 在 fprintf(stderr,'cdw');其中stderr是c库函数中宏定义了的,这是VC6.0找到的 #define stderr (&_iob[2]) ;也就是说stderr是一个被宏定义了的指针,它是标准错误输出流对象(stderr),输出到屏幕上。 fprintf是C/C++中的一个格式化写—库函数,位于头文件中,其作用是格式化输出到一个流/文件中;(重点是流/文件) printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出(重点是标准输出设备,有时候输出的不一定显示在屏幕上,只是编译器规定显示到屏幕上而已。) 总结:也就是说printf()其实不是输出屏幕上的,只是这个标准输出设备中,编译器规定显示到屏幕上而已,而真正输出到屏幕是fprintf(stderr,'cdw');其中stderr就是输出到屏幕上的流。它也可以 fprintf( FILE *stream, const char *format,...),这个就是输出到文件流中的。 比如:一般情况下,你这两个语句运行的结果是相同的,没有区别,只有一下情况才有区别:运行你的程序的时候,命令行上把输出结果进行的转向,比如使用下面的命令把你的程序a.c运行的结果转向到记事本文件a.txt:a.exe > a.txt 在这样的情况,如果使用printf输出错误信息,会保存到a.txt文件里面,如果使用fprintf输出错误,会显示在屏幕上。 <4>上面中的__VA_ARGS__也是一个宏定义,表示预处理时实际的参数。 则允许的效果是 DBG(debug.c, main( ), 14): tiaoshi.
九、链表&状态机与多线程(9.9.1?具体链表实现留到驱动模块讲解)
<2> 定义一个node *head=NULL,想要改变head值通过函数传参是不行的,因为head是一个地址,传参过去,只是赋值给另一个指针而已,只能修改它指向的数据,而本身(地址)是不能修改的,所以要先返回修改好的地址,然后再head=node_add(head) <3>定义、用指针都应该想到NULL,如 node *head=NULL; node *new=(node *)mallo(sizeof(node));if(NULL!=new){ } <4>在结构体想定义一个字符串时不要用 char *name; 应该要用char name[10];如果使用第一种的话,编译通过,执行错误,因为为name赋值时就要放在代码段中,而代码段已确定了,所以报段错误。 头节点可以想成数组的0位置,其余节点当作从1开始,所以有头节点的长度可以定义为就是含有真实数据节点的个数。 <缺点>单链表各个节点之间只由一个指针单向链接,这样实现有一些局限性。局限性主要体现在单链表只能经由指针单向移动(一旦指针移动过某个节点就无法再回来,如果要再次操作这个节点除非从头指针开始再次遍历一次),因此单链表的某些操作就比较麻烦(算法比较有局限)。 回忆之前单链表的所有操作(插入、删除节点、 遍历、从单链表中取某个节点的数·····),因为单链表的单向移动性导致了不少麻烦。 总结:单链表的单向移动性导致我们在操作单链表时,当前节点只能向后移动不能向前移动,因此不自由,不利于解决更复杂的算法。 9.9.1、 内核链表的思想是:<1>先做一个纯链表,没有数据区,只有节点的链接方法。然后要做一个链表出来,直接用纯链表然后稍加修改就可以了。 <2>内核中__的方法不要轻易使用,是给内核用的,否则容易出错,用户应该使用没有__的方法;如:__list_add() ; list_add(); <3>内核默认是头指针+头节点的思路。 <4>其实质就是操作里面内嵌 纯链表这个变量,再利用controf宏来访问结构体的数据。详情见驱动。 <1>概念:其实就是有多种状态切换,如电脑的休眠、关机、睡眠。 <2>类型:(1)Moore型状态机特点是:输出只与当前状态有关(与输入信号无关)。相对简单,考虑状态机的下一个状态时只需要考虑它的当前状态就行了。 (2)Mealy型状态机的特点是:输出不只和当前状态有关,还与输入信号有关。状态机接收到一个输入信号需要跳转到下一个状态时,状态机综合考虑2个条件(当前状态、输入值)后才决定跳转到哪个状态。 <3>理解:要时时刻刻检查当前状态,用循环+switch(状态);然后根据输入信号,进行更多的处理,转换到其他状态。 十、增补知识 10.1、一个字节可以表示8位字符,字符真的有256种,128~255表示西欧字符,是不常见,详情见文档。 字符相加的时候,会自动转成 int型加。 首先在内存中,char与unsigned char没有什么不同,都是一个字节,唯一的区别是,char的最高位为符号位,因此char能表示-127~127,unsigned char没有符号位,因此能表示0~255,这个好理解,8个bit,最多256种情况,因此无论如何都能表示256个数字。 点赞▼ |
|
来自: 西北望msm66g9f > 《培训》