【我的《冒号课堂》学习笔记】值与引用(1)语法类型

值与引用

值(value)与引用(reference)因其天生的对立性,提供了一个二分法(dichotomy)的准则。

  把数据分成两类:

    值——具有某种类型的数据

    引用——可用来获取特定数据的值

  把变量分成两类:

    值变量——表示值的变量

    引用变量——表示引用的变量

  把数据类型分成两类:

    值类型——能直接被访问的数据类型

    引用类型——借助引用才能被访问的数据类型

  把对象分成两类:  值类型对象  引用类型对象

(按此定义,C++是没有引用类型的,但这里有两处令人疑惑的地方。一、C++有一种被称为引用(reference)的类型(一种被指针更安全的引用);二、指针(包括第一点的特定引用)具有引用功能,可认为属于引用端的引用类型,在上文提到的引用类型都是指被引用端的)

 

以下言论主要限于C++、Java和C#三种语言。

先简单说说内存分配,不同语言采取的内存分配策略不尽相同,相同的语言也可能有不用的实现,但一般都有三种基本机制,按灵活度递增依次为:静态分配(static allocation)、栈分配(stack allocation)和堆分配(heap allocation)。其中静态分配发生于编译期,在静态内存区内为全局变量、静态变量、常数变量(在一些语言实现中为常数预备了常数变量存储区)等安排空间。栈分配与堆分配发生在运行期,但前者一般在编译器就可确定待分配内存的空间大小和生命周期(例外,C提供了非标准的alloca函数,可让程序员在栈上分配动态大小的内存,但仍由编译器释放),后者则可能推迟到运行期。栈内存区用于存放由new运算符、malloc函数等动态分配而得的空间(C++中,new分配的free store和由malloc分配的栈内存区有可能不一致;C#中new产生的值类型对象也可能在栈上)。

栈分配是基于简单的堆栈结构,因此效率很高。另外通常每一个线程都有独立的栈区。故而栈变量天然是线程安全(thread-safe)的。栈分配的主要缺点就是须提前分配内存,而且栈内存总容量有限,容易发生栈溢出(stack overflow)。至于栈内存的静态有效期,优劣参半:无需担心内存管理的同时无法突破作用域。

堆分配虽然比栈分配更强大灵活,但其复杂的算法影响了时间效率,内存碎片、元数据开销和可能的内存泄漏等问题也影响了空间效率。此外程序员还要负担更多的内存管理、线程安全等方面的责任。

一个变量是在栈还是堆与是值变量还是引用变量无关。比如Java中,局部变量总在栈里,而实例变量总在堆里,与变量类型无关。其次,‘被引用的对象总在堆中’的说法在Java和C#中成立,但在C++中则未必——C++也可在栈上的对象创建引用。由于C++只有引用类型的变量,却没有引用类型的对象,因此更保险的说法是:引用类型对象总在堆中。值对象不是总在栈中,这是一个著名的误解,正确的说法是:值对象可能再栈中。假如它嵌在引用类型对象当中,那么将与后者一道分配在堆中。

值变量与引用变量的区别好比名词与代词。对值变量而言,它对应的是目标数据;对引用变量而言,它对应的不是目标数据本身,而是用来访问目标数据的数据,可以说是一种元数据(关于数据的数据)。举例来说,引用最原始的形式是指针,任何有效的非空指针对应的数据都是一个内存地址——目标数据的地址。

//Java
SomeType a=new SomeType();
//上面的代码实际上是2步原子操作,如下
SomeType a;
a=new SomeType();

  

 SomeType是引用类型,a是引用变量。由于a是局部变量,所以分配在栈上的,但它所指代的对象本身则是分配在堆上。

 

Java和C++是两种极端,Java中无法自定义值类型(Java中的基本类型就是值类型)而C++中无法自定义引用类型(C++中虽不能创建引用类型,却可以创建引用)。C#则是兼收并蓄,可以自定义引用类型和值类型。假如上述代码是C#代码,并且SomeType不是引用类型的class而是值类型的struct的话,那么系统将不在堆中分配内存,而在栈中直接分配一个对象。

//C++如下

SomeType* a=new SomeType();//堆上

SomeType a=SomeType();//栈上  简化SomeType a;

值变量与引用变量的区别不在于它们存放的地点——栈或堆,而在于它们存放的内容——数据或地址,在于它们获取目标数据的方式——直接或间接,在于它们存放目标数据的方式——在线(变量的目标数据在空间上内嵌于包含该变量的对象或环境中)或离线。这个关系好比是现金和银行卡。值重在价值,引用重在使用价值。

值与引用本来是相对的概念,引用也是一种特殊的值——包含被引用对象的地址信息的值。再者,Java与C#中的引用变量在形式上兼具双重身份:以引用的身份被调动方法,以值的身份被赋值或作为参数传递;C++中的指针是一种引用,但在指针的指针即二重指针的面前,它摇身一变成了后者的值。

在调用函数时,需要参数传递,即有一个将实际参数映射到形式参数的过程。最常见有两种机制,一种是按值传递,函数收到的是实际参数的值——按位拷贝(bitwise copy);另一种是按引用传递,函数收到的是实际参数的引用——内存地址。此外还有拷贝-恢复(copy-restore),按名调用(call-by-name),宏展开(macro expansion)等机制。

Java中只有按值传递,没有按引用传递!

在引用变量作为参数按值传递后,方法体获得了对象引用的拷贝,与原引用对应着同一个对象,故仍可以操作该对象。Java方法尽管不能改变引用原件,即不能为其重新赋值,但仍可通过引用复件来改变对象的内容。“Java按值传递对象引用”的说法就合情合理。

//C++

static void change(string& s){s=”new“;}

//C#

static void change(ref string s){s=”new“;}

C++中string为值类型 加上”&“实现值类型的引用传递,而C#中string是引用类型,使用关键字ref表示传进去的是string引用的引用,而不是拷贝。虽然看上去很迷,但是由此可以得出“Java按值传递对象引用”的结论。

当一个对象给一个变量赋值或作为参数按值传递是,C++中复制的是该对象的值,而Java只复制的却是该对象的引用。因此C++有专门的赋值运算符合复制构造函数,而Java则没有。如果Java要达到复制对象值的目的,不能隐式地通过变量赋值或参数传递,只能显式地重新构造对象或者通过克隆(clone)、序列化(serialization)等手段。这就是值语义与引用语义的区别。

 

原文地址:https://www.cnblogs.com/guihuo/p/5631696.html