C++类与C不同的文件

C++ classes vs. C different files

本文关键字:文件 类与 C++      更新时间:2023-10-16

目前,在一个项目中,有一组明确的功能与明确的责任有关。因为责任是一个周期性的过程,每次迭代都需要缓冲区和计数器,所以我必须在每个周期后重置这些函数使用的变量。变量大多是静态的,因为每个周期函数被调用数千次。(它是一组数字FIR滤波器,每2分钟左右处理一次5秒的数据)。变量必须在文件作用域中声明,因为函数共享它们。例如,重置/初始化功能和实际过滤器功能。

到目前为止,整个项目都是用C语言编写的(但C++很容易得到支持,因为可能中断的部分已经包含"extern C{}")。为了使代码更简洁,我想将函数和变量分组在一个单独的实现文件中。当然,我也可以使用C++类,我想更多地使用这些类。

这些选项之间的本质区别是什么?

我的意思的简化示例:在这个示例中,我只是保留了程序的结构。例如,对于第一次迭代,Filter()函数在5秒内被调用1000次。然后,对于下一次迭代,在实际的Filter()函数调用之前调用Reset()函数,以重置所有使用的缓冲区。

// File-scope variables
static float   buffer[BUFFER_SIZE];
static uint8_t bufferOffset = 0;
// Filter
static float Filter (const float sample)
{
buffer[bufferOffset] = sample;
// Removed actual filter code here
return result;
}
// Reset functions
static void Reset (void)
{
memset(buffer, 0, sizeof(buffer));
bufferOffset = 0;
}

C中避免这些共享状态的常用方法是定义一个封装所有相关状态的结构,将其传递给每个函数并仅对其进行操作。

示例:

// buffer.h
#pragma once
// opaque data structure whose content
// isn't available to the outside
struct buffer;
// but you may allocate and free such a data structure
struct buffer *alloc_buffer();
void free_buffer(struct buffer *b);
// and you may operate on it with the following functions
float filter_buffer(struct buffer *b);
void reset_buffer(struct buffer *b);
void add_to_buffer(struct buffer *b, const float *data, size_t size);

来源如下:

// buffer.c
#include "buffer.h"
struct buffer {
float content[BUFFER_SIZE];
uint8_t offset;
}
struct buffer *alloc_buffer() {
return malloc(sizeof(struct buffer));
}
void free_buffer(struct buffer *b) {
free(b);
}
float filter_buffer(struct buffer *b) {
// work with b->content and b->offset instead
// on global buffer and bufferOffset
return result;
}
void reset_buffer(struct buffer *b) {
memset(b->content, 0, BUFFER_SIZE);
b->offset = 0;
}
void add_to_buffer(struct buffer *b, const float *data, size_t num) {
memcpy(b->content + b->offset, data, sizeof(float) * num);
b->offset += num;
}

因此,您可以避免全局状态,例如,这种状态极大地简化了代码的多线程应用程序。由于返回的是不透明的数据结构,因此可以避免泄露缓冲区内部结构的信息。

现在,您可以在不同的源文件中使用此数据结构:

#include "buffer.h"
int main() {
struct buffer *const b = alloc_buffer();
// b->content[0] = 1; // <-- error, buffer is an opaque data type and
//     you may only use the functions declared in
//     buffer.h to access and modify it
const float data[2] = { 3.1415926, 2.71828 }
add_to_buffer(b, data, sizeof(data) / sizeof(data[0]));
const float result = filter_buffer(b);
return 0;
}

为了回答您的问题:即使您可以将函数和全局状态进一步分离为几个编译单元,但最终您仍然有一个共享的全局状态。除了在某些特殊情况下,我认为这是一种代码气味。

上述解决方案或多或少地对应于C++解决方案。您定义了一个类,该类封装了一些状态和对其进行操作的方法。所有实例化的对象都是相互独立的。

声明static文件范围变量是最简单的私有封装形式。这种设计在嵌入式系统和硬件相关代码中特别常见。在单线程程序中,只有一个使用变量的模块/ADT实例("singleton模式"),这是完全可以的做法。

举一个简单的例子,这对您的具体情况来说应该很好。程序设计的艺术是知道何时添加额外的抽象层,何时避免它们。教书并不容易,这主要来自经验。

经验不足的程序员的一条经验法则是:如果你不确定如何使代码更抽象,那么就不要添加额外的抽象。它很可能造成的伤害远大于好处。


如果代码变得更加复杂,下一个抽象级别只是将其拆分为几个相关的文件。或者更确切地说,分成几个.h+.c文件对。

这会变得很麻烦,因为您需要模块的多个实例来做相同的事情。假设您需要使用相同代码的多个筛选器,但却被不相关的调用方代码调用。那么,拥有一组static变量是行不通的。

在C中进一步进行这种抽象的草率但老派的方法是使结构定义对调用方可见,然后提供一个接口,如void fir_init (fir_t* obj);,其中obj在调用方一侧分配。这解决了多实例问题,但破坏了私有封装。

专业的设计更倾向于使用不透明类型的概念(在本网站其他地方的多篇文章中对此进行了解释),即只向调用方公开一个不完整的结构类型,并让您的模块处理分配。这提供了真正的OO设计——您可以在维护私有封装的同时声明对象的多个实例。


不透明类型的C++等价物是class,抽象基类的行为方式与C中的不透明类型完全相同——调用方可以声明指向一个类型的指针/引用,但不能声明对象。C++还提供了构造函数/析构函数,这比手动调用一些"init"函数更方便。但当静态存储持续时间对象在启动时调用其默认构造函数时,这也会导致执行开销。

此外,C++成员函数自带this指针,因此不需要手动传递指向对象的指针。在一个类中也可以有static成员,它们的行为就像C文件作用域static,所有实例之间共享一个实例。