写在前面

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

容器指的是什么

  说到容器,你可能会联想到日常生活中家里的用喝水的杯子;如果接触过计算机的人,可能会想到高大上的东西,比如应用广泛的虚拟化容器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个字节的容器,那是因为有IntelCPU集成了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类型,它也是字节类型。

  既然说到符号和无符号,它们到底有什么不同。前面说charunsigned char都是我所谓的字节类型。从内存或者寄存器存储的视角来看,它只是显示方式的不同。举个例子,如果一个数,在一个字节大小的容器中,存储0xF4这一个数(这个是16进制的写法,如果不会,可参考我的下一篇文章)。如果用有符号显示,它是-12,如果用无符号的显示,它是244

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

#include <iostream>

int 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>

int 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; 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;
}

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

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

  我们可以下如下结论:所有的指针都是一个大小,为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>

int main()
{
int a = 5;
const int b = 5; system("pause");
return 0;
}

  如下就是验证结果:

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

#include <iostream>

int 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的旧值:5
b的新值:5
请按任意键继续. . .

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

  那么,是什么原因导致更改后的值仍然是5呢,看汇编你就会明白了,这都是编译器的把戏,如下图:

  你可以看到,编译器编译好后调用此函数时压根就没有把变量b的地址的内容放入堆栈中,而是直接将5压入堆栈,所以导致以上“奇怪”的问题。

局部变量与全局变量

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

  先准备如下代码以供实验:

#include <iostream>

int 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;
}

  有些眼尖的朋友一眼就能看出,这个代码是编译不过去的,因为hideMyStruct的私有全局变量,不能访问。但既然是全局变量,就是一个地址,难道就不能访问吗,答案是能够访问,需要一点手段——指针。我们来看下代码:

#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>

int 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>

int 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>

int 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>

int 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;
}

  汇编代码如下:

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

结构体(不含函数)

  接下来继续介绍结构体。之所以在后面加上“不含函数”,是因为这篇文章只是介绍容器,并不介绍结构体所有的使用细节,那个是你在阅读本系列教程之前应该干的事情。

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

#include <iostream>
using namespace std; 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; union MyUnion
{
int a;
int b;
int c;
unsigned char ch;
}; int main()
{
cout << sizeof(MyUnion) << endl;
MyUnion test;
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;
}

  反汇编结果:

  输出结果:

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

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

下一篇

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

(二)羽夏看C语言——容器的更多相关文章

  1. (五)羽夏看C语言——结构体与类

    写在前面   由于此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇 ...

  2. (一)羽夏看C语言——简述

    "羽夏看C语言"介绍什么   本系列从汇编的角度,比较翔实的介绍C语言.C++和C其实是一样的东西,C++的编译器只是更强大,更能帮助我们写代码,例如模板.没有特殊说明,本系列不会 ...

  3. (八)羽夏看C语言——C番外篇

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章 ...

  4. (四)羽夏看C语言——循环与跳转

    写在前面   由于此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇 ...

  5. (三)羽夏看C语言——进制

    写在前面   由于此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇 ...

  6. (六)羽夏看C语言——函数

    写在前面   由于此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇 ...

  7. (七)羽夏看C语言——模板(C++)

    写在前面   由于此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇 ...

  8. (九)羽夏看C语言——C++番外篇

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章 ...

  9. 羽夏看Win系统内核——简述

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

随机推荐

  1. Java 中 this 和 super 的用法详解

    前言 这次我们来回顾一下this和super这两个关键字的用法,作为一名Java程序员,我觉得基础是最重要的,因为它决定了我们的上限,所以我的文章大部分还是以分享Java基础知识为主,学好基础,后面的 ...

  2. SpringMVC 参数中接收之一 List

    作者:张艳涛 time:2020-07-31 SpingMVC 一.前台传数组,SpingMVC用addusers(@RequestBody List<UserPojo> userlist ...

  3. 【阅读笔记】Java核心技术卷一 #2.Chapter4

    4 对象和类 4.1 面向对象程序设计概述(略) 4.2 使用预定义类 java.time.LocalDate static LocalDate now(); static LocalDate of( ...

  4. Lazysysadmin靶机

    仅供个人娱乐 靶机信息 Lazysysadmin靶机百度云下载链接:https://pan.baidu.com/s/1pTg38wf3oWQlKNUaT-s7qQ提取码:q6zo 信息收集 nmap全 ...

  5. noi linux 2.0 体验

    一.起因 下午,我打开 noi 官网准备报名 csp j/s,一看官网展板:"noi linux 2.0 发布" 我就兴奋了起来.(9 月 1 日起开始使用, 也就意味着 csp ...

  6. C# 为什么你应该更喜欢 is 关键字而不是 == 运算符

    前言 在C# 进行开发中,检查参数值是否为null大家都用什么?本文介绍除了传统的方式==运算符,还有一种可以商用is关键字. C# 7.0 中 is 关键字的使用 传统的方式是使用==运算符: if ...

  7. Salesforce Integration 概览(七) Data Virtualization数据可视化

    本篇参考:https://resources.docs.salesforce.com/sfdc/pdf/integration_patterns_and_practices.pdf Salesforc ...

  8. 别再用CSV了,更高效的Python文件存储方案

    CSV无可厚非的是一种良好的通用文件存储方式,几乎任何一款工具或者编程语言都能对其进行读写,但是当文件特别大的时候,CSV这种存储方式就会变得十分缓慢且低效.本文将介绍几种在Python中能够代替CS ...

  9. 从零开始实现简单 RPC 框架 2:扩展利器 SPI

    RPC 框架有很多可扩展的地方,如:序列化类型.压缩类型.负载均衡类型.注册中心类型等等. 假设框架提供的注册中心只有zookeeper,但是使用者想用Eureka,修改框架以支持使用者的需求显然不是 ...

  10. Longhorn,Kubernetes 云原生分布式块存储

    Longhorn 是用于 Kubernetes 的轻量级.可靠且功能强大的分布式块存储系统. Longhorn 使用容器(containers)和微服务(microservices)实现分布式块存储. ...