VC 维是衡量函数类的复杂度的一种方式,通过评估函数类中函数的弯曲程度实现。WIKI上的解释是:空间中的点在经过排列之后,能够被模型f打散(shatter)的最大数量。
通过$y=a_0+a_1^Tx$将平面分割为两部分,如果满足平面中任意N个点(无论如何取值)总能被一条直线分开,而N+1个点却不行,则称该函数情况下的VC维为N。

举个无穷的VC维的例子:

从这两个例子,可以看出VC维刻画了函数的弯曲程度,越弯曲其VC维越大。
VC 维是衡量函数类的复杂度的一种方式,通过评估函数类中函数的弯曲程度实现。WIKI上的解释是:空间中的点在经过排列之后,能够被模型f打散(shatter)的最大数量。
通过$y=a_0+a_1^Tx$将平面分割为两部分,如果满足平面中任意N个点(无论如何取值)总能被一条直线分开,而N+1个点却不行,则称该函数情况下的VC维为N。

举个无穷的VC维的例子:

从这两个例子,可以看出VC维刻画了函数的弯曲程度,越弯曲其VC维越大。
https://www.cnblogs.com/pinard/p/6053344.html
https://blog.csdn.net/a857553315/article/details/95620881
如果瞎猜中的概率与特征选择出来的概率相差无几,那么就可以放弃该特征了。特征选择的标准是信息增益或信息增益比。
ID3算法与信息增益:(木有剪枝,只能处理离散数据)
得知特征X的信息而使输出的分类Y的不确定性减少的程度。
条件熵:$H(Y|X)=\sum_{n}^{i=1}p_iH(Y|X=x_i)$
信息增益:$g(D,A)=H(D)-H(D|A)$,D是数据集,A是特征。
(1)计算数据集D的经验熵:
$H(D)=-\sum_{k=1}^{K}\frac{|C_k|}{|D|}log_2\frac{|C_k|}{|D|}$ ,k为第一级特征{纹理,色泽,触感}
(2)计算条件熵:
$H(D|A)=\sum{i=1}^{n}\frac{|D_i|}{D}H(D_i)=\sum{i=1}^{n}\frac{|Di|}{|D|}\sum{k=1}^{K}\frac{|D{ik}|}{D_i}log_2\frac{|D{ik}|}{|D_i|}$
i为第一级特征{ 纹理 / 色泽 / 触感 },ik为纹理中的第二级特征{ 清晰,模糊,稍糊 }
(3)$g(D,A)=H(D)-H(D|A)$,越大越好。
C4.5算法与增益率(后剪枝,可以处理连续数据—>多叉树,特征要计算排序)
$Gainratio(D,a)=\frac{Gain(D,a)}{IV(a)}$
固有值:$IV(a)=-\sum_{v=1}^{V}\frac{|D_v|}{|D|}log_2\frac{|D_v|}{|D|}$ ,Dv是第一级特征a下的第二级特征,固有值随V的个数增加而增加。
CART决策树(减少log的使用降低计算量,还可以用于回归,二叉树)
使用Gini系数替代ID3里的熵,Gini系数越小越均衡(被错分的概率低),说明该样本只属于同一类的概率(纯度)越高。
$Gini(D)=\sum{k=1}^{y}\sum{k’ \ne k}pk*p_k’=1-\sum{k=1}^{y}p_k^2$
pk表示选中的样本属于k类别的概率,则这个样本被分错的概率是(1-pk)
基尼指数(基尼不纯度)= 样本被选中的概率 * 样本被分错的概率
预剪枝使得很多分支没有展开,这不仅降低了过拟合的风险,还显著减少了决策树的训练时间开销和测试时间。但是,有些分支虽当前不能提升泛化性。甚至可能导致泛化性暂时降低,但在其基础上进行后续划分却有可能导致显著提高,因此预剪枝的这种贪心本质,给决策树带来了欠拟合的风险。
后剪枝通常比预剪枝保留更多的分支,其欠拟合风险很小,因此后剪枝的泛化性能往往优于预剪枝决策树。但后剪枝过程是从底往上裁剪,因此其训练时间开销比前剪枝要大。
连续(非离散)特征可以将特征值从小到大排列然后取
按照 $T_a$ 进行划分 { - ,+ },从而得到该情况下的信息增益。
(1)如何在属性值缺失的情况下进行划分属性的选择?(创建决策树的时候怎么计算缺失值存在下的信息增益,选择正确的属性)
(2)给定划分属性,若样本在该属性上的值是缺失的,那么该如何对这个样本进行划分?(在分类过程中有缺失值的话怎么进行划分)
无缺失样本所占比例:$p=\frac{ \sum{x \in \tilde{D}}w_x}{ \sum{x \in D} wx}$
无缺失样本中第k类所占比例:$\tilde{p}_k=\frac{ \sum{x \in \tilde{D}k}w_x}{ \sum{x \in \tilde{D}} wx}$
无缺失样本中在特征a上取值为$a_v$的样本所占比例:$\tilde{r}_v=\frac{ \sum{x \in \tilde{D}k}w_x}{ \sum{x \in \tilde{D}} w_x}$
最后得到了推广了的公式:
多变量决策树:
一般的决策树分类边界由若干个与坐标轴平行的分段组成:
判断过程:密度 -> 含糖率 -> 密度 -> 含糖率…

多变量决策树有d个属性对应d维空间的一个数据点,对样本分类表示在坐标空间中找到不同样本之间的分类边界。

“多变量决策树”能实现斜的划分边界,使决策树模型简化。在多变量决策树的学习过程中,不是为非叶结点寻找最优划分属性,而是试图建立合适的线性分类器:

可以通过最小二乘或者嵌入神经网络进一步优化。
增量学习:根据新样本可对学得的模型进行调整适应当前形势,而不需要重新训练。如ID4,ID5R还有ITI
和熵模型的度量方式比,基尼系数对应的误差有多大呢?对于二类分类,基尼系数和熵之半的曲线如下:
1 | Gini = 2 * p * (1-p) |

1 |
|
从上图可以看出,基尼系数和熵之半的曲线非常接近,因此,基尼系数可以做为熵模型的一个近似替代。而CART分类树算法就是使用的基尼系数来选择决策树的特征。为了进一步简化,CART分类树算法每次仅仅对某个特征的值进行二分,而不是多分,这样CART分类树算法建立起来的是二叉树,而不是多叉树。
有N个传教士和N个野人来到河边渡河,河岸有一条船,每次至多可供k人乘渡。河两岸以及船上的野人数目总是不超过传教士的数目(否则不安全,传教士有可能被野人吃掉)。即求解传教士和野人从左岸全部摆渡到右岸的过程中,任何时刻满足M(传教士数)≥C(野人数)和M+C≤k的摆渡方案。以下讨论三个传教士三个野人还有一条船最多能载两个人的方案。
状态空间
我们用一个三元组(m,c,b)来表示河岸上的状态,其中m、c分别代表某一岸上传教士与野人的数目,b=1表示船在这一岸,b=0则表示船不在
约束条件是: 两岸上M≥C, 船上M+C≤2
(mi,ci)为船上的传教士和野人数量
左岸初态为(3,3,1),终态为(0,0,0)

为什么不能直接暴力穷举然后剪枝?
因为可能运过去两个人,然后又把同样的两个人运回来了(或者使用别的形式但是依旧是空转),所以要杜绝这种可能,当然最好的方法就是使用带有记忆的状态,如果之前遇到过这种状态那就拒绝执行return。
所以….如何实现去重的目标???
答:set
1 | #include<iostream> |
评估函数的建立:
M 表示左岸的传教士的人数,N 表示左岸野人的数目,B 取值为0或1 ,1 表示船在左岸,0 表示船在右岸。d 表示节点的深度。
我们分两种情况考虑。
(1)先考虑船在左岸的情况。如果不考虑限制条件,也就是说,船一次可以将三人从左岸运到右岸,然后再有一个人将船送回来。这样,船一个来回可以运过河2人,而船仍然在左岸。而最后剩下的三个人,则可以一次将他们全部从左岸运到右岸。所以,在不考虑限制条件的情况下,也至少需要摆渡[(M+N-3)/2]*2+1次。其中分子上的”-3”表示剩下三个留待最后一次运过去。除以”2”是因为一个来回可以运过去2人,需要[(M+N-3)/2]个来回,而”来回”数不能是小数,需要向上取整,这个用符号[ ]表示。而乘以”2”是因为一个来回相当于两次摆渡,所以要乘以2。而最后的”+1”,则表示将剩下的3个运过去,需要一次摆渡。
化简得:需要 M+N-2次单向摆渡
(2)再考虑船在右岸的情况。同样不考虑限制条件。船在右岸,需要一个人将船运到左岸。因此对于状态(M,N,0)来说,其所需要的最少摆渡数,相当于船在左岸时状态(M+1,N,1)或(M,N+1,1)所需要的最少摆渡数,再加上第一次将船从右岸送到左岸的一次摆渡数。因此所需要的最少摆渡数为:(M+N+1)-2+1。其中(M+N+1)的”+1”表示送船回到左岸的那个人,而最后边的”+1”,表示送船到左岸时的一次摆渡。
化简有:(M+N+1)-2+1=M+N。
综合船在左岸和船在右岸两种情况下,所需要的最少摆渡次数用一个式子表示为:$M+N-2B$ ,其中B=1表示船在左岸,B=0表示船在右岸。该摆渡次数是在不考虑限制条件下,推出的最少所需要的摆渡次数。
鸣谢:
https://www.jianshu.com/p/0eaea4cc5619
https://blog.csdn.net/tanrui519521/article/details/80980135
https://zhuanlan.zhihu.com/p/24367771
删除:https://www.cnblogs.com/tongy0/p/5460623.html
红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
性能优于AVL树,C++中的map,以及Java中的TreeMap,TreeSet, Java8中的HashMap的实现也采用了红黑树。
5条基本特征:
(1)每个结点要么是红的要么是黑的。
(2)根结点是黑的。
(3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
(4)如果一个结点是红的,那么它的两个儿子都是黑的。
(5)对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

树的插入:(警惕红)
新插入的节点总是设为红色的,所以如果父节点为黑色,就不需要修复,因为没有任何性质被改变,所以只有在父节点为红色节点时需要做修复操作。
可以肯定调节的时候该点一定有grandparent,因为根节点为黑,连续两个红才需要调整。
则将parent,uncle改为黑,pParent改为红,然后把pParent当成cur,继续向上调整。
对策 :把父节点和叔叔节点变黑,爷爷节点涂红,然后把当前节点指针给到爷爷,让爷爷节点那层继续循环,接受红黑树特性检测。


对策:当前节点的父节点作为新的当前节点,以新当前指点为支点左旋
parent为pParent的左孩子,cur为parent的左孩子,则进行右单旋转。

对策: 父节点变黑,祖父变红,以祖父节点为支点右旋
1为当前点,祖父节点为下一轮遍历的当前节点。

4为当前点,取初始父节点3为下一轮遍历的当前节点。然后交换一下3和4,然后再重组

3为当前点,取父亲为下一轮遍历的当前节点。

1 | #include<iostream> |
树的删除:(警惕黑)
在红黑树中,删除一个节点往大的说,只有以下四种情况。
情况一:删除的节点的左、右子树都非空;
情况二:删除的节点的左子树为空树,右子树非空;
情况三:删除的节点的右子树为空树,左子树非空;
情况四:删除的节点的左、右子树都为空树;
其中情况一,可以按与其他二叉搜索树的删除方式一样处理,最终可以转换到后面的三种情况。具体为:找到(Old)D节点的直接后继节点(暂且称为X节点),然后将X的值转移到D节点,最后将X节点作为真正要被删除掉的节点(即:(Real)D节点)。这样删除操作后,可以保证该树仍然为一棵二叉搜索树。但这样删除(Real)D节点后,可能会破坏红黑树的性质。所以需要额外做一些调整处理,这便是下面将要详细讨论的内容。
说明:下文中所提到的D,除非有特别说明,否则都将指的是(Real)D(一般可以认为该RealD节点的左子树为空,OldD不一定),最终要删除的节点一般是OldD的右节点或者右节点的最前面的一个左节点,当然也可能无右节点只能删自己。

(1)首先 ,从当前点D和DR之间的颜色顺序看。
< 1 > 红->黑/NULL ,P肯定为黑 , 直接删掉红,用黑(NULL)替换。

< 2 > 黑->红 ,直接拿红替换黑,然后红变黑。

< 3 > 黑->黑 / NULL , 情况变得复杂。
(2)然后 ,根据uncle的颜色再分一次情况讨论:
< 1 > 如果uncle是红,那就很容易了。因为P不可能是红只能是黑,只有一种情况。


< 2 > uncle为黑时,特别复杂。
将S右旋转时,接着将SL改为P的颜色,P的颜色改为黑色(用这个黑色来填补DR分支的黑节点数),将P左旋转。




计算时采用线性扫描的方式O(n^2),效率奇低。采用平衡二叉树的方法存储各个点,用中位数做分割,划分左右区间,并进行以x-y轴为中心进行交替选择。


算法复杂度:
构建:O(log2n)
插入:O(log n)
删除:O(log n)
查询:平均情况下 O(log n) ,最好情况 O(1),最坏情况O(n)
- 构建kd树:
(1)按y排序,抽取其中的中位数(向上取整)对应的点,axis代表的维自增。每个node保留一个指针指向父节点。
分别计算x,y方向上数据的方差得知x方向上的方差最大
- 搜寻确定查询点最小范围的点:
(1)先以y分割判断点A>Sy,向左子树查。
(2)再以x分割判断B<Sy,向右子树搜索。
- while查找二维空间里的最近点。
如果点非空而且栈非空(在根节点退到root->f即空节点而且栈为空(叶节点情况下一般栈非空))退出。
(1)如果 minr < r(当前点,搜索点),则无需查找另外一侧的子节点。r=r->f
(2)如果minr > r(当前点,搜索点),则去另一侧的子节点找找看。r=r->l/r(看你搜索点在线的哪一侧(根据x/y相对大小),取反方向即可),同时,stack记录r。
(3)当r==NULL,触底回退到stack为空(保留stack[0]),然后r=r->f(会不会重新陷回r->l/r ?不会左边的minr可能值都遍历了,所以会使r指向另一侧,等栈pop回到原点时又毫不留情r=r->f)
1 | #include<iostream> |
感知机是二分类的线性模型,其输入是实例的特征向量,输出的是事例的类别,分别是+1和-1,属于判别模型。假设训练数据集是线性可分的,感知机学习的目标是求得一个能够将训练数据集正实例点和负实例点完全正确分开的分离超平面。如果是非线性可分的数据,则最后无法获得超平面

点到线的距离公式:
$d=\frac{Ax_0+By_0+C}{\sqrt{A^2+B^2}}$
假设有一超平面,h=w*x+b,其中w=(w0,w1…wm),x=(x0,x1…xm),样本x’到超平面的距离为:
$d=\frac{w*x’+b}{||w||}$
输出的模型如下:
$f(x)=sign(w*x+b)$
$sign(x)=\begin{cases} 1 \quad\quad\quad x>0
-1 \quad\quad\quad x\leq0\end{cases}$
如果 $ \frac{w\star x’+b}{||w||}>0 $,则y=1。如果<0,则y=-1。这样分类正确的话 $y*\frac{w\star x’+b}{||w||}>0$ 恒成立(||w||是L2范数)
$L(w,b)=-\frac{1}{||w||}\sum_{x_i \in M}y_i(w*x_i+b)$(M集合是误分类点的集合)
当然因为||$w$||>0所以我们可以去掉它
$L(w,b)=-\sum_{x_i \in M}y_i(w*x_i+b)$
感知机分类的最终目的是让最终值=0,所以少除也无所谓,还能降低计算量。(所以有个前提:能收敛到0)
用普通的基于所有样本的梯度和的均值的批量梯度下降法(BGD)是行不通的,原因在于我们的损失函数里面有限定,只有误分类的M集合里面的样本才能参与损失函数的优化。所以我们不能用最普通的批量梯度下降,只能采用随机梯度下降(SGD)
$\nablawL(w,b)=-\sum{x_i \in M}y_i*x_i$
$\nablabL(w,b)=-\sum{x_i \in M}y_i$
$w \gets w+\etay_ix_i$
$b \gets b+\eta*y_i$
1 | 例:点x1=(3,3),w0=0,b0=0,y1=1,步长为1 |


对偶形式的目的是降低运算量,但是并不是在任何情况下都能降低运算量,而是在特征空间的维度很高时才起到作用。
原:
$w \gets w+\etay_ix_i$
$b \gets b+\eta*y_i$
现:
初始值为(0,0)的w和b经过了n次修改后:($a_i=n_i\eta$)
$w=\sum_{i=1}^na_iy_ix_i$
$b=\sum_{i=1}^na_iy_i$
$sign(wx+b)$,将$w=\sum{i=1}^na_iy_ix_i$带入得$f(x)=\sum{i=1}^na_iy_ix_ixj+b$
$a_i \gets a_i+\eta$
$b \gets b+\eta y_i$
gram矩阵,$G=[xi*x_j]{N*N}$
很可惜,因为不能解决异或问题躺尸了很多年。。。
(1)申请的内存所在位置:
new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
那么自由存储区是否能够是堆,这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区,但是new其实是对malloc的封装,所以只是逻辑上有所区别….
(2)返回类型安全性:
new操作符内存分配成功时,返回的是对象类型的指针,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void ,需要通过强制类型转换将void指针转换成我们需要的类型。
(3)是否调用构造函数/析构函数:
使用new操作符来分配对象内存时会经历三个步骤:
第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
第三部:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
第一步:调用对象的析构函数。
第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
智能指针:
https://blog.csdn.net/u012501459/article/details/48229399
C++11中引入了智能指针的概念。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,使用智能指针能更好的管理堆内存。

1 | //使用int*指针初始化ptr,注意必须要放在初始化列表中 |

1 | //赋值操作符,右操作数的引用计数要减1,左操作数的引用计数要加1 |
1 | //初始化这个类,引用计数设为1,并且将p指向传入的地址 |
基于安全考虑:
1
2
3 auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation;
vocaticn = ps;
因为程序将试图删除同一个对象两次,要避免这种问题,方法有多种:
(1)定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
(2)建立所有权概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。
(3)创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
unique_ptr由C++11引入,旨在替代不安全的auto_ptr。
unique_ptr不共享它的所管理的对象。它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法。只能移动 unique_ptr,即对资源管理权限可以实现转移。

1
2
3
4
5
6
7
8
9
10
11
12//智能指针的创建
unique_ptr<int> u_i; //创建空智能指针
u_i.reset(new int(3)); //"绑定”动态对象
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d); //创建空unique_ptr,执行类型为T的对象,用类型为D的对象d来替代默认的删除器delete
//所有权的变化
int *p_i = u_i2.release(); //释放所有权
unique_ptr<string> u_s(new string("abc"));
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针”
u_s2.reset(u_s.release());//所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价
使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译期因下述代码行出现错误。一句话总结就是:避免因潜在的内存问题导致程序崩溃。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int main()
{
auto_ptr<string> films[5] ={
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors"))
};
auto_ptr<string> pwin;
pwin = films[2];
// films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
for(int i = 0; i < 4; ++i)
{
cout << *films[i] << endl;
}
return 0;
}
从上面可见,unique_ptr比auto_ptr更加安全,因为auto_ptr有拷贝语义,拷贝后原象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr则禁止了拷贝语义,但提供了移动语义,即可以使用std::move()进行控制权限的转移1
2
3
4
5
6
7
8unique_ptr<string> upt(new string("lvlv"));
unique_ptr<string> upt1(upt); //编译出错,已禁止拷贝
unique_ptr<string> upt1=upt; //编译出错,已禁止拷贝
unique_ptr<string> upt1=std::move(upt); //控制权限转移,正确的写法
auto_ptr<string> apt(new string("lvlv"));
auto_ptr<string> apt1(apt); //编译通过
auto_ptr<string> apt1=apt; //编译通过
参看内存垃圾管理(智能指针)
被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。
循环引用:
一般来讲,解除这种循环引用有下面三种可行的方法:
(1)当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
(2)当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
(3)使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法,使用弱引用的智能指针std:weak_ptr来打破循环引用。
关于函数的调用规则(调用约定),大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要delphi调用,则需要了解。
1.__cdecl
__cdecl 是 C Declaration 的缩写,表示 C 语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
2.__stdcall
__stdcall 是 Standard Call 的缩写,是 C++ 的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是 this 指针。这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retnX,X 表示参数占用的字节数,CPU 在 ret 之后自动弹出 X 个字节的堆栈空间,称为自动清栈。函数在编译的时候就必须确定参数个数,并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。
3.__fastcall
fastcall 是编译器指定的快速调用方式。由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此 fastcall 通常规定将前两个(或若干个)参数由寄存器传递,其余参数还是通过堆栈传递。不同编译器编译的程序规定的寄存器不同,返回方式和 __stdcall 相当。
4.__pascal
pascal 是 Pascal 语言(Delphi)的函数调用方式,也可以在 C/C++ 中使用,参数压栈顺序与前两者相反。返回时的清栈方式与 stdcall 相同。
5.__thiscall
thiscall 是为了解决类成员调用中 this 指针传递而规定的。thiscall 要求把 this 指针放在特定寄存器中,该寄存器由编译器决定。VC 使用 ecx,Borland 的 C++ 编译器使用 eax。返回方式和 __stdcall 相当。