Linux C 经典题目总结

更新至: 2012-04-05

Author : Boatman Yang 

【第一部分 C基本概念】

【几个关键字】

1)、auto关键字: 声明变量的生存期为自动,即将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。不明白?无视他好了,编译器默认的缺省情况下,所有的变量都是auto的。

2)、extern关键字: 我们都知道,一个变量或函数,可以在a.c文件中定义,而在b.c文件中使用,这个时候,b.c就需要使用extern关键字来声明这个变量和函数,目的是为了告诉编译器,这个函数在b.c之外,别让我编译不过!

3)、register关键字: 这个关键字就很少用到了,但是却十分有用。它的目的是告诉编译器尽量把这个变量放到寄存器中,这样提高存取速度,但是不是能真的放到寄存器中却不一定,毕竟寄存器的数量是有限的。在我们的二进制翻译器中,这个关键字被巧妙的用于线程切换。

4)、static关键字: 好吧,我承认我土了,我就是栽在这个关键字上的。static有两种修饰,分别如下:

(1)修饰变量:变量分为全局变量和静态变量,都存储在内存的静态区中。

首先,当static修饰全局变量的时候,该变量的作用域仅被限定在当前文件中,别的文件即使使用extern关键字也无法使用这个变量。

其次,当static修饰局部变量的时候,该变量在哪个函数体中定义,就只能在哪个函数体中使用。也许你会说,这不跟普通局部变量一样么?不一样!别忘了他是被存储在内存的静态区中,所谓的静态区就是全局区,用来存放全局变量和静态变量的,程序不结束,这个区是不会被释放的,所以即使定义静态局部变量的函数结束,改静态局部变量仍然存在,下次访问改函数的时候,这个变量的值仍然是上次的值!

(2)修饰函数: 经常见这种形式,但没怎么用过,也就没去想。其实这个作用跟静态全局变量相似,也是限定函数的作用域为本文件。这样作的好处就是不用操心是否会跟别人编写的文件里的函数重名。

(3)注意:函数的形参不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。

在《c和指针》中这样描述:当static用于函数定义时,或用于代码之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。

当static用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。

5)、const关键字: 这是一个很有意思的关键字,他修饰的变量是只读的,不能被修改;很多时候,编译器会将其优化成一个常量。const经常被用来修饰函数的参数,表示不希望这个参数值被函数体内的代码意外的改变。其实,最有意思的是用const修饰一个指针,让我们看下面这个例子:

        
       const int *p;   //p可变,p指向的对象不可变
        int const *p;   //同上
        int *const p;   //p不可变,p指向的对象可变
        const int *const p; //p和p指向的对象都不可变

这些各表示什么呢?注释里面给出了答案!是不是很不好记?我们只需要记得,const修饰的是*p的时候,p指向的内容不可变;const修饰的是p的时候,p就不可变!

6). sizeof关键字:很多人也许会大吃一斤,我类个去,sizeof居然是关键字?(高手请无视这里,我当初就是这种表现)。不错,sizeof确实是关键字,而不是库函数!所以,如果编译时得不到一个数组的大小,那么就不能使用sizeof关键字来获取改数组的大小!

7). typedef关键字: typedef说白了就是给一个已知的类型起一个外号。

8). volatile关键字: 也许你见过这个关键字,但一般你都没有用过。哈哈,我用过!这个关键字表示改变量的值可能在外部被改变,编译器在用到这个变量时不能过度的优化,必须每次都重新从内存中读取这个变量的值,而不是将其优化在寄存器中。

【链接】:c定义了三类链接:外部链接,内部链接和无链接。

通常,函数和全局变量具有外部链接,这意味着它们对构成程序的所有文件是可用的。

声明为static的文件域对象具有内部链接,仅在声明它们的文件中是已知的。

局部变量没有链接,因此仅在他们自己的块中是已知的。

【对齐】:

使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
 使用伪指令#pragma pack (),取消自定义字节对齐方式

 __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
 __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

【Standard linux memory layout

【变量的值】:就是分配给该变量的内存位置所存储的数值,即使指针也不例外。

inline函数和用macro定义的函数区别

Macro定义
只是很初级的一种代换,实现的功能很单一 而且安全性很差,比如类型错误、括号漏写
都会造成很大的错误, 而且错误不容易被发现,隐患很大


inline函数

内联函数要比前者好很多 功能也要全面很多! 最主要的是内联函数能够进行安全检查(比如参数类型等) 如果在能够使用两着的情况之下 推荐使用内联

不过有两点要注意:
1     内联   是以代码膨胀为代价的,
   
      不是所有的函数都适合用   内联   方式
 
      要考虑函数的实际情况
  
2     macro定义   也不是说一无是处了
 
      在合适的时候使用   也许会有意想不到的效果

【汇编基础】

ebp和esp是32位的SP,BP 
esp是堆栈指针 bp是基址指针

EIP寄存器里存储的是CPU下次要执行的指令的地址。 
ESP与SP的关系就象AX与AL,AH的关系
.

32位CPU所含有的寄存器有:


4个数据寄存器(EAX、EBX、ECX和EDX)
2个变址和指针寄存器(ESI和EDI) 2个指针寄存器(ESP和
EBP)
6个段寄存器(ES、CS、SS、DS、FS和
GS)
1个指令指针寄存器(EIP) 1个标志寄存器(EFlags)

附:Linux下使用objdump –S obj可以反汇编

【第二部分 Linux基础】

【编译过程】通常在使用gcc编译程序时,编译过程通常会分为4个阶段,即预处理编译汇编链接

【僵尸进程】如果某个进程自身终止了,在调用exit清理完相关的内容文件等资源后,它就会进入ZOMBIE状态

【孤儿进程】当某个进程还在运行时,它的父进程却退出了,这个进程却没有成为孤儿进程

fork两次产生守护进程】父进程先fork出一个儿子进程,儿子进程再fork出孙子进程做为守护进程,然后儿子进程立刻退出,守护进程被init进程接管,这样无论父进程做什么事,无论怎么被阻塞,都与守护进程无关了。所以,fork两次的守护进程很安全,避免了僵尸进程出现的可能性。

【内核:各种地址】CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址

inline

通过在函数声明的前面加上inline,即可告诉编译程序优化对函数的调用。从技术上讲,这意味着函数的代码将被有序扩展而不是调用。当然,inline只是对编译程序的一个请求,可以忽略。注意,c++也支持inline说明符。

Linux内存分配类型】

系统为进程分配数据空间有三种形式。

静态分配

整块静态分配空间,包括其中的所有数据实体,都是在进程创建时由系统一次性分配的(同时为UNIX称为Text的代码分配空间)。这块空间在进程运行期间保持不变。

初始化的和未初始化的实体分别放在初始化数据段和未初始化数据段(BSS)。后者和前者不同,在.o文件a.out文件里都不存在(只有构架信息),在进程的虚拟空间里才展开。

extern变量和static变量采用静态分配。

在进程创建时做静态分配,分配正文(text)段、数据段和栈空间。

正文和初始化数据是按a.out照样复制过来;未初始化数据按构架信息展开,填以0或空;栈空间的大小由链接器开关(具体哪个开关忘了)决定。

栈分配

整个栈空间已在进程创建时分配好。栈指针SP的初值的设定,确定了栈空间的大小。链接器的某个开关可以设定栈空间的大小。在进程运行期间,栈空间的大小不变。但是,在进程刚启动时,栈空间是空的,里面没有实体。在进程运行期间,对具体实体的栈分配是进程自行生成(压栈)和释放(弹出)实体,系统并不参与。

auto变量和函数参数采用栈分配。

只要压入的实体的总长度不超过栈空间尺寸,栈分配就与系统无关。如果超过了,就会引发栈溢出错误。

堆分配

当进程需要生成实体时,向系统申请分配空间;不再需要该实体时,可以向系统申请回收这块空间。

堆分配使用特定的函数(如malloc()等)或操作符(new)。所生成的实体都是匿名的,只能通过指针去访问。

对实体来说,栈分配和堆分配都是动态分配:实体都是在进程运行中生成和消失。而静态分配的所有实体都是在进程创建时全部分配好的,在运行中一直存在。

同为动态分配,栈分配与堆分配是很不相同的。前者是在进程创建时由系统分配整块栈空间,以后实体通过压栈的方式产生,并通过弹出的方式取消。不管是否产生实体,产生多少实体,栈空间总是保持原来的大小。后者并没有预设的空间,当需要产生实体时,才向系统申请正好能容纳这个实体的空间。当不再需要该实体时,可以向系统申请回收这块空间。因此,堆分配是真正的动态分配。

显然,堆分配的空间利用率最高。

栈分配和静态分配也有共性:整块空间是在进程创建时由系统分配的。但是,后者同时分配了所有实体的空间,而前者在进程启动时是空的。另外,栈上的实体和数据段里的实体都是有名实体,可以通过标识符来访问。

 

静态分配

栈分配

堆分配

整块空间生成

进程创建时

进程创建时

用一点分配一点

实体生成时间

进程创建时

进程运行时

进程运行时

实体生成者

操作系统

进程

进程申请/系统实施

生命期

永久

临时

完全可控

有名/匿名

有名

有名

匿名

访问方式

能以标识访问

能以标识访问

只能通过指针访问

空间可否回收

不可

不可

可以

 

栈溢出的后果是比较严重的,或者出现Segmentation fault错误,或者出现莫名其妙的错误。

Linux大小端问题】

  所谓的大端模式,是指数据的低位(就是权值较小的后面那几位)保存在内存的高地址中,而数据的高位,保存在内存的低地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;

所谓的小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

【自旋锁、互斥量】

自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。

uboot

Boot Loader stage1 通常包括以下步骤(以执行的先后顺序)

1.       硬件设备初始化(关看门狗,关中断,设置cpu时钟,初始化sdram,关闭 CPU 内部指令/数据 cache)。

2.       为加载 Boot Loader stage2 准备 RAM 空间。

3.       拷贝 Boot Loader stage2 RAM 空间中。

4.       设置好堆栈。

5.       跳转到 stage2 C 入口点。

Boot Loader stage2 通常包括以下步骤(以执行的先后顺序)

1.       初始化本阶段要使用到的硬件设备。

2.       检测系统内存映射(memory map)

3.       kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中。

4.       为内核设置启动参数。

5.       调用内核。

【第三部分 Linux内核和驱动】

sofitirqtaskletworkqueue

中断处理分为两个部分:上半部和下半部。中断处理程序属于上半部,这三者属于下半部分,下半部的任务就是执行与中断处理程序密切相关但中断处理程序本身不执行,推后执行的工作。

 

1软中断代码最为简洁,执行效率也最快,但相同类型的软中断可以同时执行(包扩自己同时是指同一时刻在多个cpu上执行)这样一来它提供的执行序列化保障也就最少,意味着在软中断中必须对同享数据进行加锁,而且软中断不同于进程,不能重新调度,这样任何潜在睡眠的代码不能出现在软中断中。
2tasklet 在软中断的基础上演化行成,对数据结构加了一些限制所以执行效率上较软中断慢些,但提供了执行序列化的保障,相同类型的tasklet不能同时执行,不同类型可以。但是若出现不同类型的tasklet同享数据仍需要相应的同步处理。tasklet与软中断类似不能睡眠。
3工作队列完全不同于软中断和tasklet,将下半部封装为内核线程的一个任务,是唯一一种可以允许睡眠的方法,当然性能开销也是最大的。同步处理同线程。
总之,选择时试问你有任何休眠的需要吗?有,那工作队列是你唯一的选择。否则最好用tasklet,要是必须专注性能,那就考虑软中断吧。

likely and unlikely

定义形式:

#define unlikely(x) __builtin_expect(!!(x), 0)

#define unlikely(x) __builtin_expect(!!(x), 1)

作用:

unlikely指示gcc编译器这种情况很少发生,在编译优化的时候分支预测会跳到else情况中。

likely指示这种情况发生的多,分支预测按照下面的语句走。

事实上,根据这两个函数可以得知条件语句中最可能发生的情形,从而告知编译器以允许它正确地优化条件分支。

 

skb_clone and skb_copy

skb_copy是对skb的数据完整复制,也可以说是深拷贝。

skb_clone并没有真正申请数据段,只是申请了一个skb_struct,并让其指针指向原skb的数据段。

对比skb_copyskb_clone,前者是一个深拷贝,而后者只是一个浅拷贝。

 

kmalloc and vmallocmalloc

1.       kmallocvmalloc是分配的是内核的内存,malloc分配的是用户的内存

2.       kmallocmalloc 相似,该函数返回速度快快(除非它阻塞)并对其分配的内存不进行初始化(清零),分配的区仍然持有它原来的内容,分配的区也是在物理内存中连续。kmalloc 能够分配的内存块的大小有一个上限。kmalloc分配内存是基于slab,因此slab的一些特性包括着色,对齐等都具备,性能较好。物理地址和逻辑地址都是连续的。

原型:void *kmalloc(size_t size, int flags);

———————————————————————————————-
情形                                       相应标志
———————————————————————————————-
进程上下文,可以睡眠                   GFP_KERNEL
进程上下文,不可以睡眠                GFP_ATOMIC
中断处理程序                             GFP_ATOMIC
软中断                                    GFP_ATOMIC
Tasklet                                    GFP_ATOMIC
用于DMA的内存,可以睡眠            GFP_DMA | GFP_KERNEL
用于DMA的内存,不可以睡眠         GFP_DMA | GFP_ATOMIC
———————————————————————————————-

3.       vmalloc分配内存的时候逻辑地址是连续的,但物理地址一般是不连续的,适用于那种一下需要分配大量内存的情况,如insert模块的时候。这种分配方式性能不如kmalloc

4.       kmalloc能分配的大小有限,vmallocmalloc能分配的大小相对较大

5.       内存只有在要被DMA访问的时候才需要物理上连续

6.       vmallockmalloc要慢。最主要的区别是分配大小的问题。
比如你需要28个字节,那一定用KMALLOC,如果用VMALLOC,分配不多次机器就罢工了。

OFFSET_OF(type,member)  SIZE_OF

1)、#define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER )

1. ( (TYPE *)0 ) 将零转型为TYPE类型指针;
2. ((TYPE *)0)->MEMBER 访问结构中的数据成员;
3. &( ( (TYPE *)0 )->MEMBER )取出数据成员的地址;
4.(size_t)(&(((TYPE*)0)->MEMBER))结果转换类型;
巧妙之处在于将0转换成(TYPE*),结构以内存空间首地址0作为起始地址,则成员地址自然为偏移地址。2) SIZE_OF宏

2). #define SIZEOF(s,m) sizeof(((s *)0)->m)(引申使用)

3). #define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})


 

宏功能:从结构体(type)某成员变量(member)指针(ptr)来求出该结构体(type)的首指针。

1、typeof( ( (type *)0)->member )为取出member成员的变量类型。

2、定义__mptr指针ptr为指向该成员变量的指针

3、mptr为member数据类型的常量指针,其指向ptr所指向的变量处

4、.(char *)__mptr转换为字节型指针。(char *)__mptr - offsetof(type,member))用来求出结构体起始地址(为char *型指针),然后(type *)( (char *)__mptr -offsetof(type,member) )在(type *)作用下进行将字节型的结构体起始指针转换为type *型的结构体起始指针。
5、.({ })这个扩展返回程序块中最后一个表达式的值。

【自旋锁如何保护critical region

自旋锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转,直到第1个线程释放自旋锁。注意:这里所说的线程不是内核线程,而是执行的线程。

【互斥体】

与自旋锁不同的是,互斥体在进入一个被占用的临界区之前不会原地打转,而是使当前线程进入睡眠状态。如果要等待的时间较长,互斥体比自旋锁更合适,因为自旋锁会消耗CPU资源。在使用互斥体的场合,多于2次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠,而当其被唤醒时,它需要被切换回来。

因此,在很多情况下,决定使用自旋锁还是互斥体相对来说很容易:

(1) 如果临界区需要睡眠,只能使用互斥体,因为在获得自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的;

(2) 由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自旋锁。

[ISR即中断服务程序中可以调用printk函数吗?]

尽量不要使用,printk可能会引起睡眠。而中断处理程序是不允许睡眠的!

【下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。】

__interrupt double compute_area (double radius) 
{
    double area = PI * radius * radius;
    printf("\nArea = %f", area);
    return area;
}


 

这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
2) ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3) 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4) 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

 

【第四部分编程】

【指针】

int *p; 

p++; (p=p+sizeof(int))

p=p+12;p指向当前位置后的第12个元素。


 

【变元函数】func/*illegal*/

cc++函数原型声明】

cc++处理函数原型的方式上,存在着细小但重要的差别,C++的函数原型没有参数。在C++中,原型中不带任何参数表示空参数表。

C中,函数没有参数时,其原型使用参数表中的void。如,float f(void) ; 它告诉编译程序该函数没有参数,且对没有变元的函数的任何调用都将出错。

【向函数传递全结构】

结构用做函数的变元时,用标准值调用规则把全结构传给函数。因此,函数对结构内容的修改不影响原结构,即退出函数后,原结构内容不变。同时,当执行函数调用时,向栈中压入数据需要花费开销。

向函数传结构指针时,压栈的只是指针本身,使函数调用特别快;在有些情况下,传递指针的第二个优点是,在传递指针时,该函数可以修改作为变元的结构的内容。

cc++结构声明方法不同】

C++中,一旦结构已经声明,仅使用此结构的标记即可声明此类变量,而不必在其前面加上关键字struct。造成这种差别的原因是,在C中,结构名并未定义完整的类型名。这就是C语言将这个名字称为标记的原因。但在c++中,结构名是完整的类型名,可以被自身用以定义变量。但请记住,在C++程序中使用c风格的声明始终是完全合法的。上述讨论可以推   广到联合和枚举。

【位域】C语言具有访问字节中位的内设机制。type namelength

struct bs
{
 int a:8;
 int b:2;
 int c:6;
}data;


 

说明databs变量,共占两个字节。其中位域a8位,位域b2位,位域c6位。对于位域的定义尚有以下几点说明:

1.       一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。

2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。

 

sizeof联合的尺寸总是等于联合中最大的成员的尺寸。

charunsigned char的区别】

char的值在-128127之间,

unsigned char的值在0255之间

for example:

char c = 0x8f;

printf("c is %d. \n",c);//113,取反加一


 

【右移一位等于除以2右移一位就等于除以2,但是这里需要加一个条件,这里指的是正数。而对于有符号整数,且其值为负数时,在C99标准中对于其右移操作的结果的规定是implementation-defined.

【大小端程序实现】

unsigned char chk_cpu(){

int data=0x0011;

char temp=*(char *)&data;

if(temp==0) return 0; //大端返回0;

else return 1;   //小段返回1;

}


 

【一个二进制数中含有的“1”的个数】

int count(int val)

{

         int num = 0;

         while(val)

         {        

                   num += val & 0x01;

                   val>>=1;

         }

         return num;

}


 

【实现memmove

void memmove(void* pDest, const void* pSrc, unsigned int count)
{
char* dest ;

const char *src;
int i;
dest = pDest;
src = pSrc;
if((pDest < pSrc)|| (pSrc + count > pDest))
{ 
for(i = 0; i < count; i++)
   *dest++ = *src++;
}
else
{ //有重叠的情形
   dest += count - 1;
   src += count - 1;
   for(i = 0; i < count; i++)
      *dest-- = *src--;
}
}


find the nth to the last of single linked node 查找链表中倒数第k个结点】

方法一:求出正数第n-k+1个结点即可

代码如下:

ListNode* FindKthToTail_Solution1(ListNode* pListHead, unsigned int k)
{
         if(pListHead == NULL)
                   return NULL;

         // count the nodes number in the list
         ListNode *pCur = pListHead;
         unsigned int nNum = 0;
         while(pCur->m_pNext != NULL)
         {
                   pCur = pCur->m_pNext;
                   nNum ++;
         }

         // if the number of nodes in the list is less than k
         // do nothing
         if(nNum < k)
                   return NULL;

         // the kth node from the tail of a list 
         // is the (n - k)th node from the head
         pCur = pListHead;
         for(unsigned int i = 0; i < nNum - k; ++ i)
                   pCur = pCur->m_pNext;

         return pCur;
}


 

方法二:如果我们在遍历时维持两个指针,第一个指针从链表的头指针开始遍历,在第k-1步之前,第二个指针保持不动;在第k-1步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第k个结点。

这种思路只需要遍历链表一次。对于很长的链表,只需要把每个结点从硬盘导入到内存一次。因此这一方法的时间效率前面的方法要高。

代码如下:

ListNode* FindKthToTail_Solution2(ListNode* pListHead, unsigned int k)
{
         if(pListHead == NULL)
                   return NULL;

         ListNode *pAhead = pListHead;
         ListNode *pBehind = NULL;
//先让头指针走k-1步
         for(unsigned int i = 0; i < k; ++ i)
         {
                   if(pAhead->m_pNext != NULL)
                            pAhead = pAhead->m_pNext;
                   else
                   {
                            // if the number of nodes in the list is less than k, 
                            // do nothing
                            return NULL;
                   }
         }

//然后两个指针同时行动
         pBehind = pListHead;
         // the distance between pAhead and pBehind is k
         // when pAhead arrives at the tail, p
         // Behind is at the kth node from the tail
         while(pAhead->m_pNext != NULL)
         {
                   pAhead = pAhead->m_pNext;
                   pBehind = pBehind->m_pNext;
         }

         return pBehind;
}


 

【单链表全】

typedef struct node

{

    int nDate;

    struct node *pstnext;

}Node;

//链表输出

void output(Node *head)

{

    Node *p = head->pstnext;

    while(NULL != p)

    {

          printf("%d  ", p->nDate);

          p = p->pstnext;

    }

    printf("\r\n");

}

//链表建立

Node* creat()

{

    Node *head = NULL, *p = NULL, *s = NULL;

    int Date = 0, cycle = 1;

    head = (Node*)malloc(sizeof(Node));

    if(NULL == head)

    {

          printf("分配内存失败\r\n");

          return NULL;

    }

    head->pstnext = NULL;

 

    p = head;

    while(cycle)

    {

        printf("请输入数据且当输入数据为0时结束输入\r\n");

        scanf("%d", &Date);

        if(0 != Date)

        {

           s = (Node*)malloc(sizeof(Node));

           if(NULL == s)

           {

                    printf("分配内存失败\r\n");

                    return NULL;

           }

           s->nDate = Date;

           p->pstnext = s;

           p = s;

      }

      else

      {

            cycle = 0;

      }

    }

    p->pstnext = NULL;

    return(head);

}

//单链表测长

void length(Node *head)

{

    Node *p = head->pstnext;

    int j=0;

    while(NULL != p)

    {

          p = p->pstnext;

          j++;

    }

    printf("%d\r\n", j);

}

//链表按值查找

void research_Date(Node *head, int date)

{

    Node *p;

    int n=1;

    p = head->pstnext;

    while(NULL != p && date != p->nDate)

    {

          p = p->pstnext;

          ++n;

    }

    if(NULL == p)

    {

        printf("链表中没有找到该值");

    }

    else if(date == p->nDate)

    {

        printf("要查找的值%d在链表中第%d个位置\r\n", date, n);

    }

    return;

}

//按序号查找

void research_Number(Node *head, int Num)

{

    Node *p=head;

    int i=0;

    while(p!=NULL )

    {

        p=p->pstnext;

        i++;

        if( i == Num)

            break;

    }

    if(p == NULL)

    {

        printf("Not found\n");

    }

     else if(i == 0)

    {

        printf("查找位置为头结点\r\n");

    }

    else

    {

        printf("the %d date is %d.\n",Num,p->nDate);

    }

}

 

//在指定元素之前插入新结点

void insert_1(Node *head, int i, int Newdate)

{

    Node *pre = head, *New = NULL;

    int j = 0;

    while(NULL != pre && j < i-1)

    {

          pre = pre->pstnext;

          j++;

    }

    if(NULL == pre || j > i-1)

    {

        printf("插入位置不存在\r\n");

    }

    else

    {

        New = (Node*)malloc(sizeof(Node));

        if(NULL == New)

        {

            printf("分配内存失败\r\n");

            return;

        }

        New->nDate = Newdate;

        New->pstnext = pre->pstnext;

        pre->pstnext = New;

    }

 

}

//在指定元素之后插入新结点

void insert_2(Node *head, int i, int Newdate)

{

    Node *pre = head, *New = NULL;

    int j = 0;

    while(NULL != pre->pstnext && j < i)

    {

        pre = pre->pstnext;

        j++;

    }

    if(j == i)

    {

        New = (Node*)malloc(sizeof(Node));

        if(NULL == New)

        {

            printf("分配内存失败\r\n");

            return;

        }

        New->nDate = Newdate;

        New->pstnext = pre->pstnext;

        pre->pstnext = New;

    }

    else

    {

        printf("插入位置不存在\r\n");

    }

}

//删除指定结点

void Delete_1(Node *head, int i3)

{

    Node *p = head, *pre = NULL;

    int j = 0;

    while(NULL != p && j < i3)

    {

          pre = p;

          p = p->pstnext;

          j++;

    }

    if(NULL == p)

    {

        printf("删除位置不存在\r\n");

    }

    else

    {

        pre->pstnext = p->pstnext;

        free(p);

    }

}

//指定删除单链表中某个数据,并统计删除此数据的个数

int Delete_2(Node *head, int Delete_date)

{

    Node *pTmp = NULL, *nextNode=NULL;

    pTmp = head;

    int count = 0;

    while(pTmp->pstnext != NULL)

    {

        nextNode = pTmp->pstnext;

        if( Delete_date == nextNode->nDate )

        {

            count++;

            pTmp->pstnext = nextNode->pstnext;

            free(nextNode);

        }

         else

         {

             pTmp = nextNode;

         }

    }

    return count;

}

 

//链表逆置

void Reverse_list(Node *head)

{

    Node *q, *s;

    if(NULL == head->pstnext || NULL == head->pstnext->pstnext)

    {

        return;

    }

    q = head->pstnext->pstnext;

    head->pstnext->pstnext = NULL;

    while(NULL != q)

    {

          s = q->pstnext;

          q->pstnext = head->pstnext;

          head->pstnext = q;

          q = s;

    }

}

//单链表的连接

void connect_list(Node *head, Node *head_New)

{

Node *p = head;

while(NULL != p->pstnext)

{

  p = p->pstnext;

}

p->pstnext = head_New->pstnext;

}

//单链表销毁

void destroy_list(Node* head)

{

    while (NULL != head)

    {

        Node* temp = head;

        head = head->pstnext;

        free(temp);

    }

}

【查找一个字符串中最后出现子串的位置】

char *my_strrstr(char const *src,char const *substr)

{

    register char * last;

    register char * current;

 

    last = NULL;

 

    if(*substr != '\0')

    {

        current = strstr(src,substr);

        while(current!=NULL)

        {

            last = current;

            current=strstr(last+1,substr);

        }

    }

    return last;

}


 

给定一个整型变量a,写两段代码,第一个设置abit 3,第二个清除a bit 3。在以上两个操作中,要保持其它位不变。

【林锐getMemory

#define BIT3 (0x1 << 3)//8

static int a=0x07;

void set_bit3(void)

{

    a |= BIT3;

    printf("after set bit : %d.\n",a);//15

}

void clear_bit3(void)

{

    a &= ~BIT3;//~BIT3 = 7

    printf("after clear bit : %d.\n",a);//7

}


 

【访问固定的内存位置】

在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。

int *ptr;
    ptr = (int *)0x67a9;
    *ptr = 0xaa66;

或者晦涩一点的方法:*(int * const)(0x67a9) = 0xaa55;


 

【林锐高质量c/c++编程习题(选)】

void GetMemory(char *p) 

{ 

  p = (char *)malloc(100); 

}   

void Test(void){ 

  char *str = NULL; 

  GetMemory(str); 

  strcpy(str, "hello world"); 

  printf(str); 

} 


 

请问运行Test函数会有什么样的结果?

答:程序崩溃。因为GetMemory并不能传递动态内存, Test函数中的 str一直都是 NULLstrcpy(str, "hello world");将使程序崩溃。

char *GetMemory(void) 

  { 

  char p[] = "hello world"; 

  return p; 

 } 

void Test(void) 

  { 

  char *str = NULL; 

  str = GetMemory(); 

  printf(str); 

 } 


 

请问运行Test函数会有什么样的结果?

答:可能是乱码。 因为GetMemory返回的是指向“栈内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

void GetMemory2(char **p, int num) 

{ 

  *p = (char *)malloc(num); 

} 

void Test(void) 

{ 

  char *str = NULL; 

  GetMemory(&str, 100); 

  strcpy(str, "hello"); 

  printf(str); 

} 


 

请问运行Test函数会有什么样的结果?

答: (1)能够输出hello (2)内存泄漏

void Test(void) 

{ 

   char *str = (char *) malloc(100); 

   strcpy(str, “hello”);  

    free(str); 

  if(str != NULL) 

  { 

    strcpy(str, “world”); 

    printf(str); 

 } 

} 


 

请问运行Test函数会有什么样的结果?

答:篡改动态内存区的内容,后果难以预料,非常危险。 因为free(str);之后str成为野指针, if(str != NULL)语句不起作用。

 

 

【排序算法(合集)】

插入排序

算法概要:插入排序依据遍历到第N个元素的时候前面的N-1个元素已经是排序好的,那么就查找前面的N-1个元素把这第N个元素放在合适的位置,如此下去直到遍历完序列的元素为止。

void shellSort(int array[], int length)  

{  

    int key;  

    int increment;  

    for(increment = length/2; increment>0; increment /= 2)  

    {  

        for(int i=increment; i<length; i++)  

        {  

            key = array[i];  

            for(int j = i-increment; j>=0 && array[j] > key; j -= increment)  

            {  

                array[j+increment] = array[j];  

            }  

            array[j+increment]=key;  

        }  

    }  

}


 

希尔排序 

算法概要:shell排序是对插入排序的一个改装,它每次排序排序根据一个增量获取一个序列,对这这个子序列进行插入排序,然后不断的缩小增量扩大子序列的元素数量,直到增量为1的时候子序列就和原先的待排列序列一样了,此时只需要做少量的比较和移动就可以完成对序列的排序了。

void shellSort(int array[], int length)  

{  

        int key;  

        int increment;  

        for(increment = length/2; increment>0; increment /= 2)  

        {  

            for(int i=increment; i<length; i++)  

            {  

                key = array[i];  

                for(int j = i-increment; j>=0 && array[j] > key; j -= increment)  

                {  

                    array[j+increment] = array[j];  

                }  

                array[j+increment]=key;  

            }  

        }      

}


 

冒泡排序

算法概要:冒泡排序是经过n-1趟子排序完成的,第i趟子排序从第1个数至第n-i个数,若第i个数比后一个数大(则升序,小则降序)则交换两数。

void bubbleSort(int  array[], int length)  

{  

    int flag = 0;  

    for(int i=0; i<length-1; i++)  

    {  

        for(int j=0; j<length-1-i; j++)  

        {  

            if(array[j]>array[j+1])  

            {  

                flag = 1;  

                array[j] = array[j] + array[j+1];  

                array[j+1] = array[j] - array[j+1];  

                array[j] = array[j] - array[j+1];  

            }  

        }  

        if(flag == 0) break;  

    }  

}


 

快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

int Sort(int array[], int first, int last)  

 {  

        int pivot = array[first];  

        int temp;  

        if(last-first <=0) return -1;  

      

        while(first != last)  

        {  

            while(array[last] >= pivot && last != first) last--;  

            temp = array[first];  

            array[first] = array[last];  

            array[last] = temp;  

            while(array[first] <= pivot && last != first) first++;  

            temp = array[first];  

            array[first] = array[last];  

            array[last] = temp;  

        }  

        return last;  

 }  

      

    void quickSort(int array[], int length)  

    {  

        int temp = Sort(array, 0, length-1);  

        if(temp == -1 ) return;  

        quickSort(array,temp+1);  

        quickSort(&array[temp+1],length-temp-1);  

    }


 

归并排序

算法概要:归并排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

void Merge(int A[],int low,int mid,int high)      

 {      

     int i,j,k;    

     int *P = new int[mid-low+1],*Q = new int[high-mid];      

     

     for (i =0;i  < mid - low +1;++i)   P[i] = A[i+low];

          

     for (i = 0; i  < high - mid;++i)   Q[i] = A[mid+1+i];      

      

     i = j = 0,k = low;      

     while ((i  <= mid-low) && (j  <= high-mid-1))      

     {      

      

         if (P[i]  <= Q[j])   A[k++] = P[i++];      

         else  A[k++]= Q[j++];      

     }      

      

     if (i > mid - low)   { for (;j  <= high-mid-1;++j)   A[k++] = Q[j]; }      

     else     

     {   for (; i <= mid - low;++i)   A[k++] = P[i];  }      

      

     delete [] P;      

     delete [] Q;      

 }      

      

 void mergeSort(int A[],int left,int right)      

 {      

     if (left  < right)      

     {      

         int i = (left + right) / 2;      

         MergeSort(A,left,i);      

         MergeSort(A,i+1,right);      

         Merge(A,left,i,right);      

     }      

 }


 

【第五部分 WiFi

WiFi.

12. ToDS From Ds

 

13. RTS CTS

 

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值