1. 项目概述从使用者到创造者的视角转变在C的世界里std::vector几乎是每个开发者最早接触、也最频繁使用的容器。它就像一个智能的、会自己长大的数组我们用它存储数据、遍历元素、动态扩容享受着它带来的便利。但你是否曾好奇过这个看似简单的“动态数组”其内部究竟是如何运作的当我们调用push_back时内存发生了什么reserve和resize的本质区别是什么为什么迭代器失效问题总是让人头疼这些问题仅仅通过阅读文档或调用接口是无法获得深刻理解的。真正的理解往往来自于“亲手实现”。这就是我们这次“奇迹之旅”的核心手写一个简化版的vector类模板。这不是为了替代标准库而是为了像拆解一台精密的钟表一样探究其内部每一个齿轮的咬合方式。通过模拟实现我们将彻底揭开vector容器的核心机制包括内存管理、迭代器设计、异常安全以及那些教科书上不会写的“踩坑”经验。无论你是正在准备技术面试希望深入理解STL底层原理还是渴望提升自己的C内功这次从“使用者”到“创造者”的视角转变都将是一次宝贵的实践。2. 核心机制深度剖析vector的三大基石要手写一个vector我们必须先理解支撑它的三大核心机制动态内存管理、迭代器抽象与类型萃取、以及异常安全保证。这些机制共同构成了vector高效、灵活且健壮的基础。2.1 动态内存管理的艺术vector最核心的特性是动态扩容。它并不是每次添加元素都重新分配内存那将带来灾难性的性能损耗。其策略通常被称为“倍增扩容”或“几何增长”。工作原理vector内部维护三个关键指针或与之等效的成员_start: 指向已使用内存空间的首元素。_finish: 指向已使用内存空间的尾后位置最后一个元素的下一个位置。_end_of_storage: 指向整个已分配内存空间的尾后位置。当我们push_back一个新元素时首先检查_finish是否等于_end_of_storage。如果还有剩余空间则直接在_finish指向的位置构造对象然后_finish。如果没有空间了就需要扩容。扩容流程申请一块新的、更大的内存通常是原容量的1.5或2倍标准未规定但2倍是常见选择。将旧内存中的所有元素“移动”或“拷贝”到新内存中。这里涉及一个关键决策使用std::move如果元素类型支持移动语义可以提升效率。释放旧内存。更新三个指针使其指向新的内存区域。注意扩容是一个“昂贵”的操作它不仅涉及新内存的分配还涉及所有元素的搬迁。这就是为什么reserve接口如此重要——它允许我们在知道大致数据量的前提下提前分配足够的内存避免多次不必要的扩容和数据拷贝这是性能优化的关键点。容量增长因子为什么是2倍这是一个时间与空间的权衡。假设每次扩容增长k倍经过数学分析摊还分析2倍增长能在均摊时间复杂度为 O(1) 的push_back操作和内存利用率之间取得一个较好的平衡。过小的增长因子如1.5倍会导致更频繁的扩容拷贝过大的因子则可能导致内存浪费。在我们的模拟实现中我们将采用2倍扩容。2.2 迭代器泛型编程的桥梁迭代器是STL算法的基石它提供了一种统一的方法来访问容器中的元素而无需关心容器的内部结构。对于vector而言其迭代器本质上就是原生指针T*的封装因为其内存是连续的。迭代器的五种类型iterator: 普通迭代器可读写。const_iterator: 常量迭代器只读。reverse_iterator: 反向迭代器。const_reverse_iterator: 常量反向迭代器。以及相关的begin(),end(),rbegin(),rend()等成员函数。在我们的简化实现中为了聚焦核心我们可以先实现iterator和const_iterator。关键在于我们需要为迭代器类型定义一系列“标签”和操作使其符合STL迭代器的概念要求从而能与algorithm中的泛型算法协同工作。这通常通过为迭代器类定义iterator_category,value_type,difference_type,pointer,reference等嵌套类型来实现。迭代器失效问题这是vector使用中最常见的陷阱。任何可能导致vector重新分配内存的操作如insert,push_back导致扩容erase等都会使所有指向该vector的迭代器、引用和指针失效。理解了我们自己实现的扩容过程就能深刻明白失效的原因旧内存被释放了指向它的“指针”自然就成了野指针。在模拟实现中我们需要在相关函数的文档注释里明确指出这一点。2.3 异常安全与资源管理一个健壮的vector必须考虑异常安全。C异常可能发生在任何地方内存分配 (new) 可能抛出std::bad_alloc元素的拷贝/移动构造函数也可能抛出异常。基本保证我们的实现至少应提供“基本异常安全保证”即操作失败时容器应保持在一个有效但不确定的状态不会发生资源泄漏如内存泄漏。例如在push_back中如果新元素的构造抛出异常_finish不应被递增容器应保持原样。强异常安全保证对于某些操作如insert我们应努力实现“强异常安全保证”即操作要么完全成功要么完全失败容器状态与操作前完全一致。这通常需要通过“拷贝后交换”等技巧来实现。资源管理——RAII这是我们实现中最核心的编程范式。我们将使用一个“资源获取即初始化”的类来管理动态内存。简单来说就是在类的构造函数中分配内存在析构函数中释放内存。这样无论控制流如何离开正常返回或异常抛出内存都能被正确释放杜绝泄漏。在我们的vector类中_start,_finish,_end_of_storage这三个指针的生命周期管理就是RAII的典型应用。3. 手把手模拟实现MyVector 类模板现在让我们将理论付诸实践开始编写我们自己的MyVector。我们将遵循从简到繁、逐步完善的原则。3.1 基础框架与内存管理首先定义类模板的基本结构和三个核心指针。#include iostream #include algorithm // for std::max, std::move, etc. #include memory // for std::allocator, 但我们先手动管理 #include initializer_list namespace my_std { template typename T class vector { public: // 类型别名符合STL惯例 using value_type T; using iterator T*; using const_iterator const T*; using reference T; using const_reference const T; using size_type size_t; using difference_type ptrdiff_t; private: T* _start nullptr; // 指向数据块开始 T* _finish nullptr; // 指向最后一个有效元素的下一个位置 T* _end_of_storage nullptr; // 指向存储空间尾后 // 工具函数获取容量和大小 size_type capacity() const { return _end_of_storage - _start; } size_type size() const { return _finish - _start; } // 核心内存管理函数 void allocate_and_copy(size_type new_capacity, const T* src, size_type count); void deallocate(); public: // 构造函数系列 vector() default; explicit vector(size_type n, const T value T()); vector(std::initializer_listT init_list); template typename InputIt vector(InputIt first, InputIt last); vector(const vector other); // 拷贝构造 vector(vector other) noexcept; // 移动构造 ~vector(); // 赋值运算符 vector operator(const vector other); vector operator(vector other) noexcept; // 迭代器 iterator begin() { return _start; } const_iterator begin() const { return _start; } const_iterator cbegin() const { return _start; } iterator end() { return _finish; } const_iterator end() const { return _finish; } const_iterator cend() const { return _finish; } // 容量相关 bool empty() const { return _start _finish; } size_type size() const { return _finish - _start; } size_type capacity() const { return _end_of_storage - _start; } void reserve(size_type new_capacity); void resize(size_type new_size, const T value T()); // 元素访问 reference operator[](size_type pos) { // 不检查边界为了效率与std::vector行为一致 return _start[pos]; } const_reference operator[](size_type pos) const { return _start[pos]; } reference at(size_type pos); // 带边界检查抛出std::out_of_range const_reference at(size_type pos) const; reference front() { return *_start; } const_reference front() const { return *_start; } reference back() { return *(_finish - 1); } const_reference back() const { return *(_finish - 1); } T* data() { return _start; } const T* data() const { return _start; } // 修改器 void push_back(const T value); void push_back(T value); // 移动语义版本 void pop_back(); iterator insert(const_iterator pos, const T value); iterator insert(const_iterator pos, T value); iterator erase(const_iterator pos); iterator erase(const_iterator first, const_iterator last); void clear(); void swap(vector other) noexcept; }; }接下来是实现几个关键的内存管理函数。allocate_and_copy是扩容和拷贝构造的核心。template typename T void my_std::vectorT::allocate_and_copy(size_type new_capacity, const T* src, size_type count) { // 1. 分配原始内存 T* new_start static_castT*(::operator new(new_capacity * sizeof(T))); T* new_finish new_start; try { // 2. 在分配的内存上构造对象拷贝或移动 for (size_type i 0; i count; i) { // 使用 placement new 和拷贝构造 new (new_finish) T(src[i]); // 假设T的拷贝构造可能抛异常 new_finish; } } catch (...) { // 3. 如果构造过程中发生异常析构已构造的对象并释放内存 while (new_finish ! new_start) { (--new_finish)-~T(); } ::operator delete(new_start); throw; // 重新抛出异常 } // 4. 销毁旧对象释放旧内存 if (_start) { for (T* p _start; p ! _finish; p) { p-~T(); } ::operator delete(_start); } // 5. 更新指针 _start new_start; _finish new_finish; _end_of_storage _start new_capacity; } template typename T void my_std::vectorT::deallocate() { if (_start) { clear(); // 先析构所有对象 ::operator delete(_start); // 再释放内存 _start _finish _end_of_storage nullptr; } }关键点解析我们使用::operator new和::operator delete来分配和释放原始内存字节而不是new T[n]。这是因为new T[n]会同时分配内存并调用构造函数而我们希望将内存分配和对象构造分离以实现更精细的控制如reserve只分配不构造。placement new是核心技巧。它在已分配的原始内存地址上调用构造函数来创建对象。异常安全在allocate_and_copy的try块中如果某个元素的拷贝构造失败catch块会确保已构造的对象被正确析构并且分配的内存被释放然后重新抛出异常。这避免了内存泄漏提供了基本异常安全保证。析构函数~T()必须被显式调用这是管理原始内存的职责。3.2 构造、析构与赋值基于allocate_and_copy我们可以实现构造函数和赋值运算符。// 拷贝构造函数 template typename T my_std::vectorT::vector(const vector other) { if (other.size() 0) { allocate_and_copy(other.size(), other._start, other.size()); } } // 移动构造函数 (noexcept 对于标准库容器优化很重要) template typename T my_std::vectorT::vector(vector other) noexcept : _start(other._start), _finish(other._finish), _end_of_storage(other._end_of_storage) { other._start other._finish other._end_of_storage nullptr; } // 析构函数 template typename T my_std::vectorT::~vector() { deallocate(); } // 拷贝赋值运算符 (copy-and-swap idiom提供强异常安全保证) template typename T my_std::vectorT my_std::vectorT::operator(const vector other) { if (this ! other) { vector temp(other); // 拷贝构造可能抛异常但不会影响*this swap(temp); // swap 是 noexcept 的 } return *this; } // 移动赋值运算符 template typename T my_std::vectorT my_std::vectorT::operator(vector other) noexcept { if (this ! other) { deallocate(); // 释放当前资源 _start other._start; _finish other._finish; _end_of_storage other._end_of_storage; other._start other._finish other._end_of_storage nullptr; } return *this; } // swap 函数 template typename T void my_std::vectorT::swap(vector other) noexcept { using std::swap; swap(_start, other._start); swap(_finish, other._finish); swap(_end_of_storage, other._end_of_storage); }拷贝赋值运算符的妙用这里使用了“拷贝后交换”技术。先创建一个临时对象temp作为other的副本。如果拷贝成功再与*this交换。交换操作通常很快且不会抛出异常。如果拷贝构造失败异常会在修改*this之前抛出因此*this的状态保持不变这提供了强异常安全保证。这是C中实现安全赋值操作的经典模式。3.3 核心操作push_back, insert, erase 的实现这些是vector最常用也最体现其复杂性的操作。template typename T void my_std::vectorT::push_back(const T value) { if (_finish _end_of_storage) { // 需要扩容 size_type new_cap capacity() 0 ? 1 : capacity() * 2; // 2倍扩容 reserve(new_cap); } // 在_finish位置构造value的副本 new (_finish) T(value); // placement new _finish; } template typename T void my_std::vectorT::push_back(T value) { if (_finish _end_of_storage) { size_type new_cap capacity() 0 ? 1 : capacity() * 2; reserve(new_cap); } new (_finish) T(std::move(value)); // 移动构造 _finish; } template typename T void my_std::vectorT::reserve(size_type new_capacity) { if (new_capacity capacity()) { // 注意reserve不改变size()只改变capacity() allocate_and_copy(new_capacity, _start, size()); } // 如果 new_capacity capacity(), 标准规定什么都不做 }push_back的逻辑很清晰检查容量不足则扩容然后在尾部构造新元素。我们提供了左值和右值两个版本以支持移动语义当传入临时对象时效率更高。insert和erase的实现则更复杂因为它们涉及区间元素的移动。template typename T typename my_std::vectorT::iterator my_std::vectorT::insert(const_iterator pos, const T value) { // 计算插入位置的索引 size_type index pos - begin(); if (_finish _end_of_storage) { // 插入可能导致扩容扩容后所有迭代器失效需要重新计算pos size_type new_cap capacity() 0 ? 1 : capacity() * 2; reserve(new_cap); } // 此时pos可能已失效使用index iterator insert_pos _start index; // 将插入点及之后的元素向后移动一位 // 需要从后往前移动避免覆盖 if (_finish ! insert_pos) { // 在_finish位置构造一个T的临时对象然后移动 new (_finish) T(std::move(*(_finish - 1))); // 移动最后一个元素到新位置 // 从后向前移动元素 for (iterator it _finish - 1; it ! insert_pos; --it) { *it std::move(*(it - 1)); } *insert_pos value; // 在插入点赋值新值 } else { // 插入位置就是end()相当于push_back new (_finish) T(value); } _finish; return insert_pos; // 返回指向新插入元素的迭代器 } template typename T typename my_std::vectorT::iterator my_std::vectorT::erase(const_iterator pos) { iterator p _start (pos - begin()); // 将pos1之后的元素向前移动一位覆盖pos for (iterator it p; it ! _finish - 1; it) { *it std::move(*(it 1)); // 使用移动赋值 } // 销毁最后一个元素现在它已经被移走了 (_finish - 1)-~T(); --_finish; return p; // 返回指向被删除元素之后位置的迭代器 }insert的复杂性迭代器失效处理insert可能触发扩容。一旦扩容传入的pos迭代器指向旧内存就失效了。因此我们需要在扩容前计算出插入位置的索引index扩容后使用_start index得到新的有效迭代器。这是理解迭代器失效的关键实践。元素移动为了给新元素腾出空间需要将[pos, end())区间的元素整体向后移动一位。必须从后向前移动否则会覆盖数据。我们使用了std::move来尝试进行移动赋值如果T定义了移动赋值运算符效率会更高。异常安全上面的简化实现假设T的移动操作是noexcept的。在实际工业级实现中需要更精细地处理移动构造/赋值可能抛异常的情况以提供更强的异常安全保证。erase的注意点erase使指向被删除元素及之后所有元素的迭代器、引用和指针失效因为元素向前移动了。它返回的迭代器指向原来被删除元素的下一个位置。3.4 完善与测试resize, at, cleartemplate typename T void my_std::vectorT::resize(size_type new_size, const T value) { if (new_size size()) { // 缩小销毁多余元素 iterator new_finish _start new_size; while (_finish ! new_finish) { (--_finish)-~T(); } } else if (new_size size()) { // 扩大可能需要扩容并用value填充 if (new_size capacity()) { reserve(new_size); // 至少扩容到new_size } iterator old_finish _finish; _finish _start new_size; for (iterator it old_finish; it ! _finish; it) { new (it) T(value); // 在未初始化内存上构造 } } // 如果 new_size size()什么都不做 } template typename T typename my_std::vectorT::reference my_std::vectorT::at(size_type pos) { if (pos size()) { throw std::out_of_range(vector::at); } return _start[pos]; } template typename T void my_std::vectorT::clear() { for (T* p _start; p ! _finish; p) { p-~T(); } _finish _start; // 逻辑清空内存不释放 }resize和reserve的区别是初学者常混淆的点reserve(n)只改变capacity保证容量至少为n。不影响size不创建/销毁对象。resize(n)改变size为n。如果n size()会添加新元素用默认值或指定值构造如果n size()会销毁多余的元素。它可能隐式地调用reserve。at提供了带边界检查的安全访问越界时抛出std::out_of_range异常而operator[]则不检查行为未定义。clear只析构所有对象将_finish重置为_start但不释放内存。这是为了后续的push_back操作可以复用已分配的内存提高效率。如果想释放内存可以结合“swap技巧”vectorT().swap(v);这将用一个空的临时vector与v交换临时对象析构时会释放内存。4. 核心使用技巧与避坑指南理解了实现原理我们再来看看在实际项目中如何高效、安全地使用std::vector以及如何避开那些常见的“坑”。4.1 性能优化关键善用 reserve 和移动语义1. 预分配内存 (reserve) 这是提升vector性能最直接有效的方法。如果你能预估元素的大致数量在插入大量数据前调用reserve可以避免多次扩容和数据拷贝。std::vectorint data; data.reserve(10000); // 一次性分配足够空间 for (int i 0; i 10000; i) { data.push_back(i); // 这10000次push_back都不会触发扩容 }2. 拥抱移动语义 对于管理资源的自定义类型如包含动态数组的类实现移动构造函数和移动赋值运算符并在向vector添加临时对象右值时push_back或emplace_back会自动调用移动操作避免深拷贝。class MyResource { int* data; public: MyResource(MyResource other) noexcept : data(other.data) { other.data nullptr; // 移动后置空源对象 } // ... 其他成员 }; std::vectorMyResource vec; vec.push_back(MyResource()); // 调用移动构造高效 vec.emplace_back(); // 更高效直接在vector内存中构造对象3. 使用emplace_back替代push_backemplace_back接受构造参数直接在vector尾部内存中构造对象省去了创建临时对象再移动/拷贝的步骤效率更高。struct Point { int x; int y; Point(int a, int b) : x(a), y(b) {} }; std::vectorPoint points; points.emplace_back(1, 2); // 直接构造无需先创建Point对象 // 等价于 points.push_back(Point(1, 2)); 但更高效4.2 迭代器失效陷阱全解析这是vector最著名的“坑”。任何可能引起内存重新分配扩容或元素位置移动插入、删除非尾部元素的操作都会使部分或全部迭代器、引用、指针失效。失效场景与应对策略表操作失效范围原因与后果安全操作建议push_back/emplace_back仅当发生扩容时所有迭代器、引用、指针失效。内存地址改变旧指针悬空。扩容后必须重新获取迭代器。可用capacity()和size()预判。insert1.所有插入位置之后的迭代器、引用、指针失效。2. 如果触发扩容则所有失效。元素移动或内存重分配。insert会返回指向新元素的迭代器应使用其返回值更新循环变量。erase被删除元素及其之后的所有迭代器、引用、指针失效。元素向前移动填补空缺。使用erase的返回值指向被删元素下一位置来更新迭代器。这是正确遍历删除的关键。pop_back仅end()迭代器以及指向最后一个元素的引用/指针失效。尾部元素被销毁。通常影响较小但需注意不要持有back()的引用并随后pop_back。reserve/resize(增大)如果导致重新分配新容量 旧容量则所有失效。内存地址改变。在reserve之前获取的迭代器在reserve之后不可再用。swap/clearswap使两个容器的所有迭代器交换归属。clear使所有迭代器失效但内存可能未释放。容器内容或身份改变。操作后应视为全新容器重新获取迭代器。经典错误案例遍历时删除元素std::vectorint vec {1, 2, 3, 4, 5}; for (auto it vec.begin(); it ! vec.end(); it) { if (*it % 2 0) { vec.erase(it); // 错误erase后it失效后续的it行为未定义 } }正确做法利用erase的返回值更新迭代器。for (auto it vec.begin(); it ! vec.end(); /* 这里不递增 */) { if (*it % 2 0) { it vec.erase(it); // erase返回下一个有效迭代器 } else { it; } }4.3 选择正确的访问方式[]、at、front/back、dataoperator[]最快但不进行边界检查。仅在百分之百确定索引有效时使用例如在已知大小的循环中。at()进行边界检查越界时抛出std::out_of_range异常。安全性高但略有性能开销。适合在索引可能来自外部输入或复杂计算时使用。front()/back()访问首尾元素的便捷方法代码意图更清晰。注意在空容器上调用是未定义行为使用前应检查empty()。data()返回指向底层数组的指针。主要用于需要与C风格API交互的场景如某些C库函数。注意在vector扩容后data()返回的指针会失效。4.4 理解 shrink_to_fit 与 swap 技巧vector的capacity()只会增长不会自动缩减。即使你clear()了所有元素已分配的内存仍然保留。shrink_to_fit()这是一个非强制性的请求要求容器减少capacity()以匹配size()。实现可以忽略此请求。在C11及以后可用。swap 技巧在C11之前或者想强制释放内存可以使用“swap技巧”。std::vectorint vec(1000); // ... 使用vec ... vec.clear(); // size0, capacity 可能还是1000 std::vectorint().swap(vec); // 与一个空的临时vector交换 // 现在 vec 的 capacity() 很可能为 0或很小临时对象std::vectorint()在表达式结束后析构会释放它从vec那里交换来的大块内存。5. 进阶话题与面试常见问题5.1 vector 的特化一个“非标准”的容器std::vectorbool是标准库的一个特化版本。它并不存储真正的bool对象而是将每个bool值压缩到一个比特位bit中存储以节省空间8个bool占1字节而不是8字节。带来的问题不是标准容器因为它不满足标准容器的所有要求。例如它的reference类型是一个代理类std::vectorbool::reference而不是bool。这会导致一些泛型代码失效。性能权衡节省了空间但访问单个比特需要位运算比直接访问字节慢。并且对其元素的取址vec_bool[0]得不到一个真正的bool*。迭代器行为它的迭代器也是代理迭代器解引用返回的是代理引用可能影响某些算法。建议除非对内存极度敏感否则通常建议使用std::vectorchar或std::dequebool来替代std::vectorbool以获得标准的容器语义和性能特征。5.2 自定义分配器 (Allocator)我们之前的模拟实现使用了默认的::operator new/delete。标准库的std::vector的第二个模板参数就是分配器Allocator。它抽象了内存的分配与释放策略允许你将内存管理如使用内存池、共享内存、栈内存等与容器逻辑分离。template class T, class Allocator std::allocatorT class vector;实现一个符合“分配器”概念的类型需要定义allocate,deallocate,construct,destroy等方法。这属于高级主题但在需要极致性能优化或特殊内存环境的系统中非常重要。5.3 常见面试题深度剖析vector底层原理是什么答vector是一个封装了动态数组的顺序容器。它维护三个指针或等效物指向数组起始的指针、指向最后一个元素后位置的指针、指向分配内存末尾后位置的指针。通过指针相减得到size()和capacity()。当size() capacity()时插入新元素会触发扩容通常申请一块更大的内存如原容量的2倍将旧元素移动或拷贝过去然后释放旧内存。vector的扩容机制是怎样的时间复杂度如何答常见策略是倍增扩容如2倍。进行摊还分析Amortized Analysis假设每次插入成本为1扩容成本为kk为当前容量那么经过n次插入后总成本约为 n (1 2 4 ... n) n 2n 3n。因此单次push_back的摊还时间复杂度为 O(1)。reserve和resize的区别答reserve(n)只改变容量capacity保证至少能容纳n个元素不改变大小size不创建/销毁对象。resize(n)改变大小size为n。如果nsize会添加新元素默认初始化或指定值如果nsize会销毁多余元素。resize可能隐式调用reserve。vector迭代器失效的场景答所有可能引起内存重新分配的操作如insert,push_back导致扩容会使所有迭代器、引用、指针失效。在特定位置插入或删除元素insert,erase会使被操作位置及之后的所有迭代器、引用、指针失效。swap操作会使两个容器的所有迭代器交换“归属”。如何在遍历vector时正确删除元素答不能简单地用for (auto it vec.begin(); it ! vec.end(); it)然后在循环内erase(it)因为erase会使当前迭代器失效。正确做法是利用erase的返回值更新迭代器it vec.erase(it);当删除时否则it。vector和list/deque如何选择答选择取决于主要操作vector需要随机访问operator[] O(1)尾部插入删除频繁且元素数量相对稳定或可预估。缺点在中间或头部插入删除效率低O(n)因为需要移动元素。list(双向链表)需要在序列中任意位置频繁插入删除O(1)但需先找到位置查找是O(n)不需要随机访问。缺点内存开销大每个元素都有前后指针缓存不友好。deque(双端队列)需要在头尾两端频繁插入删除且需要随机访问效率略低于vector。它是分段连续存储。通过这次从内到外对vector的探索我们不仅实现了一个简易版本更重要的是深入理解了其设计哲学、性能权衡和陷阱所在。这种理解远比死记硬背面试题答案要深刻得多。下次当你再使用std::vector时你看到的将不再是一个黑盒而是一个由精妙指针操作、内存管理和异常安全保证构成的精密工具。这才是C学习的乐趣所在——知其然更知其所以然。