热门标签 | HotTags
当前位置:  开发笔记 > 人工智能 > 正文

算符优先分析算法的设计与实现(含代码)

算符优先分析算法的设计与实现写在最前面:我的编译原理学的不是很好,不怎么听课,所以我写代码的思路比较简单。简单的思路也就意味着代码很笨重,介意的话请点叉叉。如果有什么指教欢迎评论区




算符优先分析算法的设计与实现

写在最前面:我的编译原理学的不是很好,不怎么听课,所以我写代码的思路比较简单。简单的思路也就意味着代码很笨重,介意的话请点叉叉。如果有什么指教欢迎评论区留言。

一、实验目的


  1. 根据算符优先分析法,对表达式进行语法分析,使其能够判断一个表达式是否正确。
  2. 通过算符优先分析方法的实现,加深对自下而上语法分析方法的理解。

二、 实验内容


  1. 输入文法。可以是如下算术表达式的文法(你可以根据需要适当改变):
    E→E+T|E-T|T
    T→T*F|T/F|F
    F→(E)|i

  2. 对给定表达式进行分析,输出表达式正确与否的判断。
    程序输入/输出示例:
    输入:1+2;
    输出:正确
    输入:(1+2)/3+4-(5+6/7);
    输出:正确
    输入:((1-2)/3+4
    输出:错误
    输入:1+2-3+(*4/5)
    输出:错误

  3. 根据文法求FIRSTVT集和LASTVT集
    给定一个上下文无关文法,根据算法设计一个程序,求文法中每个非终结符的FirstVT 集和LastVT 集。
    找Firstvt的三条规则
    如果要找A的Firstvt, A的候选式中出现:
    A->a…,即以终结符开头,a入 Firstvt
    A->B…,即以非终结符开头,B的Firstvt入A的Firstvt
    A->Ba…,即先以非终结符开头,紧跟终结符,则a入Firstvt
    找Lastvt的三条规则
    如果要找A的Lastvt, A的候选式中出现:
    A->…a,即以终结符结尾,a入Lastvt
    A->…B,即以非终结符结尾,B的Lastvt入A的Lastvt
    A->…aB,即先以非终结符结尾,前面是终结符,则a入Lastvt
    根据上述求firstvt和lastvt的方法,我们计算一下我给的例子,得到的最终结果如下。
    firstvt和lastvt

  4. 构造算符优先分析表
    我先概述一下构造方法:
    首先,在右边的产生式中,找每个终结符。
    如果是就单一个终结符,那么忽略不看。
    先找出…aB…这种形式,这代表a 在算符优先分析表的左边一列中找到a,然后向右在对应的每个firstvt(B)的元素的位置上标上<。
    再找…Ba…这种形式,这代表a>lastvt(B)。
    在算符优先分析表的上面一行中找到a,然后向下在对应的每个lastvt(B)的元素的位置上标上>。
    如果有…ab…或…aCb…这两种情况,则在a行b列的位置上标上=。
    下面我来结合例子讲一下:
    首先构造好表:


+-*/()i#
+
-
*
/
(
)
i
#

然后我们先看文法的第一行:
E->E+T|E-T|T
第一个非终结符为+。
其实E+T就代表着lastvt(E)<+ 我们每次都要先看<,再看>。
+ <是从左向右,所以要在(+,*)(+,/)(+,()(+,i)这四个位置标上<。
即:


+-*/()i#
+<<<<
-
*
/
(
)
i
#

然后再看>。
+>lastvt(E),即+ > +,-,*,/,),i。
<是从上到下,所以要在( +,+)(-,+)(*,+)(/,+)(),+)(i,+)这六个位置上标上>。
即:


+-*/()i#
+><<<<
->
*>
/>
(
)>
i>
#

前两行文法:
E->E+T|E-T|T
T->T*F|T/F|F

其中的+,-,*,/都和我上面说的情况一样。
可得下表:


+-*/()i#
+>><<<<
->><<<<
*>>>><<
/>>>><<
(
)>>>>
i>>>>
#

最后,我们来看最后一个文法:
F->(E)|i
i只是一个终结符,不存在大小关系,不看它。
而(E)就是( 并且(E)满足…aBc…的情况,要在第(行,第)列的位置上标上=。
可得下表:


+-*/()i#
+>><<<><
->><<<><
*>>>><><
/>>>><><
(<<<<<=<
)>>>>>
i>>>>>
#

最后的最后,如果要考虑#。
我们按照#E#的样子考虑。
即:# 可得最终表:


+-*/()i#
+>><<<><>
->><<<><>
*>>>><><>
/>>>><><>
(<<<<<=<
)>>>>>>
i>>>>>>
#<<<<<<=

这就是我们要求的算符优先文法了。


  1. 对给定的表达式,给出准确与否的分析过程,给出表达式的计算结果。
    在规约的时候我们其实可以直接看文法就行了(如果文法比较简单的话),但我还是讲一下如何根据算符优先分析表来进行规约。
    一句话:栈的最右边的终结符,和输入串的最左边的终结符,在表中查出对应位置为<,>还是=。<则移进,>则规约,=则先移进再规约。若表中对应位置是空格,则表示该表达式是错误的。
    举个例子:#(i+i)/i+i-(i+i/i)#

步骤输入串动作
0#(i+i)/i+i-(i+i/i)#预备
1#(i+i)/i+i-(i+i/i)#查表,(#,()=<,移进
2#(i+i)/i+i-(i+i/i)#查表,((,i)=<,移进
3#(N+i)/i+i-(i+i/i)#查表,(i,+)=>,规约(N可以换成其它大写字母)
4#(N+i)/i+i-(i+i/i)#((,+)=<,移进
5#(N+i)/i+i-(i+i/i)#(+,i)=<,移进
6#(N+N)/i+i-(i+i/i)#(i,))=>,规约
7#(N)/i+i-(i+i/i)#(+,))=>,规约
8#(N)/i+i-(i+i/i)#((,))==,先移进
9#N/i+i-(i+i/i)#后规约
10#N#好了,偷个懒,不想写了,反正<,>,=三种情况都有了,自己举一反三吧

三、实验代码

//owner:junfuxiaotong
//date:2021/11/28
#include
#include
using namespace std;
#define N 100
#define true 1
#define false -1
char wenfa[N][N];
char VN[N],VT[N];
char firstvt[N][N],lastvt[N][N],table[N][N];
int vnvt(int n)//获取vn vt
{
int flag=true;
for(int i=0;i {
if((wenfa[i][0]>='A'&&wenfa[i][0]<='Z')&&wenfa[i][1]=='-'&&wenfa[i][2]=='>')
{
VN[i]=wenfa[i][0];
}
else
{
flag=false;
break;
}
}
if(flag==false)
{
return flag;
}
else
{
int k=0;
int l=0;
for(int i=0;i {
for(int j=3;wenfa[i][j]!='\0';j++)
{
if((wenfa[i][j]<'A'||wenfa[i][j]>'Z')&&wenfa[i][j]!='|')
{
for(l=0;l {
if(wenfa[i][j]==VT[l])
{
break;
}
}
if(l==k)
{
VT[k]=wenfa[i][j];
k++;
}
}
}
}
return flag;
}
}
void getfirstvt(int n)
{
// int point=0;//用于指向每一个产生式的前两个符号
for(int i=0;i {
int flag=true;
for(int j=3;;)
{
for(int k=0;k {
if(wenfa[i][j]==VT[k])
{
int mark=true;//用于检查firstvt集中是否已经存在该终结符。
for(int l=0;l {
if(wenfa[i][j]==firstvt[i][l])
{
mark=false;
break;
}
}
if(mark==true)//若不存在,则加入到firstvt集中
{
int length=strlen(firstvt[i]);
firstvt[i][length]=wenfa[i][j];
}
}
if(wenfa[i][j+1]==VT[k])
{
int mark=true;
for(int l=0;l {
if(wenfa[i][j+1]==firstvt[i][l])
{
mark=false;
break;
}
}
if(mark==true)
{
int length=strlen(firstvt[i]);
firstvt[i][length]=wenfa[i][j+1];
}
}
}
while(wenfa[i][j]!='|')
{
if(wenfa[i][j]=='\0')
{
flag=false;
break;
}
j++;
}
j++;
if(flag==false)
{
break;
}
}
}
//下面的代码是循环查看哪些非终结符的firstvt集可以加入到另一些非终结符的firstvt集中,一直循环添加,直到每个非终结符的长度不再变化为止。
int *origin=new int[n];//用于记录遍历之前的数组长度,看是否有变化。
while(1)
{
int sign=true;//用于标识遍历前后firstvt是否有变化。mark,symbol。
for(int i=0;i {
if(origin[i]!=strlen(firstvt[i]))
{
sign=false;//长度有变化
origin[i]=strlen(firstvt[i]);
}
}
if(sign==true)
{
break;
}
for(int i=0;i {
for(int j=3;;)
{
for(int k=0;k {
if(k==i)
{
continue;
}
else if(wenfa[i][j]==wenfa[k][0])
{
for(int l=0;l {
int flag=true;//用于标识一个终结符是否已经在firstvt集中,true为不在其中的意思。
for(int m=0;m {
if(firstvt[k][l]==firstvt[i][m])
{
flag=false;
break;
}
}
if(flag==false)
{
continue;
}
else
{
int length=strlen(firstvt[i]);
firstvt[i][length]=firstvt[k][l];
}
}
}
}
int flag=true;
while(wenfa[i][j]!='|')
{
if(wenfa[i][j]=='\0')
{
flag=false;
break;
}
j++;
}
if(flag==false)
{
break;
}
j++;
}
}
}
}
void getlastvt(int n)//获取lastvt集,此函数下标我是从abc...这么开始的,我也不知道为啥要这么干
{
for(int a=0;a {
for(int b=0;;)
{
int sign=true;//用于标识是否到了一句分法的末尾
while(wenfa[a][b]!='|')
{
if(wenfa[a][b]=='\0')
{
sign=false;
break;
}
b++;
}
for(int c=0;c {
if(wenfa[a][b-1]==VT[c])
{
int flag=true;
for(int e=0;e {
if(wenfa[a][b-1]==lastvt[a][e])
{
flag=false;
break;
}
}
if(flag==true)
{
int length=strlen(lastvt[a]);
lastvt[a][length]=VT[c];
}
}
if(wenfa[a][b-2]==VT[c])
{
int flag=true;
for(int e=0;e {
if(wenfa[a][b-2]==lastvt[a][e])
{
flag=false;
break;
}
}
if(flag==true)
{
int length=strlen(lastvt[a]);
lastvt[a][length]=VT[c];
}
}
}
if(sign==false)
{
break;
}
b++;//这里设置一个b++是因为wenfa[a][b]=='|',这样的话前面的while循环无法进行前进下标扫描,所以在这先把下标跳一个。
}
}
//下面的代码和getfirstvt的后半段代码一模一样,只是把所有的firstvt改成lastvt就行了。
int *origin=new int[n];//用于记录遍历之前的数组长度,看是否有变化。
while(1)
{
int sign=true;//用于标识遍历前后lastvt是否有变化。mark,symbol。
for(int i=0;i {
if(origin[i]!=strlen(lastvt[i]))
{
sign=false;//长度有变化
origin[i]=strlen(lastvt[i]);
}
}
if(sign==true)
{
break;
}
for(int i=0;i {
for(int j=3;;)
{
for(int k=0;k {
if(k==i)
{
continue;
}
else if(wenfa[i][j]==wenfa[k][0])
{
for(int l=0;l {
int flag=true;//用于标识一个终结符是否已经在lastvt集中,true为不在其中的意思。
for(int m=0;m {
if(lastvt[k][l]==lastvt[i][m])
{
flag=false;
break;
}
}
if(flag==false)
{
continue;
}
else
{
int length=strlen(lastvt[i]);
lastvt[i][length]=lastvt[k][l];
}
}
}
}
int flag=true;
while(wenfa[i][j]!='|')
{
if(wenfa[i][j]=='\0')
{
flag=false;
break;
}
j++;
}
if(flag==false)
{
break;
}
j++;
}
}
}
}
void gettable(int n)
{
char data[N];//用于保存每个产生式
for(int i=0;i {
int dt=0;
// int flag=true;
for(int j=3;;)
{
if(wenfa[i][j]=='|'||wenfa[i][j]=='\0')
{
if(strlen(data)!=1)
{
for(int k=0;k {
if(data[k]<'A'||data[k]>'Z')
{
if(data[k+1]!='\0'&&data[k+1]>='A'&&data[k+1]<='Z')
{
int x;
int yk;
for(int l=0;l {
if(data[k]==VT[l])
{
x=l;
break;
}
}
for(int l=0;l {
if(data[k+1]==VN[l])
{
yk=l;
break;
}
}
for(int l=0;l {
for(int m=0;m {
if(firstvt[yk][l]==VT[m])
{
if(table[x][m]=='\0')
{
table[x][m]='<';
break;
}
}
}
}
}
}
}
}
if(wenfa[i][j]=='\0')
{
break;
}
j++;
memset(data,'\0',sizeof(data));
dt=0;
}
else
{
data[dt]=wenfa[i][j];
j++;
dt++;
}
}
}
for(int i=0;i,和上面的<差不多其实
{
int dt=0;
// int flag=true;
for(int j=3;;)
{
if(wenfa[i][j]=='|'||wenfa[i][j]=='\0')
{
if(strlen(data)!=1)
{
for(int k=0;k {
if(data[k]<'A'||data[k]>'Z')
{
if(k>=1&&data[k-1]>='A'&&data[k-1]<='Z')
{
int y;
int xk;
for(int l=0;l {
if(data[k]==VT[l])
{
y=l;
break;
}
}
for(int l=0;l {
if(data[k-1]==VN[l])
{
xk=l;
break;
}
}
for(int l=0;l {
for(int m=0;m {
if(lastvt[xk][l]==VT[m])
{
if(table[m][y]=='\0')
{
table[m][y]='>';
break;
}
}
}
}
}
}
}
}
if(wenfa[i][j]=='\0')
{
break;
}
j++;
memset(data,'\0',sizeof(data));
dt=0;
}
else
{
data[dt]=wenfa[i][j];
j++;
dt++;
}
}
}
for(int i=0;i {
int dt=0;
// int flag=true;
for(int j=3;;)
{
if(wenfa[i][j]=='|'||wenfa[i][j]=='\0')
{
if(strlen(data)!=1)
{
for(int k=0;k {
if(data[k]<'A'||data[k]>'Z')
{
if(data[k+1]!='\0'&&(data[k+1]<'A'||data[k+1]>'Z'))
{
int x,y;
for(int l=0;l {
if(data[k]==VT[l])
{
x=l;
}
if(data[k+1]==VT[l])
{
y=l;
}
}
if(table[x][y]=='\0')
{
table[x][y]='=';
}
}
if(data[k+2]!='\0'&&(data[k+2]<'A'||data[k+2]>'Z'))
{
int x,y;
for(int l=0;l {
if(data[k]==VT[l])
{
x=l;
}
if(data[k+2]==VT[l])
{
y=l;
}
}
if(table[x][y]=='\0')
{
table[x][y]='=';
}
}
}
}
}
if(wenfa[i][j]=='\0')
{
break;
}
j++;
memset(data,'\0',sizeof(data));
dt=0;
}
else
{
data[dt]=wenfa[i][j];
j++;
dt++;
}
}
}
int jinghao=strlen(VT);
table[jinghao][jinghao]='=';
for(int i=0;i {
for(int j=0;j {
if(VT[i]==firstvt[0][j])
{
table[jinghao][i]='<';
break;
}
}
}
for(int i=0;i {
for(int j=0;j {
if(VT[i]==lastvt[0][j])
{
table[i][jinghao]='>';
break;
}
}
}
}
void fenxi(char *sentence)
{
char in[N];
in[0]='#';
char out[N];
for(int i=0;i {
out[i]=sentence[i];
}
out[strlen(sentence)]='#';
int step=0;
int lin=1;
int lout=strlen(sentence)+1;
cout< for(int i=0;i {
cout< }
cout<<'\t';
for(int i=0;i {
cout< }
cout<<'\t'<<"预备"< int flag=true;
while(true)
{
int i=lin-1;
while(in[i]=='N')
{
i--;
}
int j=0;
int x,y;
for(int k=0;k {
if(in[i]==VT[k])
{
x=k;
}
if(out[0]==VT[k])
{
y=k;
}
}
if((in[i]>='a'&&in[i]<='z')||(in[i]>='0'&&in[i]<='9'))
{
for(int k=0;k {
if('i'==VT[k])
{
x=k;
}
}
}
if((out[0]>='a'&&out[0]<='z')||(out[0]>='0'&&out[0]<='9'))
{
for(int k=0;k {
if('i'==VT[k])
{
y=k;
}
}
}
if(in[i]=='#')
{
x=strlen(VT);
}
if(out[0]=='#')
{
y=strlen(VT);
}
if(x==y&&y==strlen(VT))
{
break;
}
if(table[x][y]=='<')
{
in[lin]=out[0];
lin++;
lout--;
for(int l=0;l {
out[l]=out[l+1];
}
step++;
cout< for(int l=0;l {
cout< }
cout<<'\t';
for(int l=0;l {
cout< }
cout<<'\t'<<"移进"< }
else if(table[x][y]=='=')
{
step++;
cout< for(int l=0;l {
cout< }
cout< for(int l=1;l {
cout< }
cout<<'\t'<<"移进"< in[i]='N';
int lin1=lin;
for(int k=i+1;k {
in[k]='\0';
lin--;
}
lout--;
for(int k=0;k {
out[k]=out[k+1];
}
step++;
cout< for(int l=0;l {
cout< }
cout<<'\t';
for(int l=1;l {
cout< }
cout<<'\t'<<"规约"< }
else if(table[x][y]=='>')
{
while(true)
{
if(in[i-1]=='N')
{
i--;
}
else
{
break;
}
}
in[i]='N';
int lin1=lin;
for(int k=i+1;k {
in[k]='\0';
lin--;
}
step++;
cout< for(int k=0;k {
cout< }
cout<<'\t';
for(int k=0;k {
cout< }
cout<<'\t'<<"规约"< }
else
{
flag=false;
break;
}
}
if(flag==true)
{
cout<<"恭喜你,你的句子没有问题。"< }
else if(flag==false)
{
cout<<"算符优先分析表中没有这俩非终结符的对应关系,匹配失败。"< }
}
int main()
{
cout<<"请使用英文输入法输入文法,输入#代表结束。另:此程序对文法右部无检错功能。"< int n;
while(1)
{
int i=0;
do
{
cin>>wenfa[i];
i++;
} while (wenfa[i-1][0]!='#');
n=i-1;//文法数量
// for(i=0;i // {
// cout< // }
if(vnvt(n)==false)
{
cout<<"您所输入的文法左部可能有误,请检查后再重新输入。"< }
else
{
break;
}
}
getfirstvt(n);
getlastvt(n);
gettable(n);
cout<<"---------------------------------------------------"< for(int i=0;i {
cout< if(i!=strlen(VN)-1)
{
cout<<',';
}
}
cout<<'}'< for(int i=0;i {
cout< if(i!=strlen(VT)-1)
{
cout<<',';
}
}
cout<<'}'< for(int i=0;i {
cout<<"FIRSTVT("< for(int j=0;j {
cout< if(j!=strlen(firstvt[i])-1)
{
cout<<',';
}
}
cout<<'}'< }
cout< for(int i=0;i {
cout<<"LASTVT("< for(int j=0;j {
cout< if(j!=strlen(lastvt[i])-1)
{
cout<<',';
}
}
cout<<'}'< }
cout<<"---------------------------------------------------"< for(int i=0;i {
cout< }
cout<<'#'< for(int i=0;i {
if(i==strlen(VT))
{
cout<<'#'<<'\t';
}
else
{
cout< }

for(int j=0;j {
cout< }
cout< }
char sentence[N];
while(true)
{
cout<<"请输入你要识别的句子,若输入#则代表没有需要识别的句子了:"< cin>>sentence;
if(sentence[0]=='#')
{
break;
}
cout<<"步骤"<<'\t'<<"栈"<<'\t'<<"输入串"<<'\t'<<"动作"< fenxi(sentence);
}
system("pause");
return 0;
}

运行结果如下:
在这里插入图片描述
输入几个例子的结果如下:说明一下,我的代码把所有数字和小写字母都看作了i。
eg1:1+2
在这里插入图片描述
eg2:(1+2)/3+4-(5+6/7) (其实就是我举的例子,我为了简单全部换成了i)
在这里插入图片描述
eg3:((1-2)/3+4 (错误的例子)
在这里插入图片描述
最后只要输入#结束运行就行了。
好了,拜拜,祝大家生活愉快。



推荐阅读
  • 本文将介绍如何使用 Go 语言编写和运行一个简单的“Hello, World!”程序。内容涵盖开发环境配置、代码结构解析及执行步骤。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
  • 本文探讨了 C++ 中普通数组和标准库类型 vector 的初始化方法。普通数组具有固定长度,而 vector 是一种可扩展的容器,允许动态调整大小。文章详细介绍了不同初始化方式及其应用场景,并提供了代码示例以加深理解。 ... [详细]
  • CMake跨平台开发实践
    本文介绍如何使用CMake支持不同平台的代码编译。通过一个简单的示例,我们将展示如何编写CMakeLists.txt以适应Linux和Windows平台,并实现跨平台的函数调用。 ... [详细]
  • 使用C#开发SQL Server存储过程的指南
    本文介绍如何利用C#在SQL Server中创建存储过程,涵盖背景、步骤和应用场景,旨在帮助开发者更好地理解和应用这一技术。 ... [详细]
  • 在Ubuntu 16.04 LTS上配置Qt Creator开发环境
    本文详细介绍了如何在Ubuntu 16.04 LTS系统中安装和配置Qt Creator,涵盖了从下载到安装的全过程,并提供了常见问题的解决方案。 ... [详细]
  • 在当前众多持久层框架中,MyBatis(前身为iBatis)凭借其轻量级、易用性和对SQL的直接支持,成为许多开发者的首选。本文将详细探讨MyBatis的核心概念、设计理念及其优势。 ... [详细]
  • XNA 3.0 游戏编程:从 XML 文件加载数据
    本文介绍如何在 XNA 3.0 游戏项目中从 XML 文件加载数据。我们将探讨如何将 XML 数据序列化为二进制文件,并通过内容管道加载到游戏中。此外,还会涉及自定义类型读取器和写入器的实现。 ... [详细]
  • 360SRC安全应急响应:从漏洞提交到修复的全过程
    本文详细介绍了360SRC平台处理一起关键安全事件的过程,涵盖从漏洞提交、验证、排查到最终修复的各个环节。通过这一案例,展示了360在安全应急响应方面的专业能力和严谨态度。 ... [详细]
  • 解读MySQL查询执行计划的详细指南
    本文旨在帮助开发者和数据库管理员深入了解如何解读MySQL查询执行计划。通过详细的解析,您将掌握优化查询性能的关键技巧,了解各种访问类型和额外信息的含义。 ... [详细]
  • 本文详细介绍了macOS系统的核心组件,包括如何管理其安全特性——系统完整性保护(SIP),并探讨了不同版本的更新亮点。对于使用macOS系统的用户来说,了解这些信息有助于更好地管理和优化系统性能。 ... [详细]
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • Google最新推出的嵌入AI技术的便携式相机Clips现已上架,旨在通过人工智能技术自动捕捉用户生活中值得纪念的时刻,帮助人们减少照片数量过多的问题。 ... [详细]
  • 解决微信电脑版无法刷朋友圈问题:使用安卓远程投屏方案
    在工作期间想要浏览微信和朋友圈却不太方便?虽然微信电脑版目前不支持直接刷朋友圈,但通过远程投屏技术,可以轻松实现在电脑上操作安卓设备的功能。 ... [详细]
author-avatar
montplus手工饼干
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有