C言語をはじめて学ぶときに、文字列の扱いは多くの人がつまずくポイントです。
中でも文字列をコピーする関数strcpyは便利な一方で、仕組みを理解していないと簡単にエラーやバグを生み出してしまいます。
本記事では、char配列と文字列リテラルの違いから出発し、strcpyの正しい使い方と注意点、よくあるエラーとデバッグ方法まで、初心者向けに丁寧に解説します。
strcpyとは
strcpyとは何をする関数か
strcpyは、C言語で文字列をコピーする標準ライブラリ関数です。
標準ヘッダファイルstring.hで宣言されており、主にある文字列を別の領域にそのまま複製したいときに使います。
strcpyの役割を一言で言うと、「コピー先ポインタが指すメモリ領域に、コピー元の文字列(終端文字\0まで)を順番に書き写す」ことです。
具体的には、次のような処理を行います。
- コピー元文字列を先頭から1文字ずつ読み取る
- その文字をコピー先の領域に1文字ずつ書き込む
- コピー元の
'\0'(終端文字)も含めてコピーする
このように、「文字列全体を丸ごとコピーする」のがstrcpyです。
strcpyの基本的な書式と使い方
まずは、strcpyのプロトタイプ宣言と簡単な使用例を確認します。
strcpyの宣言
C言語の標準ライブラリでは、strcpyは次のように宣言されています。
char *strcpy(char *dest, const char *src);
ここで重要なのは次の3点です。
destはコピー先を指すポインタ(書き込み可能な領域である必要があります)srcはコピー元を指すポインタ(読み取り専用でもよいです)- 戻り値は
destと同じアドレスが返されます(主にチェインして使うときに便利ですが、初心者のうちは気にしなくても構いません)
最小限のサンプルコード
#include <stdio.h> // printfを使うためのヘッダ
#include <string.h> // strcpyを使うためのヘッダ
int main(void) {
// コピー先のchar配列(バッファ)を十分なサイズで用意
char dest[16];
// コピー元の文字列リテラル
const char *src = "Hello";
// strcpyでsrcの文字列をdestにコピー
strcpy(dest, src);
// 結果を表示
printf("コピー結果: %s\n", dest);
return 0;
}
コピー結果: Hello
この例では、srcが指す”Hello”という文字列を、destというchar配列にコピーしています。
コピーが成功すると、destは"Hello"という文字列として扱えるようになります。
strcpyでコピーされるのは「終端文字\0まで」
C言語の文字列は、最後に'\0'(ヌル文字、終端文字)が付いている配列として表現されます。
strcpyは、この終端文字'\0'が現れるまで文字をコピーし続け、最後に'\0'自身もコピーします。
実際にどのようにコピーされるかをイメージしやすいように、簡単なデモコードで確認してみます。
#include <stdio.h>
#include <string.h>
int main(void) {
char src[] = "ABC"; // 実際のメモリ上は 'A' 'B' 'C' '#include <stdio.h>
#include <string.h>
int main(void) {
char src[] = "ABC"; // 実際のメモリ上は 'A' 'B' 'C' '\0'
char dest[8];
strcpy(dest, src);
// 1文字ずつASCIIコード(整数)として表示してみる
printf("dest[0] = '%c' (%d)\n", dest[0], dest[0]);
printf("dest[1] = '%c' (%d)\n", dest[1], dest[1]);
printf("dest[2] = '%c' (%d)\n", dest[2], dest[2]);
printf("dest[3] = '%c' (%d)\n", dest[3], dest[3]); // 終端文字のはず
return 0;
}
'
char dest[8];
strcpy(dest, src);
// 1文字ずつASCIIコード(整数)として表示してみる
printf("dest[0] = '%c' (%d)\n", dest[0], dest[0]);
printf("dest[1] = '%c' (%d)\n", dest[1], dest[1]);
printf("dest[2] = '%c' (%d)\n", dest[2], dest[2]);
printf("dest[3] = '%c' (%d)\n", dest[3], dest[3]); // 終端文字のはず
return 0;
}
dest[0] = 'A' (65)
dest[1] = 'B' (66)
dest[2] = 'C' (67)
dest[3] = ' ' (0)
dest[3]に入っている0が、終端文字’\0′です。
画面に表示すると空白のように見えますが、整数値としては0になっていることが分かります。
このように、strcpyは「文字列の見た目の文字」だけでなく、「見えない終端文字\0まで」含めてコピーする関数です。
char配列と文字列リテラルの違い
strcpyを正しく理解するには、char配列と文字列リテラルの違いをおさえることが非常に重要です。
この違いを知らないと、プログラムが突然セグメンテーションフォルトで落ちるといった問題につながります。
char配列に文字列をコピーする考え方
char配列は、自分で確保した「書き込み可能なメモリ領域」です。
strcpyのコピー先として最もよく使われるのが、このchar配列です。
基本的なイメージ
#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[16]; // 長さ16の書き込み可能な領域
const char *msg = "C入門";
strcpy(buffer, msg); // bufferに"C入門"をコピー
printf("buffer: %s\n", buffer);
return 0;
}
buffer: C入門
この例ではbufferという配列が、「文字を入れてもよい箱」として確保されています。
strcpyはこの箱の中に、msgの中身(終端文字'\0'を含む)を順番に書き込んでいきます。
ここで大切なのは、bufferは自分のプログラムのために確保された領域なので、書き換えても安全だという点です。
その代わりに、確保したサイズを超えて書き込んではいけないという制約があります。
この点については後の「バッファオーバーフロー」で詳しく説明します。
なお、char配列は宣言と同時に初期化することもできます。
char str1[16] = "Hello"; // "Hellochar str1[16] = "Hello"; // "Hello\0"が格納される
char str2[] = "World"; // 必要なサイズ(6)が自動的に決まる
"が格納される
char str2[] = "World"; // 必要なサイズ(6)が自動的に決まる
この場合、strcpyを使わなくても最初から文字列が入っていますが、後から別の文字列を代入したいときにはstrcpyが必要になります。
文字列リテラルは書き換えできないことに注意
一方、「”Hello”」のような文字列は文字列リテラルと呼ばれます。
多くの処理系では、文字列リテラルは「読み取り専用の領域」に配置されます。
そのため、strcpyで文字列リテラルを書き換えようとすると、未定義動作(多くの場合セグメンテーションフォルト)になります。
次のコードは、やってはいけない典型的な例です。
#include <stdio.h>
#include <string.h>
int main(void) {
// pは「書き換えてはいけない領域」に置かれた文字列リテラルを指す
char *p = "Hello";
// ここで"Hello"という文字列リテラルを書き換えようとしている
// 多くの環境ではセグメンテーションフォルトなどのエラーになる
strcpy(p, "World");
printf("p: %s\n", p);
return 0;
}
このコードはコンパイルこそ通りますが、実行時にクラッシュする可能性が極めて高い危険なコードです。
なぜなら、pが指している先は文字列リテラルであり、そこは書き込み禁止の領域であることが多いからです。
文字列リテラルをコピー先に使いたい場合は、次のように一度char配列にコピーしてから編集するのが正しい手順です。
#include <stdio.h>
#include <string.h>
int main(void) {
const char *literal = "Hello";
char buffer[16];
// 読み取り専用の文字列リテラル literal から
// 書き込み可能な配列 buffer にコピー
strcpy(buffer, literal);
// bufferは書き込み可能なので、さらにコピーしても安全
strcpy(buffer, "World");
printf("buffer: %s\n", buffer);
return 0;
}
buffer: World
「ポインタ変数に文字列リテラルを代入しただけ」の状態でstrcpyのコピー先にしてはいけないという点が、初心者にとってとても重要なポイントです。
strcpyで使える領域と使えない領域
strcpyのコピー先には、「書き込み可能で、かつ十分な大きさを持つメモリ領域」を指定しなければなりません。
ここを間違えると、プログラムがクラッシュしたり、意図しないメモリを書き換えてバグの原因になります。
使ってよい代表例と、使ってはいけない代表例を表にまとめます。
| 種類 | 例 | strcpyのコピー先にしてよいか |
|---|---|---|
| ローカル変数のchar配列 | char buf[32]; | 使える |
| グローバル変数のchar配列 | char gbuf[128]; | 使える |
| mallocで確保した領域 | char *p = malloc(...); | 使える(サイズに注意) |
| 文字列リテラルへのポインタ | char *p = "abc"; | 使えない |
| 初期化されていないポインタ | char *p; | 使えない |
| NULLポインタ | char *p = NULL; | 使えない |
「自分でサイズや確保を管理している領域(char配列、mallocで確保した領域)のみが安全なコピー先」だと理解するとよいです。
strcpyを安全に使うためのポイント
strcpyは便利ですが、使い方を誤るとバッファオーバーフローなどの深刻な問題を起こします。
この章では、安全にstrcpyを使うために必ず意識すべきポイントを解説します。
バッファサイズと終端文字\0の重要性
strcpyでは、コピー先のバッファサイズと、終端文字'\0'を入れるための1文字分の余裕が非常に重要です。
コピー先のサイズが足りないと、配列の外側まで書き込んでしまうことになります。
ギリギリのサイズでの危険な例
#include <stdio.h>
#include <string.h>
int main(void) {
// "Hello"は実際には 'H' 'e' 'l' 'l' 'o' '#include <stdio.h>
#include <string.h>
int main(void) {
// "Hello"は実際には 'H' 'e' 'l' 'l' 'o' '\0' の6文字
char small[5]; // 5バイトしかない -> 足りない
const char *src = "Hello";
// smallに6バイトを書き込もうとするのでバッファオーバーフロー
strcpy(small, src);
printf("small: %s\n", small);
return 0;
}
' の6文字
char small[5]; // 5バイトしかない -> 足りない
const char *src = "Hello";
// smallに6バイトを書き込もうとするのでバッファオーバーフロー
strcpy(small, src);
printf("small: %s\n", small);
return 0;
}
このコードはコンパイルできますが、未定義動作になり、たまたま動くこともあれば、クラッシュしたり、他の変数が壊れたりします。
文字列の長さはstrlen(src)で取得できますが、これは終端文字'\0'を含まない長さです。
そのため、必要なバッファサイズ = strlen + 1と考える必要があります。
正しい確保サイズの計算例
#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "Hello";
size_t len = strlen(src); // lenは5
// 終端文字'#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "Hello";
size_t len = strlen(src); // lenは5
// 終端文字'\0'の1文字分を足して確保
char buffer[6];
// または、より安全にするなら少し余裕を持たせる
// char buffer[16];
strcpy(buffer, src);
printf("buffer: %s (長さ: %zu)\n", buffer, strlen(buffer));
return 0;
}
'の1文字分を足して確保
char buffer[6];
// または、より安全にするなら少し余裕を持たせる
// char buffer[16];
strcpy(buffer, src);
printf("buffer: %s (長さ: %zu)\n", buffer, strlen(buffer));
return 0;
}
buffer: Hello (長さ: 5)
「文字数 + 1バイトの終端文字分」を常に意識することが、安全な文字列操作の第一歩です。
バッファオーバーフローとは何か
バッファオーバーフローとは、用意した配列やメモリ領域のサイズを超えてデータを書き込んでしまうことを指します。
strcpyはコピー元の長さを一切確認しないため、コピー先が小さすぎると簡単にバッファオーバーフローを起こします。
バッファオーバーフローが起きると、次のような問題が発生します。
- 他の変数の値が勝手に書き換えられてしまう
- 関数の戻り先アドレスが壊れて、変な場所にジャンプしてしまう
- セグメンテーションフォルトでプログラムが強制終了する
- セキュリティホールとなり、攻撃者に悪用される
初心者の学習段階では、まず「オーバーフローはバグの元であり、必ず避けなければならない」という意識を持つことが大切です。
strcpyとstrncpyの違いと使い分け
バッファオーバーフローを防ぐために、strncpyという関数も用意されています。
strncpyは、「最大で何文字までコピーするか」を指定できるため、一見すると安全そうに見えますが、使い方を間違えると別の問題を引き起こします。
strcpyとstrncpyの比較
| 関数名 | プロトタイプ | 特徴 |
|---|---|---|
| strcpy | char *strcpy(char *d, const char *s); | 終端'\0'まで全てコピー。長さチェックなし |
| strncpy | char *strncpy(char *d, const char *s, size_t n); | 最大n文字だけコピー。'\0'が付かない場合がある |
strncpyの落とし穴
strncpyは、n文字に達する前に終端'\0'に出会えば、'\0'も含めてコピーし、残りの部分を'\0'で埋めます。
しかし、コピー元の長さがn以上だった場合は、終端'\0'をコピーしません。
#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "1234567890"; // 長さ10
char dest[5];
// 最大4文字コピーする(終端'#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "1234567890"; // 長さ10
char dest[5];
// 最大4文字コピーする(終端'\0'のために1バイト残す想定)
strncpy(dest, src, 4);
// ここでdest[4]に'\0'を明示的に入れないと、文字列としては未終端になる
dest[4] = '\0';
printf("dest: %s\n", dest); // 正しく"1234"と表示される
return 0;
}
'のために1バイト残す想定)
strncpy(dest, src, 4);
// ここでdest[4]に'#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "1234567890"; // 長さ10
char dest[5];
// 最大4文字コピーする(終端'\0'のために1バイト残す想定)
strncpy(dest, src, 4);
// ここでdest[4]に'\0'を明示的に入れないと、文字列としては未終端になる
dest[4] = '\0';
printf("dest: %s\n", dest); // 正しく"1234"と表示される
return 0;
}
'を明示的に入れないと、文字列としては未終端になる
dest[4] = '#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "1234567890"; // 長さ10
char dest[5];
// 最大4文字コピーする(終端'\0'のために1バイト残す想定)
strncpy(dest, src, 4);
// ここでdest[4]に'\0'を明示的に入れないと、文字列としては未終端になる
dest[4] = '\0';
printf("dest: %s\n", dest); // 正しく"1234"と表示される
return 0;
}
';
printf("dest: %s\n", dest); // 正しく"1234"と表示される
return 0;
}
dest: 1234
このように、strncpyを使う場合は「自分で終端’\0’を付ける」意識が必須であり、初心者にはかえって分かりにくく感じることもあります。
使い分けの基本的な考え方
初心者向けの指針としては、次のように考えるとよいです。
- バッファサイズに十分な余裕があることが明らかな場合は
strcpyを使う - 「絶対にこれ以上はコピーしたくない」という上限が必要な場合は
strncpyを使い、終端'\0'を自分で補う
より高度な話として、実務ではstrlcpy(一部の環境で利用可能)や、独自の安全ラッパー関数を使うことも多いですが、まずはstrcpyを使うときはバッファサイズを絶対に守るという意識を身につけることが重要です。
安全な文字列コピーのためのチェック方法
strcpyを安全に使うには、「コピー先のバッファサイズが、コピー元の長さ+1以上であること」を事前にチェックする習慣をつけるとよいです。
サイズチェック付きのコピー例
#include <stdio.h>
#include <string.h>
int main(void) {
char dest[16];
const char *src = "Hello C World";
size_t dest_size = sizeof(dest); // destの全体のバイト数
size_t src_len = strlen(src); // 終端'#include <stdio.h>
#include <string.h>
int main(void) {
char dest[16];
const char *src = "Hello C World";
size_t dest_size = sizeof(dest); // destの全体のバイト数
size_t src_len = strlen(src); // 終端'\0'を含まない文字数
// 終端'\0'の1文字分を加えたサイズが、バッファより小さいか確認
if (src_len + 1 <= dest_size) {
strcpy(dest, src); // 安全にコピーできる
printf("コピー成功: %s\n", dest);
} else {
printf("エラー: バッファサイズが足りません。\n");
printf("必要サイズ: %zu, 用意されたサイズ: %zu\n",
src_len + 1, dest_size);
}
return 0;
}
'を含まない文字数
// 終端'#include <stdio.h>
#include <string.h>
int main(void) {
char dest[16];
const char *src = "Hello C World";
size_t dest_size = sizeof(dest); // destの全体のバイト数
size_t src_len = strlen(src); // 終端'\0'を含まない文字数
// 終端'\0'の1文字分を加えたサイズが、バッファより小さいか確認
if (src_len + 1 <= dest_size) {
strcpy(dest, src); // 安全にコピーできる
printf("コピー成功: %s\n", dest);
} else {
printf("エラー: バッファサイズが足りません。\n");
printf("必要サイズ: %zu, 用意されたサイズ: %zu\n",
src_len + 1, dest_size);
}
return 0;
}
'の1文字分を加えたサイズが、バッファより小さいか確認
if (src_len + 1 <= dest_size) {
strcpy(dest, src); // 安全にコピーできる
printf("コピー成功: %s\n", dest);
} else {
printf("エラー: バッファサイズが足りません。\n");
printf("必要サイズ: %zu, 用意されたサイズ: %zu\n",
src_len + 1, dest_size);
}
return 0;
}
コピー成功: Hello C World
このように、strcpyを呼ぶ前に、サイズ条件を明示的にチェックするだけでも、バッファオーバーフローの多くを防ぐことができます。
strcpyでよくあるエラーとデバッグのコツ
strcpyに関するエラーは、初心者が必ずといってよいほど遭遇します。
この章では、代表的なエラーのパターンと、その原因・対処法・デバッグのコツを解説します。
セグメンテーションフォルトの原因と対処
セグメンテーションフォルト(しばしばSegmentation faultと表示されます)は、不正なメモリアクセスをしたときにOSがプログラムを強制終了する現象です。
strcpyで発生する主な原因は次の2つです。
- コピー先が書き込み禁止の領域(文字列リテラルなど)だった
- コピー先やコピー元のポインタが全く無効(初期化されていない、NULLなど)だった
文字列リテラルを書き換えてしまう例
先ほども示しましたが、次のようなコードが典型的な例です。
char *p = "Hello"; // pは読み取り専用の文字列リテラルを指す
strcpy(p, "World"); // 書き込み禁止領域に書こうとしてクラッシュ
対処としては、必ず書き込み可能なchar配列を用意して、そこにコピーすることが必要です。
char p[16] = "Hello"; // pは書き込み可能な配列
strcpy(p, "World"); // 安全
無効なポインタを使ってしまう例
char *p; // 初期化されていないポインタ(どこを指しているか不明)
strcpy(p, "abc"); // 不明な場所に書き込もうとしてクラッシュ
この場合は、pに対して有効なメモリ領域を割り当てる必要があります。
char buf[16];
char *p = buf; // bufという書き込み可能な配列を指すようにする
strcpy(p, "abc"); // 安全
または、動的にメモリを確保する場合は次のようにします。
#include <stdlib.h> // malloc, free
char *p = malloc(16); // 16バイト分の書き込み可能な領域を確保
if (p == NULL) {
// メモリ確保に失敗したときの処理を書く
}
strcpy(p, "abc"); // 安全(サイズに注意)
初期化されていないポインタにstrcpyした場合
初期化されていないポインタは、「中身がゴミのまま」の変数です。
整数変数と違い、ポインタのゴミ値は「どこか分からないメモリアドレス」になっているため、そこにstrcpyで書き込もうとすると非常に危険です。
次のコードを考えてみます。
#include <stdio.h>
#include <string.h>
int main(void) {
char *p; // ここではどこも指していない(ゴミ値)
// コンパイルは通るが、実行時にセグメンテーションフォルトの可能性大
strcpy(p, "Hello");
printf("p: %s\n", p);
return 0;
}
このようなコードを避けるためには、ポインタを宣言したら、必ず「どこを指させるか」を決めてから使うという習慣が重要です。
例えば次のようにします。
#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[16];
char *p = buffer; // pはbufferという配列の先頭を指す
strcpy(p, "Hello");
printf("p: %s\n", p);
return 0;
}
この例では、pというポインタが、明確に「bufferの先頭」を指しているので、安全にstrcpyを使うことができます。
デバッガ(gdbなど)を使ったstrcpyのトレース方法
strcpyでセグメンテーションフォルトが出た場合、デバッガ(gdbなど)を使って原因を調べると、理解と解決が早くなります。
ここでは、簡単なgdbの使い方を紹介します。
例: セグメンテーションフォルトを起こすプログラム
// ファイル名: bad_strcpy.c
#include <stdio.h>
#include <string.h>
int main(void) {
char *p; // 初期化していない
const char *src = "Hello";
strcpy(p, src); // ここでクラッシュ予定
printf("p: %s\n", p);
return 0;
}
このプログラムをデバッグ情報付きでコンパイルします。
gcc -g bad_strcpy.c -o bad_strcpy
次にgdbで実行します。
gdb ./bad_strcpy
(gdb) run
セグメンテーションフォルトが起きると、gdbはその場で停止します。
そのときに次のコマンドを入力します。
(gdb) bt # backtraceの略。関数呼び出しの履歴を表示
(gdb) list # 現在の位置周辺のソースコードを表示
(gdb) print p # 変数pの中身(アドレス)を表示
ここで、pがどのようなアドレスを指しているか、そしてstrcpyを呼ぶ直前にどの行にいるかが分かります。
もしpが0x0(NULL)や明らかにおかしな値になっていれば、それが原因だと判断できます。
初心者のうちは、「セグメンテーションフォルトが出たらgdbでどの行で止まるかを確認する」という練習をするだけでも、ポインタとメモリの理解が深まります。
学習用におすすめのstrcpy練習パターン
最後に、strcpyに慣れるための練習パターンをいくつか紹介します。
これらを自分で書いて動かしてみることで、理解が定着しやすくなります。
パターン1: 複数の文字列を1つのバッファに順番にコピーする
#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[64];
// まず"Hello"をコピー
strcpy(buffer, "Hello");
printf("1回目: %s\n", buffer);
// 次に"World"をコピー(上書きされる)
strcpy(buffer, "World");
printf("2回目: %s\n", buffer);
// 最後に"Goodbye"をコピー
strcpy(buffer, "Goodbye");
printf("3回目: %s\n", buffer);
return 0;
}
1回目: Hello
2回目: World
3回目: Goodbye
この練習では、同じバッファに対して何度もstrcpyすると、以前の内容は完全に上書きされることが分かります。
パターン2: ユーザー入力を別のバッファにコピーする
#include <stdio.h>
#include <string.h>
int main(void) {
char input[32];
char copy[32];
printf("文字列を入力してください(最大31文字): ");
// fgetsは安全のために長さを指定して読み取る
if (fgets(input, sizeof(input), stdin) == NULL) {
printf("入力エラー\n");
return 1;
}
// fgetsで読み取った文字列をそのままコピー
strcpy(copy, input);
printf("入力された文字列(元): %s", input);
printf("入力された文字列(コピー): %s", copy);
return 0;
}
文字列を入力してください(最大31文字): test
入力された文字列(元): test
入力された文字列(コピー): test
ここでは、文字列入力とstrcpyの組み合わせに慣れることができます。
パターン3: バッファサイズを意図的に変えて安全・危険を体験する
同じソースコードで、char buffer[8];やchar buffer[4];など、さまざまなサイズを試し、どのサイズなら安全にコピーできるかを実験してみると、バッファサイズの感覚が身についてきます。
まとめ
strcpyは、C言語で文字列を扱ううえで欠かせない基本関数です。
しかし、char配列と文字列リテラルの違いや、終端文字\0とバッファサイズの重要性を理解していないと、簡単にセグメンテーションフォルトやバッファオーバーフローを引き起こします。
コピー先は必ず「書き込み可能で十分な大きさの領域」に限定し、必要に応じてstrlenやsizeofでサイズを確認すると安全です。
また、エラーが起きたときにはgdbなどのデバッガを活用して原因を追うことで、ポインタとメモリへの理解が深まり、C言語の力を着実に身につけることができます。
