C 文字列

提供:yonewiki

C++へ戻る


※このページではC言語にも存在していたという意味で記事タイトルがC 文字列になっていますが、
C++でも同様です。C++だけの機能がある場合は明記します。

文字列

文字はChar型で1文字を表現できるのでした。
例 ※C++の場合のみcout関数が使えます。#include <iostream>が必要

#include <iostream>
int main(){
    char c; //文字型の変数定義
    c = 'a'; //aという文字を変数cに代入。実際にはaの文字コード番号が代入される。
    std::cout << c << "\n";
    std::cout << std::hex << (int)c << "\n";
    return 0;
}

出力結果

a
61 //16進数の61は10進数では97です。asciiコードですね。

という具合に半角の1文字を扱うことができました。私たちは日本人なので日本語を使いますから、日本語を格納したりする変数を最終的には使いたいと思いますが、まずはこのasciiコードの範囲での処理にしぼって考えましょう。日本語対応まで考えると基本を理解することが難しいと思います。基本がわかったら日本語対応へ向かうのです。


上記のとおり1文字を扱う変数の型があるのは理解できたとしても、文字変数の1文字では文章が表現できないので、通常のプログラミングでは1文字では情報の表現に支障があります。やはり人間は人間らしくということで、複数の文字の集まりによって文章を構成できるのですから、その複数の文字を変数として処理するということで、文字列の扱い方について覚える必要があります。


どうすればいいか?


文字変数の配列として考えることで、文字列の表現が実現され、文章を格納することが出来るようになります。


C言語の面倒なところはこのあたりの厳密さ、多様性にあると自分は感じます。ここで記述するのは基本的なことだけになってしまいそうですが、文字列を変数に格納するという手法については、多種多様の手法が存在しているので、文字列の理解をするだけでも大変です。自分でプログラムを全部組むのであれば、一つのやり方を覚えればよいのですが、誰かが作ったプログラムを再利用する場合には、その多種多様な方法の中で、どの手法が使われるかは定まっていません。ですから、あらゆる事態に備えるのであれば、その多種多様な方法を
できるだけ網羅して理解する必要もあります。更には、文字コード群もいっぱいあるし、文字列関係の操作はプログラム技術全体の大きな領域になります。


とは、いったものの覚えることが多すぎると嘆いても仕方ないので、ひとつづつ覚えてみる覚悟を決めましょう。
文字列は以下のようにして変数を宣言したり、初期値を入力できます。

//★パターン1
char cStr1[10] = {'y','o','n','e','w','i','k','i'}; //このやり方はあまり使うことは無いですけど、配列の基礎的な代入方法かも。
//ちなみに配列の添え字は0から始まって9までが作られます。
//cStr1[8]とcStr1[9]には自動的に"\0"が代入されます。
//数字の0で初期化してくれるってこと。
cStr1[10] = {'y','o','r','u','w','i','k','i'};//初期化以外ではまとめて代入する記述はできないので、これはエラー
cStr1 = {'y','o','r','u','w','i','k','i'};//配列の添え字がいらないんじゃねぇ?というのも間違い。これもエラーになります。

//★パターン2
char cStr2[10] = "yonewiki"; //このやり方は便利だね、文字型配列の優しさをちょっぴり感じます。
//同じくcStr2[8]とcStr2[9]には自動的に"\0"が代入されます。
//数字の0で初期化してくれるってこと。
cStr2[10] = "yonewiki";//この方法でも初期化以外ではまとめて代入する記述はできないので、エラー
cStr2 = "yonewiki";//同じくエラー
//でも以下のようにChar型の代入と同じやり方はできます。
cStr2[2] = 'r';
cStr2[3] = 'u';

//★パターン3
char cStr3[] = "yonewiki";
//変数宣言時に値の代入(初期化)の記述があれば、配列の大きさを省略してもOK。
//自動的にcStr3[0]からcStr2[8]までの9の大きさの配列が生成されて、cStr3[8]には自動的に"\0"が代入されます。
//パターン1のような面倒な方法での値の代入(初期化)でも同じ結果になります。

//★パターン4
char cStr4[][9] = {"yonewiki","wiki"};
//これは文字列の配列です。配列の大きさを明記した後ろ側の方が文字列の長さ。2つの文字列があるってのは自動で認識して
//cStr4[0][0~8]
//cStr4[1][0~8]
//といった変数が定義されたことになります。

と、まずは4つのパターンを示しました。
パターン3はいいよね。パターン4は応用です。だけどちょっと手抜きの手法なんです。
なぜかっていうと最初の文字列のyonewikiは大きさ9の配列にぴったり格納できて、最後一文字のcStr4[0][8]には"\0"も格納できる。だけど2つめの文字列のwikiは大きさが5あれば十分でcStr4[1][5~8]は無駄に"\0"という文字のコードである値0が格納されてしまいます。それででてくるのが、メモリの使い方を厳密に支配できるポインタって奴による処理になります。ややこしくてうんざりって感じる人も多い。今どきのPCには4GByteとか8GByteとかメモリつんでるんだし4Byteくらいでケチケチすんなよと思うプログラマもいるでしょう。


小さな規模のアプリを開発している人にとってはその感覚って正しいと思います。でも、大規模なアプリを開発している熟練プログラマは、その小さな無駄を失くすことで資源を有効利用するし、動作速度の速いプログラムを提供しているわけで、それで飯を食ってる人もいる。あの人に、あの会社に頼めば最大限有効活用した素晴らしいアプリケーションが作成されるという評価をもって、経済社会で争っているのです。でも、まぁこのような無駄くらいは、許容している人は多いかもしれません。ときどき、理解しにくい程の、凄い実装をみかけます。


再利用可能なクラスやプログラムを提供している組織やプロジェクトでは、同じように資源の有効活用や動作速度の速いプログラムになるように組み込まれているので、自分は規模の小さいアプリしか作らないから覚えなくてもいいと思っているとしたら、その難しい部分を理解しようとしなかったばかりに、それらの有効な再利用すべきプログラムを使いこなすのに、苦労することになるし、C言語の基礎は抑えたのになんだかわかった気分で 満足して、より踏み込んだところへ行けずに終わってしまうケースにおちいっているのだと思います。自分も同じようなもんですが…


というわけで、また一歩踏み込んでいきます。それにしても文字列は初期化のときはあんなに簡単に代入できるのに、それ以降の代入では cStr = "yonewiki"; みたいにして代入できないのか?不便ですよね。


そういった不便さも踏み込んでいけば一定の理解を得ることができると思います。


配列の性質から、文字列を出力する際は以下のように指定します。※C++の場合のみcout関数が使えます。#include <iostream>が必要

char cStr1[] = "yonewiki";
std::cout << "cStr1     =" << cStr1 << "\n";
std::cout << "&cStr1    =" << &cStr1 << "\n";
std::cout << "&cStr1[0] =" << &cStr1[0] << "\n";


cStr[]で定義された配列変数は初期化の際に配列の大きさが9になり、配列の番号0~8までを保持した状態になります。
そして、配列要素を取り除いた記述で cStr1 すると先頭のアドレスを返します。
したがって、アドレス演算子を使って、&cStr1[0] や[0]を省略して&cStrと指定したのと同じことになります。
つまり、cStr1と&cStr1と&cStr1[0]は同じアドレスを保持しています。
ただし、std::coutで出力処理をするときには、扱いが異なります。

cStr1     =yonewiki
&cStr1    =003AF914
&cStr1[0] =yonewiki

となります。ありゃ、2つめの結果&cStr1がアドレスになるのは意外(汗。
ちなみに、うけとったアドレスから\0が現れるまで出力してくれるprintf関数の書式指定演算子%sを使った場合は
プログラミング部分

printf("cStr1      = %s\n" , cStr1);
printf("&cStr1     = %s\n" , &cStr1);
printf("&cStr1[0]  = %s\n" , &cStr1[0]);

出力結果

cStr1     =yonewiki
&cStr1    =yonewiki
&cStr1[0] =yonewiki

となります。std::cout << &cStr1でアドレスが表示されるっていうのは、実際に確かめて、初めて知った。普段、こういうことしないからね。
で、どういうことなんだろうって調べてみた。それには数値データの配列の場合はどうなるん?って疑問から解消できないか試みてみる。 そうすると

int iInt1[] = {10,11,,19};
std::cout << "iInt1     =" << iInt1 << "\n";
std::cout << "&iInt1    =" << &iIntr1 << "\n";
std::cout << "&iInt1[0] =" << &iInt[0] << "\n";


出力結果は当然ですが、以下のようにすべてアドレス表記になる。

iInt1     =003AD914
&iInt1    =003AD914
&iInt1[0] =003AD914

このことから、std::cout関数はcStr1[8]という配列変数に対して、cStr1という表記と&cStr1[0]という具合に指定した場合だけ、先頭アドレスから文字列として出力してくれていることになる。つまり文字列を返してくれることの方が特別なのだ。更に、この特別な処理の実態を知るために調査を進めるとなると少し難しいが配列番号も省略した癖にアドレス演算子までつけやがって、もう面倒見れきれませんわ。となっているのがcout関数の実情なのだろう。cout関数のソースコードを追ってみる?んや、やめとく。さすがに時間がかかりそうだ。ちょろっとWebで検索してみたが、ここまで追いかけた人もあまりいなさそう。てな具合で、話をそらしつつ、出力の異なりに蹴りをつけようと思ったのですが、このようになる法則はありまして…


ん、まてよ。


ポインタの変数を使ってないのに、cStr1はアドレスを保持する変数なの?って気付いた人は鋭いっす。そこから、cout関数の動作を説明し得るのです。そうなんです。配列を宣言すると、配列番号の添え字を省くと、アドレス変数を指し示すし、配列番号を与えると中身を見てくれる。ポインタの変数と同じなのです。つまり、cStr1はchar *cStr1と宣言されたポインタ変数で、cStr1=&cStr1[0]とされている状態と同じなのです。

であるから、以下のように

#include <iostream>
int main(){
    char *pcStr1; //文字型のポインタ変数定義
    char cStr1[] = "yonewiki"; 
    pcStr1 = &cStr1[0]
    std::cout << "pcStr1 = "<< pcStr1 << "\n";
    return 0;
}

出力結果

pcStr1 =yonewiki

となります。だとしたら、さきほどの少し疑問だったcout関数での扱いはごく自然な振る舞いにも思えませんか?&pcStr1って指示したら、アドレスを返す。printf関数は書式指定で%sだから、引数で指定されたアドレスから\0が中身に格納されているアドレスまで文字列を返す。そういうことです。別に面倒になことしやがってということではなくて、ポインタ変数の扱いについての一貫性をもってますよね。cout関数とprintf関数とでは出力指示が異なるために違う結果になるわけです。アドレスそのものを出力するのがアドレス演算子の役割であり、一方で文字列配列のアドレスを意味する形でcStr1であるだとか&cStr1[0]のように文字型のアドレスを渡されたら文字列を出力する。ということですから、文字型の配列になっていない1文字だけが格納されている文字変数のアドレスを出力すると\0が無いので、以下のように使用すると不具合がおこります。

#include <iostream>
int main(){
    char *pcChar; //文字型のポインタ変数定義
    char cChar = 'a'; 
    pcChar = &cChar
    std::cout << "cout      pcChar ="<< pcChar << "\n";
    std::cout << "cout      *pcChar ="<< *pcChar << "\n";
    std::cout << "cout      &pcChar ="<< &pcChar << "\n"; //アドレスが表示されるけど、pcCharのアドレスではなく、pcCharのアドレスを
                                         //格納しているアドレス。
    std::cout << "cout pcChar addr ="<< (void*)pcChar << "\n";
    printf("printf %s pcChar  =%s\n", pcChar);
    printf("printf %c *pcChar =%c\n", *pcChar);
    printf("printf %c pcChar =%c\n", pcChar);
    return 0;
}

出力結果

cout      pcChar  =aフフフフフフフフフフフフ
cout      *pcChar =a
cout      &pcChar =0013FAD0
cout *pcChar addr =0013FA2B
printf %s pcChar  =aフフフフフフフフフフフフ
printf %c *pcChar =a
printf %c pcChar  =+

というお話でした。どうでしょう。納得していただけたでしょうか。printf関数でアドレスを引数にpcCharとして%c書式でで出力しようとするとおそらくアドレス番号の下位1バイトの文字コードが動かすたびに割り当てられるアドレスが異なるのでランダムに出力されるでしょう。x64(64bitPC)もx32(64bitPC)もリトルエンディアンという方式で値を格納しているので、32bitアプリの4byteで構成されるpcCharには、上記の例で言うと先頭のアドレスには2Bが格納されています。これを%cで出力するということは、2B以外をそぎ落とした形で処理され、文字コードの2Bに相当する"+"プラス記号が表示されます。C言語の文字列という話だけで、ここまで掘り下げて理解してたら日が暮れる。Char型でアドレス変数である pcCharをCout関数で出力すると文字列として出力しようとしますから、アドレスを表示する場合には(void*)pcCharとして、ポインタ型にキャストすることで、アドレスが表示がされます。


★まめ知識 Visual StudioのDebugモードではデバッグ無しでプログラムを実行するのようなプログラム動作時にはプログラムが利用するメモリ領域を0x10101010で1Byteの空間を初期化してくれます。これは0xCCでシフトJISの1バイト文字のコードで半角カタカナの”フ”に相当します。なので、初期化されていない領域を出力しようとするcout命令やprintf命令が半角カタカナのフを出力したことになります。\0が無いと出てくるおもしろい現象です。フフフフフで検索すると結構おもしろい2ch(巨大掲示板2ちゃんねる)の記事にたどり着ける。


疲れますよね。VBとか、JAVAとか、Perlとか、こんなことしなくてもプログラムできるのに。でも、この程度でひるんじゃいけないよ。まだまだ複雑な世界が広がっています。プログラムをより柔軟に記述するための要素がC言語には存在します。おそろしく複雑です。


C言語では、プログラムをより記述しやすくするための手法も存在し、演算子の意味を付け足したりもできちゃうのです。そうやってCの関数は作られていたりして、有識者の能力は半端ないことをやってのける。そこの勉強はしないにしても、これくらい掘り下げないとポインタは理解できない。自分もいまさらながらに思い知らされることは多いです。でも、まぁ調べてみるのも楽しいと思えないと疑問は解決できないのかもしれない。しっかし、このC言語っていう体系を作った人たちって凄くないか?完璧だよね。


次はポインタを使った文字列操作についての記事を書こうと思います。なかなかやるきになんないすけどね。自分の理解していることを文章にするって意外と難しいし、意外と自分でもわかっていないことに気付く、そんなだから、わかってもらおうとするのも難しい。自分もわかったつもりの無能なのかもしれないし。会社で、講師やってくれないかって言われてもなんとなく嫌だ。ものすごい時間をかけて調べたことを、体系的に理解できる状態にまとめることなんて、そうたやすくはない。しかも2時間程度でバシッととか考えてる人もいるし、無理だろ。ぉぃ。


文字列の操作だけでかるく1カ月分くらいのお話をしないと、本当の意味での理解なんてできないと思う。でも、最近、気が付いたのは、何か躓いたらネットで調べて、答えを見つける。あるいは自分で考える。だとしたら、自分が気が付いていることくらいはネットに残していくのも大事なんじゃないかってこと。誰かがそれを見て、また前進する力になれているのだとしたら、それはそれでいいことだと思うから。


最近はMediaWikiの中に文字を打ち込む時間も多く取れてるので、更新していけるような気がする。でも今日はここまでかな。


C++へ戻る