找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4622

积分

0

好友

606

主题
发表于 昨天 02:02 | 查看: 10| 回复: 0

在嵌入式等存储紧张的环境中,C++ 解释器通常比 C 语言解释器多占用约 500 KB 存储空间。为了压缩体积、降低成本并提高运行效率,把原本用 C++ 编写的源程序改写为 C 语言,是很有必要的。

C++ 与 C 最大的区别就是类及其相关特性。因此,将 C++ 改写为 C,问题就转化成了如何“去类”。

两种基本思路

方法有两种:

  • 第一种,直接去掉 C++ 里的面向对象特征,先完整理解源代码逻辑,再动手改写;
  • 第二种,在 C 中保留面向对象的部分特征,用结构体实现类的功能。

第一种方式适合类数量很少的项目。当类数量较多时,如果还要求完全理解源码再重写,既耗时又容易出错。要是面对大型项目,想通读并理解全部逻辑几乎不可能。

如果程序中有 140 多个类,更适合用第二种方法。可以一个类一个类地改,本身没有太高难度;只要不是笔误,几乎不会出错。更重要的是,这种改法基本不用完整理解程序逻辑。可能改完之后,你仍然不太清楚程序具体实现了什么功能。这里并不是说完全不用理解业务,而是强调这种改法对程序逻辑本身的依赖度很低。

下面围绕 C++ 的常见特性,以及它们在 C 中如何实现或替代,做初步探讨。

先做一组约定:

  • 函数 Ixx 为类 xx 的构造函数实现;
  • 原类的成员函数改为前缀为“结构体名 + _”的函数;
  • 函数指针 U 为原类析构函数的声明;
  • U + 结构体名称为原类析构函数的实现;
  • Fun_ + 结构体名为对该结构体成员函数指针进行指向;
  • 后面遇到上述情况不再重复说明。

类的成员函数和数据成员

struct 没有成员访问权限控制。如果非要做访问控制,就必须额外增加机制,这会显著复杂化程序,所以只能放弃访问权限控制。

  1. 类的数据成员,可以直接转成 C 结构体的数据成员。
  2. 函数则需要转成对应的函数指针,因为 struct 里不允许出现函数声明和定义。virtualinline 等修饰符也要去掉。例如,void funca(int a); 改为 void (*funca)(struct B *p, int a);。可以看到,函数指针原型里多了一个 struct B 指针,这是因为在函数内部操作类成员时,要靠这个指针指定结构体成员。类的成员函数里,其实也隐含了一个指向自身的 this 指针。
  3. 静态成员必须定义成全局变量或全局函数,因为结构体中不能有静态成员。

类的构造函数

类在实例化时会调用默认构造函数。在 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 中必须显式调用,并准确判断调用时机。

类的拷贝构造函数

拷贝构造函数主要用于提升以下三类场景中类的构建速度:

  1. 对象作为参数传给函数,如 additem(Item a)
  2. 对象作为函数返回值;
  3. 实例化类时传入对象参数。

这三种情况下,系统会调用拷贝构造函数,而不是普通构造函数。

注意:C = D; 不会调用拷贝构造函数,它走的是重载 = 运算符的路径。

由于 C 中定义 struct 变量时使用的都是指针,不会用到拷贝构造函数,所以暂不考虑。原来函数参数或返回值需要类变量的,要全部转成类指针。实例化类时作参数的情况,可以另外定义一个带参数的构造函数解决。

类的内联函数和虚函数

inlinevirtual 修饰符要全部去掉。内联函数体也要提出来,在外面定义成普通函数。

原代码:

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;
}

重载

类中的重载分为函数重载和运算符重载。

  1. 函数重载:函数名相同,但参数个数或参数类型不同。调用时会根据传入参数匹配具体函数。在 C 里只能给不同版本起不同名字。
  2. 运算符重载:主要是为了延续运算符的书写习惯,同时避免误用。C 不支持运算符重载,可以定义一个函数来承担这个能力。

类的继承

单继承

如果类之间存在继承关系,先把基类按一般类的改法处理好,然后把基类定义部分完整拷贝到子类前部。

除了将基类构造函数名改为子类构造函数名外,不要改动基类定义部分。还要在构造函数里调用基类构造函数。如果子类覆盖了基类的函数,要把对应函数指针重定向到子类函数,以保留继承带来的动态联编特性。

继承关系往往复杂且多变。为了保证基类在所有子类中唯一、方便修改,最好的做法是把基类结构体部分做成宏,在子类中直接使用。

多继承

个人建议尽量不用多继承,它容易引入多条继承路径的问题。除非是出于方便编程,比如继承接口等情况。

多继承也可以改:把多个基类的成员全部拷到子类里。遇到重名成员,就在前面加前缀区分。这里说的是基类之间重名;如果是派生类和基类重名,则会覆盖基类。

其他

上述内容是 C++ 与 C 差异较大、较常用的特性及修改方法。

像模板等特性,主要是为了复用代码。C 语言没有模板,只能手动写多个函数分别实现。

另外,参数列表中的 & 符号要用指针替代,缺省值也要去掉,并在调用时显式补上原来的缺省值。




上一篇:嵌入式Linux构建系统怎么选?Buildroot与Yocto对比
下一篇:GNU C 与 ANSI C 九大关键差异:零长度数组、typeof 与内建函数
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-3 19:23 , Processed in 1.359775 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表