自学内容网 自学内容网

C++17新特性(二)模板特性

1. 类模板参数推导

在C++17之前,你必须明确指出类模板的所有参数,例如:

complex<double> c{5.1,3.3};

mutex mx;
lock_guard<mutex> lg(mx);

C++17起必须指明类模板参数的限制被放宽了。通过类模板参数推导,只要编译器能够根据初始值推导模板参数,就可以不指名参数。

例如:

complex c{5.1,3.3};

mutex mx;
lock_guard lg(mx);

vector v1{1,2,3};
1.1 使用类模板参数推导

只要能根据初始值推导出所有模板参数就可以使用类模板参数推导。

complex c1{1.1,2.2};
complex c2(1.1,2.2);
complex c3 = 3.3;
complex c4 = {4.4};

但如果推导过程中有歧义,是不能通过编译的。

complex c5{5,3.3};

对于可变参数模板也可以使用类模板参数推导,例如tuple

tuple t{42,'x',nullptr}; // 推导为int,char,nullptr_t

也可以推导非类型模板参数。例如:

template<typename T, int SZ>
class MyClass
{
public:
MyClass (T(&) [SZ])
{}
};

MyClass mc("hello"); // T为const char, SZ为6
1.1.1 默认以拷贝方式推导

类模板参数推导过程中首先尝试以拷贝的方式初始化。

vector v1{42};

vector v2{v1}; // 会推导为vector<int>而不是vector<vector<int>>

但如果传递的是两个vector<int>来初始化,那么会被推导为vector<vector<int>>

vector vv{v1,v2}; // 会推导为vector<vector<int>>
1.1.2 推导lambda的类型

通过使用类模板参数推导,我们可以用lambda的类型作为模板参数来实例化类模板。例如:

// 对于一个回调函数进行包装,并统计调用次数
template<typename CB>
class CountCalls
{
private:
CB callback; // 回调函数
long calls = 0; // 调用次数
public:
CountCalls(CB cb) : callback(cb) {}

template<typename...Args>
decltype(auto) operator()(Args&&... args)
{
++calls;
return callback(forward<Args>(args)...);
}

long count() const
{
return calls;
}
};

int main()
{
CountCalls sc{ [](auto x,auto y) { return x > y; } };
vector v{ 1,7,2,5,6,9,3 };
sort(v.begin(), v.end(), ref(sc)); // 必须引用传递,否则只会修改拷贝的计数器
cout << sc.count() << "calls" << endl;
    
    // for_each会返回传入的回调函数
auto fo = for_each(v.begin(), v.end(), CountCalls{ [](auto i) {
cout << "elem: " << i << " ";
} });
cout << endl;
cout << "output with " << fo.count() << "calls" << << endl;
}

输出结果如下:

39 calls
elem: 19
elem: 17
elem: 13
elem: 11
elem: 9
elem: 7
elem: 5
elem: 3
elem: 2
output with 9 calls
1.1.3 没有类模板部分参数推导

注意,不像函数模板,类模板不能只指明一部分模板参数,希望编译器去推导剩余的部分参数。

template<typename T1, typename T2, typename T3 = T2>
class C
{
public:
C(T1 x = {}, T2 y = {}, T3 z = {})
{}
};

int main()
{
C c1(22, 44.3, "hi"); // OK: int,double,const char*
C c2(22, 44.3); // OK: int,double,double
C c3("hi", "guy"); // OK: T1=T2=T3=const char*

C<string> c4("hi", "my"); // ERROR
C<> c5(22, 44.3); // ERROR
C<> c6(22, 44.3, 42);  // ERROR
}

为什么不支持部分参数推导,这里有一个导致这个决定的例子:

tuple<int> t(42,43); // ERROR

std::tuple是一个可变参数模板,因此你可以指明任意数量的模板参数。在这个例子中,并不能判断出只指明一个参数是一个错误还是故意的。

1.1.4 使用类模板参数推导代替快捷函数

快捷函数就是通过传入的参数实例化相应的类模板,一个明显的例子make_pair(),能够帮我们避免指明传入参数的类型,但现在已经不需要这种了,例如:

vector<int> v;
auto p = make_pair(v.begin(),v.end());

pair p(v.begin(),v.end());
1.2 推导指引

你可以定义特定的推导指引来给类模板参数添加新的推导或者修正构造函数定义的推导。例如:

在没有定义特定的推导指引之前,Pair p1{"hi","world"}会将T1推导为char[3],T2推导为char[6]

template<typename T1,typename T2>
struct Pair
{
T1 first;
T2 second;
Pair(const T1& x, const T2& y) : first{ x }, second{ y }
{}
};

// 为构造函数定义推导指引
template<typename T1,typename T2>
Pair(T1, T2) -> Pair<T1, T2>;

->的左侧我们声明了我们想要推导声明。这里我们声明的是使用两个以值传递且类型分别为T1T2的对象创建一个Pair对象。在->的右侧,定义了推导的结果。也就是说,推导的类型会根据传入的值本身的类型做决定,不会被const &所影响。上面的例子就会推导T1T2const char*

1.2.1 使用推导指引强制类型退化

重载推导规则的一个非常重要的用途就是确保模板参数T在推导时发生退化

template<typename T>
struct C
{
C(const T&) {}
};

如果我们传递了一个字面量"hello",传递的类型是const char(&)[6],因此,T就会被推到为char[6]

但只要进行简单的定义推导指引:

template<typename T>
C(T) -> C<T>;
1.2.2 非模板推导指引

推导指引不一定用于模板,也不一定用于构造函数。

template<typename T>
struct S
{
T val;
};

S(const char*) -> S(string);

但推导出来的结果是const char*,那么就会被推到为string类型。

1.2.3 推导指引VS构造函数

推导指引会和类的构造函数产生竞争。类模板参数推导时会根据重载情况选择最佳匹配的构造函数/推导指引。如果一个构造函数和推导指引匹配的优先级一样,那么就会优先匹配推导指引。

1.2.4 显示推导指引

推导指引可以用explicit声明。当出现explicit不允许的初始化或转换时这一条推导指引就会被忽略。例如:

template<typename T>
struct S
{
T val;
};

explicit S(const char*) -> S<string>;

如果使用拷贝初始化,将会忽略这一条推导指引。

S s1 = {"hello"}; // 无效

S s2{"hello"}; // OK
S s3 = S{"hello"}; // OK
S s4 = {S{"hello"}}; // OK

另一个例子如下:

template<typename T>
struct Ptr
{
Ptr(T) { cout << "Ptr(T)\n"; }
template<typename U>
Ptr(U) { cout <<"Ptr(U)\n"; }
};

template<typename T>
explicit Ptr(T) -> Ptr<T*>

Ptr p1{42}; // Ptr<int*>
Ptr p2 = 42; // Ptr<int>
int i = 42;
Ptr p2{&i}; // Ptr<int**>
Ptr p4 = &i; // Ptr<int*>
1.2.5 聚合体的推导指引

泛型聚合体中也可以使用推导指引来支持类模板参数推导。例如,对于:

template<typename T>
struct A
{
T val;
};

在没有推导指引的情况下使用类模板推导会导致错误:

A i1{42}; // ERROR
A s1{"hello"}; // ERROR
1.2.6 标准推导指引

C++17在标准库引入了很多推导指引。

pairtuple的推导指引:

// pair
template<typename T1,typename T2>
pair(T1,T2) -> pair<T1,T2>;

// tuple
template<typename... Types>
tuple(Types...) -> tuple<Types...>;

迭代器推导指引:

// vector
template<typename Iterator>
vector(Iterator,Iterator) -> vector<typename iterator_traits<Iterator>::value_type>;

array推导指引:

template<typename T,typename... U> 
array(T,U...) -> array<enable_if_t<(is_same_v<T,U> && ...), T>,(1+sizeof...(U))>; // is_same_v确保所有参数类型一致

map推导指引:

// 定义
namespace std
{
template<typename Key,typename T,typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key,T>>>
class map {
// ...
};
}

// 推导指引
template<typename Key, typename T, typename Compare = less<Key>,
typename Allocator = allocator<pair<const Key, T>>>
map(initializer_list<pair<Key, T>>, Compare = Compare(), Allocator = Allocator()) ->
map<Key, T, Compare, Allocator>;

智能指针不存在推导指引:

shared_ptr sp{new int(7)}; // ERROR

上边的写法是错误的。

2. 编译期的if语句

通过语法if constexpr可以计算编译期的条件表达式来在编译期决定使用if语句的哪一个部分。其余部分的代码将不会被生成,但是还是会做语法检查。

template<typename T>
string asString(T x)
{
if constexpr (is_same_v<T, string>)
return x;
else if constexpr (is_arithmetic_v<T>)
return to_string(x);
else
return string(x);
}

int main()
{
cout << asString(42) << '\n';
cout << asString(string("hello")) << '\n';
cout << asString("hello") << '\n';
}
2.1 编译期if语句的动机
template<typename T>
string asString(T x)
{
if (is_same_v<T, string>)
return x;
else if (is_arithmetic_v<T>)
return to_string(x);
else
return string(x);
}

这段代码不能通过编译,因为模板在实例化时整个模板会作为一个整体进行编译。然而if语句的条件表达式都是运行时特性。即使在编译期就能确定条件表达式的值一定是false,then的部分也必须能够通过编译。因此,当传递一个string的时候,会因为to_string无效而导致编译失败。

2.2 使用编译期if语句
2.2.1 编译期if的注意事项

编译期if可能会影响函数的返回值类型。例如:

auto foo()
{
if constexpr (sizeof(int) < 4)
return 42;
else
return 42u;
}

如果使用运行期的if可能将永远不能通过编译,因为推导返回值类型会考虑到所有可能的返回值类型。

有的时候,如果在if语句就返回了,可以跳过else语句部分,也就是说:

auto foo()
{
    if()
    {
        return a;
    }
    else 
    {
        return b;
    }
}

auto foo()
{
    if()
    {
        return a;
    }
    return b;
}

这两种方式是等价的,但是,第二种写法如果用在编译期的if可能会导致错误。

auto foo()
{
if constexpr(sizeof(int) > 4)
return 42;
return 42u;
}

编译器会推导两个不同的返回值类型,会导致错误。

在考虑如下代码:

template<typename T>
constexpr auto foo(const T& val)
{
if constexpr (is_integral_v<T>)
{
if constexpr (T{} < 10)
return val * 2;
}
return val;
}

int main()
{
constexpr auto x1 = foo(42);
constexpr auto x2 = foo("hi");
}

上面的代码没有问题,但是如果将两个if语句的条件表达式写在一起,并使用&&来判断,可能会导致短路现象。

template<typename T>
constexpr auto foo(const T& val)
{
if constexpr (is_integral_v<T> && T{} < 10)
{
return val * 2;
}
return val;
}

int main()
{
constexpr auto x1 = foo(42);
constexpr auto x2 = foo("hi"); // ERROR

}

然而,编译期的if的条件表达式总是作为整体实例化并且整体有效,这就意味着如果传入的是一个不能<10的运算将不能通过编译。

因此,要写成嵌套if constexpr而不是&&连接。

2.2.2 其他编译期if的示例

编译期if的一个应用就是先对返回值进行一些处理,再进行完美转发。因为decltype(auto)不能推导为void,因此:

#include <functional>
#include <type_traits>

template<typename Callable,typename... Args>
decltype(auto) call(Callable op, Args&&... args)
{
if constexpr (is_void_t<invoke_result_t<Callable, Args...>>)
{
// 返回值类型是void
op(forward<Args>(args)...);
return;
}
else
{
// 返回值不是void
decltype(auto) ret{ op(forward<Args>(args)...) };
return ret;
}
}

编译期if的一个典型应用是类型分发。在C++17之前,你必须为每一个想处理的类型重载一个单独的函数。现在,有了编译期if,你可以把所有的逻辑放在一个函数里。

例如,重载版本的advance()算法:

template<typename Iterator,typename Distance>
void advance(Iterator& pos, Distance n)
{
using cat = iterator_traits<Iterator>::iterator_category;
advanceImpl(pos, n, cat{});
}

template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, random_access_iterator_tag)
{
pos += n;
}

template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, bidirectional_iterator_tag)
{
if (n >= 0)
{
while (n--)
++pos;
}
else
{
while (n++)
--pos;
}
}

template<typename Iterator, typename Distance>
void advanceImpl(Iterator& pos, Distance n, input_iterator_tag)
{
while (n--)
++pos;
}

我们可以将所有的实现都放在同一个函数中:

template<typename Iterator, typename Distance>
void advance(Iterator& pos, Distance n)
{
using cat = iterator_traits<Iterator>::iterator_category;
if constexpr (is_convertible_v<cat, random_access_iterator_tag>)
{
pos += n;
}
else if constexpr (is_convertible_v<cat, bidirectional_iterator_tag>)
{
if (n >= 0)
{
while (n--)
++pos;
}
else
{
while (n++)
--pos;
}
}
else
{
while (n--)
++pos;
}
}
2.3 带初始化的编译期if语句

注意编译期if语句也可以使用新的带初始化的方式。例如,有一个constexpr函数foo(),你可以这样写:

template<typename T>
void bar(const T x)
{
if constexpr (auto obj = foo(x); is_same_v<decltype(obj), T>)
{
cout << "foo(x) yields same type\n";
}
else
{
cout << "foo(x) yields different type\n";
}
}

如果有一个参数类型也为T的constexpr函数foo(),可以根据返回的值来判定,可以这么写:

constexpr auto c = 1;
constexpr foo(constexpr auto c);
if constexpr(constexpr auto obj = foo(c); obj == 0)
{
cout << "foo() == 0\n";
}
2.4 在模板之外使用编译期if

if constexpr可以在任何函数中使用,并非仅限于模板。只要条件表达式是编译期的,并且可以转换为bool类型。

3. 折叠表达式

C++17起,有一个新的特性可以计算对参数包中所有参数应用一个二元运算符的结果。

例如,下面的函数将会返回所有参数的总和:

template<typename... T>
auto foldSum(T... args)
{
return (... + args); // ((arg1) + (arg2) + arg3)
}

返回语句的括号是折叠表达式的一部分,不可省略。

注意折叠表达式参数的位置很重要,如果反着写:

(... + args) // (arg1 + (arg2 + arg3))
3.1 折叠表达式的动机

折叠表达式的出现让我们不必递归实例化模板的方式来处理参数包。在C++17之前:

template<typename T>
auto foldSumRec(T arg)
{
return arg;
}

template<typename T1,typename... Ts>
auto foldSumRec(T1 arg1, Ts... otherArgs)
{
return arg1 + foldSumRec(otherArgs...);
}

这样的实现不仅麻烦,而且编译器也很难处理。

3.2 使用折叠表达式

给定一个参数args和一个操作符op,C++17语法规范:

  • 一元左折叠

    ( ... op args ) => ( ( arg1 op arg2 ) op arg3 ) op ...
    
  • 一元右折叠

    ( args op ... ) => arg1 op ( arg2 op ... ( argN-1 op argN ) )
    
3.2.1 处理空参数包

当使用折叠表达式处理空参数包时,将遵循如下规则:

  • 如果使用了&&运算符,值为true
  • 如果使用了||运算符,值为false
  • 如果使用了逗号运算符,值为void()
  • 使用所有其他的运算符,格式错误。

对于其他情况,我们可以添加一个初始值value,一个参数包args,一个操作符op,C++17语法规范:

  • 二元左折叠

    ( value op ... op args ) => ( ( ( value op arg1) op arg2 ) op arg3 ) op ...
    
  • 二元右折叠

    ( args op ... op value ) => arg1 op ( arg2 op ... ( argN op value ) )
    

例如,下面的定义在进行加法时允许传入一个空参数包:

template<typename... T>
auto foldSum(T... s)
{
return (0 + ... + s); // sizeof...(s) == 0 也能执行
}

有的时候第一个操作数是特殊的,比如:

template<typename... T>
void print(const T&... args)
{
(cout << ... << args) << '\n';
}

这里,传递给print()的第一个参数输出之后将返回输出流,所以后面的参数可以进行输出。但是,其他的实现可能会发生一些意料之外的结果,例如:

cout << (args << ... << '\n');

类似像print(1)是可以调用,但会打印出1左移'\n'位的结果,'\n'的ASCII码是10,结果为1024。

注意这个例子,两个参数之间没有输出空格字符。可能会导致输出结果观看效果极差,我们可以使用一个辅助函数来完成。

template<typename T>
const T& spaceBefore(const T& arg)
{
cout << ' ';
return arg;
}

template<typename First,typename... Args>
void print(const First& firstArg, const Args&... args)
{
cout << firstArg; // 当只有一个参数的时候,不需要打印空格
(cout << ... << spaceBefore(args)) << '\n';
}

这里的折叠表达式会展开成:

cout << spaceBefore(arg1) << spaceBefore(arg2) << ... << '\n'; 

我们也可以使用lambda表达式来在print()定义spaceBefore()

template<typename First,typename... Args>
void print(const First& firstArg, const Args&... args)
{
cout << firstArg; // 当只有一个参数的时候,不需要打印空格
auto outWithSpace = [](const auto& arg)
{
cout << ' ' << arg;
};
(..., outWithSpace(args));
}
3.2.2 支持的运算符

你可以对除了.->[]之外的所有二元运算符使用折叠表达式。

折叠函数调用

// 对每个参数调用foo()函数
template<typename T>
void foo(const T& t);

template<typename... Types>
void callFoo(const Types&... args)
{
(..., foo(args)); // foo(arg1), foo(arg2), ...
}

另外,也可以支持移动语义:

template<typename... Types>
void callFoo(const Types&&... args)
{
(..., foo(forward<Types>(args))); // foo(arg1), foo(arg2), ...
}

如果foo()函数的返回类型重载了,运算符,那么代码行为可能会改变。不过可以将返回值转换为void()

template<typename... Types>
void callFoo(const Types&&... args)
{
(..., (void)foo(forward<Types>(args))); // foo(arg1), foo(arg2), ...
}

组合哈希函数

template<typename T>
void hashCombine(size_t& seed, const T& val)
{
seed ^= hash<T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

template<typename... Types>
size_t combinedHashValue(const Types&... args)
{
size_t seed = 0; // 初始化seed
(..., hashCombine(seed, args));
return seed;
}

有了这些定义,我们可以轻易的定义出一个新的哈希函数,并将这个函数用于某一个类型。

struct CustomerHash
{
size_t operator()(const Customer& c) const
{
return combinedHashValue(c.getFirstname(), c.getLastname(), c.getValue());
}
};

unordered_set<Customer, CustomerHash> coll;
unordered_map<Customer,string, CustomerHash> map;

折叠基类的函数调用

template<typename... Bases>
class MultiBase : private Bases...
{
public:
void print()
{
// 调用基类所有的print()函数
(..., Bases::print());
}
};

struct A
{
void print()
{
cout << "A::print()\n";
}
};

struct B
{
void print()
{
cout << "B::print()\n";
}
};

struct C
{
void print()
{
cout << "C::print()\n";
}
};

int main()
{
MultiBase<A, B, C> mb;
mb.print();
}

折叠路径遍历

使用折叠表达式遍历一个二叉树的路径。

struct Node
{
int value;
Node* Left{ nullptr };
Node* Right{ nullptr };
Node(int i = 0) : value{ i }
{}

int getValue() const
{
return value;
}

static constexpr auto left = &Node::Left;
static constexpr auto right = &Node::Right;

template<typename T,typename... Tp>
static Node* traverse(T np, Tp... paths)
{
return (np->* ...->*paths); // np->*paths1->*paths2
}
};

int main()
{
Node* root = new Node{ 0 };
root->Left = new Node{ 1 };
root->Left->Right = new Node{ 2 };

Node* node = Node::traverse(root, Node::left, Node::right);
cout << node->getValue() << '\n';
node = root->*Node::left->*Node::right;
cout << node->getValue() << '\n';
}
3.2.3 使用折叠表达式处理类型

通过使用类型特征,我们也可以使用折叠表达式来处理模板参数包。例如:

// 检查所有类型是否相同
template<typename T1,typename... Tn>
struct IsHomogeneous
{
static constexpr bool value = (is_same_v<T1, Tn> && ...); // is_same_v<T1,T2> && is_same_v<T1,T3> && ...
};

4. 处理字符串字面量模板参数

C++一直在放宽对模板参数的标准。C++17也是如此。

4.1 在模板中使用字符串

非类型模板参数只能是常量整数值、对象/函数/成员的指针、对象或函数的左值引用、nullptr_t类型。

对于指针,C++17之前需要外部或者内部链接。C++17之后,可以使用无链接的指针。

template<const char* str>
class Message
{};

extern const char hello[] = "Hello World!"; // 外部链接
const char hello1[] = "Hello World!"; // 内部链接

void foo()
{
Message<hello> msg; // OK
Message<hello1> msg1; // C++11起OK
static const char hello2[] = "Hello World"; // 无链接
Message<hello2> msg2; // C++17起OK
Message<"hi"> msg3; // ERROR
}

5. 占位符类型作为模板参数

C++17起,可以使用占位符类型autodecltype(auto)作为非类型模板参数的类型。

5.1 使用auto模板参数

C++17起,可以使用auto来声明非类型模板参数:

template<auto N>
class S
{};

但是,在实例化的时候,不能实例化一些不允许作为非类型模板参数的参数:

S<2.5> s; // ERROR
5.1.1 字符和字符串模板参数

这个特性的一个应用就是你可以定义一个即可能是字符也可能是字符串的模板参数。例如:

template<auto Sep = ' ',typename First,typename... Args>
void print(const First& first, const Args&... args)
{
cout << first;
auto outWithSep = [](const auto& arg)
{
cout << Sep << arg;
};
(..., outWithSep(args));
cout << endl;
}

我们可以自定义每个输出结果直接的间隔,可以是默认的空格,也可以是其他字符或者字符串。

5.1.2 定义元编程常量

auto模板参数特性的另一个应用是可以让我们更轻易的定义编译期常量。

template<typename T,T v>
struct constant
{
static constexpr T value = v;
};

int main()
{
using i = constant<int, 42>;
using c = constant<char, 'x'>;
using b = constant<bool,true>;
}

可以简单实现为:

template<auto v>
struct constant
{
static constexpr auto value = v;
};

int main()
{
using i = constant<42>;
using c = constant<'x'>;
using b = constant<true>;
}
5.2 使用auto作为变量模板的参数

可以使用auto来实现变量模板。

例如:

template<typename T, auto N>
array<T, N> arr{};

int main()
{
    arr<int,5>[0] = 17;
    arr<int,5>[3] = 42;
    arr<int,5u>[1] = 11;
    arr<int,5u>[3] = 3;
}

arr<int,5>arr<int,5u>是不同的变量。

5.3 使用decltype(auto)模板参数。

decltype(auto)是C++14引入的,现在也可以作为模板参数。如果用这个来推导表达式而不是变量名,那么推导的结果将依赖于表达式的值类型:

  • 纯右值推导type
  • 左值推导type&
  • 将亡值推导type&&

例如:

template<decltype(auto) N>
struct S
{
void printN()  const
{
cout << "N:" << N << endl;
}
};

static const int c = 42;
static int v = 42;

int main()
{
S<c> s1; // N=const int
S<(c)> s2; // N=const int&
s1.printN();
s2.printN();

S<(v)> s3; // int&
v = 77;
s3.printN();
}

6. 扩展的using声明

using声明支持逗号分割的名称列表,也可以用于参数包。

class Base
{
public:
void a();
void b();
void c();
};

class Derived : private Base
{
public:
using Base::a,Base::b,Base::c;
};
6.1 使用变长的using声明

创建一个重载的lambda的集合。通过如下定义:

// 继承所有基类的函数调用运算符
template<typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
};

// 基类的类型从传入的参数中推导
template<typename... Ts>
overload(Ts...)->overload <Ts...>;

int main()
{
auto twice = overload{
[](string& s) { s += s; },
[](auto& v) { v *= 2; }
};
    
    int i = 42;
    twice(i); // 84
    string s = "hi";
    twice(s); // hihi
}
6.2 使用变长using声明构造函数

除了出个声明继承构造函数之外,现在还支持如下方式:可以声明一个可变参数类模板Multi,继承每一个参数类型的基类:

template<typename T>
class Base
{
T value{};
public:
Base() {}
Base(T v) : value{ v } {}
};

template<typename... Types>
class Multi : private Base<Types>...
{
public:
using Base<Types>::Base...;
};

原文地址:https://blog.csdn.net/CHAKMING1/article/details/135708924

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!