1、堆排序
思路:每次都构建堆,然后将根节点与最后一个节点交换位置,然后重新构建堆直至排序结束。
void sift(int a[],int start,int end)//构建堆
{
int i=start,j=i*2;//j是i左孩子
int t=a[i];
while(j<=end)
{
if(j<end&&a[j]<a[j+1])
j++;
if(t<a[j])
{
a[i]=a[j];
i=j;
j=i*2;
}
else
break;
}
a[i]=t;
}
void Heapsort(int a[],int n)//堆排序
{
int i,t;
for(i=n/2; i>=1; i--) //初次建立堆
sift(a,i,n);
for(i=n; i>=2; i--)
{
t=a[i];
a[i]=a[1];
a[1]=t;
sift(a,1,i-1);
}
}
2、快速排序
思路:从n个数中,选取一个基准值,根据基准值将数字分为两部分,一直到排序结束。
void Divide(int a[],int length,int start,int end)
{
if(a=NULL||length<=0||start==0||end>=length)
return 0;
int index=RandomInRange(start,end);
swap(&a[index],&a[end]);
int small=start-1;
for(i=start;i<end; i++)
{
if(a[i]<a[end])//找到比基准值小的数了
{
small++;
if(small!=i)
swap(&a[i],&a[small]);
}
}
small++;
swap(&a[small],&a[end]);
return small;//返回基准值所在的位置
}
void QuickSort(int a[],int length; int start; int end)
{
if(start==end)
return;
int index=Divide(a,length,start,end);
if(index>start)
QuickSort(a,length,start,index-1);
if(index<end)
QuickSort(a,length,index+1,end);
}
3、直接插入排序
思路:从选定的元素向前寻找,当寻找到合适的位置时,将选定的元素插入。
void InsertSort(int a[],int n)//对a[0..n-1]按递增顺序直接插入排序
{
int i,j,t;
for(i=1;i<n; i++)
{
t=a[i];
j=i-1;
while(j>=0&&t<a[j])
{
a[j+1]=a[j];
j--;
}
a[j+1]=t;
}
}
4、折半插入排序
思路:从选定的元素向前寻找,通过二分查找寻找到合适的位置时,将选定的元素插入。
void InsertSort1(int a[],int n)
{
int i,j,low,high,mid,t;
for(i=1; i<n; i++)
{
t=a[i];
low=0;
high=i-1;
while(low<=high)
{
mid=(low+high)/2;
if(t<a[mid])
high=mid-1;
else
low=mid+1;
}
for(j=i-1; j>=high+1; j--)
a[j+1]=a[j];
a[high+1]=t;
}
}
5、冒泡排序
void BubbleSort(int a[],int n)
{
int i,j,t;
for(i=0; i<n-1; i++)
{
for(j=n-1; j>i; j--)
if(a[j]<a[j-1])
{
t=a[j];
a[j]=a[j-1];
a[j-1]=t;
}
}
}
6、选择排序
void SelectSort(int a[],int n)
{
int i,j,k,t;
for(i=0; i<n-1; i++)
{
k=i;
for(j=i+1; j<n; j++)
if(a[j]<a[k])
k=j;
if(k!=i)
{
t=a[i];
a[i]=a[k];
a[k]=t;
}
}
}
7、归并排序
#include<iostream>
using namespace std;
void Msort(int data[], int result[], int low, int high);
void Merge(int a[], int b[], int low, int mid, int high);
int main()
{
int data[100] = {1,3,2,4,6,5,9,8,7,10};//排序该序列
int result[100];
Msort(data, result, 0, 9);
for (int i = 0; i < 10; i++)
{
cout<< data[i] <<" ";
}
}
void Msort(int data[], int result[], int low, int high)
{
int mid;
if (high - low == 1)//子序列中只有两元素进行比较
{
if (data[low] > data[high])
{
int t = data[low];
data[low] = data[high];
data[high] = t;
}
return;
}
else if (high == low)//子序列只有一个元素时直接返回
{
return;
}
else
{
mid = (low + high) / 2;
Msort(data, result, low, mid);//继续划分子区间,分别对左右子区间进行排序
Msort(data, result, mid + 1, high);
Merge(data, result, low, mid, high); //开始归并已经排好序的start到end之间的数据
for (int i = low; i <= high; i++)//把排序后的区间数据复制到原始数据中去
{
data[i] = result[i];
}
}
}
void Merge(int data[], int result[], int low, int mid, int high)
{
int i = low;
int j = mid + 1;
int k = low;
/*第一阶段*/
while (i <= mid && j <= high)
{
if (data[i] <= data[j])
{
result[k++] = data[i++];
}
else
{
result[k++] = data[j++];
}
}
/*第二阶段*/
while (i <= mid)
{
result[k++] = data[i++];
}
while (j<=high)
{
result[k++] = data[j++];
}
}
各种排序算法的性能
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
平均情况 | 最坏情况 | 最好 情况 | ||||
直接插入排序 | 稳定 | 简单 | ||||
希尔排序 | 不稳定 | 较复杂 | ||||
冒泡排序 | 稳定 | 简单 | ||||
快速排序 | 不稳定 | 较复杂 | ||||
直接选择排序 | 不稳定 | 简单 | ||||
堆排序 | 不稳定 | 较复杂 | ||||
归并排序 | 稳定 | 较复杂 | ||||
基数排序 | 稳定 | 较复杂 |
8、二叉树先序、中序、后序遍历递归算法:
void PreOrder(BTNode *b)//先序遍历
{
if(b!=NULL)
{
printf("%c",b->data);
PreOrder(b->lchild);
PreOrder(b->rchild);
}
}
void InOrder(BTNode *b)//中序遍历
{
if(b!=NULL)
{
InOrder(b->lchild);
printf("%c",b->data);
InOrder(b->rchild);
}
}
void PostOrder(BTNode *b)//后序遍历
{
if(b!=NULL)
{
PostOrder(b->lchild);
PostOrder(b->rchild);
printf("%c",b->data);
}
}
9、二叉树先序、中序、后序遍历非递归算法:
void PreOrder(BTNode *b)//先序遍历
{
BTNode *St[MaxSize],*p;
int top=-1;
if(b!=NULL)
{
top++;
St[top]=b;
while(top>-1)
{
p=St[top];
top--;
printf("%c",p->data);
if(p->rchild!=NULL)
{
top++;
St[top]=p->rchild;
}
if(p->lchild!=NULL)
{
top++;
St[top]=p->lchild;
}
}
printf("\n");
}
}
void InOrder(BTNode *b)//中序遍历
{
BTNode *St[MaxSize],*p;
int top=-1;
if(b!=NULL)
{
p=b;
while(top>-1||p!=NULL)
{
while(p!=NULL)
{
top++;
St[top]=p;
p=p->lchild;
}
//执行到此处时,栈顶元素没有左孩子或左子树均已被访问
if(top>-1)
{
p=St[top];
top--;
printf("%c",p->data);
p=p->rchild;
}
}
printf("\n");
}
}
void PostOrder(BTNode *b)//后序遍历
{
BTNode *St[MaxSize],*p;
int flag,top=-1;
if(b!=NULL)
{
do
{
while(b!=NULL)
{
top++;
St[top]=b;
b=b->lchild;
}
//执行到此处时,栈顶元素没有左孩子或左子树均已访问过
p=NULL;//p指向栈顶节点的前一个已访问的节点
flag=1;//表示*b的左孩子已访问或为空
while(top!=-1&&flag)
{
b=St[top];
if(b->rchild==p)//若p=NULL,表示b的右孩子不存在,而其左子树不存在或已访问,
//所以可以访问*b;若P!=NULL,表示b的右子树已访问
{
printf("%c",b->data);
top--;
p=b;
}
else
{
b=b->rchild;
flag=0;
}
}
}
while(top!=-1);
printf("\n");
}
}
10、层次遍历算法
void LevelOrder(BTNode *b)
{
BTNode *p,*qu[MaxSize];
int front,rear;
front=rear=-1;
rear++;
qu[rear]=b;
while(front!=rear)
{
front=(front+1)%MaxSize;
p=qu[front];
printf("%c",p->data);
if(p->lchild!=NULL)
{
rear=(rear+1)%MaxSize;
qu[rear]=p->lchild;
}
if(p->rchild!=NULL)
{
rear=(rear+1)%MaxSize;
qu[rear]=p->rchild;
}
}
}
11、
class Test
{
public:
Test() : a(count), b(0)
{
cout << "默认构造函数" << count << endl;
pArray = NULL;
++count;
}
Test(size_t ArraySize, int _a, int _b) : a(_a), b(_b), pArray(new int[ArraySize])
{
cout << "带参数构造函数" << endl;
}
~Test()
{
cout << "析构函数" << a << endl;
if (pArray != NULL)
{
cout << "需要释放空间" << endl;
delete[] pArray;
}
}
private:
int a, b;
int *pArray;
static int count;
};
int Test::count = 0;
12、求二叉树中节点的最大距离
思路:首先,求左子树距根节点的最大距离,其次,求右子树距根节点的最大距离,然后二者相加。
Java:
class Node
{
public int data;
public Node left;
public Node right;
public int leftMax;
public int rightMax;
public Node(int data)
{
this.data=data;
this.left=null;
this.right=null;
}
}
public class BinaryTree
{
private int maxLen=0;
private int max(int a,int b)
{
return a>b?a:b;
}
public void Find(Node root)
{
if(root==null)
return;
if(root.left==null)
root.leftMax=0;
if(root.right==null)
root.rightMax=0;
if(root.left!=null)
Find(root.left);
if(root.right!=null)
Find(root.right);
if(root.left!=null)//计算左子树中距离根节点的最大距离
root.leftMax=max(root.left.leftMax,root.left.rightMax)+1;
if(root.right!=null)//计算右子树中距离根节点的最大距离
root.rightMax=max(root.right.leftMax,root.right.rightMax)+1;
if(root.leftMax+root.rightMax>maxLen)
{
maxLen=root.leftMax+root.rightMax;
}
}
}
C++定义:
#include <iostream>
#include <algorithm>
using namespace std;
class Node
{
public:
int a;
Node *left;
Node *right;
Node()
{
this->a=0;
this->left=NULL;
this->right=NULL;
}
Node(int data)
{
this->a=data;
this->left=NULL;
this->right=NULL;
}
};
int main()
{
Node *t=new Node(2);
Node *tt=new Node();
cout<<tt->a<<" "<<t->a<<endl;
return 0;
}
13、strcmp实现
int strcmp(char *str1, char *str2)
{
if(str1==NULL && str2==NULL)
return 0;
else if(str1!=NULL && str2==NULL)
return 1;
else if(str1==NULL && str2!=NULL)
return -1;
else
{
while((*str1!='\0')&&(*str1==*str2))
{
str1++;
str2++;
}
int t=*str1-*str2;
if(t>0)
return 1;
if(t<0)
return -1;
if(t==0)
return 0;
}
}
14、strcpy实现
char *strcpy(char *dst, const char *src)
{
assert(dst!=NULL && src!=NULL);
char *t=dst;
while((*dst++=*src++)!='\0');
return t;
}
总结:
1. const char* 字符串 以 “\0”结尾。
2. char[] 字符串 以 “\0”结尾。
3.string 字符串 不以 “\0”结尾。
4. char[n] = "string", 当string 长度+“\0”>n时,会因空间不足出错。
5.string.c_str() 转 const char* 时, 会在字符串末尾 自动补“\0”
6.char* 转string 时, 会自动把末尾的 “\0” 去掉。
7.strlen()是取字符串除去结尾符 “\0” 的长度。
15、单例模式
public class Test{
private Test(){}
private static Test t=new Test();
public static Test getT(){
return t;
}
}
C++ 内存分区
C++ 内存分为:堆区、栈区、全局区/静态区、字符串常量和代码区。
- 栈区:由系统进行内存的管理。主要存放函数的参数以及局部变量。栈区由系统进行内存管理,在函数完成执行,系统自行释放栈区内存,不需要用户管理。整个程序的栈区的大小可以在编译器中由用户自行设定,默认的栈区大小为 3M。
- 全局/静态区:全局、静态数据存放在一起,初始化的全局变量和静态变量是在一起的,未初始化的全局变量和静态变量是在相邻的空间的。全局变量和静态全局变量的存储方式是一致的,但是其区别在于,全局变量在整个源代码中都可以使用,而静态全局变量只能在当前文件中有效。比如我们的一个程序有5个文件,那么某个文件中申请了静态全局变量,这个静态全局变量只能在当前文件中使用,其它四个文件均不可以使用。而某个文件中申请了全局变量,那么其它四个文件都可以使用该全局变量(只需要通过关键字 extern 申请以下就可以了)。事实上 static 改变了变量的作用范围。
- 字符串常量区:存放字符串常量,程序结束后,由系统进行释放。比如我们定义 char *p = "Hello World",这里的 "Hello World" 就是在字符串常量中,最终系统会自动释放。
- 代码区:存放程序体的二进制代码。比如我们写的函数,都是在代码区的。
- 堆区:由用户手动申请,手动释放。在 C 中使用 malloc,在 C++ 中使用 new(当然 C++ 中也可以使用 malloc)。
类各种成员变量初始化规则
- const static 数据成员可以在类内初始化(即类内声明的同时初始化),也可以在类外,即类的实现文件中初始化;不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化;
- static non-const 数据成员只能在类外,即类的实现文件中初始化;不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化;
- 非静态的常量数据成员不能在类内初始化,也不能在构造函数中初始化,只能且必须在构造函数的初始化列表中初始化;
- 非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化。
为什么构造函数不能为虚函数
1,从存储空间角度
虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
2,从使用角度
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3、构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数。
#define和const的区别
这个区别用从几个角度来说:
角度1:
就定义常量说的话:
const 定义的常数是变量 也带类型, #define 定义的只是个常数 不带类型。
角度2:
就起作用的阶段而言:
define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用。
角度3:
就起作用的方式而言:
define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。
正因为define只是简单的字符串替换会导致边界效应,具体举例可以参考下面代码:
#define N 2+3 //我们预想的N值是5,我们这样使用N
double a = N/2; //我们预想的a的值是2.5,可实际上a的值是3.5
角度4:
就空间占用而言:
例如:
#define PI 3.14 //预处理后 占用代码段空间
const float PI=3.14; //本质上还是一个 float,占用数据段空间
角度5:
从代码调试的方便程度而言:
const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了
用户用浏览器访问一个网站的过程与步骤?
利用DNS协议进行域名解析 --> 建立tcp协议三次握手过程 --> 客户端发出访问网站相应页面请求(发出http协议请求报文) --> 服务端发出相应访问页面的请求信息(发出http) --> 断开tcp协议四次挥手过程
进程的常见状态?以及各种状态之间的转换条件?
- 就绪:进程已处于准备好运行的状态,即进程已分配到除CPU外的所有必要资源后,只要再获得CPU,便可立即执行。
- 执行:进程已经获得CPU,程序正在执行状态。
- 阻塞:正在执行的程进由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态。
同步机制遵循的原则:
(1)空闲让进;
(2)忙则等待(保证对临界区的互斥访问);
(3)有限等待(有限代表有限的时间,避免死等);
(4)让权等待,(当进程不能进入自己的临界区时,应该释放处理机,以免陷入忙等状态)。
进程的通信方式有哪些?
低级通信:信号量
高级通信机制可归结为三大类:
(1)共享存储器系统(存储器中划分的共享存储区);实际操作中对应的是“剪贴板”(剪贴板实际上是系统维护管理的一块内存区域)的通信方式,比如举例如下:word进程按下ctrl+c,在ppt进程按下ctrl+v,即完成了word进程和ppt进程之间的通信,复制时将数据放入到剪贴板,粘贴时从剪贴板中取出数据,然后显示在ppt窗口上。
(2)消息传递系统(进程间的数据交换以消息(message)为单位,当今最流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。应用举例:邮槽(MailSlot)是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。
(3)管道通信系统(管道即:连接读写进程以实现他们之间通信的共享文件(pipe文件,类似先进先出的队列,由一个进程写,另一进程读))。实际操作中,管道分为:匿名管道、命名管道。匿名管道是一个未命名的、单向管道,通过父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上两个进程之间的通信,而不能实现跨网络的通信。命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信。
-
管道:管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
-
信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
消息队列:是一个在系统内核中用来保存消 息的队列,它在系统内核中是以消息链表的形式出现的。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
共享内存:共享内存允许两个或多个进程访问同一个逻辑内存。这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
-
套接字:套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。
首先来看下集中常见的进程调度算法:
1.先来先服务调度算法
2.短作业优先调度算法
3.优先级调度算法
4.高响应比优先调度算法
5.基于时间片的轮转调度算法
6.多级反馈队列调度算法
线程的通信方式
1 锁机制
互斥锁:提供了以排他方式防止数据结构被并发修改的方法。
读写锁:允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
2信号量机制(Semaphore)
包括无名线程信号量和命名线程信号量。
信号量(semaphore):信号量的数据结构为一个值和一个指针,
指针指向等待该信号量的下一个进程。
信号量的值与相应资源的使用情况有关:
当它的值大于0时,表示当前可用资源的数量;
当它的值小于0时,其绝对值表示等待使用该资源的进程个数。
信号量的值仅能由PV操作来改变。
3 信号机制(Signal)
类似进程间的信号处理。
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
进程与线程的区别和联系?
- 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
进程和线程的关系
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
(3)处理机分给线程,即真正在处理机上运行的是线程。
(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
进程与线程的区别?
(1)进程有自己的独立地址空间,线程没有
(2)进程是资源分配的最小单位,线程是CPU调度的最小单位
(3)进程和线程通信方式不同(线程之间的通信比较方便。同一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。)
(4)进程上下文切换开销大,线程开销小
(5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程
(6)对进程进程操作一般开销都比较大,对线程开销就小了
为什么进程上下文切换比线程上下文切换代价高?
进程切换分两步:
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文
对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
切换的性能消耗:
1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor's Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
什么是死锁?其条件是什么?怎样避免死锁?
死锁的概念:在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗地讲,就是两个或多个进程被无限期地阻塞、相互等待的一种状态。
死锁产生的原因主要是:? 系统资源不足;? 进程推进顺序非法。
产生死锁的必要条件:
(1)互斥(mutualexclusion),一个资源每次只能被一个进程使用;
(2)不可抢占(nopreemption),进程已获得的资源,在未使用完之前,不能强行剥夺;
(3)占有并等待(hold andwait),一个进程因请求资源而阻塞时,对已获得的资源保持不放;
(4)环形等待(circularwait),若干进程之间形成一种首尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解除与预防:理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
死锁的处理策略:鸵鸟策略、预防策略、避免策略、检测与恢复策略。
静态链接库(静态函数库)
扩展名:这类函数通常扩展名类似于libxxx.a -->"lib"、"a"必须都加上,中间的"xxx"表示库的名称
编译行为:当程序在使用时,整个静态函数库的所有数据都会整合到执行文件中.即当用户进行编译操作时,静态函数库会加入到执行文件中,所以,利用静态函数库生成的可执行文件会比较大一些.
举例:一工程中包含一个file.c源文件,当使用静态函数库进行编译时,系统会将file.c源文件和静态函数库放在一起,然后再生成一个可执行文件file.elf
独立执行状态:静态函数库最大特点是,编译成功的可执行文件可以独立运行,不需要再向外部要求读取函数库的内容(因为函数库的内容已经随着源文件一起编译成了可执行文件).
升级难以程度:静态函数库最大缺点是升级难度大.比如当函数库升级后,还必须将源文件和升级后的函数库进行重新编译.这样才能得到升级后的可执行文件,因此操作比较麻烦.
动态链接库(动态函数库,又称共享库)
扩展名:这类函数通常扩展名类似于libxxx.so
编译行为:动态函数库在编译时,在程序中只有一个"指向(pointer)"的位置而已.或者说,最终生成的可执行程序中只是记录了动态链接库的名字、路径等其它少量的登记信息.因此,动态链接库并没有随着源文件一起整合到可执行文件中,而是当执行文件要用到函数库时,程序才会去读取.由于可执行文件中仅具有动态函数库所在的指针(或地址),并不包含函数库的内容,所以它会小一点.
举例:一工程中包含一个file.c源文件,当使用动态函数库进行编译时,系统只会将file.c源文件进行编译生成可执行文件file.o,并且在file.o中包含了动态链接库在系统的地址或者路径.当执行file.o时,系统会到指定的地址或路径去读取动态链接库.
独立执行状态:由于动态链接库是在需要的进行读取,所以函数库必须存在,而且函数库所在目录也不能改变.由于可执行文件中只有"指针",即当要采用该动态函数库时,程序会主动去某个路径下读取,因此动态函数库不能随意移动或删除,这会影响很多代码的执行.
升级难以程度:虽然这类可执行文件无法独立运行,然而由于具有指向功能,所以,当函数库升级后,执行文件不再需要进行重新编译,执行文件会直接指向新的函数库文件(前提是函数库新旧版本中文件名及路径不变).