【问题标题】:Declaring a struct (that's already been typedef'd) within another struct?在另一个结构中声明一个结构(已经被 typedef 了)?
【发布时间】:2013-12-23 09:59:30
【问题描述】:

我对 C 的理解是,有两个独立的命名空间,一个用于标记(例如用于结构),一个用于所有其他变量(包括结构)。在结构定义之前使用 typedef 会将结构变量视为一种类型,所以如果你使用

struct car_part {/* Code Here */} CarPart;

(其中 CarPart 是可选的)
你必须使用

struct car_part engine;

申报汽车零件。
而如果你使用 typedef 和

typedef car_part {/* Code Here */} CarPart;

你现在可以使用

CarPart engine;

改为。

typedef struct tag {/* Code here */} struct_name;

1) 在块代码之前或之后声明实际变量有什么区别吗?即

typedef struct tag struct_name
{
    /* Code here */
};

typedef struct tag
{
    /* Code here*/
} struct_name;

2) 即使您不声明该类型的另一个结构变量,不使用 typedef 进行结构定义有什么好处吗?

3) 下面的代码表明存在标识符 Node 的语法错误 C2061,但我看不出它有什么问题。我尝试在每个元素声明之前添加关键字 struct ,但这只会产生更多错误。有什么想法吗?

typedef struct Ticket
{
    char customer_name[20];
    int ticket_number;
} Ticket;

typedef struct Node
{
    Ticket ticket_info;
    Node *next;
    Node *previous;
} Node;

typedef struct Queue
{
    Ticket *front;
    Ticket *rear;
    int queue_count;
} Queue;

编辑:修复前两行代码以明确说明元素声明的位置。

【问题讨论】:

  • 我习惯用_st后缀命名我的struct,所以struct node_st { /*fields*/ };typedef struct node_st Node;
  • 您的第一行代码无效。你希望struct car_part CarPart; 做什么?当我编译第一行的程序时,gcc 只是给我一个错误“'CarPart' 的存储大小未知”。
  • @David 对不起,意味着将代码放在 car_part 和 CarPart 之间,就像在其他代码中一样。这就是元素声明的所在。
  • 1) 第一种方法我没试过,第二种方法我一般不做;无论哪种方式,我都不会真正看到拥有全局结构的必要性。我更喜欢局部变量和正确的传递/返回。 2)是的!当您开始使用 ADT(抽象数据类型)时,您会希望您没有对所有结构进行类型定义。他们的行为不同,有时甚至很奇怪。
  • 您可以编辑问题以使您的代码有效。

标签: c struct typedef declaration


【解决方案1】:

1) 这两个代码块的区别在于第一个是无效的语法,而第二个是好的和有用的。我使用第二个来定义一个结构并同时为该结构定义一个 typedef。我的代码看起来像这样:

typedef struct Dog {
  int age, barks;
} Dog;

在那一行之后,我可以用Dog mydog;struct Dog mydog; 定义狗。

理解上面的代码做了两件事很重要。它定义了一个名为struct Dog 的类型,然后它定义了一个名为Dog 的类型,它只引用了struct Dog。您可以将其拆分为两个单独的步骤,如下所示:

struct Dog {
  int age, barks;
};    
typedef struct Dog Dog;

2) 我一直使用上面第一段代码中的typedef,发现没有问题。我想说省略 typdef 没有任何好处。仅作记录,如果您想省略 typedef 并只定义一个结构,那么您的代码将是:

struct Dog {
  int age, barks;
};

如果你这样做,你只能通过输入struct Dog mydog;来制作新狗;换句话说,类型的名称只有struct Dog,而Dog没有命名类型。

3) 问题是您试图在“节点”的定义中使用“节点”。那将是一个循环定义。你可以通过这样写来解决所有问题:

struct Node;
typedef struct Node
{
    struct Node * next;
    struct Node * previous;
} Node;

【讨论】:

  • 删除typedef 有一些风格和偏好方面的优势(例如明确指出您指的是结构,而不是原子类型),仅此而已。
  • 结构体名称总是在块中的定义之后,明白了。
【解决方案2】:

1) 你的第一个例子是无效的语法。正确的做法是这样的:

typedef struct tag {
    /* ... */
} struct_name;

2) 对结构使用 typedef 会使它们看起来像原子数据类型。它还允许您使类型不透明(因此其他代码块无法看到结构的内部)。就个人而言,我发现结构的 typedef 是一个非常糟糕的习惯(因为 struct 标识符有助于区分原子类型的结构和 typedef)。

3) 您正在尝试在其内部使用节点结构的 typedef 版本!在结构内部定义结构时,您需要使用struct Node 标识符。像这样:

typedef struct Node {
    Ticket ticket_info;
    struct Node *next;
    struct Node *previous;
} Node;

【讨论】:

  • 啊,这就是我要找的。那么用该结构的原始定义自引用的结构还没有收到 typedef 吗?这就解释了为什么在 Node 中使用 Ticket 是可以的,而不是 Node 本身?
  • 是的,因为typedef 在技术上发生在结构定义之后,因此结构在typedef 之前不能使用typedef
  • 一个(相对较小的)缺陷是“原子”类型毕竟不一定是原子的。例如,在某些 CPU 上,一个 4 或 8 字节长的类型(longlong longfloatdouble 等)实际上是由多个较短的项目内部组成的,并且看起来是原子的赋值 (double x = y) 不是(有两个 4 字节的“mov”将 y 复制到 x,并且中断/信号可能导致明显不可能的值)。不过,总的来说,我更喜欢让struct 暴露在外。
  • 嗯,从 C 的角度来看,它们是原子的。虽然您可以证明它们不是 hacky union 魔术,但在大多数情况下,这些数据类型在寄存器中的排列方式并不是那么重要。
【解决方案3】:

C 中实际上有四个命名空间(尽管这取决于特定的计数方式,有些包括宏名称作为第五个空间,我认为这是考虑它们的有效方式):

  • goto 标签
  • 标签(structunionenum
  • 结构或联合类型的实际成员(每种类型一个,因此您可以将其视为“多”而不是“一个”命名空间)
  • 所有其他(“普通”)标识符,例如函数和变量名称以及通过typedef 与其他类型同义的名称。

虽然(理论上)应该可以为 structunion 使用单独的空格,例如,C 没有,所以:

struct foo; union foo; /* ERROR */

无效。然而:

struct foo { int a, b; };
struct bar { char b; double a; };

很好,表明两种不同的struct 类型的成员位于不同的命名空间中(所以这再次使上面的“4 个命名空间”的计数变得可疑:-)。

除此之外,C 有一些适度(在某些方面是不必要的)复杂但在实践中相当可行的结构类型如何工作的规则。

每个struct 创建一个新类型除非它引用回现有类型。 struct 关键字后面可以跟一个标识符,或者只是一个左大括号{。如果只有一个左大括号,struct 会创建一个新类型:

struct { ... } X; /* variable X has a unique type */

如果存在标识符,编译器必须查看(单个)标记名称空间以查看该名称是否已定义。如果没有,struct 定义一个新类型:

struct blart { ... } X; /* variable X has type <struct newname>, a new type */

如果标识符已经存在,通常这指的是现有的类型:

struct blart Y; /* variable Y has the same type as variable X */

不过,有一个特殊的例外。如果你在一个新的范围内(例如在函数的开头),一个“空声明”——struct 关键字,后跟一个标识符,后跟一个分号——“清除”之前的可见类型:

void func(void) {
    struct blart; /* get rid of any existing "struct blart" */
    struct blart { char *a; int b; } v;

这里v 有一个new 类型,即使struct blart 已经在func 之外定义。

(这种“空洞声明”技巧在混淆代码竞赛中最有用。:-))

如果您不是在一个新的范围内,一个空洞的声明用于声明该类型存在的目的。这主要用于解决不同的问题,我稍后会介绍。

struct blart;

这里struct blart 提醒您(和编译器)现在有一个名为“struct blart”的类型。此类型只是声明,这意味着如果struct blart 尚未定义,则结构类型是“不完整的”。如果struct blart 已定义,则此类型已定义(并且“完整”)。所以:

struct blart { double blartness; };

定义它,然后任何更早或更晚的struct blarts 引用相同的类型。


这就是为什么这种声明很有用。在 C 中,标识符的任何声明都有范围。有四种可能的作用域:“文件”、“块”、“原型”和“函数”。最后一个(函数范围)专门用于goto 标签,因此我们可以从这里开始忽略它。这留下了文件、块和原型范围。文件范围是大多数人认为的“全局”的技术术语,与“本地”的“块范围”形成对比:

struct blart { double blartness } X; /* file scope */
void func(void) {
    struct slart { int i; } v; /* block scope */
    ...
}

这里struct blart 具有文件范围(与“全局”变量X 一样),struct slart 具有块范围(与“本地”变量v 一样)。

当块结束时,struct slart 消失。你不能再用名字来引用它;后来的struct slart 创建了一个新的不同类型,与后来的int v; 创建一个新的v 完全相同,并且不在函数func 内的块范围内引用v

唉,设计原始 C 标准的委员会(有充分的理由)在函数原型内部增加了一个范围,与这些规则的交互相当糟糕。如果你写一个函数原型:

void proto(char *name, int value);

标识符(namevalue)在右括号后消失,正如您所期望的那样 - 您不希望这会创建一个名为 name 的块范围变量。不幸的是,struct 也是如此:

void proto2(struct ziggy *stardust);

名称stardust 消失了,但struct ziggy 也消失了。如果struct ziggy 没有更早出现,那么在原型中创建的新的、不完整的类型现在已经从人类的所有范围内移除。它永远无法完成。好的 C 编译器会在此处打印警告。

解决方案是在编写原型之前声明结构(无论是否完整 [*]):

struct ziggy; /* hey compiler: "struct ziggy" has file scope */
void proto2(struct ziggy *stardust);

这一次,struct ziggy 有一个已经存在的可见声明可供引用,因此它使用现有类型。

[* 例如,在头文件中,您通常不知道 定义 struct 的头文件是否已包含,但您可以声明构造自己,然后定义使用指向它的指针的原型。]


现在,至于typedef...

typedef 关键字在语法上是一个存储类说明符,如 registerauto,但它的行为很奇怪。它在编译器中设置了一个标志,上面写着:“将变量声明更改为类型名称别名”。

如果你写:

typedef int TX, TY[3], *TZ;

您(和编译器)可以理解这一点的方式是从 删除typedef 关键字开始。结果需要在语法上有效,它是:

int TX, TY[3], *TZ;

这将声明三个变量:

  • TX 具有类型 int
  • TY 的类型为“int 的数组 3”
  • TZ 的类型为“指向int 的指针”

现在您(和编译器)将typedef 放回原处,并将“has”更改为“is another name for”:

  • TX 是类型 int 的另一个名称
  • TY 是“int 的数组 3”的另一个名称
  • TZ 是“指向int 的指针”的另一个名称

typedef 关键字与 struct 类型的工作方式完全相同。 struct 关键字创建了新类型;然后typedef 将变量声明从“具有类型...”更改为“是类型...的另一个名称”。所以:

typedef struct ca ca_t;

首先创建新类型,或者像往常一样引用现有类型struct ca。然后,不是将变量 ca_t 声明为具有类型 struct ca,而是将名称声明为类型 struct ca 的另一个名称。

如果省略结构标记名称,则只剩下两个有效的句法模式:

typedef struct; /* note: this is pointless */

或:

typedef struct { char *top_coat; int top_hat; } zz_t, *zz_p_t;

在这里,struct { 创建了一个新类型(请记住,我们在开头说过这种方式!),然后在结束 } 之后,将声明变量的标识符现在创建类型别名。 同样,该类型实际上是由 struct 关键字创建的(尽管这一次几乎无关紧要;typedef-names 现在是引用该类型的唯一方法)。

(第一个毫无意义的模式之所以如此,是因为没有大括号,您粘贴在 中的第一个标识符是 struct-tag:

typedef struct tag; /* (still pointless) */

因此您毕竟没有省略标签!)


至于最后一个问题,关于语法错误,这里的问题是 C 被设计为一种“单程”语言,您(和编译器)在这种语言中,您(和编译器)永远不必看得很远就能找出什么是什么.当你尝试这样的事情时:

typedef struct list {
    ...
    List *next; /* ERROR */
} List;

您给编译器的内容太多,无法一次消化。它首先(实际上)忽略typedef 关键字,除了设置更改变量声明方式的标志。这给你留下了:

struct list {
    ...
    List *next; /* ERROR */
}

名称List 根本不可用。尝试使用List *next; 不起作用。最终编译器会到达“变量声明”(并且因为设置了标志,所以将其更改为类型别名),但到那时为时已晚;错误已经发生。

解决方案与函数原型相同:您需要“前向声明”。前向声明会给你一个不完整的类型,直到你完成定义 struct list 部分,但这没关系:C 允许你在许多位置使用不完整的类型,包括当你想要声明一个指针时,包括 @987654415 @ 别名创建。所以:

typedef struct list List; /* incomplete type "struct list" */

struct list { /* begin completing "struct list" */
    ...
    List *next; /* use incomplete "struct list", through the type-alias */
}; /* this "}" completes the type "struct list" */

与到处写struct list 相比,这收获相对较小(它节省了一些打字,但那又怎样?好吧,我们中的一些人遭受了一些腕管/ RSI 问题:-)。


[注意:这最后一段会引起争议……它总是如此。]

事实上,如果你将struct 替换为type,C 代码对于“强类型语言”爱好者来说会变得更好。而不是可怕的 [%],弱酱:

typedef int distance; /* distance is measured in discrete units */
typedef double temperature; /* temperatures are fractional */

他们会写:

#define TYPE struct

TYPE distance;
TYPE temperature;

这些是不完整的类型,确实是不透明的。要使用距离值创建或销毁或确实执行任何操作,您必须调用一个函数(并且——无论如何,对于大多数变量;外部标识符有一些例外——使用指针,唉):

TYPE distance *x = new_distance(initial_value);

increase_distance(x, increment);
use_distance(x);
destroy_distance(x);

没有人会写:

*x += 14; /* 3 inches in a dram, 14 ounces in a foot */

它根本无法编译。

那些对其类型系统的束缚和约束较少的人可以通过完成类型来放松约束:

TYPE distance { int v; };
TYPE temperature { double v; };

当然,现在“骗子”可以做到:

TYPE distance x = { 0 };
x.v += 14; /* 735.5 watts in a horsepower */

(嗯,至少最后一条评论是正确的)。

[% 没那么糟糕,我想。有些人似乎不同意。]

【讨论】:

  • 信息量很大,尤其是命名空间解释
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-17
  • 1970-01-01
  • 2012-04-24
  • 1970-01-01
  • 2013-06-23
相关资源
最近更新 更多