各语言特有 · 36
结构体
把相关的值打包成一个整体,并弄清这个整体是被复制还是被共享。
本单元适用于 C# · C++ · C。
Python、JavaScript、TypeScript 和 Java 用类和对象来组织数据(见类一课),Craykon 用 사전() 字典。
它们在赋值时都通过引用共享同一个对象,这和会被复制的结构体不同。
总是一起出现的值,比如点的 x、y,学生的姓名、分数,就用结构体(struct)打包。
下面的例子先复制一个点、只改副本,再移动原来的点,以展示副本和原件的区别。
using System; struct Point // 값 타입: class로 바꾸면 q = p 가 같은 객체를 가리켜요{ public int X, Y; public void Move(int dx, int dy) { X += dx; Y += dy; } public int Len2() => X * X + Y * Y; public override string ToString() => $"({X}, {Y})";} class Program{ static void Main() { string[] t = Console.ReadLine().Split(); var p = new Point { X = int.Parse(t[0]), Y = int.Parse(t[1]) }; Console.WriteLine($"p = {p}"); Point q = p; // 데이터가 복사돼요 q.X = 10; Console.WriteLine($"q = {q}"); Console.WriteLine($"p = {p}"); p.Move(1, 1); Console.WriteLine($"p = {p}"); Console.WriteLine($"len2 = {p.Len2()}"); }}#include <iostream>#include <string> struct Point { int x = 0, y = 0; void move(int dx, int dy) { x += dx; y += dy; } // 멤버 함수 int len2() const { return x * x + y * y; } // 바꾸지 않으면 const}; void print(const std::string& name, const Point& p) { std::cout << name << " = (" << p.x << ", " << p.y << ")\n";} int main() { Point p; std::cin >> p.x >> p.y; print("p", p); Point q = p; // 멤버가 통째로 복사돼요 q.x = 10; print("q", q); print("p", p); p.move(1, 1); print("p", p); std::cout << "len2 = " << p.len2() << '\n';}#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;}3 4
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& |
| C | struct Point { int x, y; }; 每次使用都要写 struct Point p;,所以通常写 typedef struct { int x, y; } Point; — 修改原件用 move(&p, 1, 1),通过指针访问成员用 p->x 代替 (*p).x,只读时用 const Point * |
常见错误
修改列表里结构体的成员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;
用 == 比较结构体
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 · C++
void move(Point p, int dx, int dy) {
p.x += dx;
p.y += dy;
}→ 调用方的 p 没变。没有错误,也没有警告
C 用 Point * 接收,C++ 用 Point&。
各语言的差异
每次传递或赋值结构体时,都要想一想这是副本还是同一个东西。
C、C++、C# 的
每次传递或赋值结构体时,都要想一想这是副本还是同一个东西。
C、C++、C# 的
struct 都会被复制,想修改原来的就要用指针(Point *)、引用(Point&)或 C# 的 ref;而 C# 的 class 共享同一个对象。