{ }codelper

学习 › C

各语言特有 · 36

结构体

把相关的值打包成一个整体,并弄清这个整体是被复制还是被共享。

本单元适用于 C# · C++ · C。
Python、JavaScript、TypeScript 和 Java 用类和对象来组织数据(见类一课),Craykon 用 사전() 字典。
它们在赋值时都通过引用共享同一个对象,这和会被复制的结构体不同。

总是一起出现的值,比如点的 x、y,学生的姓名、分数,就用结构体(struct)打包。
下面的例子先复制一个点、只改副本,再移动原来的点,以展示副本和原件的区别。

#include <stdio.h> typedef struct {    int x, y;} Point; void print_point(const char *name, Point p) {    printf("%s = (%d, %d)\n", name, p.x, p.y);} void move(Point *p, int dx, int dy) {  /* 원본을 바꾸려면 포인터로 */    p->x += dx;    p->y += dy;} int len2(Point p) {  /* 읽기만 하니 값으로 받아도 돼요 */    return p.x * p.x + p.y * p.y;} int main(void) {    Point p;    scanf("%d %d", &p.x, &p.y);    print_point("p", p);     Point q = p;  /* 멤버가 통째로 복사돼요 */    q.x = 10;    print_point("q", q);    print_point("p", p);     move(&p, 1, 1);    print_point("p", p);    printf("len2 = %d\n", len2(p));    return 0;}
stdin
3 4
stdout
p = (3, 4)
q = (10, 4)
p = (3, 4)
p = (4, 5)
len2 = 41

要点

  • 一个包就成了一种新类型。
    可以作为一个变量来传递、返回、放进数组。
  • 里面的值叫成员(字段),用点来取,比如 p.x。
  • 给结构体赋值或把它传给函数时,所有成员都会整体复制。
    所以 q = p 之后再 q.x = 10,p 仍然是 (3, 4)。
  • 要修改原来的,就用指针或引用传递。
    大结构体复制代价高,所以即使只读,也常用 const 指针或引用传递。

各语言的写法

C#struct 是值类型,Point q = p; 会复制数据;class 是引用类型,同样的赋值会指向同一个对象(把例子改成 class Point,q.X = 10 之后 p 也变成 (10, 4))— int、double、DateTime 也都是结构体。
坐标、颜色、日期这类小值用 struct(想不可变就用 readonly struct 或 record struct),其余用 class。
传原件用 ref
C++不用 typedef,直接写 Point。
里面可以放成员函数(void move(int dx, int dy)),不修改成员的函数加 const,如 int len2() const — struct 和 class 只差默认访问权限(struct 是 public,class 是 private)。
传原件用 Point& 或 const Point&
Cstruct Point { int x, y; }; 每次使用都要写 struct Point p;,所以通常写 typedef struct { int x, y; } Point; — 修改原件用 move(&p, 1, 1),通过指针访问成员用 p->x 代替 (*p).x,只读时用 const Point *

常见错误

按值接收,只移动了副本C · C++

void move(Point p, int dx, int dy) {
    p.x += dx;
    p.y += dy;
}

→ 调用方的 p 没变。没有错误,也没有警告

C 用 Point * 接收,C++ 用 Point&。

用 == 比较结构体

if (p == q) { … }

→ C 中是编译错误。C++ 没有 operator==、C# 的 struct 没有定义 ==(CS0019)时,同样编译失败

C 要逐个比较成员(memcmp 可能因为成员之间的填充字节而出错)。
C++20 可写 bool operator==(const Point&) const = default;;C# 用 p.Equals(q)(比较成员值),或连 == 都会生成的 record struct。

修改列表里结构体的成员C#

var list = new List<Point> { new Point() };
list[0].X = 5;

→ 编译错误 CS1612:索引器返回的是副本。数组的 arr[0].X = 5 可以,但 foreach 取出的结构体成员也不能改(CS1654)

取出副本,改完再放回去:var t = list[0]; t.X = 5; list[0] = t;

各语言的差异
每次传递或赋值结构体时,都要想一想这是副本还是同一个东西。
C、C++、C# 的 struct 都会被复制,想修改原来的就要用指针(Point *)、引用(Point&)或 C# 的 ref;而 C# 的 class 共享同一个对象。

练习题