堆栈和静态存储区,值类型和引用类型,继承和子类的构造,虚方法和隐藏方法
堆栈和静态存储区
程序内存区域:堆 栈 静态存储区
栈的特征:
数据只能从栈的顶端插⼊和删除
把数据放⼊栈顶称为⼊栈(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
区别:隐藏方法只有声明的对象是子类的时候,才会调用重写的方法
虚方法即使声明的对象是父类,但是是子类构造的,也会调用虚方法