16

我的第一篇文章,所以请放轻松!

我知道 C++ 中的结构和类之间没有真正的区别,但是包括我在内的很多人都使用结构或类来显示意图 - 用于对“普通旧数据”进行分组的结构和用于封装数据的类具有有意义的操作。

现在,这很好,但是你什么时候开始认为某些东西不再只是一个结构,而应该成为一个类?

我认为结构具有合理性的事情:

  1. 仅具有简单初始化代码的构造函数。
  2. 序列化代码,例如流插入/提取运算符。

我不太确定但可能会做的事情:

  1. 比较运算符
  2. 简单的转换功能 - 例如在从外部源接收数据后对所有成员进行字节交换。

我不认为结构应该有:

  1. 动态内存分配。
  2. 析构函数。
  3. 复杂的成员函数。

界限在哪里???

另外,将类实例作为结构的成员是否合理?例如

class C {private: int hiddenData; public: void DoSomething();};

struct S {int a; float b; C c; };

S s; s.c.DoSomething();

请记住,我不是在谈论你可以用 C++ 做什么,我感兴趣的是你在设计好的软件时应该做什么。

想法?

4

19 回答 19

13

我认为有三个主要的、连贯的思想流派:

  1. 不关心任何一种方式并使用structclass互换的人。
  2. 仅使用structs 来表示小 POD 的人。
  3. 使用structs 作为记录的人。

我无法对这两种策略中的任何一种做出决定性的论据。我倾向于遵循路径 2,但当我认为它适合时,我也会将结构用于非 POD 类型,特别是对于函数对象(即使这些可能不满足 POD 要求)。

(顺便说一句,C++ FAQ lite 对POD有一个很好的定义)。

编辑我没有接触模板元编程技术,例如使用struct占位符(类型标签)或实现元功能。我想在这些情况下绝对没有争议:因为它们从不包含方法(甚至数据),所以总是使用struct)。

于 2009-03-17T15:07:21.397 回答
10

我个人的偏好是只在根本没有方法的情况下使用结构。如果我出于任何原因需要添加一个方法,那就是一个类。

于 2009-03-17T15:05:44.927 回答
9

类与结构

使用 class 或 struct 关键字是一个品味问题,以及它给读者带来的“感觉”。从技术上讲,它们是等价的,但是如果将结构用于 POD 并将 C-struct 类型和类用于其他任何东西,则可读性会更好。

应该在 C++ 结构中包含的基本内容:初始化数据的构造函数(我不喜欢使用 memset,如果 POD 演变成不同的东西,它以后可能会反悔)或从其他类型构造但不是复制构造函数。

如果因为生成的编译器不够好而需要定义复制构造函数或赋值运算符,则将其设为类。

通常也将结构用于将传递给 STL 算法和模板元编程的函子,如

struct square_int {
   int operator()( int value )
   {
      return value*value;
   }
};
std::transform( v.begin(), v.end(), v.begin(), square_int() );

或者

// off the top of my head
template <typename T>
struct is_pointer { enum { value = false } };

template <typename T>
struct is_pointer<T*> { enum { value = true } };

成员方法与自由函数

除了我之前说过的,不要添加到其他人已经回答的内容之外,我想把重点放在你在帖子中评论的其他类型的函数上,比如比较运算符等。

对称的运算符(比较、算术)、插入和删除运算符以及转换通常最好作为自由函数实现,无论您将其声明为类还是结构。

对称运算符(关于数据类型)如果被实现为成员函数,则它们不是对称的。查找规则不会将左侧转换为调用成员函数,但它会应用相同的转换来匹配自由函数。

   // Example of symmetry with free functions where method would be asymmetric
   int main()
   {
      std::string( "Hello " ) + "world"; // compiles as free / member function
      "Hello " + std::string( "world" ); // compiles with free function, fails with member function definition of +
   }

在上面的代码中,如果 operator+ 是 std::string 的成员方法,则编译器将无法编译,因为它无法将 const char* 文字转换为 std::string 以使用该成员方法。

流的插入和提取必须始终作为自由函数实现,因为流始终位于操作的左侧。

将转换保持为自由函数将两种不同的类型解耦。如果 A 和 A' 可以相互转换,并且您决定将转换作为 A 的成员来实现,那么 A 必须知道 A' 并且 A 的所有使用都将取决于 A' 是否使用它。如果将转换定义为自由函数,则没有 A' 的 A 是完整的,并且两个类/结构之间的耦合会更小。与网络、序列化和反序列化的转换也是如此。当您在类/结构中实现它们时,您会强制所有用户了解这些转换。

于 2009-03-17T16:19:20.397 回答
3

通过阅读 Visual Studio 附带的一些 STL 源代码,似乎使用的一个标准是“大部分是公共的”(以struct开头)还是“大部分是私有的”(以class开头)?

同样,如果您在班级顶部附近(可能是因为它很重要)写的内容是public,那么请使用struct。另一方面,如果您首先列出成员数据,请使用class

于 2009-03-17T15:12:34.597 回答
3

当我需要维护不变量和数据完整性时,我会使用类和封装。如果您没有不变量,并且数据实际上只是一堆项目,那么即使您添加了精美的辅助构造函数或函数,在我们的商店中使用 struct 也总是可以的。然而,你装饰得越多,那应该会让你停下来想也许它应该是一堂课。

如果您确实必须与 POD 兼容(例如与 C 代码接口),您仍然需要使用 struct. 但是,您可以将此结构包装在一个类中,并使用 get() 函数将其公开以与 C API 接口。或者创建一个辅助函数以从您的类中返回正确的 POD 结构。

于 2009-03-17T15:33:58.073 回答
3

你可以看看标准库做了什么。每个人都喜欢的 struct std::pair只有构造函数。

我发现使用带有结构的构造函数非常方便和自然,我无法想象没有它们。我从不给结构体提供任何其他方法,但当然可能有自由函数或其他类的成员将它们作为参数。

于 2009-03-17T16:28:26.723 回答
2

为了方便起见,我将在结构中添加很少的方法,前提是我实际使用它们。当然,它们都是公开的。如果我需要更多,它会立即转换为一个类。

  1. 默认构造函数,用于将数据成员初始化为已知值。
  2. 每个成员都有一个参数的构造函数。
  3. 运算符<,以便轻松包含在集合和映射中。
  4. 运营商==。
于 2009-03-17T16:38:32.737 回答
2

我可能是少数,但我用structs 来表示一件事,“位的顺序很重要”。任何必须序列化到磁盘或网络的东西,或者必须与某些第三方库兼容并且需要以正确的顺序排列的东西,或者如果它正在做某种位域魔术作为处理器特定的优化,那总是会进入struct. 因此,重新排列 a 中的字段struct总是会产生某种后果,应该仔细考虑。

class但是,es 具有仅在语义上有意义的字段。如果出于某种原因我或其他人想要重新排列或修改 a 的数据成员class,这可以很自由地发生。

于 2009-03-17T18:29:50.537 回答
1

一致性是最重要的。约定的要点是为所有将来阅读您的代码的人提供一个共同的参考点。

就个人而言,如果我觉得我需要类的功能,我会避免使用结构。
又名:“普通旧数据”方法。

查看您的项目并设定标准,或坚持已有的标准。

不过,我建议你尽量避免继承,特别是如果所有结构都在做的是持有 POD。没有比跟踪一堆超类的整数或字符更糟糕的了。

于 2009-03-17T15:10:13.433 回答
1

包括我在内的很多人都使用结构或类来显示意图 - 用于对“普通旧数据”进行分组的结构和用于封装具有有意义操作的数据的类。

恕我直言,这种区分是一种误解,但很好理解为什么会这样使用它。它基于传统约定和对类与结构的内在感觉。我会遵循 Marshall Cline 的建议:

7.8 关键字struct和class有什么区别?

由于这是大多数人已经拥有的含义,因此如果您的类具有很少的方法并且具有公共数据(这样的东西确实存在于设计良好的系统中!),您可能应该使用 struct 关键字,否则您可能应该使用该类关键词。

就个人而言,我(过度)使用struct关键字metafunctions

于 2010-02-04T00:59:15.917 回答
0

结构的另一个常见用途是用于模板内容和本地 RAII 或仿函数类。这些是一次性的代码,它们更接近于函数而不是整个类,并且需要额外的public:声明只是愚蠢的。如果你看一下 boost,你会看到很多这样的东西:

template<
      bool C
    , typename T1
    , typename T2
    >
struct if_c
{
    typedef T1 type;
};

它显然不是 POD(实际上它根本不保存任何数据)。在其他情况下,您可以拥有仅包含构造函数/析构函数的 RAII 类:

struct LogOnLeavingScope : public NonCopyable
{
    string Message;
    LogOnLeavingScope(const string &message) : Message(message) {}
    ~LogOnLeavingScope() { Log(Message); }
];

Functor 类将提供相同的论点:

struct Logger
{
    void operator()(const string &message) { Log(message); }
}

我想说 C++ 没有一个特性意味着你应该使用一个类而不是一个结构(可能除了虚函数)。这完全取决于您要呈现的界面 - 如果您对所有内容都公开,请使用结构。如果你发现你想要添加private:部分,这是一个好兆头,你真的想要一个类而不是一个结构。

于 2009-03-17T15:34:56.433 回答
0

如果数据成员有访问器方法,那么它就是一个类。

如果您可以直接访问数据并且可以随意修改它,那么它就是一个结构。

结构没有理由不应该有构造函数、比较运算符等。

于 2009-03-17T18:38:24.807 回答
0

我喜欢在结构上添加的唯一方法是属性类型方法、简单转换(以及,如果适合类型,还有运算符)和非默认构造函数。

特性

例如,我可以定义一个RECT结构如下:

typedef struct tagRECT{
    int left;
    int top;
    int right;
    int bottom;

    int get_width(){return right - left;}
    int get_height(){return bottom - top;}
    int set_width(int width){right = left + width; return width;}
    int set_height(int height){bottom = top + height; return height;}
} RECT, *PRECT;

在编译器支持属性作为扩展的情况下,“set”方法返回新值以支持赋值链接。

简单的变换

对于存储复杂数据的 POD 类型,我可能会包含对该数据执行简单转换的方法。一个明显的示例可能是在 TransformationMatrix 结构上包含 Rotate、Scale、Shear 和 Translate 方法。

运营商

实际上只是上述的扩展,如果运算符对类型有意义,我会将它们添加到结构中。这对于保持对象的原子性可能是适当且必要的。一个明显的例子是复杂结构上的标准算术运算符。

构造函数

不想在我的结构上使用默认构造函数。我不想分配一个 POD 数组并遭受一千个(或其他)默认初始化的调用。如果memset初始化还不够,我将提供一个 Initialize 方法。

但是,我将在结构上提供一个非默认构造函数。当可以从部分构造中推断出一个或多个字段时,这尤其有用。

于 2009-03-17T18:46:41.620 回答
0

我总是将结构用于“数据块”,即使它们用构造函数和有时比较运算符装饰,它们也永远不会添加方法(甚至没有 get/set 方法)。

我认为这表明了该类型的意图 - 结构只是要被其他事物操作的数据。

于 2009-03-17T19:11:36.753 回答
0

每当我想将数据成员用作对象的接口时,我都会使用 struct。每当需要添加私有部分时,我会将结构更改为类。

于 2009-03-17T21:58:35.473 回答
0

通常,我class在需要使用访问说明符时使用。这样做的效果是,我的大多数顶级内容都保留为类,而稀有的 POD 集合和我不那么稀有的内部类 pimpl 通常是结构。

于 2009-03-18T15:40:23.533 回答
0

我对结构和类的简单经验法则:

if (data_requires_strict_alignment == TRUE) {
  use(struct);
} else {
  use(class);
}

也就是说,如果您要表示的数据对应于某些具有严格成员顺序和对齐要求的数据对象(例如,在驱动程序级别与硬件交换的数据结构),请使用结构。对于所有其他情况,请使用类。类具有结构没有的许多特性和功能,根据我的经验,尽可能使用类是有益的,即使您目前没有使用任何这些附加特性(如果没有别的,纯数据类是像具有更安全的默认访问级别的结构)。当您需要结构的独特属性时,为那些情况保留结构;即,能够以精确的顺​​序和精确的对齐/填充(通常使用低级通信,例如驱动程序)指定结构成员,以便您可以将其转换为字节数组,使用memcpy()如果你遵循这个模型,那么类就不会被用作结构的成员(因为类定义没有指定对齐方式或 的可预测值sizeof(class))。同样,如果您正在考虑构造函数或运算符,请使用类。

这个经验法则还有助于更轻松地与 C 代码交互,因为结构的使用方式与 C 结构一致。

于 2009-03-18T16:40:40.050 回答
0

当我考虑这个问题时,我并没有区分成员函数和自由函数,但我现在发现这是一个错误。现在在我看来,结构应该很少有成员函数。很明显,结构中的所有内容都应该是公开的。

因此,拥有 struct 成员函数通常没有意义,因为任何函数都可以更改 struct 中的数据。结构上的成员函数将是一种便利而不是必需品。

唯一的例外是为了其他目的而需要成为成员函数的东西——用于初始化数组的构造函数;与地图一起使用的比较;模板使用的东西;等等

也许类和结构应该被视为对立面——类公开函数并隐藏数据,而结构公开数据并允许您隐藏函数。

回到字节交换示例:

struct S
{
    int data;
    S() {data = 1234;}
    void ByteSwap() {network::ByteSwap( &data );}
};

S s;
s.ByteSwap();

会成为:

struct S
{
    S() {data = 1234;}
    int data;
};

namespace network
{
    void ByteSwap( int* i ) {/*byte swap *i*/}
    void ByteSwap( S* s ) {ByteSwap( &s->data );}

    S s;
    ByteSwap(&s);
}

当数据和某些函数并不总是强烈地归属在一起时,这是有道理的。字节交换只对网络系统感兴趣,但更高级别的函数仍然可以使用该结构,甚至不知道字节交换之类​​的低级内容。

这种情况下的另一个好处是相关的字节交换操作都保存在同一个地方。

于 2009-03-24T21:27:44.920 回答
-1

这纯粹是风格问题,对与错将由您的商店决定。有时决定是不做决定,但套用拉什的话说,他们仍然会做出选择。

根据经验,我通常将结构用于更简单的数据类型,范围从 POD 到具有成员简单函数的对象。但是没有明确的界限,一旦我定义了一个结构,我通常不会因为我添加了更多的功能而返回并将其更改为一个类。我认为这没有任何价值。

于 2009-03-17T15:12:57.747 回答