C#实用教程(第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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 类型变量的值被修改时,实际上是创建了另外一个内存,并由该变量指向新的内存。这也是由字符串长度不确定,必须重新分配内存的特点决定的。