C言語での文字列とメモリの基礎
文字列の一部をコピーする操作を理解する前に、まず知っておくべきことがあります。
それは、「C言語で文字列がどうやってメモリに保存されているか」という仕組みです。
この基本を理解しておくと、文字列操作で起こるエラーの原因がわかるようになり、安全なプログラムを書けるようになります。
C言語の文字列とNULL文字の構造
C言語の文字列には、他の言語と大きく違う特徴があります。
それは、文字列の最後に必ず「NULL文字(\0)」という特殊な文字が付くということです。このNULL文字が「ここで文字列が終わりですよ」という目印になっています。
例えば、「ネコ」という文字列は、実際には「ネ」「コ」「\0」という3つの要素で構成されています。
文字列をコピーするときは、このNULL文字も含めて適切に処理することが大切です。
もしNULL文字がない場合は、プログラムは「どこまでが文字列なのか」がわからなくなってしまいます。
出力結果
文字列: ネコ
文字数: 6この例で「文字数: 6」と表示されるのは、日本語文字が複数バイトで構成されるためです。見た目は2文字ですが、実際のバイト数は6バイトになっています。
メモリ領域と配列・ポインタの関係
C言語で文字列を扱うとき、「配列」と「ポインタ」という2つの概念が出てきます。
実は、配列名はその配列の先頭要素のアドレス(場所)を表しているため、ポインタのように使うことができます。
どういうことかというと、例えば「イヌネコウサギ」という文字列があったとき、「ネコ」の部分から始まる文字列を表示したい場合、ポインタを使って開始位置を指定できるということです。
出力結果
配列での表示: イヌネコウサギ
ポインタでの表示: イヌネコウサギ
3番目から表示: ネコウサギptr + 6のように、ポインタに数値を足すことで「6バイト目から始まる文字列」を指定できます。
この仕組みを使うことで、文字列の途中から取り出すことができるのです。
文字列操作関数がメモリに与える影響
文字列をコピーする際に最も注意しなければならないのが、「コピー先のメモリ領域は十分に確保されているか」という点です。
もしコピー先の領域が小さすぎると、「バッファオーバーフロー」という深刻なエラーが発生します。バッファオーバーフローとは、用意されたメモリ領域(バッファ)を超えてデータを書き込んでしまう現象のことです。例えば、10文字分しか入らない配列に20文字の文字列をコピーしようとすると、余った10文字分が本来書き込んではいけないメモリ領域に書き込まれてしまいます。
プログラムが異常終了したり、予期しない動作を引き起こしたりする原因になるため、事前に必要な領域サイズをしっかり計算し、適切なメモリ管理を行うことが重要です。
出力結果
コピー結果: トラこの例では、destという配列に10バイトの領域を確保しています。「トラ」という文字列は6バイト程度なので、十分な余裕があります。
このように、コピー先には余裕を持った領域を確保することが安全なプログラムを作る第一歩です。
文字列を指定コピーする基本手法
いよいよ文字列の一部分を取り出す方法を学んでいきましょう。
文字列の一部分だけを取り出したい場合は、開始位置と文字数を指定してコピーを行います。
C言語には、このような「指定コピー」を行うための関数がいくつも用意されています。それぞれの特徴を理解して適切に使い分けられるようになりましょう。
文字列の指定コピーの考え方
文字列の一部分をコピーするには、次の3つの情報を決める必要があります。
- どこから始めるか(開始位置)
- 何文字コピーするか(文字数)
- どこに保存するか(コピー先の領域)
例えるなら、本の中から必要なページだけをコピーするときに、「何ページ目から」「何ページ分」「どの用紙に」を決めるのと同じです。
開始位置はポインタ演算で指定し、文字数は関数の引数で制御します。そして、コピー先には必要な分だけメモリ領域を確保しておきます。
出力結果
抜き出し結果: ゾウこの例では、animals + 9で9バイト目(「ゾウ」の位置)を指定し、そこから6バイト分をコピーしています。最後に\0を追加して、文字列として正しく終端させているのがポイントです。
strncpyによる部分コピー
strncpyは、文字列の指定した文字数だけをコピーする関数です。
この関数の便利なところは、第3引数でコピーする最大文字数を指定できることです。これにより、「コピー先の領域を超えてしまう」というミスを防ぐことができます。
ただし、注意点があります。指定した文字数の中にNULL文字が含まれていない場合、コピー先にNULL終端が付かないことがある可能性があるのです。
そのため、安全のために自分でNULL文字を追加することをおすすめします。
出力結果
結果: パンダstrncpyの書き方は次の通りです。
strncpy(dest, src, 9)で「最初の6バイト分をコピー」するように指示しています。
その後、dest[9] = '\0'で確実にNULL終端を付けることで、文字列として正しく扱えるようにしています。
memcpyを使ったメモリコピー
memcpyは、もう少し低レベルなメモリコピーを行う関数です。
「低レベル」というのは、「文字列専用ではなく、メモリそのものをコピーする」という意味です。文字列だけでなく、数値の配列や構造体など、どんなデータでもコピーできます。
strncpyと違って、memcpyは指定したバイト数分をただひたすらコピーするだけです。NULL終端の自動付与などはないため、文字列として使う場合は自分でNULL文字を追加する必要があります。
出力結果
結果: ウマmemcpyの書き方は次の通りです。strncpyと同じですね。
この例では、animals + 6で6バイト目(「ウマ」の位置)を指定し、そこから6バイト分をコピーしています。
memcpyは高速ですが、文字列としての安全性は自分で管理する必要があります。
開始位置と文字数を指定したコピー
実際のプログラムでは、「もっと自由に、文字列のどこからでも取り出せるようにしたい」ということがよくあります。
そんなとき、開始位置と文字数を両方とも指定できれば、文字列の任意の部分を自在に取り出せるようになります。
開始位置と文字数を指定したコピー
文字列の途中から必要な部分だけを取り出すには、ポインタ演算と適切な関数を組み合わせます。
ポインタ演算とは、「元の文字列のアドレスに、開始位置の数値を足す」という操作のことです。
こうすることで、「ここから始めてほしい」という位置を指定できます。
例えば、「オオカミキツネイタチ」という文字列から「キツネ」だけを取り出したい場合、「12バイト目から9バイト分」というように指定します。
出力結果
抜き出し結果: キツネtext + startの部分で「textの12バイト目から」という開始位置を指定し、lengthで「9バイト分コピーする」と指定しています。この組み合わせで、文字列の中から好きな部分を取り出せます。
開始位置をポインタで指定する方法
開始位置と文字数を組み合わせる処理を、関数として作ってみましょう。
こうすることで、何度も同じような処理を書かずに済みますし、コードも読みやすくなります。
さらに、範囲外アクセスを防ぐために、事前に文字列の長さをチェックしておくことで、より安全なプログラムになります。
出力結果
結果: モルモットextract_stringという関数を作ることで、「この文字列の、この位置から、この長さだけ取り出す」という処理を簡単に呼び出せるようになりました。
再利用できるコードにすることで、プログラム全体がすっきりします。
strncpyとmemcpyの使い分け
ここまでstrncpyとmemcpyの2つの関数を使ってきましたが、自分がコードを書く際には「どっちを使えばいいの?」と迷うかもしれません。
次のように使い分けるといいでしょう。
- strncpy:文字列をコピーしたいとき
- memcpy:文字列以外をコピーしたいときや、処理速度を重視したいとき
出力結果
strncpy: カンガルー
memcpy: カンガルーこの例では両方とも同じ結果になっていますが、それぞれの特性を理解しておきましょう。
関連する文字列操作関数との比較
C言語には、文字列をコピーするための関数がいくつかあります。それぞれの関数には特徴があり、使うべき場面が違います。
各関数の違いを理解して、目的に合った関数を選べるようになりましょう。
strcpyとstrncpyの違い
strcpyとstrncpyは、名前がよく似ていますが、大きな違いがあります。
strcpyは、文字列全体をコピーします。NULL文字まで自動的にコピーしてくれるため、シンプルで使いやすいです。ただし、コピー先の領域が足りないとバッファオーバーフローを起こす危険があります。
strncpyは、指定した文字数だけをコピーします。文字数制限があるため安全性が高く、「文字列の一部だけを取り出したい」という場面にも適しています。
出力結果
strcpy: ペンギンアザラシセイウチ
strncpy: ペンギンstrcpyは文字列全体をコピーするのに対し、strncpyは指定した12バイト分(「ペンギン」の部分)だけをコピーしています。
文字列を部分的に取り出したいときはstrncpyが便利です。
memcpy・memmoveの用途の違い
memcpyとmemmoveは、どちらもメモリをコピーする関数ですが、重要な違いがあります。
memcpyは高速なメモリコピーを行いますが、コピー元とコピー先の領域が重複している場合は正しく動作しません。
memmoveは領域の重複があっても安全にコピーできます。同じ配列内で文字列を移動させたいときなど、重複が避けられない場合に使います。
出力結果
移動結果: シャシャチイルカmemmoveの書き方は次の通りです。
今回のように、同じ配列内でデータを後ろにずらして「特定の文字を繰り返す」ような場合は、以下のように当てはめるとわかりやすくなります。
これを考慮すると、memmove(text + 6, text, 18);の意味はこうなります。
-
コピーする範囲を決める
まず、コピー元である text(0バイト目)から、指定した 18バイト分(「シャチイルカ」の6文字) をコピーの対象とします。 -
貼り付け先(開始地点)を決める
次に、text + 6(6バイト目)を貼り付けのスタート地点にします。ここは元々 「チ」 がいた場所です。 -
ずらしながら上書きする
memmove は、元の「シャ」を残したまま、その直後の「チ」がいた場所からコピーした18バイト分の文字を置きます。
コピー元とコピー先が重複していますが、memmoveを使うことで安全に処理できています。
指定コピーを安全に扱うための注意点
文字列のコピーは便利な機能ですが、使い方を間違えると深刻なエラーを引き起こす可能性があります。
「プログラムが突然止まってしまった」
「変な文字が表示される」
こういったトラブルの多くは、メモリ管理やNULL終端の扱いが原因です。
ここからは、安全なプログラムを作るために気を付けるべきポイントを見ていきましょう。
NULL終端の付与と扱いの落とし穴
strncpyを使うときに、初心者がよく陥る落とし穴があります。
それは、「指定した文字数の中にNULL文字が含まれていない場合、コピー先にNULL終端が付かない」ということです。
NULL終端がないと、プログラムはどこまでが文字列なのかがわからなくなり、変な文字が表示されたり、エラーが起きたりします。
安全なプログラムを作るには、strncpyを実行した後に必ず自分でNULL文字を追加するか、コピー先の配列を事前にゼロで初期化しておくことが大切です。
出力結果
安全なコピー: フクロウdest[9] = '\0'の一行を追加することで、文字列が確実に終端されます。たった一行ですが、この一行がプログラムの安定性を大きく左右します。
コピー範囲の妥当性チェックの実装
「コピーしようとした範囲が、元の文字列の長さを超えていた」というミスも、よくあるエラーの原因です。
例えば、10文字しかない文字列から20文字取り出そうとしたら、どうなるでしょうか?範囲外のメモリにアクセスしてしまい、プログラムがクラッシュする可能性があります。
これを防ぐには、コピーを実行する前に「コピー元の長さは十分か」「コピー先の容量は足りるか」をチェックすることが重要です。
出力結果
結果: アルパカsafe_copy関数では、コピーを実行する前に範囲をチェックしています。
この関数は、文字列の一部を安全にコピーするために、次の5つのパラメータを受け取っています。
- dest:コピー先の配列
- src:コピー元の文字列
- start:コピーを開始する位置(バイト単位)
- length:コピーする長さ(バイト数)
- dest_size:コピー先の配列のサイズ
関数内では、コピーを実行する前に2つの安全性チェックを行います、
チェック1:start + length > strlen(src)
→ コピー範囲が元の文字列の長さを超えていないか確認
チェック2:length >= dest_size
→ コピー先の配列に十分な空きがあるか確認(NULL終端分も考慮)
どちらかの条件に引っかかった場合は-1を返してエラーを知らせ、両方のチェックをパスした場合のみ実際のコピーを実行します。
例えば、このコード例で、safe_copy(result, animals, 12, 12, sizeof(result))の部分の値を次のように変更すると、エラーが発生します。
のように、開始位置を大きくしすぎた場合は、30バイト目から開始しようとしますが、元の文字列にそこまでの長さがありません。
のように、コピーする長さを長くしすぎた場合は、12バイト目から30バイトコピーしようとしますが、元の文字列が足りません。
このような事前チェックを入れることで、プログラムの信頼性が大幅に向上します。
memcpy使用時の注意点と安全な代替案
memcpyは高速ですが、使うときに気をつけるべき点があります。
それは、「コピー元とコピー先の領域が重複していると、正しく動作しない」ということです。例えば、同じ配列の中で文字列をずらすような処理では、memcpyを使うと予期しない結果になる可能性があります。
こんなときは、memmoveを使うのが安全です。memmoveは重複があっても正しく処理してくれます。
また、memcpyは文字列の終端処理を行わないため、文字列として使う場合は必ずNULL文字を手動で追加しましょう。
出力結果
memcpyの結果: バッファロー
memmoveの結果: バッファバッファロー前述したポイントが理解できれば、このコード例で実行していることもわかるかと思います。
例えば、memmoveの形は次の通りでしたね。
もし、出力結果がこうなるということがわからない場合は、もう一度復習してみましょう。
よくある質問(Q&A)
Q: 日本語文字列での文字数計算方法は?
A: 日本語はマルチバイト文字のため、strlen関数は文字数ではなくバイト数を返します。正確な文字数を得るには、ロケール設定とmblen関数を使用するか、UTF-8であればバイト単位で処理を行います。
【関連】C言語のstrlen関数の使い方は?初心者向けに徹底解説
Q: strncpyで配列サイズを超えたら?
A: コピー先の配列サイズを超える文字数を指定すると、バッファオーバーフローが発生してプログラムが異常終了する可能性があります。事前にサイズをチェックするか、配列サイズ以下の値を指定しましょう。
Q: memcpyとstrncpyの処理速度の違いは?
A: memcpyの方が一般的に高速です。strncpyは文字列処理のための追加チェックを行うため、純粋なメモリコピーであるmemcpyより若干遅くなります。ただし、実用上の差はわずかです。
Q: コピー後のNULL終端確認方法は?
A: コピー先の配列の最後の文字がNULL文字かどうかを直接確認するか、strlen関数で期待する長さと一致するかをチェックします。安全策として、常に手動でNULL終端を付与することを推奨します。
出力結果
正しく終端されていますQ: 文字列の一部を置換する方法は?
A: まず置換したい位置を特定し、その位置以降を一時的に保存してから、新しい文字列をコピーし、最後に保存した部分を連結します。strncpyやmemcpyを組み合わせて実現できます。
まとめ
文字列の指定コピーは、プログラミングにおいて頻繁に使用される基本的な操作です。
関数の使い方や安全な実装方法を理解することで、効率的で信頼性の高いプログラムを作成できます。
この記事で学んだポイントを振り返ってみましょう。
文字列指定コピーの重要なポイント
-
strncpyとmemcpyの使い分け
文字列専用ならstrncpy、高速なメモリコピーならmemcpyを選びましょう。 -
開始位置と文字数の指定方法
ポインタ演算で開始位置を指定し、関数の引数で文字数を制御します。 -
NULL終端の付与
strncpyやmemcpyの後は、手動でNULL文字を追加しましょう。 -
バッファオーバーフロー対策
コピーする前に範囲をチェックし、配列サイズを超えないように注意しましょう。 -
領域重複時の安全な処理
コピー元とコピー先が重複する場合は、memmoveを使用しましょう。
初心者の方が特に注意すべき点は、NULL終端の付け忘れとバッファオーバーフローです。
コピー範囲を事前にチェックし、必ず手動でNULL終端を付与するように心がけましょう。
実際の開発では、文字列の一部を取り出す処理、入力値の検証、配列やリストの全要素処理など、さまざまな場面で文字列の指定コピーを使います。
この記事で学んだ基本をしっかり身に付けて、実践的なプログラムに活かしていきましょう。