基础系列(2)—— 常用数据类型

一、数据类型初探

     电脑是由什么来存储所使用的数据? 这个问题用一句话比较笼统的概括,那就是:电脑使用内存来记忆计算时所使用的数据。在现实生活中的数据各种各样,整数、小数、字符串、字符等等,它们的类型是不一样的,所以你要想在计算机中使用这些类型,就必须在内存中为它申请一块合适的空间。

     数据类型总结起来有以下几点:

  (1)C程序是一组函数和数据类型,C++程序是一组函数和类,而C#程序是一组类型声明;

  (2)类型是一种模板:模板本身不是数据结构,但它详细说明了由该模板构造的对象的特征;

  (3)C#提供了16种预定义类型:13种简单类型(数值类型:int,float,double,decimal等;非数值类型:bool,char),3种非简单类型(object,string,dynamic);

       所有的预定义类型都直接映射到底层的.NET类型。C#的类型名称其实就是.NET类型的别名,所以使用.NET的类型名称也符合C#语法,不过并不鼓励这样做。在C#程序中,应当尽量使用C#类型名称而不是.NET类型名称;

       (4)除了上面提到的16种预定义类型外,还可以创建自己的用户定义类型,一共有6种用户定义类型可以由用户自己创建,它们是:类(Class)、结构体(Struct)、数组(Array)、枚举(Enum)、委托(Delegate)和接口(Interface);

 二 数据类型详解

(一)整型

 

(二)浮点类型  

    float数据类型用于较小的浮点数,因为它要求的精度较低。

    double数据类型比float数据类型大,提供的精度也大一倍(15位)。

    如果在代码中没有对某个非整数值(如12.3)硬编码,则编译器一般假定该变量是double

    如果想指定该值为float,可以在其后加上字符F(或f),如:float f = 12.3F;

(三)decimal类型

    decimal类型专门用于进行财务计算,使用decimal类型提供的28位的方式取决于用户。

    要把数字指定为decimal类型,可以在数字的后面加上字符M或(m),如:decimal d=12.30M;

  (四)bool(布尔)类型

 

  (五)char字符类型

 

     char类型的字变量是用单引号括起来的,如'A'。如果把字符把在"A"(双引号)内,编译器会把它看作是字符串,从而产生错误。

 (六)引用类型(Object类型和字符串类型)

 

三 数据类型存储双雄:栈和堆

(一) 栈

         (1)栈是一个内存数组,是一个LIFO(Last In First Out,后进先出)的数据结构。

  (2)栈存储几种类型的数据:某些类型变量的值(主要是值类型);程序当前的执行环境;传递给方法的参数;

  (3)栈具有几种显著的特征:数据只能从栈顶插入和删除;将数据放到栈顶叫做入栈;将数据从栈顶移除叫做出栈

(二)堆

  (1)堆是一块内存区域,在堆里可以分配大块的内存用于存储某类型(主要是引用类型)的数据对象;与栈不同,堆里的内存能够以任意的顺序插入或移除;

       (2)堆中的数据不能显示地删除,CLR中的自动GC(Garbage Collector,垃圾收集器)会自动清除无主(判断程序代码是否将不再访问某数据项的时候)的堆内存对象。因此,我们可以骄傲地说:妈妈再也不用担心我的垃圾了。

 四、值类型和引用类型:屌丝和高富帅

  (一)值类型:只需要一段单独的内存,用于存储实际的数据;TIP:对于值类型,数据存放在栈里;(byte,int,long,float,double,struct,enum等)

 

  (二)引用类型:需要两段内存,第一段存储实际的数据,它总是位于堆中;第二段是一个引用,指向数据在堆中的存放位置;TIP:对于引用类型,实际数据存放在堆里,而引用存放在栈里。(object,string,dynamic,class,interface,delegate,array)

 

  (三)引用类型对象的数据始终存放在堆里,无论它们是值类型还是引用类型。

 

五 、 数据类型转换

数据类型在一定的条件下是可以相互转换的,如将int型数据转换成double型数据。C#允许使用两种转换方式:隐式转换和显式转换。

(一)隐式转换

隐式转换:从类型A到类型B的转换可以在所有情况下进行,执行转换的规则非常简单,可以让编译器执行转换。隐式转换不需要做任何工作,也不需要另外编写代码。如将int型数据转换成double型数据:

int a = 10;double b = a;//隐式转换

隐式转换规则是:任何类型A,只要其取值范围完全包含在类型B的取值范围内,就可以隐式转换为类型B。基于这个转换规则,C#的隐式转换不会导致数据丢失。需要注意的是我们最常用的简单类型bool和string没有隐式转换。

(二)显式转换

显式转换:从类型A到类型B的转换只能在某些情况下进行,转换规则比较复杂,应进行某种类型的额外处理。显式转换又叫强制类型转换,显式转换需要用户明确的指定转换类型。如将double类型数据转换成int类型数据:        

 double c = 10.5;
 int d = (int)c;//显示转换

提醒:

(1)、显式转换可能会导致错误。进行这种转换时编译器将对转换进行溢出检测。如果有溢出说明转换失败,就表明源类型不是一个合法的目标类型。无法进行类型转换。

(2)、强制类型转换会造成数据丢失,如上面的例子中,最终得到的d值为10。

(三)通过方法进行类型转换

(1)、使用ToString()方法。所有类型都继承了Object基类,所以都有ToString()这个方法(转化成字符串的方法)。

(2)、通过int.Parse()方法转换,参数类型只支持string类型。注意:使用该方法转换时string的值不能为为NULL,不然无法通过转换;另外string类型参数也只能是各种整型,不能是浮点型,不然也无法通过转换 (例如int.Parse("2.0")就无法通过转换)。

            int i;
            i = int.Parse("100");

(3)、通过int.TryParse()方法转换,该转换方法与int.Parse()转换方法类似,不同点在于int.Parse()方法无法转换成功的情况该方法能正常执行并返回0。也就是说int.TryParse()方法比int.Parse()方法多了一个异常处理,如果出现异常则返回false,并且将输出参数返回0。          

 int i;
 string s = null;
 int.TryParse(s,out i);

(4)、通过Convert类进行转换,Convert类中提供了很多转换的方法。使用这些方法的前提是能将需要转换的对象转换成相应的类型,如果不能转换则会报格式不对的错误。注意:使用Convert.ToInt32(double value)时,如果 value 为两个整数中间的数字,则返回二者中的偶数;即 4.5 转换为 4,而 5.5 转换为 6。

(5)、实现自己的转换,通过继承接口IConventible或者TypeConventer类,从而实现自己的转换。

六、值传递

  (一)定义:

     值类型在复制的时候,传递就是这个值得本身,例如:A同学的笔记复印了一份,给B同学,B同学任意修改笔记的内容,A同学都不会受到影响。

 

(二)例子:

            int n1 = 10;
            int n2 = n1;
            n2 = 20;
            Console.WriteLine(n1);
            Console.WriteLine(n2);
            Console.ReadKey(); //  输出结果:n1=10;  n2=20;      

分析:变量n1在栈区开辟一块空间,其地址为0x000000e4a99fe094 ,并且为其赋初值10;

          变量n2在栈区也开辟了一块空间,其地址为0x000000e4a99fe090,并n1值赋给了 n2,n2:10

          变量n2的值被重新赋值20,n2:20

(三)内存代码:

            &n1    0x000000e4a99fe094    n1: 0
            &n1    0x000000e4a99fe094    n1: 10
            &n2    0x000000e4a99fe090    n2: 0
            &n2    0x000000e4a99fe090    n2: 10
            &n2    0x000000e4a99fe090    n2: 20

由此可以看出:n1和n2在栈上的内存地址并不一样,所以在改变任意其中的而一个值,另一个并不受其影响

(四) 内存示意图:

 

七 、引用传递

(一)定义:

      引用类型在复制的时候,传递的是对这个对象的引用

(二)例子:         

  Person p1 = new Person();
  p1.Name = "张三";
  Person p2 = p1;
  p2.Name="李四";
  Console.WriteLine(p1.Name);
  Console.WriteLine(p2.Name);//   输出结果:李四 李四

分析:

      变量p1在栈中开辟一块空间,地址为:0x00000095e5efdfd ,并且在堆中也有一块空间,存放的是new Person();这个对象,其内存空间地址为:0x0000000000000000,并且为p1.name赋值张三

     变量p2在栈中也开辟一块空间,地址为:0x00000095e5efdfc8  ,此时,把p1对象赋值给p2对象,即p2也指向new Person()开辟的这块空间,其内存空间地址为:0x0000000000000000,随后,我们把p2.name赋值为 李四,那么由于p1.name也指向这块空间,所以,p1.name=”李四”

(四)内存代码:                        

 &p1  0x00000095e5efdfd   p1: 0x0000000000000000
 &p2  0x00000095e5efdfc8   p2: 0x0000000000000000

(五)内存示意图:

 八 、特殊的引用类型

  例子:      

            string s1 = "张三";
            string s2 = s1;
            s2 = "李四";
            Console.WriteLine(s1)
            Console.WriteLine(s2);

结果:s1=张三 s2=李四  string虽然为引用类型,但是string具有不可变性

九 、方法的参数类型

(一) 两类参数的定义:

实参:具有实际意义的参数,一般在调用方法时,在方法的括号里面传递的参数为实参

形参:在方法声明时,方法名后面括号里面的参数为形参,一般(值传递)形参是接收实参传过来的值

(二) out参数:

1、定义:

       在一个方法需要返回多个参数的时候,一般在参数列表里面声明out类型的参数,以便输出多个返回值。其中,out参数只负责把结果输出,不负责输入参数。一般out声明的变量需要在方法体内部赋值

2、例子:

class Program
    {
        static void Main(string[] args)
        {
            int[] ShuZi = { 1,2,3,4,5,6};
            int max1;
            int min1;
            int sum1;
            JiSuan(ShuZi,out max1,out min1,out sum1);
            Console.WriteLine("数组的最大值为:{0},数组的最小值为:{1},数组的和为:{2}",max1,min1,sum1);
            Console.ReadKey();
        }
        public static void JiSuan(int []number,out int max,out int  min,out int sum)
        {
            max = number[0];
            min = number[0];
            sum = 0;
            for (int i = 0; i < 6; i++)
            {
                sum += number[i];
                if (number[i] > max)
                {
                    max = number[i];
                }
                else
                {
                min=number[i];
                }
            }
        }
    }

输出结果为:最大值:6,最小值:1,和为21 

3 注意

   out指定的参数必须在函数定义的时候就赋初值。否则则出现错误。对比ref指定的参数则可以不在函数内部进行赋初值,在函数调用时候再赋初值也可以。

(二)ref参数

1、定义:

ref 关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。若要使用 ref 参数,则方法定义和调用方法都必须显式使用 ref 关键字。也即是说,在方法中对参数的设置和改变将会直接影响函数调用之处(代码①及②)。无论是函数的定义还是调用时均不可忽略关键字ref.可以对比代码:
代码①:
复制代码
 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Program pg = new Program();
 6             int x = 10;
 7             int y = 20;
 8             pg.GetValue(ref x, ref  y);
 9             Console.WriteLine("x={0},y={1}", x, y);
10 
11             Console.ReadLine();
12           
13         }
14 
15         public void GetValue(ref int x, ref int y)
16         {
17             x = 521;
18             y = 520;
19         }
20     }
复制代码
运行结果为

 

代码②:

复制代码
 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Program pg = new Program();
 6             int x = 10;
 7             int y = 20;
 8             pg.GetValue(ref x, ref  y);
 9             Console.WriteLine("x={0},y={1}", x, y);
10 
11             Console.ReadLine();
12           
13         }
14 
15         public void GetValue(ref int x, ref int y)
16         {
17             x = 1000;
18             y = 1;
19         }
20     }
复制代码

由代码① 和②的运行结果可以看出,在方法中对参数所做的任何更改都将反映在该变量中,而在main函数中对参数的赋值却没有起到作用,这是不是说明不需要进行初始化呢?来看第二点

2 注意
ref定义的参数在使用前必须初始化,无论在函数定义时候参数有没有赋予初值。这条正好区分out指定的参数可以不在调用函数的时候进行初始化。
来看代码③ 以及其运行结果:
复制代码
 1  class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Program pg = new Program();
 6             int x;
 7             int y;        //此处x,y没有进行初始化,则编译不通过。
 8             pg.GetValue(ref x, ref  y);
 9             Console.WriteLine("x={0},y={1}", x, y);
10 
11             Console.ReadLine();
12           
13         }
14 
15         public void GetValue(ref int x, ref int y)
16         {
17             x = 1000;
18             y = 1;
19         }    
20     }
复制代码

出现的错误为:使用了未赋值的局部变量“x”,“y”。故可以说明ref指定的参数无论在函数定义的时候有没有赋予初值,在使用的时候必须初始化。

(四)总结

ref和out使用时的区别是:
①:ref指定的参数在函数调用时候必须初始化,不能为空的引用。而out指定的参数在函数调用时候可以不初始化;
②:out指定的参数在进入函数时会清空自己,必须在函数内部赋初值。而ref指定的参数不需要。

参考资料:《c#图解教程》

posted @ 2017-09-03 16:09  StrugglingDave  阅读(4294)  评论(0编辑  收藏  举报