首页
社区
课程
招聘
[原创]03 C语言-数据类型
2021-3-26 17:49 7054

[原创]03 C语言-数据类型

2021-3-26 17:49
7054

变量类型

    我们都知道编程语言当中有变量这样的概念。我的理解是变量就是一种容器,它可以存储数据。那么这个变量存储什么样的格式的数据就是由变量类型来决定的了。

    C语言当中,它的变量类型有如下几种:


变量类型分类
注释
基本类型整数类型
浮点类型
int , double,char .....
构造类型数组类型
结构体类型
共用体类型

指针类型


空类型

    注意:在学习数据类型主要从两个角度来思考,一是占用的宽度,也可以认为是占用的字节数。二是写入的数据格式。


一;基本类型

1.1;整数类型

    整数类型的种类有char , short , int , long 。。。。这些类型的不同点在哪里呢?首先他们不同的种类对应的宽度不一样。简单说就是占用的字节数不一样。他们的格式是什么呢?我们来详细分析。注释:整数类型在二进制当中的存储格式为补码形式存储。

    无符号数

    我们存储的数据都是以补码的形式存储(正数的补码与原码相同)。所以,我们用如下代码来查看一下。

#include <stdio.h>
#include <stdlib.h>

int main() {
	unsigned int a = 1;

	__asm {
		int 3
	}
	return 0;
}

    运行之后,我们来看一下汇编语句以及在内存中查看一下存储的数据

    现在我们知道了,无符号数的占用多少字节有数据格式决定。存储的方式有编码方式决定。

    有符号数

    接下来,我们用下面的代码,来看看有符号数是如何存储的。

#include <stdio.h>
#include <stdlib.h>

int main() {
	signed int a = -1;
	signed int b = 1;

	__asm {
		int 3
	}
	return 0;
}

    由反汇编代码,我们可以看到在有符号的数据类型当中,都是以补码的形式存储在内存当中。

    

1.2;浮点类型

    常见的浮点类型的种类有float(4字节) , double(8字节) 。我们知道整数类型是采用的是补码的格式存储在主存当中的。浮点类型的存储方式就很不一样。我们来看看float 和double 这两个浮点类型是如何存储的。详细如下图:他是遵从IEEE编码规范的

    首先,我们知道如何将十进制的浮点数如何转换成二进制表示。我们使用8.25来计算一下这个浮点数。我们先将9.25拆分开来,分为整数部分9,以及小数部分0.25。

    整数部分转换为二进制: 9 = 1001

    小数部分转换为二进制:0.25 = 01

    接下来我们就知道了。十进制9.25转为二进制表示1001.01。那么我们将该表示方法转换为科学计数法即:1.00101 * 2e3 。接下来我们将数值对应放入Float 类型来看看。 它存入主存当中得二进制为0   10000010    00101000000000000000000  ,转换为十六进制为:41140000 (注释:其中指数部分填写运算,如果在转换科学计数法得时候小数点是向左边移动在指数部分最高位至1,如果小数点向右移动,则指数部分最高位至0,且余下部分由指数值-1得出新值在低位部分填充,比如文中案例,指数为3,即小数点向左移动,且3-1=2 换算成二进制为10。所以指数部分为10000010 )


    上面,我们已经计算出来,接下来,我们设计一个代码,看看是不是正确的。 

#include <stdio.h>
#include <stdlib.h>

int main() {
	__asm {
		int 3
	}
	float a = 9.25F;
	return 0;
}

    编译发现结果完全正确

二;构造类型

2.1;数组

    数组是一种数据结构,简单的说他就是一段连续的变量。那么,我们有可能会听到什么int 类型的数组,这又是什么呢?我们来想想。还记得我们的数据类型嘛?有int 类型,占用4个字节。有Char类型,占用1个字节。那么什么类型的数组,本质上就是什么样的数据类型组成的连续的存储空间。我们用代码感受一下:

#include <stdio.h>
#include <stdlib.h>

int main() {

	__asm {
		int 3
	}
	int age[10] = { 1,2,3,4,5,6,7,8,9,0 };
	double age2[10] = { 1,2,3,4,5,6,7,8,9,0 };
	char age3[10] = { 1,2,3,4,5,6,7,8,9,0 };
	return 0;
}

    代码描述,该代码主要是定义了int , double,char类型的数组,且都在这两个数组当中预先存入的值。接下来我们编译一下,查看一下对应的汇编代码,我们发现,int 类型的数组元素存储的大小为dword,double类型的数组元素存储的大小为mmword,char类型的数组元素存储的大小为byte。

     9: 	int age[10] = { 1,2,3,4,5,6,7,8,9,0 };
006A511F  mov         dword ptr [age],1  
006A5126  mov         dword ptr [ebp-28h],2  
006A512D  mov         dword ptr [ebp-24h],3  
006A5134  mov         dword ptr [ebp-20h],4  
006A513B  mov         dword ptr [ebp-1Ch],5  
006A5142  mov         dword ptr [ebp-18h],6  
006A5149  mov         dword ptr [ebp-14h],7  
006A5150  mov         dword ptr [ebp-10h],8  
006A5157  mov         dword ptr [ebp-0Ch],9  
006A515E  mov         dword ptr [ebp-8],0  
    10: 	double age2[10] = { 1,2,3,4,5,6,7,8,9,0 };
006A5165  movsd       xmm0,mmword ptr [__real@3ff0000000000000 (06A7BD0h)]  
006A516D  movsd       mmword ptr [age2],xmm0  
006A5175  movsd       xmm0,mmword ptr [__real@4000000000000000 (06A7BD8h)]  
006A517D  movsd       mmword ptr [ebp-7Ch],xmm0  
006A5182  movsd       xmm0,mmword ptr [__real@4008000000000000 (06A7BE0h)]  
006A518A  movsd       mmword ptr [ebp-74h],xmm0  
006A518F  movsd       xmm0,mmword ptr [__real@4010000000000000 (06A7BE8h)]  
006A5197  movsd       mmword ptr [ebp-6Ch],xmm0  
006A519C  movsd       xmm0,mmword ptr [__real@4014000000000000 (06A7CD0h)]  
006A51A4  movsd       mmword ptr [ebp-64h],xmm0  
006A51A9  movsd       xmm0,mmword ptr [__real@4018000000000000 (06A7CD8h)]  
006A51B1  movsd       mmword ptr [ebp-5Ch],xmm0  
006A51B6  movsd       xmm0,mmword ptr [__real@401c000000000000 (06A7CE0h)]  
006A51BE  movsd       mmword ptr [ebp-54h],xmm0  
006A51C3  movsd       xmm0,mmword ptr [__real@4020000000000000 (06A7CE8h)]  
006A51CB  movsd       mmword ptr [ebp-4Ch],xmm0  
006A51D0  movsd       xmm0,mmword ptr [__real@4022000000000000 (06A7CF0h)]  
006A51D8  movsd       mmword ptr [ebp-44h],xmm0  
006A51DD  xorps       xmm0,xmm0  
006A51E0  movsd       mmword ptr [ebp-3Ch],xmm0  
    11: 	char age3[10] = { 1,2,3,4,5,6,7,8,9,0 };
006A51E5  mov         byte ptr [age3],1  
006A51EC  mov         byte ptr [ebp-97h],2  
006A51F3  mov         byte ptr [ebp-96h],3  
006A51FA  mov         byte ptr [ebp-95h],4  
006A5201  mov         byte ptr [ebp-94h],5  
006A5208  mov         byte ptr [ebp-93h],6  
006A520F  mov         byte ptr [ebp-92h],7  
006A5216  mov         byte ptr [ebp-91h],8  
006A521D  mov         byte ptr [ebp-90h],9  
006A5224  mov         byte ptr [ebp-8Fh],0

    我们在跟踪一下内存看看。

    我们仔细看看,在汇编代码当中,数组的元素表示是使用的EBP,我们直到EBP寄存器当中的值始终指向的是当前栈底部。所以,这么看起来他是在栈空间咯。前面我们直到,使用栈空间的函数预先一定会分配一定的缓冲区。我们由下图位置直到确实有划分缓冲区。

    那么总结一下,所谓的数组,就是连续的不同类型的数组空间。

2.2;多维数组

    既然提到了数组,不免我们会联想到多维数组。那么多维数组的本质又是什么呢。同样我们用如下代码对比看看。

#include <stdio.h>
#include <stdlib.h>

int main() {

	__asm {
		int 3
	}
	int age[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int age2[2][5] = {
		{1,2,3,4,5},
		{6,7,8,9,0}
	};
	return 0;
}

    该代码同样是定义了一个一维数组,和多维数组。那么我们看看对应的汇编代码。

     9: 	int age[10] = { 1,2,3,4,5,6,7,8,9,0 };
00A650FF  mov         dword ptr [age],1  
00A65106  mov         dword ptr [ebp-28h],2  
00A6510D  mov         dword ptr [ebp-24h],3  
00A65114  mov         dword ptr [ebp-20h],4  
00A6511B  mov         dword ptr [ebp-1Ch],5  
00A65122  mov         dword ptr [ebp-18h],6  
00A65129  mov         dword ptr [ebp-14h],7  
00A65130  mov         dword ptr [ebp-10h],8  
00A65137  mov         dword ptr [ebp-0Ch],9  
00A6513E  mov         dword ptr [ebp-8],0  
    10: 	int age2[2][5] = {
    11: 		{1,2,3,4,5},
00A65145  mov         dword ptr [age2],1  
00A6514C  mov         dword ptr [ebp-58h],2  
00A65153  mov         dword ptr [ebp-54h],3  
00A6515A  mov         dword ptr [ebp-50h],4  
00A65161  mov         dword ptr [ebp-4Ch],5  
    12: 		{6,7,8,9,0}
00A65168  mov         dword ptr [ebp-48h],6  
00A6516F  mov         dword ptr [ebp-44h],7  
00A65176  mov         dword ptr [ebp-40h],8  
00A6517D  mov         dword ptr [ebp-3Ch],9  
00A65184  mov         dword ptr [ebp-38h],0  
    13: 	};
    14: 	return 0;

    这个时候,我们由汇编代码来看,好像两者没有什么区别,一维数组age[10]通过EBP偏移来定位数组元素。多维数组age2[2][5]同样是如此。本质上好像没有什么区别。由此可见,无论是一维数组还是多维数组他的本质都是线性的连续存储在内存空间的。(即物理数据结构相同,逻辑结构不同)


    现在我们了解了本质,那么问题来了。编译器是怎么能够做到分组并且定位的呢?比如?age2[1][3] 表示的是第二个分组的第3个元素。那他是怎么做到的呢?其实很简单。编译器只是使用了一个简单的算法。如:1*5+3。这样得出的结果就是下标为8的元素。

三;指针类型

    指针得概念通常是这么说的“存入地址得变量”我想了想,对也不全对。我得理解是:指针也是变量类型得一种,它和整数类型变量,浮点数类型变量是一样的,只是它有自己不同的类型属性罢了。其中“存入地址得变量”这个,我认为是这样的,编译器在处理指针类型当中存储的值的时候,会将它的值按照“地址”来处理。并非将地址放入指针当中。

    下面我们详细的介绍一下指针类型。我们知道,指针类型是使用“*”配合常见的数据类型或者自定义的数据类型组合成为指针类型的,比如:int * 就是int类型的指针,double * 便是double类型的指针。

3.1;基本数据类型指针的长度;

    既然指针也是一种数据类型,那么同样我们也是需要从“长度”以及“数据格式”来学习。我们这里只是考虑一下长度问题,我们通过常见得使用方法,汇编状态下看看代码:

int main() {
	__asm int 3
	int a = 1;
	double c = 1.2;
	char d = 'a';


	int * q = &a;
	double* b = &c;
	char* e = &d;
	return 0;
}

    在“反汇编”窗口当中,我们发现对应的汇编代码。在基本类型当中,指针类型的长度为dword ,也就是4个字节。

    11: 	int * q = &a;
00D45121  lea         eax,[a]  
00D45124  mov         dword ptr [q],eax  
    12: 	double* b = &c;
00D45127  lea         eax,[c]  
00D4512A  mov         dword ptr [b],eax  
    13: 	char* e = &d;
00D4512D  lea         eax,[d]  
00D45130  mov         dword ptr [e],eax

3.2;多“*”指针;

    了解指针的朋友,都知道,或者见过这样的一个写法“char ** a ”两个星号甚至多个星号等定义指针。我们用下面代码,来看看多*号指针的长度是多少。由验证得知,多星号的指针长度依然是4个字节。

int main() {
	__asm int 3
	char** b;
	char*** c;
	char**** e;
	b = (char **)4; //强制转换
	c = (char***)4;
	e = (char****)4;
	return 0;
}

//汇编代码
     9: 	b = (char **)4; //强制转换
006E50FF  mov         dword ptr [b],4  
    10: 	c = (char***)4;
006E5106  mov         dword ptr [c],4  
    11: 	e = (char****)4;
006E510D  mov         dword ptr [e],4

    接下来,我们用如下代码测试一下,多*号指针的类型是什么样子的。

#include <stdio.h>
#include <stdlib.h>

int main() {
	__asm int 3

	char**** e;

	e = (char***)4;
	return 0;
}

    运行完成之后,我们发现,代码报错,出现如下提示:

    由此可见,多*号指针虽然同样是指针,但是星号的数目不同,他们就不是同一个指针类型。   

3.2;指针其他特性;

    3.2.1;指针的自增(++),自减(--)操作。

#include <stdio.h>
#include <stdlib.h>

int main() {
	//__asm int 3

	int b = 1;
	int * a = &b;
	printf("%p\n", a);
	a--;
	printf("%p\n", a);
	a++;
	printf("%p\n", a);

	system("pause");
	return 0;
}

    运行结果如下:

    我们知道,在其他基本类型,++ , -- 都是自增1,自减1。那么对于单*类型指针,就是自增4,自减4。

3.3;指针与&关系以及指针类型问题

    首先,我们都知道一个“*”表示单指针类型,多个“*”表示多级指针。今天我们来具体的说明一下。我们先进行一步步的推导吧。首先来看如下代码:

#include <iostream>
using namespace std;

int main() {
    int a = 100;
    int* p = &a;
    int** p2 = &p;
    int*** p3 = &p; //报错代码
    int* b = *p2;
    return 0;
}

    它运行是一定会产生错误的。那么会产生什么错误呢?我们看下图,它的报错提示是“int**”类型的值不能用于初始化“int ***”类型的实体。"int ***"类型我们是知道它是我们定义修饰的P3指针的类型。那么,很明显,我们就知道“int **” 类型就是指的是“&p”了。在细心发现。“p ”指针的类型为“int*”,那么加上符号“&”之后形成新的类型即为“int **”。也就是符号“&”生成的新类型的数据就是在原来类型基础上增加一个“*”。

    接下来,我们修改一下代码,

#include <iostream>
using namespace std;

int main() {
    int a = 100;
    int* p = &a;
    int** p2 = &p;
    int*** p3 = &p2; 

    int* b = *p; //报错位置
    return 0;
}

    同样的,代码运行会产生错误,错误如下图。“int*”类型,我们知道是b ,那么int 类型就是指的是*p 。这样总结下来就是符号“*”产生的新的数据类型就是在原来基础上面减去一个“*”。

    简单总结一下:

    1;"int a = 100" 变量a 的数据类型就是int 。

    2;&a 的数据类型就是“int * ”

    3;“int* p = &a;”... “int* b = *p” 中 “*p”的数据类型就是int。


3.4;函数指针

     函数指针的格式:    返回类型 (调用约定 *变量名)(参数列表);

     接下来,我们举例说明:

#include <iostream>
using namespace std;

int main() {
    //定义函数指针变量
    int(__cdecl * pFun)(int, int);

    //为函数指针变量赋值
    pFun = (int(__cdecl*)(int, int))123;

    //使用函数指针
    int r = pFun(1, 2);
    return 0;
}

    











补充知识点

原码:最高位为符号位,其余各位的数值本身的绝对值

反码:

1;正数:反码与原码相同

2;负数:符号位为1,其余位对原码取反

补码:

1;正数:补码与原码相同

2;负数:符号位为1,其余位对原码取反加1



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-5-1 11:24 被天象独行编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (1)
雪    币: 997
活跃值: (1518)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
WMBa0 2023-1-25 19:52
2
0
谢谢大佬
游客
登录 | 注册 方可回帖
返回