C#基础篇
内存分区
1、栈区:
由编译器自动分配释放,存放值类型的对象本身,引用类型的引用地址(指针),静态区对象的引用地址(指针),常量区对象的引用地址(指针)等。其操作方式类似于数据结构中的栈。
2、堆区(托管堆):
用于存放引用类型对象本身,在c#中由.net平台的垃圾回收机制(GC)管理。栈,堆都属于动态存储区,可以实现动态分配。
3、静态区及常量区:
用于存放静态类,静态成员(静态变量,静态方法),常量的对象本身。由于存在栈内的引用地址都在程序运行开始最先入栈,因此静态区和常量区内的对象的生命周期会持续到程序运行结束时,届时静态区内和常量区内对象才会被释放和回收(编译器自动释放)。应尽量限制使用静态类,静态成员(静态变量,静态方法),常量,否则程序负荷高。
4、代码区:
存放程序编译之后生成的二进制代码,例如我们写的函数,就是存储在这里的。就是函数在程序编译之后,存储于代码区。调用函数的时候,会压到栈区执行其中的代码。无法寻址。
C#知识点
值类型和引用类型
区别:
1、值类型和引用类型存储在的内存区域是不同的,存储方式是不同的。
2、值类型用于储存数据的值,存储在栈空间 — 系统分配,自动回收,小而快。
3、引用类型用于储存对数据的引用,引用地址在栈,数据存储在堆空间 — 手动申请和释放,大而慢。
赋值:
开辟一个空间在栈中,是值拷贝值内容,是地址拷贝地址内容,new则新地址。
String
解释:
string对象称为不可变的(只读),因为一旦创建了该对象,就不能修改该对象的值,因为底层是被final的byte数组。有的时候看来似乎修改了,实际是string经过了特殊处理,每次改变值时都会建立一个新的string对象,变量会指向这个新的对象,而原来的还是指向原来的对象,所以不会改变。
缺点:
频繁的改变string 重新赋值会产生内存垃圾,影响性能。
方法:使用StringBuilder
解释:
String Builder 类是一个字符串缓冲区,可以提高字符串效率,底层是个数组但没有被final修饰,可以改变长度。stringBuilder提供 Append方法,能在已有对象的原地进行字符串的修改,就不会产生大量内存垃圾,对大量字符串进行添加操作,stringbuilder耗费的时间比string少的多。
扩容的逻辑:
分配一个容量翻倍的新数组,然后将原内容拷贝到这个新数组中。无法直接重新赋值,要先清空。
out与ref
public void person(ref/out int a)
都是传递引用(内存地址),相当于C++的&。
处理问题:
当传入的值类型参数在内部修改时或者引用类型参数在内部重新申明时,外部的值会发生变化。(不加以上处理,引用类型重新声明,是形参重新开辟一个空间,实参不变)
out与ref的区别(地址早绑定和晚绑定区别)
1、ref是有进有出,参数原来数值可以传入,不会清空,地址早绑定,要初始化。
2、out是只出不进(初始化也没用,调用就清空),地址晚绑定,不一定用初始化,不过调用后绑定地址,则必须赋值(即函数内赋值)。
3、ref可以把参数的值传入函数,但无法通过out把一个数值传入方法中。
params变长参数
public void person(int a,params int[] b)
1、params后面关键字为数组,类型可以为任意类型。
2、函数参数可以有别的参数和params关键字一起用。
3、函数参数最多只可以出现一个params关键字,并且一定是在最后一组参数,前面可以有n个参数。
结构体与类
结构体 struct 是变量和函数的集合 用来表示特定的数据集合
1、结构体具备封装特性,但不具备继承和多态特性
2、结构体中不允许初始化字段,类可以。
3、结构体的构造函数必须传入参数,且要所有字段都赋值。类可以选择性赋值。
4、结构体存在默认的无参构造函数,并且新写的一个构造函数是,默认的无参构造函数还在。类则被覆盖了。
5、结构体是值类型,类是引用类型。储存空间不同,前者在栈上,后者在堆上。
使用和性能上:
1、如果只是单纯地存储数据的话,推荐使用结构体;如果需要用到面向对象的思想,推荐使用类。
2、如果可以尽量使用值类型,可以减少CG。但同时复制赋值操作,如果数据量特别大,struct对象还需要频繁赋值和传递参数,代价会非常高,因为struct对象在复制赋值时会将栈上所有的数据进行赋值,而Class对象则复制一份内存地址,相对代价小一点。
垃圾回收机制
垃圾回收,英文简写GC (Garbage Collector)
垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是垃圾,所谓的垃圾就是没有被任何变量,对象引用的内容,而垃圾就需要被回收释放。
垃圾回收有很多种算法:
引用计数(Reference Counting)、标记清除(Mark Sweep)、标记整理(Mark Compact)、复制集合(Copy Collection)。
注意:
1、GC只负责堆(Heap)内存的垃圾回收。
引用类型都是存在堆(Heap)中的,所以它的分配和释放都通过垃圾回收机制来管理。
2、栈(Stack)上的内存是由系统自动管理的。
值类型在栈(Stack)中分配内存的,他们有自己的生命周期,不用对他们进行管理,会自动分配和释放。
C#中內存回收机制的大概原理
0代内存 1代内存 2代内存
内存是一代比一代大,但是速度是越来越慢。2代内存可以扩容
代的概念:
代是垃圾回收机制使用的一种算法(分代算法)。
新分配的对象都会被配置在第0代内存中,每次分配都可能会进行垃圾回收以释放内存(比如0代内存满时)
在一次内存回收过程开始时,会进行以下两步:
1、从根对象(如全局变量、静态变量等)开始,垃圾回收器遍历对象图,标记所有可达的对象。标记过程通常使用的是深度优先搜索算法,然后在堆上找到剩余没有标记的对象进行删除释放内存。
2、搬迁对象,压缩堆(挂起执行托管代码线程) 搬迁可达对象,0代到1代,1代到2代,修改引用地址。
手动回收:CG.Collect();(游戏开发中通常在过场景的时候使用,防止卡顿不舒服体验)
Unity中的GC
贝姆垃圾回收(Boehm conservative collector):
无分代\并行,执行时所有线程阻塞;每次标记都会访问所有可达的对象(穷举搜索垃圾)。这种方式极有可能在短时间造成帧率下降,影响玩家体验。
存在问题:
如果存在一个值和引用地址在栈中是同一个值,此时即使引用类型已经被标记为null,但由于相同的那个值类型还在使用,贝姆垃圾回收也无法释放地址引用之前指向的那块堆内存。因为保守垃圾收集器并不会精确区分值对象和引用对象。
索引器
private int[,] arr;
public int this(int a,int b){
get{return arr[a,b];
}set{
arr[a,b]=value;}}
概念:
索引器允许类或结构的实例就像数组一样进行索引。无需显式指定类型或实例成员,即可通过运算符[]设置或检索索引值。索引器类似于属性,不同的是索引器的访问器需要使用参数。
定义:
在 C# 中可以使用 this 关键字作为属性名声明索引器,并在方括号内声明参数。索引器也可以使用 C# 中任何有效的访问修饰符,在索引器中为 get 和 set 访问器前指定不同的访问修饰符即可。和属性的定义一样,索引器内也需要定义 get 和 set 访问器,其中 get 访问器返回值。 set 访问器分配值。
静态
static public float pi=3.14;
静态成员
1、程序开始运行时就会分配内存空间,创建在“静态存储区”里面。我们能直接使用。
2、静态成员和程序同生共死,只要使用了它,直到程序结束时内存空间才会被释放。
3、一个静态成员会有自己唯一的一个“内存小房间”这让静态成员就有了唯一性。
4、在任何地方使用都是用的小房间里的内容,改变了它也是改变小房间里的内容。
静态成员和实例成员的区别:
1、生命周期不一样。
2、在内存中存储的位置不一样。(静态区/栈堆)
静态类
1、被static关键字修饰的类。
2、静态类里面只能声明静态成员。
3、静态类的本质,是一个抽象的密封类,所以不能被继承,也不能被实例化。
4、如果一个类下面的所有成员,都需要被共享,那么可以把这个类定义为静态类。
静态构造函数
1、这个类的成员,第一次被访问之前,就会执行静态构造函数。
2、静态构造函数只被执行一次。
静态与常量区别:
1、const必须初始化,不能修改 static没有这个规则。
2、const只能修饰变量、static可以修饰很多。
拓展方法
访问修饰符 static 返回值 函数名(this 拓展类名 参数名,参数类型 参数名)
public static void speakValue(this int value){}
概念:
为现有非静态、变量类型添加新方法
作用:
1、提升程序拓展性。
2、不需要再对象中重新写方法。
3、不需要继承来添加方法。
4、为别人封装的类型写额外的方法。
特点:
1、一定是写在静态类中
2、一定是个静态函数
3、第一个参数为拓展目标(拓展int、string、自定义类)
4、第一个参数用this修饰
注意:
当然拓展的方法如果与自己类中的函数重名,则调用自己的
里氏替换原则
GameObject father_p = new Player();
父类容器装载子类对象,方便对象的存储和管理(比如游戏中的众多对象,用一个父类数组就可以了)
Player p = father_p as Player;
父类容器是不能使用子类里面函数(后面通过多态解决)
is:判断一个对象是否是指定类对象
返回值:bool 是为真 不是为假
as:将一个对象转换为指定类对象(必须是引用类型)
返回值:指定类型对象
成功返回指定的类型对象,失败返回nul
继承中的构造函数
特点:
当申明一个子类对象时,先执行父类的构造函数再执行子类的构造函数
注意:
1、父类的无参构造很重要,没有指定则会优先调用父类的无参构造
2、子类可以通过base关键字,代表父类调用父类构造
万物之父object之装箱拆箱
装箱:把值类型用引用类型存储 栈内存移到堆内存中
拆箱:把引用类型存储的值类型取出来 堆内存移到栈内存中
装箱(从值类型转换到引用类型)需要经历如下几个步骤:
首先在堆上分配内存。这些内存主要用于存储值类型的数据。
接着发生一次内存拷贝动作,将当前存储位置的值类型数据拷贝到堆上分配好的位置。
最后返回对堆上的新存储位置的引用。
拆箱(从引用类型转换为值类型)的步骤则相反:
首先检查已装箱的值的类型兼容目标类型。
接着发生一次内存拷贝动作,将堆中存储的值拷贝到栈上的值类型实例中。
最后返回这个新的值。
好处:不确定类型时可以方便参数的存储和传递
坏处:存在内存迁移,增加性能消耗
var与object的区别:
var是编译阶段由编译器根据值自动判断类型,编译后var就已经被转换成为对应的类型,就不存在了,而object可以在运行阶段才被确定具体类型。
多态
virtual override base
1、多态按字面的意思就是“多种状态”
2、让继承同一父类的子类们 在执行相同方法时有不同的表现(状态)
主要目的:
同一父类的对象 执行相同行为(方法)有不同的表现
解决的问题:
让同一个对象有唯一行为的特征
底层原理:
1、每个类使用一个虚函数表,每个类对象用一个虚表指针,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。
2、父类对象包含一个虚表指针,指向父类中所有虚函数的地址表,而子类会继承父类的虚函数表,同时也自己的一个虚表指针,指向自己的虚函数表。
3、如果子类重写了父类的虚函数的话,那么子类的重写虚函数入口地址将会替代继承过来的虚函数入口地址。
4、如果父类中的虚方法没有在子类中重写,那么子类将继承父类中的虚方法,而且子类中虚函数表将保存父类中未被重写的虚函数的地址。
5、如果子类中定义了新的虚方法,则该虚函数的地址也将被添加到子类虚函数表中。
那么在程序运行时会发生动态绑定,将父类指针绑定到对应的实例化对象,从而实现多态。
一、函数重载
函数名相同,参数类型,数量,顺序不同
二、虚函数(virtual),重写(override)
public virtual void Atk(){}
子类可以重写父类中虚函数方法。
作用:父类容器也能调用对应子类重写的函数。
三、抽象(abstract)
public abstract void Eat();
抽象类不能被实例化(父类中的行为不太需要被实现 只希望子类去定义具体的规则)
特点:可以包含抽象方法,继承抽象类则必须重写其抽象方法。
抽象方法:1、只能在抽象类中声明。2、没有方法体。3、不能是私有。4、继承必须重写
四、接口(Interface)
interface IFly()
接口是行为的抽象规范,不能被实例化,类可以继承多个接口。(提高程序复用性)
1、不包含成员变量,只包含属性、索引器、事件
2、成员没有任何实现,并且没有访问修饰符(默认但是public)
3、子类必须实现接口所有成员,直接实现(抽象类、虚函数需要override)
抽象类与接口区别:
1、抽象类中可以有构造函数;接口中没有。
2、抽象类只能被单一继承;接口可以被继承多个。
3、抽象类中可以有成员变量;接口中没有。
4、抽象类中可以申明成员方法,虚方法,抽象方法,静态方法;接口中只能申明没有实现的抽象方法。
5、接口支持回调,且可以作用于值类型(struct),抽象类只作用于引用类型。
接口与抽象使用:
1、接口多定义对象的行为,抽象类多定义对象的属性。
2、如果要设计小而简练的功能块,则使用接口,如果要设计大的功能单元,则使用抽象类
命名空间
namespace _ {}
1、命名空间是个工具包 用来管理类的。
2、不同命名空间中可以有同名类。
3、不同命名空间中相互使用 需要using引用命名空间或者指明出处。
4、命名空间可以包裹命名空间。