C++ 结构体初始化

378

在C++中,是否可以按照下面的方式初始化结构体:

struct address {
    int street_no;
    char *street_name;
    char *city;
    char *prov;
    char *postal_code;
};

address temp_address = { .city = "Hamilton", .prov = "Ontario" };

这些链接herehere提到仅限于C语言中使用这种样式。如果是这样,为什么在C++中不可能呢?这是否有任何潜在的技术原因导致它不能在C++中实现,或者使用这种方式是不好的做法。我喜欢使用这种初始化方式,因为我的结构很大,这种方式使我清楚地知道分配给哪个成员的值。

如果有其他方法可以实现相同的可读性,请与我分享。

在发布此问题之前,我已参考了以下链接:

1. AIX的C/C++ 2. 使用变量进行C结构初始化 3. 在C++中使用标签进行静态结构初始化 4. C++11适当的结构初始化

24
个人世界观:在C++中,你不需要使用这种对象初始化方式,而应该使用构造函数来代替。 - Philip Kendall
7
是的,我考虑过那种方法,但我有一个大型结构体数组。对我来说,使用这种方式更容易阅读和操作。你有没有使用构造函数进行初始化的样式/最佳实践,以提高代码可读性的建议? - Dinesh P.R.
25
这段话与编程关系不是很大:这个地址只在美国有效。在法国,我们没有“省”这个概念;在世界其他地方,也没有邮政编码。我的一个朋友的祖母住在一个非常小的村庄里,她的地址只是“X女士,邮政编码+小村庄名称”(对,没有街道名)。因此,请认真考虑您将要应用此地址到的市场,以便确定一个合法的地址。;) - Matthieu M.
6
@MatthieuM。在美国没有省份(这可能是加拿大的格式?),但是有州、领土,甚至一些不愿命名街道的小村庄。因此,地址规范的问题在这里也同样适用。 - Tim
7
在C++11标准中,这个特性被有意地省略了。但是这个特性将会在C++20中得到支持。具体信息可以参考这个链接:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0329r0.pdf。 - Jon
显示剩余12条评论
17个回答

237

如果你想清楚每个初始化值的含义,只需将其分成多行,并在每行加上注释:

address temp_addres = {
  0,  // street_no
  nullptr,  // street_name
  "Hamilton",  // city
  "Ontario",  // prov
  nullptr,  // postal_code
};

12
我个人喜欢并推荐这种风格。 - Dinesh P.R.
85
使用点符号准确地访问字段本身与直接这样做之间有什么区别?如果关心的是节省空间,那就不会有任何区别。当涉及到一致性和编写可维护代码时,我真的不太理解C++程序员,他们似乎总是想做些不同的事情来使自己的代码脱颖而出,代码应该反映要解决的问题,而不应该成为一种独立的习惯用语,目标是可靠性和易于维护。 - user1043000
22
@user1043000,首先,在这种情况下,您放置成员的顺序非常重要。如果在结构体的中间添加一个字段,您将不得不返回此代码并查找确切的位置来插入新初始化,这很麻烦且乏味。使用点符号表示法,您可以简单地将新的初始化放置在列表末尾,而无需担心顺序。如果您要在结构体中添加与其他上面或下面的成员相同类型(例如char *)的成员,则点符号表示法更加安全,因为没有交换它们的风险。 - Gui13
11
如果数据结构定义被改变,而没有人想到查找初始化,或无法找到所有初始化,或在编辑它们时出现错误,那么事情将会崩溃。 - Edward Falk
10
大多数(如果不是全部)的POSIX结构体没有定义的顺序,只有定义的成员。(struct timeval) { .seconds = 0, .microseconds = 100 } 总是表示一百微秒,但是 timeval { 0, 100 } 可能会表示一百 。您不希望以艰难的方式发现这样的问题。 - yyny
显示剩余6条评论

132

我的问题没有得到满意的回答(因为C++不支持基于标签的结构初始化)后,我采用了在这里找到的技巧:C++结构体的成员变量默认被初始化为0吗?

对你而言,这将是这样做:

address temp_address = {}; // will zero all fields in C++
temp_address.city = "Hamilton";
temp_address.prov = "Ontario";

这肯定是最接近你最初想要的方式(将所有字段都清零,除了你想初始化的那些字段)。


14
这不适用于静态初始化的对象。 - user877329
5
static address temp_address = {}; 这句话可以正常工作。填充内容取决于运行时,是的。您可以通过提供一个静态函数来绕过这个问题,该函数会为您进行初始化:static address temp_address = init_my_temp_address(); - Gui13
1
在C++11中,init_my_temp_address可以是一个lambda函数:static address temp_address = [] () { /* initialization code */ }(); - dureuill
3
不好的想法,它违反了RAII原则。 - Galaxy
2
真的是一个非常糟糕的想法:如果您在address上添加了一个成员,您将永远不知道所有创建address的地方,并且现在没有初始化您的新成员。 - mystery_doctor
显示剩余2条评论

60

正如其他人所提到的,这是指定初始化器。

此功能是C++20的一部分。


5
更多信息请参考:https://en.cppreference.com/w/cpp/language/aggregate_initialization聚合初始化是C++中一种用于初始化数组和结构体的简便方法。您可以使用花括号将值列表括起来并分别初始化每个元素,或者使用花括号括起来的键值对列表初始化结构体的成员变量。这种初始化方法不仅简单易懂,而且在某些情况下可以提高代码的可读性。 - user6547518
不,它是来自C++11而不是C++20! - John
2
@John 不符合 cppreference 的规定。 - Matthieu

26

这些字段标识确实是C初始化语法。在C++中,只需按正确顺序给出值而不需要字段名称。不幸的是,这意味着您需要全部提供它们(实际上,您可以省略尾随的零值字段,结果将相同):

address temp_address = { 0, 0, "Hamilton", "Ontario", 0 }; 

4
是的,目前我只使用这种方法(结构体对齐初始化)。但我觉得可读性不太好。由于我的结构体很大,初始值设定项有很多数据,我很难追踪哪个值被分配给了哪个成员。 - Dinesh P.R.
8
@DineshP.R. 然后编写一个构造函数! - Mr Lister
4
@MrLister(或者任何人)也许我现在被愚蠢的云朵所笼罩,但你能解释一下为什么使用构造函数会更好吗?在我的看来,使用初始化列表提供一堆有序的无名值与使用构造函数提供一堆有序的无名值之间几乎没有区别...? 请问您需要翻译哪种语言的内容呢? - yano
2
@yano 老实说,我真的不记得为什么我认为构造函数会是解决问题的答案。如果我想起来了,我会回来告诉你的。 - Mr Lister
1
构造函数可能是 IDE 提示的更好选项。 - Danogentili
显示剩余4条评论

22

这个功能被称为指定初始化器。它是C99标准的一个补充。然而,这个功能被C++11忽略了。根据《C++程序设计语言》第四版44.3.3.2节(未被C++采纳的C特性):

C99中的一些补充(与C89相比)故意没有被采用到C++中:

[1] 可变长度数组(VLA);使用vector或某种形式的动态数组

[2] 指定初始化器;使用构造函数

C99语法有指定初始化器 [参见ISO/IEC 9899:2011, N1570 Committee Draft - April 12, 2011]

6.7.9 初始化

initializer:
    assignment-expression
    { initializer-list }
    { initializer-list , }
initializer-list:
    designation_opt initializer
    initializer-list , designationopt initializer
designation:
    designator-list =
designator-list:
    designator
    designator-list designator
designator:
    [ constant-expression ]
    . identifier

另一方面,C++11不支持指定初始化器 [参见ISO/IEC 14882:2011、N3690委员会草案-2013年5月15日]

8.5 初始化

initializer:
    brace-or-equal-initializer
    ( expression-list )
brace-or-equal-initializer:
    = initializer-clause
    braced-init-list
initializer-clause:
    assignment-expression
    braced-init-list
initializer-list:
    initializer-clause ...opt
    initializer-list , initializer-clause ...opt
braced-init-list:
    { initializer-list ,opt }
    { }
为了达到相同的效果,请使用构造函数或初始化列表:

1
应该被接受为答案,因为它实际上回答了提问者所问的问题。 - Paul Childs

14

我知道这个问题非常古老,但是我发现另一种初始化的方式,使用constexpr和柯里化:

struct mp_struct_t {
    public:
        constexpr mp_struct_t(int member1) : mp_struct_t(member1, 0, 0) {}
        constexpr mp_struct_t(int member1, int member2, int member3) : member1(member1), member2(member2), member3(member3) {}
        constexpr mp_struct_t another_member(int member) { return {member1, member, member3}; }
        constexpr mp_struct_t yet_another_one(int member) { return {member1, member2, member}; }

    int member1, member2, member3;
};

static mp_struct_t a_struct = mp_struct_t{1}
                           .another_member(2)
                           .yet_another_one(3);

这种方法也适用于全局静态变量,甚至是constexpr变量。唯一的缺点是维护性不好:每当另一个成员必须使用此方法进行初始化时,所有成员初始化方法都必须更改。


3
这是建造者模式。成员方法可以返回对要修改的属性的引用,而不是每次创建一个新的结构体。 - phuclv
@phuclv,实际上,如果@Fabian这样做,他们将无法像在使用示例中那样进行多个调用。但是,如果他们不使用constexpr,他们只能更改值并作为引用返回*this;。这将导致相同的使用模式,并避免每次重构一个新对象。 - sprite

10
我可能漏掉了某些东西,但为什么不这样做呢:
#include <cstdio>    
struct Group {
    int x;
    int y;
    const char* s;
};

int main() 
{  
  Group group {
    .x = 1, 
    .y = 2, 
    .s = "Hello it works"
  };
  printf("%d, %d, %s", group.x, group.y, group.s);
}

2
我使用MinGW C++编译器和Arduino AVR C++编译器编译了上述程序,两者都按预期运行。请注意 #include <cstdio>。 - run_the_race
10
@run_the_race,这涉及到C++标准规定的内容,而不是某个编译器的行为。然而,这个特性将在C++20中推出。 - Jon
只有当结构体是POD时,这才有效。因此,如果您向其添加构造函数,则编译将停止。 - oromoiluig

10

你可以通过构造函数进行初始化:

struct address {
  address() : city("Hamilton"), prov("Ontario") {}
  int street_no;
  char *street_name;
  char *city;
  char *prov;
  char *postal_code;
};

14
只有当您控制 struct address 的定义时,这种情况才适用。此外,POD类型通常故意没有构造函数和析构函数。 - user4815162342

7
你甚至可以将Gui13的解决方案打包到一个单独的初始化语句中:
struct address {
                 int street_no;
                 char *street_name;
                 char *city;
                 char *prov;
                 char *postal_code;
               };


address ta = (ta = address(), ta.city = "Hamilton", ta.prov = "Ontario", ta);

声明:我不推荐这种风格


1
这仍然是危险的,因为它允许您向address添加成员,并且代码仍将编译,只初始化原始的五个成员。结构初始化的最好部分是,您可以将所有成员设置为const,并且它将强制您初始化它们所有。 - mystery_doctor

4

在C++中,C风格的初始化器被构造函数所取代,编译时可以确保只执行有效的初始化(即,在初始化后,对象成员是一致的)。

这是一个好习惯,但有时预初始化很方便,就像您的示例一样。面向对象编程通过抽象类或创建型设计模式来解决这个问题。

在我看来,使用这种安全方式会破坏代码的简洁性,有时安全性的权衡可能太昂贵了,因为简单的代码不需要复杂的设计来保持可维护性。

作为另一种替代方案,我建议定义使用lambda表达式的宏来简化初始化过程,使其看起来几乎像C风格:

struct address {
  int street_no;
  const char *street_name;
  const char *city;
  const char *prov;
  const char *postal_code;
};
#define ADDRESS_OPEN [] { address _={};
#define ADDRESS_CLOSE ; return _; }()
#define ADDRESS(x) ADDRESS_OPEN x ADDRESS_CLOSE

ADDRESS宏扩展为

[] { address _={}; /* definition... */ ; return _; }()

这将创建并调用lambda函数。宏参数也是逗号分隔的,因此您需要将初始化程序放入括号中并像这样调用:

address temp_address = ADDRESS(( _.city = "Hamilton", _.prov = "Ontario" ));

你也可以编写通用的宏初始化器。
#define INIT_OPEN(type) [] { type _={};
#define INIT_CLOSE ; return _; }()
#define INIT(type,x) INIT_OPEN(type) x INIT_CLOSE

但是这个调用略显不美观

address temp_address = INIT(address,( _.city = "Hamilton", _.prov = "Ontario" ));

然而,您可以使用通用的INIT宏轻松定义ADDRESS宏。
#define ADDRESS(x) INIT(address,x)

真的很丑陋,但你是为数不多的那些已经读过原帖实际问题的人之一。 - Paul Childs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接