点(.)操作符用于访问结构体的成员,而C中的箭头操作符(->)用于访问由所讨论的指针引用的结构体的成员。指针本身没有任何可以用点运算符访问的成员(它实际上只是一个描述虚拟内存中位置的数字,所以它没有任何成员)。因此,如果我们只定义点运算符来自动解引用指针,如果它被用在指针上(编译器在编译时afaik知道的信息),就不会有歧义。那么,为什么语言的创造者决定通过添加这个看似不必要的操作符来使事情变得更加复杂呢?重大的设计决策是什么?
.
->
2wnc66cl1#
我把你的问题理解为两个问题:1)为什么->甚至存在,以及2)为什么.不会自动解引用指针。这两个问题的答案都有历史根源。
为什么->存在?
在C语言的最早版本之一(我将其称为CRM,用于“C Reference Manual”,于1975年5月随第6版Unix一起发布)中,运算符->具有非常专有的含义,而不是*和.组合的同义词CRM所描述的C语言在许多方面与现代C语言有很大的不同。在CRM结构体中,成员实现了 byte offset 的全局概念,它可以添加到任何地址值中,没有类型限制。也就是说,所有struct成员的所有名称都有独立的全局含义(因此必须是唯一的)。例如,您可以声明
*
struct S { int a; int b; };
名称a代表偏移量0,而名称b代表偏移量2(假设int类型的大小为2且无填充)。该语言要求翻译单元中所有结构的所有成员要么具有唯一的名称,要么代表相同的偏移值。例如,在同一翻译单元中,您可以另外声明
a
b
int
struct X { int a; int x; };
这是可以的,因为名称a始终代表偏移量0。但这份附加声明
struct Y { int b; int a; };
将正式无效,因为它试图将a“重新定义”为偏移量2,将b“重新定义”为偏移量0。这就是->运算符的作用。由于每个结构成员名都有自己独立的全局含义,因此该语言支持以下表达式
int i = 5; i->b = 42; /* Write 42 into `int` at address 7 */ 100->a = 0; /* Write 0 into `int` at address 100 */
第一个赋值被编译器解释为“取地址5,将偏移量2添加到它,并将42赋值给结果地址的int值”。也就是说,上面将42分配给int地址7值。请注意,->的这种使用并不关心左侧表达式的类型。左手被解释为右值数字地址(无论是指针还是整数)。*和.组合是不可能的。你做不到
5
2
42
7
(*i).b = 42;
因为*i已经是无效表达式。由于*运算符与.分开,因此对其操作数施加了更严格的类型要求。为了提供解决这个限制的能力,CRM引入了->运算符,它独立于左操作数的类型。正如基思在评论中所指出的,->和* + .组合之间的这种差异就是CRM在7中所说的“放宽要求”。1.8:* 除了E1是指针类型的要求放宽外,表达式E1−>MOS完全等价于(*E1).MOS *后来,在K&R C中,许多最初在CRM中描述的功能被显著地重新设计。“结构成员作为全局偏移标识符”的想法被完全删除。->运算符的功能与*和.组合的功能完全相同。
*i
E1
E1−>MOS
(*E1).MOS
为什么.不能自动解引用指针?
同样,在CRM版本的语言中,.运算符的左操作数必须是 * 左值 *。这是对该操作数施加的 * 唯一 * 要求(这也是它与->不同的原因,如上所述)。请注意,CRM * 不 * 要求.的左操作数具有结构类型。它只要求它是一个左值,any 左值。这意味着在C语言的CRM版本中,您可以编写这样的代码
struct S { int a, b; }; struct T { float x, y, z; }; struct T c; c.b = 55;
在这种情况下,编译器会将55写入int值,该值位于连续内存块(称为c)中的字节偏移量2处,即使类型struct T没有名为b的字段。编译器根本不关心c的实际类型。它所关心的是c是一个左值:某种可写内存块注意如果你这样做
55
c
struct T
S *s; ... s.b = 42;
代码将被认为是有效的(因为s也是左值),并且编译器将简单地尝试将数据 * 写入指针s本身 *,在字节偏移量2处。不用说,这样的事情很容易导致内存溢出,但语言本身并不关心这些问题。也就是说,在那个版本的语言中,你提出的关于为指针类型重载运算符.的想法是行不通的:运算符.在与指针(左值指针或任何左值)一起使用时已经具有非常特定的含义。这是非常奇怪的功能,毫无疑问。但它当时就在那里。当然,这个奇怪的功能并不是反对在C - K&R C的修改版本中引入指针的重载.操作符(如您所建议的)的有力理由。但还没有完成。也许在那个时候,有一些遗留代码是用C的CRM版本编写的,必须得到支持。
s
(The 1975 C参考手册的URL可能不稳定。另一个副本是here,可能有一些细微的差异。)
9avjhtql2#
除了历史原因(好的和已经报道的)之外,操作符优先级也有一个小问题:点运算符的优先级高于星星运算符,因此如果您有指向结构的结构包含指向结构的指针。..这两个是等价的:
(*(*(*a).b).c).d a->b->c->d
但第二个显然更具有可读性。箭头操作符具有最高优先级(就像点一样),从左到右关联。我认为这比使用点运算符更清楚,因为我们从表达式中知道类型,而不必查看声明,甚至可以在另一个文件中。
62o28rlo3#
C也做得很好,不使任何模棱两可。当然,圆点可以重载表示这两种情况,但是箭头确保程序员知道他在操作指针,就像编译器不允许混合两种不兼容的类型一样。
pu3pd22g4#
在C中,没有任何技术上的理由需要一个单独的->操作符。但它确实增加了清晰度-如果你看到一个->,你知道它是一个指针,可能是null,所以你可能需要在取消引用它之前检查null。在C++中,有些类在某种程度上假装是指针(std::unique_ptr,std::shared_ptr,std::optional)。它们支持*和->类似指针,但它们也有自己的成员函数,可以通过.访问。以这种方式分隔符号可以避免任何可能的成员名称冲突,同时也增加了清晰度。
std::unique_ptr
std::shared_ptr
std::optional
4条答案
按热度按时间2wnc66cl1#
我把你的问题理解为两个问题:1)为什么
->
甚至存在,以及2)为什么.
不会自动解引用指针。这两个问题的答案都有历史根源。为什么
->
存在?在C语言的最早版本之一(我将其称为CRM,用于“C Reference Manual”,于1975年5月随第6版Unix一起发布)中,运算符
->
具有非常专有的含义,而不是*
和.
组合的同义词CRM所描述的C语言在许多方面与现代C语言有很大的不同。在CRM结构体中,成员实现了 byte offset 的全局概念,它可以添加到任何地址值中,没有类型限制。也就是说,所有struct成员的所有名称都有独立的全局含义(因此必须是唯一的)。例如,您可以声明
名称
a
代表偏移量0,而名称b
代表偏移量2(假设int
类型的大小为2且无填充)。该语言要求翻译单元中所有结构的所有成员要么具有唯一的名称,要么代表相同的偏移值。例如,在同一翻译单元中,您可以另外声明这是可以的,因为名称
a
始终代表偏移量0。但这份附加声明将正式无效,因为它试图将
a
“重新定义”为偏移量2,将b
“重新定义”为偏移量0。这就是
->
运算符的作用。由于每个结构成员名都有自己独立的全局含义,因此该语言支持以下表达式第一个赋值被编译器解释为“取地址
5
,将偏移量2
添加到它,并将42
赋值给结果地址的int
值”。也就是说,上面将42
分配给int
地址7
值。请注意,->
的这种使用并不关心左侧表达式的类型。左手被解释为右值数字地址(无论是指针还是整数)。*
和.
组合是不可能的。你做不到因为
*i
已经是无效表达式。由于*
运算符与.
分开,因此对其操作数施加了更严格的类型要求。为了提供解决这个限制的能力,CRM引入了->
运算符,它独立于左操作数的类型。正如基思在评论中所指出的,
->
和*
+.
组合之间的这种差异就是CRM在7中所说的“放宽要求”。1.8:* 除了E1
是指针类型的要求放宽外,表达式E1−>MOS
完全等价于(*E1).MOS
*后来,在K&R C中,许多最初在CRM中描述的功能被显著地重新设计。“结构成员作为全局偏移标识符”的想法被完全删除。
->
运算符的功能与*
和.
组合的功能完全相同。为什么
.
不能自动解引用指针?同样,在CRM版本的语言中,
.
运算符的左操作数必须是 * 左值 *。这是对该操作数施加的 * 唯一 * 要求(这也是它与->
不同的原因,如上所述)。请注意,CRM * 不 * 要求.
的左操作数具有结构类型。它只要求它是一个左值,any 左值。这意味着在C语言的CRM版本中,您可以编写这样的代码在这种情况下,编译器会将
55
写入int
值,该值位于连续内存块(称为c
)中的字节偏移量2处,即使类型struct T
没有名为b
的字段。编译器根本不关心c
的实际类型。它所关心的是c
是一个左值:某种可写内存块注意如果你这样做
代码将被认为是有效的(因为
s
也是左值),并且编译器将简单地尝试将数据 * 写入指针s
本身 *,在字节偏移量2处。不用说,这样的事情很容易导致内存溢出,但语言本身并不关心这些问题。也就是说,在那个版本的语言中,你提出的关于为指针类型重载运算符
.
的想法是行不通的:运算符.
在与指针(左值指针或任何左值)一起使用时已经具有非常特定的含义。这是非常奇怪的功能,毫无疑问。但它当时就在那里。当然,这个奇怪的功能并不是反对在C - K&R C的修改版本中引入指针的重载
.
操作符(如您所建议的)的有力理由。但还没有完成。也许在那个时候,有一些遗留代码是用C的CRM版本编写的,必须得到支持。(The 1975 C参考手册的URL可能不稳定。另一个副本是here,可能有一些细微的差异。)
9avjhtql2#
除了历史原因(好的和已经报道的)之外,操作符优先级也有一个小问题:点运算符的优先级高于星星运算符,因此如果您有指向结构的结构包含指向结构的指针。..这两个是等价的:
但第二个显然更具有可读性。箭头操作符具有最高优先级(就像点一样),从左到右关联。我认为这比使用点运算符更清楚,因为我们从表达式中知道类型,而不必查看声明,甚至可以在另一个文件中。
62o28rlo3#
C也做得很好,不使任何模棱两可。
当然,圆点可以重载表示这两种情况,但是箭头确保程序员知道他在操作指针,就像编译器不允许混合两种不兼容的类型一样。
pu3pd22g4#
在C中,没有任何技术上的理由需要一个单独的
->
操作符。但它确实增加了清晰度-如果你看到一个->
,你知道它是一个指针,可能是null,所以你可能需要在取消引用它之前检查null。在C++中,有些类在某种程度上假装是指针(
std::unique_ptr
,std::shared_ptr
,std::optional
)。它们支持*
和->
类似指针,但它们也有自己的成员函数,可以通过.
访问。以这种方式分隔符号可以避免任何可能的成员名称冲突,同时也增加了清晰度。