解答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});
}
実装のポイント
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で終端する文字列を表します。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. 循環バッファの拡張
動的にサイズが変わる循環バッファを実装してみましょう。
まとめ
この解答では、以下の重要な概念を学びました:
これらの技術を適切に使うことで、安全で効率的なシステムプログラミングが可能になります。