嵌入式技术
64Mbit(8MB)
。1.2、c语言中其实没有 bool 类型:以 0 表示假,非0 表示真,则在内存存储是以 int 型存放的。如果想要表示真假,可以用int/char
型做替换,在 c++ 中就有bool x=true/false
;1.3、内存对齐:内存对齐(提高访问效率速度,编译器一般默认是4字节对齐)1.4、char/int/short/long/float/doubl
e型:放在内存的长度和解析作用。(int *)0
,使0地址指向一个int型。又比如0000111010101
可以解析成int型也可以解析成 float 型。1.5、Linux 内核是面向对象的,而 c语言是面向过程的,但可以用结构体内嵌指针变成面向对象。如struct student{ int age; //变量 int lenth; //将相当于一个类,有变量有函数 char *name; void (*eat)(void); //函数指针 }1.6、栈的理解:(1) 运行时自动分配 & 自动回收:栈是自动管理的,程序员不需要手工干预。方便简单。(表现在汇编代码,编译时,会自动编译成汇编码实现函数调用完立即改变栈顶)(2) 反复使用:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。(硬件上有个寄存器,用来存放栈的栈顶地址,栈是有大小的空间)(3) 脏内存:栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。(4) 临时性:(函数不能返回栈变量的指针,因为这个空间是临时的)(5) 栈会溢出:因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完。栈的操作(怎么出栈怎么入栈)是由具体硬件来干预,程序员只要明白原理就可以了,但是要给相应的栈寄存器赋值。当调用函数时,变量会自动放在栈中(入栈)当函数调用完后,栈会自动出栈.( 6 ) 栈的 "发展"有四种情况,满增栈,满减栈,空增栈,空减栈,至于是那种要根据编译器决定,而 s5pv21 是满减栈。1.7、堆的理解:(1)操作系统堆管理器管理:堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配。(2)大块内存:堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放。(3)脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的。(4)临时性:堆内存只在 malloc 和 free 之间属于我这个进程,而可以访问。在 malloc 之前和 free 之后都不能再访问,否则会有不可预料的后果。(5)程序手动申请&释放:手工意思是需要写代码去申请 malloc 和释放 free。(记住:不要把申请的地址给搞丢了, 不然自己用不了,也释放不了)申请一段内存,可以是:malloc(10*sizeof ( int ) ); 原型:
void *malloc(size_t size); //指针函数
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.8、内存里的数据:(1)代码段:存放代码二进制、常量(char *p="linux"
,则 ”linux“ 存放在代码段,是不可更改的)(2) 数据段: 存放非 0 全局变量、静态局部变量(局部只属于函数的,不是整个程序的)(3) bss : 存放为0的全局变量 / 为 0 的静态局部变量、存放未初始化全局变量 / 静态局部变量注意:const int a=9;
有两种存放方式:第一种确实存放在代码段,让a不能修改,第二种是仍然存放在数据段中,让编译器来判断,如果有改变的代码就会报错。至于那种,是不确定的,像单片机就属于第一种。1.9、(1) 一个源文件实际上是以段为单位编译成连接成可执行文件(a .out );这个可执行文件总的说是分为数据段,代码段,自定义段,数据段还可以细分成 .bbs 段。而杂段会在执行的时候拿掉。所以 a.out 分为杂段,数据段(存放的是非 0 全局变量).bbs 段,代码段。(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 的内存空间,看不到其他程序。搜索公众号C语言中文社区,后台回复“资源”,免费获取200G编程资料。#define SET_NTH_BIT(x, n) (x | ((1U)<<(n-1))) #define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))
printf("%p ");
其中 %p 表示输出一个指针,就是指针变量(其存放的那个地址),可以理解为输出一个地址。3.2 int* p1, p2 ;
等同于 int *p1;
int p2
; int *p="Linux"
,其不能改变 *P ,因为 ”linux" 是一个常数。3.3 ( 代码规范性 )在定义指针时,同时赋值为 NULL,在用指针时,先判断它是不是 NULL 。尤其是在 malloc 申请内存后,free(p) ;则一定要让 p=NULL3.4 C/C++ 中对 NULL 的理解:#ifdef _cplusplus// 定义这个符号就表示当前是C++环境
#define NULL 0;// 在C++中NULL就是0 #else #define NULL (void *) 0;// 在C中NULL是强制类型转换为void *的0 #endif 3.5、修饰词:const (修饰变量为常量,应该理解为不应该去变它,当作常量,而并非永远不能改变,当然要看具体运行环境,在 gcc,const 这种就可以采用指针方式修改,但是在在 VC6.6++ 中就不可以修改):其虽然是当作常数,但是仍然存放在数据段中,用指针仍然可以改变值。
//第一种:const int *p; //第二种:int const *p; //第三种:int * const p; //第四种:const int * const 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] 看起来更方便,封装了一下而已,实质还是指针。3.7、 siziof() 是一个运算符,测试所占内存空间,如 int a[100] ;``sizeof(a)=400;
与strlen( )要有所区别,他是测字符串实际长度的,不包括 ‘‘ ,如果给 strlen 传的参数不是一个字符串,则它会一直去找,直到 找到第一个 ‘’,然后再计算其长度。如 char a[]="chen";
char *p=a;
则strlen(p)=4;
3.8、 当数组作为一个形参时,其实参是一个数组名(也可以是指针,其本质就是指针),意义是首元素的首地址,则传过去只影响形参的第一个元素。形参数组的地址被实参数组地址所绑定;实参的大小会丢失,所以往往会传一个 int num 大小进去。3.9、 结构体做为形参时,应尽量用指针/地址方式来传,因为结构体变量有时会占很大,效率很低。3.10、 int *p=&u;
p 存放的是变量u的地址,而 &p 的意思就是变量 p 本身的地址。3.11、当要传参的个数比较多时,我们可以打包成一个结构体,传参的个数越多,其开销就更大.3.12 一个函数作用其实就是输入输出,参数可以作为输入,返回可以作为输出,但是当要返回多个输出时,这时候就不够用了,所以常常返回值用来判断程序又没有出错,而参数就是当作输入输出的,输入时可以加 const 表示它没必要去修改,而输出都是指针,因为要改变它的值,只能采用地址传递这种方式。比如:char *strcpy(char *dest,const char *src)
int *p;
是定义的一指针变量 p,而int ( *p)[4]
;也是一个指针变量p;也可以这样想:凡是遇到(*p)什么的判断他是指针后,就可以说他是指针变量,包括函数指针。4.4、一个函数 int max(int a ,int b);
则他的函数指针是 int ( *p ) (int ,int );
其意思就是定义这个类型的函数指针变量 p; p=max 是赋值,引用是p();则相当于 max()调用这个函数。函数指针必须和原函数的类型一样。4.5 函数指针其实就是为了做结构体内嵌指针的,这样就构成了高级语言中的类。再一个就是上述 4.4 中 p=&max;也是可以的,它和 p=max,值和意义都是一样的,这个和数组有所区别,数组的 a 和 &a 的值虽然一样,但是意义完全不一样。int a[4];
a 有两层意思,第一层是数组名,&a 表示整个数组的地址,第二层表示首元素的首地址。4.6 int (*p[4])(int ,int)
其意思是函数指针数组,一个 4 长度的数组,里面存了4个函数指针。4.7 printf 在做输出时,其机制是缓冲行来输出,即当遇到一个 后再打印出来,即使再多 printf,没有遇到 ,都不是一个一个打印。' '是回车,' '是换行,前者使光标到行首,后者使光标下移一格,通常敲一个回车键,即是回车,又是换行( )。Unix中每行结尾只有“<换行>,即“ ”;Windows中每行结尾是“<换行><回车>”,即“ ”;Mac中每行结尾是“<回车>”。scanf("");里面不要加 符。4.8 在一个c文件中,有时候会多次引入一个.h文件,所以在写.h文件时,要写#ifndef _FINE_ #define _FINE_ XXXXXXXX XXXXXXXXXXX #endif 4.9、
typedef int *int Type;
const intType p
,其意思是指针 p 为 const;4.9.1 对于 typedef 的定义:如typedef const int cdw;
可以这样理解,typedef 就是给一个类型区别名的,那么系统会自动识别该类型,如果typedef const int char
则就报错。4.9.2 在开发中经常会typedef int int32_t ;
typedef short int16_t;
这样做的目的是便于在不同平台下的移植,如果当在另一个平台下,int 是64位的,但是我的项目中都是用的int32_t;所以只需要修改int32_t就可以了,我可以让他typedef short int32_t;这样我只更改一次,其余的都改了,做到一改全改。4.9.3 int **p; int *a[4]; p=a;可以这样理解:首先它是指针数组,既然是数组,则a即表示数组名又表示首元素的首地址,a[0]是一个一重指针,而a是a[0]的地址,那么a就是一个二重指针;{ 一重指针的地址就是二重指针变量,所以有p=a; 而 int a[4][3] ,a和一维数组的意思是一样的,如 int a[3][6],int *p ;p=a[0];所以不能p=a,int *a[3][3],int **p;p=a[0];}4.9.4、二维数组是为了简化编程,平面型。数组以下标示方式访问其实是编译器封装起来的,实质是指针访问。int (*p)[5]; int a[2][5];则有 p=a; 关键是要把二维数组抽象成 n 行 n 列用指针访问方式理解:二维数组可以看作是一个方格子的矩阵,比如 a[2][5],那么就是 2 行 5 列的 10 个小格子,第一行可以收纳起来变成一个指向一维数组的指针,第二行也是如此;这样收纳后就变成了一个新的数组 a[2],每一个格子存放的是它收纳的第一个元素的地址,如 a[0] 存放的是第一行第一列元素的地址,“a”[1]存放的是第二行第一列的地址;再与一维数组相联系,一维数组名即表示数组名又表示数组第一个元素的地址,所以 a[2][5] 中的 a 表示 “a"[2] 数组第一个元素的地址;那么再把 p=a;层层推递,(p+i)表示指向第几行的地址,(p+i)表示取第几行的值(而这个值存放的是第几行一列元素的首地址),(p+i)+j 表示指向第几行第几列的地址,最后在引用这个地址,((p+i)+j)就表示第几行第几列的值了。一重指针----------->一维数组二重指针----------->指针数组数组指针----------->二维数组函数指针----------->普通函数char a[6]={'l','i','n','u','x',''};
'' 的字符编码为 0 就是 NULL;也就是说内存中遇到 0,翻译成字符是就是 '' ,或这是 NULL;char a[6]="linux"; char *p="linux"; 5.2、 sizeof(a)=6 是运算符,其意思是所占空间大小,包括字符串后面的‘',strlen(a)=5 是一个函数,其意思是字符串的长度。strlen( p);其中 p 只有是字符指针变量才有意义,它的形参是数组变量是没有意义的,因为 strlen 是根据什么时候遇到 '',才结束测试字符串的长度,就算数组不初始化也是有长度的。
char *p="linux";
sizeof(p) 永远等于 4,因为 p 是指针变量,存的是地址。所以总结:sizeof() 是拿来测数组的大小,strlen()
是拿来测试字符串的长度。5.3、结构体用 . 或者是 -> 访问内部变量,其实质是用的指针访问。如struct student{ int a; double b; char c; }s1; 则 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型。5.4、对齐方式:(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字节(默认)
#pragma pack(1) struct stu1 { (结构体本身以及变量) 对齐规则:2字节对齐(2的整数倍),只要是0、2、4地址就行了,4字节对齐(4的整数倍),就必须是0、4地址, 8字节对齐(8的整数倍),就必须是0、8、16
char c; int a; //short d; }s1; struct stu2 { char c; int a; //short d; }s2; <2> gcc 推荐的对齐指令
__attribute__((packed)) __attribute__((aligned(n)))
,在 VC 中就不行,没有定义这个命令(1)__attribute__((packed))
使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。(2)__attribute__((aligned(n)))
使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐,内部元素按照默认对齐方式)例子:struct mystruct11 {// 1字节对齐4字节对齐 int a;// 44 char b;// 12(1+1) short c;// 22 }__attribute__((packed)); typedef struct mystruct111 {// 1字节对齐4字节对齐2字节对齐 int a;// 44 4 char b;// 12(1+1)2 short c;// 22 2 short d;// 2 4(2+2)2 }__attribute__((aligned(1024))) My111; 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地址开始算的,所以这个地址就是相对起始地址的偏移量。5.6 container_of宏:#define container_of(ptr, type, member) ({ const typeof(((type *)0)->member) * __mptr = (ptr); (type *)((char *)__mptr - offsetof(type, member)); }) 两条语句;,然后用{ } ,表示提示编译器本行因为屏幕不够,链接下一行。用#(也就是宏定义)时,如果本行不够要用 提示编译器接着是下一行的。必须要用 ,猜测因为宏定义一行就算结束了。(1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。(2)typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。(3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。5.7 p是一个地址,(int)p+6 和(char *)+6;效果是一样的,第一种是将地址p当作int型加减,第二种是将地址p做为char *指针,他每次加减都是一字节一字节相加减的,如果是 (int )P+6,那么他每次加减都是按照4字节一跳。就相当于加了+46;5.8 小端模式:变量的高地址存放在高地址,低地址存放在低地址;通信模式也要分大小端,先发送/接受的是高地址还是低地址,大端模式:变量的高地址存放在低地址,低地址存放在高地址;测试:有用共用体 union 和指针方式来测试,基本思路是让 int a=1; 看低地址 char 是0还是1 ;变量都是从地址开始存放,只是变量的高地址和低地址先存放谁不确定。不能用位与来测,因为存放和读取都是按照某一个方式来的,结果永远都是一样的。int a=1; char b=(char)a;这种方式不可以测试,因为不管大小端,它都以变量a的低地址部分赋给b;union stu{ int a; int ce( ) { int a=1; int b=*((char *)&a); return b; } char b; } int ce( ) { union stu s; s.a=1; return s.b; } 5.9、枚举类型(int型):这样写默认从第一个常量开始是0,1,2,3,4.........也可以自己赋值,但是每一个值是不一样的,否则逻辑上出错。e
num week{sunday, sunday=1,moday, moday=5,tuseday, 然后其他常量以此递增。wenzeday,friday,saterday,}today; today=sunday;* // 错误1,枚举类型重名,编译时报错:error: conflicting types for ‘DAY’typedef enum workday{MON, // MON = 1;TUE,WEN,THU,FRI,}DAY;typedef enum weekend{SAT,SUN,}DAY;*// /错误2,枚举成员重名,编译时报错:redeclaration of enumerator ‘MON’typedef enum workday{MON, // MON = 1;TUE,WEN,THU,FRI,}workday;typedef enum weekend{MON,SAT,SUN,}weekend;}
#ifdef DEBUG #define debug(x) printf(x) #else #define debug(x) #endif 6.5、函数:(1)整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。(2)函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。(3)函数的目的就是实现模块化编程。说白了就是为了提供程序的可移植性。<1>函数书写的一般原则:第一:遵循一定格式。函数的返回类型、函数名(男女厕所)、参数列表(太多用结构体)等。第二:一个函数只做一件事:函数不能太长也不宜太短(一个屏幕的大小),原则是一个函数只做一件事情。第三:传参不宜过多:在ARM体系下,传参不宜超过4个。如果传参确实需要多则考构体打包考虑。第四:尽量少碰全局变量:函数最好用传参返回值来和外部交换数据,不要用全局变量。<2> 之所以函数能被调用,根本实质是在编译时,检查到了该函数的声明,不是因为函数定义了(当然也要定义才行,只是不是本质)。6.6、递归函数:自己调用自己的函数,常用举例:阶乘 int jiecheng( int n) 斐波那契数例:f(n)=f(n-1)+f(n-2) n>2的正整数{ int he(int n)注意:if(n<1) if(3==n||4==n)栈溢出:递归函数会不停的耗费栈空间 { { 所以要注意递归不要太多 printf("error "); return 1; 收敛性:必须 要有一个终止递归的条件 } }
else if(n>1) else if(n>4) { { return n*jiecheng(n-1); return he(n-1) +he(n-2) } } else { return 1; } 6.7、函数库:<1>静态链接库其实就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫静态链接库文件)。商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用;客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在连接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段链接进去形成最终的可执行程序。<2>动态链接库本身不将库函数的代码段链接入可执行程序,只是做个 标记。然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。也就是在运行时,会把库函数代码放入内存中,然后多个程序要用到库函数时,就从这段内存去找,而静态链接对于多程序就是重复使用库函数,比较占内存。(1) gcc中编译链接程序默认是使用动态库的,要想静态链接需要显式用-static来强制静态链接。(2) 库函数的使用需要注意3点:第一,包含相应的头文件;第二,调用库函数时注意函数原型;第三,有些库函数链接时需要额外用-lxxx来指定链接;第四,如果是动态库,要注意-L指定动态库的地址。6.8、常见的两个库函数:<1>C库中字符串处理函数包含在string.h中,这个文件在ubuntu系统中在/usr/include中字符串函数 如:memcpy(内存字符串复制,直接复制到目标空间)确定src和dst不会overlap重复,则使用memcpy效率高memmove(内存字符串复制,先复制到一个内存空间,然后再复制到目标空间)确定会overlap或者不确定但是有可能overlap,则使用memove比较保险
memset strncmp memcmp strdup memchr strndup strcpy strchr strncpy strstr strcat strtok strncat 。。。 strcmp <2> 数学函数:math.h 需要加 -lm 就是告诉链接器到libm中去查找用到的函数。C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。6.9、自制静态链接库:(1)第一步:自己制作静态链接库,首先使用gcc -c只编译不连接,生成.o文件;然后使用ar工具进行打包成.a归档文件库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件 注意:制作出来了静态库之后,发布时需要发布.a文件和.h文件。(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 arm-ar -rc libaston.a aston.o6.9.1、自制动态链接库:<1>动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a .<2>第一步:创建一个动态链接库。gcc aston.c -o aston.o -c -fPIC (-fPIC表示设置位置无关码) gcc -o libaston.so aston.o -shared (-shared表示使用共享库) 注意:做库的人给用库的人发布库时,发布libxxx.so和xxx.h即可。第二步:使用自己创建的共享库。gcc cdw.c -o cdw -lmax.so -L ./动态链接库 -I ./动态链接库第三步:上述编译成功了,但是在 ./cdw 执行时会报错,原因是采用动态链接,在可执行文件只是做了一个标记,标记是使用了哪个函数库的哪个函数。并没有将库函数加载到源文件中,所以可执行文件很小,在执行时,需要立即从系统里面找到使用到的函数库,然后加载到内存中,在linux系统中 默认是从 /usr/bin 中寻找,(不确定:如果使用shell中运行)会先执行环境变量的路径然后再查找 /usr/bin;所以我们可以用两种办法解决运行的问题第四步:将动态库 libmax.so 复制到 /usr/lib 下面,但是如果以后所有的库都这样放的话,会越来越臃肿,导致运行速度变慢(一个一个查找);或者是新添加一个环境变量export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include (将库 libmax.so 复制到这个路径下面)这样就可以运行了。<3>使用 ldd 命令判断一个可执行文件是否能运行成功;ldd cdw linux-gate.so.1 => (0xb77a8000) libmax.so => not found //发现 not found意思就是没有找到对应的函数库 libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75e2000) /lib/ld-linux.so.2 (0xb77a9000)
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; } <6> restrict (1)c99中才支持的,所以很多延续c89的编译器是不支持restrict关键字,gcc支持的。(2)restrict 作用是让编译器对这个变量做一些优化,让它提高效率。下面的网站有列子。(3)restrict只用来修饰指针,不能修饰普通变量,它表示只能该指针才能修改它的内容。如用memcpy时,两个内存存在重叠的现象。(4)(这个网站里面有详细的例子)(5)memcpy和memmove的区别 void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n);这样它可以优化成memmove原理的方式(当存在重叠时,先复制到一段内存空间,然后再把它复制到目的空间)7.4、作用域:(1)全局变量起名字一般是 g_a;(2)名字加前缀表示7.5、总结:<1>局部变量地址由运行时在栈上分配得到,多次执行时地址不一定相同,函数不能返回该类变量的地址(指针)作为返回值。<2>为什么要分为数据段和.bbs段?因为当加载到内存重定位时,如果这些数据(包括0)一个一个的复制,会降低效率,为0的变量,直接清内存就可以了,这样提高效率。<3>在头文件声明全局变量时, extern int a; 声明函数就是 void int max(int a, int b);<4>写程序尽量避免使用全局变量,尤其是非static类型的全局变量。能确定不会被其他文件引用的全局变量一定要static修饰。(因为全局变量占内存的时间是最长的,要看你的变量是不是需要这么长的时间,这样可以节约内存空)
int *p = NULL;// 定义p时立即初始化为NULL p = xx; if (NULL != p) { *p // 在确认p不等于NULL的情况下才去解引用p } (1)''是一个转义字符,他对应的ASCII编码值是0,内存值是0,一个char空间。(2)'0'是一个字符,他对应的ASCII编码值是48,内存值是int型48,一个char空间。(3)0是一个数字,没有ASCll编码, 内存值是int型0,一个int空间。(4)NULL是一个表达式,是强制类型转换为void *类型的0,内存值是0(内存地址),一个int空间。8.9.1、运算中的临时匿名变量<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;最后匿名值销毁。8.9.2 分析DEBUG宏学习级:
#define DEBUG #undef DEBUG 是注销 DEBUG 宏 #ifdef DEBUG #define debug(x) printf(x) #else #define debug(x) #endif 应用级:
#ifdef DEBUG #define DBG(...) fprintf(stderr, " DBG(%s, %s( ), %d): ", __FILE__, __FUNCTION__, __LINE__); fprintf(stderr, __VA_ARGS__) #else #define DBG(...) #endif 解 释:<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("tiaoshi. ");则允许的效果是 DBG(debug.c, main( ), 14): tiaoshi.内核级:
#ifdef DEBUG_S3C_MEM#define DEBUG(fmt, args...)printk(fmt, ##args)#else#define DEBUG(fmt, args...)do {} while (0)#endif
全部0条评论
快来发表一下你的评论吧 !