浅谈——C++和C#差异
虽然这个话题看着似乎有些关公战秦琼的味道,但是作为游戏开发者,C++和C#一定是绕不开的两门语言。不过虽然说是比较二者差异,因为我学习的过程主要是先学C++,所以我先基于C++的认知,再来聊聊C#之中的不同。
(为什么会想到写这个帖子是因为我发现老是记混二者名字相同但是底层完全不同的概念,只能说很烦)
讨论差异之前,我们先来聊聊大体上二者的相似之处。
二者都是编译执行的强类型语言,都是基于OOP思想,都支持泛型。
我们首先来聊第一个不同的点——C++的指针。
指针
C++中的指针是一个用来存储变量地址的变量,其底层涉及到了内存管理的内容,众所周知指针给C++带来了高性能的同时也带了很多麻烦,指针本身的管理就是一个麻烦,比如什么野指针,悬空指针,空指针,指针与指针之间又可以传递,还有一系列诸如常量指针和指针常量的内容。
C#中并非不可以使用指针,C#中有内存分层的概念:
针对原生内存我们就需要像C++一样通过指针去访问和管理,但是不讨论这种特殊情况的话,在C#的代码中我们是看不到指针的,那这时候我们还是需要一个存储变量地址的变量呀,不然怎么进行内存的访问呢?这个就是我们C#的引用类型。
这里我先说一下C#的数据类型分类,大体上分成值类型和引用类型。
这里补充一下关于System.Object和System.ValueType的内容:
Object类是C#中所有对象的基类。
通过上述的介绍,相比你也知道了,引用类型就是一个类型安全的托管指针,用来存储堆中对象的地址,我们修改引用类型的值就会直接修改实际的数据,我们拷贝值类型时会拷贝出一个副本而拷贝引用类型会拷贝实际的数据。
这里又涉及到一个问题,我们知道修改C++的指针时,我们既可以修改指针指向的地址也可以通过指针修改地址存储的值,那我们修改对应的C#的引用类型的变量时,如何具体判断修改的是地址还是地址存储的值呢?
先说结论的话,C#的引用是无法修改存储的地址的值的,所以我们不用区分,因为一般情况下我们只能修改引用类型的值。
难道我们就没有办法去修改引用指向的地址吗?
ref关键字可以帮助我们实现引用的地址修改。
C# 引用类型实现了 C++ 指针的核心权能——提供对堆内存对象的间接访问与修改能力,并支持高效的数据共享,移除了指针算术、任意重定向等危险操作,通过 GC 自动管理生命周期,体现了 C# “安全优先”的设计理念。
抽象类&&接口
C++如何实现抽象类?很简单,用一个纯虚函数来做就好了。
class A {
public: // ✅ 必须 public 才能被覆盖virtual void a() = 0; // ✅ 虚函数需 virtual
};
但是当你来到C#中,你发现好像没有纯虚函数这种写法,取而代之的是abstract关键字:
public abstract class A { // ✅ 类必须标记 abstractpublic abstract void a(); // ✅ 无方法体,无{},直接分号结束
}
这个概念同样地影响到了接口interface的实现:
// C++ 通过纯抽象类模拟接口(无成员变量,无实现方法)
class IShape {
public:virtual ~IShape() = default; // 虚析构(必须)virtual double GetArea() const = 0; // 纯虚方法(接口函数)virtual void Draw() const = 0; // 纯虚方法(接口函数)
};
这是C++的接口实现,所有接口内的方法都必须是纯虚函数。
// C# 原生接口(无成员变量,无构造逻辑)
public interface IShape {double GetArea(); // 接口方法(自动public)void Draw(); // 接口方法(自动public)
}
C#中已经专门集成了接口的概念,用关键字interface即可。
总结来说的话,C的抽象类是通过我们的一个纯虚函数来实现的,某一个类之中如果有一个纯虚函数我们就把这个类认作这个抽象类。然后C++的接口就是类中的所有函数都是纯虚函数我们就把这个类看做一个接口,继承这个接口的方法就必须要提供这些方法的全部实现。然后C#的话关于抽象类它有一个专门的关键字叫abstract,我们用这个关键字修饰类那么这个类就是一个抽象类,要求其中的这个类中的至少有一个方法也是被abstract修饰的抽象方法,而我们的那个接口的话也是有原生集成的一个interface关键字,我们用interface关键字来修饰类那么这个类就会被视作一个接口我们只需要在这个类中来呃提供方法的声明就可以。
只读
在C++中,实现只读的关键字有constexpr和const。
对于C++来说,这两个关键字最大的差异就是具体何时赋予变量只读的属性,对应的,什么时候赋予只读的属性就要求什么时候进行初始化。而在C#中也有两个关键字实现只读,但是这两个关键字变成了const和read only。
可以看到同样叫const,在C++中是运行时常量结果到了C#变成了编译常量。
String
C#的string作为一个引用类型,我们在讨论值类型和引用类型的区别时有提到,修改值类型往往都是修改其副本而修改引用类型往往都是直接修改其值,那么string作为引用类型,一定也是这样的吧?
如果真是这样我就不会在这里把这个点列举出来了,实时上C#中的string非常特殊:具有不可变性。
如何理解不可变性?当我们写这样一段代码:
string a="abc";
a+='c';
首先a本身就是一个string的对象,一开始的时候是一个没有内容(但是有内存)的空字符串"",然后"abc"本身也是一个字符串,我们把"abc"丢给a,然后我们后续要对a添加字符'c'时,c本身也是一个字符(串?双引号就是字符串单引号就是字符),由于C#中string的不可变性,我们的堆中会重新生成一个a的副本,然后把'c'丢给a的副本,然后返回a的副本,a本身就作为堆上的垃圾等待回收了。
一言以蔽之,C#将string赋予了不可变性,这是有理由的:
可以看到赋予string不可变性后,线程安全的问题就从根源上被解决了。
迭代器
C++中的迭代器是STL库中帮助我们访问容器内部具体元素的一个封装了指针的工具,但是C#中可没有STL库,更没有指针用来封装,那么C#的迭代器是干嘛的呢?
聊到这个,我需要先来说一下C#的foreach和for这两种数据遍历方式。
foreach是只读循环,不可以修改遍历的元素,不用获取数组的长度,以及由额外的GC开销。前两者非常好理解,但是第三点额外的GC开销是从何而来的呢?
这里其实也可以看到C#底层的数据集合实现,很多都是基于IEnumerable接口实现的,这样我们调用foreach进行遍历时直接调用接口的方法获取一个迭代器来遍历元素。然后这里还牵扯到一个状态机类:
当我们使用yield return方法时,编译器就会帮助我们实现一个状态机类实时记录方法执行到哪一步,这个状态机对象本身在堆上,所有会有GC开销。
Using
这里算是一个补充,C++和C#中的using都有着引用命名空间以及修改命名空间命名的作用,但是C#中还多了一个功能就是可以帮助我们释放实现了IDisposable接口的资源。
委托/回调
C++中并没有委托这个概念,但是有基本的回调函数的概念,具体来说就是允许在函数的参数列表中放入函数的指针,然后在函数体中直接利用这个指针来调用函数体以外的函数。
void callback(int x) { /* ... */ }void doSomething(void (*func)(int)) {func(42); // 回调}
C#中的委托其实本质上也是一样的思想:不过delegate是一种类型安全的函数指针。
public delegate void MyCallback(int x);void DoSomething(MyCallback cb) {cb(42); // 回调}
在这个delegate的基础之上,C#还实现了event,action和function。
C# 中的 delegate 是类型安全的函数指针,是回调和事件机制的基础;event 是对委托的事件化封装,实现发布-订阅模式,限制了外部访问;Action 和 Func 是 .NET 内置的泛型委托,分别用于无返回值和有返回值的场景,极大简化了委托的声明和使用。四者本质上都是委托机制的不同表现形式,适用于不同的开发需求。
结构体
有一个最基本的差异:C++的结构体支持继承而C#的不支持。