2.1 数据类型
C#是一种强类型语言,在程序中用到的变量、表达式和数值等都必须有类型,编译器将检查所有数据类型操作的合法性。这个特点保证了变量中存储的数据的安全性。C#的数据类型分成两大类:一类是值类型(Value Types),另一类是引用类型(Reference Types)。每一大类又可再分成几个小类,如图2.1所示。
图2.1 C#数据类型
2.1.1 值类型
所谓值类型就是一个包含实际数据的变量。当定义一个值类型变量时,C#会根据所声明的类型,以堆栈方式分配一块大小相适应的存储区域给这个变量,对这个变量的读/写操作就直接在这块存储区域进行。
例如:
int iNum=10; // 分配一个32位内存区域给变量iNum,并将10放入该内存区域 iNum=iNum+10; // 从变量iNum中取出值,加上10,再将计算结果赋给iNum
C#中的值类型包括:简单类型、枚举类型和结构类型。
简单类型是系统预置的,一共有13个,如表2.1所示。
表2.1中“C#关键字”是指在C#中声明变量时可使用的类型说明符。
例如:
int myNum // 声明myNum为32位的整数类型
.NET 平台包含所有简单类型,它们位于.NET 框架的 System名字空间。C#的类型关键字就是.NET中所定义类型的别名。从表2.1可见,C#的简单数据类型可分为整数类型(包括字符类型)、实数类型和布尔类型。
表2.1 C#简单类型
● 整数类型
该类型共有 9 种,它们的区别在于所占存储空间的大小,有无符号位及所能表示的数的范围,这些是程序设计时定义数据类型的重要参数。char类型归属于整数类别,但它与整数类型有所不同,不支持从其他类型到 char 类型的隐式转换。即使 sbyte、byte、ushort 这些类型的值在char表示的范围之内,也不存在其隐式转换。
● 实数类型
该类型有3种,其中浮点类型float、double关键字采用IEEE 754格式来表示,因此浮点运算一般不会产生异常。decimal 类型主要用于财务和货币计算,它可以精确地表示十进制小数(如0.001)。虽然它具有较高的精度,但取值范围较小,因此从浮点类型到 decimal 的转换可能会产生内存溢出异常;而从 decimal 到浮点类型的转换则可能导致精度的损失,所以浮点类型与decimal之间也不存在隐式转换。
● 布尔类型
该类型表示布尔逻辑量,它与其他类型之间不存在标准转换,即不能用一个整型数表示true或false,反之亦然,这点与C/C++不同。
2.1.2 引用类型
引用类型包括 class(类)、interface(接口)、数组、delegate(委托)以及object和string。其中object和string是两个比较特殊的类型。object是 C#中所有类型(包括所有的值类型和引用类型)的根类。string是一个从 object 类直接继承的密封类型(不能再被继承),其实例表示Unicode字符串。
一个引用类型的变量不存储它们所代表的实际数据,而是存储实际数据的引用。引用类型分3步创建:首先在栈内存上创建一个引用变量,然后在堆内存上创建对象本身,最后把这个对象所在内存的句柄(首地址)赋给引用变量。
例如:
string s1, s2; s1="ABCD"; s2 = s1;
其中,s1、s2 都是指向字符串“ABCD”的引用变量,s1 的值是“ABCD”存放在内存中的地址(即引用),两个引用型变量之间的赋值,使得 s2、s1 都成为对“ABCD”的引用,如图2.2所示。
图2.2 引用类型赋值示意图
引用类型的值是对引用类型实例的引用,特殊值 null 适用于所有引用类型,它表明没有任何引用的对象。当然也可能存在若干引用变量同时引用同一个对象的实例,对任何一个变量的修改都会导致该对象值的修改。
注意:
栈(stack)是按先进后出(FILO)的原则存储数据项的一种数据结构;堆(heap)则是用于动态内存分配的一块区域,可以按任意顺序和大小进行分配和释放。C#中,值类型就分配在栈中,栈内存保存着值类型的值,可以通过变量名来存取。引用类型分配在堆中,对象分配在堆中时,返回的是地址,而这个地址被赋值给引用。
2.1.3 两者关系
可以把值类型与引用类型的值赋给 object 类型变量,C#用“装箱”和“拆箱”来实现两者之间的转换。
1.装箱
所谓“装箱”就是将值类型包装成引用类型的过程。当一个值类型被要求转换成一个object 对象时,“装箱”操作自动进行:首先创建一个对象实例,然后把值类型的值复制到其中,最后由 object引用这个对象实例。
例如:
int x = 123; object obj1=x; // 装箱操作 x=x+100; // 改变x的值时,obj1的值并不会随之改变 Console.WriteLine("x={0}",x); // x=223 Console.WriteLine("obj1={0}",obj1); // obj1=123
上段代码的操作机制,如图2.3所示。
图2.3 装箱操作机制
2.拆箱
“拆箱”操作与“装箱”相反,是将一个 object 转换成值类型:首先检查由 object 引用的对象实例值类型的包装值,然后把实例中的值复制到值类型变量中。
例如:
int x = 123, y; object obj1=x; // 装箱操作 y=(int)obj1; // 拆箱操作,必须进行强制类型转换 Console.WriteLine("y={0}",y); // y=123
注意:
当一个装箱操作把值类型转换为一个引用类型时,不需要强制类型转换;而拆箱操作把引用类型转换到值类型时,则必须显式地强制类型转换。
【例2.1】 编写程序,以探索C#两大类数据类型的性质及其相互转换机制。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Ex2_1 { class Program { static void Main(string[] args) { double d1 = 3.14; double d2 = d1; Console.WriteLine("d1与d2内存地址是否相同:" + ((object)d1 == (object)d2)); object o1=d1; // 装箱操作 object o2 = o1; Console.WriteLine("o1与o2是否指向同一个内存地址:" + ((object)o1 == (object)o2)); d1 = 3.1416; Console.WriteLine((double)o1); //d1改变不影响o1的值,说明o1不指向d1的内存地址 string s1 = "Visual C#"; string s2 = s1; Console.WriteLine("s1与s2是否指向同一个内存地址:" + ((object)s1 == (object)s2)); s1="C#"; //修改字符串,创建了新的s1实例,在内存中存放的位置与原来不同 Console.WriteLine("改变s1后,s1与s2是否指向同一个地址:"+ ((object)s1 == (object)s2)); s2="C#"; //修改字符串,在内存中创建新的内存位置,与s1内存位置不同 Console.WriteLine("改变s2使之与s1的值相同后,它们地址是否一样呢:"+ ((object)s1 == (object)s2)); Console.WriteLine("s1与s2是否相等呢:"+(s1 == s2)); } } }
运行程序,结果如图2.4所示。
图2.4 【例2.1】程序运行结果
注意:
(1)代码“(object)d1”是把double类型的d1强制转换为object类型,以获得d1的内存地址。
(2)string 也是引用类型,当一个 string 类型变量的值被修改时,实际上是创建了另外一个内存,并由该变量指向新的内存。这也是由字符串长度不确定,必须重新分配内存的特点决定的。