做 WebGIS、图形学或者干脆就是写 CAD 插件的朋友,应该都遇到过这么个需求:手里只有一串多边形顶点坐标,想快速算出面积。如果你当场去搜“多边形面积 算法”,大概率会看到一个叫“鞋带公式”的东西。我第一次看到这个名字时还以为是玩笑,后来发现它在工程里是真的能打——复杂度 O(n),C 语言十几行就能写完,对凹多边形照样有效,甚至还能顺手判断顶点绕向。这篇我就围绕多边形面积求和的鞋带公式,从它名字的来历、数学推导,到 C/C++ 落地实现,再把实际踩过的坑都翻出来讲一遍。无论你是刚接触几何算法的学生,还是已经在写业务代码的工程师,看完都应该能把这段逻辑稳稳写进自己的工具库。
1. 鞋带公式到底是什么:一次循环求和多边形面积
1.1 “鞋带”这个名字,来自交叉相乘的写法
鞋带公式的英文是 Shoelace Formula,也常叫 Gauss's Area Formula。为什么叫“鞋带”?你把多边形的顶点坐标按顺序排成两列,x 坐标一列,y 坐标一列,然后把第一行的 x 和第二行 y 交叉相乘、再按下标“z 字形”连线,所有交叉线围起来的图案看起来特别像一根来回穿插的鞋带。我当年看到这个示意图才明白,原来不是公式本身的形状像鞋带,是中间那个“交叉相乘再求和”的连线方式像。
这个公式专门用来计算平面坐标给出的简单多边形的面积。所谓简单多边形,就是边不交叉、不自交的多边形,包括凸多边形和凹多边形。你只需要知道每个顶点的坐标,不需要做三角剖分,也不需要知道这个多边形到底是什么形状,一次遍历求和,再除以 2,面积就出来了。实际使用的时候,顶点顺序是有讲究的:按顺时针或者逆时针排好,公式都能算,但是结果的正负号会不同,所以一般会取绝对值。
1.2 公式长什么样,以及两种等价写法
设多边形有 n 个顶点,坐标依次是:
[
(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)
]
并且约定第 n 个顶点后面接第一个顶点,也就是 (x_{n+1} = x_1),(y_{n+1} = y_1)。那么鞋带公式的常用写法是:
[
S = \frac{1}{2} \left|\sum_{i=1}^{n} (x_i y_{i+1} - x_{i+1} y_i)\right|
]
另一种更对称的写法是:
[
S = \frac{1}{2} \left|(x_1 y_2 + x_2 y_3 + \cdots + x_n y_1) - (y_1 x_2 + y_2 x_3 + \cdots + y_n x_1)\right|
]
我个人更喜欢第一种写法,因为它跟代码里的累加过程一一对应:每条边相邻两个点做一次叉积,累加,最后取绝对值除以 2。第二种写法适合口算和手推,因为拆成两排求和之后,一眼就能看出“交叉相乘”这个说法的来源。
1.3 先手算一个三角形找找手感
纸上得来终觉浅,手算一次最直观。拿一个最简单的直角三角形,顶点分别是 ((0,0))、((4,0))、((0,3))。套公式:
[
\begin{aligned}
sum &= (0 \times 0 - 4 \times 0) + (4 \times 3 - 0 \times 0) + (0 \times 0 - 0 \times 3) \
&= 0 + 12 + 0 \
&= 12
\end{aligned}
]
面积就是 (12 / 2 = 6),跟直角三角形面积公式算出来的一致。如果把顶点顺序改成 ((0,0))、((0,3))、((4,0)),也就是反过来绕一圈,算出来的 sum 是 -12,绝对值仍然是 6。这个符号正是顶点绕向的体现,后面我会专门讲它有什么用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C语言和C++落地方案:十几行代码就能收工
2.1 C语言版:double数组版实现
先给一个最标准的 C 语言实现。注意一个约定:传入的顶点数组是不闭合的,也就是三角形传 3 个点,四边形传 4 个点,最后一个点的下一个点用取模运算回到第一个点。
c复制#include <math.h>
double polygon_area(const double x[], const double y[], int n)
{
double sum = 0.0;
int i, j;
for (i = 0; i < n; i++) {
j = (i + 1) % n;
sum += x[i] * y[j] - x[j] * y[i];
}
return fabs(sum) * 0.5;
}
这段代码的重点在 j = (i + 1) % n。% 让最后一个顶点自动跟第一个顶点配对,省掉单独处理边界的麻烦。如果你的多边形有 4 个点,那么循环里实际算的是边 0-1、1-2、2-3、3-0,四条边的叉积和。时间复杂度 O(n),内存占用也是 O(1),只多了两个指针变量。
实际施工的时候,x 数组和 y 数组往往不会单独存在,而是藏在结构体或者更复杂的链表里。不过算法本身不关心存储方式,只要你能按顺序取出第 i 个和第 i+1 个顶点坐标就行。
2.2 C++容器版:vector与pair一起用
用 C++ 写,我更习惯把点塞进 std::pair 或者自定义结构体,配合 std::vector 存动态顶点列表:
cpp复制#include <vector>
#include <cmath>
#include <utility>
#include <stdexcept>
double polygonArea(const std::vector<std::pair<double, double>>& pts)
{
if (pts.size() < 3) {
throw std::invalid_argument("polygon must have at least 3 vertices");
}
double sum = 0.0;
int n = static_cast<int>(pts.size());
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
sum += pts[i].first * pts[j].second
- pts[j].first * pts[i].second;
}
return std::fabs(sum) * 0.5;
}
这里有个细节:pts[i].first * pts[j].second - pts[j].first * pts[i].second,与公式里的 (x_i y_{i+1} - x_{i+1} y_i) 完全对应。有人写代码时会把前后两项写反,结果是整个 sum 变成相反数,由于最后取了绝对值,面积仍然正确,但如果你后面还要用带符号面积判断绕向,写反了就会把方向判断搞错。所以写 C++ 时建议保持跟公式完全一致的顺序,顺手还能省一个取反操作。
pts.size() < 3 这个防御性检查不能省。我见过不少输入空数组直接崩溃的案例,两行代码就能挡掉的事儿,别偷懒。
2.3 整数坐标版本:别把半面积直接整除
工程里经常遇到坐标是整数的场景,比如栅格地图、像素边界、整数网格上的多边形。这时候可以用 long long 累加,避免 double 在大量中间计算时引入不必要的误差:
c复制#include <stdlib.h>
double polygon_area_ll(const long long x[], const long long y[], int n)
{
long long sum = 0;
int i, j;
for (i = 0; i < n; i++) {
j = (i + 1) % n;
sum += x[i] * y[j] - x[j] * y[i];
}
return (double)llabs(sum) * 0.5;
}
一个最容易踩的坑是:以为整数坐标算出来的面积一定是整数,然后用 llabs(sum) / 2 去整除。不对。比如三角形 ((0,0))、((1,0))、((0,1)),sum 是 1,面积是 0.5。直接整除会得到 0。原因很简单,鞋带公式的 sum 实际上是“两倍带符号面积”,而两倍面积可能是奇数。所以稳妥的做法是先转成 double,再乘以 0.5。如果你的下游逻辑只想要整数面积,也要单独定义清楚“0.5 怎么四舍五入”,不要依赖整除。
3. 别再背公式:交叉相乘为什么就是面积
3.1 每次叉积都是原点三角形面积
很多人会用鞋带公式,但说不清为什么。其实核心就一句话:每一条边和坐标原点能组成一个带符号的三角形,把这些带符号三角形的面积全部加起来,就是多边形的面积。
具体看相邻两个顶点 (P_i = (x_i, y_i)) 和 (P_{i+1} = (x_{i+1}, y_{i+1}))。向量 (\overrightarrow{OP_i}) 和 (\overrightarrow{OP_{i+1}}) 的叉积模长正好是这两个向量张成的平行四边形面积,除以 2 就是三角形 (O P_i P_{i+1}) 的面积,而叉积的数值就是:
[
x_i y_{i+1} - x_{i+1} y_i
]
这个值的正负取决于原点在这条边的哪一侧。你把每条边都这样处理一遍,得到 n 个有正有负的三角形面积,相加之后,多边形外面的部分会因为符号相反而被抵消,剩下的就是多边形本身的带符号面积。
这个解释听起来有点像“从原点往各个顶点拉线,把多边形切成三角形”。如果你把多边形想象成一块披萨,原点在圆心,每条边跟圆心连线围出来的“三角形扇”确实可以铺满整个披萨。只不过当原点跑到多边形外面时,有些扇区是负的,整体一变,正好用负面积把不属于多边形的部分消掉。
3.2 有向面积的正负号与顶点绕向
上面提到叉积有符号,所以鞋带公式不取绝对值时,算出的“带符号面积”本身就携带方向信息。在标准坐标系里,x 轴向右、y 轴向上,如果你按逆时针方向排列顶点,sum 是正数;按顺时针排列,sum 是负数。
判断绕向有什么用?用处多了。图形学里做背面剔除时,三角形绕向直接决定法线朝外还是朝内;GIS 里处理多边形边界时,外环和内环往往要求相反绕向,否则空间关系会乱。你完全可以用鞋带公式算一次带符号面积,替代单独的绕向判断函数。代码都是一样的一段循环,区别只是最后不取绝对值。
如果只需要面积,那就取绝对值。如果还需要判断绕向,建议保留符号,不要再写第二套逻辑。这个“一鱼两吃”的技巧,很多三维引擎的早期代码里都能看到。
3.3 凹多边形和L形案例:正负抵消自动成立
新手最常见的一个疑问是:鞋带公式只能用在凸多边形吧?不是。凹多边形照样用,因为凹进去的部分会形成反向的负面积,正好把“不该算”的那块抵消掉。
用 L 形多边形验证一下。坐标按顺序取 ((0,0))、((2,0))、((2,1))、((1,1))、((1,2))、((0,2))。这是一个边长为 2 的正方形去掉右上角 1×1 小正方形,面积应该是 3。套鞋带公式:
[
\begin{aligned}
sum &= (0 \times 0 - 2 \times 0) + (2 \times 1 - 2 \times 0) + (2 \times 1 - 1 \times 1) \
&+ (1 \times 2 - 1 \times 1) + (1 \times 2 - 0 \times 2) + (0 \times 0 - 0 \times 2) \
&= 0 + 2 + 1 + 1 + 2 + 0 \
&= 6
\end{aligned}
]
面积 (6 / 2 = 3),完全正确。整个计算过程里,凹角所在的边贡献了正负不同的叉积分量,但最终抵消得干干净净。所以你不需要做任何预处理,凹多边形可以直接交给鞋带公式。
4. 实操踩坑合集:从闭合数组到浮点误差都要防
4.1 首尾重复的顶点数组,直接套公式会出问题
很多图形库和文件格式喜欢把多边形存成“闭合环”,也就是把第一个顶点在结尾再复制一遍。比如矩形存成 ((0,0), (4,0), (4,3), (0,3), (0,0)),数组长度是 5,但实际顶点是 4 个。
如果你直接把这个长度为 5 的数组传给上面的 polygon_area,n 就是 5,循环会多算一条从 ((0,3)) 到 ((0,0)) 的边,然后又算一条从 ((0,0)) 到 ((0,0)) 的零长度边。结果大概率不是错得离谱,而是会多算或漏算,具体取决于你把闭合点放在哪段。反正就是不对。
处理方式有两种:要么在调用前把最后一个重复点剔除,要么把函数改成自动识别“首尾相同时 n 减 1”。我一般写一个封装:
c复制double polygon_area_closed(const double x[], const double y[], int n)
{
if (n >= 2 && x[0] == x[n - 1] && y[0] == y[n - 1]) {
n--;
}
return polygon_area(x, y, n);
}
这个封装放在工具库里很实用,因为你没法保证每个上游接口都把数据格式清理干净。
4.2 大坐标导致精度丢失,先平移再算
鞋带公式看起来只是加法和乘法,但遇到超大坐标时,浮点精度会给你上一课。比如说 GIS 里常见的投影坐标,x、y 动辄几百万米,多边形面积可能只有几十平方米。这个时候 x[i] * y[j] - x[j] * y[i] 每一项的值都在十亿甚至万亿级别,而最终面积却很小,累加过程中会发生严重的对消,最后几位有效数字基本丢光了。
怎么避免?利用一个几何性质:平移坐标系不改变多边形面积。我先算一个基准点,比如所有顶点的平均坐标或者直接取第一个顶点,然后把每个顶点减去基准点再代入公式。这样乘积从“百万 × 百万”直接缩到“几十 × 几十”,有效数字保住了。
c复制double polygon_area_centered(const double x[], const double y[], int n)
{
double cx = x[0], cy = y[0];
double sum = 0.0;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
double dx1 = x[i] - cx;
double dy1 = y[i] - cy;
double dx2 = x[j] - cx;
double dy2 = y[j] - cy;
sum += dx1 * dy2 - dx2 * dy1;
}
return fabs(sum) * 0.5;
}
这个版本在生产环境里更稳。我在实际项目里遇到过同样一块地,用原始坐标算出来的面积跟用平移后坐标算出来的面积差了零点几平方米,排查半天才发现是浮点精度问题。后来所有面积计算一律先平移。
4.3 自交多边形与退化多边形要提早识别
鞋带公式的数学前提是简单多边形。如果多边形出现自交,比如画成了一个“8”字或者“沙漏”形,公式不会报错,但算出来的是一个代数抵消后的“净面积”,跟肉眼看到的区域面积根本不是一回事。
我拿一个极端例子:顶点 ((0,0))、((1,1))、((0,1))、((1,0)),这是一个自交的沙漏形。代入公式 sum 正好是 0,面积也算出 0。图形明明占了很大一片视觉范围,结果却是 0,如果拿去算地价、算材料用量,后果不堪设想。
所以在上游数据不可控的时候,最好提前检测多边形是否自交。检测方法有很多种:线段两两求交、检查所有边是否有非相邻交点。如果检测到自交,就得先做多边形分割或者直接拒绝计算。另外还有一种退化情况:三个及以上顶点共线,或者所有顶点在一条直线上。这种多边形面积应该为 0,鞋带公式也不会报错,但要注意别把 0 当成异常输入丢掉,有时候上游确实会传这种“扁掉”的多边形。
4.4 常见问题速查表
| 现象 | 原因 | 解决方式 |
|---|---|---|
| 结果比预期大 | 传入闭合数组,多算了一条边 | 检测首尾顶点相同并 n-1 |
| 结果比预期小 | 多边形自交,正负面积抵消 | 先做自交检测或分割 |
| 面积有时出现 0.5 误差 | 整数坐标直接整除 | 用 (double)llabs(sum) * 0.5 |
| 大坐标下结果不稳定 | 乘积量级过大,浮点对消 | 先整体平移到原点附近再算 |
| 面积正确但绕向相反 | 顶点顺序反了 | 带符号面积取负值即可 |
| 运行时崩溃 | 顶点数小于 3 | 函数入口加 n >= 3 防御 |
这张表我建议直接贴在代码注释里。很多几何算法的 bug 都不是算法复杂度高,而是这些边界情况没处理干净。
5. 应用场景延伸:从测绘图到工程里的面积计算
5.1 梯形面积公式与鞋带公式的对照
热词里突然冒出一个“求梯形的面积 C++”,可能有人会觉得奇怪:梯形面积不是初中就学过了吗,跟鞋带公式有什么关系?其实梯形的面积公式就是鞋带公式的一个特例。你可以取一个直角梯形,四个顶点是 ((0,0))、((a,0))、((b,h))、((0,h)),代入鞋带公式:
[
\begin{aligned}
sum &= (0 \times 0 - a \times 0) + (a \times h - b \times 0) + (b \times h - 0 \times h) + (0 \times 0 - 0 \times 0) \
&= ah + bh \
&= h(a + b)
\end{aligned}
]
面积就是 (sum / 2 = (a+b)h / 2),跟梯形面积公式一模一样。也就是说,你用 C++ 写一个鞋带公式的函数,顺手就能当万能面积计算器用,梯形、矩形、三角形都是同一套代码。真正到了工程项目里,凡是涉及任意多边形的面积统计,调用鞋带公式往往比记一堆特例公式更省心。
5.2 铺铜多边形、洁净区布点等面积前置计算
“面积”这个概念在很多看似不相关的行业里,其实是同一个几何问题。PCB 设计软件里的多边形铺铜区域,形状经常是多边形甚至带大量圆弧边,但估算铜箔面积时,底层离不开叉积求和这类算法;洁净环境监测里常见的采样点数量估算,有一种简便做法是用房间面积 A 开方作为参考布点数,这里的前提同样是先把不规则房间的平面多边形面积算准。这些场景的共同点是:图形不规则、无法直接套矩形或梯形公式,但手里恰好有一份顶点坐标。
在这些工程场景里,鞋带公式的价值不仅仅是省事,而是稳定。你不需要理解整个图形的拓扑结构,只需要知道边界顶点,面积就出来了。等面积拿到手,后面的单位换算、经验公式、成本估算,全都建立在同一个可靠的数字上。
5.3 用带符号面积判断绕向,顺手解决拓扑问题
最后说一个我经常用的衍生功能:判断环的方向。在 GIS 或者建筑平面图里,一个带孔洞的面往往由外环和内环组成,外环通常是逆时针,内环是顺时针,然而不同数据标准可能相反。如果不知道当前环的方向,后续的拓扑处理很容易出乱子。
只要把鞋带公式的返回值去掉绝对值,看看正负号就能确定方向。符号判断不需要额外写算法,不需要对每条边算角度,一次遍历顺手就出来了。而且一旦你确定方向,还可以做规范化:把所有内环统一转成与外环相反的方向,这样在处理布尔运算、缓冲区时,才不容易出现“内外不分”的问题。用鞋带公式统一环方向,是性价比极高的做法。
最后再分享一个我自己的习惯:不管代码写得多顺手,面积函数里永远保留带符号版本和取绝对值版本,至少留一个内部参数控制。因为“只要面积数值”和“既看面积也看方向”是两种需求,临时改函数签名很烦,多一个开关,后面几乎不用再动这个工具函数。实测下来,这套思路从测绘数据到图形编辑器里的批量统计,都能稳稳跑通。
