4

私は最近ダミーのために C++ を読んでいますが、タイトルが間違っているか、彼らは私を当てにしていませんでした。文字列でポインターの配列を利用することに関するセクションでは、私が完全に困惑し、どこに向かうべきかわからない関数を示しています。

char* int2month(int nMonth)
{
//check to see if value is in rang
if ((nMonth < 0) || (nMonth > 12))
    return "invalid";

//nMonth is valid - return the name of the month
char* pszMonths[] = {"invalid", "January", "February", "March", "April", "May", "June", 
                     "July", "August", "September", "October", "November", "December"};

return pszMonths[nMonth];
} 

まず (主な質問ではありませんが)、戻り値の型がポインターである理由と、範囲外にならずに pszMonths を返す方法がわかりません。この本とオンラインでそれについて読んだことがありますが、この例ではわかりません。

私が持っている主な質問は、「これはどのように機能するのですか?!?!」です。ポインターの配列を作成して実際に初期化する方法がわかりません。私の記憶が正しければ、数値データ型でこれを行うことはできません。「ポインターの配列」内の各ポインターは、単語を構成する個々の文字を含む配列自体のようなものですか? このすべてが私の心を揺さぶるだけです。

8 月 20 日 - 私の混乱が実際にどこから生じているのかについて、私を助けようとしている人々によって混乱しているように思われるので、私はそれをよりよく説明しようとします. 特に私が懸念しているコードのセクションは次のとおりです。

//nMonth is valid - return the name of the month
char* pszMonths[] = {"invalid", "January", "February", "March", "April", "May", "June", 
                 "July", "August", "September", "October", "November", "December"};

ポインターを作成すると、それを別の所定の値にしか割り当てることができないと思いました。ポインターの配列のように見えるもの(ここで本を参照)が月の名前を初期化することに混乱しています。ポインターが実際に値を初期化できるとは思いませんでした。配列は動的にメモリを割り当てていますか? 「無効」は本質的に「新しい文字」と同等です。ステートメントまたは同様のもの?

質問に答えてくれた場合に備えて投稿を読み直しますが、最初は理解できませんでした。

4

6 に答える 6

4

では、一度に 1 行ずつ見ていきましょう。
 

char* int2month(int nMonth)

この行はおそらくWRONGです。なぜなら、関数は変更可能なものへのポインターを返すと述べているからですchar(慣例により、これはchar配列の最初の要素になります)。代わりに、結果の型としてchar const*orを指定する必要があります。const char*これら 2 つの指定はまったく同じ意味です。つまり、char変更できない へのポインタです。
 

{

これは、関数本体の開き中括弧です。関数本体は、対応する右中括弧で終了します。
 

//check to see if value is in rang

これはコメントです。これはコンパイラによって無視されます。
 

if ((nMonth < 0) || (nMonth > 12))
    return "invalid";

ここで、returnステートメントは、条件が成立する場合にのみ実行されifます。目的は、正しくない引数値を予測可能な方法で処理することです。ただし、暦年には 12 か月しかないのに対し、値 0 と 12 の両方が有効であり、合計で 13 個の有効な値が得られるため、このチェックはおそらく間違っています。

ちなみに、技術的には、returnステートメントの場合、指定された戻り値は 8 つcharの要素の配列です。つまり、7 文字と最後にヌルバイトがあります。この配列は、型Decayと呼ばれる最初の要素へのポインターに暗黙的に変換されます。この特定の減衰 (文字列リテラルからポインター、非 constへ) は、​​古い C との互換性を保つために C++98 および C++03 で特別にサポートされていますが、今後の C++0x 標準では無効になります。char

本はそのような醜いことを教えるべきではありません。const結果タイプに使用します。


 

//nMonth is valid - return the name of the month
char* pszMonths[] = {"invalid", "January", "February", "March", "April", "May", "June", 
                     "July", "August", "September", "October", "November", "December"};

この配列の初期化にも、その減衰が含まれます。ポインタの配列です。各ポインターは文字列リテラルで初期化されます。これは型ごとに配列であり、ポインターに減衰します。

ちなみに「psz」という接頭辞はハンガリー記法という怪物です。C プログラミング用に発明され、Microsoft の Programmer's Workbench のヘルプ システムをサポートしています。現代のプログラミングでは、それは何の役にも立ちませんが、代わりに最も単純なコードを意味不明なように読むだけです。あなたは本当にそれを採用したくありません。
 

return pszMonths[nMonth];

インデックス 12 には配列要素がないため、値が 12 の場合、このインデックスには正式なUndefined Behaviorがあり、親しみを込めて「UB」としても知られていnMonthます。実際には、意味不明な結果が得られます。

編集:ああ、著者が月名「無効」を先頭に配置したことに気づきませんでした。これにより、13個の配列要素が作成されます。コードを隠す方法...非常に悪くて予期しないため、気づきませんでした。「無効」のチェックは、関数の上位で行われます。


 

} 

そして、これは関数本体の閉じ中括弧です。

乾杯 & hth.,

于 2011-08-20T04:41:18.283 に答える
2

おそらく、行ごとの説明が役立つでしょう。

/* This function takes an int and returns the corresponding month
 0 returns invalid
 1 returns January
 2 returns February
 3 returns March
 ...
 12 returns December
*/
char* int2month(int nMonth)
{
// if nMonth is less than 0 or more than 12, it's an invalid number
if ((nMonth < 0) || (nMonth > 12))
    return "invalid";

// this line creates an array of char* (strings) and fills it with the names of the months
//
char* pszMonths[] = {"invalid",  // index 0
                     "January",  // index 1
                     "February", // index 2
                     "March",    // index 3
                     "April",    // index 4
                     "May",      // index 5
                     "June",     // index 6
                     "July",     // index 7
                     "August",   // index 8
                     "September",// index 9
                     "October",  // index 10
                     "November", // index 11
                     "December"  // index 12
                    };

// use nMonth to index the pszMonths array to return the appropriate month
// if nMonth is 1, returns January because pszMonths[1] is January
// if nMonth is 2, returns February because pszMonths[2] is February
// etc
return pszMonths[nMonth];
} 

あなたが知らないかもしれない最初のことは、プログラム内の文字列リテラル (二重引用符で囲まれたもの) が実際にはchar*タイプ1であるということです。

2 番目に気付いていないことは、char*s の配列 (つまり ) にインデックスを付けると、文字列である がchar* pszStrings[]生成されることです。char*

このインスタンスでローカル スコープから何かを返すことができる理由は、文字列リテラルがコンパイル時にプログラムに格納され、破棄されないためです。たとえば、これはまったく問題ありません。

char* blah() { return "blah"; }

そして、それはこれを行うのとほとんど同じです2

int blah() { return 5; }

次に、= {/* stuff */}配列宣言の後に がある場合、それは初期化子リストと呼ばれます。あなたが行っているように配列のサイズを省略した場合、コンパイラは、初期化子リストにある要素の数によって、配列を作成する大きさを計算します。つまりchar* pszMonths[]、「char* の配列」を意味し、イニシャライザ リストに 、 、 などがあり、それらが"invalid"s "January"1であるため、s の配列をいくつかの s で初期化しているだけです。そして、数値型でこれを行うことができないことについて誤解していました。これは、数値型と文字列が含まれている任意の型で行うことができるためです。"February"char*char*char*

1それは実際にはchar*ではなくchar const[x]、 であり、 のようにそのメモリを変更することはできませんがchar*、それは今のあなたにとって重要ではありません。

2実際にはそうではありませんが、そのように考えるのに役立つ場合は、C++ が上手になり、死なずにさまざまな微妙な点を処理できるようになるまで、自由に感じてください。

于 2011-08-20T04:31:05.803 に答える
2

int2month が何をすることになっているかについて、あなたは何を期待していますか?

記憶がどのように見えるかのメンタルモデルを持っていますか? たとえば、これが私の記憶の絵による表現です。

pszMonths =      [   .       ,     .   ,   .    , ...]
                     |             |       |
                     |             |       |
                     V             |       |   
                     "invalid"     |       V
                                   |    "February"
                                   V
                               "January"

pszMonths は配列であり、既によく知っているはずです。ただし、配列の要素はポインターです。矢印をたどって値に到達する必要があります。この場合は文字列です。この種の間接的な表現が必要です。各月の名前には独自の可変長があるため、フラットな表現でこれを行うのは簡単ではありません。

これ以上議論しないと、どこで行き詰まっているかを判断するのは非常に困難です。もっと言う必要があります。

[編集]

わかりました、もう少し言いました。C のプログラム モデルについてもう少し知っておく必要があるようです。プログラムがコンパイルされると、コード部分とデータ部分に縮小されます。

データ部分には何が含まれていますか? 文字列リテラルのようなもの。各文字列リテラルは、メモリ内のどこかに配置されます。コンパイラが優れていて、同じリテラルを 2 回使用すると、コンパイラは 2 つのコピーを持たず、それらを再利用します。

デモ用の小さなプログラムを次に示します。

#include <stdio.h>
int main(void) {
  char *name1 = "foo";
  char *name2 = "foo";
  char *name3 = "bar";

  printf("The address of the string in the data segment is: %d\n", (int) name1);
  printf("The address of the string in the data segment is: %d\n", (int) name2);
  printf("The address of the string in the data segment is: %d\n", (int) name3);
  return 0;
}

このプログラムを実行すると、次のようになります。

$ ./a.out
The address of the string in the data segment is: 134513904
The address of the string in the data segment is: 134513904
The address of the string in the data segment is: 134513908

C プログラムを実行すると、プログラムのデータ部分 (もちろんプログラムのコード部分も) がメモリに読み込まれます。プログラムが実行し続ける限り、データ内の場所を参照するポインターはどれでも問題ありません。特に、データ内のどこかへのポインターは、関数呼び出し全体で有効です。

出力をさらに詳しく見てください。name1 と name2 は同じリテラル文字列であるため、データ内の同じ場所へのポインターです。多くの場合、C コンパイラは、データをコンパクトで断片化されないように保つのに非常に優れています。そのため、「bar」のバイトが「foo」のバイトに対して正しく保存されていることがわかります。

(私たちが見ているのは低レベルの詳細であり、コンパイラが文字列リテラルを並べてパックする場合は必ずしもそうではない可能性があります。コンパイラには、これらの文字列の表現をほとんどどこにでも自由に配置できます。しかし、それはここでそうしているのを見るのはかわいいです。)

関連するメモとして、C プログラムが次のようなことをしても問題ないのはそのためです。

char* good_function() {
  char* msg = "ok";
  return msg;
}

しかし、このようなことをするのはよくありません:

char* bad_function() {
  char msg[] = "uh oh";
  return msg;
}

この 2 つの機能はまったく異なる意味を持っています。

  1. 1 つ目は、「この文字列をデータ セグメントに格納します。この関数を実行するときは、アドレスをデータ セグメントに戻してください」とコンパイラに伝えます。
  2. ここでの 2 番目の悪い関数は、「この関数を実行するとき: 'uh oh' を書き込むのに十分なスペースを持つ一時変数をスタックに作成します。一時スペースをポップオフして、スタックにアドレスを返します... ああ、待ってください。 、そのアドレスは適切な場所を指していません。それは...」
于 2011-08-20T04:33:45.920 に答える
1

C では、文字列は単純に連続したメモリ位置に格納された一連のバイトであり、バイト 0 が文字列の終わりを示します。例えば、

char *s = "abcd"

コンパイラは 2 つのメモリ ロケーションを割り当てることになりabcdます0。2 番目の場所はポインタ変数で、最初の場所はそれが指すものです。

文字列の配列の場合、コンパイラは 2 つのメモリ ロケーションを再度割り当てます。為に

char *strings[] = {"abc", "def"}

stringsには 2 つのポインターがあり、他の場所には bytes がありますabc\0def\0。次に、最初のポインターが を指しa、2 番目のポインターが を指しますd

于 2011-08-20T04:39:17.107 に答える
1

このコードは を返しませんpszMonthsが、 に含まれるポインタの 1 つを返しますpszMonths。これらは文字列リテラルを指しており、スコープ外に出ても有効なままです。

このコードの紛らわしい部分の 1 つは、char*ではなく を返すことchar const*です。これは、誤って文字列を変更しやすいことを意味します。そうしようとすると、未定義の動作が発生します。

通常、文字列リテラルは、実行可能ファイルのデータ セクションに文字列を配置することによって実装されます。これは、それらへのポインタが常に有効であることを意味します。のコードint2monthが実行されると、pszMonths はポインターでいっぱいになりますが、基になるデータは実行可能ファイルの別の場所にあります。

先に述べたように、このコードは非常に安全ではなく、本に掲載して安置する価値はありません。文字列リテラルは にバインドできますがchar*、実際には で構成されていchar constます。これにより、誤ってそれらを変更しようとすることが非常に簡単になり、実際には未定義の動作が発生します。この動作が存在する唯一の理由は、C との互換性を維持することであり、新しいコードでは決して使用しないでください。

于 2011-08-20T04:35:42.293 に答える
0

まず、 をchar*に置き換えることができると仮定しましょうstring

そう:

string int2month(int nMonth)
{ /* ... */ }

C または C++ では s のchar配列を返すことができないため、ポインターを返します。char


この行で:

return "invalid";

"invalid"プログラムのメモリに存在します。これは、それが常にあなたのそばにあることを意味します。(ただし、最初に使用せずに直接変更しようとすると、未定義の動作になりますstrcpy()! 1 )


これを想像してください:

char* szInvalid = "invalid";
char* szJanuary = "January";
char* szFebruary = "February";

string szMarch = "March";

char* pszMonths[] = {szInvalid, szJanuary, szFebruary, szMarch};

それが s の配列である理由がわかりますchar*か?


1これを行う場合:

char* szFoo = "invalid";
szFoo[0] = '!'; szFoo[1] = '?';

char* szBar = "invalid"; // This *might* happen: szBar == "!?valid"
于 2011-08-20T04:32:45.790 に答える