LoginSignup
7
8

More than 3 years have passed since last update.

【Go】Pointer, Array, String (ポインタと配列と文字列と)

Last updated at Posted at 2021-03-08

Goでプログラミングの基礎を学ぶシリーズ

スクールでは教えてくれないプログラミングの基礎、データ構造とアルゴリズムをGoで学んでいくシリーズです。
そのデータ構造がどのようなものであるかは、理解を助けてくれるサイトを紹介しつつ、簡単に説明に留めさせていただきます。(ご自身でも調べてみてください!)
筆者自身、Exerciseに取り組みながら理解を深めていったので、コードの解説を中心に記事を書いていきたいと思います。

タイトル
#0 はじめに (環境構築と勉強方法)
#1 Pointer, Array, String (ポインタと配列と文字列と) ☜ here
#2 File operations (ファイル操作)
#3 Linked List (連結リスト)
#4 Stack & Queue (スタックとキュー)
#5 Search algorithms (探索アルゴリズム)
#6 Tree (木構造)
#7 Sorting algorithms (ソートアルゴリズム)
#8 String pattern matching algorithms (文字列探索アルゴリズム)

初回である今回は、#1 Pointer, Array, String, Pointer (ポインタと配列と文字列と) です。
これらがわからないと、#2以降のコードが全く書けないのです。

A Tour of GoでいうところとのBasicsまで、
【Go】基本文法シリーズ (筆者も大変お世話になっている記事シリーズで、A Tour of Goをかなり噛み砕いて解説してくれています)でいうところの、
1. 基礎
2. フロー制御文
3. ポインタ・構造体
4. 配列・スライス
まではコードが書けるよ!という状態で、データ構造とアルゴリズムに臨みましょう。

メモリとアドレス

Pointer, Array, String 全てに通じることなので、少し説明します。
かなりざっくりとした言い方になりますが、コンピュータはCPUがメモリ上の命令を実行することで動いています。

メモリのイメージ ☟
スクリーンショット 2021-03-08 1.26.06.png

ビット(bit)とは、
コンピューターの中で扱うデータの最小単位。1ビットで2進数の1桁が0か1かを表せる。
文字や画像など、すべての情報はビットの組み合わせで表現されている。
ただし、実際にコンピューターが扱う情報の単位は8ビットを1組にしたバイト単位で表すことが多い。1バイト=8ビットである。
引用: コトバンク

参考: コンピューターは0と1だけで計算している?

1byteごとに区分番号が割り振られており、それが アドレス です。
メモリ空間は1byteごとに番号で区分されているとも言い換えることができます。

「0と1なのはわかったけれど、数字ではない"a"とか"b"とかはどうやって表すの?」
と疑問に思う方は、ASCIIコード表をみるとイメージしやすいかもしれません。
参考: 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 アスキーコード (ASCIIコード)

Pointer / ポインタ

ポインタとは、何かの位置を指し示すための仕組みや道具などのこと。
プログラミングでは、変数や関数などが置かれたメインメモリ上の番地などを格納する特殊な変数のことをポインタという。
引用: IT用語辞典 e-Words

ポインタは、メモリにある値を読み書きする時に使います。
メモリにある値が直接読み書きできることの何が嬉しいかというと、関数をまたいでメモリにアクセスすることが可能になります。
参考: C言語 ポインタのメリットと必要性【なぜなぜから真相に迫る】

どういうことか。

package main

import "fmt"

func main() {
    a, b, c := 1, 1, 1

    // main関数内で a に 2 をかける
    a *= 2
    fmt.Printf("main() aの値: %d\n\n", a) // 2

    // multiply2関数内で b に 2 をかける. 引数には b の値を渡す.
    multiply2(b)
    fmt.Printf("main() bの値: %d, bのアドレス: %p\n\n", b, &b)

    // multiply2pointer関数内で c に 2 をかける. 引数には c のアドレスを渡す.
    multiply2pointer(&c)
    fmt.Printf("main() cの値: %d, cのアドレス: %p\n\n", c, &c)
}

func multiply2(b int) {
    b *= 2
    fmt.Printf("multiply2() bの値: %d, bのアドレス: %p\n", b, &b)
}

func multiply2pointer(c *int) {
    *c *= 2 // c のアドレスに格納されている値: 1 に 2 をかける.
    fmt.Printf("multiply2pointer() cの値: %d, cのアドレス: %p\n", *c, c)
}

// output >
// main() aの値: 2

// multiply2() bの値: 2, bのアドレス: 0xc000016120
// main() bの値: 1, bのアドレス: 0xc0000160f8

// multiply2pointer() cの値: 2, cのアドレス: 0xc000016100
// main() cの値: 2, cのアドレス: 0xc000016100

Goでのポインタの書き方などは、以下の記事がとてもわかりやすかったので、こちらを見ていただきたいです。
参考:
Goで学ぶポインタとアドレス
【Go】基本文法③(ポインタ・構造体)

ざっとコードにしてしまいましたが、
bのケースは値渡しと呼ばれるもので、main()内のbと、multiply2()内のbは、1という値は同じですが、異なるメモリに格納されているデータを参照しています。
cのケースはポインタ渡しと呼ばれるもので、main()内のcのアドレスをmultiply2pointer()に渡しているので、同じアドレスに格納されているデータを参照しています。

これから学ぶデータ構造とアルゴリズムのコードでも、ポインタをよく使います。
main()にすべての処理を書いてしまうと、同じ処理を何度も書かなくてはいけなくなってしまったり、main()自体が長くなり可読性が下がってしまったりする場合、ある機能に特化した関数を切り出していきます。(上の例だとmultiply2()のような感じです)
異なる関数で同じデータを参照する必要がある場合、それらのデータはポインタで扱っています。(あくまでポインタの一使用例です)

💻 Exercise

3つの変数a,b,cについて、a=b, b=c, c=aとなるような関数をポインタで実装しましょう。

☟ 解答例

package main

import "fmt"

func main() {
    a, b, c := 1, 2, 3
    swap1(a, b, c)
    fmt.Println(a, b, c) // 1 2 3

    swap2(&a, &b, &c)
    fmt.Println(a, b, c) // 2 3 1
}

func swap1(a, b, c int) {
    a, b, c = b, c, a
}

func swap2(a, b, c *int) {
    *a, *b, *c = *b, *c, *a
}

Array / 配列

Array(配列)はGoでは固定長のものを指します。
可変長のものはSlice(スライス)といいます。

コードの書き方は、以下の記事をご覧ください。
参考:
【Go】基本文法④(配列・スライス)
Go Slices: usage and internals (The Go Blog)
スクリーンショット 2021-03-08 1.01.01.png
Go Slicesのブログにもある通り、Sliceは、配列へのポインタ ptr、セグメントの長さ len、およびその容量 cap で構成されます。
配列へのポインタは、通常先頭の要素 slice[0] を指します。
参考: Try Golang! Sliceってポインタなの?それともポインタじゃないの?

💻 Exercise

任意の英語の文章(すべて小文字)で、各文字が出現する回数を出力するプログラムを作成しましょう。

例:
word: hello world

output > 
letter: h count: 1
letter: e count: 1
letter: l count: 3
letter: o count: 2
letter:   count: 1
letter: w count: 1
letter: r count: 1
letter: d count: 1

☟ 解答例

package main

import "fmt"

func main() {
    word := "hello world"

    var elements []string  // 文字を入れておくSlice
    var elementCount []int  // 文字が何回出現したかを入れておくSlice

    for _, w := range word {
        letter := fmt.Sprintf("%c", w)  // w は rune型(後述) なので、 string型 に変換
        if !isExist(letter, elements) {  // 初めて出現した文字について実行
            elements = append(elements, letter) 
            elementCount = append(elementCount, count(letter, word))
        }
    }

    for i := 0; i < len(elements); i++ {
        fmt.Printf("letter: %s count: %d\n", elements[i], elementCount[i])
    }
}

// 文字が word のなかに何回出現するかカウントする
func count(letter string, word string) int {
    count := 0
    for _, w := range word {
        l := fmt.Sprintf("%c", w)
        if l == letter {
            count++
        }
    }
    return count
}

// すでにカウントした文字かどうかの判定
func isExist(letter string, elements []string) bool {
    for _, e := range elements {
        if letter == e {
            return true
        }
    }
    return false
}

// output >
// letter: h count: 1
// letter: e count: 1
// letter: l count: 3
// letter: o count: 2
// letter:   count: 1
// letter: w count: 1
// letter: r count: 1
// letter: d count: 1

ポイントは、wordを頭から見ていき、初めて出現した文字の場合には、その文字がwordの中に何回出現するかカウントしてしまうというところですかね。
!isExistで、すでにカウントした文字かどうかを判定し、カウントが重複しないようにしています。

上記の例ではSliceのみで実装していますが、mapを使えばもっとスタイリッシュに書けそうな気がします。
参考: 【Go】基本文法⑤(連想配列・ Range)

package main

import "fmt"

func main() {
    word := "hello world"

    elements := make(map[string]int)
    for _, w := range word {
        key := fmt.Sprintf("%c", w)
        if _, ok := elements[key]; ok {
            elements[key]++
        } else {
            elements[key] = 1
        }
    }

    for k, v := range elements {  // elements ▶︎ map[ :1 d:1 e:1 h:1 l:3 o:2 r:1 w:1]
        fmt.Printf("letter: %s count: %d\n", k, v)
    }
}

2021/03/09追記 : @kts_h さんのコメントのコードの方がよりスタイリッシュでわかりやすいので、そちらも参考にしてください!

String / 文字列

実はArrayのExerciseでも出てきていますが、Goにはcode pointを単位として文字を扱う rune型 が用意されています。
詳しくはこちらの記事をご覧ください。
参考: Goのruneを理解するためのUnicode知識

メモリへの格納は配列と同じようなイメージになりますが、stringは terminator (終端null文字)があります。
スクリーンショット 2021-03-08 2.00.17.png

💻 Exercise

単語と2つの文字を入力させ、単語に含まれる入力した1つ目の文字を、入力した2つ目の文字に変換して出力するプログラムを作成しましょう。

例:
input > papa p m
output > mama

☟ 解答例

package main

import (
    "fmt"
)

func main() {
    var word string
    var b, a rune
    fmt.Println("Please input word and letters to convert. ex) papa p m\n> ")
    fmt.Scanf("%s %c %c\n", &word, &b, &a)

    for _, w := range word {
        if w == b {
            w = a
        }
        fmt.Printf("%c", w)
    }
}

// Please input word and letters to convert. ex) papa p m
// > 
// papa p m

// output >
// mama

おわりに

Exerciseの解答例はあくまで例なので、もっといい書き方あるよ!という方、ぜひコメントをお寄せください!
説明についても、筆者自身が初心者であるため、ご指摘や補足は大歓迎でございます。

株式会社Link Sportsでは、あらゆるスポーツを楽しむ人たちに送る、チームマネジメントアプリを開発しています。
未経験でも経験豊富なエンジニアの方でも、スポーツ好きなら活躍できる環境があります!
絶賛エンジニア募集中です!
Wantedly ▶︎ https://www.wantedly.com/projects/324177
Green ▶︎ https://www.green-japan.com/job/82003

次回は、データ構造とアルゴリズム#2 File operations(ファイル操作)です。
乞うご期待。

7
8
2

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
7
8