解答7: ポインタとスライスの実践

概要

この解答では、Zigのメモリ操作機能を実装します。ポインタの基本操作から、スライスを使った効率的な配列処理、センチネル終端文字列の操作、そして安全なメモリ管理まで、実践的なコードを通して学びます。

Part 1: ポインタ基礎

完全な実装

const std = @import("std");

// 2つの値を交換
fn swap(comptime T: type, a: *T, b: *T) void {
    const temp = a.*;
    a.* = b.*;
    b.* = temp;
}

// ポインタを使って配列の要素を変更
fn multiplyByTwo(array: []i32) void {
    for (array) |*item| {
        item.* *= 2;
    }
}

// ポインタ演算で配列の合計を計算
fn sumUsingPointer(ptr: [*]const i32, len: usize) i64 {
    var total: i64 = 0;
    var i: usize = 0;

    while (i < len) : (i += 1) {
        total += ptr[i];
    }

    return total;
}

// 配列内の最大値のポインタを返す
fn findMaxPtr(array: []i32) ?*i32 {
    if (array.len == 0) return null;

    var max_ptr: *i32 = &array[0];

    for (array[1..]) |*item| {
        if (item.* > max_ptr.*) {
            max_ptr = item;
        }
    }

    return max_ptr;
}

pub fn main() void {
    std.debug.print("=== Swap ===\n", .{});
    var x: i32 = 10;
    var y: i32 = 20;
    std.debug.print("Before: x={}, y={}\n", .{x, y});
    swap(i32, &x, &y);
    std.debug.print("After: x={}, y={}\n", .{x, y});

    std.debug.print("\n=== Multiply by Two ===\n", .{});
    var numbers = [_]i32{ 1, 2, 3, 4, 5 };
    std.debug.print("Before: ", .{});
    for (numbers) |n| std.debug.print("{} ", .{n});
    std.debug.print("\n", .{});
    multiplyByTwo(&numbers);
    std.debug.print("After: ", .{});
    for (numbers) |n| std.debug.print("{} ", .{n});
    std.debug.print("\n", .{});

    std.debug.print("\n=== Sum Using Pointer ===\n", .{});
    const arr = [_]i32{ 1, 2, 3, 4, 5 };
    const sum = sumUsingPointer(&arr, arr.len);
    std.debug.print("Sum: {}\n", .{sum});

    std.debug.print("\n=== Find Max Pointer ===\n", .{});
    if (findMaxPtr(&numbers)) |max_ptr| {
        std.debug.print("Max value: {}\n", .{max_ptr.*});
        max_ptr.* = 999;
        std.debug.print("Modified: ", .{});
        for (numbers) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
    }
}

実装のポイント

  • swap関数: ジェネリックにして任意の型で動作するようにしています。
  • ポインタ演算: ptr[i]の形式で配列要素にアクセスします。
  • ポインタの返却: 最大値への参照を返すことで、呼び出し側で値を変更できます。
  • Part 2: スライス操作

    完全な実装

    const std = @import("std");
    
    // スライスを反転
    fn reverseSlice(comptime T: type, slice: []T) void {
        if (slice.len == 0) return;
    
        var left: usize = 0;
        var right: usize = slice.len - 1;
    
        while (left < right) {
            const temp = slice[left];
            slice[left] = slice[right];
            slice[right] = temp;
    
            left += 1;
            right -= 1;
        }
    }
    
    // スライス内の要素を検索
    fn findInSlice(comptime T: type, slice: []const T, target: T) ?usize {
        for (slice, 0..) |item, index| {
            if (item == target) {
                return index;
            }
        }
        return null;
    }
    
    // 2つのスライスを連結(allocatorを使用)
    fn concatSlices(
        comptime T: type,
        allocator: std.mem.Allocator,
        slice1: []const T,
        slice2: []const T,
    ) ![]T {
        const result = try allocator.alloc(T, slice1.len + slice2.len);
    
        @memcpy(result[0..slice1.len], slice1);
        @memcpy(result[slice1.len..], slice2);
    
        return result;
    }
    
    // スライスをコピー
    fn copySlice(comptime T: type, dest: []T, src: []const T) usize {
        const copy_len = @min(dest.len, src.len);
    
        for (0..copy_len) |i| {
            dest[i] = src[i];
        }
    
        return copy_len;
    }
    
    // スライスが等しいか判定
    fn slicesEqual(comptime T: type, slice1: []const T, slice2: []const T) bool {
        if (slice1.len != slice2.len) return false;
    
        for (slice1, slice2) |item1, item2| {
            if (item1 != item2) return false;
        }
    
        return true;
    }
    
    pub fn main() !void {
        const allocator = std.heap.page_allocator;
    
        std.debug.print("=== Reverse Slice ===\n", .{});
        var numbers = [_]i32{ 1, 2, 3, 4, 5 };
        std.debug.print("Before: ", .{});
        for (numbers) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
        reverseSlice(i32, &numbers);
        std.debug.print("After: ", .{});
        for (numbers) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
    
        std.debug.print("\n=== Find in Slice ===\n", .{});
        if (findInSlice(i32, &numbers, 3)) |index| {
            std.debug.print("Found 3 at index {}\n", .{index});
        } else {
            std.debug.print("3 not found\n", .{});
        }
    
        std.debug.print("\n=== Concat Slices ===\n", .{});
        const arr1 = [_]i32{ 1, 2, 3 };
        const arr2 = [_]i32{ 4, 5, 6 };
        const concat = try concatSlices(i32, allocator, &arr1, &arr2);
        defer allocator.free(concat);
        std.debug.print("Concatenated: ", .{});
        for (concat) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
    
        std.debug.print("\n=== Copy Slice ===\n", .{});
        var dest: [3]i32 = undefined;
        const copied = copySlice(i32, &dest, &arr1);
        std.debug.print("Copied {} elements: ", .{copied});
        for (dest) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
    
        std.debug.print("\n=== Slices Equal ===\n", .{});
        const eq = slicesEqual(i32, &arr1, &arr1);
        std.debug.print("arr1 == arr1: {}\n", .{eq});
    }
    

    実装のポイント

  • 反転アルゴリズム: 両端から中央に向かって交換していきます。
  • @memcpy: 効率的なメモリコピーのための組み込み関数を使用します。
  • 動的メモリ管理: concat関数では新しいメモリを確保し、呼び出し側で解放します。
  • Part 3: 文字列操作

    完全な実装

    const std = @import("std");
    
    // 文字列の長さを計算
    fn stringLength(str: [*:0]const u8) usize {
        var len: usize = 0;
        while (str[len] != 0) : (len += 1) {}
        return len;
    }
    
    // 文字列をコピー
    fn stringCopy(dest: [*]u8, src: [*:0]const u8) void {
        var i: usize = 0;
        while (src[i] != 0) : (i += 1) {
            dest[i] = src[i];
        }
        dest[i] = 0; // null終端を追加
    }
    
    // 文字列を連結
    fn stringConcat(dest: [*]u8, src1: [*:0]const u8, src2: [*:0]const u8) void {
        var i: usize = 0;
    
        // src1をコピー
        while (src1[i] != 0) : (i += 1) {
            dest[i] = src1[i];
        }
    
        // src2をコピー
        var j: usize = 0;
        while (src2[j] != 0) : (j += 1) {
            dest[i + j] = src2[j];
        }
    
        dest[i + j] = 0; // null終端を追加
    }
    
    // 文字列を比較
    fn stringCompare(str1: [*:0]const u8, str2: [*:0]const u8) i32 {
        var i: usize = 0;
    
        while (str1[i] != 0 and str2[i] != 0) : (i += 1) {
            if (str1[i] < str2[i]) {
                return -1;
            } else if (str1[i] > str2[i]) {
                return 1;
            }
        }
    
        // 一方が先に終了した場合
        if (str1[i] == 0 and str2[i] == 0) {
            return 0; // 等しい
        } else if (str1[i] == 0) {
            return -1; // str1が短い
        } else {
            return 1; // str2が短い
        }
    }
    
    // 文字列内で文字を検索
    fn stringFindChar(str: [*:0]const u8, ch: u8) ?usize {
        var i: usize = 0;
    
        while (str[i] != 0) : (i += 1) {
            if (str[i] == ch) {
                return i;
            }
        }
    
        return null;
    }
    
    pub fn main() void {
        std.debug.print("=== String Length ===\n", .{});
        const str1: [*:0]const u8 = "Hello, Zig!";
        const len = stringLength(str1);
        std.debug.print("Length of '{s}': {}\n", .{str1, len});
    
        std.debug.print("\n=== String Copy ===\n", .{});
        var dest: [100]u8 = undefined;
        stringCopy(&dest, str1);
        std.debug.print("Copied: {s}\n", .{dest[0..len]});
    
        std.debug.print("\n=== String Concat ===\n", .{});
        const src1: [*:0]const u8 = "Hello";
        const src2: [*:0]const u8 = " World";
        var concat: [100]u8 = undefined;
        stringConcat(&concat, src1, src2);
        const concat_len = stringLength(&concat);
        std.debug.print("Concatenated: {s}\n", .{concat[0..concat_len]});
    
        std.debug.print("\n=== String Compare ===\n", .{});
        const cmp1 = stringCompare("abc", "abc");
        const cmp2 = stringCompare("abc", "abd");
        const cmp3 = stringCompare("abd", "abc");
        std.debug.print("'abc' vs 'abc': {}\n", .{cmp1});
        std.debug.print("'abc' vs 'abd': {}\n", .{cmp2});
        std.debug.print("'abd' vs 'abc': {}\n", .{cmp3});
    
        std.debug.print("\n=== String Find Char ===\n", .{});
        if (stringFindChar(str1, 'Z')) |index| {
            std.debug.print("Found 'Z' at index {}\n", .{index});
        } else {
            std.debug.print("'Z' not found\n", .{});
        }
    
        if (stringFindChar(str1, 'H')) |index| {
            std.debug.print("Found 'H' at index {}\n", .{index});
        }
    }
    

    実装のポイント

  • センチネル終端: [*:0]型は0で終端する文字列を表します。
  • 文字列比較: 辞書順比較で-1, 0, 1を返します。
  • バッファ管理: 呼び出し側で十分なバッファを確保する責任があります。
  • Part 4: メモリ管理

    完全な実装

    const std = @import("std");
    
    // 動的配列の作成と初期化
    fn createArray(comptime T: type, allocator: std.mem.Allocator, size: usize, value: T) ![]T {
        const array = try allocator.alloc(T, size);
    
        for (array) |*item| {
            item.* = value;
        }
    
        return array;
    }
    
    // 配列のリサイズ
    fn resizeArray(
        comptime T: type,
        allocator: std.mem.Allocator,
        old_array: []T,
        new_size: usize,
    ) ![]T {
        const new_array = try allocator.alloc(T, new_size);
    
        // 既存の要素をコピー(新しいサイズまで)
        const copy_len = @min(old_array.len, new_size);
        @memcpy(new_array[0..copy_len], old_array[0..copy_len]);
    
        // 新しい要素は未初期化(または0で初期化)
        if (new_size > old_array.len) {
            for (new_array[old_array.len..]) |*item| {
                item.* = undefined; // または0などのデフォルト値
            }
        }
    
        return new_array;
    }
    
    // 安全な配列アクセス
    fn safeGet(comptime T: type, array: []const T, index: usize) ?T {
        if (index >= array.len) {
            return null;
        }
        return array[index];
    }
    
    // 安全な配列設定
    fn safeSet(comptime T: type, array: []T, index: usize, value: T) bool {
        if (index >= array.len) {
            return false;
        }
        array[index] = value;
        return true;
    }
    
    // メモリプールの実装
    const MemoryPool = struct {
        buffer: []u8,
        offset: usize,
    
        pub fn init(buffer: []u8) MemoryPool {
            return MemoryPool{
                .buffer = buffer,
                .offset = 0,
            };
        }
    
        pub fn alloc(self: *MemoryPool, size: usize) ?[]u8 {
            if (self.offset + size > self.buffer.len) {
                return null; // バッファが満杯
            }
    
            const result = self.buffer[self.offset..self.offset + size];
            self.offset += size;
            return result;
        }
    
        pub fn reset(self: *MemoryPool) void {
            self.offset = 0;
        }
    
        pub fn remaining(self: MemoryPool) usize {
            return self.buffer.len - self.offset;
        }
    };
    
    pub fn main() !void {
        const allocator = std.heap.page_allocator;
    
        std.debug.print("=== Create Array ===\n", .{});
        const arr = try createArray(i32, allocator, 5, 42);
        defer allocator.free(arr);
        std.debug.print("Array: ", .{});
        for (arr) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
    
        std.debug.print("\n=== Resize Array ===\n", .{});
        const resized = try resizeArray(i32, allocator, arr, 10);
        defer allocator.free(resized);
        std.debug.print("Resized to {} elements\n", .{resized.len});
        std.debug.print("First 5 elements: ", .{});
        for (resized[0..5]) |n| std.debug.print("{} ", .{n});
        std.debug.print("\n", .{});
    
        std.debug.print("\n=== Safe Get/Set ===\n", .{});
        if (safeGet(i32, arr, 2)) |value| {
            std.debug.print("arr[2] = {}\n", .{value});
        }
        if (safeGet(i32, arr, 100)) |value| {
            std.debug.print("arr[100] = {}\n", .{value});
        } else {
            std.debug.print("arr[100] is out of bounds\n", .{});
        }
    
        const set_ok1 = safeSet(i32, arr, 2, 100);
        const set_ok2 = safeSet(i32, arr, 100, 100);
        std.debug.print("Set arr[2] = 100: {}\n", .{set_ok1});
        std.debug.print("Set arr[100] = 100: {}\n", .{set_ok2});
    
        std.debug.print("\n=== Memory Pool ===\n", .{});
        var buffer: [1024]u8 = undefined;
        var pool = MemoryPool.init(&buffer);
    
        std.debug.print("Pool size: {} bytes\n", .{pool.buffer.len});
    
        if (pool.alloc(100)) |mem1| {
            std.debug.print("Allocated {} bytes, remaining: {}\n", .{mem1.len, pool.remaining()});
        }
    
        if (pool.alloc(200)) |mem2| {
            std.debug.print("Allocated {} bytes, remaining: {}\n", .{mem2.len, pool.remaining()});
        }
    
        std.debug.print("Total used: {} bytes\n", .{pool.offset});
    
        pool.reset();
        std.debug.print("Pool reset, remaining: {} bytes\n", .{pool.remaining()});
    }
    

    実装のポイント

  • リサイズ: 新しい配列を確保し、古い配列の内容をコピーします。
  • 安全性: 境界チェックを行い、不正なアクセスを防ぎます。
  • メモリプール: 単純なバンプアロケータを実装し、高速な割り当てを実現します。
  • ボーナス課題の解答

    Bonus 1: スマートポインタの実装

    const std = @import("std");
    
    fn RefCounted(comptime T: type) type {
        return struct {
            const Self = @This();
    
            value: T,
            ref_count: usize,
            allocator: std.mem.Allocator,
    
            pub fn init(allocator: std.mem.Allocator, value: T) !*Self {
                const self = try allocator.create(Self);
                self.* = Self{
                    .value = value,
                    .ref_count = 1,
                    .allocator = allocator,
                };
                return self;
            }
    
            pub fn addRef(self: *Self) void {
                self.ref_count += 1;
                std.debug.print("RefCount incremented to {}\n", .{self.ref_count});
            }
    
            pub fn release(self: *Self) void {
                self.ref_count -= 1;
                std.debug.print("RefCount decremented to {}\n", .{self.ref_count});
    
                if (self.ref_count == 0) {
                    std.debug.print("Freeing memory\n", .{});
                    const allocator = self.allocator;
                    allocator.destroy(self);
                }
            }
        };
    }
    
    pub fn main() !void {
        const allocator = std.heap.page_allocator;
    
        std.debug.print("Creating RefCounted<i32>\n", .{});
        const rc1 = try RefCounted(i32).init(allocator, 42);
        std.debug.print("Value: {}, RefCount: {}\n", .{rc1.value, rc1.ref_count});
    
        rc1.addRef();
        rc1.addRef();
    
        rc1.release();
        rc1.release();
        rc1.release();
    }
    

    Bonus 2: リングバッファの実装

    const std = @import("std");
    
    fn RingBuffer(comptime T: type, comptime size: usize) type {
        return struct {
            const Self = @This();
    
            buffer: [size]T,
            read_pos: usize,
            write_pos: usize,
            count: usize,
    
            pub fn init() Self {
                return Self{
                    .buffer = undefined,
                    .read_pos = 0,
                    .write_pos = 0,
                    .count = 0,
                };
            }
    
            pub fn push(self: *Self, value: T) bool {
                if (self.isFull()) {
                    return false;
                }
    
                self.buffer[self.write_pos] = value;
                self.write_pos = (self.write_pos + 1) % size;
                self.count += 1;
    
                return true;
            }
    
            pub fn pop(self: *Self) ?T {
                if (self.isEmpty()) {
                    return null;
                }
    
                const value = self.buffer[self.read_pos];
                self.read_pos = (self.read_pos + 1) % size;
                self.count -= 1;
    
                return value;
            }
    
            pub fn len(self: Self) usize {
                return self.count;
            }
    
            pub fn isEmpty(self: Self) bool {
                return self.count == 0;
            }
    
            pub fn isFull(self: Self) bool {
                return self.count == size;
            }
    
            pub fn peek(self: Self) ?T {
                if (self.isEmpty()) {
                    return null;
                }
                return self.buffer[self.read_pos];
            }
        };
    }
    
    pub fn main() void {
        var buffer = RingBuffer(i32, 5).init();
    
        std.debug.print("=== Push ===\n", .{});
        _ = buffer.push(1);
        _ = buffer.push(2);
        _ = buffer.push(3);
        std.debug.print("Pushed 3 elements, count: {}\n", .{buffer.len()});
    
        std.debug.print("\n=== Pop ===\n", .{});
        if (buffer.pop()) |value| {
            std.debug.print("Popped: {}\n", .{value});
        }
        std.debug.print("After pop, count: {}\n", .{buffer.len()});
    
        std.debug.print("\n=== Fill Buffer ===\n", .{});
        _ = buffer.push(4);
        _ = buffer.push(5);
        _ = buffer.push(6);
        std.debug.print("Is full: {}\n", .{buffer.isFull()});
    
        // 満杯なのでpushは失敗
        const push_result = buffer.push(7);
        std.debug.print("Try to push when full: {}\n", .{push_result});
    
        std.debug.print("\n=== Empty Buffer ===\n", .{});
        var count: usize = 0;
        while (buffer.pop()) |value| {
            std.debug.print("Popped: {}\n", .{value});
            count += 1;
        }
        std.debug.print("Popped {} elements\n", .{count});
        std.debug.print("Is empty: {}\n", .{buffer.isEmpty()});
    }
    

    よくある間違い

    1. ポインタの寿命管理

    // 悪い例: ローカル変数へのポインタを返す
    fn badPointer() *i32 {
        var x: i32 = 42;
        return &x; // 危険!関数を抜けるとxは破棄される
    }
    
    // 良い例: アロケータを使う
    fn goodPointer(allocator: std.mem.Allocator) !*i32 {
        const ptr = try allocator.create(i32);
        ptr.* = 42;
        return ptr;
    }
    

    2. センチネル終端の忘れ

    // 悪い例: null終端を忘れる
    fn badStringCopy(dest: [*]u8, src: [*:0]const u8) void {
        var i: usize = 0;
        while (src[i] != 0) : (i += 1) {
            dest[i] = src[i];
        }
        // null終端を忘れている!
    }
    
    // 良い例: null終端を追加
    fn goodStringCopy(dest: [*]u8, src: [*:0]const u8) void {
        var i: usize = 0;
        while (src[i] != 0) : (i += 1) {
            dest[i] = src[i];
        }
        dest[i] = 0; // null終端
    }
    

    3. バッファオーバーフロー

    // 悪い例: 境界チェックなし
    fn unsafeCopy(dest: []u8, src: []const u8) void {
        for (src, 0..) |byte, i| {
            dest[i] = byte; // destが小さい場合にクラッシュ
        }
    }
    
    // 良い例: 境界チェックあり
    fn safeCopy(dest: []u8, src: []const u8) usize {
        const len = @min(dest.len, src.len);
        @memcpy(dest[0..len], src[0..len]);
        return len;
    }
    

    発展課題

    1. アリーナアロケータの実装

    単純なアリーナアロケータを実装し、一括解放機能を追加してみましょう。

    2. UTF-8文字列操作

    UTF-8エンコーディングを考慮した文字列操作関数を実装してみましょう。

    3. スマートポインタの拡張

    弱参照(weak reference)をサポートしたスマートポインタを実装してみましょう。

    4. 循環バッファの拡張

    動的にサイズが変わる循環バッファを実装してみましょう。

    まとめ

    この解答では、以下の重要な概念を学びました:

  • ポインタ操作: 値の参照と変更、ポインタ演算
  • スライス処理: 効率的な配列操作、部分配列の扱い
  • 文字列操作: センチネル終端文字列の安全な扱い
  • メモリ管理: 動的メモリの確保と解放、メモリプール
  • 安全性: 境界チェック、null チェック、リソース管理

これらの技術を適切に使うことで、安全で効率的なシステムプログラミングが可能になります。