异常处理、动态内存申请在不同编译器之间的表现…

2020-03-23 05:35:01来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

异常处理、动态内存申请在不同编译器之间的表现差异

不同编译器在异常处理时的表现差异(《 terminate() 函数、set_terminate() 函数 》,《 unexpected() 函数、set_unexpeced() 函数 》,《 throw 关键字 》); 不同编译器在动态内存申请时的表现差异(《 new_handler() 函数、set_newhandler() 函数 》,《 重载 new、delete 操作符 》,《 重载 new[]、delete[] 操作符 》,《throw 关键字》,《 nothrow 关键字》,《new 关键字的新用法 --- 在指定位置上创建对象 》);

续上节内容 c++中的异常处理 ...

目录

  1、在main() 函数中抛出异常会发生什么

  2、在析构函数中抛出异常会发生什么

  3、函数的异常规格说明

    4、动态内存申请结果的分析

    5、关于 new 关键字的新用法

1、在main() 函数中抛出异常会发生什么

  由上节中的 异常抛出(throw exception)的逻辑分析  可知,异常抛出后,会顺着函数调用栈向上传播,在这期间,若异常被捕获,则程序正常运行;若异常在 main() 函数中依然没有被捕获,也就是说在 main() 函数中抛出异常会发生什么呢?(程序崩溃,但因编译器的不同,结果也会略有差异)  

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 class Test
 7 {
 8 public:
 9     Test()
10     {
11         cout << "Test()" << endl;
12     }
13     
14     ~Test()
15     {
16         cout << "~Test()" << endl;
17     }
18 };
19 
20 int main()
21 {
22     cout << "main() begin..." << endl;
23 
24     static Test t;
25     
26     throw 1;  
27               
28     cout << "main() end..." << endl;
29           
30     return 0;
31 }
在 main() 函数中抛出异常

  将上述代码在不同的编译器上运行,结果也会不同;

  在 g++下运行,结果如下:

main() begin...

Test()

terminate called after throwing an instance of 'int'

Aborted (core dumped)

  在 vs2013下运行,结果如下:

  main() begin...

  Test()

  弹出异常调试对话框

   从运行结果来看,在 main() 中抛出异常后会调用一个全局的 terminate() 结束函数,在 terminal() 函数中不同编译器处理的方式有所不同。

  c++ 支持自定义结束函数,通过调用 set_terminate() 函数来设置自定义的结束函数,此时系统默认的 terminal() 函数就会失效

 (1)自定义结束函数的特点:与默认的 terminal() 结束函数 原型一样,无参无返回值;

     关于使用 自定义结束函数的注意事项:

    1)不能在该函数中再次抛出异常,这是最后一次处理异常的机会了;

    2)必须以某种方式结束当前程序,如 exit(1)、abort();

    exit():结束当前的程序,并且可以确保所有的全局对象和静态局部对象全部都正常析构;

    abort():异常终止一个程序,并且异常终止的时候不会调用任何对象的析构函数;

 (2)set_terminate() 函数的特点:1)参数类型为函数指针 void(*)();2)返回值为自定义的 terminate() 函数入口地址;

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 class Test
 7 {
 8 public:
 9     Test()
10     {
11         cout << "Test()" << endl;
12     }
13     
14     ~Test()
15     {
16         cout << "~Test()" << endl;
17     }
18 };
19 
20 void mterminate()
21 {
22     cout << "void mterminate()" << endl;
23     abort();   // 异常终止一个程序,不会析构任何对象
24     //exit(1); // 结束当前程序,但会析构所有的全局和静态局部对象
25 }
26 
27 int main()
28 {
29     terminate_handler f = set_terminate(mterminate);
30     
31     cout << "terminate() 函数的入口地址 = " << f << "::" << mterminate << endl;
32     
33     cout << "main() begin..." << endl;
34  
35     static Test t;  
36     
37     throw 1;  
38               
39     cout << "main() end..." << endl;
40           
41     return 0;
42 }
43 /**
44  * 以 exit(1) 结束程序时的运行结果:
45  * terminate() 函数的入口地址 = 1::1
46  * main() begin...
47  * Test()
48  * void mterminate()
49  * ~Test()
50  */
51 
52 /**
53  * 以 abort() 结束程序时的运行结果:
54  * terminate() 函数的入口地址 = 1::1,《为什么全局函数的地址都是 1 ?》
55  * main() begin...
56  * Test()
57  * void mterminate()
58  * Aborted (core dumped)
59  */
自定义结束函数测试案例

2、在析构函数中抛出异常会发生什么

  一般而言,在析构函数中销毁所使用的资源,若在资源销毁的过程中抛出异常,那么会导致所使用的资源无法完全销毁;若对这一解释深入挖掘,那么会发生什么呢?

  试想程序在 main() 函数中抛出了异常,然而该异常并没有被捕获,那么该异常就会触发系统默认的结束函数 terminal();因为不同编译器对 terminal() 函数的内部实现有所差异,

 (1)若 terminal() 函数是以 exit(1) 这种方式结束程序的话,那么就会有可能调用到析构函数,而此时的析构函数中又抛出了一个异常,就会导致二次调用 terminal() 函数,后果不堪设想(类似堆空间的二次释放),但是,强大的 windows、Linux系统会帮我们解决这个问题,不过在一些嵌入式的操作系统中可能就会产生问题。

 (2)若 terminal() 函数是以 abort() 这种方式结束程序的话,就不会发生(1)中的情况,这就是 g++ 编译器为什么会这么做的原因了。

  注:terminal() 结束函数是最后处理异常的一个函数,所以该函数中不可以再次抛出异常,而(1)中就是违反了这条规则;

    若在 terminal() 结束函数中抛出异常,就会导致二次调用 terminal() 结束函数。

  结论:在析构函数中抛出异常时,若 terminate() 函数中以 exit() 这种方式结束程序的话会很危险,有可能二次调用 terminate() 函数,甚至死循环。

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 class Test
 7 {
 8 public:
 9     Test()
10     {
11         cout << "Test()" << endl;
12     }
13     
14     ~Test()
15     {
16         cout << "~Test()" << endl;
17 
18         throw 1// 代码分析:会二次调用 mterminate()
19     }
20 };
21 
22 void mterminate()
23 {
24     cout << "void mterminate()" << endl;
25     exit(1); // 结束当前程序,但会析构所有的全局和静态局部对象
26 }
27 
28 int main()
29 {
30     set_terminate(mterminate);
31 
32     cout << "main() begin..." << endl;
33 
34     static Test t;  
35     
36     throw 1;  
37               
38     cout << "main() end..." << endl;
39           
40     return 0;
41 }
在析构函数中抛出异常案例测试

   将上述代码在不同的编译器上运行,结果也会不同;

  在 g++下运行,结果如下:

main() begin...

Test()

void mterminate()      // 在 main() 函数中第一次抛出异常,调用 自定义结束函数 mterminate()

~Test()          // exit(1) 程序结束时,调用了析构函数,在析构函数中再次抛出了异常,会调用 abort() 函数

Aborted (core dumped)    // 注:一些旧版本的编译器可能会调用 自定义结束函数 mterminate(),此行显示 void mterminate()

  在 vs2013下运行,结果如下:

main() begin...

Test()

void mterminate()

~Test()          // exit(1) 程序结束时,调用了析构函数,在析构函数中再次抛出了异常,会 弹出异常调试对话框

弹出异常调试对话框         // 注:一些旧版本的编译器可能会调用 自定义结束函数 mterminate(),此行显示 void mterminate()

  结论:新版本的编译器对 析构函数中抛出异常这种行为 做了优化,直接让程序异常终止。

3、函数的异常规格说明

  如何判断某个函数是否会抛出异常,或许有很多办法,如查看函数的实现(可惜第三方库不提供函数实现)、查看技术文档(可能查看的文档与当前所使用的函数版本不一致),但刚才列举的这些方法都会存在缺陷。其实有一种更为简单高效的方法,就是直接通过异常声明来判断这个函数是否会抛出异常,简称为函数的异常规格说明

  异常声明作为函数声明的修饰符,写在参数列表的后面;  

1 /* 可能抛出任何异常 */
2 void func1();
3 
4 /* 只能抛出的异常类型:char 和 int */
5 void func2() throw(char, int);
6 
7 /* 不抛出任何异常 */
8 void func3() throw();

  异常规格说明的意义:

     (1)提示函数调用者必须做好异常处理的准备;(如果想知道调用的函数会抛出哪些类型的异常时,只用打开头文件看看这个函数是怎么声明的就可以了;)

  (2)提示函数的维护者不要抛出其它异常;

       (3)异常规格说明是函数接口的一部分;(用于说明这个函数如何正确的使用;)

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void func() throw(int)
 6 {
 7     cout << "func()" << endl;
 8     
 9     throw 'c';
10 }
11 
12 int main()
13 {
14     try 
15     {
16         func();
17     } 
18     catch(int) 
19     {
20         cout << "catch(int)" << endl;
21     } 
22     catch(char) 
23     {
24         cout << "catch(char)" << endl;
25     }
26 
27     return 0;
28 }
异常规格之外的异常测试案例

将上述代码在不同的编译器上运行,结果也会不同;

  在 g++下运行,结果如下:

func()

terminate called after throwing an instance of 'char'

Aborted (core dumped)

  在 vs2013下运行,结果如下:

func()

catch(char)  // 竟然捕获了该异常,说明不受异常规格说明限制

  通过对上述代码结果的再次研究,我们发现在 g++中,当异常不在函数异常规格说明中,就会调用一个 全局函数 unexpected(),在该函数中再调用默认的全局结束函数 terminate();

  但在 vs2013中,异常并不会受限于函数异常规格说明的限制。

  结论:g++ 编译器遵循了c++规范,然而 vs2013 编译器并不受限于这个约束。

  提示:不同编译器对函数异常规格说明的处理方式有所不同,所以在进行项目开发时,有必要测试当前所使用的编译器。

  c++ 中支持自定义异常函数;通过调用 set_unexpected() 函数来设置自定义异常函数,此时系统默认的 全局函数 unexpected() 就会失效

    (1)自定义异常函数的特点:与默认的 全局函数 unexpected() 原型一样,无参无返回值;

    (2)关于使用 自定义异常函数 的注意事项:

    可以在函数中抛出异常(当异常符合触发函数的异常规格说明时,恢复程序执行;否则,调用全局 terminate() 函数结束程序);

    (3)set_unexpected() 函数的特点:1)参数类型为函数指针 void(*)();2)返回值为自定义的 unexpected() 函数入口地址;

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 void m_unexpected()
 7 {
 8     cout << "void m_unexpected()" << endl;
 9   
10     throw 1;  // 2 这个异常符合异常规格说明,所以可以被捕获
11     // terminate(); // 若这么写,与上个程序的运行结果相同
12 }
13 
14 void func() throw(int)
15 {
16     cout << "func()" << endl;
17     
18     throw 'c';  // 1 由于不符合异常规格说明,此时会调用 m_unexpected() 函数
19 }
20 
21 int main()
22 {
23     set_unexpected(m_unexpected);
24     
25     try 
26     {
27         func();
28     } 
29     catch(int) 
30     {
31         cout << "catch(int)" << endl;
32     } 
33     catch(char) 
34     {
35         cout << "catch(char)" << endl;
36     }
37 
38     return 0;
39 }
自定义 unexpected() 函数的测试案例

  将上述代码在不同的编译器上运行,结果也会不同;

  在 g++下运行,结果如下:

func()

void m_unexpected()

catch(int)  // 由于自定义异常函数 m_unexpected() 中抛出的异常 throw 1 符合函数异常规格说明,所以该异常被捕获

    在 vs2013下运行,结果如下:

func()

catch(char)  // vs2013 没有遵循c++规范,不受异常规格说明的限制,直接捕获函数异常规格说明中 throw ‘c’这个异常

  结论:(g++)unexpected() 函数是正确处理异常的最后机会,如果没有抓住,terminate() 函数会被调用,当前程序以异常告终;

     (vs2013)没有函数异常规格说明的限制,所有的函数都可以抛出任意异常。

 4、动态内存申请结果的分析

  在 c 语言中,使用 malloc 函数进行动态内存申请时,若成功,则返回对应的内存首地址;若失败,则返回 NULL 值。

  在 c++规范中,通过重载 new、new[] 操作符去动态申请足够大的内存空间时,

  (1)若成功,则在获取的空间中调用构造函数创建对象,并返回对象地址;

  (2)若失败(内存空间不足),根据编译器的不同,结果也会不同;

    1)返回 NULL 值;(早期编译器的行为,不属于 c++ 规范)

    2)抛出 std::bad_alloc 异常;(后期的编译器会抛出异常,一些早期的编译器依然返回 NULL 值)

    注:不同编译器  对如何抛出异常  也是不确定的,c++ 规范是在 new_handler() 函数中抛出 std::bad_alloc 异常,而 new_handler() 函数是在内存申请失败时自动调用的。

   当内存空间不足时,会调用全局的 new_hander() 函数,调用该函数的意义就是让我们有机会整理出足够的内存空间;所以,我们可以自定义 new_hander() 函数,并通过全局函数 set_new_hander() 去设置自定义 new_hander() 函数。(通过实验证明, 有些编译器没有定义全局的 new_hander() 函数,比如 vs2013、g++ ,见案例1 )

  特别注意:set_new_hander() 的返回值是默认的全局 new_hander() 函数的入口地址。

        而 set_terminate() 函数的返回值是自定义 terminate() 函数的入口地址;

        set_unexpected() 函数的返回值是自定义 unexpected() 函数的入口地址。

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void my_new_handler()
 6 {
 7     cout << "void my_new_handler()" << endl;
 8 }
 9 
10 int main(int argc, char *argv[])
11 {
12     // 若编译器有全局 new_handler() 函数,则 func != NULL,否则,func == NULL;
13     new_handler func = set_new_handler(my_new_handler);
14 
15     try
16     {
17         cout << "func = " << func << endl;  
18         
19         if( func )
20         {
21             func();
22         }
23     }
24     catch(const bad_alloc&)
25     {
26         cout << "catch(const bad_alloc&)" << endl;
27     }
28     
29     return 0;
30 }
案例1:证明 编译器是否定义了全局 new_handler() 函数

  将上述代码在不同的编译器上运行,结果也会不同;

  在 vs2013 和 g++下运行,结果如下:

func = 0   // => vs2013 and g++ 中没有定义 全局 new_handler() 函数 

   在 BCC下运行,结果如下:

func = 00401468

catch(const bad_alloc&)  // 在 BCC 中定义了全局 new_handler() 函数,并在该函数中抛出了 std::bad_alloc 异常

 

 1 #include <iostream>
 2 #include <new>
 3 #include <cstdlib>
 4 #include <exception>
 5 
 6 using namespace std;
 7 
 8 class Test
 9 {
10     int m_value;
11 public:
12     Test()
13     {
14         cout << "Test()" << endl;
15         
16         m_value = 0;
17     }
18     
19     ~Test()
20     {
21         cout << "~Test()" << endl;  
22     }
23     
24     void* operator new (size_t size)
25     {
26         cout << "operator new: " << size << endl;
27         
28         return NULL;
29     }
30     
31     void operator delete (void* p)
32     {
33         cout << "operator delete: " << p << endl;
34         
35         free(p);
36     }
37     
38     void* operator new[] (size_t size)
39     {
40         cout << "operator new[]: " << size << endl;
41         
42         return NULL;
43     }
44     
45     void operator delete[] (void* p)
46     {
47         cout << "operator delete[]: " << p << endl;
48         
49         free(p);
50     }
51 };
52 
53 int main(int argc, char *argv[])
54 {
55     Test* pt = new Test();  
56     
57     cout << "pt = " << pt << endl;
58     
59     delete pt;
60     
61     pt = new Test[5];
62     
63     cout << "pt = " << pt << endl;
64     
65     delete[] pt; 
66     
67     return 0;
68 }
案例2:不同编译器在内存申请失败时的表现

  将上述代码在不同的编译器上运行,结果也会不同;

  在 g++下运行,结果如下: 

operator new: 4

Test()  // 由于堆空间申请失败,返回 NULL 值,接着又在这片失败的空间上创建对象,当执行到 m_value = 0;时(相当于在 非法地址上赋值),编译器报 段错误

Segmentation fault (core dumped)

   在 vs2013下运行,结果如下:

operator new: 4

pt = 00000000

operator new[]: 24

pt = 00000000

  在 BCC下运行,结果如下: 

operator new: 4

pt = 00000000

operator new[]: 24

pt = 00000000

operator delete[]: 00000000

   总结:在 g++ 编译器中,内存空间申请失败,也会继续调用构造函数创建对象,这样会产生 段错误;在 vs2013、BCC 编译器中,内存空间申请失败,直接返回NULL。

   为了让不同编译器在内存申请时的行为统一,所以必须要重载 new、delete 或者 new[]、delete[] 操作符,当内存申请失败时,直接返回 NULL 值,而不是抛出 std::bad_alloc 异常,这就必须通过 throw() 修饰 内存申请函数。

 1 #include <iostream>
 2 #include <new>
 3 #include <cstdlib>
 4 #include <exception>
 5 
 6 using namespace std;
 7 
 8 class Test
 9 {
10     int m_value;
11 public:
12     Test()
13     {
14         cout << "Test()" << endl;
15         
16         m_value = 0;
17     }
18     
19     ~Test()
20     {
21         cout << "~Test()" << endl;  
22     }
23     
24     void* operator new (size_t size) throw()
25     {
26         cout << "operator new: " << size << endl;
27         
28         return NULL;
29     }
30     
31     void operator delete (void* p)
32     {
33         cout << "operator delete: " << p << endl;
34         
35         free(p);
36     }
37     
38     void* operator new[] (size_t size) throw()
39     {
40         cout << "operator new[]: " << size << endl;
41         
42         return NULL;
43     }
44     
45     void operator delete[] (void* p)
46     {
47         cout << "operator delete[]: " << p << endl;
48         
49         free(p);
50     }
51 };
52 
53 int main(int argc, char *argv[])
54 {
55     Test* pt = new Test();
56     
57     cout << "pt = " << pt << endl;
58     
59     delete pt;
60     
61     pt = new Test[5];
62     
63     cout << "pt = " << pt << endl;
64     
65     delete[] pt; 
66     
67     return 0;
68 }
案例3:(优化)不同编译器在内存申请失败时的表现

  通过测试,g++、vs2013、BCC 3款编译器的运行结果一样,输出结果如下:

operator new: 4

pt = 00000000

operator new[]: 24

pt = 00000000

5、关于 new 关键字的新用法

(1)nothrow 关键字

 1 #include <iostream>
 2 #include <exception>
 3 
 4 using namespace std;
 5 
 6 void func1()
 7 {
 8     try
 9     {
10         int* p = new(nothrow) int[-1]; 
11         
12         cout << p << endl;
13         
14         delete[] p; 
15     }
16     catch(const bad_alloc&)
17     {
18         cout << "catch(const bad_alloc&)" << endl;
19     }    
20     
21     cout << "--------------------" << endl;
22     
23     try
24     {
25         int* p = new int[-1];
26         
27         cout << p << endl;
28         
29         delete[] p; 
30     }
31     catch(const bad_alloc&)
32     {
33         cout << "catch(const bad_alloc&)" << endl;
34     }    
35 }
36 
37 int main(int argc, char *argv[])
38 {
39     func1();
40     
41     return 0;
42 }
nothrow 关键字的使用

  将上述代码在不同的编译器上运行,结果也会不同;

  在 g++、BCC下运行,结果如下:

0                // 使用了 nothrow 关键字,在动态内存申请失败时,直接返回 NULL
--------------------
catch(const bad_alloc&)  // 没有 nothrow 关键字,动态内存申请失败时,抛出 std::bad_alloc 异常

    在 vs2013下编译失败:

原因是 内存申请太大,即数组的总大小不得超过 0x7fffffff 字节;

  结论:nothrow 关键字的作用:无论动态内存申请结果是什么,都不要抛出异常,然而不同编译器之间也会有差异。

(2)通过 new 在指定的地址上创建对象

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void func2()
 6 {
 7     int bb[2] = {0};
 8     
 9     struct ST
10     {
11         int x;
12         int y;
13     };
14     
15     // 通过 new 在指定的地址上创制对象
16     // 将动态内存ST 创建到栈空间上(int bb[2] = {0}),但要保证二者的内存模型相同,此处是 8 bytes
17     ST* pt = new(bb) ST();  
18     
19     pt->x = 1;
20     pt->y = 2;
21     
22     cout << bb[0] << "::" << bb[1] << endl;
23     
24     bb[0] = 3;
25     bb[1] = 4;
26     
27     cout << pt->x << "::" << pt->y << endl;
28     
29     pt->~ST();  // 由于指定了创建对象的空间,必选显示的调用析构函数
30 }
31 
32 int main(int argc, char *argv[])
33 {   
34     func2();
35     
36     return 0;
37 }
通过 new 在指定的地址上创制对象

  在 g++、vs2013、BCC下运行,结果如下:

1::2

3::4

 动态内存申请的结论:

    (1)不同的编译器在动态内存分配上的实现细节不同;

    (2)编译器可能重定义 new 的实现,并在实现中抛出 bad_alloc 异常;(vs2013、g++)

    (3)编译器的默认实现中,可能没有设置全局的 new_handler() 函数;(vs2013、g++)

    (4)对于移植性要求高的代码,需要考虑 new 的具体细节;

  我们可以进一步验证上述结论,就以 vs2013 举例,在编译器的安装包找到 new.cpp、new2.cpp 这两个文件(文 件路径:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\crt\src),分析其源码发现,在内存申请失败时,会调用 _callnewh(cb) 函数,该函数可以通过如下方式查看:https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/reference/callnewh?view=vs-2015;

  

   

  所以在 vs中,当动态内存申请失败时,会抛出 std::bad_alloc异常,而不会返回 NULL 值;

 1 #ifdef _SYSCRT
 2 #include <cruntime.h>
 3 #include <crtdbg.h>
 4 #include <malloc.h>
 5 #include <new.h>
 6 #include <stdlib.h>
 7 #include <winheap.h>
 8 #include <rtcsup.h>
 9 #include <internal.h>
10 
11 // 两个版本的 new 实现方式,失败时都会抛出 bad_alloc 异常
12 void * operator new( size_t cb )
13 {
14     void *res;
15 
16     for (;;) {
17 
18         //  allocate memory block
19         res = _heap_alloc(cb);
20 
21         //  if successful allocation, return pointer to memory
22 
23         if (res)
24             break;
25 
26         //  call installed new handler
27         if (!_callnewh(cb))  // 申请失败,则抛出 bad_alloc 异常
28             break;
29 
30         //  new handler was successful -- try to allocate again
31     }
32 
33     RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0));
34 
35     return res;
36 }
37 #else  /* _SYSCRT */
38 
39 #include <cstdlib>
40 #include <new>
41 
42 _C_LIB_DECL
43 int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);
44 _END_C_LIB_DECL
45 
46 void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
47         {       // try to allocate size bytes
48         void *p;
49         while ((p = malloc(size)) == 0)
50                 if (_callnewh(size) == 0)
51                 {       // report no memory
52                         _THROW_NCEE(_XSTD bad_alloc, );
53                 }
54 
55         return (p);
56         }
源码分析 new.cpp
 1 #include <cruntime.h>
 2 #include <malloc.h>
 3 #include <new.h>
 4 #include <stdlib.h>
 5 #include <winheap.h>
 6 #include <rtcsup.h>
 7 
 8 void *__CRTDECL operator new(size_t) /*_THROW1(std::bad_alloc)*/;
 9 
10 void * operator new[]( size_t cb )
11 {
12     void *res = operator new(cb);
13 
14     RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0));
15 
16     return res;
17 }
源码分析 new2.cpp

  


原文链接:https://www.cnblogs.com/nbk-zyc/p/12536982.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:2019.3.14解题报告&amp;补题报告

下一篇:小游戏二之---------------五子棋