C++11: その他の機能

1 委任コンストラクターと継承されたコンストラクター

1.1 委任コンストラクター

委任されたコンストラクターを使用すると、あるコンストラクターが同じクラス内の別のコンストラクターを呼び出すことができるため、初期化中の変数の初期化が簡素化されます。

class class_c {
    
    
public:
	int max;
	int min;
	int middle;
	class_c(){
    
    )
	class_c(int my_max) {
    
    
		max = my_max > 0 ? my_max : 10;}
	class_c(int my_max, int my_min){
    
    
		max = my_max >0 ? my_max : 10 ;
		min = my_min >0 & & my_min < max ? my_min : l;}
	class_c(int my_max,int my_min,int my_middle) {
    
    
		max = my_max > 0 ? my_max : 10;
		min = my_min > 0 & &my_min < max ? my_min : 1;
		middle = my_middle < max && my_middle > min ? my_middle : 5;}
};

上記の例には実際的な意味はなく、多くのメンバー変数、複雑な初期化、および複数のコンストラクターの場合、各コンストラクターはメンバー変数に値を代入する必要があり、これは繰り返しで面倒であることを示すだけです。このプロセスはコンストラクターを委任することで簡素化できます。コードは次のとおりです。

class class_c {
    
    
public:
	int max;
	int min;
	int middle;
	class_c(int my_max){
    
    
		max = my_max > 0 ? my_max : 10;}
	class_c(int my_max, int my_min) : class_c(my_max){
    
    
		min = my_min > 0 && my_min < max ? my_min : l;}
	class_c(int my_max, int my_min,int my_middle) : class_c(my_max,my_min){
    
    
		middle = my_middle < max & & my_middle > min ? my_middle : 5;}
};

int main ()
{
    
    
	class_c c1{
    
     1,3,2 };
}

上記の例では、class_c(int, int, int) を構築するときに class_c(int, int) が最初に呼び出され、class_c(int, int) が class_c(int)、つまり class_c(int, int, int) を呼び出します。 ) は、メンバー変数の初期化を完了するために 2 つのコンストラクターを再帰的に委託しており、コードは簡潔でエレガントです。このチェーン呼び出しコンストラクターはリングを形成できないことに注意してください。リングを形成しないと、実行時に例外がスローされます。
デリゲート コンストラクターを使用する場合の注意点: デリゲート コンストラクターをクラス メンバーで初期化することはできません。例えば:

class class_a {
    
    
public:
	class_a() {
    
     }
	//member initialization here, no delegate
	class_a(string str) : m_string{
    
     str } {
    
    }
	//调用了委托构造函数,不能用类成员初始化了
	class_a(string str, double dbl) : class_a(str), m_double{
    
     dbl } {
    
    } //error
	//只能通过成员赋值来初始化
	class_a (string st, double dbl) : class_a(str) {
    
     m_double = dbl; )
	double m_double{
    
     1.0 };
	string m_string;
};

1.2 コンストラクターの継承

C++11 の継承コンストラクターを使用すると、派生クラスはコンストラクター自体を記述することなく、基本クラスのコンストラクターを直接使用できます。特に、基本クラスに多くのコンストラクターがある場合、派生クラスのコンストラクターの書き込みを大幅に簡素化できます。たとえば、次の構造体には 3 つのコンストラクターがあります。

struct Base
{
    
    
	int x;
	double y;
	string s;
	Base(int i) : x(i), y(0) {
    
     }
	Base(int i, double j) : x(i), y(j) {
    
    }
	Base(int i, double j, const string& str) : x(i), y(j), s(str) {
    
    }
};

派生クラスがある場合、派生クラスは基本クラスと同じ構築メソッドを採用することが期待されます。その場合、C++ 派生クラスは基本クラスの関数を隠すため、基本クラスから直接派生しても基本クラスのコンストラクターを取得できません。同じ名前で。例えば:

struct Derived : Base
{
    
    
};

int main()
{
    
    
	Derived d(1, 2.5, "ok");	//编译错误,没有合适的构造函数
}

上記の例では、派生クラスのデフォルト コンストラクターが基本クラスを隠蔽するため、基本クラスのコンストラクターを介して派生クラス オブジェクトを構築することは違法です。基本クラスのコンストラクターを使用したい場合の 1 つの方法は、派生クラスでこれらのコンストラクターを定義することです。

struct Derived : Base
{
    
    
	Derived(int i) : Base(i){
    
    }
	Derived(int i, double j) : Base(i,j){
    
    }
	Derived(int i, double j, const string& str) : Base(i, j, str){
    
    }
};

int main ()
{
    
    
	int i = 1;
	double j = 1.23;
	Derived d(i) ;
	Derived d1(i, j);
	Derived d2(i, j, "");
	return 0;
}

派生クラスの基本クラスと一致するコンストラクターを再定義することは可能ですが、煩雑で反復的です。C++11 の継承コンストラクター機能を使用して、派生クラスが同じ名前の関数を隠蔽する問題を解決します。基本クラスの同じ名前の関数の使用を示すには Base::SomeFunction を使用し、基本クラスのコンストラクターの使用を宣言するには Base::Base を使用します。これにより、基本クラスのコンストラクターを直接使用して構築できるようになります同じコンストラクター クラス オブジェクトを定義せずに派生したもの。継承されたコンストラクターは、派生クラスの新しく定義されたデータ メンバーを初期化しないことに注意してください。例えば:

struct Derived : Base
{
    
    
	using Base::Base;	//声明使用基类构造函数
};

int main()
{
    
    
	int i = 1;
	double j = 1.23;
	Derived d(i);	//直接使用基类构造函数来构造派生类对象
	Derived d1(i, j);
	Derived d2(i, j, "");
	return 0;
}

Base::Base を使用して基本クラス コンストラクターを使用すると、多くのコンストラクターを節約できることがわかります。この機能はコンストラクターだけでなく、同じ名前の他の関数にも適用されます。次に例を示します。

struct Base
{
    
    
	void Fun()
	{
    
    
		cout << "call in Base" << endl;
	}
};

struct Derived : Base
{
    
    
	void Fun(int a)
	{
    
    
		cout <<"call in Derived" << endl;
	}
);

int main ()
{
    
    
	Derived d;
	d.Fun();	//编译报错,找不到合适的函数
	return 0;
}

上記の例では、派生クラスは基本クラスの関数を使用したいと考えていますが、派生クラス内の同じ名前の関数が基本クラスの関数を隠しているため、コンパイラは適切な関数が見つからないことを示すメッセージを表示します。基底クラスと同じ名前の関数を使用したい場合は、名前の再導入を使用する必要があります。

2 つの生のリテラル

元のリテラルは、文字列の実際の意味を直接表すことができます。これは、一部の文字列には特殊文字が含まれているためです。たとえば、文字列をエスケープする場合、文字列を特別に処理する必要があることがよくあります。たとえば、ファイル パスを出力するには:

#include <iostream>
#include <string>
using namespace std;

int main()
{
    
    
	string str = "D:\AlB\test.text";
	cout<<str<<endl;
	string str1 = "D:\\A\\B\\test.text";
	cout<<str1<<endl;
	string str2 = R"(D:\A\B\test.text) ";
	cout<<str2<<endl;
	return 0 ;
}

出力結果は以下の通りです。
D:AB est.text
D:\A\B\test.text
D:\A\B\test.text
元の文字列リテラルを通じて元の文字列を直接取得できることがわかります。 R、HTML タグを出力する別の例を見てみましょう。

string s =
"<HTML>\
<HEAD>\
<TITLE>this is my title</TITLE>\
</HEAD>\
<BODY>\
<P>This is a paragraph</P>\
</BODY>\
</HTML>";

C++11 以前では、複数行のインデントされた HTML コードを取得するには、この方法で記述する必要がありましたが、この方法では面倒なだけでなく、本来の表現する意味が損なわれてしまいます。C++11 の元の文字列リテラルで表すと、次のようにシンプルで直感的になります。

#include <iostream>
#include <string>
using namespace std;

int main ()
{
    
    
	string s=
	R"(<HTML>
	<HEAD>
	<TITLE>This is a test</TITLE>
	</HEAD>
	<BODY>
	<P>Hello,C++ HTML world!</P>
	</BODY>
	</HTML>) ";
	cout <<s <<endl;
	return 0;
}

3 つの最終キーワードと上書きキーワード

C++11 では、final キーワードが追加され、クラスの継承や仮想関数の書き換えができないことを制限します。これは、Java の Final キーワードや C# の sealed キーワードと同様です。関数を変更する場合、final は仮想関数のみを変更できるため、仮想関数はクラスまたは関数の後ろに配置する必要があります。Final の使用法は次のとおりです。

struct A
{
    
    
	//A::foo is final,限定该虚函数不能被重写
	virtual void foo() final;
	//Error: non-virtual function cannot be final,只能修饰虚函数
	void bar() final;
};

struct B final : A	//struct B is final
{
    
    
	//Error: foo cannot be overridden as it's final in A
	void foo();
};

struct C : B	//Error: B is final
{
    
    
};

override キーワードを使用すると、派生クラスで宣言されたオーバーライドされた関数が、基本クラスの仮想関数と同じシグネチャを持つようになります。仮想関数は、オーバーロードされたものとして宣言されます。これにより、書き換えられた仮想関数の正当性が保証され、コードの可読性が向上する。override キーワードは、final キーワードと同じであり、メソッドの後に配置する必要があります。

struct A
{
    
    
	virtual void func(){
    
    )
} ;

struct D: A
{
    
    
	//显式重写
	void func() override
	{
    
    
	}
};

4 メモリの調整

4.1 メモリアライメントの概要

メモリ アラインメント、つまりバイト アラインメントは、データ型が格納できるメモリ アドレスのプロパティです。この属性は符号なし整数であり、この整数は 2 の N 乗 (1、2、4、8、...、1024、...) である必要があります。データ型のメモリ アラインメントが 8 であるということは、このデータ型によって定義されるすべての変数のメモリ アドレスが 8 の倍数であることを意味します。
基本データ型 (Fundamental Types) の整列属性がデータ型のサイズと等しい場合、この整列は自然整列 (Naturally Aligned) と呼ばれます。たとえば、4 バイトの int 型データの場合、そのバイト アライメントもデフォルトでは 4 になります。
なぜメモリの調整が必要なのでしょうか? すべてのハードウェア プラットフォームが任意の場所のメモリに自由にアクセスできるわけではないからです。Alpha、IA-64、MIPS、SuperH アーキテクチャなどの多くのプラットフォーム上の CPU では、読み取りデータが整列されていない場合 (奇数のメモリ アドレスにある 4 バイト int など)、アクセスが拒否されるか、ハードウェア例外がスローされます。CPU がメモリを処理する方法 (32 ビット x86 CPU、1 クロック サイクルで 4 つの連続したメモリ ユニット、つまり 4 バイトを読み取ることができる) を考慮すると、バイト アライメントを使用するとシステムのパフォーマンスが向上します (つまり、CPU はメモリ データを読み取ります)。効率)。たとえば、奇数のメモリ位置に int を配置し、これらの 4 バイトを読み取る場合、32 ビット CPU では 2 回の読み込みが必要になります。ただし、4バイトずつアライメントしてから一度読み込むことは可能です)。

4.2 ヒープメモリのメモリアライメント

メモリのアライメントについて議論するとき、ヒープ メモリを無視するのは簡単です。多くの場合、メモリの割り当てには malloc が使用されますが、このメモリのアライメントは無視されます。実際、malloc は通常、現在のプラットフォームのデフォルトの最大メモリ アライメント番号を使用してメモリをアライメントします。たとえば、MSVC は通常、32 ビットでは 8 バイト、64 ビットでは 16 バイトにアライメントします。従来のデータでは問題ありません。ただし、カスタム メモリ アライメントがこの範囲を超える場合、malloc を直接使用してメモリを取得することはできません。
特定のメモリ アライメントでメモリ ブロックを割り当てる必要がある場合、MSVC では _aligned_malloc を使用する必要があり、gcc では通常、memalign などの関数が使用されます。

4.3 alignas を使用してメモリ アライメント サイズを指定する

デフォルトのメモリ アライメントに従ってアライメントしたくない場合がありますが、このときは、alignas を使用してメモリ アライメントのサイズを指定できます。以下は alignas の基本的な使用法です。

alignas (32) long long a = 0;

#define xx 1
struct alignas(Xx) MyStruct_1 {
    
    };	//OK

template <size_t YY =1>
struct alignas(YY) MyStruct_2 {
    
    };	//OK

static const unsigned ZZ = 1;
struct alignas(ZZ) Mystruct_3 {
    
    };	//OK

MyStruct_3 のコンパイルは問題ないことに注意してください。C++11 では、コンパイル時の値 (静的 const を含む) である限り、alignas がサポートされます。
さらに、アライナはより大きくなるようにのみ変更でき、小さくすることはできないことに注意してください。アライメントを 1 に設定するなど、サイズを変更する必要がある場合でも、#pragma Pack を使用する必要があります。あるいは、#pragma _Pragma に相当する C++11 を使用することもできます (現在 Microsoft ではサポートされていません)。

_Pragma("pack(1)")
struct MyStruct
{
    
    
	char a;		//1 byte
	int b;		//4 bytes
	short c;	//2 bytes
	long long d;//8 bytes
	char e;		//1 byte
} ;
_Pragma("pack ()")

alignas は次のように使用することもできます。

alignas(int) char c;

この char は int の方法で整列されます。

4.4 alignof と std::alignment_of を使用してメモリ アライメント サイズを取得する

alignof はメモリ アライメント サイズを取得するために使用され、その使用法は比較的簡単です。以下は alignof の基本的な使用法です。

Mystruct xx;
std::cout << alignof(xx) << std::endl;
std::cout << alignof(MyStruct) << std::endl;

std::alignment_of の機能は、コンパイル時に型のメモリ アライメントを計算することです。std::alignment_of は、
alignof の機能を補完するために std に提供されています。alignof は size_t のみを返すことができ、alignment_of は std::integral.constant を継承するため、value_type、type、value などのメンバーがあります。
std::alignment_of および alignof を通じて構造体のメモリ アラインメントのサイズを取得できます。次に例を示します。

struct MyStruct
{
    
    
	char a;
	int b;
	double c;
} ;

int main ()
{
    
    
	int alignsize = std::alignment_of<Mystruct>::value;	//8
	int sz = alignof (Mystruct) ;						//8
	return 0;
}

std::alignment_of は align によって実装できます。

template< class T>
struct alignment_of : std::integral_constant<std::size_t, alignof(T)> {
    
    };

sizeof と同様に、alignof は可変長型に適用できます。たとえば、alignof(Args) は、変数パラメーターのメモリ アライメント サイズを取得するために使用されます。

4.5 メモリアライン型 std::aligned_storage

std::aligned_storage はメモリアライメントされたバッファとみなすことができ、通常は配置 new と組み合わせて使用​​されます。その基本的な使用法は次のとおりです。

#include <iostream>
#include <type_traits>

struct A
{
    
    
	int avg;
	A(int a, int b): avg ((a+b)/2) {
    
     }
};

typedef std::aligned_storage<sizeof(A), alignof(A)>::type Aligned_A;

int main()
{
    
    
	Aligned_A a, b;
	new (&a) A (10,20);
	b = a;
	std::cout << reinterpret_cast<A&>(b).avg << std::endl;
	return 0;
}

なぜ std:aligned_storage を使用するのでしょうか? new char[32] などの単純なメモリ ブロックを割り当ててから、配置 new を使用してこのメ​​モリ上にオブジェクトを構築する必要があることがよく知られています。

1 char xx[32];
2 ::new (xx)MyStruct;

ただし、char[32] は 1 バイト アライメントであり、xx は MyStruct で指定されたアライメントにない可能性があります。現時点では、placement new を呼び出してメモリ ブロックを構築すると効率の問題やエラーが発生する可能性があるため、現時点では std::aligned_storage を使用してメモリ ブロックを構築する必要があります。

1 std::aligned_storage<sizeof (Mystruct), alignof (MyStruct) >::type xx;
2 ::new ( &xx) MyStruct;

現在のコンパイラでは、デフォルトの最大アライメントを超えた後にメモリ アライメントが正しいことを new が保証できないため、ヒープ メモリを使用する場合には aligned_malloc も必要になる場合があることに注意してください。たとえば、MSVC 2013 では、次のコードは 32 ビットに従ってアライメントされていない可能性があるというコンパイラ警告を受け取ります。

struct alignas (32) MyStruct
{
    
    
	char a;		//1 byte
	int b;		//4 bytes
	short c;	//2 bytes
	long long d;//8 bytes
	char e ;	//1 byte
};

void* p = new MyStruct;
//warning V4316:‘Mystruct' : object allocated on the heap may not be aligned 32

4.6 std::max_align_t および std::align 演算子

std::max_align_t は、現在のプラットフォームの最大のデフォルトのメモリ アライメント タイプを返すために使用されます。malloc によって返されるメモリの場合、そのアライメントは max_align_t タイプのアライメント サイズと一致している必要があります。現在のプラットフォームの最大のデフォルト メモリ アライメントは、次の方法で取得できます。

 std::cout << alignof(std::max_align_t) << std::endl;

std::align は、大きなメモリ ブロック内で指定されたメモリ要件を満たすアドレスを取得するために使用されます。次の例を考えてみましょう。

char buffer[] = "---------";
void * pt = buffer;
std::size_t space = sizeof(buffer)-1;
std::align(alignof(int), sizeof(char), pt, space);

この例は、バッファの大きなメモリ ブロックでメモリ アライメントを alignof(int) として指定し、sizeof(char) のサイズのメモリ部分を見つけて、このメモリ部分を見つけた後にアドレスを pt に入れることを意味します。バッファを pt から移動します。開始長がスペースに入れられます。
C++11 には、メモリ アライメントを簡単に操作できる便利なツールが多数提供されていますが、ヒープ メモリに関しては、独自の方法を見つける必要があると思われます。ただし、通常のアプリケーションでは、システムのデフォルトのアライメントよりも大きいメモリ アライメントを手動で指定することはほとんどないため、毎回新規/削除を心配する必要はありません。

C++11 の 5 つの新しい便利なアルゴリズム

C++11 では、コード記述をより簡潔かつ便利にする便利なアルゴリズムがいくつか追加されました。ここでは、一般的に使用される新しいアルゴリズムをいくつか紹介します。

5.1 すべて、いずれか、およびなし

アルゴリズム ライブラリには、all_of、any_of、none_of という 3 つの新しい判定アルゴリズムが追加されました。
all_of は、区間 [first, last) 内のすべての要素が単項判定式 p を満たすかどうかを確認し、すべての要素が条件を満たしている場合は true を返し、そうでない場合は false を返します。 。
any_of は、区間 [first, last) 内の少なくとも 1 つの要素が単項判定式 p を満たすかどうかを確認し、1 つの要素が条件を満たす場合は true を返し、それ以外の場合は true を返します。
none_of は、区間 [first, last) 内のすべての要素が単項判定式 p を満たしていないかどうかを確認し、すべての要素が条件を満たさない場合は true を返し、そうでない場合は false を返します。
以下に、all_of、any_of、none_of の例を示します。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    
    
	vector<int> v= {
    
    1,3,5,7,9 };
	auto isEven = [](int i) {
    
     return i % 2 != 0 ;}
	bool isallOdd = std::all_of(v.begin(), v.end(), isEven);
	if(isallodd)
		cout <<"all is odd" << endl;
	bool isNoneEven = std::none_of(v.begin(), v.end(), isEven);
	if(isNoneEven)
		cout << "none is even" << endl;
	vector<int> v1={
    
     1,3,5,7,8,9 };
	bool anyof = std::any_of(v1.begin(), vl.end(), isEven);
	if(anyof)
		cout << "at least one is even" <<endl;
}

出力は次のようになります。
すべて奇数です。
奇数はありません。
少なくとも 1 つは偶数です。

5.2 見つからない場合

アルゴリズムライブラリの検索アルゴリズムに新しいfind_if_notが追加されました find_ifとは逆の意味、つまり条件を満たさない要素を見つけるという意味です find_ifはfind_if_notの機能も実装できますが判定を変えるだけですしかし、find_if_not が追加されたことで、否定的な判定式を書く必要がなくなり、可読性が向上しました。以下にその基本的な使い方を示します。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    
    
	vector<int> v= {
    
     1,3,5,7,9,4 };
	auto isEven = [](int i){
    
     return i % 2== 0; };
	auto firstEven = std::find_if(v.begin(), v.end(), isEven);
	if(firstEven != v.end())
		cout << "the first even is " << *firstEven << endl;
		
	//用find_if来查找奇数则需要重新写一个否定含义的判断式
	auto isNotEven = [](int i){
    
     return i % 2 !=0;};
	auto firstodd = std::find_if(v.begin(), v.end(), isNotEven);
	if(firstodd !=v.end())
		cout << "the first odd is " << *firstodd << endl;
		
	//用find_if_not来查找奇数则无须新定义判断式
	auto odd = std::find_if_not(v.begin(), v.end(), isEven);
	if(odd != v.end())
		cout << "the first odd is " << *odd << endl;
}

出力結果は以下の通りです。
最初の偶数は 4
最初の奇数は 1
最初の奇数は 1
find_if_not を使用すると、新たに否定的な意味の判定式を定義する必要がなくなり、便利であることがわかります。

5.3 コピーイフ

アルゴリズム ライブラリには、元のコピー アルゴリズムよりも使いやすい copy_if アルゴリズムも追加されています。以下はcopy_ifの基本的な使い方です。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main ()
{
    
    
	vector<int> v = {
    
    1,3,5,7,9,4 };
	std::vector<int> v1(v.size());
	//根据条件复制
	auto it = std::copy_if(v.begin(), v.end(), v1.begin(), [](int i){
    
     return i%2!=0; });
	//缩减vector到合适大小
	v1.resize(std::distance(v1.begin(), it));
	for(int i:v1)
		cout<<i<<"";
	cout<<endl;
}

5.4イオタ

アルゴリズム ライブラリに、順序付けされたシーケンスを簡単に生成するために使用される iota アルゴリズムが追加されました。たとえば、固定長の配列が必要で、この配列の要素はすべて特定の値に基づいてインクリメントされますが、この配列は iota を使用して簡単に生成できます。以下はiotaの基本的な使い方です。

#include <numeric>
#include <array>
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    
    
	vector<int> v(4);	//循环遍历赋值来初始化数组
	std::iota(v.begin(), v.end(), 1);
	for(auto n: v)
		cout << n << ' ';
	cout << endl;
	std::array<int, 4>array;
	std::iota(array.begin(), array.end(), 1);
	for(auto n:array)
		cout << n << ' ';
	std::cout << endl;
}

出力結果は次のとおりです。
1 2 3 4
1 2 3 4
iota を使用して配列を初期化する方が、割り当てを走査するよりも簡潔であることがわかります。iota 初期化シーケンスではサイズを指定する必要があることに注意してください。上記のコードのベクトル v(4); で初期化サイズ 4 が指定されていない場合、出力は空になります。

5.5 minmax_element

アルゴリズム ライブラリには、最大値と最小値を同時に取得するための新しいアルゴリズム minmax_element も追加されているため、最大値と最小値を取得したいときに、max_element アルゴリズムと min_element アルゴリズムを個別に呼び出す必要はありません。 minmax_element は最小
値を設定し、最大値の反復子をペアにして返します。minmax_elemen の基本的な使用方法は次のとおりです。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    
    
	vector<int> v = {
    
     1, 2, 5, 7, 9, 4 };
	auto result = minmax_element(v.begin(), v.end() );
	cout<<*result.first<<" "<<*result.second<<endl;
	return 0;
}

出力は次のようになります:
1 9

5.6 is_sorted と is_sorted_until

アルゴリズム ライブラリには、is_sorted および is_sorted_until アルゴリズムが追加されました。is_sort はシーケンスがソートされているかどうかを決定するために使用され、is_sort_until はシーケンス内で以前にソートされた部分シーケンスを返すために使用されます。以下は、is_sorted および is_sorted_until アルゴリズムの基本的な使用法です。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    
    
	vector<int> v = {
    
     1,2,5,7,9,4 };
	auto pos = is_sorted_until(v.begin(), v.end());
	for(auto it = v.begin(); it !=pos; ++it)
		cout<<*it<<" ";
	cout<<endl;
	bool is_sort = is_sorted(v.begin(), v.end());
		cout<<is_sort<<endl;
	return 0;
}

出力は次のようになります:
1 2 5 7 9
0

おすすめ

転載: blog.csdn.net/taifyang/article/details/129107974