組み込み C 言語の 3 つの技術的問題

C 言語は組み込み学習において必須の知識であり、ほとんどのオペレーティング システムも C 言語を中心に構築されていますが、その中には3 つの技術的な困難があり、ほぼ「骨の折れる」と認識されています。

今日は、これら 3 つの硬い骨を詳しく分解して、明確に理解できるようにします。

写真

写真

0x01 ポインタ

ポインターは理解するのが最も難しい概念として認識されており、多くの初心者が諦めてしまう直接の理由でもあります。

ポインタが理解しにくい理由は、ポインタ自体が変数であるためです。ポインタは、特にアドレスを格納する非常に特殊な変数です。このアドレスは、物を保持するためのスペースを確保する必要があり、値を代入できる変数であるためです。途中でめまいがして曲がり角を曲がれなくなって困っている人も多いと思います。

C言語が多くの専門家に好まれる理由は、途中で柔軟に切り替えられ、実行効率が非常に高いポインタの魅力であり、初心者が戸惑う原因でもあります。

ポインタは学習において避けては通れない知識であり、C言語を学習した次のステップはデータ構造とアルゴリズムへの切り替えです。次のステップに進むのは難しく、多くの人が諦めてしまいます。

ポインタはメモリ構造に直接接続されています。C 言語ではポインタがランダムに指すのが一般的です。配列の範囲外の根本原因はメモリの問題です。ポインターのこの時点では、無限の遊びの余地があります。多くのプログラミングスキルがここに集まります。

ポインタには、メモリの解放を申請する方法も含まれます。解放が適時に行われないと、メモリ リークが発生します。ポインタは効率的で使いやすいですが、完全に理解していないと、一部の人にとっては単なる悪夢です。

指針に関しては、マスターの経験を参照できます。

複合型の説明

ポインタを理解するには、多少なりとも複雑な型が必要になります。そこで、まず複合型を完全に理解する方法を紹介します。

複雑な型を理解することは実際には非常に簡単です。型には多くの演算子が含まれます。これらの演算子にも通常の式と同様に優先順位があります。それらの優先順位は演算の優先順位と同じです。

そこで著者は、変数名から開始し、演算子の優先順位に従って変数を結合し、段階的に分析するという原則を要約しました。

単純なタイプから始めて、ゆっくりと分析してみましょう。

int p;

これは通常の整数変数です。

int *p;

まず、pから始まり、*と結合されているので、pはポインタであることを意味します。次に int と組み合わせると、ポインタが指すコンテンツの型が int 型であることがわかり、p は整数データを返すポインタになります。

int p[3];

まず、p から開始して [] と組み合わせて、p が配列であることを示します。次に int と組み合わせると、配列内の要素が整数であることが示されるため、p は整数データで構成される配列になります。

int *p[3];

まずpから始めて[]で結合しますが、優先度が高いのでpは配列となります。次に、それを * と組み合わせて、配列内の要素がポインター型であることを示します。次に int と組み合わせると、ポインタが指すコンテンツの型が整数であることが示されるため、p は整数データを返すポインタで構成される配列になります。

int (*p)[3];

まず、p から開始し、それを * と組み合わせて、p がポインターであることを示します。次に、それを [] と結合します (優先順位を変更するためだけに、「()」を使用したこの手順は無視できます)。これは、ポインターが指すコンテンツが配列であることを示します。次に、それを int と組み合わせて、配列内の要素が整数であることを示します。したがって、p は整数データで構成される 3 つの整数へのポインタです。

int **p;

まず、p で開始し、それを * と組み合わせて、p がポインターであることを示します。次に、それを * と組み合わせて、ポインターが指す要素がポインターであることを示します。次に、これを int と組み合わせて、ポインターが指す要素が整数データであることを示します。第 2 レベルのポインターと上位レベルのポインターは複合型ではほとんど使用されないため、後でより複雑な型のマルチレベル ポインターについては考慮せず、最大でも第 1 レベルのポインターのみを考慮します。

int p(int);

p から始めて、まず () と組み合わせて、p が関数であることを示します。次に、() を入力して分析し、関数に整数変数のパラメーターがあることを示し、外部の int と結合して、関数の戻り値が整数データであることを示します。

int (*p)(int);

p から開始して、まず p をポインターと組み合わせて、p がポインターであることを示します。次に () と組み合わせると、ポインターが関数を指していることを示します。次に、() 内の int と結合され、関数に int 型パラメータがあることを示します。さらに、最も外側の int と結合されて、関数の戻り値の型が整数であるため、p が整数パラメータを持つポインタであることを示します。戻り値の型は整数関数へのポインタです。

int (*p(int))[3];

最初はスキップしても構いません。このタイプは複雑すぎるため、見ないでください。p から始めて、まず () と組み合わせて、p が関数であることを示します。次に、() を入力して int と結合します。これは、関数に整数変数パラメーターがあることを示します。次に、外側の * と組み合わせると、関数がポインターを返すことを示します。次に、最も外側のレイヤーに移動し、最初に [] と結合します。これは、返されたポインターが配列を指すことを示します。次に、配列内の要素がポインターであることを示す int と結合され、最後に int と結合されて、ポインターが指す内容が整数データであることを示します。したがって、 p は整数をパラメータとして受け取り、整数ポインタ変数の配列を指すポインタ変数を返す関数です。

それだけです。これらのタイプを理解すれば、他のタイプも簡単に理解できます。ただし、あまり複雑な型はプログラムの可読性を大幅に低下させるため、通常は使用しませんので、注意して使用してください。上記のタイプで十分です。

ポインタの詳細

ポインタは、そこに格納されている値がメモリ内のアドレスとして解釈される特別な変数です。

ポインタを理解するには、ポインタの 4 つの側面を理解する必要があります。ポインタの型、ポインタが指す型、ポインタの値またはポインタが指すメモリ領域、およびポインタが占有するメモリ領域です。自体。それぞれ説明しましょう。

まず、例としていくつかのポインターを宣言します。

(1)int *ptr;

(2)char *ptr;

(3)int **ptr;

(4)int (*ptr)[3];

(5)int *(*ptr)[4];

ポインタ型

文法の観点から見ると、フレンドはポインタ宣言ステートメント内のポインタ名を削除するだけで済み、残りの部分はポインタの型になります。これはポインター自体の型です。

上の例の各ポインターの型を見てみましょう。

(1)int ptr;//指针的类型是int

(2)char ptr;//指针的类型是char

(3)int ptr;//指针的类型是int

(4)int (ptr)[3];//指针的类型是int()[3]

(5)int *(ptr)[4];//指针的类型是int(*)[4]

どうでしょうか?ポインタの型を調べる簡単な方法ではないでしょうか?

ポインタが指す型

ポインタが指すメモリ領域にポインタを介してアクセスする場合、ポインタが指す型によって、コンパイラがそのメモリ領域の内容をどのようなものとして扱うかが決まります

構文の観点から見ると、ポインタ宣言ステートメント内のポインタ名と名前の左側にあるポインタ宣言子 * を削除するだけで済み、残るのはポインタが指す型です。

上記の例の各ポインターが指す型は次のとおりです。

(1)int ptr; //指针所指向的类型是int

(2)char *ptr; //指针所指向的的类型是char*

(3)int *ptr; //指针所指向的的类型是int*

(4)int (*ptr)[3]; //指针所指向的的类型是int(*)[3]

(5)int *(*ptr)[4]; //指针所指向的的类型是int*(*)[4]

ポインタ算術演算では、ポインタが指す型が大きな役割を果たします。

ポインタの型 (つまり、ポインタ自体の型) とポインタが指す型は 2 つの概念です。C に慣れてくると、ポインタと混在する「型」の概念を、「ポインタの型」と「ポインタが指す型」の 2 つの概念に分けることが必須であることがわかるでしょう。ポインターに習熟していることも重要なポイントの一つです。

著者はたくさんの本を読みましたが、いくつかの下手に書かれた本ではポインタの 2 つの概念が混同されているため、その本には一貫性がないように見え、読めば読むほど混乱してしまうことがわかりました。

ポインタ値

つまり、ポインタが指すメモリ領域またはアドレスです。

ポインタの値は、ポインタ自体に格納されている値であり、この値はコンパイラによって一般的な値ではなくアドレスとして扱われます。

32 ビット プログラムでは、メモリ アドレスはすべて 32 ビット長であるため、すべてのタイプのポインタの値は 32 ビット整数です。ポインタが指すメモリ領域は、ポインタの値が示すメモリアドレスから始まり、sizeof(ポインタが指す型)の長さのメモリ領域である。

以降、ポインタの値が○○であるというときは、そのポインタが○○を先頭とするアドレスのメモリ領域を指していることと同じであり、ポインタがあるメモリ領域を指しているというときは、これは、ポインタの値がメモリ領域の最初のアドレスであると言うのと同じです。

ポインタが指すメモリ領域とポインタが指す型は全く異なる概念です。例 1 では、ポインタが指す型はすでに存在しますが、ポインタが初期化されていないため、ポインタが指すメモリ領域は存在しないか、意味がありません。

これからは、ポインターに遭遇するたびに、「このポインターの型は何ですか?」と尋ねる必要があります。ポインタはどの型を指していますか? このポインタはどこを指しているのでしょうか?

ポインタ自体が占有するメモリ領域

ポインタ自体はどれくらいのメモリを占有しますか? 関数 sizeof (ポインターのタイプ) を使用してテストするだけでわかります。32 ビット プラットフォームでは、ポインター自体の長さが 4 バイトを占めます。ポインター自体が占有するメモリの概念は、ポインター式が左辺値であるかどうかを判断するのに役立ちます。

0x02関数

プロセス指向オブジェクトモジュールの基本単位と、それに対応する各種組み合わせ、関数ポインタ、ポインタ関数。

関数とは、プロセス指向のビジネスロジックのブロックであり、関数の実行時に仮引数と実引数がどのようにデータをやり取りし、どのようにデータを転送し、どのように設計するかなどの単位モジュールの最小単位です。合理的な関数は、関数を解決するだけでなく、車輪の再発明を避けるために再利用できるかどうかにも依存します。

関数ポインタとポインタ関数は文字通りの意味は交換可能ですが、実際にはまったく異なる意味を持ちます。ポインタ関数はポインタを返す関数であるため理解しやすいですが、関数ポインタは主にコールバック関数で使用されますが、関数がまだ理解されていないと感じている人が多く、コールバック関数はさらにわかりにくいです。実際、関数へのポインタはそれ自体がポインタ変数ですが、初期化中に関数を指し、ポインタ レベルに戻ることは一般的に理解できます。ポインターが再び深くなると、先に進むのが非常に困難になることがわかりませんでした。

写真

C 言語の開発者は、後の開発者の労力を節約するために、多くのコードを作成し、他の人が直接使用できるように、共通の基本関数をすべて完成させました。しかし、コードが多すぎると、どうやって必要なものを見つけられるのでしょうか? すべてのコードを取得するのは明らかに非現実的です。

ただし、これらのコードは初期の開発者によって長い間別のファイルに分類されており、コードの各部分には一意の名前が付けられています。したがって、実際には、特に実践的な演習やプロジェクトを通じて学習できる場合、C 言語の学習はそれほど難しくありません。コードを使用するときは、対応する名前の後に () を追加するだけです。このようなコードが関数であり、関数は独立して特定の機能を完了でき、一度記述した後は何度でも使用できます。

多くの初心者は、C 言語の関数と数学の関数の概念を混同しているかもしれません。実際はそれほど複雑ではなく、C言語の関数にはルールがあり、概念を理解していれば非常に興味深いものになります。関数の英語名は Function で、中国語の「関数」の意味に対応します。C言語の関数も関数と密接な関係があります。

C 言語コードの小さな部分を見てみましょう。

#include<stdio.h>
int main()
{
    puts("Hello World");
    return 0;
}

4 行目に注目すると、「Hello World」がモニターに出力されます。put の後には () が続き、文字列も () 内に配置される必要があることはすでに述べました。

C 言語では、括弧を使用できないステートメントと、括弧を使用する必要があるステートメントがあります。括弧付きのものは関数です。

C 言語には多くの関数が用意されており、それらを使用するには簡単なコードだけが必要です。ただし、これらの機能の基礎となる機能は比較的複雑で、通常はソフトウェアとハ​​ードウェアの組み合わせであり、多くの詳細と境界を考慮する必要があります。これらの機能をプログラマーに任せると、プログラマーの学習コストが大幅に増加し、プログラミングのコストが削減されます。効率。

関数を使用すると、C 言語のプログラミング効率は成果物を持つようなものであり、開発者はいつでもそれを呼び出すだけでよく、処理関数、演算関数、日時関数などは、C 言語自体の機能を直接実現するのに役立ちます。 。

C言語の関数を再利用できます

関数の明らかな特徴は、括弧 () を使用する必要があることです。必要に応じて、括弧内に処理するデータを含めることもできます。たとえば、puts (「Learn Embedded Together」) は出力関数を持つコードを使用します。このコードの名前は put で、「Learn Embedded Together」はこのコードで処理されるデータです。関数の使用には、プログラミングにおける専門用語があり、「関数呼び出し」と呼ばれます。

関数で複数のデータを処理する必要がある場合は、次のようにカンマを使用してデータを区切ります。

pow(10, 2);

この関数は、10 の 2 乗を見つけるために使用されます。

さて、これを読んだ後、C 言語の関数は実際には非常に興味深いものであり、それほど複雑で難しいものではないと思いますか。将来、また初心者に出会ったとき、あなたは C 言語の関数を話しながら、その場で数え切れないほどの感嘆の視線を集めるかもしれません。

0x03 構造体、再帰

大学で C 言語を勉強している学生は多く、まだ科目を修了していないため、構造を学習していない 章の配置からすると、構造の学習が教科書の後半に配置されているように感じられ、多くの学生がそう感じています構造は重要ではなく、学校の試験に対処するためだけ、あるいは卒業証書を取得するためだけのものでは、確かに学ぶ意味はほとんどありません。

プログラミング業界で働きたいと思っていても、この概念を理解していなければ、基本的にデータ モデルを構築することはできません。ネイティブ データ型を使用して完全に完了するビジネスはありません。多くの専門家がデータ モデルを設計するとき、通常は、最初にデータ モデルに構造を追加します。ヘッダファイル、ボディデータを整理します。その後、関数のパラメータや名前を設計し、実際にCソースコードを書き始めます。

スペース節約の観点から構造体のデータの配置順序が異なる場合、メモリに占有されるスペースも異なります。構造体間に値が割り当てられます。構造体にポインタがある場合は、特別な注意を払う必要があります。課題への深い課題が求められます。

再帰は通常、一部のデータを最初から最後まで数えたりリストしたりするために使用されます。多くの初心者は、それを使用するときにぎこちなく感じます。どうやって自分自身を呼び出せるのでしょうか? そして使用する際には飛び出し条件を設定しておかないと延々と続き、無限ループになってしまいます。

構造に関する知識については、上司の経験を参照することもできます。

皆さんも構造物についてはよくご存知だと思います。ここでは、C言語の構造について私が調べて勉強したことをまとめてお話したいと思います。この要約の中にまだ理解できていない点があると思われる場合、この記事はある程度の価値があります。もちろんレベルには限りがありますので、至らない点がございましたらご指摘ください。コードファイルtest.cを以下に置きます。ここでは、次の 2 つの問題を中心に C 言語の構造を分析して適用します。

  1. C言語における構造体の機能とは何ですか?

  2. 構造体メンバ変数のメモリアライメントの重要性(キーポイント)

一部の概念の説明については、C 言語の教科書の定義を写すことはしません。座ってゆっくり話しましょう。

1. 構造の機能は何ですか?

3か月前、教育研究部門の上級生がファーウェイの南京研究所での面接中にこの問題に遭遇した。もちろん、これは面接における最も基本的な質問にすぎません。と聞かれたら、どう答えますか?私の理解では、C 言語の構造体には少なくとも次の 3 つの機能があります。

(1) オブジェクトの属性を有機的に整理します。

たとえば、STM32 の RTC 開発では、日付と時刻を表すデータが必要ですが、これらのデータは通常、年、月、日、時、分、秒です。構造体を使用しない場合、それを表すために 6 つの変数を定義する必要があります。この場合、プログラムのデータ構造は緩やかであり、データ構造は「高凝集性、低結合性」が最適です。したがって、プログラムの可読性、移植性、保守性の観点から、構造を使用してそれを表現することをお勧めします。

typedef struct //公历日期和时间结构体
{
vu16 year;
vu8 month;
vu8 date;
vu8 hour;
vu8 min;
vu8 sec;
}_calendar_obj;
_calendar_obj calendar; //定义结构体变量

(2) 構造体のメンバ変数を変更する方法は、関数(エントリパラメータ)の再定義を置き換えます。

オブジェクトの性質を有機的に整理した構造であれば、その構造は「中間」であることを意味し、関数(入力パラメータ)を再定義する代わりに構造のメンバ変数を変更する方法が構造の「中間使用」に相当します。 。上記の構造を例として分析してみましょう。日付と時刻を表示する次の関数があるとします。

void DsipDateTime( _calendar_obj DateTimeVal)

次に、構造体タイプ _calendar_obj の変数を実際のパラメーターとして使用して DsipDateTime() を呼び出すだけで済みます。DsipDateTime() は、DateTimeVal の変数を使用してコンテンツを表示します。構造体を使用しない場合は、おそらく次のような関数を記述する必要があります。

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 hour,vu8 min,vu8 sec)

明らかに、このような形式的なパラメータは印象的ではなく、データ構造の管理も非常に面倒です。関数の戻り値が日付と時刻を表すデータの場合、処理はさらに複雑になります。これは 1 つの側面にすぎません。

一方、ユーザーが日付と時刻を表すデータに曜日 (週) を含める必要がある場合、この時点でその構造体が以前に使用されていない場合は、別の仮パラメータ vu8 week を追加する必要があります。 DsipDateTime() 関数へ:

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 week,vu8 hour,vu8 min,vu8 sec)

パラメータを渡すこの方法は非常に面倒であることがわかります。したがって、構造体を関数のエントリ パラメーターとして使用する利点の 1 つは、関数宣言 void DsipDateTime(_calendar_obj DateTimeVal) を変更する必要がなく、構造体のメンバー変数を追加するだけで済むことです。関数の内部実装で、calendar.week に対応する調整を行うだけです。このように、プログラムの修正や保守において重要な役割を果たします。

typedef struct //公历日期和时间结构体
{
vu16 year;
vu8 month;
vu8 date;
vu8 week;
vu8 hour;
vu8 min;
vu8 sec;
}_calendar_obj;
_calendar_obj calendar; //定义结构体变量

(3) この構造のメモリ アラインメント原理により、CPU のメモリへのアクセス速度 (空間と時間の交換) が向上します。

また、構造体メンバ変数のアドレスは、ベースアドレス(オフセット)に基づいて計算できます。まず、次の簡単なプログラムを見てみましょう。このプログラムの解析については、第 2 部「構造体メンバ変数のメモリ アライメント」で詳しく説明します。

#include<stdio.h>

int main()
{
    struct    //声明结构体char_short_long
    {
        char  c;
        short s;
        long  l;
    }char_short_long;

    struct    //声明结构体long_short_char
    {
        long  l;
        short s;
        char  c;
    }long_short_char;

    struct    //声明结构体char_long_short
    {
        char  c;
        long  l;
        short s;
    }char_long_short;

printf(" \n");
printf(" Size of char   = %d bytes\n",sizeof(char));
printf(" Size of shrot  = %d bytes\n",sizeof(short));
printf(" Size of long   = %d bytes\n",sizeof(long));
printf(" \n");  //char_short_long
printf(" Size of char_short_long       = %d bytes\n",sizeof(char_short_long));
printf("     Addr of char_short_long.c = 0x%p (10进制:%d)\n",&char_short_long.c,&char_short_long.c);
printf("     Addr of char_short_long.s = 0x%p (10进制:%d)\n",&char_short_long.s,&char_short_long.s);
printf("     Addr of char_short_long.l = 0x%p (10进制:%d)\n",&char_short_long.l,&char_short_long.l);
printf(" \n");

printf(" \n");  //long_short_char
printf(" Size of long_short_char       = %d bytes\n",sizeof(long_short_char));
printf("     Addr of long_short_char.l = 0x%p (10进制:%d)\n",&long_short_char.l,&long_short_char.l);
printf("     Addr of long_short_char.s = 0x%p (10进制:%d)\n",&long_short_char.s,&long_short_char.s);
printf("     Addr of long_short_char.c = 0x%p (10进制:%d)\n",&long_short_char.c,&long_short_char.c);
printf(" \n");

printf(" \n");  //char_long_short
printf(" Size of char_long_short       = %d bytes\n",sizeof(char_long_short));
printf("     Addr of char_long_short.c = 0x%p (10进制:%d)\n",&char_long_short.c,&char_long_short.c);
printf("     Addr of char_long_short.l = 0x%p (10进制:%d)\n",&char_long_short.l,&char_long_short.l);
printf("     Addr of char_long_short.s = 0x%p (10进制:%d)\n",&char_long_short.s,&char_long_short.s);
printf(" \n");
return 0;
}

プログラムの実行結果は次のとおりです (注: 括弧内のデータはメンバー変数のアドレスの 10 進数形式です)。

写真

2. 構造体メンバ変数のメモリアライメント

まず、上記のプログラムの実行結果を分析してみましょう。最初の 3 行は、私のプログラムでは char 型が 1 バイト、short 型が 2 バイト、long 型が 4 バイトを占めることを示しています。char_short_long、long_short_char、char_long_short は同じメンバーを持つ 3 つの構造体ですが、メンバー変数の順序が異なります。そしてプログラムの実行結果から判断すると、

Size of char_short_long = 8 bytes
Size of long_short_char = 8 bytes
Size of char_long_short = 12 bytes //比前两种情况大4 byte !

また、1 バイト (char) + 2 バイト (short) + 4 バイト (long) = 8 バイトではなく、7 バイトであることに注意してください。

したがって、構造体のメンバー変数が配置される順序は、構造体が占有するメモリ空間のサイズに影響します。構造体変数が占めるメモリ サイズは、そのメンバー変数が占めるスペースの合計と必ずしも同じではありません。ユーザー プログラムやオペレーティング システム (uC/OS-II など) に多数の構造体変数がある場合、このメモリ使用量を最適化する必要があります (つまり、構造体内のメンバー変数の順序を特定する必要があります)。

構造体のメンバー変数はどのように保存されますか?

ここでは、あまり気取らず、#pragma Pack マクロを使用せずに、次の結論を直接述べます。

  • 原則 1 構造体 (構造体または共用体) のデータ メンバーの場合、最初のデータ メンバーはオフセット 0 に配置されます。後続の各データ メンバー記憶域の開始位置は、メンバー サイズの整数倍から開始する必要があります (たとえば、int は32 ビットマシンは 4 バイトなので、格納は 4 の整数倍のアドレスから開始する必要があります。

  • 原則 2: 構造体の合計サイズ、つまり sizeof の結果は、その最大の内部メンバーの整数倍でなければならず、不足分は補わなければなりません。

  • 原則3:構造体をメンバとして使用する場合、構造体のメンバは内部要素の最大サイズの整数倍のアドレスから格納する。(struct a に struct b が含まれ、b に char、int、double などの要素が含まれる場合、sizeof(double) = 8 バイトであるため、b は 8 の整数倍であるアドレスから格納される必要があります)

ここでは、上記のプログラムと組み合わせて分析します (原則 3 については今のところ説明しません)。

まず 2 つの構造体 char_short_long と long_short_char を見てみましょう。メンバー変数のアドレスから、これら 2 つの構造体が原則 1 と 2 に従っていることがわかります。char_short_long のメンバー変数のアドレスでは、char_short_long.s のアドレスは 1244994 であることに注意してください。つまり、1244993 は「空」であり、単なる「プレースホルダー」です。

メンバー変数 メンバ変数の16進アドレス メンバ変数の10進アドレス
char_long_short.c 0x0012FF2C 1244972
char_long_short.l 0x0012FF30 1244976
char_long_short.s 0x0012FF34 1244980

メモリ分布図は次のようになり、合計 12 バイトであることがわかります。

写真

まず、1244972は1で割り切れるので、char_long_short.cを1244972に配置しても問題ありません(実際、char型メンバ変数自体に関しては、どのアドレス単位に配置しても問題ありません)。原則 1 によると、1244973 以降 ~1244975 は 4 で割り切れるものはありません (sizeof(long)=4bytes のため)。1244976 は 4 で割り切れるので、char_long_short.l は 1244976 に配置する必要があります。同様に、最後の.s(sizeof(short)= 2 バイト) は 1244980 に配置する必要があります。

これが最後か?いいえ、原則 2 がまだあります。原則 2 の要件に従って、char_long_short 構造体が占めるスペースは、最大のメモリスペースを占めるメンバー変数のサイズの整数倍でなければなりません。ここで終了すると、char_long_short が占有するメモリ空間は 1244972 ~ 1244981 の合計 10 バイトとなり、原則 2 に準拠しません。したがって、最後に 2 バイト (1244982 ~ 1244983) が完成する必要があります。

この時点で、構造体のメモリ レイアウトが完了します。

以下では、上記の原則に従って、この分析が正しいかどうかを検証します。上記の分析によると、アドレスユニット 1244973、1244974、1244975、および 1244982、1244983 はすべて空です (少なくとも char_long_short は使用されておらず、単なる「プレースホルダー」です)。

私たちの分析が正しければ、このような構造体の定義は 12 バイトのメモリを占有するはずです。

struct //声明结构体char_long_short_new
{
char c;
char add1; //补齐空间
char add2; //补齐空间
char add3; //补齐空间
long l;
short s;
char add4; //补齐空间
char add5; //补齐空间
}char_long_short_new;

私たちの分析が正しいことがわかります。原則 3 については、自分でプログラミングして検証できるため、ここでは説明しません。

したがって、VC6.0、Keil C51、または Keil MDK のいずれを使用している場合でも、構造体を定義する必要がある場合、構造体のメンバー変数のメモリ アラインメントに少し注意を払う限り、MCU を大幅に節約できます。 .RAMの。これは実際のプログラミングだけでなく、IBM、Microsoft、Baidu、Huawei などの多くの大企業の筆記試験や面接でもよく見られます。

この3つの固い骨がC言語学習の障害になっているのですが、頑張ってC言語の大動脈を取り除けば大動脈が開き、他の内容を学ぶのは比較的簡単になります。

プログラミングの学習は苦痛であればあるほど学ぶことが多く、過去を乗り越えることで自分のスキルが向上し、これまでの努力を諦めていた時間もクリアされます。習得が難しい言語ほど、始めた後の楽しさは大きく、中毒になりやすいものでもあります。あなたは依存症ですか?それとも諦めましたか?

おすすめ

転載: blog.csdn.net/weixin_41114301/article/details/132916821