C++ を理解する (中) - 「C++」

CSDN のみなさん、こんにちは。今日は Xiao Yalan の C++ コラムです。C++ の最初の章が始まります。C++ の世界に入りましょう!


デフォルトパラメータ

デフォルトパラメータの概念

デフォルトパラメータは、関数の宣言または定義時に関数のパラメータのデフォルト値を指定します。関数を呼び出すとき、実パラメータが指定されていない場合は仮パラメータのデフォルト値が採用され、それ以外の場合は指定された実パラメータが使用されます。

デフォルトのパラメータは現実のスペアタイヤに相当します。

#include<iostream>
using namespace std;
void Func(int a = 0)
{
	cout << a << endl;
}

int main()
{
	Func(); // 没有传参时,使用参数的默认值
	Func(10);// 传参时,使用指定的实参
	return 0;
}

 

デフォルトパラメータの分類

すべてのデフォルトパラメータ

void Func(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}

関数呼び出しの場合、パラメーターを渡さないことも、1 つのパラメーター、2 つのパラメーター、または 3 つのパラメーターを渡すこともできます。

しかし、C++ はそのような再生方法をサポートしていません。つまり、2 番目のパラメーターにのみパラメーターを渡し、残りは与えられない、または 3 番目のパラメーターにのみパラメーターを渡したいが、最初の 2 つは渡しません。

int main()
{
	Func();
	Func(1);
	Func(1, 2);
	Func(1, 2, 3);
	return 0;
}

 

準デフォルトパラメータ(一部のパラメータのみが欠落しています)

void Func(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

パラメータを渡す必要があります。

1 つを通過することも、2 つを通過することも、3 つを通過することもできます。

  • 準デフォルトのパラメータは右から左に順番に指定する必要があり、交互に指定することはできません。
int main()
{
	Func(1);
	Func(1, 2);
	Func(1, 2, 3);
	return 0;
}

以前C言語でスタックを書いたとき、初期化スタックとスタックは次のように書き、容量を拡張する必要がありました。

// 初始化栈 
void StackInit(Stack* pst)
{
	assert(pst);
	pst->a = NULL;
	pst->top = 0;
	pst->capacity = 0;
}

// 入栈 
void StackPush(Stack* pst, STDataType x)
{
	assert(pst);
	//扩容
	if (pst->top == pst->capacity)
	{
		int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapacity;
	}
	pst->a[pst->top] = x;
	pst->top++;
}

C++ のデフォルト パラメーターを学習したので、これを行うことができます。スタックを初期化するときに、デフォルト パラメーターを指定して、最初にスペースの一部を開くことができます。これにより、その後の拡張の問題を解決できます。(拡張は本質的に不安定で消費が多いため)

namespace zyl
{
	typedef struct Stack
	{
		int* a;
		int top;
		int capacity;
	}Stack;

	// 不允许声明和定义同时给缺省参数
	// 声明给,定义不给
	void StackInit(Stack* ps, int N = 4);
	void StackPush(Stack* ps, int x);
}
void zyl::StackInit(Stack* pst, int N)
{
	pst->a = (int*)malloc(sizeof(int) * N);

	pst->top = 0;
	pst->capacity = 0;
}

void zyl::StackPush(Stack* pst, int x)
{
	// ...
}
int main()
{
	zyl::ST st1;
	StackInit(&st1, 10);
	for (size_t i = 0; i < 10; i++)
	{
		StackPush(&st1, i);
	}

	zyl::ST st2;
	StackInit(&st2, 100);
	for (size_t i = 0; i < 100; i++)
	{
		StackPush(&st2, i);
	}

	// 不知道可能会插入多少个
	zyl::ST st3;
	StackInit(&st3);

	return 0;
}
  • デフォルトパラメータを関数の宣言と定義に同時に使用することはできません(宣言は指定され、定義は指定されません)。
 //a.h
  void Func(int a = 10);
  
  // a.cpp
  void Func(int a = 20)
  {}
  
  // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该
用那个缺省值。
  • デフォルト値は定数またはグローバル変数である必要があります
  • C言語はサポートしていません(コンパイラはサポートしていません)

関数のオーバーロード

自然言語では、単語は複数の意味を持つことができ、人々は文脈を通じてその単語の本当の意味を判断できます。つまり、単語が多重定義されています。

例: かつて、人々は国営の 2 つのスポーツ イベントを見たり心配したりする必要はないというジョークがありました。1 つは卓球、もう 1 つは男子サッカーです。前者は「誰も勝てない!」、後者は「誰も勝てない!」です。

 関数のオーバーロードの概念

関数のオーバーロード: これは関数の特殊なケースです。C++ では、同様の関数を持つ同じ名前の複数の関数を同じスコープ内で宣言できます。同じ名前のこれらの関数には、異なる仮パラメータ リスト (パラメータの数や型、または型の順序) があり、類似した関数を実装するさまざまなデータ型の問題に対処するためによく使用されます。

C 言語では、同じ名前の関数は使用できません。

// 1、参数类型不同
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}

double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}

int main()
{
	cout << Add(1,2) << endl;
	cout << Add(1.1,2.2) << endl;
	return 0;
}

この機能の使い方はとても簡単です!

C 言語の段階では、Swap 関数は次のように記述されることがよくあります。

void Swap1(int* p1, int* p2)
{
	//...
}

void Swap2(double* p1, double* p2)
{
	//...
}

C言語で書くにはこれしかありません。

ただし、C++ では、関数がオーバーロードされている限り、同じ名前の関数が許可されます。

void Swap(int* p1, int* p2)
{
	//...
}

void Swap(double* p1, double* p2)
{
	//...
}
int main()
{
	int i = 1, j = 2;
	double k = 1.1, l = 2.2;
	Swap(&i, &j);
	Swap(&k, &l);
	return 0;
}

自動マッチング!

// 2、参数个数不同
void f()
{
	cout << "f()" << endl;
}

void f(int a)
{
	cout << "f(int a)" << endl;
}

// 3、参数类型顺序不同
void f(int a, char b)
{
	cout << "f(int a,char b)" << endl;
}

void f(char b, int a)
{
	cout << "f(char b, int a)" << endl;
}
int main()
{
	f();
	f(10);

	f(10, 'a');
	f('a', 10);

	return 0;
}

 

注: 異なる戻り値は関数のオーバーロードにはなりません。

しかし:

名前空間 zyl1
{     void func(int x)     {} }名前空間 zyl2 {     void func(double x)     {}







関数のオーバーロードにはなりません。ドメインが違うからです。

zyl2 が zyl1 に変更されると、関数はオーバーロードされます。

void func(int a)
{     cout << "void func(int a)" << endl; void func(int a, int b = 1) {     cout << "void func(int a,int b)" << endl; }





上記 2 つの関数は確かに関数のオーバーロードを構成します。

ただし呼び出しには曖昧さがあり、main 関数に func(1) を記述すると上記 2 つの関数を呼び出すことができ、コンパイラはどの関数を呼び出せばよいかわかりません。

C++ は、関数のオーバーロードの原則、名前マングリング (名前マングリング) をサポートしています。

C++ は関数のオーバーロードをサポートしているのに、C 言語は関数のオーバーロードをサポートしていないのはなぜですか?

C/C++ では、プログラムを実行するには、前処理、コンパイル、アセンブル、リンクの各段階を経る必要があります。

void func(int i, double d)
{
	cout << "void func(int i, double d)" << endl;
	
}

void func(double d, int i)
{
	cout << "void func(double d, int i)" << endl;
}
int main()
{
	func(1, 1.1);
	func(1.1, 1);
	return 0;
}

なぜコンパイラはこれほど賢いのでしょうか? C++ は可能だが C は不可能なのはなぜですか? C++ はなぜパラメータに基づいて相互に呼び出しを行うのですか?

 

 

  • 実際のプロジェクトは通常複数のヘッダファイルと複数のソースファイルで構成されており、C 言語のコンパイルとリンクを通じて、[b.cpp で定義された Add 関数が現在の a.cpp で呼び出された場合] コンパイル後リンク前に、ao のオブジェクト ファイルには Add の関数アドレスが存在しないことがわかります。Add は b.cpp で定義されているため、Add のアドレスは bo にあります。じゃあ何をすればいいの?
  • リンカは、ao が Add を呼び出しているが、Add のアドレスがないことを確認すると、bo のシンボル テーブルで Add のアドレスを見つけて、それらをリンクします。
  • 次に、リンクするときに、Add 関数に直面して、リンカーはそれを見つけるためにどの名前を使用しますか? ここでの各コンパイラには、関数名を装飾するための独自のルールがあります。
  • Windows 上の VS の変更ルールは複雑すぎるのに対し、Linux 上の g++ の変更ルールはシンプルで理解しやすいため、以下では g++ を使用して変更された名前を示します。
  • 以下から、gcc 関数の名前は変更後も変更されていないことがわかります。そして、変更された g++ の関数は [_Z+関数の長さ+関数名+型の頭文字] になります。

        C言語コンパイラでコンパイルした結果

         結論: Linux では、gcc でコンパイルした後、関数名の変更は変わりません。

         C++コンパイラでコンパイルした結果

         結論: Linux では、g++ でコンパイルした後、関数名の変更が変更され、コンパイラは変更された名前に関数パラメータの型情報を追加します。

        Windows での名前変更ルール

        Linux と比較すると、Windows 上の VS コンパイラの関数名変更ルールが比較的複雑で理解しにくいことがわかりますが、原理は似ているため、詳細な調査は行いません。  

C/C++ 関数呼び出し規約 - 控えめなライオンのブログ - CSDN ブログ

  • このことから、C 言語では同じ名前の関数を区別する方法がないため、オーバーロードをサポートできないことがわかりました。C++ は関数の変更ルールによって区別されており、パラメータが異なれば変更名も異なり、オーバーロードもサポートされます。
  • 2 つの関数の関数名とパラメーターが同じでも、戻り値が異なる場合、コンパイラーには呼び出し時に区別する方法がないため、オーバーロードにはなりません。

 


引用

参考コンセプト

参照は変数の新しい定義ではなく、既存の変数のエイリアスです。コンパイラは参照変数用にメモリ空間をオープンせず、参照する変数と同じメモリ空間を共有します。

たとえば、李逵は国内では「鉄の雄牛」、世界では「黒い旋風」と呼ばれています。

 

 型&参照変数名(オブジェクト名) = 参照エンティティ;

void TestRef()
{
	int a = 10;
	int& ra = a;//<====定义引用类型
	//给a取了一个别名:ra

	cout << &a << endl;//&表示取地址
	cout << &ra << endl;
}
int main()
{
	TestRef();
	return 0;
}

注: 参照型は、参照されるエンティティと同じ型である必要があります。

以前 Xiao Yalan によって書かれたバイナリ ツリーのプリオーダー トラバースでは、ポインタの代わりに参照を使用できます。

int TreeSize(struct TreeNode* root)
{
    return root==NULL?0:TreeSize(root->left)+TreeSize(root->right)+1;
}
//递归里面传数组下标要注意!!!
//每个栈帧里面都有一个i
void preorder(struct TreeNode* root,int* a,int* pi)
{
    if(root==NULL)
    {
        return;
    }
    a[(*pi)++]=root->val;
    preorder(root->left,a,pi);
    preorder(root->right,a,pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    //root是输入型参数,returnSize是返回型参数
    *returnSize=TreeSize(root);
    int* a=(int*)malloc(*returnSize*sizeof(int));
    int i=0;
    preorder(root,a,&i);
    return a;
}

 代わりに引用します:

int TreeSize(struct TreeNode* root)
{
    if (root == NULL)
        return 0;
    else
        return TreeSize(root->left) + TreeSize(root->right) + 1;
}

void _preorderTraversal(struct TreeNode* root, int* a, int& ri)
{
    if (root == NULL)
        return;

    printf("[%d] %d ", ri, root->val);
    a[ri] = root->val;
    ++ri;
    _preorderTraversal(root->left, a, ri);
    _preorderTraversal(root->right, a, ri);
}

int* preorderTraversal(struct TreeNode* root, int& returnSize) {
    int size = TreeSize(root);
    int* a = (int*)malloc(sizeof(int) * size);
    int i = 0;
    _preorderTraversal(root, a, i);
    returnSize = size;
    return a;
}

ri は i の別名です。

スワップ機能には新しいプレイ方法もあります。

void swap(int& x1, int& x2)
{     int tmp = x1;     x1 = x2;     x2 = tmp;



以前の単一リンク リストを再生する新しい方法もあります。

typedef struct ListNode{     int val;     struct ListNode* next; }ListNode;  //C 言語でセカンダリ ポインタを再生する方法void PushBack(ListNode** pphead, int x) {     ListNode* newnode;     if (*pphead == NULL)     {         *pphead = newnode;     }     else     {












    }
}

int main()
{     ListNode* plist = NULL;     PushBack(&plist, 1);     PushBack(&plist, 2);     PushBack(&plist, 3);



    0を返します。
}

typedef struct ListNode {     int val;     struct ListNode* next; }ListNode、*PListNode;


//PListNode は struct ListNode*
 //CPP、参照されるゲームプレイ
void PushBack(ListNode*& phead, int x)

//ListNode* の名前
//void PushBack(PListNode& phead, int x)
{     ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));     // ...     if (phead == NULL)     {         phead = newnode;     }     それ以外の場合は     {







    }
}

int main()
{     ListNode* plist = NULL;     PushBack(plist, 1);     PushBack(plist, 2);     PushBack(plist, 3);



    0を返します。
}

引用特性

  • 参照は定義時に初期化する必要があります
  • 変数には複数の参照を含めることができます
  • 参照がエンティティを参照すると、別のエンティティを参照できなくなります
void TestRef()
{
	int a = 10;
	// int& ra;// 该条语句编译时会出错

	int& ra = a;
	int& rra = a;
	//printf("%p %p %p\n", &a, &ra, &rra);
	cout << &a << endl;
	cout << &ra << endl;
	cout << &rra << endl;
}
int main()
{
	TestRef();
	return 0;
}

 別名を付けることも可能です!

 

使用するシーン

パラメータを作成する

void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

値を返す

 

これは、 n が破棄され、 n のエイリアスが返されることと同じです

このような動作は非常に危険です (ワイルド ポインタに似ています)

int& Count()
{
	int n = 0;
	n++;
	
	// ...
	return n;
}

int main()
{
	int ret = Count();
	// 这里打印的结果可能是1,也可能是随机值
	cout << ret << endl;
	cout << ret << endl;
	return 0;
}

ここではnのエイリアスをretにコピーしていますが、スタックフレームをクリアしていなければ出力結果は1、クリアしていればランダムな値になります。

int& Count()
{
	int n = 0;
	n++;
	
	// ...
	return n;
}

int main()
{
	int& ret = Count();
	cout << ret << endl;
	cout << ret << endl;
	return 0;
}

 

 

次のコードの出力は何でしょうか? なぜ?

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}

int main()
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}

注: 関数が関数のスコープ外に戻った場合、返されたオブジェクトがまだそこにある (まだシステムに返されていない) 場合は、参照によって返すことができます。また、オブジェクトがシステムに返されている場合は、値によって返す必要があります。  

値渡しと参照渡しの効率の比較

 value をパラメータまたは戻り値の型として使用します。パラメータの受け渡しおよび戻りの際、関数は実際のパラメータを直接渡したり、変数自体を直接返したりはしませんが、実際のパラメータを渡すか、変数の一時コピーを返します。したがって、パラメータまたは戻り値の型として value を使用することは非常に非効率であり、特にパラメータまたは戻り値の型が非常に大きい場合、効率はさらに低くなります。

#include <time.h>
struct A 
{ 
	int a[10000]; 
};

void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数

	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();

	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();

	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

戻り値の型としての値と参照のパフォーマンスの比較

#include <time.h>
struct A 
{ 
	int a[10000]; 
};
A a;
// 值返回
A TestFunc1() 
{ 
	return a;
}

// 引用返回
A& TestFunc2() 
{ 
	return a; 
}

void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();

	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();

	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

上記のコードを比較すると、値とポインタをパラメータとして渡す効率と戻り値の型が大きく異なることがわかります。

struct SeqList
{     int a[10];     整数サイズ; };


 //C インターフェイス設計
 //i 番目の位置の値を読み取ります
int SLAT(struct SeqList* ps, int i)
{     assert(i < ps->size);     // ...     return ps->a[i]; }  //i 番目の位置の値を変更void SLModify(struct SeqList* ps, int i, int x) {     assert(i < ps->size);







    // ...
    ps->a[i] = x;
}

 //CPP インターフェース設計
 //i 番目の位置の値を読み取るか変更します
int& SLAT(struct SeqList& ps, int i)
{     assert(i < ps.size);     // ...     return (ps.a[i]); }



int main()
{     struct SeqList s;     s.size = 3;     // ...     SLAT(s, 0) = 10;     SLAT(s, 1) = 20;     SLAT(s, 2) = 30;     cout << SLAT(s, 0) << endl;     cout << SLAT(s, 1) << endl;     cout << SLAT(s, 2) << endl;








    0を返します。
}


参照とポインタの違い

文法上の概念では、参照は別名であり、独立した空間を持たず、参照先の実体と同じ空間を共有します。

int main()
{
 int a = 10;
 int& ra = a;
 
 cout<<"&a = "<<&a<<endl;
 cout<<"&ra = "<<&ra<<endl;
 return 0;
}

参照はポインターの形式で実装されるため、基礎となる実装には実際にはスペースが存在します。

int main()
{
 int a = 10;
 int& ra = a;
 ra = 20;
 
 int* pa = &a;
 *pa = 20;
 
 return 0;
}

参照とポインターのアセンブリ コードの比較を見てみましょう。

参照とポインタの違い:

  1.  参照は概念的に変数の別名を定義し、ポインターは変数のアドレスを格納します。
  2.  参照は定義時に初期化する必要があり、ポインタは必要ありません
  3.  初期化中に参照がエンティティを参照した後は、他のエンティティを参照することはできず、ポインタはいつでも同じタイプのエンティティを指すことができます。
  4.  NULL 参照はありませんが、NULL ポインターはあります
  5. sizeof では意味が異なります。参照結果は参照型のサイズですが、ポインタは常にアドレス空間が占有するバイト数です (32 ビット プラットフォームでは 4 バイト)。
  6. 参照の自己インクリメントは、参照されるエンティティが 1 ずつ増加することを意味し、ポインターの自己インクリメントは、ポインターが型のサイズを後方にオフセットすることを意味します。
  7. マルチレベル ポインタはありますが、マルチレベル参照はありません
  8. エンティティにアクセスするにはさまざまな方法があり、ポインタは明示的に逆参照する必要があり、リファレンス コンパイラがそれを単独で処理します。
  9. 参照はポインタよりも比較的安全に使用できます。

よく引用される

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量

    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量

    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同

    const int& rd = d;
}

さて、Xiao Yalan の今日の学習内容はこれで終わりです。彼女はまだ Xigaga の学習を続ける必要があります。

 

おすすめ

転載: blog.csdn.net/weixin_74957752/article/details/131870598