问题

C 语言执行 a=a++; 后,a 的值应该加一还是不变?

回答
这个问题在 C 语言中,关于表达式 `a = a++;` 的结果,是初学者乃至一些有经验的程序员都可能感到困惑的地方。它的行为并非我们直观想象的那么简单,并且会涉及到 C 语言中一个非常重要的概念:未定义行为(Undefined Behavior)。

首先,让我们尝试理解一下 C 语言是如何处理这个表达式的。`a = a++;` 这个表达式包含了两个主要的动作:

1. 自增操作符 `a++`:这是一个后缀自增操作符。它的工作方式是:
返回 `a` 的当前值:在表达式的计算过程中,`a++` 会先将其原始值(也就是操作前的 `a` 的值)“拿出来”备用。
然后将 `a` 的值加一:在原始值被取出来之后,`a` 的值才会真正地增加 1。

2. 赋值操作符 `=`:这个操作符会将右侧表达式计算出的最终值赋给左侧的变量 `a`。

现在,将这两个动作结合起来分析 `a = a++;` 这个表达式:

第一步:自增操作符 `a++` 开始执行。
它会读取 `a` 的当前值。我们假设 `a` 的初始值为 `5`。
它会将 `5` 这个值“记住”(或者说放到一个临时存储区域)。
然后,它会将 `a` 的值加一,使 `a` 变成 `6`。

第二步:赋值操作符 `=` 开始执行。
它需要一个右侧的值来赋值给左侧的 `a`。
关键在于,赋值操作符 `=` 会使用 `a++` 在第一步返回的“原始值”。也就是说,它会使用我们之前“记住”的那个 `5`。
所以,表达式的最终结果是将 `5` 这个值赋给 `a`。

结论(理论上的解释):

根据上述的理论分析,如果我们严格按照 C 语言规范中对后缀自增和赋值的顺序解释,那么表达式 `a = a++;` 的结果应该是:

1. `a` 的原始值被取出来(例如 `5`)。
2. `a` 的值被递增 1(变为 `6`)。
3. 原始值(`5`)被赋回给 `a`。

所以,从这个理论推导来看,`a` 的值应该保持不变(即回归到自增操作前的那个值)。

但是!现实往往更复杂,也更危险。

在 C 语言标准中,一个对象(在这里是变量 `a`)的同一个值在同一个顺序点(sequence point)之前,不能被修改超过一次。而 `a = a++;` 这个表达式,涉及到对 `a` 的两次修改(一次是 `a++` 的内部递增,一次是赋值操作),并且这两次修改之间没有定义明确的顺序点来分隔它们。

C 语言的编译器在处理这种表达式时,可能会有不同的实现方式。例如:

一种可能的实现:编译器可能先执行 `a++`,它会读取 `a` 的值,然后递增 `a`,最后返回 `a` 的原始值给赋值语句。在这种情况下,结果如我们上面理论分析的那样,`a` 的值不变。
另一种可能的实现:编译器可能先读取 `a` 的值用于赋值,然后才去执行 `a` 的递增操作。这同样会导致 `a` 的值不变。
还有一种(可能是最常见但最不推荐的)实现:编译器可能先执行递增,但由于赋值操作也依赖于 `a` 的“老值”并且两者没有明确的顺序,编译器可能在赋值时仍然使用了递增前的值。
更糟糕的是:由于没有明确的顺序点保证,编译器甚至可能自由地安排这两个操作的执行顺序,从而导致结果不确定。例如,它可能先递增 `a`,但赋值时因为某种优化,仍然使用了递增前的值,或者更糟糕的是,它可能在递增之前就决定了要赋值的值,导致最终结果不是我们期望的那样。

因此,根据 C 语言的标准,`a = a++;` 这样的表达式属于“未定义行为”(Undefined Behavior)。

什么是未定义行为?

未定义行为意味着 C 语言标准不作任何规定。编译器可以这样做任何事情:

它可能产生你预期的结果(例如,`a` 的值不变)。
它可能产生出乎意料的结果(例如,`a` 的值可能变成 `a+2`,或者 `a` 的值发生其他奇怪的变化)。
它可能导致程序崩溃。
它可能在你的电脑上看起来正常,但在别人的电脑上却表现异常。
甚至可能在编译时就出现警告或错误(但不是强制性的)。

为什么会出现这种情况?

C 语言的设计哲学是简洁、高效,并且让程序员对底层硬件有很大的控制权。为了这种灵活性,它在一些地方牺牲了安全性,将一些复杂或容易出错的语义交由编译器来处理。当涉及到对同一个变量进行多次修改而没有明确的顺序时,编译器就有了很大的自由度来优化代码,但也因此引入了不确定性。

实际测试:

如果你在不同的编译器(GCC, Clang, MSVC 等)或同一编译器的不同优化级别下测试 `int a = 5; a = a++; printf("%d ", a);`,你很可能会得到不同的结果。

在某些编译器或优化级别下,你可能会看到输出 `5`。
在其他情况下,你可能会看到输出 `6`。
甚至可能出现更意想不到的行为。

结论和建议:

尽管从理论上分析,我们似乎可以推导出 `a` 的值应该不变,但由于 `a = a++;` 涉及未定义行为,所以你永远不应该依赖这种表达式的结果。

正确的理解是:

表达式 `a = a++;` 的行为在 C 语言中是不确定的,是未定义行为。
你不能预测它的确切结果,它可能导致程序行为异常。
为了写出可移植、可预测且健壮的代码,你应该绝对避免使用这种形式的表达式。

如果你想实现“让 `a` 的值加一”,并且确保其值被使用后才进行递增,你应该写成:

```c
int temp = a; // 先保存 a 的原始值
a++; // 然后递增 a
// 在这里使用 temp (原始值) 或者 a (递增后的值)
// 如果你想将原始值赋给 a,那写成 temp = temp; 或者 a = temp;
// 但这个例子本身就是自相矛盾的,因为你想用原始值赋值给自己,又想递增。
```

更实际的情况是,如果你想递增 `a` 并将其新值赋给另一个变量,应该写成:

```c
int b = a++; // b 得到 a 的原始值,然后 a 加一
// 或者
int b;
b = a; // b 得到 a 的原始值
a++; // a 加一
```

如果你想递增 `a` 并将其新值赋给自己(也就是简单地让 `a` 加一),那么最清晰明了的方式就是:

```c
a++;
```

或者

```c
a = a + 1;
```

总之,永远不要写 `a = a++;` 这样的代码,它是一个经典的 C 语言陷阱。

网友意见

user avatar

首先需要指出,对于C语言来说a=a++;这种写法是完全错误的不规范的写法,其结果a到底应该是多少根本不值得讨论,甚至可以认为它的结果是不可预期的。

然后我们再讨论为什么你这里结果会是22:

       #include <stdio.h>  int main(int argc, char *argv[]) {         int a = 22;         a = (a++);          printf("a = %d
", a);          return 0; }     

编译执行:

       # gcc -o mytest mytest.c -Wall mytest.c: In function ‘main’: mytest.c:6:4: warning: operation on ‘a’ may be undefined [-Wsequence-point]   a = (a++);   ~~^~~~~~~ # ./mytest  a = 22     

这里我们已经看到,合格一点的编译器这里已经可以发现你的问题,指出“a=(a++);”不知道你在表达什么。但是编译器只是给出了警告没有直接错误退出,所以很多时候编译器的警告最好不要直接忽视,我还写过一篇关于编译警告的文章,有兴趣可以看一下

言归正传,我们继续看这里为什么是22,而不是你以为的23。为了探寻指令到底是怎么执行的,我们需要查看a=a++;这句C语言语句被编译器编译后的汇编代码:

       # gcc -S -o mytest.S mytest.c -Wall # less mytest.S       1         .file   "mytest.c"       2         .text       3         .section        .rodata       4 .LC0:       5         .string "a = %d
"       6         .text       7         .globl  main       8         .type   main, @function       9 main:      10 .LFB0:      11         .cfi_startproc      12         pushq   %rbp      13         .cfi_def_cfa_offset 16      14         .cfi_offset 6, -16      15         movq    %rsp, %rbp      16         .cfi_def_cfa_register 6      17         subq    $32, %rsp      18         movl    %edi, -20(%rbp)      19         movq    %rsi, -32(%rbp)      20         movl    $22, -4(%rbp)      21         movl    -4(%rbp), %eax      22         leal    1(%rax), %edx      23         movl    %edx, -4(%rbp)      24         movl    %eax, -4(%rbp)      25         movl    -4(%rbp), %eax      26         movl    %eax, %esi      27         movl    $.LC0, %edi      28         movl    $0, %eax      29         call    printf      30         movl    $0, %eax      31         leave      32         .cfi_def_cfa 7, 8      33         ret      34         .cfi_endproc      35 .LFE0:      36         .size   main, .-main      37         .ident  "GCC: (GNU) 8.3.1 20190223 (Red Hat 8.3.1-2)"      38         .section        .note.GNU-stack,"",@progbits     

如上所示,那么a=22; a=a++;对应的就是下面这几行:

            20         movl    $22, -4(%rbp)      21         movl    -4(%rbp), %eax      22         leal    1(%rax), %edx      23         movl    %edx, -4(%rbp)      24         movl    %eax, -4(%rbp)     

第20行,将22赋值到main函数的堆栈-4(%rbp)处,-4(%rbp)对应的就是main函数的临时变量a。这句也就是a=22;

a=a++;对应21~24四行指令,分别如下:

  1. 第21行,将a的数值赋值到eax寄存器。此时eax里存的是22。
  2. 第22行,相当于把a+1的结果存到edx寄存器。此时edx里存的是23。
  3. 第23行,将edx的数值赋值给a,也就是a=23。此时完成了a++。
  4. 第24行,将eax的数值赋值给a, 也就是a自增之前的值赋值给a,也就是a=22。

到此就可以看出a为什么是22了,后面就是把a作为参数给printf了,这里printf打印出来的当然是22。

(编译器表示:惊不惊喜?意不意外?没想到我还有这种操作吧[手动滑稽])

注意“这里”这个词。因为我最开始就说过了,这种写法是非常不规范的甚至错误的,其结果往往是无法评估的,不同的编译器不同的系统可能给你编译出不同的结果。所以,讨论其结果并没有意义,记住不要这样写才是有用的。


补充一:对于讨论较多的两个问题我说一下纯个人看法:

  1. 确认某一编译器的行为一直是“一致”的,这样的情况是否可以算做行为定义了?

即使你对一个编译器做大量的测试,确认其行为在某一版本后一直保持“一致”,这样仍是没有用的。在程序员界有这样一种开玩笑的说法:“A documented bug is a feature, an undocumented feature is a bug.” 意思就是一种被文档和标准明确定义的行为,即使再古怪也可以被认为是一种“特性”。而一个没有被明确定义的行为,即使你用的再“舒服”也是一个定时炸弹。

我来举个例子吧。大家都知道RHEL-8.0现在已经发布了,其实在其开发过程中出现了一个很严重的“特性”问题(问题已经过去了,我觉得应该可以说了),是我在做文件系统相关工作时发现并立即上报后不断力争其得到“修复”的。在一次内部小版本更新后突然很多工具出现使用异常,研究后发现类似[a-z]等写法都不能正常工作了,类似的正则表达式写法不再匹配以前人们“熟知”的范围。

简单看一下一个简单的测试:

       $ echo ABCD | egrep '[a-z]' ABCD     

发现问题了没?我们所熟知的[a-z]应该是只匹配小写的字母a到字母z的,但是它现在却匹配的更多内容,甚至包括大写字母。为什么呢?
因为glibc在一次更新后终于加入了它认为应有的一次“完善”工作,这种完善可以让其语义更明确,但是却导致在不同的语言环境下类似[a-z]这种表达将具有不同的意义。我所熟知的[a-z]代表字母a到字母z的理解其实一直是建立在默认LANG=C的语言环境之上的,而如果语言环境是其它的,比如LANG=en_US.utf-8,它的排序不是和ASCII一样的,[a-z]里面不再只是小写26个字母。所以这个正则表达式才匹配了更多。而我们在使用类似[a-z]这种用法时,其实应该是预置语言环境是LANG=C,否则你最好使用例如"[[:lower:]]"这种。

这本来是一次特性的完善更新,结果却发现漫长的使用过程中很多人将这种不规范的写法用的到处都是,现在特性被完善后问题一下子都出来了。所以说不是你一直以来觉得对的用法就一定是对的。不过对于上面这个问题,为了不给customers带来更多的困扰,我还是提议做了妥协,glibc的人最终也在下面这个patch中“修复”了部分问题:

       commit 7cd7d36f1feb3ccacf476e909b115b45cdd46e77 Author: Carlos O'Donell <carlos@redhat.com> Date:   Wed Jul 25 17:00:45 2018 -0400      Keep expected behaviour for [a-z] and [A-z] (Bug 23393).     

决定对如en_US.utf-8等常用默认语言环境维持[a-z]等正则表达式长久以来的“预期结果”,但并不是所有语言环境都是这样的“预期结果”。这个问题虽然最终没有给大客户们带来更多的麻烦。但是对于[a-z]等正则表达式的不全面理解和使用确实是一个隐藏的问题。建议明确定义语言环境再使用。(对上述问题感兴趣的可以尝试使用上游glibc的2.26-27版本,就能看见这个问题了。在正规的操作系统发行版中,这个问题应该是已经“修复”的)。

2. 一个语言在某一新版本规范中定义了此前未定义的行为,那是否代表我就可以无顾忌的使用了?

我觉得还是应该酌情考虑,尽量不要使用兼容性不好的“特性”。一个特别新的“特性”,在它没有大范围普及和替换原有版本前,最好别太早将其引入项目(特别是如果这个“特性”除了能让你耍帅以外并不能带来任何大的好处的时候。)。如果你过早引入一个语言特性,则必须明确限定编译环境。比如像a=a++这种问题,如果你觉得新规范已经定义了,于是你在项目中使用这样的写法,并且预期其等于规范中的结果。但是如果其它人不知道你在项目中引入了只有最新标准才定义的行为,然后获取源码后在别的很多地方大量编译使用,那么问题将是严重的。

作为工程师,我们除了遵守规范来做产品以外,还要考虑产品的适用性普遍性以及对已有用户的影响等很多因素。就像上面那个[a-z]的例子,glibc那些定义规则的大佬们觉得自己这么多年来终于将自己长久以来没有完善的一个特性完善了。他们喜出望外,觉得自己特别了不起。但没想到他们却打破了大部分用户日积月累下来了大量他们认为不规范的错误的用法。而这个时候一味的耍牛脾气是没有用的,只能作出折中的妥协,然后让这种新的“规范”在日后慢慢得到普及。


补充二:

唉,被某评论气到了。我一再强调,不要用你以为的方式去理解编译器的行为,更不要瞎抬杠,编译器的行为跟你所学的语言语法里的行为是完全两个概念,否则为什么学完C语言程序设计还需要学编译原理等高阶课程? 老郭有句笑话说的话糙理不糙:“我和火箭专家碰面,我问他火箭是不是火柴点的,对方看我一眼算他输。”

以前宫博就老说我,让我多看看书再来和他接着讨论问题,更不要在什么都不知道的时候先质疑他。他不讨厌别人质疑他,他喜欢别人发现他的实质性错误,并指出来大家一起探讨,他只是不喜欢和解释不清的人讨论对方的质疑。我觉得他说的有道理,所以后来每次我要和他讨论问题前,不看够几本相关书籍,都不敢张嘴。就这样,我每次都还要被他鄙视一遍,再回去多看点再回来讨论。我并没有觉得他不尊重“小辈”,反而觉得他是为我好,是值得尊重的,反而是我的行为有“挑衅”他的嫌疑(虽然我不是这么想的)。

咱也不是那不讲道理的人,比如下面这个回答,当时李海怪直接“踩”的我说我写的偏题,XXX比我写的好。但是我看了之后发现那确实是我疏忽了,那咱就得好好感谢人家,然后把回答尽量改好。

类似的还有一些。所以说问问题归问问题,回答归回答,讨论问题归讨论问题,抬杠归抬杠。

最后我补充一下gcc的manual文档里对a=a++类似问题的一点面向使用者的说明(里面明确说到a=a++)



题外话:

很多人表示这种问题的根源应该怪某些大学教材。说实话,我也从事计算机专业相关学习和工作十多年了,我真的没有印象我在哪本教材里看到过类似这样的问题。我从一开始就是从外界的讨论声中听到的,根本没见过一个有“官方”支持的地方公开出现过这种问题。

当然我也不能说是读了万卷书,总是有我没见过的书。所以谁要是找到一半带有这样问题的教材或习题什么的,哪怕是大学老师的板书,请拍照把问题描述清楚的情况下给我看看,我很欢迎。

对于这种类似问题的根源,我觉得不一定要归结给大学教材。因为当一个人在知识不够充足的时候,往往就爱在自己仅掌握的一点非常有局限性的知识里“专研”各种钻牛角尖似的问题。殊不知很多问题和习题并不适合不断深入的“钻”研,它们只是阶段性的让你巩固一下当下的知识而已,并没有过多停留下来“深入”研究展开的必要。要做的应该是继续向前,在掌握更多知识后再时不时回头看,从更高的角度看待问题。

诸如本问题这样的问题,明显就是对一个非常局限的知识点过分“钻研”的结果和产物。至于问题到底是从哪里出来的,我觉得并不重要,因为所有在小角落里过分“钻研”这种问题的人都有可能发出这样的疑问,并深以为然。


更多内容请参阅:

醉卧沙场:README - 专栏文章总索引

类似的话题

  • 回答
    这个问题在 C 语言中,关于表达式 `a = a++;` 的结果,是初学者乃至一些有经验的程序员都可能感到困惑的地方。它的行为并非我们直观想象的那么简单,并且会涉及到 C 语言中一个非常重要的概念:未定义行为(Undefined Behavior)。首先,让我们尝试理解一下 C 语言是如何处理这个表.............
  • 回答
    在 MATLAB 中执行 C 语言代码,或者将 C 代码转换为 MATLAB 代码,这在实际工作中是很常见的需求。这通常是为了充分发挥 C 语言在性能上的优势,或者将已有的 C 库集成到 MATLAB 的开发流程中,以及利用 MATLAB 强大的数据分析和可视化能力来处理 C 代码生成的数据。下面我.............
  • 回答
    .......
  • 回答
    C 语言的设计理念是简洁、高效、接近硬件,而其对数组的设计也遵循了这一理念。从现代编程语言的角度来看,C 语言的数组确实存在一些“不改进”的地方,但这些“不改进”很大程度上是为了保持其核心特性的兼容性和效率。下面我将详细阐述 C 语言为何不“改进”数组,以及这种设计背后的权衡和原因:1. 数组在 C.............
  • 回答
    C 语言王者归来,原因何在?C 语言,这个在编程界已经沉浮数十载的老将,似乎并没有随着时间的推移而消逝,反而以一种“王者归来”的姿态,在许多领域焕发新生。它的生命力如此顽强,甚至在 Python、Java、Go 等语言层出不穷的今天,依然占据着不可动摇的地位。那么,C 语言究竟为何能实现“王者归来”.............
  • 回答
    C语言指针是否难,以及数学大V认为指针比范畴论还难的说法,是一个非常有趣且值得深入探讨的话题。下面我将尽量详细地阐述我的看法。 C语言指针:理解的“门槛”与“终点”首先,我们需要明确“难”的定义。在编程领域,“难”通常指的是: 学习曲线陡峭: 需要花费大量时间和精力去理解和掌握。 容易出错:.............
  • 回答
    C 语言中的 `void main()` 并非是语言标准规定的写法,它的出现和流传,更像是一个历史遗留问题、编译器兼容性以及开发者习惯共同作用的结果。要详细讲解,我们需要从 C 语言的诞生和演变说起。1. C 语言的起源和早期标准 (K&R C) C 语言的诞生: C 语言最初是由 Dennis.............
  • 回答
    C语言自学能到什么高度?详细解析C语言,作为一门强大且经典的编程语言,其学习曲线相对陡峭,但一旦掌握,其应用范围之广,性能之优越,是许多其他语言难以比拟的。 仅凭自学,C语言可以让你达到一个非常高的技术高度,足以让你在许多领域成为一名优秀的开发者甚至专家。以下将从多个维度详细阐述C语言自学所能达到的.............
  • 回答
    在 C 语言中判断一个数列是否为等差数列,核心思想是验证数列中任意相邻两项的差值是否恒定不变。下面我将从概念、算法实现、注意事项以及代码示例等方面进行详细讲解。 一、什么是等差数列?在数学中,等差数列(Arithmetic Progression 或 Arithmetic Sequence)是指一个.............
  • 回答
    在 C 语言中,不用 `goto` 和多处 `return` 进行错误处理,通常依靠以下几种模式和技术。这些方法旨在提高代码的可读性、可维护性,并遵循更结构化的编程原则。核心思想: 将错误处理的逻辑集中到函数退出前的某个点,或者通过特定的返回值来指示错误。 1. 集中错误处理(Single Exit.............
  • 回答
    这个问题很有意思,也触及到了C语言作为一种基础性语言的根本。很多人听到“C语言本身是用什么写的”时,会先想到“用更高级的语言写的”,比如Python或者Java。但事实并非如此,或者说,这个答案需要更深入的理解。首先,我们需要明确一点:C语言最初的实现,也就是早期的C编译器,并不是用C语言本身写的。.............
  • 回答
    C 语言中,一些自带函数返回的是指向数组的指针,而你无需手动释放这些内存。这背后涉及到 C 语言的内存管理机制以及函数设计哲学。要弄清楚这个问题,我们需要从几个关键点入手: 1. 返回指针的函数,内存的归属至关重要首先,理解函数返回指针时,内存的“所有权”是谁的,是解决这个疑问的核心。当一个函数返回.............
  • 回答
    在 C 语言中,枚举(`enum`)是一种用户定义的数据类型,它允许你为一组整数常量命名。这使得代码更具可读性和可维护性。而枚举中的 `end` 关键字,严格来说,它本身并不是 C 语言标准枚举定义的一部分,而是一种常见的编程约定或模式,用于标记枚举序列的结束。让我来详细解释一下,并尽可能剥离 AI.............
  • 回答
    在C语言中,严格来说,不能直接“判断”一个变量的类型是否是`int`或`float`。C语言是一种静态类型语言,变量的类型在编译时就已经确定,并且不能在运行时随意更改或检查。当你声明一个变量时,你就已经告诉了编译器它的类型。不过,如果你想表达的是“根据当前存储的值,推断出这个变量应该被视为整数还是浮.............
  • 回答
    在 C 语言中,让不同线程之间能够交流信息、协同工作,这本身就是多线程编程中最核心也是最需要仔细处理的部分。别把它想得太玄乎,无非就是大家共享一块内存,然后约定好怎么读写这块内存罢了。只不过,这“约定”怎么立得住,不让大家互相捣乱,才是关键。咱们把线程通信这事儿,拆解成几个层面来说。 1. 共享内存.............
  • 回答
    在C语言中, `a > b ? a < c ? a : b : c` 这种写法是利用了三元运算符 (?:) 的嵌套。它是一种简洁的条件表达式,用来根据条件的真假返回不同的值。理解它的关键在于一步步拆解它的逻辑。咱们就来好好捋一捋这串表达式的判断过程,讲得透彻一些,保证让你明白它到底是怎么回事儿。首先.............
  • 回答
    C 语言里,一旦你用了 ` ` 来进行换行,确实就“回不去了”——至少在标准的输出流中是这样。这背后的原理,要从计算机如何处理文本输出和终端(或者说显示器)的工作方式说起。核心点:文本流与终端的坐标系统想象一下你的程序输出的文本,就像一条源源不断地向前流动的河流。` `(换行符)就是这条河流中的一个.............
  • 回答
    在C语言中,关于“乘以0.01”和“除以100”哪个更快速,这是一个非常值得探讨的话题,尤其是在追求极致性能的底层开发或者对浮点运算效率敏感的场景下。要回答这个问题,我们需要深入理解计算机如何处理这两种操作,以及它们在硬件层面的具体实现。理解基础:乘法和除法在计算机中的运算首先,我们得明白计算机进行.............
  • 回答
    朋友,咱们这话题聊得挺实在的。C语言现在还有没有“必要”学,未来还有没有“用”,这绝对是个值得深入掰扯掰扯的问题。别听那些虚头巴脑的,咱就从实际出发,好好说说。C语言现在还有没有“必要”学?我想说,如果你想在计算机底层或者和效率打交道,那 C 语言的“必要性”依然挺强的,甚至可以说是基石性的。你得明.............
  • 回答
    在 C 语言编程的世界里,选择一个趁手的编辑器就像是给了你一对飞翔的翅膀。这不仅关乎效率,更影响着你的开发体验和创造力。市面上的编辑器琳琅满目,各有千秋,要说哪个“最好用”,这其实是个非常主观的问题,取决于你的个人习惯、项目需求以及你追求的侧重点。不过,如果你想在众多选择中找到最适合你的那位,不妨先.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有