【问题标题】:Try catch statements in C尝试 C 中的 catch 语句
【发布时间】:2012-05-22 02:17:03
【问题描述】:

我今天在想其他语言中存在的 try/catch 块。谷歌了一段时间,但没有结果。据我所知,C 中没有 try/catch 这样的东西。但是,有没有办法“模拟”它们?
当然,有断言和其他技巧,但没有像 try/catch 这样的技巧,它们也可以捕获引发的异常。谢谢

【问题讨论】:

  • 如果没有在堆栈展开时自动释放资源的机制,类似异常的机制通常不会有用。 C++ 使用 RAII; Java、C#、Python 等使用垃圾收集器。 (请注意,垃圾收集器只释放内存。为了自动释放其他类型的资源,它们还添加了诸如终结器或上下文管理器之类的东西......)
  • @jamesdlin,为什么我们不能用 C 做 RAII?
  • @Pacerier RAII 要求在对象被销毁(即析构函数)时自动调用函数。您如何建议在 C 中这样做?

标签: c


【解决方案1】:

C 本身不支持异常,但您可以通过 setjmplongjmp 调用在一定程度上模拟它们。

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened here\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjmp(s_jumpBuffer, 42);
}

这个网站有一个很好的教程,介绍如何使用setjmplongjmp 模拟异常

【讨论】:

  • 很棒的解决方案!这个解决方案是交叉的吗?它在 MSVC2012 上对我有用,但在 MacOSX Clang 编译器中没有。
  • 提示我:我认为 try catch 子句允许您捕获异常(例如除以零)。这个函数似乎只允许你捕获你自己抛出的异常。调用 longjmp 不会引发真正的异常,对吗?如果我使用此代码执行try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; 之类的操作,它就无法正常工作?
  • 除零甚至不是 C++ 中的异常,要处理它,您需要检查除数不为零并处理它,或者处理运行除零公式时抛出的 SIGFPE .
  • 我来自 Java 世界,在那里您可以捕获一般异常。我和@Sam 有同样的担忧。 Null 指针取消引用呢?还是调用内部失败的函数?
  • @Heroman C完全没有例外。 C++ 有例外,但通常不鼓励使用它们;与 Java 和 .NET 不同,没有语言标准的异常基类型(即you can throw anything,而不仅仅是std:exception 的子类)。在 C++ 中取消引用 nullptr does not throw anything 时,它是 UB,这意味着您需要查看您的编译器或操作系统是否有一种优雅地处理访问冲突的方法(大多数都有,但它不在 ISO 标准 C++ 中)。
【解决方案2】:

您在 C 中使用 goto 来处理类似的错误处理情况。
这是您可以在 C 中获得的最接近异常的等价物。

【讨论】:

  • @JensGustedt 这正是 goto 当前经常使用的示例,也是有意义的示例(setjmp/ljmp 是更好的选择,但 label+goto 通常使用更多)。
  • @AoeAoe,可能goto更多用于错误处理,但那又怎样?问题不在于错误处理本身,而是明确地关于 try/catch 等价物。 goto 不是 try/catch 的等价物,因为它仅限于相同的功能。
  • @JensGustedt 我对 goto 和使用它的人的仇恨/恐惧有点反应(我的老师也告诉我在大学里使用 goto 的可怕故事)。 [OT] 关于 goto 唯一真正、真正有风险和“多云”的事情是“倒退”,但我在 Linux VFS 中看到了这一点(git blame guy 发誓它对性能至关重要 - 有益)。跨度>
  • 请参阅systemctl sources,了解goto 的合法用途,作为现代、广泛接受、同行评审来源中使用的 try/catch 机制。在 goto 中搜索“throw”等效项,在 finish 中搜索“catch”等效项。
  • 这个答案似乎是最好的和可移植的,看看 php/bcmath 如何使用它:github.com/php/php-src/blob/php-8.0.7/ext/bcmath/bcmath.c#L455
【解决方案3】:

好的,我忍不住回复了这个。首先让我说,我认为在 C 中模拟它不是一个好主意,因为它对于 C 来说确实是一个陌生的概念。

我们可以使用滥用预处理器和本地堆栈变量来使用有限版本的C++ try/throw/catch。

版本 1(本地范围抛出)

#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

版本 1 仅是本地抛出(不能离开函数的范围)。它确实依赖于 C99 在代码中声明变量的能力(如果 try 是函数中的第一件事,它应该在 C89 中工作)。

这个函数只是创建一个本地变量,所以它知道是否有错误并使用 goto 跳转到 catch 块。

例如:

#include <stdio.h>
#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

int main(void)
{
    try
    {
        printf("One\n");
        throw();
        printf("Two\n");
    }
    catch(...)
    {
        printf("Error\n");
    }
    return 0;
}

结果如下:

int main(void)
{
    bool HadError=false;
    {
        printf("One\n");
        HadError=true;
        goto ExitJmp;
        printf("Two\n");
    }
ExitJmp:
    if(HadError)
    {
        printf("Error\n");
    }
    return 0;
}

第 2 版(范围跳跃)

#include <stdbool.h>
#include <setjmp.h>

jmp_buf *g__ActiveBuf;

#define try jmp_buf __LocalJmpBuff;jmp_buf *__OldActiveBuf=g__ActiveBuf;bool __WasThrown=false;g__ActiveBuf=&__LocalJmpBuff;if(setjmp(__LocalJmpBuff)){__WasThrown=true;}else
#define catch(x) g__ActiveBuf=__OldActiveBuf;if(__WasThrown)
#define throw(x) longjmp(*g__ActiveBuf,1);

第 2 版要复杂得多,但基本上以相同的方式工作。它使用一个 长跳出当前函数到try块。然后尝试块 使用 if/else 将代码块跳过到检查本地的 catch 块 变量,看看它是否应该捕获。

示例再次展开:

jmp_buf *g_ActiveBuf;

int main(void)
{
    jmp_buf LocalJmpBuff;
    jmp_buf *OldActiveBuf=g_ActiveBuf;
    bool WasThrown=false;
    g_ActiveBuf=&LocalJmpBuff;

    if(setjmp(LocalJmpBuff))
    {
        WasThrown=true;
    }
    else
    {
        printf("One\n");
        longjmp(*g_ActiveBuf,1);
        printf("Two\n");
    }
    g_ActiveBuf=OldActiveBuf;
    if(WasThrown)
    {
        printf("Error\n");
    }
    return 0;
}

这使用全局指针,因此 longjmp() 知道上次运行的尝试。 我们正在使用滥用堆栈,因此子函数也可以有一个 try/catch 块。

使用此代码有许多缺点(但这是一种有趣的心理锻炼):

  • 它不会释放分配的内存,因为没有调用解构函数。
  • 在一个范围内不能有超过 1 个 try/catch(无嵌套)
  • 您实际上不能像在 C++ 中那样抛出异常或其他数据
  • 根本不是线程安全的
  • 您正在为其他程序员设置失败,因为他们可能不会注意到黑客攻击并尝试像 C++ try/catch 块一样使用它们。

【讨论】:

  • 不错的替代解决方案。
  • 版本 1 是个好主意,但 __HadError 变量需要重置或限定范围。否则,您将无法在同一块中使用多个 try-catch。也许使用像bool __ErrorCheck(bool &amp;e){bool _e = e;e=false;return _e;} 这样的全局函数。但是局部变量也会被重新定义,所以事情会有点失控。
  • 是的,在同一个函数中只能使用一个try-catch。然而,一个更大的问题是变量是标签,因为您不能在同一个函数中有重复的标签。
  • 实际上我有同样的问题,我不能使用 t2o 或更多,只能在一个范围内尝试 catch,你有解决方案吗?
  • @Parhamsagharichiha 没有简单的解决方法。我建议您直接使用 goto 而不是尝试使用 try..catch。
【解决方案4】:

在 C99 中,您可以使用 setjmp/longjmp 进行非本地控制流。

在单个范围内,在存在多个资源分配和多个出口的情况下,C 的通用结构化编码模式使用goto,如in this example。这类似于 C++ 如何在底层实现自动对象的析构函数调用,如果您坚持这一点,即使在复杂的函数中也应该允许您在一定程度上保持清洁。

【讨论】:

    【解决方案5】:

    虽然其他一些答案涵盖了使用 setjmplongjmp 的简单案例,但在实际应用中,有两个问题非常重要。

    1. try/catch 块的嵌套。为您的 jmp_buf 使用单个全局变量将使这些无效。
    2. 线程。给你一个全局变量jmp_buf会在这种情况下造成各种痛苦。

    解决方案是维护jmp_buf 的线程本地堆栈,该堆栈会随着您的使用而更新。 (我认为这是 lua 内部使用的)。

    所以不是这个(来自 JaredPar 的真棒答案)

    static jmp_buf s_jumpBuffer;
    
    void Example() { 
      if (setjmp(s_jumpBuffer)) {
        // The longjmp was executed and returned control here
        printf("Exception happened\n");
      } else {
        // Normal code execution starts here
        Test();
      }
    }
    
    void Test() {
      // Rough equivalent of `throw`
      longjump(s_jumpBuffer, 42);
    }
    

    你会使用类似的东西:

    #define MAX_EXCEPTION_DEPTH 10;
    struct exception_state {
      jmp_buf s_jumpBuffer[MAX_EXCEPTION_DEPTH];
      int current_depth;
    };
    
    int try_point(struct exception_state * state) {
      if(current_depth==MAX_EXCEPTION_DEPTH) {
         abort();
      }
      int ok = setjmp(state->jumpBuffer[state->current_depth]);
      if(ok) {
        state->current_depth++;
      } else {
        //We've had an exception update the stack.
        state->current_depth--;
      }
      return ok;
    }
    
    void throw_exception(struct exception_state * state) {
      longjump(state->current_depth-1,1);
    }
    
    void catch_point(struct exception_state * state) {
        state->current_depth--;
    }
    
    void end_try_point(struct exception_state * state) {
        state->current_depth--;
    }
    
    __thread struct exception_state g_exception_state; 
    
    void Example() { 
      if (try_point(&g_exception_state)) {
        catch_point(&g_exception_state);
        printf("Exception happened\n");
      } else {
        // Normal code execution starts here
        Test();
        end_try_point(&g_exception_state);
      }
    }
    
    void Test() {
      // Rough equivalent of `throw`
      throw_exception(g_exception_state);
    }
    

    同样,一个更现实的版本将包括一些将错误信息存储到exception_state 的方法,更好地处理MAX_EXCEPTION_DEPTH(可能使用 realloc 来增加缓冲区,或类似的东西)。

    免责声明:上面的代码是在没有任何测试的情况下编写的。这纯粹是为了让您了解如何构建事物。不同的系统和不同的编译器需要以不同的方式实现线程本地存储。该代码可能同时包含编译错误和逻辑错误 - 因此,尽管您可以随意使用它,但请在使用前对其进行测试;)

    【讨论】:

      【解决方案6】:

      一个快速的谷歌搜索产生 kludgey 解决方案,如 this 使用 setjmp/longjmp 正如其他人提到的那样。没有什么比 C++/Java 的 try/catch 更直接和优雅了。我自己比较偏爱 Ada 的异常处理。

      使用 if 语句检查所有内容 :)

      【讨论】:

        【解决方案7】:

        这可以使用 C 中的 setjmp/longjmp 来完成。P99 有一个非常舒适的工具集,它也与 C11 的新线程模型一致。

        【讨论】:

          【解决方案8】:

          这是在 C 中进行错误处理的另一种方法,它比使用 setjmp/longjmp 更高效。不幸的是,它不适用于 MSVC,但如果只使用 GCC/Clang 是一个选项,那么您可能会考虑它。具体来说,它使用“标签作为值”扩展,它允许您获取标签的地址,将其存储在一个值中并无条件跳转到它。我会用一个例子来展示它:

          GameEngine *CreateGameEngine(GameEngineParams const *params)
          {
              /* Declare an error handler variable. This will hold the address
                 to jump to if an error occurs to cleanup pending resources.
                 Initialize it to the err label which simply returns an
                 error value (NULL in this example). The && operator resolves to
                 the address of the label err */
              void *eh = &&err;
          
              /* Try the allocation */
              GameEngine *engine = malloc(sizeof *engine);
              if (!engine)
                  goto *eh; /* this is essentially your "throw" */
          
              /* Now make sure that if we throw from this point on, the memory
                 gets deallocated. As a convention you could name the label "undo_"
                 followed by the operation to rollback. */
              eh = &&undo_malloc;
          
              /* Now carry on with the initialization. */
              engine->window = OpenWindow(...);
              if (!engine->window)
                  goto *eh;   /* The neat trick about using approach is that you don't
                                 need to remember what "undo" label to go to in code.
                                 Simply go to *eh. */
          
              eh = &&undo_window_open;
          
              /* etc */
          
              /* Everything went well, just return the device. */
              return device;
          
              /* After the return, insert your cleanup code in reverse order. */
          undo_window_open: CloseWindow(engine->window);
          undo_malloc: free(engine);
          err: return NULL;
          }
          

          如果您愿意,您可以在定义中重构​​通用代码,从而有效地实现您自己的错误处理系统。

          /* Put at the beginning of a function that may fail. */
          #define declthrows void *_eh = &&err
          
          /* Cleans up resources and returns error result. */
          #define throw goto *_eh
          
          /* Sets a new undo checkpoint. */
          #define undo(label) _eh = &&undo_##label
          
          /* Throws if [condition] evaluates to false. */
          #define check(condition) if (!(condition)) throw
          
          /* Throws if [condition] evaluates to false. Then sets a new undo checkpoint. */
          #define checkpoint(label, condition) { check(condition); undo(label); }
          

          那么例子就变成了

          GameEngine *CreateGameEngine(GameEngineParams const *params)
          {
              declthrows;
          
              /* Try the allocation */
              GameEngine *engine = malloc(sizeof *engine);
              checkpoint(malloc, engine);
          
              /* Now carry on with the initialization. */
              engine->window = OpenWindow(...);
              checkpoint(window_open, engine->window);
          
              /* etc */
          
              /* Everything went well, just return the device. */
              return device;
          
              /* After the return, insert your cleanup code in reverse order. */
          undo_window_open: CloseWindow(engine->window);
          undo_malloc: free(engine);
          err: return NULL;
          }
          

          【讨论】:

            【解决方案9】:

            警告:以下内容不是很好,但确实可以。

            #include <stdio.h>
            #include <stdlib.h>
            
            typedef struct {
                unsigned int  id;
                char         *name;
                char         *msg;
            } error;
            
            #define _printerr(e, s, ...) fprintf(stderr, "\033[1m\033[37m" "%s:%d: " "\033[1m\033[31m" e ":" "\033[1m\033[37m" " ‘%s_error’ " "\033[0m" s "\n", __FILE__, __LINE__, (*__err)->name, ##__VA_ARGS__)
            #define printerr(s, ...) _printerr("error", s, ##__VA_ARGS__)
            #define printuncaughterr() _printerr("uncaught error", "%s", (*__err)->msg)
            
            #define _errordef(n, _id) \
            error* new_##n##_error_msg(char* msg) { \
                error* self = malloc(sizeof(error)); \
                self->id = _id; \
                self->name = #n; \
                self->msg = msg; \
                return self; \
            } \
            error* new_##n##_error() { return new_##n##_error_msg(""); }
            
            #define errordef(n) _errordef(n, __COUNTER__ +1)
            
            #define try(try_block, err, err_name, catch_block) { \
                error * err_name = NULL; \
                error ** __err = & err_name; \
                void __try_fn() try_block \
                __try_fn(); \
                void __catch_fn() { \
                    if (err_name == NULL) return; \
                    unsigned int __##err_name##_id = new_##err##_error()->id; \
                    if (__##err_name##_id != 0 && __##err_name##_id != err_name->id) \
                        printuncaughterr(); \
                    else if (__##err_name##_id != 0 || __##err_name##_id != err_name->id) \
                        catch_block \
                } \
                __catch_fn(); \
            }
            
            #define throw(e) { *__err = e; return; }
            
            _errordef(any, 0)
            

            用法:

            errordef(my_err1)
            errordef(my_err2)
            
            try ({
                printf("Helloo\n");
                throw(new_my_err1_error_msg("hiiiii!"));
                printf("This will not be printed!\n");
            }, /*catch*/ any, e, {
                printf("My lovely error: %s %s\n", e->name, e->msg);
            })
            
            printf("\n");
            
            try ({
                printf("Helloo\n");
                throw(new_my_err2_error_msg("my msg!"));
                printf("This will not be printed!\n");
            }, /*catch*/ my_err2, e, {
                printerr("%s", e->msg);
            })
            
            printf("\n");
            
            try ({
                printf("Helloo\n");
                throw(new_my_err1_error());
                printf("This will not be printed!\n");
            }, /*catch*/ my_err2, e, {
                printf("Catch %s if you can!\n", e->name);
            })
            

            输出:

            Helloo
            My lovely error: my_err1 hiiiii!
            
            Helloo
            /home/naheel/Desktop/aa.c:28: error: ‘my_err2_error’ my msg!
            
            Helloo
            /home/naheel/Desktop/aa.c:38: uncaught error: ‘my_err1_error’ 
            

            请记住,这是使用嵌套函数和__COUNTER__。如果你使用 gcc,你会很安全。

            【讨论】:

              【解决方案10】:

              在 C 中,您可以通过手动使用 if + goto 来“模拟”异常以及自动“对象回收”以进行显式错误处理。

              我经常编写如下 C 代码(归结为突出错误处理):

              #include <assert.h>
              
              typedef int errcode;
              
              errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
              {
                  errcode ret = 0;
              
                  if ( ( ret = foo_init( f ) ) )
                      goto FAIL;
              
                  if ( ( ret = goo_init( g ) ) )
                      goto FAIL_F;
              
                  if ( ( ret = poo_init( p ) ) )
                      goto FAIL_G;
              
                  if ( ( ret = loo_init( l ) ) )
                      goto FAIL_P;
              
                  assert( 0 == ret );
                  goto END;
              
                  /* error handling and return */
              
                  /* Note that we finalize in opposite order of initialization because we are unwinding a *STACK* of initialized objects */
              
              FAIL_P:
                  poo_fini( p );
              
              FAIL_G:
                  goo_fini( g );
              
              FAIL_F:
                  foo_fini( f );
              
              FAIL:
                  assert( 0 != ret );
              
              END:
                  return ret;        
              }
              

              这是完全标准的 ANSI C,将错误处理与您的主线代码分开,允许(手动)堆栈展开初始化对象,就像 C++ 所做的那样,这里发生的事情是完全显而易见的。因为您在每个点都明确地测试失败,所以在每个可能发生错误的地方插入特定的日志记录或错误处理确实更容易。

              如果您不介意一点宏魔法,那么您可以在执行其他操作(例如使用堆栈跟踪记录错误)时使其更加简洁。例如:

              #include <assert.h>
              #include <stdio.h>
              #include <string.h>
              
              #define TRY( X, LABEL ) do { if ( ( X ) ) { fprintf( stderr, "%s:%d: Statement '%s' failed! %d, %s\n", __FILE__, __LINE__, #X, ret, strerror( ret ) ); goto LABEL; } while ( 0 )
              
              typedef int errcode;
              
              errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
              {
                  errcode ret = 0;
              
                  TRY( ret = foo_init( f ), FAIL );
                  TRY( ret = goo_init( g ), FAIL_F );
                  TRY( ret = poo_init( p ), FAIL_G );
                  TRY( ret = loo_init( l ), FAIL_P );
              
                  assert( 0 == ret );
                  goto END;
              
                  /* error handling and return */
              
              FAIL_P:
                  poo_fini( p );
              
              FAIL_G:
                  goo_fini( g );
              
              FAIL_F:
                  foo_fini( f );
              
              FAIL:
                  assert( 0 != ret );
              
              END:
                  return ret;        
              }
              

              当然,这不像 C++ 异常 + 析构函数那样优雅。例如,以这种方式在一个函数中嵌套多个错误处理堆栈不是很干净。相反,您可能希望将它们分解为类似处理错误的自包含子函数,像这样显式地初始化 + 终结。

              这也只在单个函数中有效,并且不会继续向上跳栈,除非更高级别的调用者实现类似的显式错误处理逻辑,而 C++ 异常只会继续向上跳栈,直到找到合适的处理程序。它也不允许你抛出任意类型,而只能抛出一个错误代码。

              以这种方式系统地编码(即 - 使用单个入口和单个出口点)还可以很容易地插入无论如何都会执行的前后(“最终”)逻辑。您只需将“最终”逻辑放在 END 标签之后。

              【讨论】:

              • 非常好。我倾向于做类似的事情。 goto 非常适合这种情况。唯一的区别是我看不到最后一个“goto END”的必要性,我只是在那个时候插入一个成功返回,在其余部分之后插入一个失败返回。
              • 谢谢@NeilRoy goto END 的原因是我喜欢我的绝大多数函数都有一个入口点和一个出口点。这样,如果我想为任何功能添加一些“最终”逻辑,我总是可以轻松做到,而无需担心在某处潜伏着一些其他隐藏的回报。 :)
              【解决方案11】:

              Redis 使用 goto 来模拟 try/catch,恕我直言,它非常干净优雅:

              /* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success. */
              int rdbSave(char *filename) {
                  char tmpfile[256];
                  FILE *fp;
                  rio rdb;
                  int error = 0;
              
                  snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
                  fp = fopen(tmpfile,"w");
                  if (!fp) {
                      redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
                          strerror(errno));
                      return REDIS_ERR;
                  }
              
                  rioInitWithFile(&rdb,fp);
                  if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {
                      errno = error;
                      goto werr;
                  }
              
                  /* Make sure data will not remain on the OS's output buffers */
                  if (fflush(fp) == EOF) goto werr;
                  if (fsync(fileno(fp)) == -1) goto werr;
                  if (fclose(fp) == EOF) goto werr;
              
                  /* Use RENAME to make sure the DB file is changed atomically only
                   * if the generate DB file is ok. */
                  if (rename(tmpfile,filename) == -1) {
                      redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
                      unlink(tmpfile);
                      return REDIS_ERR;
                  }
                  redisLog(REDIS_NOTICE,"DB saved on disk");
                  server.dirty = 0;
                  server.lastsave = time(NULL);
                  server.lastbgsave_status = REDIS_OK;
                  return REDIS_OK;
              
              werr:
                  fclose(fp);
                  unlink(tmpfile);
                  redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
                  return REDIS_ERR;
              }
              

              【讨论】:

              • 代码已损坏。 errno 只能在失败的系统调用之后使用,不能在三个调用之后使用。
              • 此代码在多个地方重复了错误处理逻辑,并且可能会执行不正确的操作,例如多次调用 fclose(fp)。最好使用多个标签并使用这些标签对仍需要回收的内容进行编码(而不是对所有错误仅使用一个标签),然后根据错误发生在代码中的位置跳转到正确的错误处理位置。
              【解决方案12】:

              如果你在 Win32 中使用 C,你可以利用它的 Structured Exception Handling (SEH) 来模拟 try/catch。

              如果您在不支持 setjmp()longjmp() 的平台上使用 C,请查看 pjsip 库的 Exception Handling,它确实提供了自己的实现

              【讨论】:

                【解决方案13】:

                也许不是主要语言(不幸的是),但在 APL 中,有 ⎕EA 操作(代表 Execute Alternate)。

                用法: 'Y' ⎕EA 'X' 其中 X 和 Y 是作为字符串或函数名称提供的代码 sn-ps。

                如果 X 遇到错误,将改为执行 Y(通常是错误处理)。

                【讨论】:

                • 您好,mippo,欢迎来到 StackOverflow。虽然很有趣,但问题是专门关于在 C 中执行此操作的。所以这并不能真正回答问题。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2015-09-13
                • 1970-01-01
                • 2010-10-29
                • 2016-03-05
                • 1970-01-01
                • 2014-12-22
                相关资源
                最近更新 更多