雖然這個話題看著似乎有些關公戰秦瓊的味道,但是作為游戲開發者,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#的不支持。