面试大全 | C语言高级部分总结
来源:网络
ID:技术让梦想更伟大
整理:李肖遥
一、内存大话题
1.0、内存就是程序的立足之地,体现内存重要性。
1.1、内存理解:
内存物理看是有很多个Bank(就是行列阵式的存储芯片),每一个Bank的列就是位宽 ,每一行就是Words,则存储单元数量=行数(words)×列数(位宽)×Bank的数量;通常也用M×W的方式来表示芯片的容量(或者说是芯片的规格/组织结构)。
M是以位宽为单位的总容量,单位是兆 ,W代表位宽, 单位是bit。计算出来的芯片容量也是以bit为单位,但用户可以采用除以8的方法换算为字节(Byte)。比如8M×8,这是一个8bit位宽芯片,有8M个存储单元,总容量是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/double型:放在内存的长度和解析作用。(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的内存空间,看不到其他程序。
二、位操作
2.1 ~(0u)是全1;
2.2 位与& 位或 | 位取反~ 位异或^
2.3、位与、位或、位异或的特点总结:
位与:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0
位或:(任何数,其实就是1或者0)与1位或变成1,与0位或无变化
位异或:(任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化
2.4、左移位<< 与右移位>> C语言的移位要取决于数据类型。
对于无符号数,左移时右侧补0(相当于逻辑移位)
对于无符号数,右移时左侧补0(相当于逻辑移位)
对于有符号数,左移时右侧补0(叫算术移位,相当于逻辑移位)
对于有符号数,右移时左侧补符号位(如果正数就补0,负数就补1,叫算术移位)
2.5、小记:常与 1 拿来 做位运算。让他取反、移位 得到想要的数。
2.6、直接用宏来置位、复位(最右边为第1位)。 置位置1,复位置0 ;
#define SET_NTH_BIT(x, n) (x | ((1U)<<(n-1)))
#define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))
}
三、指针—精髓
3.1 printf("%p \n"); 其中%p表示输出一个指针,就是指针变量(其存放的那个地址),可以理解为输出一个地址。
3.2 int* p1, p2 ; 等同于 int *p1; int p2; int *p="Linux",其不能改变*P,因为”linux"是一个常数。
3.3 ( 代码规范性 )在定义指针时,同时赋值为NULL,在用指针时,先判断它是不是NULL。尤其是在malloc申请内存后,free(p);则一定要让p=NULL
3.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( )要有所区别,他是测字符串实际长度的,不包括‘\0‘,如果给strlen传的参数不是一个字符串,则它会一直去找,直到 找到第一个 ‘\0’,然后再计算其长度。
如 char a[]="chen"; char *p=a; 则strlen(p)=4;
3.8、 当数组作为一个形参时,其实参是一个数组名(也可以是指针,其本质就是指针),意义是首元素的首地址,则传过去只影响形参的第一个元素。形参数组的地址被实参数组地址所绑定;
实参的大小会丢失,所以往往会传一个int num 大小进去。
3.9、 结构体做为形参时,应尽量用指针/地址方式来传,因为结构体变量有时会占很大,效率很低。
4.0、 int *p=&u; p存放的是变量u的地址,而&p的意思就是变量p本身的地址。
4.1、当要传参的个数比较多时,我们可以打包成一个结构体,传参的个数越多,其开销就更大.
4.2 一个函数作用其实就是输入输出,参数可以作为输入,返回可以作为输出,但是当要返回多个输出时,这时候就不够用了,所以常常返回值用来判断程序又没有出错,而参数就是当作输入输出的,输入时可以加const表示它没必要去修改,而输出都是指针,因为要改变它的值,只能采用地址传递这种方式。比如:char *strcpy(char *dest,const char *src)
四、C语言复杂表达式
4.1、在表达式中,要看符号的优先级和结合性。
4.2、在理解内存时,内存0地址在最底下,至上地址逐渐增加。
4.3、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在做输出时,其机制是缓冲行来输出,即当遇到一个\n后再打印出来,即使再多printf,没有遇到\n,都不是一个一个打印。
'\r'是回车,'\n'是换行,前者使光标到行首,后者使光标下移一格,通常敲一个回车键,即是回车,又是换行(\r\n)。Unix中每行结尾只有“<换行>,即“\n”;Windows中每行结尾是“<换行><回车>”,即“\r\n”;Mac中每行结尾是“<回车>”。scanf("");里面不要加\n符。
4.8 在一个c文件中,有时候会多次引入一个.h文件,所以在写.h文件时,要写
{#ifndef _FINE_
#define _FINE_
XXXXXXXX
XXXXXXXXXXX
#endif }
4.9、typedef int *intType; 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)就表示第几行第几列的值了。
一重指针----------->一维数组
二重指针----------->指针数组
数组指针----------->二维数组
函数指针----------->普通函数
五、数组&字符串&结构体&共用体&枚举(5.6?)
5.1、c语言中定义一个字符串: char a[6]={'l','i','n','u','x','\0'}; '\0'的字符编码为0就是NULL;也就是说内存中遇到0,翻译成字符是就是'\0',或这是NULL;
char a[6]="linux";
char *p="linux";
5.2、 sizeof(a)=6是运算符,其意思是所占空间大小,包括字符串后面的‘\0',strlen(a)=5是一个函数,其意思是字符串的长度。strlen( p);其中p只有是字符指针变量才有意义,它的形参是数组变量是没有意义的,因为strlen是根据什么时候遇到 '\0',才结束测试字符串的长度,就算数组不初始化也是有长度的。
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字节一跳。就相当于加了+4*6;
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.........也可以自己赋值,但是每一个值是不一样的,否则逻辑上出错。
enum 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;
}
六、C语言宏定义与预处理、函数和函数库(看博客strcyp原函数)
6.1、源码.c->(预处理)->预处理过的 .i 文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。
<1>预处理的意义(1)编译器本身的主要目的是编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。
(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;这条语句会直接展开
还有带参宏,#define max(a,b) ((a)+(b)) 注意的是带参宏一定要( ) 不然有时候会引起错误,每一个”形参“都应该要();
#define year (365*24*60*60*60*60 ) 安理说是可以的,但是year是int型的已经超过了范围,所以要把它搞成无符号长整形。
#define year (365*24*60*60*60*60ul ) 这样才是正确的
宏定义的变量是不占内存空间的,直接替换减少开销,但是变量替换是不进行类型检查;
函数的变量要占用空间、要压栈等操作,就需要很大的开销,但是调用函数时,编译器会检查函数变量的类型是否相同。
内联函数集合普通函数、宏定义的两个优势,它直接就地展开,直接替换,减少开销,同时编译器也会检查变量的类型。但是函数体积要小,不然效率反而很低,至于
原因暂时不详。
6.3、内联函数:对函数就地展开,像宏定义一样,这样减少开销,同时也检查变量的类型。但是必须函数的内部体积小才用这种方式,以达到更好的效率。体积大的函数就作为普通函数。
内联函数通过在函数定义前加inline关键字实现。
* 6.4、条件编译的应用:做一个调试开关。#define DEBUG #undef DEBUG 是注销 DEBUG 宏
#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\n"); 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.o
6.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)
七、存储类&作用域&生命周期&链接属性
7.1、概念词:存储类(栈、堆、数据区、.bss段、.text段)
作用域(代码块作用范围,也就是变量作用的范围)
生命周期(变量的诞生和死亡)
链接属性(外链接属性、内链接属性、无连接属性)
7.2、Linux下的内存映射(分配情况、组织情况):见图内存映射。其中有关进程的空间,如进程控制块、页表等都是在内核里面的。文件区是映射外部文件的,如打开记事本,那么这个文件临时存放在文件区域。(见引用资料)
问题:虚拟地址技术? 解决:后期在Linux应用/网络编程会讲。
OS下和裸机下C程序加载执行的差异? 解决:在arm裸机第十六部分有介绍。
7.3、存储类关键字:
<1> auto 自动的(一个用法:修饰局部变量,在定义变量时可以省略) 【外链接:与第二个c文件链接】【内链接:只与本c文件链接】【无连接:就是无链接】
<2> static 静态的(有两个用法,第一个是修饰局部变量,意思是当作全局变量,存放在数据区,作用域只是定义的那个函数范围,生命周期和整个程序一样,属于无连接
第二个是修改全局变量/函数,意思是这个全局变量/函数只在当前c文件有效,其他c文件是不能使用它的,属于内链接,普通全局变量属于外连接)
<3>register 寄存器(一个用法,修饰变量,作用是让编译器把这个变量放在寄存器中,当这个变量频繁的被使用时,用这个方法可以提高效率,但有时候不一定就放在寄存器,因为寄存器是有限的,没有剩余的寄存器了)
<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;
b=a;
c=b;
那么编译器会优化成 c=b=a=3; 如果此时遇到上述三种情况,突然改变了a的值,那么,对于没有优化前是对的,但是对于优化后,那么c仍然是3,就会出错。
所以当我们程序员知道这个变量会发生改变时,就应该加 volatile,提示编译器不要帮我们做优化。
举列子2:
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)https://blog.chinaunix.net/uid-22197900-id-359209.html (这个网站里面有详细的例子)
(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修饰。(因为全局变量占内存的时间是最长的,要看你的变量是不是需要这么长的时间,这样可以节约内存空)
八、一些杂散但值得讨论的问题
8.1、操作系统的理解:
<1>它是一个管理阶级者,管理所有资源,负责调配优化等操作。这样想象,就像裸机一样的话,要实现LED闪烁的进程、串口传输的进程、蜂鸣器等这些,他们都要抢占一些资源,这个时候没有操作系统,就乱成一锅粥,当有了OS的时候,它就专门负责资源的调配,让各个任务都能很好的实施,起一个决策者的作用。
<2>如果我们要做一个产品,软件系统到底应该是裸机还是基于操作系统呢?本质上取决于产品本身的复杂度。只有极简单的功能、使用极简单的CPU(譬如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于操作系统来开发。
<3>操作系统负责管理和资源调配,应用程序负责具体的直接劳动,他们之间的接口就是API函数。当应用程序需要使用系统资源(譬如内存、譬如CPU、譬如硬件操作)时就通过API向操作系统发出申请,然后操作系统响应申请帮助应用程序执行功能。
8.2、C库函数和API的关系:
<1>从内核的角度看,需要考虑提供哪些有用的API函数,并不需要关注它们如何被使用。故编程时用API函数会感觉到不好用,没有做优化。系统只负责做出一个可以用的API,没有考虑到用户使用的方便性。所以c库函数对API做了一些优化,让用户使用库函数更容易达到我们想要的目的。
<2>库函数实质还是用的API,或者调用了一个API,也或者调用了更多的API,只不过是做了更多的优化。比如 库函数fopen ,而API是open.
<3>有的库函数没有用到API,比如strcpy函数(复制字符串)和atoi函数(转换ASCII为整数),因为它们并不需要向内核请求任何服务。
8.3、不同平台(windows、linux、裸机)下库函数的差异
(1)不同操作系统API是不同的,但是都能完成所有的任务,只是完成一个任务所调用的API不同。
(2)库函数在不同操作系统下也不同,但是相似性要更高一些。这是人为的,因为人下意识想要屏蔽不同操作系统的差异,因此在封装API成库函数的时候,尽量使用了同一套接口,所以封装出来的库函数挺像的。
但是还是有差异,所以在一个操作系统上写的应用程序不可能直接在另一个操作系统上面编译运行。于是乎就有个可移植性出来了。
(3)跨操作系统可移植平台,譬如QT、譬如Java语言。
8.4、
<1>main()函数的写法:int main(int argc,char **argv) ; int main(int argc ,char *argv[ ] ); 这两种写法是一样的。二重指针等同于指针数组。
<2>不管是主函数还是功能函数,它都应该有一个返回值,而主函数的返回值是给调用的那个人的/main函数从某种角度来讲代表了我当前这个程序,或者说代表了整个程序。main函数的开始意味着整个程序开始执行,
main函数的结束返回意味着整个程序的结束。谁执行了这个程序,谁就调用了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,这样是要出错的。
8.5 用shell脚本来看main()的返回值。如:#!/bin/sh 其文本格式为 .sh
./a.out
echo $?
8.6、argc、argv与main函数的传参:当我们的父进程不需要传参时,就用 int main(void);当我们需要传参时,就应该是 int main(int argv ,char *argc[ ]);它默认本身就是一个参数,占了argv[0]这个位置,它里面存的是 ./a.out (这个相应变化)
如: ./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,先检验个数,然后使用。
8.7、void类型的本质:即使空型又是未知类型,看具体情况。比如一个函数void表示不返回, void *malloc(20);就是未知类型。
(1)编程语言分2种:强类型语言和弱类型语言。强类型语言中所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法;弱类型语言中没有类型的概念,所有变量全都是一个类型(一般都是字符串的),程序在用的时候再根据需要来处理变量。就如:makefile、html语言。
(2)C语言就是典型的强类型语言,C语言中所有的变量都有明确的类型。因为C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法。
(3)void类型的正确的含义是:不知道类型,不确定类型,还没确定类型、未知类型,但是将来一定有类型。
(4)void *a;(编译器可以通过)定义了一个void类型的变量,含义就是说a是一个指针,而且a肯定有确定的类型,只是目前我还不知道a的类型,还不确定,所以标记为void。
void “修饰”的是指针,因为指针就是内存地址,它不知道指向的另一个变量是哪一种类型,而变量一定是确定的,void a;就是错误的。
8.9、C语言中的NULL
NULL在C/C++中的标准定义
(1)NULL不是C语言关键字,本质上是一个宏定义,其保护指针的作用,不要让他乱开枪。
(2)NULL的标准定义:
#ifdef _cplusplus // 条件编译c++环境
#define NULL 0
#else
#define NULL (void *)0 // 这里对应C语言的情况
#endif
解释:C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。
NULL的本质解析:NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000,代表内存的0地址。(void *)0这个整体表达式表示一个指针,这个指针变量本身占4字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际是0地址开始的一段内存)。如 char *p=NULL; 因为0地址本身就不是我们来访问的,所以 *p时是不可访问的。在程序运行的逻辑上就不会出错。
正规写:
int *p = NULL;// 定义p时立即初始化为NULL
p = xx;
if (NULL != p)
{
*p // 在确认p不等于NULL的情况下才去解引用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空间。
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.\n");
则允许的效果是 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
九、链表&状态机与多线程(9.9.1?具体链表实现留到驱动模块讲解)
9.1、链表是一个一个的节点,每一个节点分为两部分,一部分是数据区(可以由多个类型的数据),另一部分是指向下一个节点的指针;结构体定义里面的变量并没有生成,是不占空间的,相当于声明的作用。
9.2、链表的数据存放在内存的那个空间呢?(栈,不灵活,不能用date数据段)所以只能用堆内存,申请一个节点的大小并检测NULL, 要使用它,就得清理它,因为上一个进程用了这段内存,存的是脏数据,
然后对这个节点内存赋值,链接起来.
9.3、当要改变头节点是,也就是要给head=p赋值时,必须传 head地址即 形参(struct student *head);这样才能真正改变,不然传一个 (struct student head)只是单纯的赋值。
9.4、在scanf("%d",&(s->age)) 一定要注意,studeny *s; s->age访问的是一个变量,而不要理解成地址,所以要加&,scanf要注意&;
9.5、细节:<1>在 .h文件中声明一个函数要用分号,而且是英文符号.用无头节点的方式,需要修改头指针位置,所以比较复杂。
<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赋值时就要放在代码段中,而代码段已确定了,所以报段错误。
9.6、头节点、头指针、第一个节点:头节点是一个节点,头节点的下一个指向第一个节点,头节点的数据一般存的是链表长度等信息,也可以是空,头指针指向头节点。链表可以没有头节点,但不能没有头指针。
头节点可以想成数组的0位置,其余节点当作从1开始,所以有头节点的长度可以定义为就是含有真实数据节点的个数。
9.7、删除一个节点应该做的事:如果这个节点的数据不重要,一定要记住free()掉,你逻辑上删除,其实仍然存在内存中的,头节点的好处就是函数返回值int可以帮助我们一些信息,而没有头节点有时必须返回head;
9.8、单链表之逆序:见代码。
9.9、单链表的优点和缺点:<优点>单链表是对数组的一个扩展,解决了数组的大小比较死板不容易扩展的问题。使用堆内存来存储数据,将数据分散到各个节点之间,其各个节点在内存中可以不相连,节点之间通过指针进行单向链接。链表中的各个节点内存不相连,有利于利用碎片化的内存。
<缺点>单链表各个节点之间只由一个指针单向链接,这样实现有一些局限性。局限性主要体现在单链表只能经由指针单向移动(一旦指针移动过某个节点就无法再回来,如果要再次操作这个节点除非从头指针开始再次遍历一次),因此单链表的某些操作就比较麻烦(算法比较有局限)。
回忆之前单链表的所有操作(插入、删除节点、 遍历、从单链表中取某个节点的数·····),因为单链表的单向移动性导致了不少麻烦。
总结:单链表的单向移动性导致我们在操作单链表时,当前节点只能向后移动不能向前移动,因此不自由,不利于解决更复杂的算法。
9.9.1、 内核链表的思想是:<1>先做一个纯链表,没有数据区,只有节点的链接方法。然后要做一个链表出来,直接用纯链表然后稍加修改就可以了。
<2>内核中__的方法不要轻易使用,是给内核用的,否则容易出错,用户应该使用没有__的方法;如:__list_add() ; list_add();
<3>内核默认是头指针+头节点的思路。
<4>其实质就是操作里面内嵌 纯链表这个变量,再利用controf宏来访问结构体的数据。详情见驱动。
9.9.2、状态机:
<1>概念:其实就是有多种状态切换,如电脑的休眠、关机、睡眠。
<2>类型:(1)Moore型状态机特点是:输出只与当前状态有关(与输入信号无关)。相对简单,考虑状态机的下一个状态时只需要考虑它的当前状态就行了。
(2)Mealy型状态机的特点是:输出不只和当前状态有关,还与输入信号有关。状态机接收到一个输入信号需要跳转到下一个状态时,状态机综合考虑2个条件(当前状态、输入值)后才决定跳转到哪个状态。
<3>理解:要时时刻刻检查当前状态,用循环+switch(状态);然后根据输入信号,进行更多的处理,转换到其他状态。
十、增补知识
10.1、一个字节可以表示8位字符,字符真的有256种,128~255表示西欧字符,是不常见,详情见文档。 字符相加的时候,会自动转成 int型加。
10.2、在C中,默认的基础数据类型均为signed,现在我们以char为例,说明(signed) char与unsigned char之间的区别。
首先在内存中,char与unsigned char没有什么不同,都是一个字节,唯一的区别是,char的最高位为符号位,因此char能表示-127~127,unsigned char没有符号位,因此能表示0~255,这个好理解,8个bit,最多256种情况,因此无论如何都能表示256个数字。
10.3、为什么在链接时需要一个链接地址?因为数据是要放在一个模拟地址内存空间的,它要把这个数据先加载到寄存器,才能给cpu使用,那么寄存器怎么知道是哪个内存地址位置呢,是因为在编译时,编译出像 ldr r0 0x12345678 ,而这个0x12345678就是内存地址,再编译出像 ldr r1,[r0] ,这样就可以拿到0x12345678内存位置的数据了
10.4、printf 变参?
10.5、arm-2009q3.tar.bz2 这套编译器自带了函数库,比如有strcmp , malloc ,printf 等,但是有些库函数我们却不能用他们,比如printf,因为这个函数默认是同过屏幕输出的,而我们常用uart调试。感觉malloc也不能用,因为我们不知道内存哪一块做了堆内存,只有系统才知道。
10.6、清bss段:编译器可能已经帮我们做了,只是在重定位那节,因为要重定位那部分内存空间并没有清0 ,所以要手动编程清bss段。
嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 Qt进阶学习专辑 关注微信公众号『技术让梦想更伟大』,后台回复“m”查看更多内容。 长按前往图中包含的公众号关注