0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zigの配列・ポインタ・スライスの使い分け

0
Posted at

概要

Zigを使う際に、非常に序盤の疑問として、この4つの使い分けがあるかと思います。

var arr: [4]u8 = .{1, 2, 3, 4};    // 配列
var ptr: *u8 = &x;                  // ポインタ
var many: [*]u8 = some_ptr;         // many-timeポインタ
var slice: []u8 = &arr;             // スライス

C/C++やRustを経験しているひとには、この記述の微妙な違いに戸惑うかもしれません。

ここでは

  • 4つの型が何を保証するのか
  • なぜ見た目が似ているのか
  • 他言語との比較
  • 実用的な使い分け
    を解説していこうと思います。

結論:メモリを見る方法の違い

Zigでは、同じメモリを、どこまで安全に・どこまで正確に知っているかを型で表します。

4つの型の違い

記述 何を保証する? 長さ 所有 境界チェック
配列 [N]T N個のTがここにある compile-time
ポインタ *T 1個のTがある 1 -
many-timeポインタ [*]T Tが連続している 不明
スライス []T N個のTが連続している runtime
つまり、
  • 配列:すべてを知っている(コンパイル時)
  • ポインタ:単一要素だけを知っている
  • many-timeポインタ:何も知らない
  • スライス:長さを知っている(実行時)

配列 [N]T

var arr: [4]u8 = .{1, 2, 3, 4};
  • メモリを所有する唯一の型
  • サイズNコンパイル時定数
  • 値型(代入すると全体がコピーされる)
  • スタックに確保される

実例

const std = @import("std");

pub fn main() void {
    var arr: [4]u8 = .{1, 2, 3, 4};
    
    // コンパイル時にサイズが確定
    std.debug.print("Length: {}\n", .{arr.len}); // 4
    
    // 境界チェックあり
    std.debug.print("{}\n", .{arr[0]}); // 1
    // arr[10] = 0; // コンパイルエラー
    
    // 値型なのでコピーされる
    var arr2 = arr;
    arr2[0] = 99;
    std.debug.print("{} {}\n", .{arr[0], arr2[0]}); // 1 99
}

ポインタ *T

var x: u8 = 10;
var ptr: *u8 = &x;
  • ちょうど1個のTがあることを保証
  • ptr.*でデリファレンス(1要素アクセス)
  • 配列のような添字アクセスはできない

実例

pub fn main() void {
    var x: u8 = 10;
    var ptr: *u8 = &x;
    
    // デリファレンスで値を取得
    std.debug.print("{}\n", .{ptr.*}); // 10
    
    // 値を変更
    ptr.* = 20;
    std.debug.print("{}\n", .{x}); // 20
    
    // これはコンパイルエラー
    // _ = ptr[0]; // 添字アクセスは不可
}

many-timeポインタ [*]T

var buf: [*]u8 = some_ptr;
buf[10] = 3; // 範囲外でも通る(危険)
  • 連続メモリであることだけを保証
  • 長さを知らない
  • CのT*とほぼ同等
  • 境界チェックなし

実例

pub fn main() void {
    var arr: [10]u8 = undefined;
    var many: [*]u8 = &arr;
    
    // 添字アクセス可能(境界チェックなし)
    many[0] = 1;
    many[5] = 2;
    
    // これも通ってしまう(未定義動作)
    many[100] = 3; // 範囲外だがコンパイルエラーにならない
    
    // C FFIで使う
    const c_str: [*:0]const u8 = "hello";
}

C FFIでの使用例

// Cの関数シグネチャ
// void* memcpy(void* dest, const void* src, size_t n);

extern fn memcpy(dest: [*]u8, src: [*]const u8, n: usize) [*]u8;

pub fn copyBytes(dest: []u8, src: []const u8) void {
    _ = memcpy(dest.ptr, src.ptr, @min(dest.len, src.len));
}

スライス []T

fn sum(xs: []const u8) u32 {
    var s: u32 = 0;
    for (xs) |x| s += x;
    return s;
}
  • pointer + lengthの組み合わせ
  • 境界チェックあり
  • メモリは所有しない(ビュー)
  • 配列・多項目ポインタから作れる

実際の構造

// スライスの内部構造(概念的)
struct {
    ptr: [*]T,
    len: usize,
}

実例

pub fn main() void {
    var arr: [4]u8 = .{1, 2, 3, 4};
    
    // 配列からスライスを作成
    var slice: []u8 = &arr;
    
    // 長さを知っている
    std.debug.print("Length: {}\n", .{slice.len}); // 4
    
    // 境界チェックあり
    std.debug.print("{}\n", .{slice[0]}); // 1
    // slice[10] = 0; // panic(実行時エラー)
    
    // 部分スライス
    var sub = slice[1..3];
    std.debug.print("{any}\n", .{sub}); // [2, 3]
    
    // イテレート
    for (slice) |item| {
        std.debug.print("{} ", .{item});
    }
}

なぜ似てるのか

Cの問題

Cでは、ポインタが何を指しているか曖昧になることもあります。

void process(int* p);

Zigの解決

Zigでは、意図を型で表現します。

fn process1(p: *i32) void        // 1個のi32
fn process2(p: [*]i32) void      // 長さ不明の配列
fn process3(p: []i32) void       // 長さ既知の配列
fn process4(p: *[4]i32) void     // 4個固定の配列

型を見ることで設計意図を見えるようにしています。


見た目が似ている理由

arr[i]
many[i]
slice[i]

全部同じ[i]アクセスに見えますね。

実際に起きていること 安全性
arr[i] コンパイル時に範囲確定
ptr[i] ❌ そもそも使えない -
many[i] 生ポインタ計算
slice[i] 境界チェック付きアクセス

学習コストは低く、保証するレベルを明確に分けています。


型の変換

配列からの変換

var arr: [4]u8 = .{1, 2, 3, 4};

// 配列 → スライス
var slice: []u8 = &arr;

// 配列 → 配列ポインタ
var ptr: *[4]u8 = &arr;

// 配列 → many-timeポインタ
var many: [*]u8 = &arr;

スライスからの変換

var slice: []u8 = &arr;

// スライス → many-timeポインタ
var many: [*]u8 = slice.ptr;

// スライスの長さ
var len: usize = slice.len;

部分スライス

var arr: [10]u8 = .{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var slice: []u8 = &arr;

// 範囲指定
var sub1 = slice[2..5];      // [2, 3, 4]
var sub2 = slice[5..];        // [5, 6, 7, 8, 9]
var sub3 = slice[0..3];       // [0, 1, 2]

## 他言語との比較

C/C++ との比較

Zig C/C++ 違い
[N]T T[N] Zigは値型、Cは配列名がポインタに退化
*T T* Zigは1個を保証、Cは曖昧
[*]T T* ほぼ同じ(危険)
[]T std::span<T> (C++20) 似ている

C/C++の問題例:

void func(int* arr) {
    // arrは配列?ポインタ?
    // 長さは?
    // sizeof(arr)は常にポインタのサイズ
}

Zigの明確さ:

fn func(arr: []i32) void {
    // 配列であることが明確
    // arr.lenで長さが分かる
}

Rust との比較

Zig Rust 概念
[N]T [T; N] 所有された配列
*T &T 不変参照(1個)
*T (mut) &mut T 可変参照(1個)
[]T &[T] スライス(不変)
[]T (mut) &mut [T] スライス(可変)
[*]T *const T 生ポインタ(unsafe)

Zig的な理解

配列            → 所有
スライス        → 安全な借用
多項目ポインタ  → unsafe借用
ポインタ        → 単一要素参照

Python との比較

Pythonはすべて動的なので、Zigの静的な型システムとは根本的に異なります。


実用的な実装

関数の引数

// スライスを使う
fn sum(values: []const i32) i32 {
    var total: i32 = 0;
    for (values) |v| total += v;
    return total;
}

// 配列を直接渡す(サイズ固定)
fn sum_fixed(values: [10]i32) i32 { ... }

// many-timeポインタ(C FFI以外では避ける)
fn sum_unsafe(values: [*]i32, len: usize) i32 { ... }
  • スライスは柔軟(任意の長さ)
  • 境界チェックがある(安全)
  • 長さ情報を含む

ローカル変数

pub fn main() void {
    // サイズが固定なら配列
    var buffer: [1024]u8 = undefined;
    
    // 可変長ならスライス
    var slice: []u8 = buffer[0..512];
    
    // ローカルで多項目ポインタは不要
}

C FFI

// C関数: int read(int fd, void *buf, size_t count);
extern fn read(fd: i32, buf: [*]u8, count: usize) isize;

// Zigラッパー(安全化)
fn readSafe(fd: i32, buf: []u8) !usize {
    const n = read(fd, buf.ptr, buf.len);
    if (n < 0) return error.ReadFailed;
    return @intCast(n);
}

C FFIでは多項目ポインタを使い、Zigの公開APIではスライスに

センチネル終端

// C文字列(ヌル終端)
const c_str: [*:0]const u8 = "hello";

// Zig文字列(長さ既知)
const zig_str: []const u8 = "hello";

// 変換
pub fn toCString(s: []const u8) ![*:0]u8 {
    var buf = try allocator.alloc(u8, s.len + 1);
    @memcpy(buf[0..s.len], s);
    buf[s.len] = 0;
    return buf[0.. :0];
}

まとめ

Zigの配列・ポインタ・many-timeポインタ・スライスが似ているのは、同じメモリモデルを段階的に制限しているといえるからです。
Zigにおいては型が何を保証するかという観点が大切で、非常に理にかなっていることが分かります。

使い分けの原則

所有する → 配列 [N]T
安全に借用 → スライス []T
C FFI → many-timeポインタ [*]T
単一要素参照 → ポインタ *T

他言語経験者への対応表
概念 C/C++ Rust Zig
所有配列 T[N] [T; N] [N]T
配列ビュー std::span<T> &[T] []T
生ポインタ T* *const T [*]T
単一参照 T* &T *T
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?