言語ごとの特徴 · 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
ポイント
- ひとまとまりが新しい型になります。
変数 1 つとして渡したり、返したり、配列に入れたりできます。 - 中の値はメンバ(フィールド)といい、
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))、変更しない関数には int len2() const のように 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 · 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 はメンバを 1 つずつ比べます(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 は同じオブジェクトを共有します。