朱有鹏嵌入式学习-C语言和ARM

346 阅读28分钟

1.有关ARM前置知识

  1. ARM的分类和发展历程 image.png
  2. 嵌入式1期课程 有基础群里朱老师建议顺序43然后1256学习

image.png 3. 关于stM32的不同型号

image.png

2.有关c语言的知识

  1. 有关指针

image.png 有关const修饰

image.png 靠近const的是直接P那就是p是常量也就是地址是常量,如果不是那就是p指向的变量是常量,const只是一种信息传递,

4.3.4.2、const修饰的变量真的不能改吗? (1)课堂练习说明:const修饰的变量其实是可以改的(前提是gcc环境下)。 (2)在某些单片机环境下,const修饰的变量是不可以改的。const修饰的变量到底能不能真的被修改,取决于具体的环境,C语言本身并没有完全严格一致的要求。 (3)在gcc中,const是通过编译器在编译的时候执行检查来确保实现的(也就是说const类型的变量不能改是编译错误,不是运行时错误。)所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。 (4)更深入一层的原因,是因为gcc把const类型的常量也放在了data段,其实和普通的全局变量放在data段是一样实现的,只是通过编译器认定这个变量是const的,运行时并没有标记const标志,所以只要骗过编译器就可以修改了。

4.3.4.3、const究竟应该怎么用 (1)const是在编译器中实现的,编译时检查,并非不能骗过。所以在C语言中使用const,就好象是 一种道德约束而非法律约束,所以大家使用const时更多是传递一种信息,就是告诉编译器、也告诉读程序的人,这个变量是不应该也不必被修改的。

总结: 1:&a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。 2:a和&a[0]做右值时意义和数值完全相同,完全可以互相替代。 3:&a是常量,不能做左值。 4:a做左值代表整个数组所有空间,所以a不能做左值。

4.3.6.3、指针和数组类型的匹配问题 (1)int *p; int a[5]; p = a; // 类型匹配 (1)int *p; int a[5]; p = &a; // 类型不匹配。p是int *,&a是整个数组的指针,也就是一个数组指针类型,不是int指针类型,所以不匹配 (2)&a、a、&a[0]从数值上来看是完全相等的,但是意义来看就不同了。从意义上来看,a和&a[0]是数组首元素首地址,而&a是整个数组的首地址;从类型来看,a和&a[0]是元素的指针,也就是int 类型;而&a是数组指针,是int ()[5];类型。

  1. 有关malloc函数的使用
int main(){
//第一步 申请和绑定
int*p = (int *)malloc(1000*sizeof(int))}
//第二步:检验分配是否成功
    if (NULL = p){
        printf("malloc error.\n");
        return -1;
        }
        //第三步:使用
        *(p+1)= 2//第四步:释放
        free(p)
        return 0;
 }

4.5.2.3、malloc的一些细节表现

malloc(0)     

malloc申请0字节内存本身就是一件无厘头事情,一般不会碰到这个需要。 如果真的malloc(0)返回的是NULL还是一个有效指针?答案是:实际分配了16Byte的一段内存并且返回了这段内存的地址。这个答案不是确定的,因为C语言并没有明确规定malloc(0)时的表现,由各malloc函数库的实现者来定义。

malloc(4)

gcc中的malloc默认最小是以16B为分配单位的。如果malloc小于16B的大小时都会返回一个16字节的大小的内存。malloc实现时没有实现任意自己的分配而是允许一些大小的块内存的分配。

malloc(20)去访问第25、第250、第2500····会怎么样

实战中:120字节处正确,1200字节处正确····终于继续往后访问总有一个数字处开始段错误了 sizeof 是一个运算符 4.5.8.2、结构体为何要对齐访问 (1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。 (2)内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率是最高的;如果你不对齐访问效率要低很多。 (3)还有很多别的因素和原因,导致我们需要对齐访问。譬如Cache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。 (4)对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;而非对齐访问牺牲了访问速度性能,换取了内存空间的完全利用。

4.3.8.3、结构体对齐的规则和运算 (1)编译器本身可以设置内存对齐的规则,有以下的规则需要记住: 第一个:32位编译器,一般编译器默认对齐方式是4字节对齐。

总结下:结构体对齐的分析要点和关键: 1、结构体对齐要考虑:结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须4的倍数(编译器设置为4字节对齐时,如果编译器设置为8字节对齐,则这里的4是8) 2、结构体中每个元素本身都必须对其存放,而每个元素本身都有自己的对齐规则。 3、编译器考虑结构体存放时,以满足以上2点要求的最少内存需要的排布来算。

4.5.8.4、gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8) (1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。 (2)常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。 (3)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。 (4)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。

4.5.8.5、gcc推荐的对齐指令__attribute__((packed)) attribute((aligned(n))) (1)attribute((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。 (2)attribute((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐) 4.5.11.offsetof宏与container_of宏 4.5.11.1、由结构体指针进而访问各元素的原理 (1)通过结构体整体变量来访问其中各个元素,本质上是通过指针方式来访问的,形式上是通过.的方式来访问的(这时候其实是编译器帮我们自动计算了偏移量)。

4.5.11.2、offsetof宏: (1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。 (2)offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。 (3)学习思路:第一步先学会用offsetof宏,第二步再去理解这个宏的实现原理。 (TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。 ((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素

&((TYPE *)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0,

4.5.11.3、container_of宏: (1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。 (2)typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。 (3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。

4.5.11.4、学习指南和要求: (1)最基本要求是:必须要会这两个宏的使用。就是说能知道这两个宏接收什么参数,返回什么值,会用这两个宏来写代码。看见代码中别人用这两个宏能理解什么意思。 (2)升级要求:能理解这两个宏的工作原理,能表述出来。(有些面试笔试题会这么要求) (3)更高级要求:能自己写出这两个宏(不要着急,慢慢来)

image.png

image.png 程序调用

image.png union的使用:其实是同一个地址里面的东西,编译器用不同的解析方式来进行解释

image.png

4.5.13.大小端模式1

4.5.13.1、什么是大小端模式

(1)大端模式(big endian)和小端模式(little endian)。最早是小说中出现的词,和计算机本来没关系的。 (2)后来计算机通信发展起来后,遇到一个问题就是:在串口等串行通信中,一次只能发送1个字节。这时候我要发送一个int类型的数就遇到一个问题。int类型有4个字节,我是按照:byte0 byte1 byte2 byte3这样的顺序发送,还是按照byte3 byte2 byte1 byte0这样的顺序发送。规则就是发送方和接收方必须按照同样的字节顺序来通信,否则就会出现错误。这就叫通信系统中的大小端模式。这是大小端这个词和计算机挂钩的最早问题。 (3)现在我们讲的这个大小端模式,更多是指计算机存储系统的大小端。在计算机内存/硬盘/Nnad中。因为存储系统是32位的,但是数据仍然是按照字节为单位的。于是乎一个32位的二进制在内存中存储时有2种分布方式:高字节对应高地址(大端模式)、高字节对应低地址(小端模式) (4)大端模式和小端模式本身没有对错,没有优劣,理论上按照大端或小端都可以,但是要求必须存储时和读取时按照同样的大小端模式来进行,否则会出错。 (5)现实的情况就是:有些CPU公司用大端(譬如C51单片机);有些CPU用小端(譬如ARM)。(大部分是用小端模式,大端模式的不算多)。于是乎我们写代码时,当不知道当前环境是用大端模式还是小端模式时就需要用代码来检测当前系统的大小端。

经典笔试题:用C语言写一个函数来测试当前机器的大小端模式。 第一种:用union来进行测试

union myunion
{
    int a;
    char b;
}
//如果是小端模式则返回1,大端模式则返回0
int is_little_endian(void){
    union myunion u1;
    u1.a = 1
    return u1.b;
}

int main(){
    int i = is_little_endian(void)
    if (i){
    printf("机器是小端模式")
    }

}

第二种:用指针来进行测试(这种其实就是union的本质 即是使用的原理)

//如果是小端模式则返回1,大端模式则返回0
int is_little_endian(void){
    int a = 1;
    char b = *((char *)(&a));
    return b;
}

int main(){
    int i = is_little_endian(void)
    if (i){
    printf("机器是小端模式")
    }

}

4.6.1.4、gcc中只预处理不编译的方法

(1)gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。 (2)gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序

4.6.4.3、宏定义来实现条件编译(#define #undef #ifdef) (1)程序有DEBUG版本和RELEASE版本,区别就是编译时有无定义DEBUG宏。

image.png 有关内联函数 inline

4.6.10.数学库函数 4.6.10.1、math.h (1)真正的数学运算的函数定义在:/usr/include/i386-linux-gnu/bits/mathcalls.h (2)使用数学库函数的时候,只需要包含math.h即可。 4.6.10.2、计算开平方 (1)库函数: double sqrt(double x);

注意区分编译时警告/错误,和链接时的错误: 编译时警告/错误: 4.6.10.math.c:9:13: warning: incompatible implicit declaration of built-in function ‘sqrt’ [enabled by default] double b = sqrt(a); 链接时错误: 4.6.10.math.c:(.text+0x1b): undefined reference to `sqrt' collect2: error: ld returned 1 exit status

分析;这个链接错误的意思是:sqrt函数有声明(声明就在math.h中)有引用(在math.c)但是没有定义,链接器找不到函数体。sqrt本来是库函数,在编译器库中是有.a和.so链接库的(函数体在链接库中的)。 C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。

4.6.10.3、链接时加-lm (1)-lm就是告诉链接器到libm中去查找用到的函数。 (2)实战中发现在高版本的gcc中,经常会出现每加-lm也可以编译链接的。

4.6.11.自己制作静态链接库并使用 (1)第一步:自己制作静态链接库 首先使用gcc -c只编译不连接,生成.o文件;然后使用ar工具进行打包成.a归档文件 库名不能随便乱起,一般是lib+库名称,后缀名是.a表示是一个归档文件 注意:制作出来了静态库之后,发布时需要发布.a文件和.h文件。 创建Makefile里面写

all:
gcc aston.c -o aston.o -c
ar -rc libaston.a aston.o

(2)第二步:使用静态链接库 把.a和.h都放在我引用的文件夹下,然后在.c文件中包含库的.h,然后直接使用库函数。 第一次,编译方法:gcc test.c -o test 报错信息:test.c:(.text+0xa): undefined reference to func1' test.c:(.text+0x1e): undefined reference to func2' 第二次,编译方法:gcc test.c -o test -laston 报错信息:/usr/bin/ld: cannot find -laston collect2: error: ld returned 1 exit status 第三次,编译方法:gcc test.c -o test -laston -L. 无报错,生成test,执行正确。

(3)除了ar名另外,还有个nm命令也很有用,它可以用来查看一个.a文件中都有哪些符号

4.6.12.自己制作动态链接库并使用 (1)动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a (2)第一步:创建一个动态链接库。 gcc aston.c -o aston.o -c -fPIC gcc -o libaston.so aston.o -shared -fPIC是位置无关码,-shared是按照共享库的方式来链接。 注意:做库的人给用库的人发布库时,发布libxxx.so和xxx.h即可。 (3)第二步:使用自己创建的共享库。 第一步,编译方法:gcc test.c -o test 报错信息:test.c:(.text+0xa): undefined reference to func1' test.c:(.text+0x1e): undefined reference to func2' collect2: error: ld returned 1 exit status

第二步,编译方法:gcc test.c -o test -laston 报错信息:/usr/bin/ld: cannot find -laston collect2: error: ld returned 1 exit status

第三步,编译方法:gcc test.c -o test -laston -L. 编译成功

但是运行出错,报错信息: error while loading shared libraries: libaston.so: cannot open shared object file: No such file or directory

错误原因:动态链接库运行时需要被加载(运行时环境在执行test程序的时候发现他动态链接了libaston.so,于是乎会去固定目录尝试加载libaston.so,如果加载失败则会打印以上错误信息。)

解决方法一: 将libaston.so放到固定目录下就可以了,这个固定目录一般是/usr/lib目录。 cp libaston.so /usr/lib即可

解决方法二:使用环境变量LD_LIBRARY_PATH。操作系统在加载固定目录/usr/lib之前,会先去LD_LIBRARY_PATH这个环境变量所指定的目录下去寻找,如果找到就不用去/usr/lib下面找了,如果没找到再去/usr/lib下面找。所以解决方案就是将libaston.so所在的目录导出到环境变量LD_LIBRARY_PATH中即可。 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/Winshare/s5pv210/AdvancedC/4.6.PreprocessFunction/4.6.12.sharedobject.c/sotest

在ubuntu中还有个解决方案三,用ldconfig

(4)ldd命令:作用是可以在一个使用了共享库的程序执行之前解析出这个程序使用了哪些共享库,并且查看这些共享库是否能被找到,能被解析(决定这个程序是否能正确执行)。

4.7.3.2、static (1)static关键字在C语言中有2种用法,而且这两种用法彼此没有任何关联、完全是独立的。其实当年本应该多发明一个关键字,但是C语言的作者觉得关键字太多不好,于是给static增加了一种用法,导致static一个关键字竟然有两种截然不同的含义。 (2)static的第一种用法是:用来修饰局部变量,形成静态局部变量。要搞清楚静态局部变量和非静态局部变量的区别。本质区别是存储类不同(存储类不同就衍生出很多不同):非静态局部变量分配在栈上,而静态局部变量分配在数据段/bss段上。 (3)static的第二种用法是:用来修饰全局变量,形成静态全局变量。要搞清楚静态全局变量和非静态全局变量的区别。区别是在链接属性上不同,讲到链接属性时详细讲。 分析:

1、静态局部变量在存储类方面和全局变量一样。
    
2、静态局部变量在生命周期方面和全局变量一样。**(这里是程序结束的时候)**
    
3、静态局部变量和全局变量的区别是:作用域、连接属性。静态局部变量作用域是代码块作用域(和普通局部变量是一样的)、链接属性是无连接;全局变量作用域是文件作用域(和函数是一样的)、链接属性方面是外连接。
    

4.7.7.5、static的第二种用法:修饰全局变量和函数 (1)普通的(非静态)的函数/全局变量,默认的链接属性是外部的 (2)static(静态)的函数/全局变量,链接属性是内部链接。

4.7.7.6、一般用法总结: 思考:为什么static一个关键字可以有2种完全不同的意思?因为这两种用法是互斥的。

4.7.4.2、volatile

(1)volatile的字面意思:可变的、易变的。C语言中volatile用来修饰一个变量,表示这个变量可以被编译器之外的东西改变。编译器之内的意思是变量的值的改变是代码的作用,编译器之外的改变就是这个改变不是代码造成的,或者不是当前代码造成的,编译器在编译当前代码时无法预知。譬如在中断处理程序isr中更改了这个变量的值,譬如多线程中在别的线程更改了这个变量的值,譬如硬件自动更改了这个变量的值(一般这个变量是一个寄存器的值)

(2)以上说的三种情况(中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量)都是编译器在编译时无法预知的更改,此时应用使用volatile告诉编译器这个变量属于这种(可变的、易变的)情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。

(3)编译器的优化在一般情况下非常好,可以帮助提升程序效率。但是在特殊情况(volatile)下,变量会被编译器想象之外的力量所改变,此时如果编译器没有意识到而去优化则就会造成优化错误,优化错误就会带来执行时错误。而且这种错误很难被发现。

(4)volatile是程序员意识到需要volatile然后在定义变量时加上volatile,如果你遇到了应该加volatile的情况而没有加程序可能会被错误的优化。如果在不应该加volatile而加了的情况程序不会出错只是会降低效率。所以我们对于volatile的态度应该是:正确区分,该加的时候加不该加的时候不加,如果不能确定该不该加为了保险起见就加上。

4.7.4.3、restrict (1)c99中才支持的,所以很多延续c89的编译器是不支持restrict关键字,gcc支持的。 (2)restrict也是和编译器行为特征有关的。 (3)restrict只用来修饰指针,不能修饰普通变量。 (4)blog.chinaunix.net/uid-2219790… (5)看memcoy和memmove函数的区别

4.7.8.最后的总结

(1)普通(自动)局部变量分配在栈上,作用域为代码块作用域,生命周期是临时,连接属性为无连接。定义时如果未显式初始化则其值随机,变量地址由运行时在栈上分配得到,多次执行时地址不一定相同,函数不能返回该类变量的地址(指针)作为返回值。

(2)静态局部变量分配在数据段/bss段(显式初始化为非0则在数据段,显式初始化为0或未显示初始化则在bss段),作用域为代码块作用域(人为规定的),生命周期为永久(天然的),链接属性为无连接(天然的)。定义时如果未显式初始化则其值为0(天然的),变量地址由运行时环境在加载程序时确定,整个程序运行过程中唯一不变;静态局部变量其实就是作用域为代码块作用域(同时链接属性为无连接)的全局变量。静态局部变量可以改为用全局变量实现(程序中尽量避免用全局变量,因为会破坏结构性)。

(3)静态全局变量/静态函数和普通全局变量/普通函数的唯一差别是:static使全局变量/函数的链接属性由外部链接(整个程序所有文件范围)转为内部链接(当前c文件内)。这是为了解决全局变量/函数的重名问题(C语言没有命名空间namespace的概念,因此在程序中文件变多之后全局变量/函数的重名问题非常严重,将不必要被其他文件引用的全局变量/函数声明为static可以很大程度上改善重名问题,但是仍未彻底解决)。

(4)写程序尽量避免使用全局变量,尤其是非static类型的全局变量。能确定不会被其他文件引用的全局变量一定要static修饰。(因为全局变量是连接的,可能会和其他文件的变量重名)

(5)注意区分全局变量的定义和声明。一般规律如下:如果定义的同时有初始化则一定会被认为是定义;如果只是定义而没有初始化则有可能被编译器认为是定义,也可能被认为是声明,要具体分析;如果使用extern则肯定会被认为是声明(实际上使用extern也可以有定义,实际上加extern就是明确声明这个变量为外部链接属性)。

(6)全局变量应该定义在c文件中并且在头文件中声明,而不要定义在头文件中(因为如果定义在头文件中,则该头文件被多个c文件包含时该全局变量会重复定义)。

(7)在b.c中引用a.c中定义的全局变量/函数有2种方法:一是在a.h中声明该函数/全局变量,然后在b.c中#include <a.h>;二是在b.c中使用extern显式声明要引用的函数/全局变量。其中第一种方法比较正式。

(8)存储类决定生命周期,作用域决定链接属性

(9)宏和inline函数的链接属性为无连接。

C语言实现链表 链表的创建和逆序 blog.csdn.net/qq_32378713…

#include <stdio.h>
#include <strings.h>
#include <stdlib.h>


// 构建一个链表的节点
struct node
{
	int data;				// 有效数据
	struct node *pNext;		// 指向下一个节点的指针
};

// 作用:创建一个链表节点
// 返回值:指针,指针指向我们本函数新创建的一个节点的首地址
struct node * create_node(int data)
{
	struct node *p = (struct node *)malloc(sizeof(struct node));
	if (NULL == p)
	{
		printf("malloc error.\n");
		return NULL;
	}
	// 清理申请到的堆内存
	bzero(p, sizeof(struct node));
	// 填充节点
	p->data = data;
	p->pNext = NULL;	
	
	return p;
}

// 思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。
// 计算添加了新的节点后总共有多少个节点,然后把这个数写进头节点中。
void insert_tail(struct node *pH, struct node *new)
{
	int cnt = 0;
	// 分两步来完成插入
	// 第一步,先找到链表中最后一个节点
	struct node *p = pH;
	while (NULL != p->pNext)
	{
		p = p->pNext;				// 往后走一个节点
		cnt++;
	}
	
	// 第二步,将新节点插入到最后一个节点尾部
	p->pNext = new;
	pH->data = cnt + 1;
}

// 思路:
void insert_head(struct node *pH, struct node *new)
{
	// 第1步: 新节点的next指向原来的第一个节点
	new->pNext = pH->pNext;
	
	// 第2步: 头节点的next指向新节点的地址
	pH->pNext = new;
	
	// 第3步: 头节点中的计数要加1
	pH->data += 1;
}

// 遍历单链表,pH为指向单链表的头指针,遍历的节点数据打印出来
void bianli(struct node*pH)
{
	//pH->data				// 头节点数据,不是链表的常规数据,不要算进去了
	//struct node *p = pH;		// 错误,因为头指针后面是头节点
	struct node *p = pH->pNext;	// p直接走到第一个节点
	printf("-----------开始遍历-----------\n");
	while (NULL != p->pNext)		// 是不是最后一个节点
	{
		printf("node data: %d.\n", p->data);
		p = p->pNext;				// 走到下一个节点,也就是循环增量
	}
	printf("node data: %d.\n", p->data);
	printf("-------------完了-------------\n");
}

// 1、思考下为什么这样能解决问题;2、思考下设计链表时为什么要设计头节点
void bianli2(struct node*pH)
{
	//pH->data				// 头节点数据,不是链表的常规数据,不要算进去了
	struct node *p = pH;		// 头指针后面是头节点

	printf("-----------开始遍历-----------\n");
	while (NULL != p->pNext)		// 是不是最后一个节点
	{
		p = p->pNext;				// 走到下一个节点,也就是循环增量
		printf("node data: %d.\n", p->data);
	}

	printf("-------------完了-------------\n");
}

// 从链表pH中删除节点,待删除的节点的特征是数据区等于data
// 返回值:当找到并且成功删除了节点则返回0,当未找到节点时返回-1
int delete_node(struct node*pH, int data)
{
	// 找到这个待删除的节点,通过遍历链表来查找
	struct node *p = pH;			// 用来指向当前节点
	struct node *pPrev = NULL;		// 用来指向当前节点的前一个节点

	while (NULL != p->pNext)		// 是不是最后一个节点
	{
		pPrev = p;					// 在p走向下一个节点前先将其保存
		p = p->pNext;				// 走到下一个节点,也就是循环增量
		// 判断这个节点是不是我们要找的那个节点
		if (p->data == data)
		{
			// 找到了节点,处理这个节点
			// 分为2种情况,一个是找到的是普通节点,另一个是找到的是尾节点
			// 删除节点的困难点在于:通过链表的遍历依次访问各个节点,找到这个节点
			// 后p指向了这个节点,但是要删除这个节点关键要操作前一个节点,但是这
			// 时候已经没有指针指向前一个节点了,所以没法操作。解决方案就是增加
			// 一个指针指向当前节点的前一个节点
			if (NULL == p->pNext)
			{
				// 尾节点
				pPrev->pNext = NULL;		// 原来尾节点的前一个节点变成新尾节点
				free(p);					// 释放原来的尾节点的内存
			}
			else
			{
				// 普通节点
				pPrev->pNext = p->pNext;	// 要删除的节点的前一个节点和它的后一个节点相连,这样就把要删除的节点给摘出来了
				free(p);
			}
			// 处理完成之后退出程序
			return 0;
		}
	}
	// 到这里还没找到,说明链表中没有我们想要的节点
	printf("没找到这个节点.\n");
	return -1;
}

// 将pH指向的链表逆序
void reverse_linkedlist(struct node *pH)
{
	struct node *p = pH->pNext;		// pH指向头节点,p指向第1个有效节点
	struct node *pBack;				// 保存当前节点的后一个节点地址
	
	// 当链表没有有效节点或者只有一个有效节点时,逆序不用做任何操作
	if ((NULL ==p) || (NULL == p->pNext))
		return;
	
	// 当链表有2个及2个以上节点时才需要真正进行逆序操作
	while (NULL != p->pNext)		// 是不是最后一个节点
	{
		// 原链表中第一个有效节点将是逆序后新链表的尾节点,尾节点的pNext指向NULL
		pBack = p->pNext;			// 保存p节点后面一个节点地址
		if (p == pH->pNext)
		{
			// 原链表第一个有效节点
			p->pNext = NULL;
		}
		else
		{
			// 原链表的非第1个有效节点
			p->pNext = pH->pNext;
		}
		pH->pNext = p;
		
		//p = p->pNext;		// 这样已经不行了,因为p->pNext已经被改过了
		p = pBack;			// 走到下一个节点
	}
	// 循环结束后,最后一个节点仍然缺失
	insert_head(pH, p);
}


int main(void)
{
	// 定义头指针
	//struct node *pHeader = NULL;			// 这样直接insert_tail会段错误。
	struct node *pHeader = create_node(0);
	
	insert_tail(pHeader, create_node(11));
	insert_tail(pHeader, create_node(12));
	insert_tail(pHeader, create_node(13));
	insert_tail(pHeader, create_node(14));

	bianli2(pHeader);
	
	reverse_linkedlist(pHeader);
	printf("------------------逆序后-------------\n");
	bianli2(pHeader);
	
	return 0;
}

有关可变参数:

www.runoob.com/cprogrammin…

linux中使用man来进行查询

man 1 cd        //标准用户命令
man 2 read      //系统调用
man 3 sleep()   //库函数
/line           //查找line
 查看时需要翻屏:

        向后翻一屏:space(空格键)      向前翻一屏:b

        向后翻一行:Enter(回车键)       向前翻一行:k

   查看时需要查找:

    /关键词      向后查找    n:下一个

    ?关键词     向前查找    N:前一个

3. 有关操作系统的前置知识(哈工大李治军)

进程:表示进行/执行的程序,用来描述运行的程序,和静态程序不一样

image.png

内核级线程

image.png

image.png