百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

myzbx 2025-06-18 23:02 32 浏览

话说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("%d\n",sizeof(stru)); // 16
    printf("%d\n",int(&stru.i)-int(&stru)); // 4
}

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

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

代码示例:

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

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

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

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

看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%X\n",hz.j,hz.i,hz.L,hz.k);
    //for(int i=0;i<2;i++)
        //printf("%2X\n",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("%s\n",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-

相关推荐

如何设计一个优秀的电子商务产品详情页

加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...

怎么在JS中使用Ajax进行异步请求?

大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...

中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革

前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...

前端监控 SDK 开发分享_前端监控系统 开源

一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...

Ajax 会被 fetch 取代吗?Axios 怎么办?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...

前端面试题《AJAX》_前端面试ajax考点汇总

1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...

Ajax 详细介绍_ajax

1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...

6款可替代dreamweaver的工具_替代powerdesigner的工具

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

福斯《死侍》发布新剧照 &quot;小贱贱&quot;韦德被改造前造型曝光

时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...

不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!

Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...

2025 年 Python 爬虫四大前沿技术:从异步到 AI

作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...

最贱超级英雄《死侍》来了!_死侍超燃

死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...

停止javascript的ajax请求,取消axios请求,取消reactfetch请求

一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...