在嵌入式等存储紧张的环境中,C++ 解释器通常比 C 语言解释器多占用约 500 KB 存储空间。为了压缩体积、降低成本并提高运行效率,把原本用 C++ 编写的源程序改写为 C 语言,是很有必要的。
C++ 与 C 最大的区别就是类及其相关特性。因此,将 C++ 改写为 C,问题就转化成了如何“去类”。
两种基本思路
方法有两种:
- 第一种,直接去掉 C++ 里的面向对象特征,先完整理解源代码逻辑,再动手改写;
- 第二种,在 C 中保留面向对象的部分特征,用结构体实现类的功能。
第一种方式适合类数量很少的项目。当类数量较多时,如果还要求完全理解源码再重写,既耗时又容易出错。要是面对大型项目,想通读并理解全部逻辑几乎不可能。
如果程序中有 140 多个类,更适合用第二种方法。可以一个类一个类地改,本身没有太高难度;只要不是笔误,几乎不会出错。更重要的是,这种改法基本不用完整理解程序逻辑。可能改完之后,你仍然不太清楚程序具体实现了什么功能。这里并不是说完全不用理解业务,而是强调这种改法对程序逻辑本身的依赖度很低。
下面围绕 C++ 的常见特性,以及它们在 C 中如何实现或替代,做初步探讨。
先做一组约定:
- 函数
Ixx 为类 xx 的构造函数实现;
- 原类的成员函数改为前缀为“结构体名 +
_”的函数;
- 函数指针
U 为原类析构函数的声明;
U + 结构体名称为原类析构函数的实现;
Fun_ + 结构体名为对该结构体成员函数指针进行指向;
- 后面遇到上述情况不再重复说明。
类的成员函数和数据成员
struct 没有成员访问权限控制。如果非要做访问控制,就必须额外增加机制,这会显著复杂化程序,所以只能放弃访问权限控制。
- 类的数据成员,可以直接转成 C 结构体的数据成员。
- 函数则需要转成对应的函数指针,因为
struct 里不允许出现函数声明和定义。virtual、inline 等修饰符也要去掉。例如,void funca(int a); 改为 void (*funca)(struct B *p, int a);。可以看到,函数指针原型里多了一个 struct B 指针,这是因为在函数内部操作类成员时,要靠这个指针指定结构体成员。类的成员函数里,其实也隐含了一个指向自身的 this 指针。
- 静态成员必须定义成全局变量或全局函数,因为结构体中不能有静态成员。
类的构造函数
类在实例化时会调用默认构造函数。在 struct 中,可以定义一个同名函数指针,指向承担构造函数功能的初始化函数。和构造函数不同的是,初始化函数还要加入函数指针初始化语句。
创建结构体变量时,要使用 malloc 而不是 new,并且要手动调用初始化函数。
class A
{
public:
A();
~A();
void func(int a);
private:
int b;
};
A::A()
{
b=0;
}
void A::func(int a)
{
b=a;
}
typedef struct classA A;
struct classA
{
void (*A)(struct classA *p);//构造函数指针
void (*U)(struct classA *p);//析构函数指针
void (*func)(struct classA *p,int a);
int b;
};
void fun_A(A *p)
{
p->func=classA_func; //将函数指针初始化
}
void IA(A *p) //构造函数,命名规则在类名前加I
{
fun_A(p);
p->b=0; //原构造函数所作部分
}
void classA_func(A *p,int a)
{
p->b=a;
}
使用处可以这样写:
A *s=(A*)malloc(sizeof(A));
s->A=IA;
s->A(s);
类的析构函数
析构函数负责释放对象占用的资源。在 C 里,无论哪个 struct 都用函数指针 U 来替代析构函数。
之所以统一用 U,是因为当子类指针赋给基类指针后,基类指针释放时不必关心具体析构函数名,只需调用成员函数 U 即可。U 也要像普通成员函数一样,在 fun_类名() 函数中指定。
类的析构函数由系统自动调用;而在 C 中必须显式调用,并准确判断调用时机。
类的拷贝构造函数
拷贝构造函数主要用于提升以下三类场景中类的构建速度:
- 对象作为参数传给函数,如
additem(Item a);
- 对象作为函数返回值;
- 实例化类时传入对象参数。
这三种情况下,系统会调用拷贝构造函数,而不是普通构造函数。
注意:C = D; 不会调用拷贝构造函数,它走的是重载 = 运算符的路径。
由于 C 中定义 struct 变量时使用的都是指针,不会用到拷贝构造函数,所以暂不考虑。原来函数参数或返回值需要类变量的,要全部转成类指针。实例化类时作参数的情况,可以另外定义一个带参数的构造函数解决。
类的内联函数和虚函数
inline、virtual 修饰符要全部去掉。内联函数体也要提出来,在外面定义成普通函数。
原代码:
class B
{
...
virtual void funb();
inline int add() const {return a+b;};
private:
int a;
int b;
...
}
改为:
typedef classB B;
struct classB
{
…
void (*funb)(struct classB *p);
int (*add)(struct classB *p);
int a;
int b;
};
void classB_funb(B *p)
{
…
}
int classB_add(B *p)
{
return p->a+p->b;
}
void fun_classB(B *p)
{
…
p->funb=classB_funb;
p->add= classB_add;
}
重载
类中的重载分为函数重载和运算符重载。
- 函数重载:函数名相同,但参数个数或参数类型不同。调用时会根据传入参数匹配具体函数。在 C 里只能给不同版本起不同名字。
- 运算符重载:主要是为了延续运算符的书写习惯,同时避免误用。C 不支持运算符重载,可以定义一个函数来承担这个能力。
类的继承
单继承
如果类之间存在继承关系,先把基类按一般类的改法处理好,然后把基类定义部分完整拷贝到子类前部。
除了将基类构造函数名改为子类构造函数名外,不要改动基类定义部分。还要在构造函数里调用基类构造函数。如果子类覆盖了基类的函数,要把对应函数指针重定向到子类函数,以保留继承带来的动态联编特性。
继承关系往往复杂且多变。为了保证基类在所有子类中唯一、方便修改,最好的做法是把基类结构体部分做成宏,在子类中直接使用。
多继承
个人建议尽量不用多继承,它容易引入多条继承路径的问题。除非是出于方便编程,比如继承接口等情况。
多继承也可以改:把多个基类的成员全部拷到子类里。遇到重名成员,就在前面加前缀区分。这里说的是基类之间重名;如果是派生类和基类重名,则会覆盖基类。
其他
上述内容是 C++ 与 C 差异较大、较常用的特性及修改方法。
像模板等特性,主要是为了复用代码。C 语言没有模板,只能手动写多个函数分别实现。
另外,参数列表中的 & 符号要用指针替代,缺省值也要去掉,并在调用时显式补上原来的缺省值。