使用Thrift的网络框架搭建一般性网络应用

Idea的提出

Thrift 存在的一些问题:

  • 相比于protobuf,Thrift的序列化和反序列化性能表现欠佳,大概比protobuf慢10倍。
  • 相比于其他RPC框架,Thrift拥有优秀的底层通信框架。(作者简单比较过thrift和grpc1.0的通信框架,grpc的设计实在太过简单。)

由此提出猜想和假设:

  • 将 Thrift 的底层通信框架抛离出Thrift框架,利用其来构建一般性的网络应用。
  • 组合 Thrift 的底层通信框架 和 protobuf序列化协议,使之成为一个新的RPC框架。

从实现难度和工作量上的考虑,本文尝试实现第一个假设,“将 Thrift 的底层通信框架抛离出Thrift框架,利用其来构建一般性的网络应用”。第二个假设希望日后,作者在时间和精力富余的时候再进行试验。

使用Thrift的网络框架搭建一般性网络应用的优点

  • 快速搭建网络应用,节省时间成本
  • 当Thrift协议序列化和反序列化成为系统性能瓶颈时,可对其进行替换,同时又能保留Thrift的网络框架,减少对上下游系统的影响

如何操作

有两种方法:

  • 在IDL文本中,将自定义协议的结构体存为一个thrift的string变量。
  • 构建自定义的Processor类

下面对这两种方法做详细介绍:

在IDL文本中,将自定义协议的结构体存为一个thrift的string变量

举例:

namespace cpp com.thrift.test

struct Parameter{
    1: required string bin_data;
}

service DemoService{
    i32 demoMethod(1:string param1, 2:Parameter param2);
}

将新的协议序列化后的数据放入bin_data中,这种方法缺点是,自己定义的协议,还要被thrift的序列化反序列协议包裹,不能完全消除thrift序列化和反序列化的代价。

第一种方法太过简单和粗糙,因此经过挖掘thrift代码后,探索出了更精细的方法。

构建自定义的Processor类

Thrift 底层通信模块的四大基类,TServer、TProcotol、TProcessor、TTransport,其中TProcessor::process是负责处理具体业务逻辑入口。

class TProcessor {
 public:
  virtual ~TProcessor() {}

  virtual bool process(boost::shared_ptr<protocol::TProtocol> in, 
                       boost::shared_ptr<protocol::TProtocol> out) = 0;

  bool process(boost::shared_ptr<apache::thrift::protocol::TProtocol> io) {
    return process(io, io);
  }

 protected:
  TProcessor() {}
};

因此,只要自定义实现TProcessor的基类,重写process方法,就能自定义自己的网络应用。

下面是一个Hello world应用的简单实现:

首先实现一个HelloWorldProcessor 类。’

class HelloWordProcessor : public apache::thrift::TProcessor {
public:
  virtual bool process(boost::shared_ptr<apache::thrift::protocol::tprotocol> in, boost::shared_ptr</apache::thrift::protocol::tprotocol><apache::thrift::protocol::tprotocol> out) {
    out->writeBinary("Hello world!");
    out->getTransport()->flush();
    out->getTransport()->close();
    GlobalOutput.printf("send bytes %s", "Hello world!");
    return true;
  }
};

然后构建main函数,本实例使用TSimpleServer模型

using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::processor;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;
int main(int argc, char **argv) {
boost::shared_ptr<tprotocolfactory> protocolFactory(new TBinaryProtocolFactory());
boost::shared_ptr<tprocessor> processor(new UwsgiProcessor());
boost::shared_ptr<tservertransport> serverTransport(new TServerSocket(9090));
boost::shared_ptr<ttransportfactory> transportFactory(new TBufferedTransportFactory());
TSimpleServer server(processor,
  serverTransport,
  transportFactory,
  protocolFactory);
printf("Starting the server...\n");
server.serve();
printf("done.\n");
return 0;
}

最后编译、链接和运行。

简单实现一个socket客户端,发送请求,就能得到HelloWord。

性能测试

待完善

Thrift 底层通信框架的优化和调优

待完善

本文小结

作者写本文,正是在工作中遇到了一些真实的问题,比如thrift序列化反序列化慢,thrift底层那么优秀的通信框架如何更高的加以利用呢?因此带着工作中的一些问题,开始阅读thrift的源代码。

除了本文中的一些实例,作者还做了一个小的代码库,里面就用到了本文中的方法,单独使用了thrift了网络框架,Github地址如下:https://github.com/zuocheng-liu/GI

STL容器 vector list 性能对比(附带测试方法和测试数据)

最近在重构公司的一个C++模块,逻辑里有排序、过滤等操作。开发过程中,遇到下面的一些问题:

问题和猜想

  • 单链表的归并排序线性表的快速排序的时间复杂度都是 O(nlog(n)),但在实际情况中哪个更快呢?需要测试一下。
  • 猜想的发散,stl vector 和 list(双向链表)的迭代器遍历、顺序插入、clear操作, 时间复杂度好像都是相同的,但谁更快呢?
  • 看stl代码去研究vector和list的实现,并不能得到我们最终想要的结果,测试数据会更直观。

测试总结

先写测试总结,测试方法、测试结果都在下面。

  • 迭代器遍历,list 比vector 稍块,使用for_each 比使用for更快。
  • 顺序插入, vector比list 约快3倍
  • clear操作,vector 几乎不耗时,list 要耗费好多时间,vector比list至少快1千倍以上
  • 排序, vector 大约比list 快2倍。

结合项目,对比vector和list的性能

  • 在我重构前,模块使用的是vector存储数据,排序使用的是快排,过滤则是用临时数据vector转存。
  • 在我重构后,使用list存储,排序使用list的归并排序,过滤则是直接使用list的erase操作。
  • 重构前,耗时4000微秒,重构后1500微秒,时间减少60%。
  • list除了删除操作和遍历操作,排序和插入都比vector慢,但是我使用list后,却提升了性能。原因可能有2个: 1.单个元素数据结构大,vector移动这些元素代价较大,而list移动这些元素反而代价很小;2.去掉了中间临时存储,数据的转存代价也比较大。
  • 使用list后,模块的可扩展性也变得更好了。

这次项目最大的收获就是,如果有过滤操作,优选使用list。vector的缺点如下,1.vector快速排序需要移动元素,如果元素占据空间大,移动代价也非常大。2.vector过滤需要借助中间临时存储,直接erase的代价更大。

测试方法

  • 随机生成10万数据量,由vector和list结构的变量分别存储
  • 对vector 和 list 的数据分别做如下, 1)迭代器遍历 2)顺序插入 3) clear操作 4)排序
  • 记录每种操作消耗的时间
  • 多次测试,记录典型的数据结果

测试结果

直接列出测试结果,单位是微秒

vector for_each : 3263
list for_each : 2556
vector traverse : 4783
vector num traverse : 666
list traverse : 3078
vector push_back : 8136
list push_back : 24581
list delete : 7428
vector push_back : 7329
list push_back : 22968
vector clear : 0
list clear : 13079
vector sort : 89512
list sort : 146529

(附)测试程序

Github 托管地址,保持更新:https://github.com/zuocheng-liu/code-samples/blob/master/linux-c/stl/vector_list_performance_test.cpp

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include <list>
#include <vector>
#include <iostream>

#define MAX_NUM 100000

using namespace std;

timeval tv;
uint64_t timer;


bool NumCmp(uint32_t a, uint32_t b) { return (a > b); }

void startTimer() {
    gettimeofday(&tv,NULL);
    timer = 1000000 * tv.tv_sec + tv.tv_usec;
}

uint64_t stopTimer() {
    gettimeofday(&tv,NULL);
    timer = 1000000 * tv.tv_sec + tv.tv_usec - timer;
    return timer;
}

int main() {
    vector<uint32_t> v;
    vector<uint32_t> v2;
    list<uint32_t> l;
    list<uint32_t> l2;
    for (int i = 0; i< MAX_NUM; ++i) {
        srand((int)time(0)  * i * i + 1);
        int num = rand();
        v.push_back(num);
        l.push_back(num);
    }

    // compare oper traverse
    startTimer();   
    for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
        *iter;
    }
    cout<<"vector\t traverse\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (int i = 0 ; i < MAX_NUM; ++ i) {
        //v[i];
    }
    cout<<"vector\t num traverse\t :\t"<< stopTimer() << endl;  


    startTimer();   
    for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
    }
    cout<<"list\t traverse\t :\t"<< stopTimer() << endl;  


    // compare oper push_back   
    startTimer();   
    for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
        v2.push_back(*iter);
    }
    cout<<"vector\t push_back\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
        l2.push_back(*iter);
    }
    cout<<"list\t push_back\t :\t"<< stopTimer() << endl;  

    // compare oper delete
    startTimer();
    v2.clear();
    for (vector<uint32_t>::iterator iter = v2.begin(); iter != v2.end(); ++ iter) {
    //    v2.erase(iter);
    }
    //cout<<"vector\t delete\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (list<uint32_t>::iterator iter = l2.begin(); iter != l2.end(); ++ iter) {
        iter = l2.erase(iter);
    }
    cout<<"list\t delete\t :\t"<< stopTimer() << endl;  

    // compare oper push_back   
    startTimer();   
    for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
        v2.push_back(*iter);
    }
    cout<<"vector\t push_back\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
        l2.push_back(*iter);
    }
    cout<<"list\t push_back\t :\t"<< stopTimer() << endl;  


    // compare oper clear
    startTimer();   
    v2.clear();
    cout<<"vector\t clear      \t:\t"<< stopTimer() << endl;  

    startTimer();   
    l2.clear();
    cout<<"list\t clear     \t :\t"<< stopTimer() << endl;  

    // compare oper sort
    startTimer();   
    std::sort(v.begin(), v.end(), NumCmp);
    cout<<"vector\t sort    \t :\t"<< stopTimer() << endl;  

    startTimer();   
    l.sort(NumCmp);
    cout<<"list\t sort    \t :\t"<< stopTimer() << endl;  

    return 0;
}

Google C++ 代码风格示例

最近项目中决定使用Google的C++代码规范,然后写了两个代码示例文件(.h 和 .cc),这两个文件中的代码都是用Google的C++代码规范。

写示例代码的好处

  • 减少查看文档时间,降低学习成本
  • 帮助程序员快速了解Google的代码风格的概貌
  • 用于参考和模仿,帮助快速上手

代码托管地址

下面地址保持持续更新:

https://github.com/zuocheng-liu/code-samples/tree/master/code_style

仓促之作,错误和疏忽不可避免,但保证后续不断更新。

初版代码示例

建议点击上面的链接,获取最新版。编辑器的原因,缩进的显示可能有问题。

// Copyright (c) 2016, Zuocheng.net
//
// Author: Zuocheng Liu
//
// Lisence: GPL
//
// File: code_samples/code_style/google_code_style.h 
// (文件名要全部小写,可以包含下划线(_)或短线(-))
// C++文件以.cc 结尾,头文件以.h 结尾。
//
// Google C++ code Style 代码示例 (简体中文版)
//
// *注释  使用//或/* */,但要统一。
// *注释  每一个文件版权许可及作者信息后,对文件内容进行注释说明
// -------------------------------------------------------------最多不能超过80行
#ifndef CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE_
#define CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE_
// 命名空间的名称是全小写的,其命名基于项目名称和目录结构
// 不要声明命名空间std 下的任何内容
namespace code_samples {
namespace code_style {

// **变量命名**
// 尽可能给出描述性名称,不要节约空间
// 变量名一律小写,单词间以下划线相连
// 少用全用变量,可以以g_与局部变量区分
int g_my_exciting_global_variable;

// 只有在描述数据时用struct ,其他情况都用class
typedef struct CodeStyle {
    uint32_t type;
} CodeStyle;

// **类注释** 
// 每个类的定义要附着描述类的功能和用法的注释
//
// GoogleCodeStyle , 通过代码示例,展现google c++ code style
//
// (按需注明synchronization assumptions,是否线程安全)
class GoogleCodeStyle {
public :

  // ** 声明次序 **
  // - typedefs 和enums;
  // - 常量;
  // - 构造函数;
  // - 析构函数;
  // - 成员函数,含静态成员函数;
  // - 数据成员,含静态数据成员。

  // **类型命名**
  // 每个单词以大写字母开头,不包含下划线
  // 所有类型命名——类、结构体、类型定义(typedef)、枚举——使用相同约定
  typedef enum StyleType {
      // 枚举值应全部大写,单词间以下划线相连
      GOOGLE_CODE_STYLE = 0,
      K_AND_R,
      POCO
  } StyleType;

  // 常量命名,在名称前加k
  const int kDaysInAWeek = 7;   

  GoogleCodeStyle() {}
  ~GoogleCodeStyle() {}
  // **函数注释**
  // 函数声明处注释描述函数功能,定义处描述函数实现.
  // - inputs(输入)及outputs(输出);
  // - 对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数;
  // - 如果函数分配了空间,需要由调用者释放;
  // - 参数是否可以为NULL;
  // - 是否存在函数使用的性能隐忧(performance implications);
  // - 如果函数是可重入的(re-entrant),其同步前提(synchronization assumptions)
  // 举例 :
  // 初始化函数
  ~Init() {}

  // 普通函数名以大写字母开头,每个单词首字母大写,没有下划线
  // 
  uint32_t MyExcitingMethod(CodeStype &code_style, char *output);
  // 存取函数要与存取的变量名匹配
  inline int num_entries() const { return num_entries_; }
  inline void set_num_entries(int num_entries) { num_entries_ = num_entries; }

  // 类的成员变量以下划线结尾
  int num_completed_connections_;

protected :

private :
  // **类数据成员**
  // 每个类数据成员(也叫实例变量或成员变量)应注释说明用途。
  // 如果变量可以接受NULL 或-1, 等警戒值(sentinel values),须说明之
  int num_entries_;

}; // class GoogleCodeStyle

}} // namespace code_samples::code_style 

#endif //CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE

线程安全的单例模式-以C++代码为例

本文描述3种场景下的单例模式:

  • 进程体内无线程的单例模式
  • 进程体内多线程单例模式
  • 在单个线程体中的单例模式

本文所写单例模式代码都使用懒汉模式。

进程体内单例

注意问题:

  • 如果进程体中运行多个线程,则需要考虑多线程同步访问的问题。
  • 如果进程体重没有运行多个线程,则不需要考虑多线程同步访问。
  • 使用线程同步锁保证多进程同步

使用场景举例 :

  • 日志类、文件读写类
  • 资源管理类

代码示例:

进程体内没有运行多线程的单例模式,无需考虑线程同步与互斥

class Singleton {
  public:
    static Singleton* getInstance() {
        if (NULL == instance) {
          instance = new SingletonInside();
        }
        return instance;
    }
  private:
    SingletonInside(){}
    ~SingletonInside() {}
    static Singleton* instance;
};

Singleton::instance = NULL;    

进程体内运行多线程单例模式,使用系统mutex保证线程安全

class Singleton {
  public:
    static Singleton* getInstance() {
        pthread_once(&g_once_control, InitOnce);
        pthread_mutex_lock(&mutex);  // lock
        if (NULL == instance) {
          instance = new SingletonInside();
        }
        pthread_mutex_unlock(&mutex); // unlock
        return instance;
    }
  private:
    SingletonInside() {

    }
    ~SingletonInside() {
       pthread_mutex_destroy(&mutex);   // destroy lock
    }
    static void InitOnce(void) {
      pthread_mutex_init(&mutex,NULL);  // init lock
    }
    Singleton* instance;
    static pthread_once_t g_once_control;
    static pthread_mutex_t mutex;
};
Singleton::instance = NULL;
pthread_once_t Singleton::g_once_control = PTHREAD_ONCE_INIT;

单个线程体中的单例

某些资源在单个线程体内需要保持单例,即每个线程体内都保持唯一。每个线程体内,对象相互隔离,则无需考虑线程安全问题。

此种单例模式的实例需要调用系统提供的 TLS 接口,放于线程局部存储中。

使用场景举例:

  • 多路复用Socket封装类
  • 网络上下文环境类
  • 线程安全的资源

代码示例

class Singleton {
  public:
    static Singleton* getInstance() {
       pthread_once(&g_once_control, InitOnce);
       Singleton* instance = (Singleton*)pthread_getspecific(g_thread_data_key);

        if (NULL == instance) {
          instance = new SingletonInside();
          pthread_setspecific(g_thread_data_key, (void*)Singleton)
        }
        return instance;
    }
  private:
    SingletonInside() {}
    ~SingletonInside() {
       pthread_key_delete(g_thread_data_key);
    }
    static void InitOnce(void) {
      pthread_key_create(&g_thread_data_key, NULL);
    }

    static pthread_once_t g_once_control;
    static pthread_key_t g_thread_data_key;
};

pthread_once_t Singleton::g_once_control = PTHREAD_ONCE_INIT;
pthread_key_t Singleton::g_thread_data_key;

如果使用 Poco库 TreadLocal ,代码还会简洁很多,性能上也会好很多

    class Singleton {
      public:
        static Singleton* getInstance() {
            if (NULL == instance) {
              instance = new SingletonInside();
            }
            return instance;
        }
      private:
        SingletonInside() {}
        ~SingletonInside() {
           delete instance;
        }
        static Poco::ThreadLocal<Singleton> instance;
    };
Poco::ThreadLocal<singleton> Singleton::instance = NULL;

总结

  • 无论程序是多进程运行还是多线程运行,代码都要尽量兼容线程安全

多线程编程的陷阱

依据《Java 并 发编程实践》/《Java Concurrency in Practice》一书,一个线程安全的 class 应当满足三个条件:

  • 从多个线程访问时,其表现出正确的行为
  • 无论操作系统如何调度这些线程,无论这些线程的执行顺序 如何交织
  • 调用端代码无需额外的同步或其他协调动作

不安全的多线程编程容易造成哪些后果

  • 内存泄露
  • 资源、数据访问冲突、数据不一致
  • 进程空间用尽
  • 计算资源耗尽

* 错误处理不当导致整个进程异常结束

数据访问

  • 冲突
  • 不一致

多线程间共享的标志变量, 可能被编译器优化,存储于cpu寄存器,一个线程将其值改变(内存值),在另一个线程的cpu寄存器对应的值却未同步。

解决方法: 1. 添加 volatile 关键字修饰变量。 并且加互斥锁。 2.不使用共享的标志变量。

内存空间使用的安全性

  • 防止内存泄露,小心使用堆空间

  • 小心使用栈空间

逻辑的安全性

  • 参数检查一定要严格,任何一个不合法的参数能

  • 容错

内存泄露原因

软件的稳定性

  • 如果一个线程体挂掉

  • 悬空指针

释放时设置为NULL 声明定义时设置为NULL

线程空间限制

  • 所有线程受限于进程的空间
  • 系统会限制线程的栈空间(默认是10M)

在大规模软件设计中,线程数量的设定和栈空间的使用必须要小心。

高效使用栈空间技巧

  • 如果使用大的内存空间,则到堆里去申请
  • 不使用递归算法,而使用循环
  • 更加细致地拆分逻辑或模块,让函数体量更小
  • 复杂数据类型(类或者结构体)做为函数参数时,尽可能传递它们的指针或引用,以节省栈空间

多线程下的设计模式

资源竞争与互斥

  • 单例模式

循环中不使用wait

多线程下的全局变量、资源

线程安全

语言特性 valatile

内存泄露

和多进程编程相同的陷阱

spawn-fcgi 源码分析

梗概

本文内容对Spawn-fcgi源码进行解读,简要说明其原理,并具体说明其实现方式。

Spawn-fcgi 源码虽然只有600多行,但是初次阅读起来依然需要花很多时间。为了节省读者的学习成本,提高学习Spawn-fcgi 的效果,作者对Spawn-fcgi的源码做了裁剪,保留最核心的功能和原有的代码结构,且能编译后正常运行。最后代码只有200多行。

源码地址在这里

必备知识

要阅读Spawn-fcgi,读者至少需要掌握以下几个方面的知识或技能:

  • 了解CGI和FastCgi的概念,了解其使用场景
  • 基础的Linux C 环境编程,会使用常见的库函数比如getopt、exec 等
  • 基础的Linux C 的多进程编程,熟悉fork、waitpid、setsid等函数
  • 基础的Linux C 网络编程,熟悉建立tcp连接、select非阻塞方式通信,多路复用I/O等

裁剪后Spawn-fcgi的执行过程

  • 创建服务器socket
  • fork进程,子进程初始化会有两个主要操作:
  1. 把socket的文件描述符,复制到FCGI_LISTENSOCK_FILENO
  2. 会执行execl 函数,运行cgi程序,并让cgi程序拥有子进程的上下文环境

    运行cgi程序后,使用FCGI_LISTENSOCK_FILENO这个描述符,来与webserver进行通信。

如何编译

gcc -o spawn-fcgi spawn-fcgi.c

一行命令即可

如何调用

./spawn-fcgi -f cgi -p 9001 -F 256

裁剪后也仅支持接收这三个参数

作者对于Spawn-fcgi的思考

  • fastcgi 协议规定,fcgi管理器中把网络描述符定为FCGI_LISTENSOCK_FILENO,为了一致CGI程序中复用FCGI_LISTENSOCK_FILENO的套接字,总感觉不是很完美。
  • Spawn-fcgi太简单,不需要单独做一个软件,完全可以集成到cgi程序中
  • Spawn-fcgi使用的是多进程,如果集成到cgi程序中,可以自由选择多进程模型、多线程模型
  • 缺少进程守护监控,spawn-fcgi如果一个进程挂掉,不会被重启。
  • Spawn-fcgi 网络多路复用调用的是select,但现在最常用的是epoll

Spawn-fcgi 补充知识

与原版相比,裁剪后Spawn-fcgi的失去了哪些功能

  • 使用linux套接字文件建立tcp连接功能
  • 对IPv6的支持
  • 对root、group 用户的检查
  • 对windows、Solaris等编译环境的支持
  • 通过进程pid文件获取cgi进程
  • 去除对autoconf等编译工具的依赖

C函数和C++函数相互调用

实例解说

这个例子,示例了两点

  • C 如何调用C++对象里的函数
  • C++ 如何调用C的函数

共两个文件,test.c 和 main.cpp,代码解释如下:

在main.cpp (C++ 代码) 里定义了一个类MyMath,类里有个成员函数sum ;如何让C能调用这个c++的函数MyMath::sum呢?

即在main.cpp 中添加extern C后,声明定义一个C的函数call_MyMath_sum。在test.c 中先声明这个函数,然后通过调用call_MyMath_sum,达到调用C++ MyMath::sum的作用。

在test.c 中,定义了一个sum 的函数。如何让C++能调用这个c的函数sum呢? 这么做的,在main.cpp中 extend “C” 后声明它,然后在main函数中直接调用就可以了。

代码有点绕,C和C++调来调去的,不过仔细看就容易明白。

起关键作用的就是 extent “C” 这个关键语句,它的作用是告诉C++编译器,把后面的语句当作C语言进行处理。

代码如下

C语言中的函数,其中调用了C++中的call_MyMath_sum:

test.c

int call_MyMath_sum (int, int); // 此函数定义在main.cpp中
int sum(int a , int b) {
    return call_MyMath_sum(a,b);
}

C++语言中的函数:

main.cpp

# include <iostream>
using namespace std;
extern "C" { 
    int sum(int , int);  // 声明sum函数,已经在test.c 中定义过
} 

class MyMath { 
  public : 
    static int sum(int , int); 
};

int MyMath::sum(int a, int b) { 
    return (a + b); 
} 

extern "C" int call_MyMath_sum (int a , int b) {  // 定义call_MyMath_sum , 使其可以被c的代码调用
    return (MyMath::sum(a,b)); 
} 
int main(void) { 
    cout< <sum(5,6); return 0;  // 此sum是 在test.c中定义的
} 

如何编译:

# Makefile 
main.o:
    g++ -c -o main.o main.cpp  # 注意是g++
test.o:
    gcc -c -o test.o test.c  # 注意是gcc
main: main.o test.o
    g++ -o main main.o test.o # 最后链接用的是g++
all: main
clean:
    rm -f test.o main.o

执行 make 得到可执行文件main