C语言高级部分总结
内存就是程序的立足之地,体现内存重要性。
内存理解:
c语言中其实没有bool类型:以0表示假,非0表示真,则在内存存储是以int型存放的。如果想要表示真假,可以用int/char型做替换,在c++中就有bool x=true/false;
内存对齐:内存对齐(提高访问效率速度,编译器一般默认是4字节对齐)
char/int/short/long/float/double型:放在内存的长度和解析作用。(int *)0
,使0地址指向一个int型。又比如0000111010101
可以解析成int型也可以解析成float型。
Linux内核是面向对象的,而c语言是面向过程的,但可以用结构体内嵌指针变成面向对象。如
struct student{
int age; //变量
int lenth; //将相当于一个类,有变量有函数
char *name;
void (*eat)(void); //函数指针
}
(1) 运行时自动分配&自动回收:栈是自动管理的,程序员不需要手工干预。方便简单。(表现在汇编代码,编译时,会自动编译成汇编码实现函数调用完立即改变栈顶)
(2) 反复使用:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。(硬件上有个寄存器,用来存放栈的栈顶地址,栈是有大小的空间)
(3) 脏内存:栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。
(4) 临时性:(函数不能返回栈变量的指针,因为这个空间是临时的)
(5) 栈会溢出:因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完。栈的操作(怎么出栈怎么入栈)是由具体硬件来干预,程序员只要明白原理就可以了,但是要给相应的栈寄存器赋值。当调用函数时,变量会自动放在栈中(入栈)当函数调用完后,栈会自动出栈.
( 6 ) 栈的 "发展"有四种情况,满增栈,满减栈,空增栈,空减栈,至于是那种要根据编译器决定,而s5pv21 是满减栈。
堆的理解:
操作系统堆管理器管理:堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配。
大块内存:堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放。
脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的。
临时性:堆内存只在malloc和free之间属于我这个进程,而可以访问。在malloc之前和free之后都不能再访问,否则会有不可预料的后果。
程序手动申请&释放:手工意思是需要写代码去申请malloc和释放free。(记住:不要把申请的地址给搞丢了, 不然自己用不了,也释放不了)
malloc(10*sizeof ( int ) );
void *malloc(size_t size);
calloc( 10,sizeof ( int ) );
void *calloc(size_t nmemb, size_t size);
void *realloc(void ptr, size_t size);
在申请内存时,malloc(0)
其实也是成功的,因为系统规定少于一定数目的大小,都申请规定的大小,如在win32系统下申请少于32字节的地址,最后申请到的空间是32字节,在申请少于16字节的地址,最后申请到的是16字节,至于规定多少字节,由具体的系统而言。
内存里的数据:
(1)代码段:存放代码二进制、常量(char *p=“linux”,则”linux“存放在代码段,是不可更改的)
(2) 数据段: 存放非0全局变量、静态局部变量(局部只属于函数的,不是整个程序的)
(3) bss : 存放为0的全局变量/为0的静态局部变量、存放未初始化全局变量/静态局部变量
注意:const int a=9; 有两种存放方式:第一种确实存放在代码段,让a不能修改,第二种是仍然存放在数据段中,让编译器来判断,如果有改变的代码就会报错。 至于那种,是不确定的,像单片机就属于第一种。
一个源文件实际上是以段为单位编译成连接成可执行文件(a .out );这个可执行文件总的说是分为数据段,代码段,自定义段,数据段还可以细分成 .bbs 段。而杂段会在执行的时候拿掉。所以a.out分为杂段,数据段(存放的是非0全局变量).bbs段,代码段。
内存实际上被划分了两大区域,一个是系统区域,另一个是用户区域,而每一个区域又被划分成了几个小区域,有堆,栈,代码区,.bbs区,数据区(存放的是非0全局变量)。
对于有操作系统而言, 当我们在执行a.out可执行文件时,执行这个文件的那套程序会帮我们把杂段清掉,然后把相应的段加载到内存对应的段。对于裸机程序而言,我们是使用一套工具将a.elf的可执行程序给清掉了所有段的符号信息,把纯净的二进制做成.bin格式的烧录文件。所以我们加载到内存的程序是连续的,也就是说代码段和数据段、.bbs段都是连续的。当然,栈空间是我们自己设置的。而且在裸机中我们不能使用malloc函数,因为我们使用的只是编译器、连接器工具没有集成库函数,没有定义堆空间区。
大总结多程序运行情况: 在Linux系统中运行cdw1.out时,运行这个文件的那套程序会帮我们把相应的段加载到内存对应的段。然后操作系统会把下载到内存的具体物理地址与每条命令(32位)的链接地址映射到TTB中(一段内存空间),当我们又运行cdw2.out时,同样也像cdw1.out一样加载进去,并映射到TTB表中。而且这两个.out文件默认都是链接0地址(逻辑),当cpu发出一个虚拟地址(Linux中程序逻辑地址)通过TTB查找的物理地址是不一样的。所以对于每一个程序而言,它独占4G的内存空间,看不到其他程序。
~(0u)
是全1;
位与& 位或 | 位取反~ 位异或^
位与、位或、位异或的特点总结:
小记:常与 1 拿来 做位运算。让他取反、移位 得到想要的数。
直接用宏来置位、复位(最右边为第1位)。 置位置1,复位置0 ;
#define SET_NTH_BIT(x,n) (x|((1U)<<(n-1)))
#define CLEAR_NTH_BIT(x,n) (x&~((1U)<<(n-1)))
printf("%p \n");
其中%p表示输出一个指针,就是指针变量(其存放的那个地址),可以理解为输出一个地址。
int* p1, p2;
等同于 int *p1; int p2;
。 int *p= "linux"
,其不能改变*p
,因为"linux"是一个常数。
( 代码规范性 )在定义指针时,同时赋值为NULL,在用指针时,先判断它是不是NULL。尤其是在malloc申请内存后,free(p);
则一定要让p=NULL
C/C++中对NULL的理解:
#ifdef _cplusplus // 定义这个符号就表示当前是C++环境
define NULL 0; // 在C++中NULL就是0
#else
define NULL (void *) 0; // 在C中NULL是强制类型转换为void *的0
#endif
第一种:const int *p;
第二种:int const *p;
第三种:int * const p;
第四种:const int * const p;
数组 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]看起来更方便,封装了一下而已,实质还是指针。
siziof()
是一个运算符,测试所占内存空间,如 int a[100] ;sizeof(a)=400;
。与strlen()
要有所区别,他是测字符串实际长度的,不包括‘\0‘,如果给strlen传的参数不是一个字符串,则它会一直去找,直到 找到第一个 ‘\0’,然后再计算其长度。如 char a[]="chen"; char *p=a;
则strlen(p)=4;
。
当数组作为一个形参时,其实参是一个数组名(也可以是指针,其本质就是指针),意义是首元素的首地址,则传过去只影响形参的第一个元素。形参数组的地址被实参数组地址所绑定;实参的大小会丢失,所以往往会传一个int num 大小进去。
结构体做为形参时,应尽量用指针/地址方式来传,因为结构体变量有时会占很大,效率很低。
int *p=&u;
p存放的是变量u的地址,而&p
的意思就是变量p本身的地址。
当要传参的个数比较多时,我们可以打包成一个结构体,传参的个数越多,其开销就更大.
一个函数作用其实就是输入输出,参数可以作为输入,返回可以作为输出,但是当要返回多个输出时,这时候就不够用了,所以常常返回值用来判断程序又没有出错,而参数就是当作输入输出的,输入时可以加const表示它没必要去修改,而输出都是指针,因为要改变它的值,只能采用地址传递这种方式。比如:char *strcpy(char *dest,const char *src)
在表达式中,要看符号的优先级和结合性。
在理解内存时,内存0地址在最底下,至上地址逐渐增加。
int *p;是定义的一指针变量p,而int ( p)[4];也是一个指针变量p;也可以这样想:凡是遇到(p)什么的判断他是指针后,就可以说他是指针变量,包括函数指针。
一个函数 int max(int a ,int b); 则他的函数指针是 int ( *p ) (int ,int );其意思就是定义这个类型的函数指针变量p; p=max是赋值,引用是p();则相当于max()调用这个函数。函数指针必须和原函数的类型一样。
函数指针其实就是为了做结构体内嵌指针的,这样就构成了高级语言中的类。再一个就是上述4.4中p=&max;也是可以的,它和p=max,值和意义都是一样的,这个和数组有所区别,数组的a和&a的值虽然一样,但是意义完全不一样。int a[4];a有两层意思,第一层是数组名,&a表示整个数组的地址,第二层表示首元素的首地址。
int (*p[4])(int ,int)其意思是函数指针数组,一个4长度的数组,里面存了4个函数指针。
printf在做输出时,其机制是缓冲行来输出,即当遇到一个\n后再打印出来,即使再多printf,没有遇到\n,都不是一个一个打印。
‘\r’是回车,’\n’是换行,前者使光标到行首,后者使光标下移一格,通常敲一个回车键,即是回车,又是换行(\r\n)。Unix中每行结尾只有“<换行>,即“\n”;Windows中每行结尾是“<换行><回车>”,即“\r\n”;Mac中每行结尾是“<回车>”。scanf(“”);里面不要加\n符。
在一个c文件中,有时候会多次引入一个.h文件,所以在写.h文件时,要写
#ifndef FINE
define FINE
XXXXXXXX
XXXXXXXXXXX
#endif
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];
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'};
‘\0’的字符编码为0就是NULL;也就是说内存中遇到0,翻译成字符是就是’\0’,或这是NULL;char a[6]="linux";
char *p="linux";
sizeof(a)=6是运算符,其意思是所占空间大小,包括字符串后面的‘\0’,strlen(a)=5是一个函数,其意思是字符串的长度。strlen( p);其中p只有是字符指针变量才有意义,它的形参是数组变量是没有意义的,因为strlen是根据什么时候遇到 ‘\0’,才结束测试字符串的长度,就算数组不初始化也是有长度的。
char *p=“linux”; sizeof§永远等于4,因为p是指针变量,存的是地址。所以总结:sizeof()是拿来测数组的大小,strlen()是拿来测试字符串的长度。
结构体用.
或者是 ->
访问内部变量,其实质是用的指针访问。如
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型。
4. 对齐方式:
猜测如果是32位系统,那么编译器默认是4字节对齐,64位系统,那么编译器默认是8字节对齐,因为32位或64位一次性访问效率是最高的。
结构体对齐
1. 结构体首地址对齐(编译器自身帮我们保证,会给它分配一个对齐的地址,因为结构体自身已经对齐了,那么第一个变量也就自然对齐,所以我们才可以想象成第一个变量从0地址存放);
2. 结构体内部的各个变量要对齐。
3. 整个结构体要对齐,因为定义结构体变量s1时,再定义变量s2时,如果s1没有对齐,就坑了s2,所以也要保证整个结构体对齐。
4. 无论是按照几字节对齐,我们都可以联想到内存实际的安排。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字节 以保证整个结构体对齐。
5. 整个结构体对齐,如2字节对齐(2的整数倍),只要是0、2、4地址就行了,如果是4字节对齐(4的整数倍),就必须是0、4地址。8字节对齐(8的整数倍)
猜测4字节/8字节其实是针对int型/double型的,比如0地址是char型,那么4字节对齐,int型、float型就必须从4地址开始存放,那么8字节对齐,int型就必须从4地址存放,double型就必须从8地址开始存放.小于几字节对齐的那些,如char型和short型只要能按照规则存放就行了。
对齐命令:
#prgama pack(n)
开头,以#pragma pack()
结尾,定义一个区间,这个区间内的对齐参数就是n。(不建议使用)。如:s1占5个字节,s2占8字节(默认)/*
*(结构体本身以及变量) 对齐规则:2字节对齐(2的整数倍),只要是0、2、4地址就行了,
* 4字节对齐(4的整数倍),就必须是0、4地址,8字节对齐(8的整数倍),就必须是0、8、16
*/
#pragma pack(1)
struct stu1
{
char c;
int a;
}s1;
#pragma pack()
struct stu2
{
char c;
int a;
}s2;
attribute((packed))
和 attribute((aligned(n)))
,在VC中就不行,没有定义这个命令attribute((packed))
使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。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;
offsetof宏:#define offsetof( TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
(TYPE *)0
这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。((TYPE *)0)->MEMBER(TYPE *)0
是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素,然后对这个元素取地址,又因为改地址是从0地址开始算的,所以这个地址就是相对起始地址的偏移量。container_of宏:
#define container_of(ptr, type, member) ({ const typeof(((type *)0)->member) * __mptr = (ptr);
(1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
(2)typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。
(3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。
p是一个地址,(int)p+6 和(char *)+6;
效果是一样的,第一种是将地址p当作int型加减,第二种是将地址p做为char *指针,他每次加减都是一字节一字节相加减的,如果是 (int)P+6
,那么他每次加减都是按照4字节一跳。就相当于加了+46;
小端模式:变量的高地址存放在高地址,低地址存放在低地址; 通信模式也要分大小端,先发送/接受的是高地址还是低地址,大端模式:变量的高地址存放在低地址,低地址存放在高地址;
测试:有用共用体 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;
}
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;
<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文件。
#define cdw printf("cdw\n") ; printf("zf\n"); cdw;
这条语句会直接展开。还有带参宏,#define max(a,b) ((a)+(b))
注意的是带参宏一定要()
不然有时候会引起错误,每一个”形参“都应该用()
括起来;
#define year (36524606060*60 )
安理说是可以的,但是year是int型的已经超过了范围,所以要把它搞成无符号长整形。
#define year (36524606060*60ul )
这样才是正确的。
宏定义的变量是不占内存空间的,直接替换减少开销,但是变量替换是不进行类型检查;
函数的变量要占用空间、要压栈等操作,就需要很大的开销,但是调用函数时,编译器会检查函数变量的类型是否相同。
内联函数集合普通函数、宏定义的两个优势,它直接就地展开,直接替换,减少开销,同时编译器也会检查变量的类型。但是函数体积要小,不然效率反而很低,至于
原因暂时不详。
内联函数:对函数就地展开,像宏定义一样,这样减少开销,同时也检查变量的类型。但是必须函数的内部体积小才用这种方式,以达到更好的效率。体积大的函数就作为普通函数。
内联函数通过在函数定义前加inline关键字实现。
条件编译的应用:做一个调试开关。#define DEBUG #undef DEBUG 是注销 DEBUG 宏
ifdef DEBUG
define debug(x) printf(x)
else
define debug(x)
endif
函数:
(1)整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。
(2)函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。
(3)函数的目的就是实现模块化编程。说白了就是为了提供程序的可移植性。
函数书写的一般原则:
之所以函数能被调用,根本实质是在编译时,检查到了该函数的声明,不是因为函数定义了(当然也要定义才行,只是不是本质)。
//栈溢出:递归函数会不停的耗费栈空间所以要注意递归不要太多
//收敛性:必须 要有一个终止递归的条件
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;
函数库:
-static
来强制静态链接。-lxxx
来指定链接;常见的两个库函数
memset strncmp
memcmp strdup
memchr strndup
strcpy strchr
strncpy strstr
strcat strtok
strncat 。。。
strcmp
…
2. 数学函数:math.h 需要加 -lm 就是告诉链接器到libm中去查找用到的函数。
C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。
自制静态链接库
.a
文件,前缀一定要加lib ,如 libzf.a ; 链接属性 -l(小L),表示库名,属性-L表示库的路径。所以:gcc cdw.c -o cdw -lzf -L ./include -I(大i) ./includemakefile: arm-gcc aston.c -o aston.o -c
arm-ar -rc libaston.a aston.o
自制动态链接库
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include
(将库 libmax.so 复制到这个路径下面)这样就可以运行了。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)
概念词:
Linux下的内存映射(分配情况、组织情况):其中有关进程的空间,如进程控制块、页表等都是在内核里面的。文件区是映射外部文件的,如打开记事本,那么这个文件临时存放在文件区域。(见引用资料)
问题:虚拟地址技术? 解决:后期在Linux应用/网络编程会讲。
问题:OS下和裸机下C程序加载执行的差异? 解决:在arm裸机第十六部分有介绍。
int a=3 ,b,c;b=a; c=b;
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
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;
}
restrict
作用域:
总结:
操作系统的理解
C库函数和API的关系
不同平台(windows、linux、裸机)下库函数的差异
函数
main()
函数的写法:int main(int argc,char **argv) ;
和int main(int argc ,char *argv[ ] );
这两种写法是一样的。二重指针等同于指针数组。总结:我们有多种方法都可以执行一个程序,但是本质上是相同的。linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程中去执行直到结束。
新进程是被谁开启?在linux中进程都是被它的父进程fork出来的。
分析:命令行本身就是一个进程,在命令行底下去./xx执行一个程序,其实这个新程序是作为命令行进程的一个字进程去执行的。总之一句话:一个程序被它的父进程所调用。
结论:main函数返回给调用这个函数的父进程。父进程要这个返回值干嘛?父进程调用子进程来执行一个任务,然后字进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。(0表示执行成功,负数表示失败,正规的要求返回失败的原因,返回-1表示什么,返回-2又表示什么,然后父进程好做相应的处理)
用shell脚本来看main()的返回值。如:#!/bin/sh
其文本格式为 .sh
./a.out
echo $?
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,先检验个数,然后使用。
void类型的本质:即使空型又是未知类型,看具体情况。比如一个函数void表示不返回, void *malloc(20);就是未知类型。
C语言中的NULL:NULL在C/C++中的标准定义
#ifdef _cplusplus // 条件编译c++环境
define NULL 0
#else
define NULL (void *)0 // 这里对应C语言的情况
#endif
解释:C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。
正规写:
int *p = NULL; // 定义p时立即初始化为NULL
p = xx;
if (NULL != p)
{
*p = 1; // 在确认p不等于NULL的情况下才去解引用p
}
- '\0’是一个转义字符,他对应的ASCII编码值是0,内存值是0,一个char空间。
- '0’是一个字符,他对应的ASCII编码值是48,内存值是int型48,一个char空间。
- 0是一个数字,没有ASCll编码, 内存值是int型0,一个int空间。
- NULL是一个表达式,是强制类型转换为void *类型的0,内存值是0(内存地址),一个int空间。
运算中的临时匿名变量
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;最后匿名值销毁。分析DEBUG宏
#ifdef DEBUG
define debug(x) printf(x)
#else
define debug(x)
#endif
…
2. 应用级:
#ifdef DEBUG
define DBG(...) fprintf(stderr, " DBG(%s, %s( ), %d): ", FILE, FUNCTION, LINE); fprintf(stderr, VA_ARGS)
#else
define DBG(...)
#endif
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输出错误,会显示在屏幕上。
解释:
<1>…表示变参,提示编译器不要对参数个数斤斤计较,不要报错; 其实完全可以把 …换成 cdw 也是可以的,只是非要装一下而已。
<2> FILE 和 FUNCTION和 LINE 都是c库函数的宏定义,分别表示要输出的这句话属于哪个文件名、属于哪个函数名、在第几行。
<3> 在 fprintf(stderr,“cdw”);其中stderr是c库函数中宏定义了的,这是VC6.0找到的 #define stderr (&_iob[2]) ;也就是说stderr是一个被宏定义了的指针,它是标准错误输出流对象(stderr),输出到屏幕上。
<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
链表是一个一个的节点,每一个节点分为两部分,一部分是数据区(可以由多个类型的数据),另一部分是指向下一个节点的指针;结构体定义里面的变量并没有生成,是不占空间的,相当于声明的作用。
链表的数据存放在内存的那个空间呢?(栈,不灵活,不能用date数据段)所以只能用堆内存,申请一个节点的大小并检测NULL, 要使用它,就得清理它,因为上一个进程用了这段内存,存的是脏数据,
然后对这个节点内存赋值,链接起来.
当要改变头节点是,也就是要给head=p赋值时,必须传 head地址即 形参(struct student *head);这样才能真正改变,不然传一个 (struct student head)只是单纯的赋值。
在scanf(“%d”,&(s->age)) 一定要注意,studeny *s; s->age访问的是一个变量,而不要理解成地址,所以要加&,scanf要注意&;
细节:
node *head=NULL
,想要改变head值通过函数传参是不行的,因为head是一个地址,传参过去,只是赋值给另一个指针而已,只能修改它指向的数据,而本身(地址)是不能修改的,所以要先返回修改好的地址,然后再head=node_add(head)
。node *head=NULL; node *new=(node *)mallo(sizeof(node)); if(NULL!=new){ }
char *name;
应该要用char name[10];
如果使用第一种的话,编译通过,执行错误,因为为name赋值时就要放在代码段中,而代码段已确定了,所以报段错误。头节点、头指针、第一个节点:头节点是一个节点,头节点的下一个指向第一个节点,头节点的数据一般存的是链表长度等信息,也可以是空,头指针指向头节点。链表可以没有头节点,但不能没有头指针。头节点可以想成数组的0位置,其余节点当作从1开始,所以有头节点的长度可以定义为就是含有真实数据节点的个数。
删除一个节点应该做的事:如果这个节点的数据不重要,一定要记住free()掉,你逻辑上删除,其实仍然存在内存中的,头节点的好处就是函数返回值int可以帮助我们一些信息,而没有头节点有时必须返回head;
单链表的优点和缺点:
总结:单链表的单向移动性导致我们在操作单链表时,当前节点只能向后移动不能向前移动,因此不自由,不利于解决更复杂的算法。
内核链表的思想
__list_add() ; list_add();
状态机
一个字节可以表示8位字符,字符真的有256种,128~255表示西欧字符(不常见)。 字符相加的时候,会自动转成 int型加。
在C中,默认的基础数据类型均为signed,现在我们以char为例,说明(signed) char与unsigned char之间的区别。
-127~127
,unsigned char没有符号位,因此能表示0~255
。为什么在链接时需要一个链接地址?因为数据是要放在一个模拟地址内存空间的,它要把这个数据先加载到寄存器,才能给cpu使用,那么寄存器怎么知道是哪个内存地址位置呢,是因为在编译时,编译出像 ldr r0 0x12345678 ,而这个0x12345678就是内存地址,再编译出像 ldr r1,[r0] ,这样就可以拿到0x12345678内存位置的数据了
arm-2009q3.tar.bz2
这套编译器自带了函数库,比如有strcmp , malloc ,printf 等,但是有些库函数我们却不能用他们,比如printf,因为这个函数默认是同过屏幕输出的,而我们常用uart调试。感觉malloc也不能用,因为我们不知道内存哪一块做了堆内存,只有系统才知道。
清bss段:编译器可能已经帮我们做了,只是在重定位那节,因为要重定位那部分内存空间并没有清0 ,所以要手动编程清bss段。