1. 项目概述为什么要在椭圆内部生成点网格在图形学、物理仿真、数值计算乃至游戏开发中我们常常会遇到一个看似简单却非常基础的需求在一个二维的椭圆区域内均匀或按某种规则生成一系列的点。这个需求就是“在2D椭圆内部生成点网格”。你可能会想画个椭圆然后往里撒点不就行了但实际操作起来从效率、精度到点的分布质量处处都是学问。比如在做有限元分析的前处理时你需要为椭圆截面的构件划分网格在粒子系统初始化时你可能需要让粒子均匀地填满一个椭圆形的区域甚至在开发一个2D策略游戏时需要为单位的“视野范围”一个椭圆生成可探测的离散点阵。直接用矩形网格然后判断点是否在椭圆内是最直观的想法但效率低下边界处理粗糙。而利用椭圆的数学特性进行“适配性”的网格生成才是更优雅、高效的解决方案。今天我就结合自己多年在图形算法和科学计算中的踩坑经验手把手带你用C实现一个健壮、高效且可配置的2D椭圆内部点网格生成器并附上完整的、可直接编译运行的源码。无论你是正在学习C图形编程的学生还是需要解决类似工程问题的开发者这篇文章都能让你不仅拿到可用的代码更能透彻理解背后的原理和优化技巧。2. 核心思路与算法选型不止一种方法生成椭圆内部的点核心是解决两个问题点的生成规则和点与椭圆的位置关系判断。不同的应用场景对点的分布均匀网格、随机分布、极坐标分布和密度有不同的要求。这里我们主要探讨最常用的均匀矩形网格和极坐标网格并重点实现前者因为它更通用也更容易扩展到其他形状。2.1 方法一边界框遍历与位置判断本方案核心这是最直接、最容易理解的方法也是我们将要重点实现的。确定椭圆的轴对齐边界矩形AABB对于一个中心在(cx, cy)半长轴为a半短轴为b的椭圆其外接矩形的范围是[cx-a, cxa]和[cy-b, cyb]。定义网格步长根据需要的精度或网格分辨率确定在x和y方向上的步进距离stepX和stepY。遍历矩形网格点在边界矩形范围内以步长为间隔生成一系列候选点(x, y)。椭圆内部判断对每个候选点利用椭圆的标准不等式进行判断。对于一个标准位置的椭圆中心在原点轴与坐标轴对齐点(x, y)在椭圆内部含边界的条件是(x/a)² (y/b)² 1。对于中心平移后的椭圆条件变为((x-cx)/a)² ((y-cy)/b)² 1。收集内部点将满足条件的点存入容器如std::vectorstd::pairdouble, double。为什么选择这个方法实现简单逻辑清晰代码易于编写和维护。控制灵活可以轻松控制网格的密度通过步长和范围。通用性强该“生成-判断”的范式可以很容易地迁移到其他由不等式定义的封闭区域如圆形、多边形等。缺点在边界矩形四个角附近的区域会生成大量明显在椭圆外的无效候选点存在一定的计算浪费。但对于现代CPU和中等精度的网格来说这点开销通常是可接受的。我们可以通过优化遍历范围来部分缓解。2.2 方法二极坐标参数方程法椭圆可以用参数方程表示x cx a * cos(theta),y cy b * sin(theta)其中theta属于[0, 2π)。确定角度和径向步长在角度theta上均匀采样同时在半径比例r从0到1上也均匀采样。生成点对于每一对(r, theta)计算x cx a * r * cos(theta),y cy b * r * sin(theta)。优点生成的点天生就在椭圆内部或边界上无需进行内部判断。点在径向上分布均匀。缺点在椭圆中心区域点会过于密集而在长轴端点附近相对稀疏对于需要均匀笛卡尔网格的应用来说并不理想。更适用于需要极坐标对称性的场景。2.3 方法三变换法单位圆缩放与旋转椭圆可以看作是一个单位圆经过缩放和旋转得到的。我们可以先在单位圆内部生成均匀网格点然后通过一个仿射变换矩阵将这些点映射到目标椭圆上。在单位圆内生成点可以使用方法一在[-1,1]的正方形内判断x²y²1或其他方法。构造变换矩阵变换包括缩放(a, b)和可能的旋转θ最后平移(cx, cy)。应用变换对每个单位圆内的点(u, v)计算[x, y]^T [cx, cy]^T R(θ) * [a*u, b*v]^T其中R(θ)是旋转矩阵。优点数学上优雅一次变换即可得到所有点。特别适合需要同时处理旋转椭圆的情况。缺点实现稍复杂需要线性代数基础。对于轴对齐椭圆旋转步骤可省略此时变换简化为x cx a*u,y cy b*v本质上与方法一在数学上等价但实现视角不同。综合考量对于大多数需要规则网格的应用方法一边界框遍历在实现复杂度、灵活性和性能之间取得了最佳平衡。因此我们的源码将以此为核心进行实现和优化。3. 详细设计与实现从理论到健壮代码接下来我们深入到代码层面设计一个类来封装这个功能。一个好的实现不仅要能跑还要考虑接口的清晰度、计算的精度、以及使用的便利性。3.1 类设计EllipsePointGridGenerator我们将创建一个名为EllipsePointGridGenerator的类。它的职责很明确接收椭圆的参数和网格配置输出内部的点集。// EllipsePointGridGenerator.h #pragma once #include vector #include utility // for std::pair class EllipsePointGridGenerator { public: // 椭圆参数中心(cx, cy)半长轴a半短轴b EllipsePointGridGenerator(double centerX, double centerY, double semiMajorAxis, double semiMinorAxis); // 设置网格生成参数x和y方向的步长 void setGridStep(double stepX, double stepY); // 或者设置网格在x和y方向上的分割数量 void setGridResolution(int numPointsX, int numPointsY); // 核心方法生成点网格 const std::vectorstd::pairdouble, double generateGrid(); // 获取生成的点集 const std::vectorstd::pairdouble, double getPoints() const { return gridPoints; } // 获取实际生成的点的数量 size_t getNumberOfPoints() const { return gridPoints.size(); } // 清除已生成的点 void clear() { gridPoints.clear(); } private: double cx, cy; // 椭圆中心 double a, b; // 半长轴半短轴 (假设 a b 0) double stepX, stepY; // 网格步长 bool useStep; // 标志位true表示使用步长false表示使用分辨率 int resX, resY; // 网格分辨率点数 std::vectorstd::pairdouble, double gridPoints; // 存储生成的点 // 内部初始化函数 void initializeFromResolution(); // 核心判断函数点(x,y)是否在椭圆内部含边界 bool isPointInsideEllipse(double x, double y) const; };设计要点解析两种网格定义方式提供了setGridStep和setGridResolution两种方法。前者直接指定物理步长更直观后者指定每条轴上想要的大致点数会自动计算步长更方便控制点的总数。内部用useStep标志位来区分。数据存储使用std::vectorstd::pairdouble, double存储点。pair的first和second分别对应x和y坐标。选择vector是因为它支持动态内存、连续存储访问效率高。常量成员函数getPoints()和getNumberOfPoints()被声明为const表示它们不会修改对象状态可以在常量对象上调用也更安全。私有辅助函数将复杂的初始化逻辑和几何判断逻辑封装成私有函数保持generateGrid()主逻辑的清晰。3.2 核心实现细节与难点剖析让我们看看核心函数generateGrid()和关键辅助函数的实现。// EllipsePointGridGenerator.cpp #include EllipsePointGridGenerator.h #include cmath #include stdexcept EllipsePointGridGenerator::EllipsePointGridGenerator(double centerX, double centerY, double semiMajorAxis, double semiMinorAxis) : cx(centerX), cy(centerY), a(semiMajorAxis), b(semiMinorAxis), stepX(0.1), stepY(0.1), useStep(true), resX(20), resY(20) { // 默认值 if (a 0 || b 0) { throw std::invalid_argument(Semi-axes must be positive.); } if (a b) { // 确保a是长轴 std::swap(a, b); // 注意这里交换了a和b但椭圆的形状不变。如果业务逻辑对长短轴顺序敏感需要额外处理。 } } void EllipsePointGridGenerator::setGridStep(double stepX, double stepY) { if (stepX 0 || stepY 0) { throw std::invalid_argument(Grid step must be positive.); } this-stepX stepX; this-stepY stepY; this-useStep true; } void EllipsePointGridGenerator::setGridResolution(int numPointsX, int numPointsY) { if (numPointsX 0 || numPointsY 0) { throw std::invalid_argument(Grid resolution must be positive.); } this-resX numPointsX; this-resY numPointsY; this-useStep false; } void EllipsePointGridGenerator::initializeFromResolution() { // 根据分辨率计算步长总跨度除以点数-1确保边界点能被覆盖。 // 例如x方向从 cx-a 到 cxa总长度为 2a。 // 如果有 resX 个点则需要 (resX - 1) 个间隔。 stepX (2 * a) / (resX - 1); stepY (2 * b) / (resY - 1); } bool EllipsePointGridGenerator::isPointInsideEllipse(double x, double y) const { double dx x - cx; double dy y - cy; // 椭圆内部判断公式 (dx/a)^2 (dy/b)^2 1 // 为避免除法和多次平方通常转化为乘法判断 (dx*dx)/(a*a) (dy*dy)/(b*b) 1 // 更进一步两边乘以 a*a * b*b 以避免浮点除法 (dx*dx)*(b*b) (dy*dy)*(a*a) (a*a)*(b*b) // 但这里我们保留清晰的形式性能差异在大多数情况下可忽略。 double norm (dx*dx) / (a*a) (dy*dy) / (b*b); return norm 1.0; }关键点与避坑指南参数验证构造函数和设置函数中对半轴长、步长、分辨率进行了正数检查这是健壮性编程的基本要求。防止传入负数或零导致后续计算出现除零错误或逻辑混乱。长短轴处理在构造函数中我们通过std::swap(a, b)确保了a始终代表半长轴。这是一个重要的设计决策。它简化了内部判断逻辑公式中总是用a除x方向差b除y方向差。但这也意味着如果用户传入的semiMajorAxis实际上比semiMinorAxis小类内部会悄悄交换它们。这可能会是一个潜在的坑如果调用者后续需要知道原始的长短轴方向这个信息就丢失了。因此在更严格的实现中可以添加两个布尔成员变量来记录是否发生了交换或者干脆要求调用者必须传入正确的顺序并在文档中明确说明。浮点数比较在isPointInsideEllipse函数中我们使用 1.0来判断。这里涉及浮点数的精度问题。对于恰好落在边界上的点由于浮点误差norm可能略大于或略小于1。使用可以包含边界但如果你需要严格内部点可以使用 (1.0 - epsilon)其中epsilon是一个很小的数比如1e-12。在我们的应用场景网格生成中包含边界通常是可接受的。性能小优化注释中提到可以将判断公式两边乘以a*a*b*b来避免除法。除法操作通常比乘法慢。在需要生成数百万个点的极端性能场景下这个优化是值得的。我们可以预先计算a2 a*a,b2 b*b,a2b2 a2*b2然后判断dx*dx*b2 dy*dy*a2 a2b2。代码中保留了清晰的形式你可以根据实际需求决定是否优化。3.3 网格生成主循环generateGrid()的实现这是整个类最核心的部分。const std::vectorstd::pairdouble, double EllipsePointGridGenerator::generateGrid() { gridPoints.clear(); // 清除旧数据 gridPoints.reserve(static_castsize_t((2*a/stepX 1) * (2*b/stepY 1)) * 0.8); // 预分配内存0.8是经验系数 // 如果使用分辨率模式先计算步长 if (!useStep) { initializeFromResolution(); } // 计算遍历的边界。从中心向两边扩展确保覆盖整个外接矩形。 // 起点cx - a, cy - b // 终点cx a, cy b // 我们让循环包含终点使用 比较或者用整数循环控制。 // 更稳健的做法是计算起始点然后按步长循环用点的数量控制。 double startX cx - a; double startY cy - b; // 计算需要循环的次数。为了避免浮点累加误差我们使用整数计数。 // 点数 floor(长度 / 步长) 1 int numStepsX static_castint(std::floor((2 * a) / stepX)) 1; int numStepsY static_castint(std::floor((2 * b) / stepY)) 1; for (int i 0; i numStepsX; i) { double x startX i * stepX; // 防止最后一步因浮点误差超出范围进行钳制 if (i numStepsX - 1) { x cx a; // 确保终点正好是右边界 } for (int j 0; j numStepsY; j) { double y startY j * stepY; if (j numStepsY - 1) { y cy b; // 确保终点正好是上边界 } if (isPointInsideEllipse(x, y)) { gridPoints.emplace_back(x, y); // 使用emplace_back避免临时对象拷贝 } } } // 可选优化内存。如果预分配过多可以收缩内存。 // gridPoints.shrink_to_fit(); return gridPoints; }实现细节与经验之谈内存预分配在循环开始前我们使用reserve预分配了内存。这能显著提升性能避免vector在动态增长过程中多次重新分配和拷贝数据。估算公式(2*a/stepX 1) * (2*b/stepY 1) * 0.8是经验值。矩形内的总点数乘以一个小于1的系数因为椭圆面积约占矩形面积的78.5%即π/40.8是一个安全的估计。预分配稍多比反复分配要好。避免浮点累加误差这是关键技巧新手常写的循环是for (double x startX; x cx a epsilon; x stepX) { for (double y startY; y cy b epsilon; y stepY) { ... } }这种写法在步长不能整除范围时可能因为浮点精度问题导致少循环一次或多循环一次。我们采用整数计数循环先计算步数numSteps然后用整数i和j循环计算当前坐标x startX i * stepX。这保证了循环次数绝对正确。边界钳制在循环的最后一次迭代i numStepsX - 1我们手动将x设置为cx a。这是因为startX (numStepsX-1) * stepX由于浮点计算可能并不精确等于cx a手动赋值确保了边界点的精确性。使用emplace_back在向vector中添加点时使用emplace_back(x, y)直接在容器末尾构造pair对象比push_back(std::make_pair(x, y))效率稍高避免了临时对象的创建和移动/拷贝。循环顺序外层循环是x内层循环是y。这意味着点在内存中是按x方向连续存储的先固定x遍历所有y。这种顺序在某些后续处理如矩阵操作中可能更有优势因为它提高了内存访问的局部性。你也可以根据需求调整。4. 完整源码与使用示例将头文件和实现文件结合起来就是一个完整的可编译模块。下面提供一个简单的main.cpp来演示如何使用这个类并将生成的点输出到文件方便用其他工具如Python的Matplotlib可视化。// main.cpp #include EllipsePointGridGenerator.h #include iostream #include fstream #include iomanip int main() { try { // 示例1创建一个中心在(5,5)半长轴为4半短轴为2的椭圆 EllipsePointGridGenerator ellipseGen(5.0, 5.0, 4.0, 2.0); // 方法A设置网格步长 ellipseGen.setGridStep(0.2, 0.2); // 步长0.2 const auto points1 ellipseGen.generateGrid(); std::cout Using step size (0.2, 0.2): Generated points1.size() points.\n; // 输出到文件用于绘图 std::ofstream outFile1(ellipse_grid_step.txt); outFile1 std::fixed std::setprecision(6); for (const auto p : points1) { outFile1 p.first p.second \n; } outFile1.close(); // 示例2使用分辨率模式 ellipseGen.clear(); ellipseGen.setGridResolution(41, 21); // x方向41个点y方向21个点 const auto points2 ellipseGen.generateGrid(); std::cout Using resolution (41, 21): Generated points2.size() points.\n; std::ofstream outFile2(ellipse_grid_res.txt); outFile2 std::fixed std::setprecision(6); for (const auto p : points2) { outFile2 p.first p.second \n; } outFile2.close(); // 示例3处理异常输入 try { EllipsePointGridGenerator badEllipse(0, 0, -1, 2); // 负的半长轴 } catch (const std::invalid_argument e) { std::cerr Caught expected exception: e.what() std::endl; } } catch (const std::exception e) { std::cerr Error: e.what() std::endl; return 1; } return 0; }编译与运行你可以使用任何标准的C编译器如g, clang, MSVC进行编译。g -stdc11 -o ellipse_grid main.cpp EllipsePointGridGenerator.cpp ./ellipse_grid运行后会在当前目录生成两个数据文件ellipse_grid_step.txt和ellipse_grid_res.txt。可视化验证使用Python Matplotlib生成点之后眼见为实。我们可以用一个简单的Python脚本来绘制这些点验证它们确实落在椭圆内部。# plot_ellipse_grid.py import matplotlib.pyplot as plt import numpy as np # 读取数据 def read_points(filename): x, y [], [] with open(filename, r) as f: for line in f: if line.strip(): px, py map(float, line.split()) x.append(px) y.append(py) return np.array(x), np.array(y) # 椭圆参数 cx, cy 5.0, 5.0 a, b 4.0, 2.0 # 绘制椭圆轮廓用于对比 theta np.linspace(0, 2*np.pi, 200) ellipse_x cx a * np.cos(theta) ellipse_y cy b * np.sin(theta) fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 5)) # 绘制步长模式生成的点 x1, y1 read_points(ellipse_grid_step.txt) ax1.scatter(x1, y1, s5, cblue, alpha0.6, labelGrid Points) ax1.plot(ellipse_x, ellipse_y, r-, linewidth2, labelEllipse Boundary) ax1.set_xlabel(X) ax1.set_ylabel(Y) ax1.set_title(fGrid by Step (0.2, 0.2), {len(x1)} points) ax1.legend() ax1.axis(equal) ax1.grid(True, linestyle--, alpha0.5) # 绘制分辨率模式生成的点 x2, y2 read_points(ellipse_grid_res.txt) ax2.scatter(x2, y2, s5, cgreen, alpha0.6, labelGrid Points) ax2.plot(ellipse_x, ellipse_y, r-, linewidth2, labelEllipse Boundary) ax2.set_xlabel(X) ax2.set_ylabel(Y) ax2.set_title(fGrid by Resolution (41, 21), {len(x2)} points) ax2.legend() ax2.axis(equal) ax2.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.show()运行这个Python脚本你会看到两幅并排的图清晰展示出椭圆边界和内部均匀分布的点网格。这是验证算法正确性最直观的方式。5. 性能优化与高级话题基础的实现已经完成了但在实际项目中我们可能还需要考虑更多。5.1 性能瓶颈分析与优化对于这个算法主要的计算开销在于两层嵌套循环中的isPointInsideEllipse判断。假设网格有M x N个候选点那么判断函数就被调用M x N次。优化策略1减少无效判断我们是在椭圆的外接矩形内遍历但椭圆只占矩形面积的约78.5%。我们可以通过更精确地计算每一行或每一列的x或y范围来减少循环次数。对于给定的y椭圆在x方向上的范围是[cx - a*sqrt(1 - ((y-cy)/b)²), cx a*sqrt(1 - ((y-cy)/b)²)]。我们可以先循环y对每个y计算x的起止范围然后只在这个范围内遍历x。这样能避免在左右两侧空白区域的计算。但代价是每次y循环都需要计算一次平方根对于密集网格这可能比多做几次乘法判断更慢需要实际测试。优化策略2SIMD向量化现代CPU支持SIMD指令可以同时对多个数据进行相同的操作。我们可以将判断条件(dx/a)² (dy/b)² 1向量化。例如使用SSE或AVX指令集一次处理4个或8个点。但这会大大增加代码的复杂度和降低可移植性通常只在性能极其关键的场景如实时图形渲染中才需要考虑。优化策略3并行化两层循环是典型的可并行化结构。我们可以使用OpenMP、C标准库的execution策略或者手动线程池将y方向的循环分割成多个块并行处理。这是提升多核CPU利用率非常有效的手段。一个简单的OpenMP并行化示例#include omp.h // ... 在generateGrid函数的内层循环前 #pragma omp parallel for collapse(2) schedule(dynamic) for (int i 0; i numStepsX; i) { for (int j 0; j numStepsY; j) { // ... 计算x,y并判断 #pragma omp critical if (isPointInsideEllipse(x, y)) { gridPoints.emplace_back(x, y); } } }注意这里使用了#pragma omp critical来保护对gridPoints的并发写入但这会成为性能瓶颈。更好的做法是每个线程使用一个本地vector存储结果最后再合并。对于绝大多数应用基础的整数计数循环版本已经足够快。优化前务必进行性能剖析找到真正的热点。5.2 扩展到旋转椭圆我们的实现假设椭圆的轴与坐标轴平行。如果椭圆是旋转的判断公式需要改变。一个旋转了角度θ的椭圆其内部点(x, y)满足[ ( (x-cx)cosθ (y-cy)sinθ ) / a ]² [ ( -(x-cx)sinθ (y-cy)cosθ ) / b ]² 1这相当于将点坐标平移后旋转-θ角度再应用标准椭圆公式。我们可以在EllipsePointGridGenerator类中增加一个旋转角度成员angle并修改isPointInsideEllipse函数。同时遍历的边界矩形也需要相应扩大因为它不再是轴对齐的而是需要包含旋转后的椭圆其边界是[cx - R, cx R]和[cy - R, cy R]其中R sqrt((a cosθ)² (b sinθ)²)这是一个近似更精确需要计算旋转后的AABB。5.3 生成非均匀网格或随机点有时我们不需要均匀网格而是需要随机分布的点。这可以通过蒙特卡洛方法实现在椭圆的外接矩形内随机生成点(x, y)。用isPointInsideEllipse判断。保留内部的点直到数量达到要求。 这种方法简单但效率较低接受率约78.5%。更高效的方法是利用椭圆的变换性质在单位圆内生成随机点例如通过拒绝采样或在极坐标下生成然后通过缩放、旋转、平移变换到目标椭圆。6. 常见问题与调试技巧在实际使用中你可能会遇到一些典型问题。这里我总结了一份排查清单。问题现象可能原因解决方案生成的点数远少于预期步长 (stepX,stepY) 设置得太大。减小步长值。检查setGridStep或setGridResolution的参数。生成的点数远多于预期/程序很慢步长设置得太小导致网格过于密集。增大步长。对于预览步长设为椭圆半轴的1/10到1/20通常足够。椭圆形状不对例如看起来像圆半长轴a和半短轴b的值设置错误或顺序不对。确认a和b的值。回忆我们的实现会强制交换使a b确保你理解这个行为。椭圆位置不对中心坐标(cx, cy)设置错误。检查传入的中心坐标值。边界上的点缺失或多余浮点数精度问题导致边界判断不准确。检查isPointInsideEllipse函数中的比较运算符 (vs)。考虑引入一个微小的容差epsilon如norm 1.0 1e-12。程序崩溃访问违规在调用generateGrid()或getPoints()之前未正确初始化步长或分辨率。确保在调用generateGrid()之前至少调用过一次setGridStep或setGridResolution。可以在构造函数中设置默认步长。输出文件为空文件路径错误或程序没有写入权限。generateGrid()可能因为异常提前返回。检查输出文件路径。在main函数中添加更详细的日志或异常捕获。确保椭圆参数是正数。点在椭圆外部可视化发现isPointInsideEllipse公式实现错误。椭圆参数a,b与公式中的a,b概念混淆。用简单的点手动验证中心点(cx, cy)应该在椭圆内点(cxa, cy)应该在椭圆边界上。检查公式是否为((x-cx)/a)² ((y-cy)/b)² 1。调试技巧单元测试为isPointInsideEllipse函数编写简单的测试用例验证中心点、轴端点、外部点是否正确。小规模验证先用一个大的步长如a/2生成很少的点打印出来手动计算验证。可视化正如上面所做将点输出并绘图是最直观的调试方法。检查默认值如果你没有调用setGridStep或setGridResolution类内部的步长是默认的(0.1, 0.1)。对于半轴为几十或几百的大椭圆这个步长会生成海量的点导致程序看似“卡住”。始终记得设置合适的步长或分辨率。7. 源码的工程化扩展作为一个可复用的模块我们还可以从工程角度进行增强模板化支持不同数据类型当前的代码使用double。对于某些精度要求不高的场景使用float可能更快且节省内存。可以将类模板化如template typename T class EllipsePointGridGenerator。支持多种输出格式除了返回vectorpair还可以提供接口将点集输出为std::vectordouble交错存储x,y或者直接写入特定的网格文件格式。添加迭代器支持对于非常大的网格一次性生成所有点可能消耗大量内存。可以实现一个迭代器每次调用返回下一个点支持惰性求值。集成到图形库如果你在使用Qt、OpenGL等图形库可以添加方法直接将生成的点转换为库所需的顶点缓冲区数据。实现这个2D椭圆点网格生成器就像搭积木一样从最核心的数学判断出发逐步构建出健壮、易用的代码。理解每一行代码背后的意图和潜在的陷阱远比复制粘贴代码更重要。希望这份详细的拆解和附带的源码能成为你解决类似空间采样问题的一块坚实基石。在实际项目中根据具体需求灵活调整和优化才是工程师价值的体现。