【C++与C区别】嵌入式C改C++的一些浅显区别(大佬勿进)

前言

概述

C++和C语言有许多显著的区别,主要体现在编程范式、特性和应用场景上。以下是一些关键区别:

  1. 编程范式

    • C语言:面向过程的编程语言,强调通过函数调用来实现程序逻辑。
    • C++:面向对象的编程语言,支持类和对象,强调封装、继承和多态性。
  2. 内存管理

    • C语言:使用mallocfree函数进行动态内存分配和释放。
    • C++:除了mallocfree,还引入了newdelete运算符来进行动态内存管理。
  3. 数据结构

    • C语言:使用struct来定义数据结构,但不能包含函数。
    • C++:扩展了struct,允许包含函数,并引入了class,默认访问权限为private
  4. 函数特性

    • C语言:不支持函数重载和缺省参数。
    • C++:支持函数重载和缺省参数,允许同名函数根据参数类型和数量的不同进行区分。
  5. 命名空间

    • C语言:没有命名空间的概念。
    • C++:引入了命名空间(namespace),可以避免命名冲突。
  6. 异常处理

    • C语言:不支持异常处理,错误处理通常通过返回值和错误码来实现。
    • C++:支持异常处理机制,可以使用try, catchthrow来处理异常。
  7. 模板

    • C语言:不支持模板。
    • C++:支持模板,可以编写通用的函数和类。

语言差异与设计差异

有人认为C和C++的差别主要在面向对象和内存分配。但虽然C语言是面向对象的语言,嵌入式中常常进行静态实例化,但不代表C语言不能面向对象,这并不是语言的差异,而是设计上的差异,使用者思想的差异。

总的来说,C语言是最接近硬件的高级语言,几乎能做到与汇编代码一一对应。而C++则更偏向上层,对底层进行了一些封装。
最典型的例子是C++的引用(&),使得用户减少了对指针和内存地址的操作,以达到减少直接对硬件操作的目的。

Linux 之父 Linus Torvalds 曾经评价 C++:

C++ is a horrible language. It’s made more horrible by the fact that a lot of substandard programmers use it, to the point where it’s much much easier to generate total and utter crap with it.

可见,C++的抽象化程度更高,对于使用者的要求更高,不合格的使用者可能会用得一塌糊涂。

嵌入式编译器对C++标准的支持

  1. Arm Compiler 5 (AC5):支持C++98标准。
  2. Arm Compiler 6 (AC6):支持更现代的C++标准,包括C++11、C++14和C++17。
  3. IAR编译器:支持C++17全部特性。

由于一些特性需要很大的开销,比如RTTI、异常处理,因此嵌入式C++大多数情况下使用的是C++的子集。
下面以一个嵌入式框架QP的QP/C和QP/C++为例体会一下C和C++的差别(完全符合C++98标准)。

QP/C与QP/C++差异

struct与class

封装是面向对象的一个特性,简单地说就是把数据和操作数据的方法绑定在一起形成一个独立的单元,即类(class)。
由于C语言中没有类的概念,因此都是用struct实现的。
C:

typedef struct {
    QSignal sig;
    uint8_t poolId_;
    uint8_t volatile refCtr_;
} QEvt;

void QEvt_ctor(QEvt * const me,
    enum_t const sig);

C++:

class QEvt {
public:
    QSignal sig;
    std::uint8_t poolId_;
    std::uint8_t volatile refCtr_;

    explicit QEvt(QSignal s) noexcept
    : sig(s)
    {}

    constexpr QEvt(
        QSignal s,
        std::uint8_t /* dummy */) noexcept
    : sig(s),
      poolId_(0U),
      refCtr_(0U)
    {}

    virtual ~QEvt() noexcept {
    }
};

其实C++中的 struct 也做了一些面向对象的拓展。但一般情况下:

用struct表示一个数据结构
用class表示一个类

namespace

名字空间(namespace)更加强化了作用域的概念。
C++比C多了两种名字空间和类作用域,最直观的感受是函数名和变量名的前缀变短了:
C:

extern char const QP_versionStr[8];
bool QHsm_isIn(QHsm * const me, QStateHandler const state);

C++:

namespace QP {
	constexpr char const versionStr[] {QP_VERSION_STR};
}
class QHsm{
	bool isIn(QStateHandler const s) noexcept;

}

匿名namespace起到内部链接作用,不具名的名字空间意味着除了本文件,其他文件再也无法获取其中的定义了:

// unnamed namespace for local definitions with internal linkage
namespace {

    Q_DEFINE_THIS_MODULE("qep_msm")

    // top-state object for QMsm-style state machines
    QP::QMState const l_msm_top_s = {
        nullptr,
        nullptr,
        nullptr,
        nullptr,
        nullptr
    };
}

enum指定数据类型

C语言的enum的size不定,并且作用域为全局,常常被宏定义取代,非常“鸡肋”。
而C++中enum能够指定数据类型,再加上作用域的限制
C:

enum QReservedSig {
    Q_EMPTY_SIG,     /*!< signal to execute the default case */
    Q_ENTRY_SIG,     /*!< signal for coding entry actions */
    Q_EXIT_SIG,      /*!< signal for coding exit actions */
    Q_INIT_SIG,      /*!< signal for coding initial transitions */
};

C++:

enum ReservedSig : QSignal {
    Q_EMPTY_SIG,  //!< signal to execute the default case
    Q_ENTRY_SIG,  //!< signal for entry actions
    Q_EXIT_SIG,   //!< signal for exit actions
    Q_INIT_SIG    //!< signal for nested initial transitions
};

bool变量

C语言没有bool(C99有_Bool)。
bool类型只有true(1)和false(0)两个值。由于非0的任何值都是真,因此同为真的两个表达式不一定相等,bool类型解决了这个问题。

类型转换

C语言的类型转换是(type)var
C++推荐的是函数式type(var)(类型转换构造函数),以及模版函数cast。
C:

#define Q_EVT_CAST(class_) ((class_ const *)(e))

C++:

#define Q_EVT_CAST(subclass_) (static_cast<subclass_ const *>(e))

static_cast:自然的类型转换,向上造型,万能指针void*

QState qm_tran_hist(QMState const * const hist, void const * const tatbl) noexcept
{
    m_state.obj  = hist;
    m_temp.tatbl = static_cast<QP::QMTranActTable const *>(tatbl);
    return Q_RET_TRAN_HIST;
}

dynamic_cast:向下造型,涉及RTTI,嵌入式尽量不使用。
reinterpret_cast:重新解释位模式的低级别转换,强制类型转换(类似于C语言),不安全

int a;
	float f1 = *static_cast<float*>(&a); // 报错,int*不能static转换为float*,因为不安全
	float f2 = *reinterpret_cast<float*>(&a); // 允许不安全指针转换
bool isIn(QStateHandler const state) noexcept override {
    return reinterpret_cast<QHsm *>(this)->QHsm::isIn(state);
}

删除默认函数

有一些“特种成员函数”,C++编译器会自动生成,如果不想使用自动生成的函数,就需要删除函数。这些函数包括:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。

class MyClass {
    // 如果没有显式定义,编译器会自动生成以下函数:
    // MyClass();
    // MyClass(const MyClass&);
    // MyClass(MyClass&&);
    // MyClass& operator=(const MyClass&);
    // MyClass& operator=(MyClass&&);
    // ~MyClass();
};

QP中例子:

class QTimeEvt{
    //! private default constructor only for friends
    //!
    //! @note
    //! private default ctor for internal use only
    QTimeEvt();

    //! private copy constructor to disallow copying of QTimeEvts
    QTimeEvt(QTimeEvt const & other) = delete;

    //! disallow copying of QP::QTimeEvt
    QTimeEvt & operator=(QTimeEvt const & other) = delete;
}; // class QTimeEvt

引用&

关于函数的传参,C++除了按值传递、按址传递,还有引用传参。
按C语言的思维,引用传参会导致表面上是值传递,实际上是址传递。

#include <iostream>

// 按值传递
void passByValue(int num) {
    num = 5;
}

// 按址传递
void passByPointer(int *numPtr) {
    *numPtr = 5;
}

// 引用传参
void passByReference(int &numRef) {
    numRef = 5;
}

int main() {
    int num1 = 10;
    int num2 = 10;
    int num3 = 10;

    passByValue(num1);
    std::cout << "After passByValue, num1 = " << num1 << std::endl;  // 输出结果为10,原始值未被修改

    passByPointer(&num2);
    std::cout << "After passByPointer, num2 = " << num2 << std::endl;  // 输出结果为5,原始值被修改

    passByReference(num3);
    std::cout << "After passByReference, num3 = " << num3 << std::endl;  // 输出结果为5,原始值被修改

    return 0;
}

引用还涉及左值引用,右值引用,移动语义,引用折叠,完美转发等语言特性。

内联函数

C:显示inline。
C++:成员函数在类定义中被定义(函数体写在.hpp中)即可。
相较于C,C++编译器会更积极地展开内联函数,尤其是类成员函数,因此C++更倾向使用内联函数代替宏函数。

typedef和using

建议使用C++的新关键字using=取代typedef,为变量类型取别名。
using除了能取代typedef还有其他更丰富的作用)
C:

typedef uint8_t QSignal;

C++:

using QSignal = std::uint8_t;

try catch与noexcept

C++具有异常处理机制,但开销较大。声明noexcept的函数不进行异常检查,出现异常调用std::terminate后终止执行。
原则上只要不会发生异常,就应该加上noexcept
某些嵌入式编译器可以禁止异常处理,无法使用try catch
比如C语言除0可能会触发段错误(也可能不),C++可以用try catch捕获这个异常。

#include <iostream>
#include <stdexcept> // 包含标准异常类

int main() {
    int num1 = 10;
    int num2 = 0;

    try {
        // 如果分母为0,抛出一个runtime_error异常
        if (num2 == 0) {
            throw std::runtime_error("Division by zero error");
        }
        std::cout << "Result of division: " << num1 / num2 << std::endl;
    } catch (const std::exception& e) {
        // 捕获异常并打印错误信息
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

const

相比于C语言,C++的const关键字还能修饰类成员函数,表示该成员函数不修改任何成员变量。

void const * getAct() const noexcept {
    return m_act;
}

const与constexpr

constexprconst的限制性更强,要求必须在编译期就已经确定,而const可以在运行时初始化。

int getValue() {
    return 42; // 运行时计算
}
const int x = getValue(); // 只能用 const,不能用 constexpr

constexpr修饰函数表示,如果所有参数都是constexpr,那么返回值也是constexpr
在 C++11中,constexpr函数不得包含多于一个可执行语句,即一条return语句。

//! QP::QEvt constructor (overload for static, immutable events)
constexpr QEvt(
    QSignal s,
    std::uint8_t /* dummy */) noexcept
: sig(s),
  poolId_(0U),
  refCtr_(0U)
{}

只要有可能使用constexpr,就使用它。

函数重载(overload)、重写(override)、覆盖(hide)

这三个概念都是C++独有的特性。

  • 重载是指在同一个作用域内,可以有多个同名函数,但它们的参数列表(参数的类型、个数或顺序)必须不同。
  • 重写是指在派生类中重新定义基类中的虚函数。重写的函数必须与基类中的函数具有相同的函数签名(包括返回类型、函数名和参数列表)。重写是多态的外在表现。
  • 覆盖是指在派生类中定义一个与基类中同名的函数,但不一定具有相同的参数列表或返回类型。覆盖的函数会隐藏基类中的同名函数。
#include <iostream>
using namespace std;

// 基类
class Base {
public:
    // 重载:同名函数,参数不同
    void func(int x) {
        cout << "Base::func(int) called with " << x << endl;
    }
    void func(double x) {
        cout << "Base::func(double) called with " << x << endl;
    }

    // 虚函数,用于重写
    virtual void display() {
        cout << "Base::display() called" << endl;
    }

    // 非虚函数,用于隐藏
    void show() {
        cout << "Base::show() called" << endl;
    }
};

// 派生类
class Derived : public Base {
public:
    // 重载:同名函数,参数不同
    void func(int x, int y) {
        cout << "Derived::func(int, int) called with " << x << " and " << y << endl;
    }

    // 重写:覆盖基类的虚函数
    void display() override {
        cout << "Derived::display() called" << endl;
    }

    // 隐藏:隐藏基类的非虚函数
    void show() {
        cout << "Derived::show() called" << endl;
    }
};

int main() {
    Base base;
    Derived derived;

    // 调用重载函数
    base.func(10);         // 调用 Base::func(int)
    base.func(10.5);       // 调用 Base::func(double)
    derived.func(10, 20);  // 调用 Derived::func(int, int)

    // 调用重写函数
    Base* ptr = &derived;
    ptr->display();        // 调用 Derived::display()

    // 调用隐藏函数
    base.show();           // 调用 Base::show()
    derived.show();        // 调用 Derived::show()

    return 0;
}

如何把C语言代码改成C++

首先语法上,C++是C语言的超集,只需要加上extern "C"即可告诉编译器按照C语言的链接规则来处理被声明的函数或变量。

// example.h
#ifdef __cplusplus
extern "C" {
#endif


#ifdef __cplusplus
}
#endif

但这其实也说明C和C++其实是两种完全不同的语言,只不过C++有能力调用C编译器编译出的函数和变量。
从语言本身看,C是面向过程的,但这并不代表用C语言编写的代码都是面向过程的。要想将C项目转变为C++项目,并不是语法上的表面工作,而是要用面向对象的方式改写代码,是最核心的内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苏打豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值