文末有彩蛋~
最近学习 Unity3D,把学习过程中遇到的知识点记录下来。
C# 静态,安全语言
C++ 静态,不安全语言
C#语言是类型安全的,其本质是有关类型操作的一种规范,即不能将一种类型当作另一种类型,除非它们真的存在转换关系。
C/C++语言允许做一些非常规的事情,所以与 C#相比其功能更加强大,不过同时在使用不恰当时也带来了很多隐患。
例如,C/C++有可能会通过一些不合理的途径,将一种类型的值当作另一种完全不同类型的值。这种不合理的途径是由于 C/C++中有的代码会以错误的方式检查值中的原始字节并解释它们。
#include < stdio.h >
int main(int argc, char** argv)
{
char *word = argv[1];
int *word_int = (int *)word;
printf (“%d”, *word_int);
}
通过终端编译并运行上述代码,并将 hello 作为参数传入,
输出的结果是 1819043176 。
这是由于编译器将 int 当作 32 位的值,而 char 则是 8 位的值,同时文本使用 UTF-8 或者 ASCII 来表示。在本例中,C 代码将 char 指针当作了一个 int 指针,因此只取其前 4 个字节( 32 位),并且将它作为一个数字处理。
C#要求其所有类型全部从 System.Object 类派生。 无论是开发者自己定义的类型,还是 C#所提供的类型。因此,下面的两种定义类型的方式,其含义是完全相同的,代码如下。
//隐式派生自 System.Object
class Person{
// …
}
//显式派生自 System.Object
class Person : System.Object{
// …
}
System.Object
定义了 4 个公共方法和 2 个受保护的方法(包括但不限于):
自动推导变量:
C++: auto
C#: var
值类型和引用类型
C#中大部分类型都是引用类型,但是在实际开发中,可以发现程序员使用最多的还是值类型。 引用类型总是从托管堆分配,C#要求所有的对象都使用 new 操作符创建。
C#中操作符 new:
计算所需内存空间,new 操作符会计算目标类型和包括 System.Object 类在内的,其所有基类中定义的所有实例字段所需要的字节数。
除此之外,为了方便 Mono 运行时管理对象,还有一些额外的信息需要托管堆为其分配空间,如类型对象指针和同步索引块。
完成计算对象所需的空间后,就要为对象在托管堆上分配所需要的内存空间了。分配的所有字节都设为 0 。
内存空间分配完,接下来需要初始化(在( 1 )中所提过的)对象的“类型对象指针”以及“同步块索引”。
当前 3 个准备步骤全部完成后,最后就要调用类型的实例构造器了。
构造器中自动调用当前类型的基类构造器。每个类型的构造器都负责初始化该类型定义的实例字段。最终一定会调用 System.Object 的构造器,而该构造器仅仅是返回,而没有什么逻辑操作。
最后会返回指向新建对象的一个引用。也就是说新建的变量是一个指向某类型对象的引用,而非对象本身。
由于并没有一个和 new 操作符相对应的 delete 操作符存在,因而对象的回收工作主要由垃圾回收机制,也就是 GC 来处理。
那么在 C#语言中,到底哪些类型可以归为是引用类型,又有哪些类型应该被归为值类型呢?
根据 ECMA 的 C#语言规范( ECMA-334 规范)或者微软官方的 C#语言规范,任何被称为“类”的类型都是引用类型。
并且通常使用以下 3 个关键字来声明一个自己定义的引用类型。
当然,在 C#中也有一些内建的引用类型。
值类型包括:
特别注意的是 C#的结构体 struct
是属于值类型,而 C++中的结构体可以认为就是类。
值类型实例有两种表示方式,分别是未装箱和已装箱。而装箱机制是指将值类型转换成引用类型。 正是由于值类型不在托管堆中分配,不被垃圾回收,同时也无须通过指针来引用,因而值类型与引用类型存在很多区别。但是有很多情况需要获取和操作对值类型实例的引用。因而装箱机制为了应对这种情况,便应运而生。
值类型派生自 System.ValueType (即 struct 隐含的基类型是 System.ValueType ),而 System.ValueType 同样从 System.Object 派生而来,因此 System.ValueType 提供了和 System.Object 相同的方法,不过值得注意的是 System.ValueType 重写了 Equals 方法和 GetHashCode 方法。
如果符合以下条件,作为开发者可以选择使用值类型而不是引用类型:
装箱和拆箱
有时我们需要使用一个引用,而不是一个值类型的值,就需要装箱,装箱的步骤如下:
( 1 )在托管堆中分配内存。 需要注意的是,由于是将值类型进行引用类型化,因而分配的内存空间除了值类型各个字段所需的内存之外,还要加上托管堆所有对象都有的两个额外成员(类型对象指针和同步索引块)所需的内存。
( 2 )将值类型的字段复制到新分配的堆内存中。
( 3 )返回对象地址,即对象的引用。 值类型成了引用类型。
由引用类型转为指类型,则经过获取引用并复制:
( 1 )获取已经装箱的对象中各个字段的地址,这个过程便是所说的拆箱。
( 2 )将已经装箱的对象中各个字段的值从托管堆上复制到线程栈的新的值类型实例中。
一个在使用 C#语言进行开发的过程中经常遇到的一个误区,就是将拆箱当作了装箱的逆过程。 其实并不是这样的,相比于装箱,拆箱的代价要小得多。 拆箱其实就是获取引用的过程,获取的这个引用指向了一个分配在托管堆上的对象中的值。需要注意的是,拆箱并不涉及复制的过程,所以将值从托管堆上的对象中复制到值类型实例中,是拆箱之后紧跟的一步复制过程,而非拆箱本身。
在拆箱时,一定要注意只能转型为最初未装箱的值类型。而拆箱需要显式的指定要转型的目标类型这一点,也与装箱不同。许多编译器都是隐式生成代码来装箱对象的,因此作为开发者有时会因为不注意而忽略这一点,所以如果是一个关心程序性能的开发者,就一定要清楚自己的代码究竟是否会造成装箱操作。
以上就是最近学习 Unity3D 的学习记录啦! 最后附上一张 Unity3D, Mono, C#的关系。