C#.Net是一门优秀的跨平台编程语言，有着C++级别的运行速度，和大量用以提升开发效率的语法糖，以及极佳的跨平台能力。近期使用C#接入了一个C++库，在此稍作一下总结。1. 动态库实现方案选择近期在使用C#实现一些功能时，需要接入C++库。但C++由于众所周知的原因，同名函数可能有多种重载，因此实现的函数声明根据参数和重载等的不同，会被编译器加上不同的符号，导致无法被正确调用。上述问题有两种解决方法：使用extern "C"声明来声明一个纯C类型的函数，这也是后来实际采用的方式；仍然使用C++方式声明函数，但是在C#调用时，需要根据实际生成的函数声明来调用。第二种方式其实属于一种hack，问题就在于，不同编译器实际生成的函数名并不一致，因此可能不同版本编译器、不同平台上，都需要分别解析一次实际符号，并修改C#里的调用源码。这种方式明显不实际且不通用。而第一种，使用extern "C"的方式声明，则是通用的跨语言交互方案。缺陷有两点：缺少了C++的重载；只能使用纯C类型（值，数组，结构，指针），不能使用C++类型。而它的好处也显而易见：不同编译器、不同平台下导出的符号表统一（即为函数名）；上述限制仅针对需要EXPORT的接口，内部仍然可以用C++实现，类、模板等不受影响；这两点缺陷在可以接受范围内，因此采用这种方式实现跨语言交互。既然使用了extern "C" ，这个问题的本质其实就变成了“如何使用C实现一个C++的动态库，并允许其它语言调用”。那这其实就已经是一个方案很成熟的领域了，分为这么几步：首先用C实现动态库，并导出预期的符号（函数与类型）；其次，在不同平台上分别编译，得到不同平台上的库类型。在调用语言内实现对应的接口、类型，并添加引用；在调用语言内使用接口，测试功能是否正常。2. 使用 C 实现 C++ 动态库extern "C"方案也会带来另一个问题：如果需要让C#调用C++库，那么必须通过纯C进行一次中转：C++的模板等必须使用纯C做展开，分别以不同的声明接入；C++的类型（无论是STL还是自定义class）以及类型上的方法也需要用纯C展开；比如我们有一个这样的类型：class Document { public: Docunemt(std::string title): title_(title), content_() { } void AppendLine(int32_t number) { content_.emplace_back(std::to_str(number)); } void AppendLine(const char* str, int len) { content_.emplace_back(std::string(str, len)); } void AppendLine(const std::string& line) { content_.emplace_back(line); } std::string GetContent() const { /* do some string join works */ } private: std::string title_; std::vector<string> content_; }C语言无法使用std::string，std::vector<std::string>，也处理不了三个不同的AppendLine 重载，更不用提（没有放进示例）的模板。因此，如果我们想要用导出为动态库，需要使用C语言以如下形式声明：// 首先定义DLLEXPORT宏，指定哪些函数是需要导出的 #ifdef WIN32 #define DLLEXPORT __declspec(dllexport) #else #define DLLEXPORT __attribute__((visibility("default")) #endif class Document; // 这里是用于C++的前向声明 using i32 = int32_t; extern "C" { struct CppString { char* str = nullptr; int32_t len = 0; } DLLEXPORT Document* CreateDocument(const char* title_buff, i32 len); DLLEXPORT void AppendLineInt(Document* doc, i32 number); DLLEXPORT void AppendLineStr(Document* doc, const char* str, i32 len); DLLEXPORT void GetContent(Document* doc, CppString* str_result); DLLEXPORT void FreeCppString(CppString* str_result); DLLEXPORT void FreeDocument(Document* doc); } // extern "C"上面的声明形式其实很简单，无非是将原来一些被C++封装好的对象创建、释放，用C进行手动管理。这对熟悉C++的你来说并不会是难事，仅仅是略微繁琐一些。值得注意的是：我们使用了CreateDocument，因此也要有对应的FreeDocument；GetContent内会有动态资源分配，因此也要有对应的FreeCppString。不过不用过于担心，这里只要实现即可，这些资源的RAII仍然可以交给C#来完成。接下来是对以上接口的一些实现，由于不是重点要讲的内容，只提供一些大概思路：Document* CreateDocument(const char* title_buff, i32 len) { return new Document(std::string(title_buff, len)); } void AppendLineInt(Document* doc, i32 number) { doc->AppendLine(number); } void AppendLineStr(Document* doc, const char* str, i32 len) { doc->AppendLine(std::string(str, len)); } void GetContent(Document* doc, CppString* str_result) { auto result = doc->GetContent(); str_result.str = new char[result.size()]; str_result.len = result.size(); ::memcpy(str_result.str, result.data(), result.size()); } void FreeCppString(CppString* str_result) { delete[] str_result->str; str_result->str = nullptr; str_result->len = 0; } void FreeDocument(Document* doc) { delete doc; }接下来只要在不同平台上分别编译出来对应的DLL即可：在本文的例子中，Windows上为doc_api.dll，Linux上为libdoc_api.so，而macOS上为libdoc_api.dylib。这一步就不用做过多展开了。3. 在 C#.Net 里引入动态库C#.Net本地互操作(Native Interoperability) 有两种方式：(仅Windows) COM交互(跨平台) Platform Invoke, P/Invoke既然我们期望是能跨平台，那么自然选择P/Invoke。P/Invoke引用动态库的方式非常简单：[DllImport("nativedep")] static extern int ExportedFunction();这样就声明好了一个引用函数，动态库名为nativedep，函数名为ExportedFunction。Windows上，搜索库的顺序是：nativedepnativedep.dllLinux / macOS上，搜索库的顺序是：nativedep.so / nativedep.dyliblibnativedep.so/libnativedep.dylibnativedeplibnativedep在C#中，同样有struct与class类型。但它的定义与C++里面并不相同。粗略来讲，本质上C#的struct与C++的struct/class相同，都是值类型，而C#的class是引用类型，更接近于C++的shared_ptr概念，但是生命周期由GC管理，区别于C++的作用域，并且由GC来解决循环引用的问题。 类型传参生命周期内存布局DLL传参类型C++ struct值类型拷贝作用域/new+deleteSequential C++ class值类型拷贝作用域/new+deleteSequential C# struct值类型拷贝作用域SequentialC++ structC# class引用类型引用GC管理，自动回收AutoC++ pointerC#的struct类型相比class还有一个问题是：没有构造函数和析构函数的概念。因此，如果希望使用RAII释放CppString的资源，那么还是需要使用classC#和C语言交互过程中，常用类型转换（Marshal）的关系如下，引用自官方文档[1]：C#Cbyteuint8\_tintint32\_tuintuint32\_tlongint64\_tulonguint64\_tchar (CharSet.ANSI)charchar (CharSet.Unicode)char16\_tclass [LayoutKind.Sequential]class的指针arrayarray起始指针首先需要实现一个自定义struct：CppString如下：using System; using System.Runtime.InteropServices; /// 两种实现方式： /// 1. 析构释放，由GC管理生命周期； /// 2. IDisposable，可以写成 using var cppStr = new CppString() 的形式，超出作用域后自动释放 /// 作为示例，这里都实现了一下 [StructLayout(LayoutKind.Sequential)] internal class CppString: IDisposable { public IntPtr str; public int len; public void Dispose() { if (str == IntPtr.Zero || len == 0) return; FreeCppString(this); } ~CppString() { if (str == IntPtr.Zero || len == 0) return; FreeCppString(this); } public override string ToString() { if (str == IntPtr.Zero || len == 0) { return string.Empty; } var bytes = new byte[len]; Marshal.Copy(str, buff, 0, len); return Encoding.UTF8.GetString(bytes); } } 需要用到的数据类型实现好了，那么接下来就是引入具体的接口。这里将接口统一封装在internal class DocApi内，使用internal声明表示这仅仅是在本namespace内生效，不希望外部使用这个class。当然，也可以放在后文的class Document内部。internal class DocApi { [DllImport("doc_api", CharSet = CharSet.Ansi)] public static extert IntPtr CreateDocument( [MarshalAs(UnmanagedType.LPArray)] byte[] title, int len); [DllImport("doc_api", CharSet = CharSet.Ansi)] public static extert void AppendLineInt(IntPtr doc, int number); [DllImport("doc_api", CharSet = CharSet.Ansi)] public static extert void AppendLineStr( IntPtr doc, [MarshalAs(UnmanagedType.LPArray)] byte[] str, int len); [DllImport("doc_api", CharSet = CharSet.Ansi)] public static extert void GetContent(CppString cppStr); [DllImport("doc_api", CharSet = CharSet.Ansi)] public static extert void FreeCppString(CppString cppStr); [DllImport("doc_api", CharSet = CharSet.Ansi)] public static extert void FreeDocument(IntPtr doc); }最后就是实现对外开放的C#对象Document：public class Document { private IntPtr _ptr; public Document(string title) { var bytes = Encoding.UTF8.GetBytes(title); _ptr = DocApi.CreateDocument(bytes, bytes.Length); } ~Document() { DpcApi.FreeDocument(_ptr); } void AppendLine(int number) { DocApi.AppendLineInt(_ptr, number); void AppendLine(string str) { var bytes = Encoding.UTF8.GetBytes(str); DocApi.AppendLineStr(_ptr, bytes, bytes.Length); } public string Content // Readonly { get { using var cppStr = new CppString(); DocApi.GetContent(_ptr, cppStr); return cppStr.ToString(); } } }4. 使用 C#.Net 调用动态库调用动态库的方式很简单，只要将动态库（*.dll, *.so, *.dylib放在可执行文件同级目录即可）。比较麻烦的是，我更希望将动态库放在同一个位置，无论是在IDE内运行Debug/Release调试运行，还是运行Testing，或是Publish发布时，都可以自动将该动态库拷贝过去。这样就需要对C#的工程文件做出一些修改了。本示例中，目录结构如下，动态库文件位于${Project}/doc_api/目录下tree -L 2 . ├── doc_api │ ├── doc_api.dll │ ├── libdoc_api.dylib │ └── libdoc_api.so ├── MyDocument │ ├── DocApi.cs │ ├── Document.cs │ └── MyDocument.csproj ├── MyDocumentTest │ ├── MyDocumentTest.cs │ └── MyDocumentTestTest.csproj ├── MyProgram │ ├── MyProgram.csproj │ └── Program.cs ├── MyProject.sln └── publish.bat打开MyDocument工程的MyDocument.csproj文件，内容如下： net6.0 enable enable 修改为如下内容，即可在每次build时自动将三个文件拷贝到可执行文件所在的目录，在Publish发布时同样会一起携带。（这里没有做具体系统的区分，因此会带上全部的动态库）。 net6.0 enable enable Always doc_api.dll Always libdoc_api.so Always libdoc_api.dylib 5. C#.Net 项目发布其实这一节本不必要，但多说一些也不影响。dotnet 可以交叉编译，可以编译出仅有一个可执行文件的程序（当然，本例中由于使用了动态库，除了可执行文件还得带一个动态库文件），并且生成的可执行程序无系统依赖，不需要执行平台安装dotnet运行时。以下是我常用的发布脚本，生成三个平台上的可执行二进制，供读者参考。cd /d %~dp0 rd /s /q bin rd /s /q publish dotnet publish -c Release -p:PublishTrimmed=true -r win-x64 -p:PublishSingleFile=true --self-contained true MyProgram\MyProgram.csproj dotnet publish -c Release -p:PublishTrimmed=true -r linux-x64 -p:PublishSingleFile=true --self-contained true MyProgram\MyProgram.csproj dotnet publish -c Release -p:PublishTrimmed=true -r osx-x64 -p:PublishSingleFile=true --self-contained true MyProgram\MyProgram.csproj mkdir publish mkdir publish\win-x64 publish\linux-x64 publish\osx-x64 echo f|xcopy /s/q/y MyProgram\bin\Release\net6.0\win-x64\publish publish\win-x64 echo f|xcopy /s/q/y MyProgram\bin\Release\net6.0\linux-x64\publish publish\linux-x64 echo f|xcopy /s/q/y MyProgram\bin\Release\net6.0\osx-x64\publish publish\osx-x64参考文章：Platform Invoke (P/Invoke)C++/C# interoperability
为指定类型定制不同实现——C++模板特化（上）
0. 何为模板特化C++模板最普遍的用法是为一个（编译期确定的）未知类型定义类或者函数。但有时会遇到这样一种情况：这些通用的实现并不满足某些特定类型下的需求。这时，就需要使用C++模板特化，为指定类型定义一个不同的实现。C++的模板特化有两种：全特化（可用于类或函数）以及偏特化（仅能用于类）。全特化意味着，该实现的所有模板参数都已经被确定，对应的，偏特化表示其中只有部分模板参数是确定的。本篇将只介绍全特化，后者使用场景不多，等下一篇再展开介绍。2. 全特化 in action2.1. 定义一个使用场景在工程中比较底层的代码中，经常会遇到需要做特殊处理的模板类型。例如，定义一个通用的to_str<T>()函数，或者定义一个lesser_than<T>(a, b)，不同类型有着不同的处理方式。本篇以实现通用的to_str<T>(v)函数为例，简单介绍全特化的使用方法。2.2. 定义一个通用的实现首先我们定义一个通用的函数如下：// to_str.h template<typename T> std::string to_str(const T& t) { std::ostringstream oss; oss << "trival value: [" << t << "]"; return oss.str(); }内容很简单，无非是把typename T类型使用std::ostringstream进行导出处理（关于<<操作符在后面会有解释，见第3节）。现在，我们可以在任何地方，只要引用了这个文件，就可以做一些简单的转字符串操作了： std::cout << "Trival Values:" << std::endl; std::cout << to_str(32) << std::endl; std::cout << to_str("const char *") << std::endl; std::cout << to_str(34.56) << std::endl;2.3 声明并定义一个全特化实现但假如我们有这样一个类，很明显是无法直接使用这个函数进行to_str()操作的：// my_special_class.h class MySpecialClass { public: int val1; int val2; explicit MySpecialClass(int val1, int val2) : val1(val1), val2(val2) { } };2.3.1. 普通实现首先我们声明一个定义如下：// to_str.h template<> std::string to_str<MySpecialClass>(const MySpecialClass& t);有三点可能会引起注意的地方：template<>标明这是一个模板相关的实现；这里只进行了声明，但并没有做任何定义；模板特化声明要放在通用类型的声明与定义之后，否则编译器不认识这个声明。这时，我们将在头文件以外实现它的定义，此处放到to_str.cpp中：// to_str.cpp #include "to_str.h" #include "my_special_class.h" template<> std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); }这样我们就可以如下使用to_str()函数了： MySpecialClass spclass{ 12, 34 }; std::cout << to_str(spclass) << std::endl;2.3.2. inline实现普通实现中，可以注意到，它像一个普通的函数一样，将声明与定义分拆到了.h头文件和.cpp源文件中。为什么不直接将如下定义像模板通用实现一样放到.h头文件中呢？// to_str.cpp template<> std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); }原因是，已经全特化的函数就等于一个普通的函数，在头文件中进行定义会遇到那个经典的“重定义”问题。解决方法也很简单，要么如前一小节中所述，拆分声明与定义，要么在函数声明中加入inline关键字，这也表示着“使用该定义做展开”：// to_str.h template<> inline std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); }3. 对<<操作符的一点解释在本示例中，还有一种方法，可以不使用全特化，但仍然可以用to_str模板函数，方法就是为MySpecialClass定义<<操作符。这里暂时不展开太多内容，将来可能会另外记录一下各种操作符的使用经验。4. 完整示例代码// to_str.h #pragma once #ifndef TO_STR_H_ #define TO_STR_H_ #include <string> #include <sstream> class MySpecialClass; template<typename T> std::string to_str(const T& t) { std::ostringstream oss; oss << "trival value: [" << t << "]"; return oss.str(); } template<> std::string to_str<MySpecialClass>(const MySpecialClass& t); template<> std::string to_str<std::string>(const std::string& t) { return "[" + t + "]"; } #endif // !TO_STR_H_ // my_special_class.h #pragma once #ifndef MY_SPECIAL_CLASS_H_ #define MY_SPECIAL_CLASS_H_ #include <string> #include <sstream> class MySpecialClass { public: int val1; int val2; explicit MySpecialClass(int val1, int val2) : val1(val1), val2(val2) { } std::string str() const { std::ostringstream oss; oss << "val1: [" << val1 << "], val2: [" << val2 <<"]"; return oss.str(); } }; #endif // !MY_SPECIAL_CLASS_H_ // to_str.cpp #include "to_str.h" #include "my_special_class.h" template<> std::string to_str<MySpecialClass>(const MySpecialClass& t) { return t.str(); } // main.cpp #include <iostream> #include "to_str.h" #include "my_special_class.h" int main(int argc, char** argv) { MySpecialClass spclass{ 12, 34 }; std::cout << "Trival Values:" << std::endl; std::cout << to_str(32) << std::endl; std::cout << to_str("const char *") << std::endl; std::cout << to_str(34.56) << std::endl; std::cout << std::endl; std::cout << "Template Specialization:" << std::endl; std::cout << to_str(std::string("string val")) << std::endl; std::cout << to_str(spclass) << std::endl; return 0; }结果如下：
0. 背景最近折腾腾讯云轻量香港服务器，然而公司内网由于一些特殊设置，无法直接SSH到境外服务器上，不得已通过自己一个位于北京的VPS节点进行中转，经过一些查阅整理，总结了几个小技巧。1. VSCode Remote通过中转节点连接目标服务器VSCode的远程开发套件在『使用Docker作为C++开发环境：适用于CLion与VSCode的配置』里略有介绍。配置起来很简单，但如果需要使用中转怎么办呢？（以Windows场景为例）例如我有一个1.1.1.1作为中转节点，有一个2.2.2.2是我的目标节点。那么需要点左下角打开远程开发工具，选择这里编辑.ssh_config：Host jump_box HostName 1.1.1.1 User user Host target HostName 2.2.2.2 User ProxyCommand C:\Windows\System32\OpenSSH\ssh.exe jump_box netcat -w 120 %h %p这里的原理是使用jump_box的netcat进行转发，只要提前在1.1.1.1和2.2.2.2上的~/.ssh/authorized_keys里添加好本机的SSH Public Key即可。2. Windows Terminal中配置SSH Keep-aliveWindows Terminal由微软开发，是Windows下非常好用、几乎超过所有其他命令行的一个终端工具。在Windows Terminal中，可以添加一个默认操作的链接选项，添加如下配置，效果如上图所示。 "list": [{ "guid": "{b453ae62-4e3d-5e58-b989-0a998ec44144}", "name": "graueneko.com", "tabTitle": "graueneko.com", "commandline": "ssh -o ServerAliveInterval=60 ubuntu@graueneko.com", "cursorShape": "filledBox", "icon": "ms-appx:///ProfileIcons/{574e775e-4f2a-5b96-ac1e-a2962a402336}.png" }, ... ]由于网络中间常常有些中转服务器在链接太久没有数据传输时强制中断掉，因此为避免这种情况，这里使用了`ssh -o ServerAliveInterval=60"来为SSH每60秒发送一次心跳包，保持SSH有数据传输避免被中断。3. Windows Terminal配色在Windows Terminal的settings.json中，可以添加如下配色选项（使用的是OneDark Theme），让命令行更好看些：{ "schemes": [ { "name": "OneHalfDark", "black": "#282c34", "red": "#e06c75", "green": "#98c379", "yellow": "#e5c07b", "blue": "#61afef", "purple": "#c678dd", "cyan": "#56b6c2", "white": "#dcdfe4", "brightBlack": "#282c34", "brightRed": "#e06c75", "brightGreen": "#98c379", "brightYellow": "#e5c07b", "brightBlue": "#61afef", "brightPurple": "#c678dd", "brightCyan": "#56b6c2", "brightWhite": "#dcdfe4", "background": "#282c34", "foreground": "#dcdfe4" } ], }添加的位置如下：4. SSH连接接后加载颜色文件我很喜欢腾讯云自带的~/.bashrc文件配色，效果如下，可执行文件为绿色，文件夹为蓝色，普通文件是白色，正常软链接为青色，出错的软链接为红色，等等：没有这个~/.bashrc配置的同学可以使用这里的文件（我直接把它抄过来了）# ~/.bashrc: executed by bash(1) for non-login shells. # see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) # for examples # If not running interactively, don't do anything case $- in *i*) ;; *) return;; esac # don't put duplicate lines or lines starting with space in the history. # See bash(1) for more options #HISTCONTROL=ignoreboth # append to the history file, don't overwrite it shopt -s histappend # for setting history length see HISTSIZE and HISTFILESIZE in bash(1) #HISTSIZE=1000 HISTFILESIZE=2000 # check the window size after each command and, if necessary, # update the values of LINES and COLUMNS. shopt -s checkwinsize # If set, the pattern "**" used in a pathname expansion context will # match all files and zero or more directories and subdirectories. #shopt -s globstar # make less more friendly for non-text input files, see lesspipe(1) [ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" # set variable identifying the chroot you work in (used in the prompt below) if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then debian_chroot=$(cat /etc/debian_chroot) fi # set a fancy prompt (non-color, unless we know we "want" color) case "$TERM" in xterm-color|*-256color) color_prompt=yes;; esac # uncomment for a colored prompt, if the terminal has the capability; turned # off by default to not distract the user: the focus in a terminal window # should be on the output of commands, not on the prompt #force_color_prompt=yes if [ -n "$force_color_prompt" ]; then if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then # We have color support; assume it's compliant with Ecma-48 # (ISO/IEC-6429). (Lack of such support is extremely rare, and such # a case would tend to support setf rather than setaf.) color_prompt=yes else color_prompt= fi fi if [ "$color_prompt" = yes ]; then PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' else PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' fi unset color_prompt force_color_prompt # If this is an xterm set the title to user@host:dir case "$TERM" in xterm*|rxvt*) PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" ;; *) ;; esac # enable color support of ls and also add handy aliases if [ -x /usr/bin/dircolors ]; then test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" alias ls='ls --color=auto' #alias dir='dir --color=auto' #alias vdir='vdir --color=auto' alias grep='grep --color=auto' alias fgrep='fgrep --color=auto' alias egrep='egrep --color=auto' fi # colored GCC warnings and errors #export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' # some more ls aliases alias ll='ls -alF' alias la='ls -A' alias l='ls -CF' # Add an "alert" alias for long running commands. Use like so: # sleep 10; alert alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' # Alias definitions. # You may want to put all your additions into a separate file like # ~/.bash_aliases, instead of adding them here directly. # See /usr/share/doc/bash-doc/examples in the bash-doc package. if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases fi # enable programmable completion features (you don't need to enable # this, if it's already enabled in /etc/bash.bashrc and /etc/profile # sources /etc/bash.bashrc). if ! shopt -oq posix; then if [ -f /usr/share/bash-completion/bash_completion ]; then . /usr/share/bash-completion/bash_completion elif [ -f /etc/bash_completion ]; then . /etc/bash_completion fi fi但是仅仅这样还是不能显示颜色，问题在于，SSH连接时并不会加载~/.bashrc文件，只会加载~/.bash_profile，因此要在~/.bash_profile中添加如下内容：if [ -f ~/.bashrc ]; then . ~/.bashrc fi5. 添加HOSTS方便跳转机器我现在使用一台国内节点作为所有国外节点的中转服务器，但是每一台都有不同的IP，不能一一记住，而且它们即使绑定了域名也很长。为了简化操作，可以在HOSTS里添加别名进行中转。例如在/etc/hosts中：127.0.0.1 localhost 1.1.1.1 jump 2.2.2.2 targ这样配置以后，可以直接使用ssh user@targ的方式连接2.2.2.2服务器。
如何使用C++与虚函数表愉快地玩耍并掉进坑里
0. 背景知识虚函数表不必多说，是每位C++学习者都要面对的问题。简单来说，这是C++面向对象的基础，保证子类指针转换为父类指针类型后，调用成员函数时仍然是子类的成员函数。听上去有点绕，但如果这一部分理解不了，还请在继续阅读前，先去学习一下虚函数相关内容。虚函数表大家都知道，是存放每个对象所有虚函数的指针的一块内存区域，指针指向真正的虚函数代码。《深入理解C++对象模型》一书中讲到：同一个Class下的所有对象共享同一个虚函数表；子类会继承父类的虚函数表（并不是直接使用，而是复制一份），并在此基础上做拓展；带有虚函数表的对象大小最小（空对象）为32 or 64 bit (4 or 8 Byte)，也就是虚函数表指针的大小；与之相反的，无虚函数的空对象最小为1 Byte作为占位；调用虚函数时，相比直接调用成员函数多了一步，首先是通过对象头的虚函数表地址找到对应虚函数地址，然后才是调用对应的虚函数。有必要备注一下：第3条虽然书中说是编译器相关的，但经过测试发现，Clang/GCC/MSVC都是这么做的，因此可以视这为现行标准之一。说了这么多，虚函数表有什么可以玩的东西呢？先别急，我们再看一下override与final关键词。在成员函数声明中，override关键词表示覆写父类虚函数，且可以继续被后续子类覆写；而final关键词表示覆写父类虚函数，且禁止被后续子类覆写。这两个关键词存在时，均暗含了“此函数为virtual”的意思，因此额外声明一个“virtual”时IDE会提示冗余。那么我们开始和虚函数表玩耍吧，玩什么呢？交换对象的虚函数表。1. 示例代码[scode type="blue"]示例代码可以在64位机型上直接运行，且尽可能简化以突出主题，希望读者如果有不清楚的地方，最好还是自己亲自尝试一下。[/scode]#include <cstdio> #include <cstdint> // 引入uint64_t // 我们从定义三个类开始，分别为A，B，C，且B继承A，C继承B class A { public: virtual void Print() { printf("p - A, "); } virtual void PrintWithOverride() { printf("po - A, "); } virtual void PrintWithFinal() { printf("pf - A\r\n"); } }; class B : public A { public: virtual void Print() { printf("p - B, "); } virtual void PrintWithOverride() override { printf("po - B, "); } void PrintWithFinal() override { printf("pf - B\r\n"); } }; class C : public B { public: void Print() { printf("p - C, "); } void PrintWithOverride() override { printf("po - C, "); } void PrintWithFinal() final { printf("pf - C\r\n"); } }; int main() { A *a = new A(); B *b = new B(); C *c = new C(); // 获取虚函数表指针 uint64_t avpt = *(uint64_t *) a; uint64_t bvpt = *(uint64_t *) b; uint64_t cvpt = *(uint64_t *) c; // 打印每个类里，虚函数表和三个虚函数的指针 printf("VPT & VFuncs: \r\n"); printf("A: %p - %p, %p, %p\r\n", avpt, *(uint64_t *)avpt,*((uint64_t *)avpt+1),*((uint64_t *)avpt+2)); printf("B: %p - %p, %p, %p\r\n", bvpt, *(uint64_t *)bvpt,*((uint64_t *)bvpt+1),*((uint64_t *)bvpt+2)); printf("C: %p - %p, %p, %p\r\n", cvpt, *(uint64_t *)cvpt,*((uint64_t *)cvpt+1),*((uint64_t *)cvpt+2)); // 打印正常情况下的输出 printf("\r\nBefore swap:\r\n"); printf("A: "); a->Print(); a->PrintWithOverride(); a->PrintWithFinal(); printf("B: "); b->Print(); b->PrintWithOverride(); b->PrintWithFinal(); printf("C: "); c->Print(); c->PrintWithOverride(); c->PrintWithFinal(); printf("(A)B: "); ((A*)b)->Print(); ((A*)b)->PrintWithOverride(); ((A*)b)->PrintWithFinal(); printf("(A)C: "); ((A*)c)->Print(); ((A*)c)->PrintWithOverride(); ((A*)c)->PrintWithFinal(); /* * 输出： Before swap: A: p - A, po - A, pf - A B: p - B, po - B, pf - B C: p - C, po - C, pf - C (A)B: p - B, po - B, pf - B (A)C: p - C, po - C, pf - C */ /* Magic: 我们在这里交换三个对象的虚函数表 * C -> A * A -> B * B -> C */ *(uint64_t *) a = cvpt; *(uint64_t *) b = avpt; *(uint64_t *) c = bvpt; printf("Swap:\r\nA <- C\r\nB <- A\r\nC <- B\r\n"); // 打印并测试交换完成后的结果 printf("After swap:\r\n"); printf("A: "); a->Print(); a->PrintWithOverride(); a->PrintWithFinal(); printf("B: "); b->Print(); b->PrintWithOverride(); b->PrintWithFinal(); printf("C: "); c->Print(); c->PrintWithOverride(); c->PrintWithFinal(); printf("(C)A: "); ((C*)a)->Print(); ((C*)a)->PrintWithOverride(); ((C*)a)->PrintWithFinal(); printf("(B)C: "); ((B*)c)->Print(); ((B*)c)->PrintWithOverride(); ((B*)c)->PrintWithFinal(); /* * 输出： After swap: A: p - C, po - C, pf - C B: p - A, po - A, pf - A C: p - B, po - B, pf - C (C)A: p - C, po - C, pf - C (B)C: p - B, po - B, pf - C * 这里会发现：B->C之后，C打印出来的pf仍然为C * 尽管交换虚表这一操作完全是未定义的行为，但我们可以注意到， * 编译器在处理final时使用了特殊的方式。 */ /* * 那我们使用直接从虚表中调用PF会怎么样呢？ * 值得注意的是，这里如果打印虚函数的取地址，取到的实际是虚函数在表中的索引值 */ typedef void (C::*VFP)(); VFP cpf=(VFP)&C::PrintWithFinal; printf("\r\n%d",&C::PrintWithFinal); printf("\r\nCall C::PF via VT reference: \r\n"); (c->*cpf)(); /* * 输出： 17 Call C::PF via VT reference: pf - B */ return 0; }