关于类与对象
前言
之前写过了CPP,C#,Lua的语言基础 现在把三种语言的类与对象整理到一起。
简介
此部分讲解:类型 方法 继承
| 类别 | 描述 | 示例代码/概念 |
|---|---|---|
| 值类型 | Struct默认继承自object | struct MyStruct { /* ... */ } |
| 引用类型 | Class默认继承自object | public class MyClass { /* ... */ } |
| 接口 | 声明我是什么样子的类型(类型规范),不实现 | public interface ILifeCycle { /* ... */ } |
| 抽象类 | 不能实例化,可以包含抽象方法,可以暂时不实现接口,等待子类实现 | public abstract partial class Actor : ILifeCycle { /* ... */ } |
| 继承 | Class只能继承1或0个基类,但可以继承多个接口 | public class Mage : Actor, ILifeCycle, IDestroy { /* ... */ } |
| 结构体与继承 | Struct不能继承Struct或Class,但可以继承接口 | struct MyStruct : IMyInterface { /* ... */ } |
| 变量存储 | 父类变量可以存储子类变量(地址),不能反向存储 | Actor actor = new Mage(); |
| 面向对象概念 | 子类是一种特殊的父类 | A is Human, B is Teacher. 学生是人,但人不一定都是老师. |
面向对象的三大特点
继承: 提高代码重用度,增强软件可维护性的重要手段,符合开闭原则。继承最主要的作用就是把子类的公共属性集合起来,便与共同管理,使用起来也更加方便。你既然使用了继承,那代表着你认同子类都有一些共同的特性,所以你把这些共同的特性提取出来设置为父类。继承的传递性:传递机制 a->b; b->c; c具有a的特性 。继承的单根性:在C#中一个类只能继承一个类,不能有多个父类。
封装: 封装是将数据和行为相结合,通过行为约束代码修改数据的程度,增强数据的安全性,属性是C#封装实现的最好体现。就是将一些复杂的逻辑经过包装之后给别人使用就很方便,别人不需要了解里面是如何实现的,只要传入所需要的参数就可以得到想要的结果。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
多态性: 多态性是指同名的方法在不同环境下,自适应的反应出不同得表现,是方法动态展示的重要手段。多态就是一个对象多种状态,子类对象可以赋值给父类型的变量。
多态与虚函数-CPP
多态
什么是多态?C++的多态是如何实现的?
所谓多态,就是同一个函数名具有多种状态,或者说一个接口具有不同的行为;C++的多态分为编译时多态和运行时多态,编译时多态也称为为静态联编,通过重载和模板来实现,运行时多态称为动态联编,通过继承和虚函数来实现。
虚函数
编译器怎么实现多态的 (虚函数的实现原理是什么)
虚函数是通过虚函数表来实现的,虚函数表包含了一个类(所有)的虚函数的地址,在有虚函数的类对象中,它内存空间的头部会有一个虚函数表指针(虚表指针),用来管理虚函数表。当子类对象对父类虚函数进行重写的时候,虚函数表的相应虚函数地址会发生改变,改写成这个虚函数的地址,当我们用一个父类的指针来操作子类对象的时候,它可以指明实际所调用的函数。
- 问:virtual function的优缺点(提问概率:★★★★)
优点:实现多态
缺点:MFC中的消息机制以及Qt中都没有采用C++中的虚函数机制,原因大概如下~
1.在子类很少覆盖基类函数实现的时候内存开销太大,每个类需要产生一张虚表,每生成一个对象的时候,需要在对象里存放一个vptr。
2.基类指针指向派生类对象的情况下,不方便内联优化(有些情况可以内联,参考我前面的文章)
3.在执行效率上肯定多了一些开销,需要寻找函数地址
4.虚表的存在可能破坏一些封装安全,可以通过vptr绕过private的限制
问:多继承的优缺点(提问概率:★★★)
好处:简单来讲就是为了实现多个基类特有的功能
缺点:菱形继承;二义性(可通过虚继承解决)
构造函数与析构函数
构造和析构能是虚函数吗
- 为什么父类析构函数必须是虚函数?为什么C++默认析构函数是虚函数。
1、如果父类的析构函数不是虚函数,则不会触发动态绑定(多态),结果就是只会调用父类的析构函数,而不会调用子类的析构函数,从而可能导致子类的内存泄漏(如果子类析构函数中存在free delete 等释放内存操作时);
2、如果父类的析构函数是虚函数,则子类的析构函数一定是虚函数(即使是子类的析构函数不加virtual,这是C++的语法规则),则会在父类指针或引用指向一个子类时,触发动态绑定(多态),析构实例化对象时,若是子类则会执行子类的析构函数,同时,编译器会在子类的析构函数中插入父类的析构函数,最终实现了先调用子类析构函数再调用父类析构函数。
C++默认构造函数不是虚函数,是因为虚函数需要虚函数表和虚表指针,会占用额外内存。如果一个类没有子类,就没有必要将析构函数设为虚函数。
- 构造函数不能是虚函数,因为在对象构造过程中其内存布局和虚函数表(vtable)尚未完全初始化,无法进行虚函数调用。
- 析构函数可以也是应当为虚函数,以确保通过基类指针删除派生类对象时能够调用正确的析构函数,从而实现正确的资源管理和释放。
- 构造函数:禁止虚函数(语法限制,逻辑无意义)。
- 析构函数:基类析构函数应声明为虚函数,避免资源泄漏,保障多态对象安全销毁。
构造函数和析构函数是否能调用虚函数
答:在C++ primer中说到过是最好不要调用,不是不能调用,所以构造函数跟虚构函数里面都是可以调用虚函数的,并且编译器不会报错。但是在基类中声明纯虚函数并且在基类的析构函数中调用,编译器会报错。
对于底层:
当在构造基类部分时,派生类还没被完全创建。即当A::A()执行时,B类对象还没被完全创建,此时它被当成一个A对象,而不是B对象,因此Function()绑定的是A的Function()。
基类部分在派生类部分之前被构造,当基类执行构造函数时,派生类中的数据成员还未被初始化。如果在基类构造函数中调用虚函数被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,这是危险的,将会导致程序出现未知行为及bug。
- 在构造函数中调用虚函数时,派生类对象尚未完全构造,因此调用的是当前类的虚函数。
- 在析构函数中调用虚函数时,派生类对象已经部分销毁,因此调用的是当前类的虚函数。
复杂继承情况
虚函数
虚函数多态时机:
通过基类指针/引用调用时触发动态绑定。
直接通过对象实例调用则为静态绑定。

- 对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
- 由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
- 对于虚函数 vfunc2,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
- 从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
- 对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。
动态绑定有以下三项条件要符合:
- 使用指针进行调用
- 指针属于up-cast后的
- 调用的是虚函数
与动态绑定相对应的是静态绑定,它属于编译的时候就确定下来的,如上文的非虚函数,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。
C++中的虚指针与虚函数表 - 知乎 (zhihu.com)
虚函数内存模型

C++多态虚函数表详解(多重继承、多继承情况)_一个类有几个虚函数表-CSDN博客
复杂情况
1 | // 多重继承(多级继承) |
多重继承
(Multilevel Inheritance)指的是继承链有多层的情况,比如”人类-士兵类-步兵类”这样的层级关系,即一个类继承自另一个类,而后者又继承自第三个类,形成链式继承结构。

1 | class Base { |
1 | Derived 对象(24 字节): |
多继承
(Multiple Inheritance):指的是一个类同时从多个基类继承,比如”农民工类”同时继承”农民类”和”工人类”。

1 | class Base1 { |
1 | Derived 对象(40 字节): |
钻石继承
钻石(菱形)继承存在什么问题,如何解决?
【参考资料】:C++之钻石问题和解决方案(菱形继承问题)_Benson的专栏-CSDN博客、C++:钻石继承与虚继承 - Tom文星 - 博客园 (cnblogs.com)
钻石继承例子:
D -> B, C ->A
1 | /* |
在上面的代码中,我们给出了一个具体的钻石问题例子。Animal类对应于最顶层类(图表中的A),Tiger和Lion分别对应于图表的B和C,Liger类(狮虎兽,即老虎和狮子的杂交种)对应于D。
- 解决方法:
答:会存在二义性的问题,因为两个父类会对公共基类的数据和方法产生一份拷贝,因此对于子类来说读写一个公共基类的数据或调用一个方法时,不知道是哪一个父类的数据和方法,也会导致编译错误。
可以采用虚继承的方法解决这个问题 (父类继承公共基类时用virtual修饰),这样就只会创造一份公共基类的实例,不会造成二义性。
1 | class Tiger : virtual public Animal { /* ... */ }; |
内存模型总结

关键字·
final override
- final和override的作用,以及使用场合(提问概率:★★)
使用场合(final):
不希望这个类被继承,比如vector,编码者可能不够了解vector的实现,或者说编写者不希望别人去覆盖某个虚函数.顾名思义,final就是最终么
不希望这个函数再被其他子类覆写 (类似为C#的sealed)
使用场合(override):
第一种情况是你想覆写一个基类的函数,但是不小心参数不匹配或者名字拼错,结果导致写了一个新的虚函数。这时候如果你加上override关键字,编译器会帮你发现与基类函数不匹配从而给出编译错误的提示。
第二种,在使用别人的函数库,或者继承了别人写的类时,你想写一个新函数,但是可能碰巧与原来基类的函数名称一样,这样就会被编译器(以及其他人)误认为要重写基类的函数。如果大家都养成习惯重写基类函数时都加上override,别人在看到你的代码时就知道你当前的函数是否想重写基类里面的函数,也就容易发现你这个无意中重载的Bug。
C++内存模型
内存
| 内存区域 | 描述 | 分配和释放方式 | 特性 |
|---|---|---|---|
| 堆 | 使用malloc、free动态分配和释放空间,能分配较大的内存 | malloc/free | 1. 需要手动申请和手动释放 2. 能分配的内存较大 3. 分配和释放内存可能产生内存碎片 4. 分配效率较低 5. 地址从低向上 |
| 栈 | 为函数的局部变量分配内存,能分配较小的内存 | 由操作系统自动分配和释放 | 1. 自动申请和自动释放 2. 能分配的内存较小 3. 不会产生内存碎片 4. 分配效率高 5. 地址由高向下 |
| 全局/静态存储区(.bss 和 .data段) | 用于存储全局变量和静态变量 | 编译器分配和释放 | 存储全局和静态变量的生命周期(未初始化在.bss段,初始化的在.data段) |
| 常量存储区(.data段) | 专门用来存放常量 | 编译器分配和释放 | 存储常量,不可修改 |
| 代码区(.text段) | 存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里. | ||
| 自由存储区 | 通过new和delete分配和释放空间的内存,具体实现可能是堆或者内存池 | new/delete | 1. 是C++的术语,抽象概念 2. 可以通过重载操作符使用其他内存实现 3. 对于自定义对象,能调用构造函数和析构函数 |
关于堆和栈的内存区别:
| 区别点 | 堆 | 栈 |
|---|---|---|
| 内存申请和释放 | 手动申请和释放 | 自动申请和释放 |
| 内存大小 | 较大(4G(32位机器)) | 较小(1M) |
| 内存碎片 | 可能产生 | 不产生 |
| 分配效率 | 较低 | 较高 |
| 地址方向 | 从低向上 | 由高向下 |
C++和C分别使用什么函数来做内存的分配和释放:
| 语言 | 分配和释放函数 | 描述 |
|---|---|---|
| C | malloc/free | 库函数,只进行内存分配和释放,无法调用构造函数和析构函数 |
| C++ | new/delete | 运算符,能调用构造函数和析构函数,完成对象的空间分配、初始化、销毁和释放空间 |
C++和C的内存管理函数的区别:
- new分配内存空间无需指定分配内存大小,malloc需要。
- new返回类型指针,类型安全,malloc返回void*,再强制转换成所需要的类型。
- new是从自由存储区获得内存,malloc从堆中获取内存。
- 对于类对象,new会调用构造函数和析构函数,malloc不会。
不能混用C和C++的内存管理函数,因为它们的处理方式和内存管理机制不同。
内存布局
- 在C++中类对象的内存布局是如何分布的呢?
在这里,影响对象大小的有哪些因素呢?
成员变量的类型与数量、虚函数表的指针(_vftptr)、虚基类表指针(_vbtptr)–>产生虚函数表、单一继承、多重继承、重复继承、虚拟继承
- 问:类的内存布局是什么样的?考虑有虚函数、多继承、虚继承几种情况。(提问概率:★★★★★)
简单总结一下就是类只有成员变量占用内存(静态成员不占类内部内存,函数不占内存)。如果有虚函数,每个类对象都会有一个虚函数指针Vptr(占用一个指针大小的内存),vptr指向一个虚函数表,表里面记录了各项标记virtual的函数,子类如果覆盖父类虚函数,对应虚表位置的虚函数会被子类的替换。如果是虚继承,还会有虚基类表记录当前对象相对虚基类的偏移,以及一个虚基类指针指向这个虚基类表。
虚表在编译完成时大小与布局就被决定了,加载时其内存位置也就被确定了。
- 1.单继承一个或多个类只有一个虚表一个虚指针
- 2.普通多继承会有基类的个数个虚表,基类的个数个虚指针。派生类自己独有的虚函数可能会放在第一个虚表的最后面
- 3.单个虚继承会有两个虚表(看情况)以及一个虚基类表,两个虚指针(这个可能与我们想象中的不一样,一个指向自己独有的虚函数的虚表,一个指向覆盖基类虚函数的虚表)以及一个虚基类指针与虚基类表 注意:如果派生类自己的虚函数与基类完全相同,可能只有一个虚表,一个虚指针
- 4.菱形多虚继承会有基类的个数个虚指针以及虚表(看情况,第3条有提到),有几个虚继承就有几个虚基类指针以及虚基类表
- C++的
sizeof影响因素
- 非静态成员变量:直接贡献大小。
- 内存对齐:编译器填充提升访问速度(如
#pragma pack)。 - 虚函数表指针:存在虚函数时增加一个指针大小(64位下8字节)。
- 继承关系:基类成员+派生类成员,可能受空基类优化影响。
**空对象sizeof**:
默认为1字节(占位),若继承空基类可能优化为0(Empty Base Optimization)。
虚函数表为什么不放在全局区
原因:
- 虚函数表(vtable)是与类相关的,而不是与程序全局相关的。
- 每个类可能有多个虚函数表(如多继承)。
- 虚函数表需要与类的定义紧密关联,放在全局区会增加管理复杂度。
存储位置:
虚函数表通常存放在只读数据段(.rodata)。

const 变量的存储位置
- 常量区(.rodata):
- 全局
const变量。 - 字符串常量。
- 全局
- 栈区:
- 局部
const变量。
- 局部
内存对齐
为什么要字节对齐?
需要字节对齐的根本原因在于CPU访问数据的效率问题。假如没有字节对齐,那么一个double类型的变量会存储在4-11上(正常是0-7)这样计算机取这个数据的会会取两次,降低效率。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误。
自定义类型的内存对齐规则
- 规则:
- 对齐值为成员中最大对齐值的整数倍。
- 每个成员的偏移量必须是其对齐值的整数倍。
1 | struct Example { |
(99+ 封私信 / 80 条消息) C/C++内存对齐详解 - 知乎
什么时候不希望进行内存对齐
- 节省内存:在某些嵌入式系统中,内存资源有限,可能需要手动调整对齐方式。
- 兼容性:与某些硬件或协议交互时,可能需要特定的内存布局。
- 问:memory alignmentand padding, 内存对齐的原理与意义(提问概率:★★★★)
结构体以及类成员对齐,意义就是减少cpu读取的次数,提高效率。比如一个int变量长度为4个字节,cpu一次读4个字节,当然是一次读取比较好。但是如果前面有一个char,地址为0-1。那么这个int的地址就为1-4。导致cpu,分两次读取int值。
具体的对齐规则,要说的非常准确可能比较麻烦,简单来讲就是,每个变量看后面的变量,如果后面的变量大,就和后面的大小对齐并补充字节。最后一个变量,按照成员内最大的对齐值,对齐并补充字节
内存泄漏
- 问:内存泄露是什么意思?如何检测与避免内存泄漏?(提问概率:★★★★)
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费
常量
Const
- const的作用?指针常量和常量指针?const修饰的函数能否重载?
| 描述 | 细节 | |
|---|---|---|
| const修饰符 | 用来定义常量,表示不可变性。 | |
| 常量 | 被const修饰的成员函数不能修改类中的数据成员。 | 具有不可变性 |
| 指针常量 | 指针本身不可修改,但指向的对象可以修改。 | 指针本身的常量性 |
| 常量指针 | 指针指向的对象不可修改,但指针本身可以修改。 | 指针指向对象的常量性。 |
| const成员函数 | 不能改变类内的数据成员,也无法调用非const的成员函数。 | |
| const类对象函数调用const函数 | 只能调用const成员函数。 | |
| 非const类对象函数调用const函数 | 可以调用const和非const成员函数,但优先调用非const函数(如果有重载)。 |
- 了解const么?哪些时候用到const?与宏定义有什么差异?(提问概率:★★★★)
简单理解,const的目的就是定义一个“不会被修改的常量”,可以修饰变量、引用、指针,可以用于函数参数、成员函数修饰、成员变量,修饰成员函数本质上就是修饰“this”指针,所以不能修改函数内部的成员变量。
相比宏定义,const在编译期也会起作用(宏定义只是预编译期),会做一些类型检查,方便调试。而且,const不需要在每个用到的地方都申请一块内存空间,要更节省内存。
Static
- static的作用?static变量什么时候初始化?
| 修饰对象 | 作用范围 | 描述 |
|---|---|---|
| 文件作用域的变量 | 本文件 | static修饰的变量和函数仅在本文件可见,其他文件无法访问和使用,有助于避免重定义问题。 |
| 函数作用域的变量 | 函数内部 | 作为局部静态变量时,该变量在函数调用期间保持其值,只进行一次初始化,不会因函数调用而重置,但仅在该函数内部可见。 |
| 类的静态数据成员 | 所有类对象 | static修饰的静态数据成员是所有类对象共享的,而非每个类对象独有的。这些静态数据成员在类的声明中不占用内存,必须在.cpp文件中定义以分配内存。 |
| 类的静态成员函数 | 所有类对象 | static修饰的静态成员函数->静态数据成员和函数->x非静态数据成员和函数。 它们属于类本身而非类的某个特定对象。 |
| 初始化过程 | 文件域的静态变量和类的静态成员变量在main函数执行之前的静态初始化过程中分配内存并初始化;局部静态变量在第一次使用时分配内存并初始化。 |
说一下static关键字的作用?
全局静态变量:位于静态存储区,程序运行期间一直存在,对外部文件不可见。
局部静态变量:位于静态存储区,在局部作用域可以访问,离开局部作用域之后static变量仍存在,但无法访问。
静态函数:即在函数定义前加static,函数默认情况下为extern,即可导出的。加了static就不能为外部类访问。注意不要在头文件声明static函数,因为static只对本文件有效。
类的静态成员:可以实现多个不同的类实例之间的数据共享,且不破坏隐藏规则,不需要类名就可以访问。类的静态存储变量是可以修改的。可以通过<对象名>::<静态成员>进行访问。
类的静态函数:不能调用非静态成员,只可以通过对象名调用<对象名>::<静态成员函数>
static 不需要初始化,默认为0值。
extern与static
- 问:extern与static(提问概率:★★★)
extern 声明一个变量定义在其他文件,这样当前文件就可以使用这个变量,否则会编译失败,如果两个全局变量名称一样会出现链接失败。extern c的作用更重要,因为c++的编译方式与c是不同的,比如函数重载利用mangling的优化。
static变量,很多编译器优化后的效果就是声明一个全局变量,然后判断是否初始化,是的话之后就不需要再初始化了,但是不绝对,win7的全局变量与static的位置就有差异。static成员函数其实在编译后与class完全没有关系。static成员其实也没关系,但是private的需要通过类去调用。static全局变量需要注意,他只能在当前编译单元也就是.cpp内使用(内链接)。全局函数变量是外链接,可以跨单元调用。
static相关注意与理解:静态内存是在main前分配,在main后释放。当存在多个复杂的static变量时,你就不知道哪个先分配了,也控制不了。另外,关于static具体的存储位置,一般是我们常说的静态存储区(bss,数据区等),更贴切的说他是一个可执行文件里面的区域,到操作系统层面可能是另一种叫法,对不同的编译器、C++版本、操作系统可能都有所差异。我们一个程序编译链接好后会把一些静态数据写到exe、dll里面,注意这时候exe并没有放入到内存,所以,其实所谓的编译后内存位置就确定了只不过是一种“理解方式”,真正的静态区(全局变量、静态变量、常量)也是在程序运行后操作系统将这些数据装入内存后的一个位置,这个位置相对exe来说可以理解为静态的,然后当我们运行exe动态申请内存时就是我们 常说的堆区(也可以叫动态区、C++叫自由存储区等)
单例模式
单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
定义一个单例类:
- 私有化它的构造函数,以防止外界创建单例类的对象;
- 使用类的私有静态指针变量指向类的唯一实例;
- 使用一个公有的静态方法获取该实例。
1 | public class Singleton |
单例模式 静态类的区别
1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
2)单例模式的灵活性更高,方法可以被override,因为静态类都是静态方法,所以不能被override;
3)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
接口与抽象类-C#
纯虚函数
纯虚函数–cpp
两者的区别在于纯函数尚未被实现,定义纯虚函数是为了实现一个接口。在基类中定义纯虚函数的方法是在函数原型后加=0
1 | virtual void function() = 0; |
接口(抽象类)是什么
抽象类(接口)是一种特殊的类,不能定义对象,需要满足以下条件:
类中没有定义任何的成员变量
所有的成员函数都是公有的
至少一个成员函数是纯虚函数
子类继承接口,需要实现接口的全部的方法。
Interface与abstract之间的不同
- 省流:
- 接口不是类 不能实例化 抽象类可以间接实例化
- 接口是完全抽象抽象类为部分抽象
- 接口可以多继承 抽象类是单继承
- 异同点
1 | 相同点: |
接口和抽象类相同点
- 都是不断抽取出来的抽象概念
接口和抽象类的区别
- 接口是行为的抽象,是一种行为的规范,接口是like a 的关系;抽象是对类的抽象,是一种模板设计,抽象类是is a 的关系。
- 接口没有构造方法,而抽象类有构造方法,其方法一般给子类使用
- 接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
- 抽象体现出了继承关系,继承只能单继承。接口提现出来了实现的关系,实现可以多实现。接口强调特定功能的实现,而抽象类强调所属关系。
- 接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public abstract的。抽象类中成员变量默认default,可在子类
- 中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
重载 重写 覆写
定义
以C#为例:
一、override重写,是在子类中重写父类中的方法,两个函数的函数特征(函数名、参数类型与个数)相同。用于扩展或修改继承的方法、属性、索引器或事件的抽象或虚拟实现。提供从基类继承的成员的新实现,而通过override声明重写的方法称为基方法。
注意事项:
1.重写基方法必须具有与override方法相同的签名。
2.override声明不能更改virtual方法的可访问性,且override方法与virtual方法必须具有相同级别访问修饰符。
3.不能用new、static、virtual修饰符修改override方法。
4.重写属性声明必须指定与继承的属性完全相同的访问修饰符、类型和名称。
5.重写的属性必须是virtual、abstract或override。
6.不能重写非虚方法或静态方法。
7.父类中有abstract,那么子类同名方法必定有override,若父类中有 virtual方法,子类同名方法不一定是override,可能是overload。
8.override必定有父子类关系。
二、overload重载,在同一个类中方法名相同、参数或返回值不同的多个方法即为方法重载。
注意事项:
1.出现在同一个类中。
2.参数列表不同或返回类型和参数列表都不同,只有返回类型不同不能重载。(参数列表包括参数个数和参数类型)
三、overwrite覆写,用new实现。在子类中用 new 关键字修饰定义的与父类中同名的方法,也称为覆盖,覆盖不会改变父类方法的功能。
C# 关键字:overload、override、new
1 | class Parent |
C#关键字之override详解_public override void-CSDN博客
重载和重写的区别
封装、继承、多态所处位置不同,重载在同类中,重写在父子类中。
定义方式不同,重载方法名相同参数列表不同,重写方法名和参数列表都相同。
调用方式不同,重载使用相同对象以不同参数调用,重写用不同对象以相同参数调用。
多态时机不同,重载时编译时多态,重写是运行时多态。
以CPP为例:
1 | //overload |
Lua类与对象
Lua中 点和冒号区别
点 :无法传递自身,需要显示传递
冒号 :隐式传递自身
Metatable
在 Lua table 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table 进行操作(比如相加)。
因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。
例如,使用元表我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。
当 Lua 试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add 的字段,若找到,则调用对应的值。 __add 等即时字段,其对应的值(往往是一个函数或是 table)就是”元方法”。
有两个很重要的函数来处理元表:
- setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
- getmetatable(table): 返回对象的元表(metatable)。
1 | local function class(base) |
继承
1 | -- SubClass.lua |
类与对象写法
1 | Object = {} |
1 | ChildClass =Object:subClass("ChildClass") |
封装
1 | -- Class.lua |
多态
1 | -- main.lua |
C# 类的关键词
分部类 Partial
C# 中的分部类(Partial Classes)是一种特殊的类定义方式,它允许一个类的定义被分割到多个文件中。这对于一些大型类,或者当类的定义分布在多个自动生成的代码文件中(例如,由设计器或某些工具生成的代码)时,特别有用。
分部类的定义方式很简单,只需在类名前加上 partial 关键字即可。下面是一个简单的例子:
文件 MyClass1.cs:
1 | namespace MyNamespace |
文件 MyClass2.cs:
1 | namespace MyNamespace |
在上面的例子中,MyClass 是一个分部类,它的定义被分割到了两个文件中:MyClass1.cs 和 MyClass2.cs。这两个文件都位于同一个命名空间 MyNamespace 下,并且都定义了同一个名为 MyClass 的类。
编译器在编译时,会将所有具有相同名称和命名空间的分部类定义合并成一个完整的类定义。因此,在上面的例子中,MyClass 类将包含 Method1 和 Method2 两个方法。
需要注意的是,分部类不能分割到不同的命名空间或项目中。此外,分部类主要用于设计器生成的代码或自动代码生成工具,不建议在普通的手动编写的代码中使用。
密封sealed
一、核心特性与语法规则
- 用于类
- 作用:声明密封类,禁止其他类继承。
- 语法:
sealed class ClassName { ... } - 限制:不可与
abstract修饰符共用,因为抽象类必须被继承。
- 用于方法和属性
- 作用:阻止派生类重写已继承的虚方法或属性。
- 语法:必须与
override结合使用,如sealed override void Method() { ... }。 - 前提:仅能对基类中声明为
virtual或已重写的成员进行密封。
二、常见应用场景
- 封装与安全性
- 在第三方类库中,防止客户端代码通过继承修改核心逻辑(如加密算法、支付接口)。
- 避免继承滥用导致类层次结构混乱,例如工具类或静态辅助类。
- 性能优化
- 密封类的方法调用可被JIT编译器优化为非虚调用,减少运行时虚方法表(vtable)的查找开销。
- 适用于高频调用的方法(如游戏引擎中的渲染循环)。
三、代码示例修正与解析
1 | using System; |
四、执行结果说明
B.F的密封性:类C无法重写F(),因此new C().F()仍调用B.F()。G()的可扩展性:类C可自由重写非密封方法G(),输出自定义结果。

