深度复杂空间结构运算的逻辑

简介: 深度复杂空间结构运算的逻辑

一,空间地址的逻辑使用

       地址在C/C++程序中每次是以系统开辟空间的首个地址来表现的,而数据的类型决定了地址的一次性跳转,系统在输出时也就是根据其一次性跳转的地址来解用其数据的。例如以下:

#include<stdio.h>

int main()

{

   int a = 5;

   fprintf(stdout, "%p\n", &a);//输出0099FC78地址

   fprintf(stdout, "%p\n", &a + 1);//输出0099FC7C地址

}

       从以上代码可得出,当数据开辟一块空间时,地址是先指向开辟内存开始位置的地址,而输出或引用时,系统都是从空间的开始地址找起,然后根据类型所申请的空间大小进行一次性的输出,例如:int一次性输出4个字节,char一次性输出1个字节;int型地址一次性直接跳过4字节,char型地址一次性跳过1字节。注意,地址在内存中表现为一个地址表示一个字节的大小。一次跳过n个字节,也就是一次性跳过4个地址。代码如下:

#include<stdio.h>
int main()
{
   int a = 0x02040608;//以十六进制来赋值方便后面的查看
   char* c = (char*)&a;//将转换成char类型输出,一次将从跳动4字节转成跳动1字节
   fprintf(stdout, "%x\n", a);//输出2040608
   fprintf(stdout, "%d %d %d", *c, *(c + 1), *(c + 2));//输出8 6 4
   return 0;
}

代码分析如图:


       其它数据类型也都是同样道理,无论是在栈区还是堆区抑或是静态区,都是同理使用。在以后的跟为复杂的空间使用,一般都会用以上跟为复杂的空间转换,而要想炉火纯青的运用,必须要把指针的知识点深入掌握,之前的讲解已经详细说明,在这里我就不做过多讲解了。


二,函数的地址使用

1,函数栈帧的原理

       当我们调用函数时,系统会在内存划分中的栈区里划分一块函数栈帧,这块函数栈帧就是存储函数整个框架以及函数里的参数,当函数传参的时候,形参只是在函数栈区中对实参的临时拷贝,两者占用两块不同的内存空间。

       在此,要提醒的是,既然是在栈区里开辟空间,即一但函数结束后,函数栈帧就会被系统自动销毁,在进行使用函数时一定要考虑此因素。下面的代码将会说明这一点:

#include<stdio.h>
char* GetMemory(void)
{
   char p[] = "hello world";
   return p;
}
void Test(void)
{
   char* str = NULL;
   str = GetMemory(void);
   printf(str);//将会错误输出
}
int main()
{
   Test();
   return 0;
}

输出图:

        出现以上这种莫名其妙的输出问题在于GetMemory()函数。GetMemory()函数返回类型是一个char指针型,但是当开始调用此函数时,系统先会在栈区中开辟一块函数栈帧,以便存方函数中的给中数据,一但出了函数,系统将自动销毁这块函数栈帧,里面的数据也就随之销毁了。GetMemory()函数返回数组p的首元素地址时,Test()函数中的str接受地址,但要注意的是,GetMemory()函数的栈帧及数据已被销毁,此地址中已没有了原先的数据,将会错误输出。如图:


所以,在栈区存放函数的数据不可在另一函数中调用,然而,若我们要想利用函数中的数据,就不能在栈区中存放,首先要考虑的就是如何把要利用的数据放入其他内存区域中。static关键字可把数据放入堆区中,可能有些学者已经听说过此名称,static运用的数据之所以不会销毁数据正是因为放入堆区的数据只有自己操作或整个程序结束时才得以释放。因此,以上代码经修改后如下:

#include<stdio.h>
char* GetMemory(void)
{
   static char p[] = "hello world";//系统将会在堆区中存放,数据不会被销毁
   return p;
}
void Test(void)
{
   char* str = NULL;
   str = GetMemory();
   printf(str);
}
int main()
{
   Test();
   return 0;
}

运行图:

2,动态内存的运用

       动态内存在系统内存中是存储在堆区的,但需提醒的是,在当下学习中到慎重利用堆区,利用完堆区的数据之后要记得用free函数来释放,虽然在当下的练习中可能不会有太大影响,但在以后的程序设计中,我们可能会利用较大的系统空间,如若每次调用堆区空间不及时释放,则将会浪费很大内存空间。

       开辟动态内存空间,通常使用一级指针变量并运用函数malloc,calloc,realloc三个函数。而此时的一级指针直接指向开辟空间的首个地址。接下来请观察以下代码:

#include<stdio.h>
#include<stdlib.h>
void GetMemory(char* p)
{
   p = (char*)malloc(100);
}
void Test(void)
{
   char* str = NULL;
   GetMemory(str);
   strcpy(str, "hello world");
   printf(str);
}
int main()
{
   Test();
   return 0;
}

       首先,要说明问题的是,上面的代码会出现系统崩溃,GetMemory函数返回后,str仍为NULL,即无法GetMemory函数无法将p开辟的100个字节从中带出来。我们先来观察Test函数,当strGetMemory函数传参时,形参p将会重新开辟一块空间以进行临时拷贝,此时的p与str一样,都存放NULL。当p动态开辟完空间时,将会退出函数GetMemory,此时虽然函数栈帧将随之销毁,但动态空间依然存在,即p指向开辟空间的首个地址,而此时形参没有影响到实参,虽说两者都是指针类型,但都是指向不同空间,str指向NULL。

       这一关系可能有一点难度,但其实可以这样理解,p刚开始的时候是NULL,后来指针变量p有赋予了新的地址,这个地址就是新开辟的动态内存空间的起始地址。如图所示:


以上代码要想正常按照逻辑使用要进行以下改正:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//在动态内存中,一级指针只是直接指向开辟空间,二维指针是指向地址的地址
void GetMemory(char** p)
{
   *p = (char*)malloc(100);
}
void Test(void)
{
   char* str = NULL;
   GetMemory(&str);
   strcpy(str, "hello world");
   printf(str);
   free(str);
   str = NULL;
}
int main()
{
   Test();
   return 0;
}

       当我们用二级指针进行传参时,直接传向str的地址,然后通过解引用直接找到str指向空间进行开辟,这种操作与函数通过地址来进行数据更改的道理是一样的,因此,在程序运行上将不会出现任何问题。


三,空间地址的跳动与转换

1,地址的跳动变换

       首先,要提醒的是,平常说的数组名等于首元素的地址,但是有特殊情况,“&数组名”代表的是整个数组的地址,"sizeof(数组名)"也代表整个数组的地址,而"sizeof(数组名+0)"不是整个数组的地址,只是访问数组元素的地址。除了这两个情况外,其余情况数组名都代表首元素的地址。

       首先,我们运用一维数组来观察特性:

#include<stdio.h>
int main()
{
   int a[] = { 1,2,3,4 };
   printf("%d\n", sizeof(a));//16
   //注意:只有在sizeof(数组名),里面只有数组名时才等于整个数组的地址
   //当sizeof()里的参数有其他东西时,就不等于数组的全部地址了,按正常理解即可

   printf("%d\n", sizeof(a+0));//是首元素的地址,即4/8
   printf("%d\n", sizeof(*a));//此时a为首元素的地址,解引用为首元素,为4
   printf("%d\n", sizeof(&a));//为数组的地址,大小为4/8
  //*(解引用操作符)和&(取地址)操作符“相反”
   //两者抵消后sizeof(*&a)==sizeof(a),及大小为16
   printf("%d\n", sizeof(*&a));//16
   printf("%d\n", sizeof(&a+1));//注意,&a+1仍为地址,大小为4/8
   return 0;
}

       然后,用二位数组来观察,有关细节代码和分析如下:

#include<stdio.h>
int main()
{
   int a[3][4] = { 0 };
   fprintf(stdout, "%d\n", sizeof(a));//3*4*4=48字节
  //注意:在二维数组中,可看成其元素是一维数组
   //即a[0]代表第一行一维数组的数组名

   fprintf(stdout, "%d\n", sizeof(a[0]));//大小为16字节
   fprintf(stdout, "%d\n", sizeof(a[0]+0));//即第一行数组名的首地址,为4字节
   //a+1是直接跨越了一个一维数组,即从第二行开始,表第二行数组的地址

   fprintf(stdout, "%d\n", sizeof(a + 1));//4/8字节
   //*(a+1)等效于a[1],即第二行一维数组的数组名

   fprintf(stdout, "%d\n", sizeof(*(a + 1)));//大小为16字节
   //&a[0]==(a+0),即&a[0] + 1 == a + 1

   fprintf(stdout, "%d\n", sizeof(&a[0] + 1));//第二行数组的地址,为4/8字节
   fprintf(stdout, "%d\n", sizeof(*(&a[0] + 1)));//即第二行数组,大小为4*4=16
   return 0;
}

2,地址的转换使用

       上面,我们已经明白了普通地址的之间转换时什么情况,但是当输出地址访问的空间与数据类对应的空间出现差异的话,运行起来将会出现不一样的输出。这种情况跟系统的大小字节端存储有关,在计算机中,一般都是用小字节端存储的,在这里,我也用小字节端来运用。

       首先,来观察以下代码:

#include<stdio.h>
#include<stdlib.h>
int main()
{
   int a[4] = { 1,2,3,4 };
   int* p1 = (int*)(&a + 1);
   //计算机以小端的形式的存储的情况,此时p2指针为int型,解引用将会访问4字节
 //但是转换成(int)a+1即数值加一,转换成指针后要以大小字节端进行观察

   int* p2 = (int*)((int)a + 1);
   fprintf(stdout, "%x, %x", p1[-1], *p2);//输出4和02000000
   return 0;
}

       对于p1[-1]的输出就简单了,当整个数组的地址加1后就直接跳过整个数组的地址了,所以p[-1]将访问数组的最后的那个元素。对于*p2的观察,我们先要看(int)a + 1,当首元素地址加1时将会跳到下一个字节所对应的地址,在小端存储时,a[0]数据对应4个字节,也就是一次性跳动4个地址,这四种地址对应数据为01 00 00 00,再往后对应的为02 00 00 00,当首元素加1后的4个地址所对应的数据为00 00 00 02,即最终输出。

       有了以上的知识后,我们看以下代码,提醒一下:因为常量数据在内存中是以补码形式存储的,所以直接输出常量地址会以补码的十六进制形式输出。

#include<stdio.h>
int main()
{
   int a[5][5];
   int(*p)[4] = a;//此时p为数组指针,加1直接跳过4个元素的地址,即跳过一个数组
   fprintf(stdout, "%p %d", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
   return 0;
}

       指针与指针相减等于元素个数,两者之间相差4个元素,即小减大为-4,而-4在内存中是以补码形式存储的,数值的%p是直接将其补码当作地址展示出来,所以,以-4的补码的十六进制形式输出

相关文章
|
6月前
|
存储 算法 数据处理
数据的表现形式及其运算
在数据科学和信息技术的世界里,数据的表现形式及其运算占据了至关重要的地位。数据的表现形式决定了我们如何存储、访问和处理数据,而数据的运算则决定了我们如何从这些数据中提取有价值的信息。本文将深入探讨数据的几种常见表现形式以及它们的基本运算,并通过代码示例进行说明。
155 0
|
6月前
|
存储 Shell Python
零基础学会Python编程——不同的运算:算术、关系与逻辑(1)
零基础学会Python编程——不同的运算:算术、关系与逻辑(1)
102 0
|
5月前
|
运维 Serverless 数据库
函数计算产品使用问题之如何并行运算函数计算任务,并对任务计算后的结果再进行聚合运算
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
6月前
|
SQL 关系型数据库 MySQL
无法针对行和行之间的运算
无法针对行和行之间的运算
43 0
|
6月前
|
数据处理 Python
不同类型数据间的混合运算
在编程和数据处理中,我们经常需要处理不同类型的数据,如整数、浮点数、字符串等。当这些不同类型的数据需要进行混合运算时,我们需要特别注意数据类型之间的转换和运算规则。本文将介绍不同类型数据间的混合运算,并附上相应的代码示例。
123 0
|
6月前
|
Java
基本概念【算术、 关系、逻辑、位、字符串、条件、优先级等运算符】(三)-全面详解(学习总结---从入门到深化)
基本概念【算术、 关系、逻辑、位、字符串、条件、优先级等运算符】(三)-全面详解(学习总结---从入门到深化)
70 0
逻辑代数基础
逻辑代数基础
344 1
逻辑代数基础
|
资源调度 Serverless vr&ar
【计算理论】计算理论总结 ( 上下文无关文法 ) ★★
【计算理论】计算理论总结 ( 上下文无关文法 ) ★★
201 0
【计算理论】计算理论总结 ( 上下文无关文法 ) ★★
|
vr&ar
【计算理论】计算理论总结 ( 上下文无关文法 | 乔姆斯基范式 | 乔姆斯基范式转化步骤 | 示例 ) ★★
【计算理论】计算理论总结 ( 上下文无关文法 | 乔姆斯基范式 | 乔姆斯基范式转化步骤 | 示例 ) ★★
614 0
|
C# 存储
c#位运算基本概念与计算过程
c#位运算基本概念与计算过程前言一些非常基础的东西,在实际工作中没有用到、很少用到。一旦遇到,又不知所云。最近遇到一个问题,把一个int16(short) 、两个bool变量整合成一个int32(int),当听到这个要求时,我第一反应是不是需求弄错了,后来才发现是自己才疏学浅,这里就需要位运算相关的概念。
1507 0