AI智能
改变未来

(二)羽夏看C语言——容器


写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读**(一)羽夏看C语言——简述** ,从而方便学习本教程。

容器指的是什么

  说到容器,你可能会联想到日常生活中家里的用喝水的

杯子

;如果接触过计算机的人,可能会想到高大上的东西,比如应用广泛的虚拟化容器

Docker

。无论联想到的是什么,它们具有同一个属性——装东西。  计算机运作的数据需要容器,比如内存或者硬盘。在编程语言层面,它具有的容器就形式各样:

变量

常量

数组

结构体

共用体

等等。在汇编层面,它具有的容器有

寄存器

内存

(这个内存和计算机的内存不是一个东西,通常来说计算机的内存指内存条,此处含义为内存地址空间,请自行科普)。此篇将C语言层面的那些能够存储数据的

常见容器

汇编

逐一联系起来。

变量

  In computer programming, a variable or scalar is a storage location (identified by a memory address) paired with an associated symbolic name, which contains some known or unknown quantity of information referred to as a value; or in easy terms, a variable is a container for a particular type of data (like integer, float, String and etc…). ——《维基百科》

  英文看不懂,那就翻译一下:在计算机编程中,变量或标量是一个存储位置(由内存地址标识),与一个相关符号名配对,其中包含一些已知或未知数量的信息,称为值;或者简单地说:

变量是特定类型数据(如整数、浮点、字符串等)的容器

  变量是什么,对于编程语言来说,变量就是一个特定类型数据的**

容器

**,正如我在文本章开篇说明的。对于CPU来说,目前它具有以下容器:

容器名称 大小
BYTE 1个字节
WORD 2个字节
DWORD 4个字节
FWORD 6个字节
QWORD 8个字节

  然而对于32位CPU,它就没有比

DWORD

还大的容器了,具体原因请到汇编进行学习查看(需要额外声明,当你用x32dbg调试器随便打开一个程序,发现有大于等于8个字节的容器,那是因为有

Intel

CPU

集成了

FPU

,它是专门用于处理

浮点运算

的寄存器,不特殊说明仅指普通CPU寄存器)。

  对于C语言来说,它具有以下容器:

容器名称 大小
char 1个字节
short 2个字节
int 4个字节
long 4个字节
float 4个字节
double 8个字节

  学习C语言,很多初学者学完可能都会有的误区:认为char类型是用来存储字符的,short是用来存储短整数类型 诸如此类的印象。如果这样认识变量,就太肤浅了,你就没学会C语言,变量的本质是容器,是用来组织数据的方式

char

类型不是

字符

类型,而是

字节

类型(能够装1个字节数据的容器,字节类型是我的说法)。其他的基础变量类型以此类推。

C语言有byte类型,前提包含"Windows.h"头文件,你可以查看它的定义,你会发现如下代码:

typedef unsigned char byte;

可以下一条结论:字节类型就是无符号的

char

类型,它也是字节类型。

  既然说到符号和无符号,它们到底有什么不同。前面说

char

unsigned char

都是我所谓的

字节

类型。从内存或者寄存器存储的视角来看,它只是显示方式的不同。举个例子,如果一个数,在一个字节大小的容器中,存储

0xF4

这一个数(这个是16进制的写法,如果不会,可参考我的下一篇文章)。如果用有符号显示,它是

-12

,如果用无符号的显示,它是

244

  铺垫了这么多,是时候打开VS,来说明变量和汇编的关系。我们新建一个控制台工程,输入以下代码以供测试:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){char ch = 1;short s = 2;int i = 3;int unsigned ui=0;long l = 4;float f = 5;double d = 6;system("pause");return 0;}

  我们在

main函数

头部下一个断点,开始调试,切换到汇编模式,你会看到如下图所示的结果:

  图中汇编代码看不懂的同志,请自行补缺。

指针

  C语言的精髓是指针,我相信不少人会有所耳闻。很多初学者把指针神话了,甚至僵硬化使用,就是因为对

变量是容器

的本质没有理解到位。指针也是变量,只不过有一点点特殊,通常用来存放地址编号罢了。  下面我会给出一段代码,请回答注释当中的问题,看看你学的指针到底怎么样,也看看你对我所述的学习情况。

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int i = 65534;int* pi = (int *)5;//这样对吗?unsigned char* pch = (unsigned char*)&i;printf_s("pch指向的地址存储的值:%d\\n", *pch);//这样对吗?*pch的值到底是多少?system("pause");return 0;}

  先别着急检验,我先科普一下

小端存储

再继续:

The order of digits in a computer that is the opposite of how humans view numbers. The digits on the left are less in value than the digits on the right. ——《维基百科》

  举个例子,一个用十六进制表示的32位数据:0x12345678,存放在存储字长是32位的存储单元中,按低字节到高字节的存储顺序为0x78、0x56、0x34和0x12,通常的CPU采用小端存储,但也有大端存储的,请自行搜索。

  答案将在下一篇文章进行揭晓。

  以上只是拿一维指针进行介绍,以下拿多维指针继续介绍:

#include <iostream>using namespace std;//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.h//并删除 using namespace std;//将 cout 改为 printf_s(可以 printf,但微软编译器编译会报错,自行科普)int main(){int* pi1;int** pi2;char* pch1;char** pch2;float*** pf3;cout << "以上指针的大小:" << endl<< sizeof(pi1) << endl<< sizeof(pi2) << endl<< sizeof(pch1) << endl<< sizeof(pch2) << endl<< sizeof(pf3) << endl;cout << "以上指针取值一次的大小:" << endl<< sizeof(*pi1) << endl<< sizeof(*pi2) << endl<< sizeof(*pch1) << endl<< sizeof(*pch2) << endl<< sizeof(*pf3) << endl;cout << "对能再取值的指针进一步取值的大小:" << endl<< sizeof(**pch2) << endl<< sizeof(**pf3) << endl;system("pause");return 0;}

  这次不必看汇编代码了,看看结果:

以上指针的大小:44444以上指针取值一次的大小:44144对能再取值的指针进一步取值的大小:14请按任意键继续. . .

  我们可以下如下结论:所有的指针都是一个大小,为4个字节(32位)。

常量

In computer programming, a constant is a value that should not be altered by the program during normal execution, i.e., the value is constant.When associated with an identifier, a constant is said to be "named", although the terms "constant" and "named constant" are often used interchangeably. This is contrasted with a variable, which is an identifier with a value that can be changed during normal execution. ——《维基百科》

  在计算机编程中,常量是程序在正常执行期间不应更改的值,即该值为常量当与标识符关联时,常量被称为“命名”,尽管术语“常量”和“命名常量”经常互换使用。这与变量不同,变量是一个标识符,其值可以在正常执行期间更改。

  如上说明了常量是什么和与变量的区别。然而不幸的是,在汇编层面,它们本质是一个东西。我们将用相同的方式用如下代码进行验证:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int a = 5;const int b = 5;system("pause");return 0;}

  如下就是验证结果:

  既然常量和变量本质是一样的,常量也是可以被修改的,那么我们用以下代码进行验证:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int a = 5;const int b = 5;printf_s("b的旧值:%d\\n", b);int* pb = (int*)&b;*pb = 10;printf_s("b的新值:%d\\n", b);system("pause");return 0;}

  结果运行后你发现,与预想的根本不一致:

b的旧值:5b的新值:5请按任意键继续. . .

  你到此可能怀疑我说的是有问题的,认为常量是无法改变的,其实它已经被改了,请看一下

局部变量

b

的值,它已经是

10

了,如下图。

  那么,是什么原因导致更改后的值仍然是

5

呢,看汇编你就会明白了,这都是编译器的把戏,如下图:

  你可以看到,编译器编译好后调用此函数时压根就没有把

变量b的地址的内容

放入堆栈中,而是直接将

5

压入堆栈,所以导致以上“奇怪”的问题。

局部变量与全局变量

  学过C语言的同志,应该都知道局部变量和全局变量的区别和作用域。但是,为什么全局变量每个函数都可以到处用,而局部变量不行呢?让我们从汇编层面来看看是为什么。  先准备如下代码以供实验:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint quanju = 10;int main(){int jubu = 0;jubu = quanju + 8;system("pause");return 0;}

  然后我们看一下汇编,你会看到如下图所示结果(关键部分):

  从汇编代码我们很容易看出,局部变量被翻译为堆栈中的一个“临时地址”,这个是由于ebp寻址提栈提供的缓冲区,函数结束后会被平栈,通过普通方式无法使用该值(如果不懂的话,后面将会有一篇文章用来讲述函数的)。而全局变量直接是一个写死的地址,编译完一个程序后,该地址不会发生变化,这就是所谓的全局变量每个函数随意使用,而局部变量不行的原因。

公共变量和私有变量 ❗

  公共变量和私有变量是什么定义我就不详细描述了,就是字面意思。我们做一个实验进行验证一下:

#include <iostream>struct MyStruct{private:int hide = 10;public:int show = 20;};int main(){MyStruct stru;int h = stru.hide;int s = stru.show;system("pause");return 0;}

  有些眼尖的朋友一眼就能看出,这个代码是编译不过去的,因为

hide

MyStruct

的私有全局变量,不能访问。但既然是全局变量,就是一个地址,难道就不能访问吗,答案是能够访问,需要一点手段——指针。我们来看下代码:

#include <iostream>struct MyStruct{private:int hide = 10;public:int show = 20;};int main(){MyStruct stru;int* ph = (int*)&stru;int s = stru.show;printf_s("hide: %d\\nshow: %d\\n", *ph, s);system("pause");return 0;}

  你将会得到如下图所示结果:

  这个是不是巧合呢,让我们看一看汇编代码:

  如果你不看后面的文章,你可能不能完全弄明白,我简单说明一下。第一个图的

lea ecx,[ebp-10h]

汇编指令就是传说的

this

指针,指向该结构体的地址。下一句的

call

就是调用构造函数,虽然我没有写构造函数,但它默认会有一个无参的构造函数(你可能会犯嘀咕,这明显不是类的特征吗?是的,没错,在C语言中,类和结构体是一个东西,没有任何区别,但是通常会把只有数据的称之为结构体,还有功能函数的称之为类)。说了这些,第二张图片也就能看明白了。如果不明白可以在讨论区留言。

数组

  最经常用的数组,当属字符串了,数组是最常用的数据组织方式之一。先从最简单的一维数组来看看数组和汇编的关系。我们先用如下代码进行实验:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int a[] = { 1,2,3,4,5,6 };int a0 = a[0];int a5 = a[5];system("pause");return 0;}

  然后查看汇编,得到的结果如下:

  你可能没见过imul指令,这个是有符号乘法:

imul ecx,eax,0

用数学表达式来写的话就是

ecx = eax * 0

,以此类推。其他的关于一维数组我就不过多介绍了。

  接下来我们看看更高维数的数组,先以最简单的二维数组试刀,简单修改一下原来的代码:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int a[][2] = { 1,2,3, 4,5,6 };int a0 = a[0][0];int a5 = a[1][1];system("pause");return 0;}

  然后看一下反汇编:

  很多初学者学习二维数组的时候,都是用画表格的形式:|索引|0|1||:-:|:-:|:-:||0|1|2||1|3|4||2|5|6|

  这样的方式虽然直观好理解,但会带来一个误区,仿佛内存也是这样存储二维数组的。但是,如果是三维数组甚至更高维数的呢?在计算机中,数组是沿着线性地址顺序存储的,无论是多少维。 上面图示的汇编代码就体现出这个特性,本人不多论述。

  二维数组都试了试,再来个三维的加深印象:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int a[][3][2] = { 1,2,3,4,5,6,7,8,9,10,11,12 };int ia = a[1][1][1];system("pause");return 0;}

  汇编代码如下图所示:

  正确的内存存储示意图:

  有了这个示意图,是不是更好理解了多维数组。

数组与指针

  说到指针和数组的关系,我们看一下代码:

#include <iostream>//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.hint main(){int a[] = { 1,2,3,4,5,6 };int* p = a;printf_s("%d,%d\\n", a[2], p[2]);system("pause");return 0;}

  汇编代码如下:

  仔细观察发现,它们取值方式几乎差不多。虽然可以说数组和指针获取值本质上几乎差不多,但数组一旦定义,在程序的生命周期就不能随意改变。指针是随意的,想指哪就指哪。

结构体 ❓

  因为这篇文章只是介绍

容器

,故我们接下来继续介绍

C的结构体

。为什么这么说呢,是因为

C

不支持带函数的结构体,而

C++

可以。

  对于此类的结构体的讨论,请用以下的代码做实验,在做这个实验之前,请思考一下它的输出结果是多少:

#include <iostream>using namespace std;//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.h//将 cout 改为 printf_s(可以 printf,但微软编译器编译会报错,自行科普)struct MyStruct{int i;char ch;char ch1;int i1;double d;};int main(){cout << sizeof(MyStruct) << endl;system("pause");return 0;}

  你可能会惊奇的发现,输出的结果为

24

,到底是为什么呢,不应该是

18

个字节吗?这就是因为

字节对齐

的缘故。

  对于32位的CPU,它最擅长一次操作4个字节的容器。为了提高性能,就必须牺牲一些东西,那就是空间,即所谓的

拿空间换时间

的操作,不满4个字节按4个字节计算。

  当然,你也可以强制它一个字节接一个字节的对齐,在定义的结构体之前使用

#pragma pack(1)

就可实现,你可以添加后重新编译查看结果。

共用体

  共用体,简单来说。就是一个地址多个别名,举个简单的例子就能明白:

#include <iostream>using namespace std;//如果是C,请自行将头文件包含改为 stdio.h 和 stdlib.h//将 cout 改为 printf_s(可以 printf,但微软编译器编译会报错,自行科普)union MyUnion{int a;int b;int c;unsigned char ch;};int main(){cout << sizeof(MyUnion) << endl;MyUnion test; //如果是C,请在本行前添加 union 关键字test.a = 0xfffe;printf_s("a:%d,b:%d,c:%d,ch:%d\\n", test.a, test.b, test.c, test.ch);system("pause");return 0;}

  反汇编结果:

  输出结果:

4a:65534,b:65534,c:65534,ch:254请按任意键继续. . .

  由此可看出:共用体的所有变量(更合理的说是别名),都共用一个地址。

下一篇

  (二)羽夏看C语言——进制

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » (二)羽夏看C语言——容器