堆栈和静态存储区

堆栈和静态存储区,值类型和引用类型,继承和子类的构造,虚方法和隐藏方法

堆栈和静态存储区

程序内存区域:堆 栈 静态存储区

栈的特征:

数据只能从栈的顶端插⼊和删除

把数据放⼊栈顶称为⼊栈(push)

从栈顶删除数据称为出栈(pop)

堆是⼀块内存区域,与栈不同,堆⾥的内存能够以任意顺序存⼊和移除

GC Garbage Collector垃圾回收器

CLR的GC就是内存管理机制,我们写程序不需要关⼼内存的使⽤,因为这些都是CLR帮

我们做了

值类型和引用类型

类型被分为两种:值类型(整数,bool struct char ⼩数)和引⽤类型(string 数组 ⾃定义的

类,内置的类)

目前学的除了字符串变量都是值类型,包括bool(本质是0和1)

值类型只需要⼀段单独的内存,⽤于存储实际的数据,(单独定义的时候放在栈中)

引⽤类型需要两段内存

第⼀段存储实际的数据,它总是位于堆中

第⼆段是⼀个引⽤,指向数据在堆中的存放位置

画图理解值类型和引用类型在内存中的存储

构造函数的自动生成方法
public string name;
        public string address;
        public int age;
        public string createTime;

        public Customer(string name, string address, int age, string createTime)
        {
            this.name = name;
            this.address = address;
            this.age = age;
            this.createTime = createTime;
        }


        //给这个类创建构造函数对这些数据进行初始化
        //Unity功能:右键,快速操作和重构
        //然后选择生成构造函数
        //选择需要生成构造函数的成员,就能自动生成构造函数

创建各种各样的类型用以说明数据的存储

 int a = 123;
            float b = 34.5f;
            bool c = true;
            //以上三个值类型会直接存储到栈里面


            string name = "SiKi";
            //对于引用类型 字符串本身是一个 字符串常量
            //但是name是一个变量
            //现在栈里存储一个16进制的引用 name变量
            //常量的"SiKi"会存储到 静态存储区

            int[] array1 = new int[] { 56, 22, 31, 21, 2, 2222, 38899, 8 };
            //数组里的数据是存储在 堆 里
            //栈里面存储着一段引用 array1
            //注意在访问数据的时候每个变量名都会转换成内存地址

            string[] array2 = new string[] { "wang", "ze", "cheng" };
            //字符串数组的数据也是存放在 堆 里
            //地址保存在 栈 里面


            //创建Customer的类
            Customer c1 = new Customer("李四", "火星",90, "2001/8/696");
            //类里的数据也是保存在 堆 里
            //栈 里面保存着引用

            //注意 本质上字符串数组类型的值,也是 常量 因此实际上是保存在  静态存储区  里面的
            //在堆里保存的数据其实也是保存了  引用/地址

字符串在内存中的存储

形态字符串常量的储存

string s1 = "张三";
            string s2 = "张三";
            //因为这两个常量本质是一样的,所以只会在静态存储区存储  一个  常量“张三”
            //但是会存储两个地址,指向同一个"张三
            //可以在Debug/断点模式下看到两个张三常量的 地址 不一样
            //这样节约内存

关于常量不可修改

string s1 = "张三";
s1 = "李四";

说明:看似变量修改了,但是我们知道静态存储区是不可修改的,所以实际上发生修改的是 栈中的引用地址 而在静态存储区则分别留下了“张三”“李四”两个常量

对象引用的改变

两个不同的对象不会互相影响,对c1做操作的时候不会影响c2的值

  Customer c1 = new Customer("李四", "火星", 90, "2001/8/696");
            Customer c2 = new Customer("张三", "地狱", 9, "2001/9/0");

            c1.Show();
            c1.name = "zhangsan";
            c1.Show();

            c2.Show();

但是这种情况,值修改c2却会改变c1的数据

 Customer c1 = new Customer("李四", "火星", 90, "2001/8/696");
            Customer c2 = c1;

            c1.Show();
            c2.name = "zhangsan";
            c1.Show();

说明:通过new方法生成对象,会在堆里生成专门存放数据,栈里持有的引用就会指向这个 堆 里的对象

但是,直接吧c1赋值给c2的话,并没有直接在堆里生成新的数据,而是直接在栈里把c1的引用地址复制给了c2

c1,c2里面储存的地址都是一样的,他们同时指向同一块 堆 中的数据

因此无论是通过谁去修改数据,显示出来的都会改变

就像一个箱子的两把钥匙

GC/垃圾回收的使用

null 空引用 空对象

 c1 = null;//相当于清空了c1在 栈 中的引用地址
            //因此,在 堆 中的数据计数器就会从原来的 2 变成 1 
            c2 = null;//计数器 0
            //当 堆 中的数据计数器为0的时候,GC就会把不被使用的数据回收掉

什么是继承

继承是什么?

在上⼀节课中学习了如何定义类,⽤类当做模板来声明我们的数据。

很多类中有相似的数据,⽐如在⼀个游戏中,有Boss类,⼩怪类Enemy,这些类他们有

很多相同的属性,也有不同的,这个时候我们可以使⽤继承来让这两个类继承⾃同⼀个

类。

继承的类型

实现继承:

表⽰⼀个类型派⽣于⼀个基类型,它拥有该基类型的所有成员字段和函数。 在实现继

承中,派⽣类型采⽤基类型的每个函数的实现代码,除⾮在派⽣类型的定义中指定重写

某个函数的实现代码。 在需要给现有的类型添加功能,或许多相关的类型共享⼀组重

要的公共功能时,这种类型的继承⾮常有⽤。

接⼝继承:

表⽰⼀个类型只继承了函数的签名,没有继承任何实现代码。 在需要指定该类型具有

某些可⽤的特性时,最好使⽤这种类型的继承

多重继承

⼀些语⾔(C++)⽀持所谓的 “多重继承”,即⼀个类派⽣⾃多个类。 使⽤多重继承的优点是

有争议的:⼀⽅⾯,毫⽆疑问,可 以使⽤多重继承编写⾮常复杂、 但很紧凑的代码,。另⼀⽅

⾯,使⽤多重实现继承的代码常常很难理解和调试。 如前所述,简化健壮代码的编写⼯作是

开发 C#的重要设计 ⽬标。 因此,C#不⽀持多重实现继承。 ⽽ C#允许类型派⽣⾃多个接Unity 1143 C#编程-第⼆季-⾯向对象

16

⼝— — 多重接⼝继承。 这说明,C#类可以派⽣⾃另⼀个类和任意多个接⼝。更准确地说,

System.Object 是⼀个公共的基类,所 以每个 C#(除了Object类之外)都有⼀个基类,还可以

有任意多个基接 ⼝。

注意:如果不特意指定的话,所有的类都会继承自object这个基类

继承的代码演示

用两个类展示继承的语法特点

先创建一个父类BaseClass

 class BaseClass
    {
        private int data1;
        private string data2;

        public void Function()
        {
            Console.WriteLine("BaseClass:Function1");
        }
        public void Function2()
        {
            Console.WriteLine("BaseClass:Function2");
        }
    }

由父类派生出两个子类/派生类

怎么让这个新的类,继承刚刚的类呢?直接 :BaseClass

namespace 继承_的代码演示
{
    class DrivedClass1:BaseClass//冒号和后面的父类名字就是继承的操作
    {
    }
}

说明:现在子类拥有父类拥有的功能和数据

直接使用父类运行功能的演示

static void Main(string[] args)
        {
            BaseClass bc = new BaseClass();
            bc.Function();
            bc.Function2();
        }

派生类/子类的使用演示

DrivedClass1 dc1 = new DrivedClass1();
            dc1.Function();
            dc1.Function2();

说明:虽然子类里面什么都没有,但是他从父类里能拿到父类的代码

子类是否拿到数据成员的演示

先通过父类设置数据

 bc.data1 = 12;
            bc.data2 = "45641568635125wsafdz";//通过父类去设置对象的数据

再让子类拿到父类

 dc1.data1 = 100;
            dc1.data2 = "sda";
            Console.WriteLine(dc1.data1);

总结:继承的话,子类能同时拿到父类里面的数据成员和继承对象

在做第二个子类做演示

 DrivedClass2 dc2 = new DrivedClass2();
            dc2.Function();

说明:一个父类可以拥有多个子类

注意:子类是可以有自己的数据和函数的

定义在子类1中的数据和函数

class DrivedClass1:BaseClass//冒号和后面的父类名字就是继承的操作
    {

        public int data3;

        public void FunctionDrivedClass1()
        {
            Console.WriteLine("我是子类1功能");
        }
    }

继承和子类的构造

案例1

基类敌⼈类( hp speed ⽅法 ai move )

派⽣出来两个类

boss类

type1enemy类

type2enemy类

先创建所有敌人的基类,在这里声明敌人共同的成员和方法,这个敌人类就是所有敌人的父类

class Enemy
    {
        private int hp;
        private int speed;

        public void AI()
        {
            Console.WriteLine("我是AI方法");
        }
        public void Move()
        {
            Console.WriteLine("我是移动方法");
        }
    }

然后以此为基础创建三个子类,Boss,type1,type2

然后为每个子类增加自己的特点

Boss

 class Boss : Enemy
    {
        //假设这两个是只有boss才有的数据和成员
        private int attack;

        public void Skill()
        {
            Console.WriteLine("Boss技能");
        }

    }

一个新的访问权限 protected 介于private和public之间,也是私有的,但是子类可以访问

 protected int hp;//public private    protected
        protected int speed;

通过在子类里访问父类成员和方法打印出boss自己的数据

  public void Print()
        {
            Console.WriteLine("血量:" + " " + hp);
            Console.WriteLine("攻击力:" + " " + attack);
            Console.WriteLine("速度:" + " " + speed);
        }

需要构造方法给成员赋值

public Boss(int attack,int hp,int speed)//多传一些参数过来
        {
            this.attack = attack;
            this.hp = hp;
            this.speed = speed;
        }

使用boss类

 Boss boss1 = new Boss(100,100,100);//括号里设置的是构造函数里对应的参数
            boss1.Print();

this和base关键字

说明:this可以访问到父类里面的数据,但是base是专门访问父类数据的

演示:

public Boss(int attack,int hp,int speed)//多传一些参数过来
        {
            this.attack = attack;
            //this.hp = hp;
            //this.speed = speed;
            base.hp = hp;
            base.speed = speed;
        }

base 只能访问父类里的成员

假如说子类和父类里有重名的数据,也能通过base和this做出区分

private int hp;//和父类成员重名

        public Boss(int attack,int hp,int speed)//多传一些参数过来
        {
            this.attack = attack;
            //this.hp = hp;
            //this.speed = speed;
            base.hp = hp;//访问父类hp
            this.hp = hp;//访问子类hp
            base.speed = speed;
        }
         public void Print()
        {
            Console.WriteLine("父类血量:" + " " + base.hp);
            Console.WriteLine("子类血量:" + " " + hp);
            Console.WriteLine("攻击力:" + " " + attack);
            Console.WriteLine("速度:" + " " + speed);
        }

说明:一般情况下,不会定义重名的成员

关于虚方法Virtual

重现父类函数有两种方法:虚方法和隐藏方法

演示:

为了演示,先像之前一样创建敌人的父类和boss的子类

父类

 class Enemy
    {
        public void Move()
        {
            Console.WriteLine("我是父类行走");
        }
    }

子类中虚方法的演示

先把父类中的移动方法声明为Virtual

public virtual void Move()
        {
            Console.WriteLine("我是父类行走");
        }

在子类中庸override重写

 //在boss里用override方法进行重写
        public override void Move()
        {
            Console.WriteLine("Boss特有移动方法");
        }

说明:如此,通过Boss子类声明的对象,其调用移动方法时,就会调用boss的方法而不是父类的方法

 Boss b = new Boss();
            b.Move();//输出 Boss特有移动方法

关于继承中的引用赋值问题

先添加新子类Type1Enemy

利用Enemy声明一个对象,但是不去构造

Enemy enemy;

发现可以用子类为其赋值

enemy = new Boss();//可以用任意子类赋值
            enemy = new Type1Enemy();

说明:用那个类进行构造,那他的核心就是什么

相当于用子类的构造对象赋值给了父类的声明对象

本质上还是子类对象,但是这时候子类一些特有的东西没法直接调用

子类的特有功能:

 public void BossSkill()
        {
            Console.WriteLine("我是Boss特有技能");
        }
enemy = new Boss();
enemy = new Boss();
            enemy.Move();//因为是重写所以可以引用
            enemy.BossSkill();//引用不了特有内容 报错

父类可以用子类构造,但是子类不能用父类构造

总结:虚函数更方便被调用,当你使用父类声明对象,但是用子类构造,就能使用虚函数重写的方法

隐藏方法

用隐藏方法的话,父类的方法不需要加virtual,父类里的方法该怎么写怎么写

 public void AI()
        {
            Console.WriteLine("我是父类AI");
        }

在子类里用隐藏方法重写AI

 public new void AI()//隐藏方法 new 关键字
        {
            Console.WriteLine("我是Boss特有AI");
        }

调用演示

            Boss b = new Boss();
            b.AI();//输出 我是Boss特有AI

用父类声明的对象展示隐藏方法和虚方法之间的区别

//声明一个父类演示区别
            Enemy e = new Boss();
            e.AI();//输出 我是父类AI
            //如果是使用虚方法的话,这里输出的应该是重写过的 我是Boss特有AI

区别:隐藏方法只有声明的对象是子类的时候,才会调用重写的方法

虚方法即使声明的对象是父类,但是是子类构造的,也会调用虚方法

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值