正式化C模式:C ++的情况
#c #cpp

背景

在线,我看到了C和C ++的很多话语。这说得通;两种语言都是相关的,它们适合非常相似的领域。 c程序员通常将C ++描述为肿,过复杂的,钝的等等。

Here's one of the most famous examples of a C programmer arguing against C++, by one of the most famous programmers of all time: Linus Torvalds。从Torvalds的咆哮中可以看出,这些讨论中缺少的一个关键作品是具体的技术论证。尽管Torvalds比我的程序员要好得多,但他的论点具有带有无关示例的充电语言。

本文是记录我对主题的想法,明确指出具体论点的一种方式,并且是将来我回顾的时间胶囊。

我的看法

c ++可以做C可以做的一切,但相反的不是真的。

在在线讨论中,人们倾向于忘记C ++是C的自然演变。如果C是Bulbasaur,则C ++是Ivysaur。因为C ++不是稀薄的空气,并且因为它不打算替换C,C ++借口,正式化并改善了许多常见的C技术和范式。

正式化?

c ++不是在真空中创建的。它是经过多年的C编程而创建的,这意味着C ++在有效的模式和没有什么模式方面具有很多经验。它将这些设计模式移至正式语言功能中,而不是让用户手工滚动。

这意味着好的C代码往往是良好C ++代码的更原始版本。

通用对象-C

想象我想创建一个库来表示两个元素向量。因此,我创建了一个包含两个整数元素的结构:

typedef struct vec2_t
{
    int x; 
    int y;
} Vec2;

真棒,我现在可以代表我的代码中的向量。

但是,等等,定义一些常见操作是否有用?

对于简洁起见,我们只能实现加法,但是您会得到这个想法:

Vec2 add(Vec2 v1, Vec2 v2)
{
    Vec2 result = {0, 0};
    result.x = v1.x + v2.x;
    result.y = v1.y + v2.y;
    return result;
}

但是,等等,如果我也想支持浮子怎么办?还是长int?还是双打?

我可以做一些void* voodoo,但是宏也可以在这里工作。

#define DECL_VEC2(typename, type) \
typedef struct typename##_t { \
    type x, y;\
} typename; \
\
typename typename##_add(typename v1, typename v2) { \
    typename result;\
    result.x = v1.x + v2.x;\
    result.y = v1.y + v2.y;\
    return result;\
}

甜,我们创建了一个通用数据结构来代表两个元素向量。

此仍然存在问题;那BigInt呢?还是自定义,定点类型?突然之间,我需要更改我的添加操作功能以接受第三个add函数参数。

让我们确定是什么使此良好的C代码:

  • 我们有一个很好的界面与我们的结构交互
  • 我们的结构是通用的,这意味着我们可以重复使用代码而无需重复其他类型的代码。

通用对象-C ++

c ++看着正确的方法,并通过语言和类型系统正式化了它。

正确地做我们在C代码中所做的事情的正确方法将看起来像这样:

template <typename T>
struct Vec2
{
    T x, y;

    static Vec2<T> add(Vec2<T> v1, Vec2<T> v2)
    {
        Vec2<T> result;
        result.x = v1.x + v2.x;
        result.y = v1.y + v2.y;
        return result;
    }
};

在此示例中,我们提供了编译器来执行安全性的帮助,并且我们不必处理宏。 C ++将通用对象模式形式化为内置语言功能。

我们也不需要担心将add函数传递给我们的代码。为什么?因为存在运算符过载,并且应为传递的类型定义+运算符。使用特征,我们甚至可以扩展代码以检查T是否在编译时间内实现了操作员并报告错误。甜。

清理 - c

与普遍的信念相反,在C中,goto实际上非常有用。

当我们处理大量内存分配时,通常我们需要一种方法来释放已达到错误状态的函数:

void some_func(const char* filename)
{
    FILE* file = fopen(filename, "r");
    if (!file)
        goto no_cleanup;

    fseek(file, 0L, SEEK_END);
    int file_size = ftell(file);

    char* buffer = malloc(file_size);
    if (!buffer)
        goto cleanup_file;
    FILE* out_file = fopen("outfile", "w");
    if (!out_file)
        goto cleanup_buffer;

    // no errors, free to continue with operations

cleanup_buffer:
    free(buffer);
cleanup_file:
    fclose(file);
no_cleanup:
    return;
}

在这里,我们以某种方式组织了代码,如果遇到任何问题,我们将正常执行释放到清理代码中。没有goto,如果语句,我们的代码将嵌套一团糟,每次添加新资源时都会增加深度。

清理-C ++

c ++再次形式化了一个良好的C模式。在这种情况下,形式化的模式称为raii。

在C ++中,我们可以使用RAII在其一生结束时清理对象。

void some_func(const std::string& filename)
{
    std::fstream file(filename);
    if (!file)
        return;

    std::string buffer;

    std::fstream out_file("outfile");
    if (!out_file)
        return;

    // do stuff
}

请注意,我们没有任何清理标签。这是因为(形成良好的,内存拥有的)对象具有自动处理清理的破坏者。如果我们在do stuff区域中有错误状态并且需要纾困,我们知道我们的代码仍将清理我们的资源。

数据隐藏-C

读取文件时,具有动态的字符串数组很有用,每个索引代表文件的一行。让我们看一个简单的C示例:

typedef struct dynamic_string_array_t {
    int size;
    int capacity;
    char** arr;

} DynamicStringArray;

DynamicStringArray* DynamicStringArray_new(int capacity)
{
    DynamicStringArray* dyn_arr = malloc(sizeof(DynamicStringArray));

    dyn_arr->size = 0;
    dyn_arr->capacity = capacity;
    dyn_arr->arr = malloc(sizeof(char*) * capacity);

    return dyn_arr;
}

bool DynamicStringArray_push_back(DynamicStringArray* this, char* str)
{
    if (this->size == this->capacity)
    {
        int new_size = this->capacity * 2;
        this->arr = realloc(this->arr, new_size);
        this->capacity = new_size;

        if(!this->arr)
            return false;
    }
    this->arr[this->size] = str;
    this->size++;
    return true;
}

当然,我们需要定义其他功能以在我们的动态数组上操作,但这是一个很好的例子。

存在一个问题:没有什么可以阻止另一个程序员通过更改字段来滥用我们的动态数组。如果他们直接更改我们结构中的任何字段,我们将遇到潜在的UB或彻底崩溃。

“隐藏”我们的数据的最幼稚方法是不隐藏所有数据。取而代之的是,我们可以以单个下划线为前缀,以表明该字段应被视为私有。但是,这无效。

隐藏数据的“最佳”方法是使我们的结构成为不透明的结构。不透明的结构是通过仅通过指针访问的完全隐藏所有字段的结构。

为了实现这一目标,我们将标题文件中的struct名称typedef保留在源文件中。

dynamicsTringarray.h:

typedef struct dynamic_string_array_t DynamicStringArray;

DynamicStringArray* DynamicStringArray_new(int);
boolean DynamicStringArray_push_back(DynamicStringArray*, char*);

dynamicsTringarray.c:

typedef struct dynamic_string_array_t {
    int size;
    int capacity;
    char** arr;

} DynamicStringArray;

// rest of our function implementations

数据隐藏-C ++

在C ++中,默认方式很容易:只需将字段标记为私有,或者在类中,请不要将字段标记为公开。这允许我们的代码成为标题,这可能很不错。

class DynamicStringArray
{
    int size;
    int capacity;
    char** arr;

public:
    // define methods here
};

您看到错误了吗?

在“数据隐藏-C”部分中,我故意留下了一个错误。我直接将指针存储到数组中,而不是分配空间并复制char*参数的内容。这是一个错误;不能保证字符串得到正确管理。如果我们的动态数组超出了字符串,则指针将指向垃圾记忆。如果字符串未终止终止,那也会导致潜在的错误。

在C ++中,我们通常不必担心字符串处理问题。非参考对象参数会自动调用其各自的复制构造函数,这意味着我们不必担心复制字符串。最重要的是,std::string管理着我们的字符串内容,这意味着我们不必担心内部数组是否为无效终止(除非从char*构造*)。

包起来

列表继续进行。我可以谈论:

  • 信号和errno,返回代码与异常
  • 自定义指针和大小结构与跨度
  • 位设置宏vs std :: bitset
  • 编译时间设施
  • char vs std :: string
  • 等,等等。

但是,在那时,本文将变成一本简短的书。

我想重申我的早期观点:C ++可以做C可以做的一切。如果您对任何这些功能都有质疑,则没有什么迫使您使用它们。 C ++功能 opt ;如果您不想使用它们,就不会受到惩罚。即使您仅使用1个C ++功能,也值得开关。