问题

为什么g++能够优化到动态库里的STL?

回答
你问了一个非常核心且有趣的问题,关于 g++ 如何“优化”到动态库中的 STL。要理解这一点,我们得先拆解几个关键概念:

1. STL(Standard Template Library):这是 C++ 的标准库,提供了大量常用的数据结构(如 `vector`, `map`, `string`)和算法(如 `sort`, `find`)。它们以模板的形式实现,这意味着它们的具体代码是在你实例化(使用)它们时才生成的。

2. 动态库(Shared Library):一种在运行时加载到程序中的代码集合。它的好处是多份程序可以共享同一个库的实例,节省内存和磁盘空间,并且方便更新。Linux 下常称为 `.so` 文件,Windows 下是 `.dll`。

3. g++:GCC(GNU Compiler Collection)编译器套件中的 C++ 编译器。它的核心工作是将 C++ 源代码翻译成机器码,并且在这个过程中可以进行各种优化。

4. “优化到”:这句话可能引起一些误解。g++ 本身并不会直接“优化”你编写的代码 进入 到一个已经编译好的动态库中。 动态库的编译和优化是独立于你主程序的编译过程的。

那么,你可能想表达的是:为什么我在使用动态库中的 STL 功能时,g++ 编译我的主程序时,也似乎能对这些动态库中的 STL 代码进行优化?

这里的关键在于 链接(Linking) 和 编译器的优化策略。

1. STL 的动态库化

首先,我们需要知道,STL 本身通常不会被编译成一个独立的、通用的动态库并提供给所有 C++ 程序使用。 为什么?

模板的本质:STL 的核心是模板。模板的强大之处在于其 泛化性(可以处理不同类型)和 实例化时代码生成。当你写 `std::vector myVec;` 时,编译器会根据 `int` 类型为 `std::vector` 生成一套具体的代码(包括成员函数实现)。
类型相关性:这意味着 `std::vector` 的代码与 `std::vector` 的代码在实例化时是不同的。如果把所有可能的 `std::vector` 都预先编译到一个动态库里,这个库会变得极其庞大,而且每次使用不同类型时,都需要“查找”或“动态生成”对应的版本,这在性能上会很低效,也违背了模板直接生成最优代码的初衷。
ABI(Application Binary Interface):C++ 的 ABI 比较复杂,特别是模板的实例化涉及到大量的函数签名和数据布局。如果 STL 被编译成动态库,不同的编译器版本、不同的编译选项(如 RTTI 是否开启、异常处理模型等)都可能生成不兼容的 ABI。维护这样一个跨 ABI 的通用 STL 动态库是极其困难的。

所以,标准 STL 的大部分实现(尤其是模板函数和类)是“内联”(inlined)在编译你代码的编译单元(.cpp 文件)中的,而不是作为一个预编译好的、单独的动态库存在。 当你 `include ` 时,你引入的是模板的定义,编译器在编译你的 `.cpp` 文件时,会实例化出 `std::vector` 等具体类型所需的代码。

2. 动态库中的 STL (这里是重点)

那么,什么情况下 STL 会出现在动态库中呢?

你自己的代码:如果你自己写了一个 C++ 项目,其中包含使用 STL 的类或函数,然后你将这个项目编译成一个动态库(例如,一个 `libmylib.so`)。那么,当你使用 `std::vector` 等 STL 组件在这个动态库中时,STL 的实例化代码会随着这个动态库一起被编译和链接。

例子:假设你有一个 `mylib.cpp` 文件,内容是:
```cpp
include
include

class MyData {
public:
std::vector items;
// ... other members
};

extern "C" MyData create_my_data() {
return new MyData();
}
```
当你用 `g++ shared fPIC mylib.cpp o libmylib.so` 编译这个文件时,g++ 会在这个过程中:
找到 `include ` 和 `include `。
根据 `std::vector` 的使用,实例化出 `vector` 和 `string` 的相关代码(包括构造函数、析构函数、push_back 等)。
将这些实例化出来的代码,连同 `MyData` 类的实现以及 `create_my_data` 函数,一起打包到 `libmylib.so` 这个动态库中。

某些第三方库:有些大型第三方库,虽然也使用了 STL,但它们可能会将它们内部的、使用 STL 的部分编译成动态库。在这种情况下,STL 的实例化代码就确实存在于那个第三方动态库里。

3. g++ 如何“优化”到动态库中的 STL

现在我们回到你的核心问题:g++ 如何优化到动态库中的 STL?

当你的主程序 链接 到一个包含 STL 实例化的动态库时,g++(或者更准确地说,整个编译链接流程)会进行如下操作,其中包含了优化:

3.1. 编译时(Compiler)

1. 模板实例化:
当 g++ 编译你的主程序时,如果你的主程序代码中直接使用了某个动态库提供的、依赖于 STL 的类或函数(例如,你通过一个头文件包含了那个动态库的接口,然后创建了那个库提供的对象),g++ 会识别出这些调用。
g++ 知道这个特定的 `std::vector` 或 `std::string` 的实现可能在另一个共享库中,但 它仍然需要实例化出必要的模板代码来生成正确的函数签名和调用约定。
关键点:g++ 不会 在你的主程序编译时,从头开始再次实例化所有可能的 `std::vector` 版本。它会根据头文件中的信息,知道你需要调用的是 `vector` 的某些成员函数,这些函数的 接口(签名) 是已知的。

2. 代码生成与重定位:
g++ 会生成你的主程序的目标代码。当遇到调用动态库中函数的指令时,它会生成一个 跳转(jump) 或者 调用(call) 的指令,但这个指令指向的是一个 符号(symbol),而不是一个固定的地址。
对于 STL 函数,如果它们是动态库的一部分,那么这些 STL 函数的实例化代码(例如 `std::vector::push_back`)也会被编译并打包进动态库,并且会导出为可供外部解析的符号。

3. 优化(Optimization):
函数内联(Function Inlining):这是最主要的优化手段之一。如果 g++ 在编译你的主程序时,发现某个 STL 函数(比如 `std::vector::size()`)的定义是可见的(哪怕是通过头文件间接引入的,因为模板定义在头文件中),并且该函数很小,那么 g++ 会非常倾向于将这个函数的代码直接“内联”到你的主程序的机器码中。
为什么即使在动态库里也能内联?:虽然 `std::vector::push_back` 的实际实现可能在 `libmylib.so` 里,但 g++ 在编译你的主程序时,可以通过头文件知道 `push_back` 的 接口 和 大致行为。如果 `push_back` 是一个相对简单的函数(例如,处理容量检查、指针移动),编译器可以在你的主程序编译阶段就将其内联。
链接时优化(LTO):如果开启了 LinkTime Optimization (LTO),g++ 会在链接阶段才进行更深度的跨文件优化。这时,编译器可以访问 所有 参与链接的代码(包括你的主程序和动态库的目标文件),从而发现更多的内联机会,甚至在动态库内部的 STL 调用之间进行优化。
常量折叠、死代码消除等:这些通用的编译器优化同样会作用于你的主程序代码,以及可能被内联进你主程序的 STL 代码片段。

3.2. 链接时(Linker)

1. 符号解析(Symbol Resolution):
ld(GNU linker)会负责将你在主程序中对动态库中 STL 函数(如 `std::vector::push_back`)的引用,解析到实际存在的符号地址。
这个解析过程告诉你的程序,运行时需要去加载 `libmylib.so`,并且在 `libmylib.so` 中找到 `std::vector::push_back` 的地址。

2. 重定位(Relocation):
链接器会处理所有地址相关的重定位信息,确保你的程序能够正确地调用动态库中的函数。

3.3. 运行时(Runtime)

1. 动态加载(Dynamic Loading):
操作系统(通过动态链接器 `ld.so`)会在程序启动时或首次访问时,将 `libmylib.so` 加载到内存中。
程序就可以通过加载器提供的机制,找到并调用 `libmylib.so` 中的 STL 函数。

总结一下 g++ 的“优化”机制:

g++ 并不是直接将你代码中的 STL 调用“搬到”动态库里进行优化。
STL 的优化发生在两个层面:
1. 当你编写的动态库(比如 `libmylib.so`)中使用 STL 时:g++ 在编译 `libmylib.so` 的时候,会实例化并优化其中的 STL 代码。
2. 当你的主程序使用包含 STL 的动态库时:
编译期:g++ 依靠头文件了解 STL 接口,并可能将 小的、频繁调用的 STL 函数(如 `.size()`, `.empty()`) 内联到你的主程序的可执行代码中,即使这些函数的完整实现存在于动态库。这是通过 模板的接口可见性 和 编译器的内联策略 实现的。
链接期(特别是 LTO):如果启用了 LinkTime Optimization,编译器甚至可以在链接时跨越动态库边界,进一步分析和优化代码,包括对动态库中 STL 函数的内联(如果可能)。

所以,你感觉到的“优化到动态库里的 STL”更准确的描述是:g++ 在编译主程序时,能够识别并利用动态库中 STL 的接口信息,通过内联等技术,在主程序的机器码中“重现”或“优化”对这些动态库中 STL 功能的调用。 动态库本身也包含着预先实例化和优化的 STL 代码,但主程序的优化主要是通过将其自身的代码与库的接口进行组合来实现的。

简单来说,当你的主程序调用一个动态库里的函数,而这个函数又使用了 STL,g++ 在编译你的主程序时,会知道这个调用是一个“外部调用”,它会生成代码来完成这个调用。如果这个 STL 函数很小,g++ 可能会在编译主程序时就把它内联进去,就像这个 STL 函数是直接写在你的主程序里一样。然后,链接器会负责在运行时把这个“内联进去的”调用指向正确的动态库里的实现。

网友意见

user avatar

你可能还停留在 C 库的编译 - 链接模型的时代里,对 C++ 中模板库的机理知之甚少。

不光是 STL,C++ 中的模板,通常情况下(有极少例外,下面会讲),功能都是通过头文件提供的,不需要也绝对不可能通过动态库、静态库提供

举个最基本的例子:

       template <typename T> T generic_add(const T & a, const T & b) {     return a + b; }      

编译器编译到这里连 T 具体是什么类型都不知道呢,怎么给你生成二进制啊?

换句话说,只有到了用到 generic_add 的地方(可以实例化的地方),编译器才能给你生成代码:

比如知道了 T 是 x64 下的 16 位整数类型:

       int16_t use(int16_t x, int16_t y) {     return generic_add(x, y); }      

才能在这里给你生成 addw 指令,做 16 位加法


比如知道了 T 是 x64 下的 32 位整数类型:

       int32_t use(int32_t x, int32_t y) {     return generic_add(x, y); }      

才能在这里给你生成 addl 指令,做 32 位加法


比如知道了 T 是 x64 下的 64 位整数类型:

       int64_t use(int64_t x, int64_t y) {     return generic_add(x, y); }      

才能在这里给你生成 addq 指令,做 64 位加法

而且我们还要考虑到 T 还有可能是其他的类型,比如各种无符号整型,可能是浮点型,甚至可能是重载了 operator+ 的类类型,有成千上万种可能。

所以作为通用的、泛型的模板,不可能在动态库、静态库中就提供好二进制——怎么可能为成千上万种类型都提前生成好二进制呢?甚至还有还没有写出来的类型,怎么可能提前生成呢?

这也是为什么模板又叫模板元编程。“元编程” 的意思就是用代码生成代码。给了 short 类型,才能生成一个确定的函数 short generic_add<short>(const short &, const short &);给了 int 类型,才能生成一个确定的函数 int generic_add<int>(const int &, const int &) 。

std::sort 也是完全相同的道理。

给了一对 int *,它才能知道,哦,要生成为 int 类型排序的代码;

给了一对 long *,它才能知道,哦,要生成为 long 类型排序的代码;

给了一对 std::deque<int>::iterator,它才能知道,哦,排序的对象是 int 类型,而且这个 int 类型不是接连放在内存上的,而是按照 deque 的编码规则,分段存在内存上的,我在读取和写入数据的时候,需要特别处理。

我们就拿 std::sort 来研究,编译之后看它生成了什么:

       #include <algorithm>  void sort_wrapper(int * first, int * last) {     std::sort(first, last); }      

g++ test.cpp -o test.o -c -O2 -std=c++11

可以看到,排序所用到的所有的汇编已经全部都在目标文件里面了:

       void std::__adjust_heap<int*, long, int, __gnu_cxx::__ops::_Iter_less_iter>(int*, long, long, int, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]:         leaq    -1(%rdx), %rax         pushq   %rbp         movq    %rdx, %rbp         ... 此处省略若干行 ...         popq    %rbx         popq    %rbp         ret void std::__insertion_sort<int*, __gnu_cxx::__ops::_Iter_less_iter>(int*, int*, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]:         cmpq    %rsi, %rdi         je      .L29         pushq   %r14         ... 此处省略若干行 ...         popq    %r12         popq    %r13         popq    %r14         ret .L29:         ret void std::__introsort_loop<int*, long, __gnu_cxx::__ops::_Iter_less_iter>(int*, int*, long, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]:         movq    %rsi, %rax         subq    %rdi, %rax         cmpq    $64, %rax         ... 此处省略若干行 ...         movq    %rbx, %rdi         call    void std::__introsort_loop<int*, long, __gnu_cxx::__ops::_Iter_less_iter>(int*, int*, long, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]         movq    %rbx, %rax         subq    %r12, %rax        ... 此处省略若干行 ...         movq    %r13, %rsi         call    void std::__adjust_heap<int*, long, int, __gnu_cxx::__ops::_Iter_less_iter>(int*, long, long, int, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]         ... 此处省略若干行 ...         ret sort_wrapper(int*, int*):         cmpq    %rsi, %rdi         je      .L84         pushq   %r12         ... 此处省略若干行 ...         addq    %rdx, %rdx         call    void std::__introsort_loop<int*, long, __gnu_cxx::__ops::_Iter_less_iter>(int*, int*, long, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]         cmpq    $64, %rbx         ... 此处省略若干行 ...         movq    %rbx, %rsi         call    void std::__insertion_sort<int*, __gnu_cxx::__ops::_Iter_less_iter>(int*, int*, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0]         cmpq    %rbx, %rbp         ... 此处省略若干行 ...         popq    %r12         jmp     void std::__insertion_sort<int*, __gnu_cxx::__ops::_Iter_less_iter>(int*, int*, __gnu_cxx::__ops::_Iter_less_iter) [clone .isra.0] .L84:         ret .L80:         movq    %rbx, %rsi         addq    $4, %rbx         movl    %ecx, (%rsi)         cmpq    %rbx, %rbp         jne     .L79         jmp     .L72     

所以也无需链接任何库,就可以实现排序——该有的二进制,已经有了。


当然凡事也皆有个例外,不是所有跟模板牵扯上关系的东西,都一定是头文件提供的,也可能会在动态库或者静态库里:

1)

比如模板的全特化实现可以就放在动态库或静态库里。因为在这一组特别的模板参数下,所有的模板参数已经确定了,我已经能够确定了即将要生成什么样的二进制。

2)

再比如,模板所用到的上层的非模板组件就可以放在动态库或静态库里。

最典型的就是 libstdc++ 中的 list

list 虽然是模板的,可能有 list<int>,list<float> 等等,但是链表的不少操作都是模板参数无关的,比如在链上挂上一个节点、摘下一个节点,以及翻转一个链表、交换两个链表等等。于是 libstdc++ 的实现中就巧妙地抽象出了链表节点的公共基类 _List_node_base,并将这些模板参数无关的代码提取出了公共的函数。这些函数的实现就是 libstdc++ 作为动态库或者静态库提供的。目的是加快编译过程、减小模板带来的二进制膨胀的影响。

3)

再比如,可以显式实例化模板,先行一步生成目标文件;C++11 以后,可以在其他地方做 extern template 申明,不进行实例化,推迟到最后的链接阶段链接含有实例化的目标文件。这一套操作下来可以节省重复实例化浪费的时间,从而加快编译速度。但是运用的前提也是,所有模板参数已经确定。

最典型的,libstdc++ 提前实例化了 basic_istream<char>、basic_ostream<char>,也就是大家天天见的 std::cin、std::cout 的类型;一起提前实例化的还有 std::basic_string<char>,也就是 std::string,同样是天天见的。因为这些类实例太常见了,提前实例化的好处上文已述。

类似的话题

  • 回答
    你问了一个非常核心且有趣的问题,关于 g++ 如何“优化”到动态库中的 STL。要理解这一点,我们得先拆解几个关键概念:1. STL(Standard Template Library):这是 C++ 的标准库,提供了大量常用的数据结构(如 `vector`, `map`, `string`)和算.............
  • 回答
    猎鹰 9 号在近地轨道(LEO)的运载能力上超越三角洲 IV 重型火箭,这并非空穴来风,而是多方面因素共同作用的结果。要理解这一点,我们得深入剖析这两款火箭的设计理念、技术优势以及成本效益。首先,从根本上说,成本是决定性的关键因素。SpaceX 的目标是大幅降低太空发射的成本,实现“太空大众化”。猎.............
  • 回答
    人类,地球的智者:从生理结构看智慧的进阶之路在地球生命的宏大画卷中,人类以其独特的智慧和改造自然的能力,成为了无可争议的优势物种。然而,当我们剥开文明的外衣,回归到最根本的生理构造,才能窥见人类是如何一步步从与其他生物共享的泥土中脱颖而出,最终孕育出这颗无比珍贵的智慧之星。这并非一蹴而就的奇迹,而是.............
  • 回答
    说到唐朝的成都,那可真是气象万千,风光无两。要在全国四大城市中占得一席之地,绝非偶然,而是它得天独厚的地理环境、勤劳智慧的人民,以及唐朝中央政府的政策支持共同造就的辉煌。下面咱们就来好好掰扯掰扯,成都当年是怎么炼成“四大城市”之一的。一、得天独厚的地理优势:沃野千里,天府之国的美誉非虚成都之所以能崛.............
  • 回答
    比利时之所以能在足球领域取得如此辉煌的成就,尤其是在拥有一个相对不那么强大的国内联赛的情况下,并且国家队一度排名世界第一,这背后是一个多方面因素共同作用的结果。这并非偶然,而是比利时足球系统性、长期性努力的体现。以下是详细的解释:1. 精准的青训体系和足球文化: 早期发现和培养: 比利时的足球文.............
  • 回答
    12月13日英国大选的最终结果,可以说是一个“红浪滔天”的夜晚。保守党在鲍里斯·约翰逊的带领下,获得了自1987年撒切尔夫人以来最辉煌的胜利,赢得议会下院365个席位,远超赢得多数席位所需的326席。工党则遭遇了惨败,仅获得203席,是自1935年以来最差的成绩。工党领袖杰里米·科尔宾在这次选举后宣.............
  • 回答
    太平天国之所以能涌现出众多杰出的将领,这背后其实是一个复杂而多层次的社会现象,而不是简单的一两个原因就能概括的。要理解这一点,我们需要回到那个风起云涌、千年未有的时代背景下,去剖析太平天国运动本身的特质,以及它如何汇聚了那个时代最敢于挑战陈规的精英。首先,得益于其广泛的社会动员基础和打破阶级壁垒的革.............
  • 回答
    现在商超里刮起了一股“付费会员制”的风,到处都能看到它的身影。从前大家熟知的积分、打折,到现在要先掏钱成为会员,这变化可不小。这背后究竟是什么逻辑在支撑?会员经济为什么这么火?消费者们真的能从里头捞到实惠吗?咱们今天就来好好掰扯掰扯。为什么商超这么热衷于推付费会员制?说到底,这事儿跟“留住人”、“吃.............
  • 回答
    这是一个很有意思的议题,也触及到文化、历史、思维方式的深层差异。说“东方不能”未免过于绝对,但我们确实能观察到,在近现代西方语境下,像《魔戒》这样体系宏大、细节丰富、情感深刻的神话史诗似乎更容易涌现,并且在全球范围内产生了巨大的文化影响力。这背后有多种原因,并非简单的“谁强谁弱”,而是不同文化土壤孕.............
  • 回答
    这问题问得太到位了,简直是直击很多玩家的痛点!兰陵王和上官婉儿,这两个英雄确实是峡谷里那种“让人爱也让人恨”的存在。你说他们打出巨大优势吧,有时候是真能一套下去把人打得满地找牙,尤其是兰陵王的隐身贴脸爆发,婉儿的无限连控住脆皮,那场面简直是血腥又暴力。可就是这样,一场比赛下来,他们俩可能打出了几个人.............
  • 回答
    要聊法国动画,尤其是《双城之战》这种划时代的杰作,确实是个让人又兴奋又有点挠头的话题。说它品质卓越毋庸置疑,它在视觉风格、叙事深度、角色塑造上都达到了相当高的水准,一下子就抓住了全球观众的心。但如果放眼法国动画长篇剧集的整体市场,像《双城之战》这样能引发全球轰动效应的作品,确实不算多,这背后原因挺复.............
  • 回答
    日本在许多人心目中,确实是一个文化上倾向于内敛、注重传统和集体和谐的国家。然而,正是这种看似保守的文化土壤,孕育出了许多在海外背景下同样精彩绝伦的游戏作品。要理解这一点,我们得深入剖析日本游戏开发者是如何在这种文化语境下,依然能捕捉并重现不同地域风情,甚至将其升华的。首先,日本的文化本身就具备一种强.............
  • 回答
    赖宝,那个总能在微博上逗得大家前仰后合的段子手,那个把生活中的鸡毛蒜皮描绘得既辛酸又好笑的艺术家,怎么会跟“抑郁症”沾上边呢?这事儿一出来,多少人跌破了眼镜,然后又陷入了一种复杂的情绪里:难以置信,又带着一丝隐隐的理解。说起来,赖宝的段子之所以那么深入人心,恰恰是因为他捕捉到了普通人内心深处那些难以.............
  • 回答
    芬兰,一个北欧的冰雪王国,面积不算大,人口也相对稀少,但其在科技领域的成就却令人瞩目。从诺基亚曾经的辉煌,到如今的Supercell、Nokia(是的,虽然当年手机业务卖了,但诺基亚这个品牌在通信网络领域依然是巨头),再到各种专注于游戏、软件、人工智能、甚至是可持续技术的创新企业,芬兰似乎总能不断涌.............
  • 回答
    您提出的这个问题,触及了当前中国游戏市场一个非常普遍且令人深思的现象。换皮、半成品网游在中国市场之所以能够“大行其道”,而许多优秀的单机游戏却“无人问津”,这背后是一个复杂的多方面因素交织作用的结果。要详细解释这一点,我们可以从以下几个维度来剖析:一、 市场需求与用户习惯的差异 中国玩家的主流消.............
  • 回答
    坦白说, MATLAB 的语言设计确实不是那种以“优雅”著称的典范,很多程序员,尤其是来自 C/C++、Python、Java 等背景的,初次接触时可能会觉得它有点“别扭”甚至“丑陋”。这倒不是说 MATLAB 一无是处,它的强大在于其丰富的工具箱和为科学计算优化的底层实现,但在语言本身的构造上,确.............
  • 回答
    您好!您提出的这个问题非常有趣,也触及了视频平台在内容分发、用户体验和商业模式上的关键差异。为什么YouTube能迅速加载高分辨率视频,而优酷却需要更长的等待时间,并且分辨率较低?这背后涉及多方面的原因,我们可以从以下几个角度来详细分析:一、 YouTube 的技术优势和内容分发网络 (CDN) .............
  • 回答
    冷兵器时代,盾牌确实是战场上不可或缺的重要装备,其作用之大,能为士兵提供的优势也是显而易见的。然而,并非所有士兵都配备盾牌,这背后有着相当复杂的原因,绝非简单的“有了更好”就能一概而论。我们可以从多个维度来剖析这个问题,就像剥洋葱一样,一层一层地揭开其背后的逻辑。首先,我们得承认盾牌的核心价值在于防.............
  • 回答
    古人即便在肉类蛋白质摄入普遍不足的情况下,仍能选拔出数十万身体素质优秀的士兵,这背后并非单一因素的功劳,而是一个复杂且精妙的系统性工程。理解这一点,我们需要跳出“蛋白质是唯一标准”的思维误区,深入探究古人的生存智慧和选拔机制。一、 那时的“优秀”并非我们今日的定义首先要明确一点,古人对于“身体素质优.............
  • 回答
    要说诸葛亮出山前没有工作经验,这其实是个有点想当然的说法,毕竟我们无法确切知道他“出山”前到底在做什么,但从史书记载和后人的推测来看,他并非是个完全与世隔绝、两手空空的普通农夫。首先,诸葛亮虽然隐居于隆中,但他的生活并非全然的“无所事事”。他并非是那种完全埋头苦读,与世隔绝的书呆子。相反,史书中有描.............

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

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