《高级语言中如何操作内存——变量和值》源站链接,阅读体验更佳~
计算机世界有一个常识——所有的数据和指令必须经由内存才能进入CPU的寄存器进而被CPU使用,那么我们程序操作的主战场就是内存,内存操作也就顺理成章成为了程序中最高频的操作。

为了节目的效果,我们先来看一段8086平台下的汇编代码:

assume cs:code
code segmentdw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987hstart: mov bx, 0mov ax, 0mov cx, 8s:  add ax, cs:[bx]add bx, 2loop smov ax, 4c00hint 21h
code ends
end start

在说明上面汇编代码的功能之前,我们先来介绍一下loop指令,loop指令的格式是loop label,CPU在执行loop指令的时候要进行两步操作——1.将cx积存器中的值减一,2.判断cx寄存器中的值,不为0则转至标号处执行程序,如果为0则不跳转,继续向下执行。

上面那段代码代码的功能就是计算0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h这8个16进制数的和,为了正确加载数据,上面的代码中使用了寄存器间接寻址的寻址方式,即将寄存器bx中的值作为变址使用,而且我们在上面存储数据的时候使用了dw关键字,其指出其后的每一个数据的长度为两个字节,汇编语言的编译器会负责帮我们组织数据。

上面的代码中使用纯粹的偏移量来进行内存访问,非常不方便,我们也可以使用标号的方式来进行内存的访问,如下汇编代码:

assume cs:code, ds:data
data segmentary db 10, 20, 30, 40, 50ty  db 20
data ends
code segment
start: mov cx, (ty-ary)...
code ends
end start

上面的代码中,我们在数据段中使用了标号 ary和ty,而在代码段中使用表达式ty-ary来计算ary所代表的数组(暂且认为它就是一个数组的头指针吧)的长度。而在实际汇编的时候,这个表达式会被汇编程序计算,最终其实就等价于5-0,也就是5.

通过上面的汇编代码,我们可以知道,汇编语言中可以有表达式,但是汇编语言中的表达式完全是静态的,是由汇编程序计算的,而程序所有的运行时行为都是由指令体现出来的。实际上汇编语言中所有的标号都是一种伪指令,它们代表的都是某个地址值而已,更准确的说是一个地址的偏移量,使用标号的目的是使得我们更加方便的引用一个地址值,而且这个值是在静态阶段已知的,它只是具有辅助作用,并不直接参与内存访问。

上面的程序中我们使用了伪指令dw来告诉汇编程序我们的数据占用多大的内存空间,而我们从内存中读取数据的时候,要读取多大的内存空间呢?这一部分信息隐藏在了指令的目标寄存器中,比如上面的add ax cs:[bx]这行代码的目标寄存器是ax,是16位的寄存器,我们要从地址cs:[bx]开始读取16位也就是两个字节,如果我们把目标寄存器换成ah,那么就成了读取8位也就是一个字节了。

可以看出,我们在使用汇编语言进行内存访问的时候,数据应该以怎样的步长读取,读取到的数据可以进行什么样的操作,语言本身能提供给我们的元数据信息非常有限,而且几乎没有对我们进行任何的限制。我们所能依靠的只有代码注释和自己的大脑,这样的编程方式就像走钢丝一样,稍不留神就会坠入万丈深渊。

左值和数据存储区域

编程界流传着这样一句话——机器生汇编,汇编生B,B生C,C生万物。在高级程序设计语言领域中,很多的高级语言都深受C语言的影响,而且很多语言的自举都是从C语言开始的,而我这个系列的文章正是谈自己对于高级程序设计语言的理解,所以很多的故事都是从C语言开始讲起的。

在C语言中,用于存储值的数据存储区域统称为数据对象,但是考虑到数据对象这个术语可能会和面向对象编程中的对象混淆,所以我们这里采用数据存储区域这个术语。

那么,在汇编语言中的内存访问操作在C语言中相对应的就是对数据存储区域的访问。

在C语言中,对某个数据存储区域的引用被统称为左值(lvalue),意思是它可以出现在赋值操作的左侧(可以是赋值操作的目标操作数),这是很好理解的,因为赋值操作的目的是把值存储在某个数据存储区域上,这就意味着,只有一个数据存储区域的引用也就是左值可以作为赋值操作的目标操作数,(左值(lvalue)中的l就是来自于此)

其实左值的概念是适用于所有的高级程序设计语言的,而且,在高级语言中访问某个数据存储区域的唯一方式就是使用左值,除此之外,别无他法。

也就是说,左值和数据存储区域存在如下的关系:

我们在代码中使用左值来引用(代表)某个数据存储区域,我们可以简单地认为一个左值指向某个数据存储区域,它代表的是其引用的数据存储区域的首地址。

左值的两种操作——LHS查询和RHS查询

我们知道,访存操作一共就两种,要么读、要么写;对应的,我们对左值的操作其实一共就两种——分别是LHS(Left Hand Side)查询和RHS(Right Hand Side)查询。

"L"和"R"的含义,它们分别代表左侧和右侧。什么东西的左侧和右侧呢?是一个赋值操作的左侧和右侧

LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,比如对函数的形式参数的赋值。因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。

  • **当变量出现在赋值操作的左侧的时候,编译器或者解释器对其执行LHS查询,这个时候我们并不关心这个变量所引用的数据存储区域中的值是什么,而是想要找到这个数据存储区域,并将赋值操作的源值放到这个数据存储区域中(对其进行赋值操作),所以说,LHS查询对应内存访问中的Store(写)操作。**如下图所示:

    在进行LHS查询的时候,lvalue就代表其所引用的数据存储区域这个容器,赋值操作的源会直接被写入这个数据存储区域中,语言的编译器或者解释器负责实现底层的写内存操作。

  • **RHS查询对应的是内存访问中的load(mov/读)操作,这个时候我们不关心这个变量所引用的数据存储区域,而只是想得到这个数据存储区域中所存储的值。从这个角度来说,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说应该是非左侧。**如下js代码:

    let obj = {say: () => {console.log('Hello World')}
    }obj.say()
    

    上面的obj.say()这一行代码中,obj并不是赋值操作的目标,也就是说它出现的位置是赋值操作的非左侧,那么这个时候js解释器就会对obj进行RHS查询。如下图:

    在进行LHS查询的时候,lvalue就代表其所引用的数据存储区域中所存储的值,这时候我们相当于直接使用值,语言的编译器或者解释器负责实现底层的读内存操作。

总而言之,RHS查询关心的是变量所引用的数据存储区域中的值,它对应的是内存的读操作,而LHS查询关心的是变量所引用的数据存储区域,其对应的是内存的写操作。

左值是类型化的存储区域

上文,我们介绍了在高级语言中我们通过左值来定位一个数据存储区域,对左值进行RHS查询和LHS查询分别对应对数据存储区域的读操作和写操作。

但是如何进行读和写?应该读或者写多大的区域?我们把数据读出来之后又应该怎么使用呢?和汇编不同的是,高级语言对这方面进行了抽象,提供了数据类型的概念,数据类型提供了一个值需要占用多大的内存空间、一个值可以进行哪些操作这些元数据信息。语言的编译器或者解释器也正是根据数据类型配合左值来帮我们处理底层的访存操作。

也就是说,数据类型不仅代表了内存空间的大小,同时我们在编码的时候可以施加于其上的操作也受数据类型的限制(根据语言的不同,这种限制有可能是在编译时,有可能是在运行时或者这两个阶段都存在),而且不同的类型具有不同的语义,这样一个内存空间中数据的作用就能在一定程度上通过源代码感知到,实现了一定程度上的自文档化。

但是在数据类型这个地方,产生了两种分化,一种是静态数据类型,一种是动态数据类型。对于静态数据类型而言,左值引用的数据存储区域在语言的编译阶段就能够确认,而对于动态类型来说,则需要在运行时进行查找。

静态类型的语言在编译时左值所引用的数据存储区域中所存储的值的数据类型就是已知的,而对于动态类型的语言来说,左值所引用的数据存储区域中的值必须在运行时才能确定。但是,对于一个值来说,它一定是有一个明确且唯一的类型的。

语言的类型系统也是构成一门语言的世界观的语言核心特性,在下一篇文章中我会详细介绍语言的类型系统,这里也就不做过多介绍了。

变量实体——左值的诞生

在大多数的文献中并不区分左值和变量这两个概念,认为这两者是等价的。但是,在这篇文章中我们需要一个概念来指明某个变量是我们在编写代码的时候所声明的‘变量’这种程序实体,所以这里我们引入一个概念——变量实体,用来指代我们在源代码中所声明的‘变量’这种程序实体。如下js代码:

let obj = {a: 1,b: 2,c: 3
}

上面代码中的obj是我们声明的一个变量实体,而且是上面代码中唯一的一个变量。obj.aobj.bobj.c是变量,但是它并不是我们声明的,其身份是对象obj的一个属性。

在高级语言中,获得一个左值的最终方式是声明一个变量实体。

左值树

为什么上文如此强调变量实体这个概念呢?那是因为我们对左值的使用都是从我们声明的某个变量实体开始的,而且在引入自定义数据结构之后,自定义结构中的属性也是一个左值,而最终所有的左值会构成一个逻辑上的树形结构。

如下的C语言代码:

#include <stdio.h>typedef struct {char province[32];char city[32];char area[32];char address[128];
} Location;typedef struct {char name[32];char header[32];Location location;
} School;typedef struct {int age;char name[32];School school;
} Student;int main() {Student laomst = {.age=24,.name="laomst",.school={.name="qknd",.location={"shandong","qingdao","chengyang","changchenglu 700 hao"},.header="songxiyun"}};printf("%s", laomst.school.location.address);printf("\n");Student *laomst_ptr = &laomst;printf("%s", laomst_ptr->school.location.address);
}

上面的代码中我们声明了一个名为laomst,类型为Student的变量实体,而其实这个变量实体拉起了如下的一个左值的树形结构:

这棵树中的每一个节点都是一个左值,而我们声明的laomst这个变量实体正是这个左值树的根节点。

现在,我们来总结一下:

  • 左值就是变量,因为左值引用了一个用来存储数据的数据存储区域,而其中存储的值是可以改变的。
  • 复合数据结构类型的左值代表了一个左值树,而这个树的根最终就是我们在代码中声明的某个变量实体。

我强调变量实体这个概念的原因就是其代表的是某个左值树的根节点,我们在程序中获得左值的唯一方式是声明一个变量实体,同时,我们对于某个左值的访问也是从一个变量实体(左值树的根节点)开始的,比如上面代码中的printf("%s", laomst.school.location.address);这行代码,我们想要访问上图树中最右下角的address节点,也是从根laomst开始一层层走下去的。

当然,某些语言可以在运行是改变词法作用域的范围,比如js中的with,在这里我们不做特殊的讨论。

左值树是逻辑上的概念

上文中我们给出了laomst这个左值的内存结构的逻辑图,其实在真正存储的时候使用的是一段连续的内存空间,其物理图如下所示:

但是这并不影响其逻辑上是一棵树。当然,使用连续内存存储的根本原因是C语言是静态数据类型的,如果是动态类型的语言,可能就真的是一个树形结构了。

其实数据结构更多的是一种逻辑上的数据组织,而同一个数据结构在物理上可能有多种存储方式,比如顺序表可以使用数组存储也可以使用链表存储、完全二叉树也经常使用数组进行存储、图可以使用邻接矩阵(二维数组)也可使用邻接表(链表方式)进行存储…

不同的物理存储方式可能决定不同的逻辑特性,毕竟逻辑是基于物理的;但是根据空间局部性原理来说,使用连续的内存来存储数据在时间上可能会更加高效一些。

我后面也会有数据结构和算法相关的文章,其中会详细介绍我对数据结构和算法的理解。

简单名和限定名

我们再次回顾一下在【左值树】小节中的代码中的最后四行,如下:

  printf("%s", laomst.school.location.address);printf("\n");Student *laomst_ptr = &laomst;printf("%s", laomst_ptr->school.location.address);

我们访问address这个左值的时候使用了一系列的标识符,而不是直接使用address这个标识符。上面代码中的laomst.school.location.address其实也是一个标识符,是一个名称,其引用一个代码实体,而我们称这样的名称为限定名。而laomst_ptr->school.location.address中的形式有所不同,laomst_ptr是一个指针类型的变量,使用它访问其内部的属性需要使用->操作符,但是不管形式如何,只要涉及左值树中子节点的访问,我们使用的都是限定名称。

与限定名对应的是简单名,laomst.school.location.address中的laomst就是一个简单名,什么是简单名和限定名呢?

  • 简单名

    简单名就是指我们在声明一个代码实体的时候分配给代码实体的标识符,如下Java代码:

    public class Test {private String foo;public String foo() {return "";}
    }
    

    上面的代码中存在三个简单名,分别是类名Test、类的成员变量foo和类的成员方法foo

  • 限定名

    限定名就是指,我们在引用一个代码实体的时候,不能使用它的简单名,而是必须用其所属的那个代码实体的简单名对其进行限制,最典型的场景就是我们访问自定义类型的内部属性的时候,需要用自定义类型的变量名限定其内部属性的简单名,如下Java代码所示:

    public class Person {private String name;public Person(String name) {this.name = name;}public String getName() {return name;}
    }public class Test {public static void main(String[] args) {Person p = new Person("Tom");String pname = p.getName();}
    }
    

    上述代码中的String pname = p.getName()在访问Person类型的变量p的getName方法的时候,不是直接使用getName这个简单名,而是使用p对getName这个简单名进行了限定,在面向对象的术语中,称我们向p这个Person类型的对象发送了一个消息。

对于变量来说,不难发现,简单名对应的其实就是一个左值树的根,也就是我们声明的一个变量实体。

需要注意的是,上面的类名Person也是一个简单名,但是这里我们只讨论变量,不讨论其他类型的程序实体,其他类型的程序实体需要在具体语言中具体分析。

引出简单名和限定名这两个概念是为了方便接下来对变量作用域的介绍。

变量的作用域

我们讨论作用域的目的是弄明白编译器或者解释器是如何搜索代码实体的,它是在什么地方搜索我们所声明的代码实体来构建程序的上下文的呢?

答案就是——编译器或解释器是在作用域中搜索我们的代码实体的。

在计算机编程中,作用域是使得名称绑定(一个标识符和代码实体(例如变量)的绑定关系)有效的一块区域:在这个区域中,名称可以用来引用代码实体在程序的其他区域中,相同的名称可能引用不同的代码实体(在不同的作用域中名称可能具有不同的绑定),也有可能压根不引用任何实体(在这个作用域中没有关于这个名称的绑定关系)。

绑定的范围也称为代码实体的可见性,我们一定要注意的是,标识符的背后一定对应了一个代码实体,我们要讨论的是代码实体的可见性,而不是这个名称的可见性

对应于代码上,作用域是程序的一部分,它本身就是或者它可以作为一组绑定的范围。想要给出作用域的一个精确定义是比较困难的,但是我们在编码时作用域在很大程度上对应于块、函数或文件,具体取决于语言和代码实体的类型。

作用域也用来指所有可见的实体的集合,或在程序的一部分或程序中给定的点有效的名称,更正确的说法为上下文或环境

----- 维基百科

对于一个程序实体,如果我们能够通过简单名对其进行引用,那么我们就称当前的程序上下文处在该程序实体的作用域内,而对于限定名所引用的程序实体的绑定,可能会涉及其他的规则,这里我们不做深入的讨论,因为对于所有程序实体的引用都是从一个简单名开始的。

同样的,为了让内容尽量普适,我们这里只介绍变量实体的作用域,而且是基于运行时调用栈来进行分析的,对于其他类型的实体还是到具体语言中具体分析。

作用域共有两种主要的工作模型:静态作用域动态作用域,这两种作用域的不同之处在于其对**“程序的一部分”**的理解方式是不太一样的。

静态作用域(词法作用域)

静态作用域是被大多数编程语言所采用的作用域规则,**这时候“程序的一部分”指的是“源代码的一部分(是一个文本区域)”。**这时候“程序的一部分”是一个文本属性,由语言实现独立于运行时调用栈,使得这种名称匹配可以只通过分析静态文本就可以实现。因此,静态作用域又被称为“词法作用域”。

也就是说,在具有词法作用域的语言中,名称解析取决于源代码中的位置和词法上下文中的位置,静态范围允许程序员将参数、变量、常量、类型、函数等对象引用作为简单的名称替换进行推理。这使得制作模块化代码变得更加容易,因为可以隔离地理解本地命名结构。

词法作用域中对名称的解析可以在编译时确定,也被称为早期绑定。

另外,即使都是使用词法作用域的工作方式,不同语言的作用域规则也是有差别的,比如C、JavaScript等语言允许局部变量有“死区”(在子作用域中允许声明和父作用域中同名的变量,并且子作用域中的变量声明会覆盖父作用域中的变量声明);而在Java语言中,大多数情况下是不允许在子作用域中声明和父作用域中同名的变量的。而且在有的语言中允许在函数中定义函数(支持闭包特性),而有的则不允许。

大多数词法作用域语言的词法作用域规则都受到了ALGOL语言的影响,遵循层级嵌套的模型,我们以一段JavaScript代码为例,进行简要的讨论(该例摘自《你不知道的JavaScript 上卷》):

在这个例子中有三个逐级嵌套的作用域。为了帮助理解,可以将它们想象成几个逐级包含的气泡。

气泡1包含着整个全局作用域,其中只有一个标识符:foo

气泡2包含着foo 所创建的作用域,其中有三个标识符:abarb

气泡3包含着bar 所创建的作用域,其中只有一个标识符:c

作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的

动态作用域

目前来说,只有少部分的语言使用动态作用域规则,比如Bash脚本、Lisp语言和一些模板语言。这时候“程序的一部分”是指“部分运行时间(执行期间的时间段)”。

对于动态作用域,全局标识符是指与最新环境关联的标识符。从技术上讲,这意味着每个标识符都有一个全局绑定堆栈。引入具有名称的本地变量会将绑定推送到全局堆栈(可能为空),当控制流离开作用域时,该变量将弹出该堆栈。这在编译时无法完成,因为绑定堆栈仅在运行时存在,这就是为什么这种类型的范围范围称为动态范围。

也就是说,在具有动态作用域的语言中,名称解析取决于遇到由执行上下文调用上下文确定的名称时的程序状态,动态范围迫使程序员预测调用模块代码的所有可能的动态上下文,分析起来会比较困难。

动态作用域的名称解析通常只能在运行时确定,因此称为后期绑定(动态绑定)。

现在,我们观察一下下面的Bash脚本:

#!/bin/bashx=1function f() {echo "Line-06: f: $x";x=2;
}function g() {local x=3;f;echo "Line-13: g: $x";
}gecho "Line-18: $x"

上面脚本的运行结果如下:

观察一下我们不难发现,第6行f函数中输出的x变量的值是第11行g函数中声明的本地变量x,而不是全局中声明的变量x,这和静态作用域所产生的行为是完全不同的。

使用C语言中的预编译指令模拟动态作用域

在现代语言中,预处理器的宏扩展是事实上动态作用域的一个关键示例。例如C语言中的宏编译指令#define,它提供给编译之前运行的预编译器使用,只用来转换源代码,而不解析名称,当时当其定义的宏被展开到源代码中之后,其中的名称被解析,就像动态范围一样。

如下C语言代码:

#define ADD_A(x) x + avoid add_one(int *x) {const int a = 1;*x = ADD_A(*x); // 当宏被展开的时候,这行代码变成了 *x = *x + a;
}

上面的宏中访问了一个表示符a,而在*x = ADD_A(*x)这行代码被展开之后就变为了*x = *x + a,可以看出其访问到的正是局部变量a,这时候对a的访问就和调用ADD_A(x)这个宏的地方有关系了,这和动态作用域的行为是非常相似的

同时,我们也可以看出,对于拥有动态作用域的语言来说,对一个子程序的定义需要依赖使用它的上下文,这对提高程序的模块化是非常不利的。

右值

我们在前面的文章中已经不止一次提到过左值这个概念了,有左必有右,那么什么是右值呢?

顾名思义,右值就是出现在赋值操作的右侧的值。但是按照这个定义来说,左值也是一个右值,因为左值可以进行的操作有两种,分别是RHS和LHS,也就是说,左值既可以出现在赋值操作的左侧,也可以出现在赋值操作的右侧。

所以,右值应该是不能出现在赋值操作的左侧的值,其严格的定义为:赋值操作的右侧(作为赋值操作的数据来源)的代码实体,但是这个代码实体本身不能是一个左值。

加入了右值不能是一个左值的限制,这就把左值和右值区别开来。比如下面的C语言代码:

int ex;
int why;
why = 42;
zee = why;
ex = 2 * (why + zee);

这里,ex和why都是左值,它们既可以用于赋值操作的左侧也可以用于赋值操作的右侧;而42是一个字面值,它并不能引用一个数据存储区域,所以它不能出现在赋值操作的左侧,即它是一个右值;而表达式why+zee会产生一个值,它也不能引用一个特定的数据存储区域,所以它也是一个右值。

其实通过上面的例子,我们也可以看出,右值其实就是一个字面值或者是一个复合表达式的值。

其实在当前的C语言规范中,描述赋值操作右侧的值的时候使用的是表达式的值这个术语。使用术语表达式的值其实是更加准确的,因为每个表达式都有一个值,也就是我们可以把表达式看成一个值,而不管这个值是如何产生的。

我们这里所说的右值其实是表达式的值的一个子集——不是左值的表达式。

右值的数据存储区域

我们上面提到右值是一个不能出现在赋值操作左侧的值,也就是代表右值的代码实体并没有引用某个数据存储区域。但是常识是所有的数据必须加载到内存中,进而被CPU加载到寄存器中从而被程序使用的。那么也就意味着右值虽然没有引用一个数据存储区域,但是它也是有对应的数据存储区域的。

回想一下使用汇编编写代码的场景,在汇编中其实是没有变量这个概念的,汇编中的变量其实就是寄存器,而寄存器其实本身就是一个数据存储区域。我们在汇编中对数据进行处理的时候,处理结果要么被暂存到寄存器中供下一步使用,要么被写入内存当中供后续的步骤使用。

其实高级语言中表达式的计算过程也是类似的,表达式的计算结果肯定是要被存储起来的。其实这就对应了我们在《高级语言中的短语和句子——表达式和语句》一文中所介绍的副作用和序列点

使用右值可能会发生数据拷贝

表达式计算过程中产生的中间值需要被存储起来供下一步的计算使用,而在使用这些中间值的过程中有可能会发生值的拷贝,单纯的计算可能不会发生值的拷贝,但是一旦涉及到函数调用,在使用函数返回值的时候,值的复制往往是无法避免的。

函数调用是一种特殊的表达式,因为其涉及流程控制,所以对于函数调用表达式的值的计算需要在内存中进行中转,我们下一篇文章中会详细介绍函数。

常量

上文中我们聊完了变量(左值),这篇文章中就顺带着介绍一下常量吧。

什么是常量呢?常量应该具有如下的特征:

  • 常量具有值
  • 常量是只读的,即我们只能读取其值,而不能修改这个值
  • 常量的值在编译期就是可以确定的

如果要完全符合上面的特征,我们发现只有字面值才能被称为常量。

但是,有时候我们又需要一种这样的变量:

  • 它的值在运行时才能被计算出来
  • 在它的整个生命周期中,只能被赋值一次,也就是说其被初始化之后就不能再作为左值使用了。

这种机制需要语言提供支持,例如C语言中的const变量、Java中的final变量等等。对于这样的变量,其本质上还是一个左值,但是其符合我们上面的约束——初始化后不能再进行赋值操作,所以我们称之为不可变左值。

总的来说,语言中的常量包括两种类型,一种是字面值,这是真正意义上的常量,另一种是不可变左值,不可变左值需要语言提供支持。

不可变左值

不可变左值还涉及很多的细节,比如一些不可变左值的值在编译阶段可能就是可以计算的,这个时候语言的编译器可能会对其进行宏编译,比如Java中的常量变量

总结

在这篇文章中我们简单介绍了高级语言如何使用数据类型和变量来帮我们进行访存操作,而一门语言如何理解数据则构成了一门语言最基本的世界观。在我们学习一门语言的时候,吃透一门语言的类型系统是必不可少的。

不同语言的类型系统可能具有很大的不同,我们往往会从动态/静态和强类型/弱类型等方面来描述一门语言的类型系统的特征,下一篇文章我们就来讨论一下高级程序设计语言的类型系统。

同时,这篇文章比较长,感谢你耐心读完。本人深知技术水平和表达能力有限,如果文中有什么地方不合理或者你有其他不同的思考和看法,欢迎随时和我进行讨论(laomst@163.com)。

高级语言中如何操作内存——变量和值相关推荐

  1. 【C 语言】指针间接赋值 ( 直接修改 和 间接修改 指针变量 的值 | 在函数中 间接修改 指针变量 的值 | 在函数中 间接修改 外部变量 的原理 )

    文章目录 一.直接修改 和 间接修改 指针变量 的值 二.在函数中 间接修改 指针变量 的值 三.在函数中 间接修改 外部变量 的原理 一.直接修改 和 间接修改 指针变量 的值 直接修改 指针变量 ...

  2. 在java中的交换方法有哪些_java中交换两个变量的值有哪几种方法,交换两个变量a和b的值...

    java中交换两个变量的值有哪几种方法在Java中,有哪些方法可以交换两个变量的值, 方法: 1.定义临时变量 2.没有必要定义临时变量 3.使用位运算符 (学习视频分享:java课程) 代码示例: ...

  3. String为什么无法在方法中修改原有string变量的值

    题目 public class Exam10{ String str=new String("good"); char[]ch={'a','b','c'}; public stat ...

  4. c语言指针访问 静态变量_使用C中的指针访问变量的值

    c语言指针访问 静态变量 As we know that a pointer is a special type of variable that is used to store the memor ...

  5. 修改list中对象的值_怎样在S7-200 SMART中监控和修改变量的值?

    我们知道在S7-300 PLC编程调试的时候,可以通过在Step7的变量表中监视和修改变量.那么在S7-200 SMART编程调试的时候,如果我们希望监控某个变量的值或者对其进行修改,应如何做呢?今天 ...

  6. matlab 打印多个变量,matlab中怎么输出一个变量的值

    在MATLAB中,可以使用sprintf来格式化输出变量.MATLAB的sprintf用法几乎和C中的printf一样,参数都是printf(FORMAT,A,.)MATLAB的sprintf会返回一 ...

  7. python两个变量互换值编程_在编程中实现两个变量的值交换

    在最初接触编程的时候,使用的是C语言,在交换两个变量的值的时候需要引入第三个变量作为temp值.如下面第①种方法. 方法①:加入第三个temp变量来实现交换 我们以C语言为例,也是最常见的方法 voi ...

  8. 在c语言中函数的定义变量的值为,变量定义(C语言中变量的声明和定义)

    变量定义(C语言中变量的声明和定义),哪吒游戏网给大家带来详细的变量定义(C语言中变量的声明和定义)介绍,大家可以阅读一下,希望这篇变量定义(C语言中变量的声明和定义)可以给你带来参考价值. 3.函数 ...

  9. wuzhicms 查看模板中的所有可用变量和值

    将代码放到模板中.{php print_r(get_defined_vars());} 页面显示如下: 这样看不清楚. 通过查看页面源文件的方式打开. 例如:chrome 浏览器打开方式,在页面空白处 ...

最新文章

  1. 中国电子学会图形化四级编程题:程序优化
  2. 从无到有算法养成篇-算法基础常识
  3. linux epoll事件模型详解
  4. 【干货】周鸿祎谈雷军:能不能All In是一个核武器
  5. centos-7.2 node.js免编译安装
  6. scala 字符串转换数组_如何在Scala中将字节数组转换为字符串?
  7. c语言中闰年 日期 天数 统计出在某个特定的年份中,出现了多少次既是13号又是星期五的情形
  8. GridView日期列使用DataFormatString格式化技巧
  9. pm2 启动 nodejs 项目
  10. XiaomiRouter自学之路(02-软硬件环境搭建)
  11. mybatis中更新mysql时间多了一秒
  12. 2020 CCPC - 网络选拔赛 签到计划
  13. Oracle 11.2.0.1 rac升级到11.2.0.4
  14. 用AS实现微信界面设计
  15. 过滤器Filter方法详解(init,doFileter,destory)
  16. 日语助词て的所有的语法点,请牢记
  17. ElasticSearch重启失败的解决方案
  18. 2021年12月中国汽车企业出口量排行榜:特斯拉上海12月出口量仅245辆,占比其全年出口总量的0.15%(附月榜TOP39详单)
  19. 【LaTex】beamer中插入GIF动画
  20. 10 架构设计文档-致远OA

热门文章

  1. ZXing生成二维码、读取二维码
  2. JESD204B简介
  3. python用while写出金字塔_使用while循环的星金字塔python嵌套while循环
  4. Unity中的部分环境光照设置以及简单雾的效果
  5. 企业应如何正确运用股权激励
  6. C语言实现(封装、继承和多态)
  7. Vert.x - SpringBoot 整合 vertx 使用 thymeleaf、freemarker 模板引擎
  8. 西门子与三菱PLC报文比较
  9. xrdp linux 3389 端口,在 Linux 中使用 xrdp - Azure Virtual Machines | Microsoft Docs
  10. systemtap PHP,systemtap 进阶