您当前的位置:首页 > 电脑百科 > 数据库 > 百科

C|数据存储地址与字节偏移、数据索引

时间:2021-12-08 10:48:09  来源:  作者:小智雅汇

话说C是面向内存的编程语言。数据要能存得进去,取得出来,且要考虑效率。不管是顺序存储还是链式存储,其寻址方式总是很重要。顺序存储是连续存储。同质结构的数组通过其索引表示位置偏移,异质结构的结构体通过其成员名(字段名)的类型大小及对齐方式来计算字节偏移。链式存储通过一个额外的指针(地址)作为数据成员来指示其相邻节点的地址信息。

1 数组以数组名和索引提供对数据元素的引用

数组是对同质元素的连续存储,是许多结构数据结构的基础。

    int i = 0;
    int j = 0;
    int a[12] = {0};  
    a[i] = 1; //a[i]相当于a+i,编译器维护a的元素的类型信息,偏移i个元素长度
    int b[3][5] = {0};
    b[i][j] = 1; //b[i][j] 相当于*(*(b+i)+j),编译器维护b的元素及元素的类型信息,
                  // 偏移i个元素长度,j个元素的元素的长度

内数组名与运算符“&”、“sizeof”被编译器解释为整个数组,其它情况被解释为指针(一个指向数组首元素的地址),这是为将数组名用做函数实参准备的语法机制:

int arr[i][j][k][……];
int (*arp)[j][k][……] = arr;  //利用arp做形参,便可以传递arr做实参
// 函数体内对指针参数的解引用便是对指针指向对象的操作
void foo(int (*arp)[j][k][……], int i)// 便可在函数体内用做左值或右值
{
    *(*(*(*(arp+i)+j)+k)+……); //注意arp的类型是int[j][k][……],arp+i时便可以获得正常的偏移
}

C语言对于数组引用不进行任何边界检查。

对于局部数组,函数的局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中。这两种情况结合到一起就能导致严重的栈错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息(向低地址区溢出)。当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret指令时,就会出现很严重的错误。

一种特别常见的状态破坏称为缓冲区溢出(buffer overflow)。

对于全局数组,其溢出操作同样有不可预见的后果。

2 结构体利用结构体变量名和成员对象名提供地址偏移来访问元素

C语言的struct声明创建一个数据类型,将可能不同类型的对象聚合到一个对象中。用名字来引用结构的各个组成部分。类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段(fileld)的字节偏移。以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。

void foo()
{
    struct Stru{
        char ch;    // ch要对齐到s,否则s需要两次读取
        short s;    // s要对齐到i,否则i需要两次读取
        int i;      // i要对齐到d,否则i需要两次读取
        double d;
    };              // 结构体整体也要对齐到一个字长
    Stru stru;
    printf("%dn",sizeof(stru)); // 16
    printf("%dn",int(&stru.i)-int(&stru)); // 4
}

结构体偏移时会考虑内存对齐,以实现更好的引用效率(避免更多次的引用,因为CPU会一次读取一个字长长度的数据(sizeof(int)或sizeofof(void*))。

如果将更大的数据类型靠前放则可以减少对后续元素读取的影响,从而节省内存空间:

C|数据存储地址与字节偏移、数据索引

 

代码示例:

void foo()
{
    struct S4{
        char c;     // c要对齐到i,否则i需要两次读取
        int i;      // i要对齐到d,否则i需要两次读取
        char d;
    }s4;           // 结构体整体也要对齐到一个字长
    printf("%dn",sizeof(s4)); // 12
    printf("%dn",int(&s4.i)-int(&s4)); // 4
    struct S5{
        int i; 
        char c;
        char d;
    }s5;           // 结构体整体也要对齐到一个字长
    printf("%dn",sizeof(s5)); // 8
    printf("%dn",int(&s5.c)-int(&s5)); // 4
}

3 switch可以被编译器实现对case语句的直接跳转

编译器比你想象的要聪明,不但要做翻译,还要做优化。例如,你写的switch语句可能会被优化为 jump table,还会消除无用的语句(Dead code elimination)等,汇编代码有时候不仅仅是C代码的直译,也就是说,编译器可以执行不同程度的优化。

switch语句可以根据一个整数索引值进行多重分支。通过使用跳转表(jump table)可以实现较高的效率。跳转表是一个数组,表项 i 是一个代码段的地址,这个代码段实现当开关索引值等于 i 时程序应该采取的动作。程序代码用开关索引值来执行一个跳转表内的数组引用,确定跳转指令的目标。和使用一组很长的if else语句相比,使用跳转表的优点是执行开关语句的时间与开关情况的数量无关。编译器根据开关情况的数量和开关情况值的稀疏程度来翻译开关语句。当开关情况数量比较多(如4个以上),并且值的范围跨度比较小时,就会使用跳转表。

C|数据存储地址与字节偏移、数据索引

 

看if与switch的代码比较:

#include <stdio.h>

bool IsLeapYear(int y)			// 判断是否为闰年
{
	return((y%4==0&&y%100!=0)||y%400==0);
}
int DaysOfMonth(int month,int year)			// 判断某月天数的函数
{
	switch(month){
		case 1:
		case 3:
		case 5:
		case 7:
		case 8:
		case 10:
		case 12:return 31;
		case 4:
		case 6:
		case 9:
		case 11:return 30;
		case 2:if(IsLeapYear(year))
				 return 29;
			 else
				 return 28;
	}
	return 0;
}
int DaysOfMonth2(int month,int year)			// 判断某月天数的函数
{
    if(month==2)
    {
        if(IsLeapYear(year))
            return 29;
        else
            return 28;
    }
    else if(month==4 || month == 6 || month == 9 || month == 11)
        return 30;
    else
        return 31;
    
}

int main()
{
    for(int i=0;i<12;i++)
        printf("%d ",DaysOfMonth2(i+1,2021));
    getchar();
    return 0;
}

switch汇编:

9:        switch(month){
004010C8   mov         eax,dword ptr [ebp+8]
004010CB   mov         dword ptr [ebp-4],eax
004010CE   mov         ecx,dword ptr [ebp-4]
004010D1   sub         ecx,1
004010D4   mov         dword ptr [ebp-4],ecx
004010D7   cmp         dword ptr [ebp-4],0Bh
004010DB   ja          $L538+23h (00401118)
004010DD   mov         edx,dword ptr [ebp-4]
004010E0   jmp         dword ptr [edx*4+40112Bh]
10:           case 1:
11:           case 3:
12:           case 5:
13:           case 7:
14:           case 8:
15:           case 10:
16:           case 12:return 31;
004010E7   mov         eax,1Fh
004010EC   jmp         $L538+25h (0040111a)
17:           case 4:
18:           case 6:
19:           case 9:
20:           case 11:return 30;
004010EE   mov         eax,1Eh
004010F3   jmp         $L538+25h (0040111a)
21:           case 2:if(IsLeapYear(year))
004010F5   mov         eax,dword ptr [ebp+0Ch]
004010F8   push        eax
004010F9   call        @ILT+5(IsLeapYear) (0040100a)
004010FE   add         esp,4
00401101   and         eax,0FFh
00401106   test        eax,eax
00401108   je          $L538+1Ch (00401111)
22:                    return 29;
0040110A   mov         eax,1Dh
0040110F   jmp         $L538+25h (0040111a)
23:                else
24:                    return 28;
00401111   mov         eax,1Ch
00401116   jmp         $L538+25h (0040111a)
25:       }
26:       return 0;
00401118   xor         eax,eax
27:   }

else if 汇编:

30:       if(month==2)
004011A8   cmp         dword ptr [ebp+8],2
004011AC   jne         DaysOfMonth2+41h (004011d1)
31:       {
32:           if(IsLeapYear(year))
004011AE   mov         eax,dword ptr [ebp+0Ch]
004011B1   push        eax
004011B2   call        @ILT+5(IsLeapYear) (0040100a)
004011B7   add         esp,4
004011BA   and         eax,0FFh
004011BF   test        eax,eax
004011C1   je          DaysOfMonth2+3Ah (004011ca)
33:               return 29;
004011C3   mov         eax,1Dh
004011C8   jmp         DaysOfMonth2+65h (004011f5)
34:           else
35:               return 28;
004011CA   mov         eax,1Ch
004011CF   jmp         DaysOfMonth2+65h (004011f5)
36:       }
37:       else if(month==4 || month == 6 || month == 9 || month == 11)
004011D1   cmp         dword ptr [ebp+8],4
004011D5   je          DaysOfMonth2+59h (004011e9)
004011D7   cmp         dword ptr [ebp+8],6
004011DB   je          DaysOfMonth2+59h (004011e9)
004011DD   cmp         dword ptr [ebp+8],9
004011E1   je          DaysOfMonth2+59h (004011e9)
004011E3   cmp         dword ptr [ebp+8],0Bh
004011E7   jne         DaysOfMonth2+60h (004011f0)
38:           return 30;
004011E9   mov         eax,1Eh
004011EE   jmp         DaysOfMonth2+65h (004011f5)
39:       else
40:           return 31;
004011F0   mov         eax,1Fh
41:
42:   }

4 hash表的最好情况可以实现到数据的直接映射

哈希表是维护字典的一种非常实用的方法。他们利用了这样一个事实,即一旦有了索引(index),在数组中查找一个元素需要常数时间。散列函数是一种将键(key)映射到整数的数学函数。我们将使用散列函数的值作为数组的索引(将元素的键值映射为下标),并将元素存储在该位置。

使用这样一种称为散列(hash)的策略可以非常有效地实现映射(map),在这种策略中,键(key)被转换为一个整数,该整数决定了实现(implementation)应该在哪里查找结果。

hasing算法的一个常见实现是分配一个动态的bucket数组,每个bucket都包含一个指向该bucket的键的链表。只要条目数与存储桶数的比率不超过0.7左右,get和put方法的平均运行时间为O(1)。

下标与地址相关,让关键字也让地址相关,这就是hash,但key与下标或索引相比,包含的信息更多。

常用的hash函数种类:

4.1 除留余数法

最简单的哈希算法是将数据除以某一个常数后取余数来当索引。

h(key) = key mod b


// 哈希法的存储与查找,核心在于如何将数据的存储位置映射为数组的下标
#if 0

#include <IOStream>
using namespace std;

/*
下面这段代码是典型的用空间换时间的算法,数据与存储其所占空间的下标完全相同。
下面这段代码不具有任何的实用性,但充分说明了这种思路。
*/
int search(int h[], int key);
void store(int h[], int data);

int main()
{
    int data[1000]={0};
    int m, n;
    for (int i = 0; i < 6; i++)
    {
        cin>>n;
        store(data, n);
    }
    cin>>m;
    int result = search(data, m);
    if (result)
        cout<<"在数组中找到." <<endl;
    else
        cout<<"没有此数据!"<<endl;
    return 0;
}

int search(int d[], int key)
{
    return d[key];
}

void store(int d[], int n)
{
    d[n]=n;
}

#else
//下面是采用哈希法存储数据并实现查找的示例。
//实现哈希函数用“除法取余法”,解决冲突为“开放地址法”。

#include <iostream>
using namespace std;

int  searchHash(int h[], int len, int key);
void insertHash(int h[], int len, int data);

int main()
{
    const int hashLength = 13;      //哈希表长度
    int hashTable[hashLength]={0};
    int m, n;
                                    //创建hash
    for (int i = 0; i < 6; i++)
    {
        cout<<"请输入第"<<i<<"个值(共6个):";
        cin>>n;
        insertHash(hashTable, hashLength, n);
    }
    cout<<"请输入需要查找的值:";
    cin>>m;
    int result = searchHash(hashTable,hashLength, m);
    if (result != -1)
        cout<<"已经在数组中找到,位置为:" << result<<endl;
    else
        cout<<"没有此原始"<<endl;
    getchar();
    return 0;
}

int searchHash(int h[], int len, int key)
{
    int hashAddress = key % len;  // 哈希函数
    // 指定hashAdrress对应值存在但不是关键值,则用开放寻址法解决
    while (h[hashAddress] != 0 && h[hashAddress] != key)
    {
        hashAddress = (++hashAddress) % len;
    }
    // 查找到了开放单元,表示查找失败
    if (h[hashAddress] == 0)
        return -1;
    return hashAddress;
}

void insertHash(int h[], int len, int data) // 数据插入Hash表
{
    int hashAddress = data % len;           // 哈希函数
    // 如果key存在,则说明已经被别人占用,此时必须解决冲突
    while (h[hashAddress] != 0)
    {
        // 用开放寻址法找到
        hashAddress = (++hashAddress) % len;
    }
    // 将data存入字典中
    h[hashAddress] = data;
}
#endif

4.2 平方取中法

先计算数据的平方,然后取中间的某段数字做为索引。

4.3 折叠法

将数据转换成一串数字后,先将这串数字拆成几个部分,然后把它们加起来。

STL中的set的底层为什么用红黑树,而不是哈希表?

集合的常见运算有并、交、差等,这些运算在数据有序时对应算法的时间复杂度会大大降低,红黑树恰好是有序结构,而哈希表不是。

5 共用体只是一种字节共享(或特殊的类型泛化或类型强制转换)

联合能够规避C语言的类型系统,允许以多种类型来引用对象,用不同的字段来引用相同的内存块。与结构体不同的是,其成员不存在位移,两个字段的使用是互斥的,一个共用体总的大小等于它最大字段的大小。

void cngb()
{
    union{
        struct {
            unsigned int i:4;
            unsigned int j:4;
            unsigned int k:4;
            unsigned int L:4;
            unsigned int m:4;
            unsigned int n:4;
        };
        char hanzi[3];
    }hz;
    fflush(stdin);
    puts("查询gb2312码,请输入一个汉字:");
    gets(hz.hanzi);
    //strcpy(hz.hanzi,"中");
    printf("%X%X%X%Xn",hz.j,hz.i,hz.L,hz.k);
    //for(int i=0;i<2;i++)
        //printf("%2Xn",hz.hanzi[i]);
}

6 位域可以实现比字节长度颗粒度更小的偏移

要将一个浮点数的二进制存储用16进制的符号表示出来,使用字节的颗粒还是太大,因为一个16进制是4个二进制位,半个字节长度,可以使用位域:

#include <stdio.h>
#include <string>
using namespace std;

string double2hex(double db)
{
    union {         //用于将浮点数的二进制位解析为int位输出
        float input;
        int output;
    } data;

    union {
        struct {
            unsigned j:4;
            unsigned i:4;
        }by;
        char ch;
    }un;
    char *hexa = "0123456789ABCDEF";
    int i=0;
    int j=0;

    char *ch = (char*)&db;
    string hexd = "";
    for(i=7;i>=0;i--)
    {
        un.ch = ch[i];
        hexd += hexa[un.by.i];
        hexd += hexa[un.by.j];
        hexd += " ";
    }
    return hexd;
}
int main()
{
    string str = double2hex(-15.525);
    printf("%sn",str.c_str()); // C0 2F 0C CC CC CC CC CD
    getchar();
    return 0;
}

7 索引表以空间换时间缩小搜索范围提高搜索效率

将大范围的数据建立索引,通过查找索引来确定数据的位置是一个提高查找效率的策略。

给数据建立索引也是利用数据在文件中的偏移量。

#include <iostream>  // 建立索引表并排序
#include <fstream>
#include <cstdlib>
 
 
using namespace std;
typedef struct
{
    int NO;
    char name[8];
    int chinese;
    int math;
    int english;
    int Comprehensive;
    int total;
} Student;          //高考学生信息
 
typedef struct
{
    int NO;
    long offset;    //数据在文件中的偏移量
} StudentIndex;     //高考学生索引
 
void createIndex();
void writeIndex(StudentIndex *si, int n);
 
int main()
{
    createIndex();
    return 0;
}
/*
功能:创建索引
*/
 
void createIndex()
{
    int stuNum;
    StudentIndex *studentsIndex;                        //索引表的起始地址
    Student student;
    ifstream binaryFile("binarydata.dat", ios::in|ios::binary);
    if(!binaryFile)
    {
        cerr<<"cannot open binary file!"<<endl;
        exit(1);
    }
                                                        //建立索引
    binaryFile.read((char*)&stuNum, sizeof(stuNum));    //读入实际人数
                                                        //读入数据,建立未排序的索引表
    studentsIndex = new StudentIndex[stuNum];
    int i, j;
    long mOffset;
    for(i=0; i<stuNum; ++i)
    {
        mOffset = binaryFile.tellg();
        binaryFile.read((char*)&student, sizeof(Student));
        studentsIndex[i].NO = student.NO;
        studentsIndex[i].offset = mOffset;      //记录对应学号学生数据的偏移量
    }
                                                //关闭数据文件
    binaryFile.close();
                                                //为索引表排序
    StudentIndex temp;                          //用于交换的中间变量
    for (i=0; i<stuNum-1; i++)
        for(j=0; j<stuNum-i-1; j++)
            if (studentsIndex[j].NO>studentsIndex[j+1].NO)
            {
                temp=studentsIndex[j];
                studentsIndex[j]=studentsIndex[j+1];
                studentsIndex[j+1]=temp;
            }
                                                //将建好的索引表通过文件存储
    writeIndex(studentsIndex, stuNum);
    return;
}
 
/*
功能:将索引写入文件
参数:si - 索引表起始地址;n - 考生人数,索引记录条数
*/
void writeIndex(StudentIndex *si, int n)
{
    //打开文件
    ofstream indexFile("binarydata.idx", ios::out|ios::binary);
    if(!indexFile)
    {
        cerr<<"cannot open index file!"<<endl;
        exit(1);
    }
    int i;
    for(i=0; i<n; ++i)
    {
       //indexFile<<si[i].NO<<"t"<<si[i].offset<<endl;  //索引用作文本文件时
       indexFile.write((char*)&si[i], sizeof(StudentIndex)); //索引用二进制文件时  
    }
    //关闭文件
    indexFile.close();
    return;
}

-End-



Tags:数据存储   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
话说C是面向内存的编程语言。数据要能存得进去,取得出来,且要考虑效率。不管是顺序存储还是链式存储,其寻址方式总是很重要。顺序存储是连续存储。同质结构的数组通过其索引表...【详细内容】
2021-12-08  Tags: 数据存储  点击:(17)  评论:(0)  加入收藏
数据作为新的生产要素,其蕴含的价值日益凸显,而安全问题却愈发突出。密码技术,是实现数据安全最经济、最有效、最可靠的手段,对数据进行加密,并结合有效的密钥保护手段,可在开放环...【详细内容】
2021-11-26  Tags: 数据存储  点击:(17)  评论:(0)  加入收藏
很多小伙伴在购买电脑时,通常为了性能的大幅度提升,在加装硬盘时往往会将系统装入到单独的固态硬盘中,而我们数据存储放到机械硬盘当中,这样做有什么好处呢?本期小君就谈谈固态硬...【详细内容】
2021-10-19  Tags: 数据存储  点击:(74)  评论:(0)  加入收藏
作者:LX 一、 环境搭建 1、 启动Spark集群服务 1)启动Spark集群 2)子节点加入集群 3)查看是否加入成功 2、启动Elasticsearch数据库 1)可以启动自己安装的Elasticsearch数据库...【详细内容】
2021-06-11  Tags: 数据存储  点击:(132)  评论:(0)  加入收藏
大数据存储: HBase API,DDL,DML4.1 环境准备新建项目后在pom.xml中添加依赖:<dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-server</artifactId>...【详细内容】
2020-08-25  Tags: 数据存储  点击:(98)  评论:(0)  加入收藏
HBase写流程HBase写流程写流程:1、Client 先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server。2、访问对应的 Region Server,获取 hbase:meta 表,根据读请求的 namesp...【详细内容】
2020-07-04  Tags: 数据存储  点击:(46)  评论:(0)  加入收藏
阿里云HBase增强版(Lindorm)简介阿里云数据库HBase增强版,是基于阿里集团内部使用的Lindorm产品研发的、完全兼容HBase的云上托管数据库,从2011年开始正式承载阿里内部业务的...【详细内容】
2020-05-07  Tags: 数据存储  点击:(112)  评论:(0)  加入收藏
时至今日,互联网和物联网已经渗透到我们生活的点点滴滴,数据在其中的重要性不言而喻,一旦数据遭到恶意攻击,导致泄漏或篡改等,这两张网的安全性也就不复存在。因此,2015年重新颁布...【详细内容】
2020-04-23  Tags: 数据存储  点击:(132)  评论:(0)  加入收藏
在Windows操作系统中,如果我们通过MySql的全自动安装包安装数据库,那么数据库的数据将会默认被存储在C盘中,而C盘作为系统盘,一般情况下我们并不想放和系统无关的一些内容。那么...【详细内容】
2019-06-17  Tags: 数据存储  点击:(431)  评论:(0)  加入收藏
▌简易百科推荐
1增1.1【插入单行】insert [into] <表名> (列名) values (列值)例:insert into Strdents (姓名,性别,出生日期) values (&#39;开心朋朋&#39;,&#39;男&#39;,&#39;1980/6/15&#3...【详细内容】
2021-12-27  快乐火车9d3    Tags:SQL   点击:(1)  评论:(0)  加入收藏
最近发现还有不少做开发的小伙伴,在写存储过程的时候,在参考已有的不同的写法时,往往很迷茫, 不知道各种写法孰优孰劣,该选用哪种写法,以及各种写法的优缺点,本文以一个简单的查询...【详细内容】
2021-12-23  linux上的码农    Tags:sql   点击:(9)  评论:(0)  加入收藏
《开源精选》是我们分享Github、Gitee等开源社区中优质项目的栏目,包括技术、学习、实用与各种有趣的内容。本期推荐的HasorDB 是一个全功能数据库访问工具,提供对象映射、丰...【详细内容】
2021-12-22  GitHub精选    Tags:HasorDB   点击:(5)  评论:(0)  加入收藏
作者丨Rafal Grzegorczyk译者丨陈骏策划丨孙淑娟【51CTO.com原创稿件】您是否还在手动对数据库执行各种脚本?您是否还在浪费时间去验证数据库脚本的正确性?您是否还需要将...【详细内容】
2021-12-22    51CTO  Tags:Liquibase   点击:(3)  评论:(0)  加入收藏
场景描述:由于生产环境的表比较复杂,字段很多。这里我们做下简化,只为说明今天要聊的问题。有两张表 tab1,tab2: tab1 数据如下: tab2 数据如下: 然后给你看下,我用来统计 name=&#3...【详细内容】
2021-12-20  Bald    Tags:SQL   点击:(5)  评论:(0)  加入收藏
前言知识无底,学海无涯,知识点虽然简单,但是比较多,所以将MySQL的基础写出来,方便自己以后查找,还有就是分享给大家。一、SQL简述1.SQL的概述Structure Query Language(结构化查...【详细内容】
2021-12-16  谣言止于独立思考    Tags:SQL基础   点击:(13)  评论:(0)  加入收藏
前言作为一名测试工程师,工作中在对测试结果进行数据比对的时候,或多或少要和数据库打交道的,要和数据库打交道,那么一些常用的 SQL 查询语法必须要掌握。最近有部分做测试小伙...【详细内容】
2021-12-14  柠檬班软件测试    Tags:SQL   点击:(15)  评论:(0)  加入收藏
话说C是面向内存的编程语言。数据要能存得进去,取得出来,且要考虑效率。不管是顺序存储还是链式存储,其寻址方式总是很重要。顺序存储是连续存储。同质结构的数组通过其索引表...【详细内容】
2021-12-08  小智雅汇    Tags:数据存储   点击:(17)  评论:(0)  加入收藏
概述DBConvert Studio 是一款强大的跨数据库迁移和同步软件,可在不同数据库格式之间转换数据库结构和数据。它将成熟、稳定、久经考验的 DBConvert 和 DBSync 核心与改进的现...【详细内容】
2021-11-17  雪竹聊运维    Tags:数据库   点击:(26)  评论:(0)  加入收藏
一、前言 大家好,我是小诚,《从0到1-全面深刻理解MySQL系列》已经来到第四章,这一章节的主要从一条SQL执行的开始,由浅入深的解析SQL语句由客户端到服务器的完整执行流程,最...【详细内容】
2021-11-09  woaker    Tags:SQL   点击:(35)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条