2023 年 Weile Video Technology Co., Ltd. - C++ エンジニア第一次筆記試験問題

総合評価

ステレオタイプのエッセイには多くの基礎があり、全体的には難しくありません。新人の私にとっては少し難しいです(教師に返却したため)。以下の回答は、chatGpt+ オンライン関連コンテンツによって補足されています。

1. 多肢選択問題

1. CPP クラス メンバーのデフォルトの属性は何ですか?

public、private、protected などのアクセス指定子に依存します。通常、書き込まない場合はデフォルトで非公開になります。

詳細は以下のとおりです

class MyClass{
    
    
    int a;//默认private,私有的成员变量
public:
    void pfunc();//默认public,公有的成员函数
protected:
    void prfunc();//默认protected,受保护的成员函数
}

2. スタックの積み重ね順序は A、B、C、D、E ですが、スタックが表示できない出力順序は何ですか?

EDCBA、DECBA、DCEAB、ABCDE

スタックは、先入れ先出し、後入れ先出し、およびエッジインとエッジアウトの原則に従います。

ここで、ChatGpt はうまく応答せず、さまざまな出入りの状況を提供しませんでした。

スタックの積み重ね順序が a、b、c、d、e で​​ある場合、スタックの不可能な出力順序は ( ) です。_Stack Impossible Output Sequence_ddrrnnpp のブログ - CSDN ブログ

3. 派生クラスが保護された方法で基本クラスを継承する場合、派生クラス内の元の基本クラスの保護されたメンバーとパブリック メンバーのアクセス可能性はどうなりますか?

派生クラスがprotected基本クラスから継承すると、元の基本クラスのメンバーはprotected派生クラスでprotectedアクセス可能なままになりますが、publicメンバーはprotectedアクセス可能になります。

これは、派生クラスの内部では、protected基本クラスのメンバーに直接アクセスできますが、外部クラスおよびオブジェクトの場合、派生クラスのメンバー関数は基本クラスのメンバーにアクセスできますが、protected他のクラスおよびオブジェクトには直接アクセスできないことを意味します。

これを説明する例を次に示します。

class Base {
    
    
protected:
    int protectedMember;
public:
    int publicMember;
};

class Derived : protected Base {
    
    
public:
    void foo() {
    
    
        protectedMember = 10; // 在派生类内部可以直接访问基类的protected成员
        publicMember = 20; // 在派生类内部可以直接访问基类的public成员
    }
};

int main() {
    
    
    Derived derivedObj;
    derivedObj.foo();
   
    // 在外部无法直接访问基类的protected和public成员
    // derivedObj.protectedMember; // 错误:无法访问
    // derivedObj.publicMember; // 错误:无法访问
   
    return 0;
}

上記の例では、Derivedクラスはある方法でクラスprotectedを継承します。クラスのメンバー関数Baseでは、基本クラスの合計に直接アクセスできますただし、関数では、およびメンバーに直接アクセスできませんこれらのメンバーに直接アクセスできるのは、派生クラスおよび派生クラス内のメンバー関数のみです。Derivedfoo()protectedMemberpublicMembermain()derivedObjprotectedMemberpublicMember

4. CPP 関数で値を転送する方法は何ですか?

C++ 関数では、パラメーターの受け渡しメソッドは次のタイプに分類できます。

  1. 値渡し: 関数は実際のパラメーターのコピーを受け取ります。関数内のパラメータを変更しても、元のデータには影響しません。

    例:

    void func(int num) {
          
          
        num = 10; // 修改的是副本,不会改变原始数据
    }
    
    int main() {
          
          
        int value = 5;
        func(value);
        // value仍然是5
        return 0;
    }
    
  2. 参照渡し: 関数は実際のパラメーターへの参照を受け取り、パラメーターを変更すると元のデータに影響します。

    例:

    void func(int& num) {
          
          
        num = 10; // 修改的是原始数据
    }
    
    int main() {
          
          
        int value = 5;
        func(value);
        // value变为10
        return 0;
    }
    
  3. ポインターによる受け渡し: 関数は実際のパラメーターへのポインターを受け取り、そのポインターを通じて元のデータを変更できます。

    例:

    void func(int* ptr) {
          
          
        *ptr = 10; // 修改的是原始数据
    }
    
    int main() {
          
          
        int value = 5;
        func(&value);
        // value变为10
        return 0;
    }
    

これらの転送方法のどちらを選択するかは、関数を呼び出すときの要件に依存し、パラメーターの種類や元のデータを変更する必要があるかどうかなどの要素を考慮する必要があります。

5. 頻繁に使用される短い関数について、CPP ではどのような実装が一般的に使用されますか?

C++ では、頻繁に使用される短い関数の一般的な実装方法には次のものがあります。

  1. インライン関数 (インライン関数): キーワードを使用してinline関数を定義します。インライン関数の機能は、関数呼び出しを通じて関数を実行するのではなく、各呼び出しサイトに関数のコードを直接挿入することです。これにより、関数呼び出しのオーバーヘッドが削減され、パフォーマンスが向上します。インライン関数は、関数本体が小さく、複数の場所で頻繁に使用される関数に適しています。
inline int add(int a, int b) {
    
    
    return a + b;
}
  1. マクロ: プリプロセッサ マクロを使用して関数を定義します。マクロは、関数呼び出しのオーバーヘッドを発生させずに、コンパイル時に単純なテキスト置換を実行します。マクロの定義にはパラメータを含めることができ、複雑な式に展開できますが、マクロを使用すると副作用やデバッグが難しい問題が発生する可能性があることに注意してください。
#define ADD(a, b) ((a) + (b))
  1. constexpr 関数: コンパイル時に評価される関数のために C++11 で導入された機能。constexpr 関数はコンパイル時に評価できるため、実行時のオーバーヘッドを回避できます。constexpr 関数では、関数本体と関数が呼び出されるすべての引数がコンパイル時に評価可能である必要があります。
constexpr int add(int a, int b) {
    
    
    return a + b;
}

これらの実装方法には独自の長所と短所があり、適切な方法の選択は特定のシナリオと要件によって異なります。インライン関数は通常、比較的一般的で直感的な選択ですが、コンパイラーが実際にインライン関数を実行するかどうかは、コンパイラーの戦略と関数の複雑さに依存します。マクロと constexpr 関数は場合によってはパフォーマンスが向上しますが、その制限と潜在的な問題に注意してください。

6. CPP におけるリファレンスとポインタの違い

解答分析

正しいオプションは次のとおりです。
A. 参照は初期化する必要がありますが、ポインタは必要ありません
B. 参照は初期化後に変更できませんが、ポインタは変更できます
C. NULL への参照はありませんが、NULL へのポインタはあります

D. 参照とポインタは同等です このオプションは正しくありません。

各オプションについて説明します。

A. 参照は初期化する必要がありますが、ポインタは初期化する必要はありません。これは正しいです。C++ では、参照は定義時に初期化する必要があります。つまり、参照はオブジェクトを参照する必要があります。ポインターは定義時に初期化できません。最初にポインターを宣言してから値を割り当てるか、ポインターを nullptr に設定して null ポインターを示すことができます。

B. 参照は初期化後に変更できませんが、ポインタは変更できます。 これも同様です。参照が初期化されると、常に同じオブジェクトを参照し、参照のターゲットを変更することはできません。ポインターはいつでもそのポイントを変更でき、別のオブジェクトまたは null 値を指すことができます。

C. NULL への参照はないが、NULL へのポインタは存在します。これも当てはまります。参照は、null 値ではなく、有効なオブジェクトを参照する必要があります。ポインターは null 値、つまり nullptr (null ポインターを意味します) を指すことができます。

D. 参照とポインタは同等です: このオプションは正しくありません。参照とポインタは異なる概念であり、上で説明したように、構文とセマンティクスに明確な違いがあります。参照は便利なエイリアシング メカニズムを提供し、ポインタはより優れた柔軟性と低レベルの操作機能を提供します。参照はポインターを介して実装できますが、それらは同等ではありません。

完全版

C++ では、リファレンス (参照) とポインター (ポインター) は 2 つの異なる概念であり、使用法とセマンティクスにいくつかの重要な違いがあります。

  1. 定義と初期化:
    • 参照は定義時に初期化する必要があり、一度初期化されると、常に同じオブジェクトを参照し、参照のターゲットを変更することはできません。例えば:int a = 10; int& ref = a;
    • ポインタは初期化せずに定義することも、後で他のオブジェクトを指すこともできます。例えば:int a = 10; int* ptr = &a;
  2. 空の値 (null):
    • 参照は null 値を参照できず、常に有効なオブジェクトを参照する必要があります。
    • ポインタは、NULL 値、つまり NULL ポインタ (nullptr) を指すことができます。
  3. 再割り当て:
    • 参照が初期化されると、他のオブジェクトを参照できなくなり、常に同じオブジェクトのみを参照できるようになります。
    • ポインターはいつでもそのポイントを変更でき、別のオブジェクトまたは null 値を指すことができます。
  4. 使用:
    • 参照はより使いやすくなり、逆参照演算子 (*) を使用せずにオブジェクトと同様に参照を使用できます。
    • ポインタは、ポインタが指すオブジェクトにアクセスするために逆参照演算子を使用する必要があります。
  5. 占有スペース:
    • 参照は余分なスペースを必要とせず、単なるエイリアスです。
    • ポインターが占めるスペースはプラットフォームに関連しており、通常、32 ビット システムでは 4 バイト、64 ビット システムでは 8 バイトを占めます。
  6. スペースアドレス:
    • 参照は独自のアドレスを持たず、参照するオブジェクトと同じアドレスを共有します。
    • ポインタは独自のアドレスを持ち、指すオブジェクトのアドレスを格納します。

要約すると、参照はオブジェクトを操作するための便利な方法を提供し、コードをより簡潔で読みやすくしますが、null 値を参照できないことや、参照のターゲットを変更できないことなど、いくつかの制限があります。ポインタはより柔軟で、実行時に変更できますが、ポインタが null であるか、無効なオブジェクトを指している可能性があるため、ポインタを使用するときはより注意する必要があります。リファレンスとポインタのどちらを使用するかを選択するときは、特定のニーズや状況に応じて選択できます。

7. const シンボル定数:

(1) const char *p
(2) char const *p
(3) char * const p

上記の 3 つの記述子の違いを説明してください。正しいものは () です。

A. (1)pはconst charへのポインタであり、pは変更可能です。ただし、p が指す値は変更できません。

B. (2) p が指す内容は変更できません。

C. (3) p はポインタであり、p が指す内容は変更できますが、p は変更できません

D. (1)と(2)の定義は同じです

回答分析: A を選択してください

上記 3 つの記述子の違いは次のとおりです。

  1. const char *p: これはconst char型へのポインタです。これは、pポインタが指す文字が変更不可能であること、つまり、p指定された文字の値を渡しても変更できないことを意味します。ただし、ポインタp自体は変更されて、他の文字を指すことができます。
  2. char const *p: 上で説明したように、これはconst char型へのポインタでもあります。constキーワードはchar前に配置され、char変更できないことを示します。これは最初の説明と完全に同等です。
  3. char * const p: これはchartypeへの定数ポインタです。これは、ポインタp自体は変更できず、特定のメモリ アドレスに初期化されると、別のアドレスを指すことができないことを意味します。pただし、ポイントされた文字の値は、ポインタを介して変更できます。つまり、ポイントされた文字の内容は変更できます。

要約:

  • (1)と は型へのポインタ(2)ですconst char。指す文字は変更できませんが、ポインタ自体は変更できます。定義が異なることに注意してください。
  • (3)これは定数ポインタであり、ポインタ自体は変更できませんが、指す文字の内容は変更できます。

constここでのキーワードの位置は型とポインター間で交換でき、両方とも同じことを意味するため、結果は同じになることに注意してくださいこれは、C++ では、const修飾子はその右側の修飾子または型ではなく、その左側の修飾子または型で解釈されるためです。

選択肢 D は、(1)との定義が異なるため(2)、不正解です。

これらは両方ともポインターへのポインターですがconst char、構文表現がわずかに異なります。つまり、constキーワードの位置が異なります。

  • (1):constの前はchar定数へのポインタを意味します。
  • (2):constその後はchar定数ポインタを意味します。

意味は同じですが、どちらもポインタが指す文字が変更できないことを示しますが、文法の観点からは定義が異なります。したがって、選択肢 D は誤りです。

const char *p、char const *p、char *const p の違い (面接でよくある質問)_Lawrence_121 のブログ - CSDN ブログ

(1) const char *p (2) char const *p (3) char * const p 上記 3 つの記述の違いを説明します。- ダルタニャン - ブログ ガーデン (cnblogs.com)

const char *p は、p が文字列__Niuke.com(nowcoder.com) を指す定数であることを示します。

8. 次のコードの実行結果は何ですか?

class Base{
    
    
    public:
    Base(){
    
    printf("Base \n");};
    virtual ~Base(){
    
    printf("~Base \n");};
};

class Derived:public Base{
    
    
    public:
    Derived(){
    
    printf("Derived \n");};
    ~Derived(){
    
    printf("~Derived \n");};
};

int main(){
    
    
    Derived derived;
}

結果は次のとおりです。

ベース
派生
~派生
~ベース

それで、どうやってそれを手に入れたのですか?

これは、コード内で Base と Derived という 2 つのクラスが定義されているためです。Derivedクラスの派生オブジェクトはmain関数内で作成されます。派生オブジェクトを作成するときは、Base クラスのコンストラクターが最初に呼び出され、次に Derived クラスのコンストラクターが呼び出されます。したがって、「Base」と「Derived」が出力されます。

プログラムが終了すると、オブジェクトは逆の順序で破棄されます。Derived クラスのデストラクターが最初に呼び出され、次に Base クラスのデストラクターが呼び出されます。したがって、「 Derived」と「 Base」が出力されます

Base クラスのデストラクター宣言にタイプミスがあることに注意してください。「 Basse」ではなく「 Base」である必要があります。

2. 応募に関する質問

1. 32 ビット システムでは、CPP プログラムの sizeof の値を計算してください。

char str[] = "www.wellav.com";
char *p = str;
int n = 10;
sizeof(str) = ? (1)值是多少?
sizeof(p)   = ? (2)值是多少?
sizeof(n)   = ? (3)值是多少?
void Foo(char str[100])
{
    
    
    sizeof(str) = ? (4) 值是多少?
}
void *p = malloc(100);
sizeof(p) = ? (5)值是多少?

指定されたコードと指示から、次のsizeof値を計算できます。

(1)の値はsizeof(str)15です。strこれは、14 文字と末尾の null 文字を含む文字配列で\0、合計 15 バイトのメモリ領域を占有します。

(2)の値はsizeof(p)4です。pこれはポインタであり、それが指すデータの種類に関係なく、ポインタ自体のサイズは固定されています。32 ビット システムでは、ポインターのサイズは 4 バイトです。

(3)の値はsizeof(n)4です。n変数のタイプでありint、32 ビット システムでは、intこのタイプは 4 バイトのメモリ空間を占有します。

(4)の値はsizeof(str)4です。関数内ではstrポインタとなり、関数に渡された配列パラメータは自動的にポインタに変換されます。したがって、sizeof(str)返されるのは配列のサイズではなく、ポインターのサイズです。32 ビット システムでは、ポインターのサイズは 4 バイトです。

(5)の値はsizeof(p)4 です。pこれはvoid*ポインターの一種であり、指すデータの種類に関係なく、ポインター自体のサイズは固定されています。32 ビット システムでは、ポインターのサイズは 4 バイトです。

2. 次のコードは正しいですか? 間違っている場合は、その理由を説明してください

typedef vector<int> IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(4);
//删除array数组中所有的2
for(IntArray::iteratot itor=array.begin();itor!=array.end();++itor)
{
    
    
    if(2==*itor) itor=array.erase(itor);
}

解答分析

間違った理由:

  1. itorfor ループでは、ループ内で反復子を変更できます。を実行するとitor=array.erase(itor)、現在の要素が削除されると、itor削除された要素の次の位置を指すことになり、ループの自動インクリメント部分で自動インクリメント操作が再度実行され、itorイテレータが無効になります。

解決:

std::removeアルゴリズムを組み合わせるvector方法によりerase、指定した要素を削除する操作を実現できます。修正されたコードは次のとおりです。

typedef vector<int> IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(4);
//删除array数组中所有的2
array.erase(std::remove(array.begin(), array.end(), 2), array.end());

このように、アルゴリズムを使用してstd::remove2 をすべて配列の末尾に移動し、次にメソッドを使用して、移動した 2 から始まる部分を消去して、2 をすべて削除する効果を実現しますvectorerase

3. 純粋仮想関数 (純粋仮想関数) をいつ使用するか?

純粋仮想関数は次の場合に使用されます。

  1. 抽象クラス (Abstract Class) を定義する: 抽象クラスはインスタンス化できないクラスであり、主に他のクラスの基本クラスとして使用されます。抽象クラスには純粋な仮想関数が含まれていますが、実際の実装はなく、派生クラスで実装する必要があります。クラスを抽象化するには、その関数の少なくとも 1 つを純粋仮想として宣言します。派生クラスはインスタンス化されるために純粋仮想関数を実装する必要があります。

例えば:

class Shape {
    
    
public:
    virtual void draw() = 0; // 纯虚函数
    virtual void calculateArea() = 0; // 纯虚函数
};

class Circle : public Shape {
    
    
public:
    void draw() override {
    
    
        // 实现绘制圆形的代码
    }

    void calculateArea() override {
    
    
        // 实现计算圆形面积的代码
    }
};

class Square : public Shape {
    
    
public:
    void draw() override {
    
    
        // 实现绘制正方形的代码
    }

    void calculateArea() override {
    
    
        // 实现计算正方形面积的代码
    }
};

int main() {
    
    
    // 不能实例化Shape对象,但可以通过指针或引用调用接口函数
    Shape* shapePtr = new Circle();
    shapePtr->draw();
    // Circle和Square类是具体派生类,可以被实例化
    Circle circle;
    Square square;
    circle.draw();
    square.calculateArea();
    return 0;
}

上記の例では、Shapeこれは純粋な仮想関数を備えた抽象クラスでありdraw()calculateArea()具体的な実装はありません。派生クラスをインスタンス化するにはCircle、これら 2 つの純粋な仮想関数を実装する必要があります。Square

  1. インターフェイス クラス (インターフェイス クラス): 純粋な仮想関数はインターフェイス クラスを定義するために使用されます。インターフェイス クラスは主に一連のインターフェイス仕様を定義するために使用され、派生クラスはこれらのインターフェイスを実装する必要があります。インターフェイス クラスの関数はすべて純粋な仮想関数であり、特定の実装はありません。

例えば:

class Printable {
    
    
public:
    virtual void print() const = 0; // 纯虚函数
};

class Book : public Printable {
    
    
public:
    void print() const override {
    
    
        // 打印图书信息的具体实现
    }
};

class Magazine : public Printable {
    
    
public:
    void print() const override {
    
    
        // 打印杂志信息的具体实现
    }
};

int main() {
    
    
    Book book;
    Magazine magazine;
    book.print();
    magazine.print();
    return 0;
}

上記の例では、具体的な実装を持たないPrintable純粋な仮想関数を定義するインターフェイス クラスですprint()派生クラスBookと派生関数は、インターフェイス仕様を満たす関数Magazineを実装する必要があります。print()このようにして、さまざまな種類の印刷操作を実現でき、特定の実装は各派生クラスによって完了します。

4. 次の関数のどこが間違っているのでしょうか

int &pe(int r,int i)
{
    
    
    Int re = r*i;
    return re
}

解答分析

以下の関数には次の問題があります。

  1. 関数はローカル変数への参照を返します。関数内で定義された変数は、re関数の実行終了後に破棄されるローカル変数です。ただし、この関数はreへの参照を返すため、返された参照は無効なメモリ アドレスを指すことになります。この参照にアクセスすると、未定義の動作が発生します。
  2. 関数宣言の TypeError:関数宣言のIntshould 」のスペルが間違っています。int

修正された関数は次のようになります。

int pe(int r, int i)
{
    
    
   int re = r * i;
   return re;
}

修正した関数では、reローカル変数の型を に修正しint、通常の戻り値として返します。これにより、ローカル変数への参照ではなく、有効な値が返されることが保証されます。

5. クラスオブジェクトがコピーされたり割り当てられたりするのを防ぐためにどのような方法が使用されますか?

クラス オブジェクトがコピーされたり割り当てられたりするのを防ぐには、次の 2 つの方法を使用できます。

  1. コピー コンストラクターと代入演算子を無効にする: クラスのコピー コンストラクターと代入演算子をプライベートとして宣言し、実装を提供しないことができます。この方法では、クラスのオブジェクトをコピーして割り当てることはできません。
class MyClass {
    
    
private:
    MyClass(const MyClass&); // 禁用拷贝构造函数
    MyClass& operator=(const MyClass&); // 禁用赋值操作符

public:
    // 其他成员和函数的定义
};

上記のコードでは、コピー コンストラクターと代入演算子をプライベートとして宣言することで、外部コードがこれらの関数にアクセスできなくなり、オブジェクトのコピーと代入が防止されます。

  1. C++11 の delete キーワードの使用: コピー コンストラクターと代入演算子のデフォルト実装は、C++11 で導入された delete キーワードを使用して明示的に削除できます。
class MyClass {
    
    
public:
    MyClass(const MyClass&) = delete; // 显式删除拷贝构造函数
    MyClass& operator=(const MyClass&) = delete; // 显式删除赋值操作符

    // 其他成员和函数的定义
};

上記のコードでは、= deleteコピー コンストラクターの明示的な削除と代入演算子の既定の実装を使用して、オブジェクトのコピーと代入を防止しています。

これら 2 つのメソッドは、クラス オブジェクトのコピーと割り当てを効果的に防止し、クラスのオブジェクトが使用されるときに特定の方法でのみ作成および割り当てられるようにすることで、オブジェクトのライフ サイクルと状態をより適切に制御できます。

6. ヘッダー ファイルで #ifndef/#define/#endif を使用するとどのような影響がありますか?

解答分析

C++ では、#ifndef、#define和#endif組み合わせを使用する機能は、ヘッダー ファイルが繰り返しインクルードされることを防ぐこと、つまり、同じヘッダー ファイルが同じソース ファイルに複数回インクルードされることを防ぐことです。

具体的な機能は以下の通りです。

  1. #ifndef(定義されていない場合): このコマンドは、現在のコードにマクロが定義されているかどうかを確認するために使用されます。指定したマクロがすでに定義されている場合は、後続のコード ブロックをスキップして、 #endif「everywhere」。指定されたマクロが未定義の場合、実行は後続のコード ブロックから続行されます。
  2. #define(定義): このコマンドはマクロを定義するために使用されます。ヘッダー ファイルでは、ヘッダー ファイルを識別するために通常、大文字とアンダースコアの組み合わせを使用して特定のマクロ名が使用されます。このマクロの値は空または空でない値にすることができます。
  3. #endif(end): この命令は条件付きコンパイルの終了を示します。#ifndefペアで使用すると、条件付きコンパイルの範囲の終了を示します

#ifndef、#define、#endifを使用することでヘッダファイルの多重インクルードを防ぐことができます。ヘッダー ファイルが最初にインクルードされるとき、マクロは未定義であるため、条件付きコンパイルが通過し、マクロが定義されます。同じヘッダー ファイルが再び見つかった場合、マクロはすでに定義されているため、条件付きコンパイルによってヘッダー ファイルの内容がスキップされ、繰り返しインクルードされる問題が回避されます。

たとえば、次のような例があります。

#ifndef MY_HEADER_H
#define MY_HEADER_H

// 头文件的内容

#endif // MY_HEADER_H

上の例では、MY_HEADER_Hヘッダー ファイルを識別するために使用されるカスタム マクロ名です。ヘッダー ファイルが同じソース ファイルに複数回インクルードされている場合、条件付きコンパイルは成功し、マクロは最初にインクルードされたときにのみ定義されます。マクロはすでに定義されているため、後続の組み込み操作ではヘッダー ファイルの内容がスキップされ、繰り返しの組み込みの問題が回避されます。

ヘッダー重複保護とは

ヘッダー ファイルの二重インクルードとは、同じヘッダー ファイルが同じソース ファイルに複数回インクルードされる状況を指します。ヘッダー ファイルが複数回インクルードされると、いくつかの問題やエラーが発生します。

問題とエラーには次のようなものがあります。

  1. 重複した定義: ヘッダー ファイルに型定義、グローバル変数、マクロ定義などが含まれている場合、同じヘッダー ファイルを複数回インクルードすると、これらの定義が繰り返し表示され、再定義エラーが発生します。
  2. 関数の定義の重複: ヘッダー ファイルに関数の定義が含まれている場合、同じヘッダー ファイルを複数回インクルードすると、これらの関数が複数定義され、再定義エラーが発生します。
  3. 依存関係エラー: 複数のヘッダー ファイルが互いにインクルードしており、これらのヘッダー ファイルに同じヘッダー ファイルが含まれている場合、循環インクルージョンが形成され、コンパイル エラーが発生します。

ヘッダー ファイルが繰り返しインクルードされる問題は、#ifndef、#define和#endif組み合わせを使用するなど、条件付きコンパイル ディレクティブを使用することで解決できます。これにより、同じヘッダー ファイルが同じソース ファイルに複数回インクルードされることを防ぎ、ヘッダー ファイルの定義と宣言が 1 回だけ表示されるようにし、定義の繰り返しやコンパイル エラーを回避できます。

7. CPP における名前空間キーワードの適用シナリオを説明してください

C++ では、namespace キーワードを使用して名前空間 (ネームスペース) を作成します。これは、グローバル スコープを異なる名前空間に分離して、名前の競合を解決し、コードを整理するために使用されます。名前空間キーワードの適用シナリオは次のとおりです。

  1. 名前の競合の解決: プログラム内で複数のサードパーティ ライブラリまたは複数のモジュールを使用する場合、異なるライブラリまたはモジュールが同じ名前を定義する可能性があります。これにより、コンパイラがどの定義が使用されているかを判断できなくなり、名前の競合が発生する可能性があります。異なるコードを異なる名前空間に配置することで、名前の競合の問題を回避できます。各名前空間の名前は、他の名前空間の名前と衝突しません。
  2. コードの編成とモジュール化: 名前空間を使用すると、関連するコードをまとめて編成し、論理モジュールまたはサブシステムを形成できます。名前空間を使用すると、関連するクラス、関数、変数などを同じ名前空間に配置できるため、コードがより構造化されモジュール化され、コードの可読性と保守性が向上します。
  3. サードパーティのライブラリおよびフレームワークのカプセル化: サードパーティのライブラリまたはフレームワークを使用するプログラムを開発する場合、ライブラリまたはフレームワークのコードを独立した名前空間に配置して、独自のコードとの名前の競合を避けることができます。これにより、サードパーティ ライブラリの関数をより適切にカプセル化でき、使用時に名前空間を明確に指定できるため、コードの可読性と保守性が向上します。
  4. ネストされた名前空間: 名前空間は入れ子にすることができます。つまり、1 つの名前空間内に別の名前空間を定義します。これにより、コードの構造をさらに整理および分割して、より粒度の細かい名前空間を形成できます。

例:

namespace Math {
    
    
    // Math命名空间中的函数和变量
    int add(int a, int b);
    int subtract(int a, int b);

    namespace Geometry {
    
    
        // Geometry命名空间中的函数和变量
        double calculateArea(double radius);
        double calculatePerimeter(double side);
    }
}

int main() {
    
    
    int result = Math::add(3, 4);
    double area = Math::Geometry::calculateArea(2.5);
    return 0;
}

上の例では、2 つの関数 add() とsubtract() が Math 名前空間で定義され、ネストされた Geometry 名前空間で CalculateArea() 関数と CalculatePerimeter() 関数が定義されています。ネームスペースを使用すると、使用する関数または変数が属するネームスペースを明確に指定できるため、名前の競合が回避され、コードの編成と管理が向上します。

8. これらのクラス メンバーの名前変更が合理的かどうか、またその理由を教えてください。

string valueA; //姓名
int iValueB; //年龄
double m_iValueC; //分数

未解決の質問

特定のクラス メンバー変数名の場合:

  1. string valueA;- 名前

このメンバー変数の名前はvalueA、妥当な名前を表します。変数名は、それが何を表しているのかを明確に説明できる必要があります。意味のある名前を使用すると、コードの読みやすさと理解しやすさが向上します。

  1. int iValueB;- 年

このメンバー変数の名前はiValueB年齢を表しており、これも合理的です。iこれは一般的な命名規則ですが、名前には型情報 (整数を表す) が含まれます。age名前を付けるときは、コードの可読性をさらに高めるために、たとえば など、よりわかりやすい名前を使用することを検討してください。

  1. double m_iValueC;- 分数

m_iValueCスコアを表すこのメンバー変数の名前も合理的です。mプレフィックスはメンバ変数 (member) を示す場合もあれば、i整数型を示す場合もありますが、ここではdouble型の変数です。クラス メンバー変数の命名規則はプロジェクト、チーム、または個人によって異なる場合がありますが、その意味をより明確に伝えるわかりやすい名前を選択する必要があります (古い婉曲表現)。したがって、この変数には、よりわかりやすい名前を使用する方scoreが良いかもしれません。

概要: コードを理解し、保守しやすくするために、変数にはわかりやすい名前を付けるようにしてください。適切な変数名を選択すると、コードの読みやすさが向上し、他の開発者が変数の意味と目的をすぐに理解できるようになります。

拡張: 命名法

キャメル ケースは、単語間に区切り文字を使用せず、各単語の最初の文字を大文字にすることで単語を区別する一般的な命名スタイルです。キャメルケースの一般的な形式をいくつか示します。

  1. 小文字のキャメルケース: 最初の単語の最初の文字は小文字で、後続の各単語の最初の文字は大文字になります。例: myVariableNamefirstNametotalCount
  2. 大キャメルケース: 各単語の最初の文字が大文字になります。通常はクラス名または型名に使用されます。例: PersonMyClassPhoneNumber

CamelCase は、Java、C++、C#、JavaScript などを含む多くのプログラミング言語で広く使用されています。

キャメルケースの命名に加えて、一般的な命名スタイルには次のようなものがあります。

  1. スネークケース: 単語をアンダースコアで接続し、各単語に小文字を使用します。例: value_ai_value_bm_i_value_cアンダースコアの命名法は、C、Python などの多くのプログラミング言語で広く使用されています。
  2. 大文字: すべての単語は大文字で、単語はアンダースコアで区切られます。通常、定数に名前を付けるために使用されます。例: VALUE_AI_VALUE_BM_I_VALUE_C
  3. 小文字: すべての文字は小文字であり、単語の間に区切り文字はありません。例: valueaivaluebmivaluecこの命名スタイルは、Lisp や Scheme などの一部のプログラミング言語で広く使用されています。
  4. ハンガリー語表記: 変数名の前に型を示す 1 つ以上の接頭辞を追加し、その後にキャメル ケースまたはアンダースコア命名法を続けます。例: strValueAnIValueBdM_IValueCハンガリー語表記は過去に人気がありましたが、現代のプログラミング実践ではあまり一般的ではありません。

これらの命名スタイルの選択は、プログラミング言語、プロジェクトの規則、および個人の好みによって異なります。コードの可読性と保守性を向上させるには、同じプロジェクトまたはチーム内で一貫性を保ち、同じ命名スタイルに従うことが重要です。

9. 次のコードは何を出力しますか?

class A
{
    
    
public:
    virtual void fun1() {
    
     printf("in A fun1\n"); };
    void fun2() {
    
     printf("in A fun2\n"); };
};

class B :public A {
    
    
public:
    virtual void fun1() {
    
     printf("in B fun1\n"); };
    void fun2() {
    
     printf("in B fun2\n"); };
};

int main() {
    
    
    B obj;
    A* p = new B();
    obj.fun1();
    obj.fun2();
    p->fun1();
    p->fun2();
}

得られた結果は次のとおりです

in B fun1
in B fun2
in B fun1
in A fun2

なぜ?

解答分析

これは、クラス A とクラス B の間に継承関係があるためです。クラス A の関数 fun1() および fun2() は仮想関数として宣言されており、クラス B はクラス A を継承することでこれらの仮想関数をオーバーライドします。

コードでは、オブジェクト obj はクラス B のインスタンスであるため、obj.fun1() を呼び出すと、クラス B で書き換えられた fun1() 関数が呼び出され、「in B fun1」が出力されます。同様に、obj.fun2() を呼び出すと、クラス B の fun2() 関数が呼び出され、「in B fun2」が出力されます。

ポインタ p はクラス A へのポインタとして宣言され、クラス B のオブジェクトが new 演算子によって作成され、ポインタ p に割り当てられます。ポインタ p を使用して p->fun1() を呼び出す場合、 fun1() はクラス A で仮想関数として宣言され、クラス B で書き換えられるため、クラス B の fun1() 関数が呼び出され、出力 "in B fun1」。ただし、ポインタ p を使用して を呼び出した場合p->fun2()、クラス A では fun2() が仮想関数として宣言されていないため、ポインタ (クラス A) の型に応じて対応する関数が呼び出され、出力が出力されます"in A fun2"

要約すると、仮想関数の呼び出しは、ポインターまたは参照自体の型ではなく、ポインターまたは参照が指すオブジェクトの実際の型に依存します。非仮想関数呼び出しは、ポインターまたは参照のタイプによって異なります。

3. 包括的な質問

1. OSI 参照モデルの 7 つの層とは何ですか? TCP/IP の 4 つの層とは何ですか? FTPとTCPはどの層のプロトコルに属しますか?

質問 1 への回答: OSI (Open Systems Interconnection) 参照モデルには、次の 7 つの層が含まれています。

  1. 物理層 (物理層): ビット ストリームの送信と、物理媒体とのインターフェイスに関連する電気的、機械的、機能的特性の処理を担当します。

  2. データ リンク層 (データ リンク層): 信頼性の高いポイントツーポイント データ伝送を提供し、物理アドレス指定、エラー検出、フロー制御などのメカニズムを通じて元のビット ストリームを意味のあるフレームに変換します。

  3. ネットワーク層 (ネットワーク層): データ パケットのルーティングと転送を担当し、ネットワーク アドレス指定、論理アドレス指定、ルーティングなどの機能を含む、異なるネットワーク間の通信を実現します。

  4. トランスポート層: エンドツーエンドの信頼性の高いデータ送信を提供し、データの整合性、信頼性、およびセグメント化、再構成、確認応答、再送信などのフロー制御を保証します。

  5. セッション層 (セッション層): セッション (セッションは 2 つのアプリケーション間の通信セッションです) の確立、管理、終了を担当し、セッション層の制御と同期を提供します。

  6. プレゼンテーション層: データの表現と変換を処理し、異なるシステムのデータ形式が相互に理解できるようにし、データの暗号化、復号化、圧縮、形式変換などの機能を提供します。

  7. アプリケーション層: 電子メール、ファイル転送、リモート ログインなどのアプリケーション固有のサービスとプロトコルを提供します。

質問 2 に答える: TCP/IP プロトコル ファミリは、次の層を含む、より単純化された 4 層モデルを使用します。

  1. ネットワーク インターフェイス層 (ネットワーク インターフェイス層): 物理ネットワーク メディアと直接対話し、データ リンク層と物理層の機能を担当します。

  2. インターネット層 (インターネット層): OSI モデルのネットワーク層と同様に、ネットワークのアドレス指定、ルーティング、パケット転送を担当します。

  3. トランスポート層 (トランスポート層): OSI モデルのトランスポート層と同じ機能を持ち、主に TCP および UDP プロトコルを使用してエンドツーエンドの信頼性の高いデータ伝送を提供します。

  4. アプリケーション層 (アプリケーション層): OSI モデルのアプリケーション層と同様に、HTTP、FTP、SMTP などのさまざまなアプリケーションにサービスとプロトコルを提供します。

質問3に答えてください

FTP (File Transfer Protocol) は、クライアントとサーバー間のファイル転送に使用されるアプリケーション層に属するプロトコルです。TCP (Transmission Control Protocol) は、トランスポート層に属するプロトコルであり、信頼性の高いエンドツーエンドのデータ伝送を提供します。

2. Linux プロセス間の通信方法は何ですか?

Linux では、プロセス間通信 (IPC、プロセス間通信) のさまざまな方法があり、一般的に使用されるものには次のものがあります。

  1. パイプライン (パイプ): パイプラインは、親プロセスと子プロセスの間でデータを転送できる半二重通信方法です。匿名パイプと名前付きパイプに分かれています。名前なしパイプは関連するプロセス間でのみ使用できますが、名前付きパイプは無関係なプロセスでも使用できます。
  2. 名前付きパイプ (FIFO): 名前付きパイプは FIFO (先入れ先出し) とも呼ばれ、プロセス間の名前付き通信方法を提供し、無関係なプロセス間の通信を可能にします。
  3. 共有メモリ: 共有メモリは、複数のプロセスが同じ物理メモリ領域にアクセスできるようにする効率的なプロセス間通信方法です。プロセスはこのメモリ領域を直接読み書きできるため、データの重複が回避されます。
  4. セマフォ (セマフォ): セマフォはプロセスの同期と相互排他のためのメカニズムであり、カウンターを通じて共有リソースへのアクセスを制御します。プロセスは、セマフォの待機、セマフォの解放など、セマフォ上で動作できます。
  5. メッセージ キュー (メッセージ キュー): メッセージ キューは、プロセス間でメッセージを転送できるメカニズムです。メッセージはキューに入れられ、他のプロセスはキューからメッセージを読み取ることができます。
  6. ソケット: ソケットは、異なるホスト間のプロセス間通信のためのメカニズムです。ローカルプロセス間で通信する場合は、Unix ドメインソケット (Unix Domain Socket) を使用することもできます。
  7. シグナル: シグナルは、イベントが発生したことをプロセスに通知するために使用される非同期通信メカニズムです。プロセスはシグナル ハンドラーを登録して、シグナルをキャッチして処理できます。

これらの方法には独自の特徴があり、さまざまなシナリオやニーズに適しています。開発者は、特定の状況に応じて適切なプロセス間通信方法を選択できます。

3. MySQL 操作、テーブル T_USER(id,name,info) は、次の要件を満たす SQL ステートメントを記述します。

(1) テーブルにデータを挿入するにはどうすればよいですか?

(2) id=100のユーザーを探しますか?

(3) id=20のユーザーを削除しますか?

(4) id=10のユーザー名を123456に変更しますか?

解答分析

以下は、要件に対応する SQL ステートメントの実装です。

(1) テーブルにデータを挿入します。

INSERT INTO T_USER (id, name, info) VALUES (1, 'John', 'Some information');

(2) id=100 のユーザーを検索します。

SELECT * FROM T_USER WHERE id = 100;

(3) id=20 のユーザーを削除します。

DELETE FROM T_USER WHERE id = 20;

(4) id=10 のユーザー名を 123456 に変更します。

UPDATE T_USER SET name = '123456' WHERE id = 10;

上記の例のテーブル名、フィールド名、具体的な値は実際の状況に応じて置き換えられることに注意してください。

4. 次の検索方法のうち、最も速いのはどれですか? なぜ?

バイナリ検索、トラバーサル検索、ハッシュ検索、ランダム検索

解答分析

一般に、次の理由により、二分検索が最も速い検索方法となります。

  1. 時間計算量: 二分探索の時間計算量は O(log n) です。ここで、n は検索範囲内の要素の数です。対照的に、トラバーサル ルックアップ、ランダム ルックアップ、およびハッシュ ルックアップの時間計算量は通常 O(n) です。
  2. データの順序付けの要件: 二分探索ではデータが順序付けられている必要がありますが、データが一度順序付けられると、二分探索の効率は非常に高くなります。二分探索では、探索範囲を毎回半分にすることで、目的の要素の位置を素早く見つけることができます。
  3. 適用範囲: トラバーサル ルックアップ、ランダム ルックアップ、およびハッシュ ルックアップは、通常、順序付けされていないデータのルックアップに適していますこのような場合、データセット全体を走査するか、ハッシュ関数を使用してインデックス位置を計算する必要がありますが、二分探索のように検索範囲を迅速に絞り込むことはできません。

上記の結論は一般的な場合に確立されることに注意してください。それを正確に見つける方法は、問題の特定の状況に最も関連します。たとえば、データの量が少なく、高度に順序付けされていない場合は、走査検索の方が二分検索よりも高速である可能性があります。したがって、最速の検索方法を選択する場合は、データの特性、順序要件、実際のパフォーマンス要件を総合的に考慮する必要があります。

補足: ハッシュ検索とランダム検索が最適ではないのはなぜですか?

  1. ハッシュ ルックアップの制限: ハッシュ ルックアップは、キーを保存場所にマッピングするハッシュ関数に依存するため、ハッシュ関数を設計する際には均一な分散と衝突の回避を考慮する必要があります。ハッシュ関数の設計に無理があったり、ハッシュの衝突が多かったりすると検索効率が低下します。また、範囲検索やあいまい一致が必要な場合、ハッシュ検索では対応できません。
  2. ランダム ルックアップの非効率性: ランダム ルックアップは通常、比較のためにデータセット全体から要素をランダムに選択することによって実現されます。この検索方法は、平均して、ターゲット要素を見つけるためにほとんどのデータを走査する必要があるため、非効率的です。ランダム ルックアップは、小さなデータセットではうまく機能しますが、大規模なデータセットでは効率が低くなります。
  3. 順序付けされたデータには適していません: ハッシュ ルックアップとランダム ルックアップは、順序付けされたデータ セットには適していません。順序付けされたデータ セットでは、二分検索はデータの順序を利用でき、比較ごとに検索範囲をすばやく絞り込むことができるため、より効率的です。しかし、ハッシュ検索やランダム検索はデータの順序性を活かしていないため、その利点を最大限に発揮することができません。

5. C/CPP コードを作成し、文字列を整数に変換する関数を作成します。

以下は、文字列を整数に変換する単純な C++ 関数の例です。

#include <iostream>
#include <string>

int stringToInt(const std::string& str) {
    
    
    int result = 0;
    int sign = 1; // 符号位,默认为正数
    int i = 0;

    // 跳过字符串前面的空格
    while (str[i] == ' ') {
    
    
        i++;
    }

    // 处理正负号
    if (str[i] == '-' || str[i] == '+') {
    
    
        sign = (str[i++] == '-') ? -1 : 1;
    }

    // 转换数字部分
    while (str[i] >= '0' && str[i] <= '9') {
    
    
        // 判断是否溢出
        if (result > INT_MAX / 10 || (result == INT_MAX / 10 && (str[i] - '0') > INT_MAX % 10)) {
    
    
            return (sign == 1) ? INT_MAX : INT_MIN;
        }
        result = result * 10 + (str[i++] - '0');
    }

    return result * sign;
}

int main() {
    
    
    std::string str = "12345";
    int result = stringToInt(str);
    std::cout << "Result: " << result << std::endl;

    return 0;
}

この関数は文字列の文字を走査し、符号ビット、スペース、オーバーフローなどの処理を含む数値の規則に従って変換します。最後に変換された整数値を返します。

上記の例は単なる単純な実装であり、すべての入力状況とエラー処理が考慮されているわけではないことに注意してください。実際の開発では、より多くの入力状況やエラー処理に対処するために、機能をさらに改善および最適化する必要がある場合があります。

6. C/CPP コードを作成して、文字列の順序を逆にする関数を実装します。たとえば、順序を逆にすると、「abcd」が「dcba」になります。

以下は、文字列を反転する機能を実現する簡単な C++ 関数の例です。

#include <iostream>
#include <string>

std::string reverseString(const std::string& str) {
    
    
    std::string reversedStr;
    int length = str.length();

    // 从字符串末尾开始遍历,逐个字符添加到新字符串中
    for (int i = length - 1; i >= 0; i--) {
    
    
        reversedStr += str[i];
    }

    return reversedStr;
}

int main() {
    
    
    std::string str = "abcd";
    std::string reversedStr = reverseString(str);
    std::cout << "Reversed String: " << reversedStr << std::endl;

    return 0;
}

この関数は、文字列の末尾からたどって新しい文字列に 1 文字ずつ追加することにより、文字列の順序を逆にします。ループでは、最初のインデックスは文字列の長さから 1 を引いたものになり、インデックスが 0 になるまで文字が逆順にデクリメントされて新しい文字列に追加されます。

上記の例は単なる単純な実装であり、入力が空の文字列である場合は考慮されていないことに注意してください。実際の開発では、より多くの入力状況やエラー処理に対処するために、機能をさらに改善および最適化する必要がある場合があります。

おすすめ

転載: blog.csdn.net/kokool/article/details/130855117