OpenCV的数据类型 #-3

本文详细介绍了OpenCV中cv::Mat类的使用方法,包括数组的创建、访问、操作及成员函数等内容,并对比了cv::Mat与cv::SparseMat的区别,适合OpenCV初学者及进阶读者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、大型数组类型


1.1、cv::Mat类N维稠密数组

1.1.1、cv::Mat介绍

大型数组类中最主要的当属cv::Mat,这个结构可以视为OpenCV所有C++实现的核心,OpenCV所有的主要函数都或是cv::Mat类的成员,或是将cv::Mat作为参数,或是返回一个cv::Mat类型。

cv::Mat类用于表示任意维度稠密数组(表示该数组的所有部分都有一个值存储,即使这个值是0)。对于大多数图像来说,都是以稠密数组的形式存储的。与稠密数组相对的还有稀疏数组,稀疏数组中只有非0的数值会被存储。

cv::Mat类可以作为任意维度的数组使用,其数据可以看做是以按照栅格扫描顺序存储的n维数组。这意味着在一维数组中,元素是按顺序排列的;而在二维数组中,数据按行组织,每一行也按顺序排列;对于三维数组来说,所有的通道都被行填充,每一个通道同一按顺序排列。

所有的矩阵都含有如下元素:

元素作用
flag表示矩阵包含数组类型的元素
dims表示矩阵维度的元素
rows表示矩阵的行的数目(当dims>2的时候无效)
cols表示矩阵的列的数目 (当dims>2的时候无效)
data指示数据真正存储位置的data指针
refcount表示该内存区域有多少个引用(类似于cv::Ptr<>的引用计数)
step描述数据实体data的结构

包含在cv::Mat中的数据不要求必须是简单基元。cv::Mat的元素可以是一个简单的数字,也可以是多个数字。在包含多个数字时,它就被称为“多通道数组”。


1.1.2、创建数组

可以通过实例化变量cv::Mat来创建一个数组,通过这种方式创建的数组没有大小和数据类型。当然,之后也可以再次使用成员函数create()来申请一个内存区域。一个create的变体是指定行数和列数以及数据类型来配置二维数据的规模。数组的类型(type)决定了它含有什么样的 元素。一个有效的数据类型需要同时指明数据的类型通道数,数据类型的定义如下:

CV_<bit_depth>(S|U|F)C<number_of_channels>
参数说明
bit_depth比特数(矩阵中每个像素的大小),有8bite\16bite\32bite\64bite
S|U|FS:signed int;U:unsigned int;F:float;
number_of_channels所存储的图片的通道数,灰度图像(单通道图像),RGB彩色图像(3通道图像), 带Alpha通道的RGB彩色图像(4通道图像)

定义超过3通道的数据类型,需要调用CV_{8U,16S,32S,32F,64F}C(),这些函数只有一个参数,表明通道数,所以CV_8UC(3)等效于CV_8UC3。

实例解释
CV_8UC38位、无符号整型、三通道、数据
CV_32FC132位、浮点型、单通道、数据
cv::Mat m;
m.create(3, 10, CV_32FC3);
m.setTo(cv::Scalar(1.0f, 0.0f, 1.0f));

↑等价于↓

cv::Mat m(3, 10, CV_32FC3, cv::Scalar(1.0f, 0.0f, 1.0f));

附:对象cv::Mat是数据实体的头。原则上来说,它和数据实体是完全不同的两个东西。比如,我们可以将一个矩阵n赋值给矩阵m(m == n)。在这里,m内部的数据指针之前所指向的数据实体m(如果存在的话)将会被释放掉。与此同时,m和n所共享的内存区域的引用指针会增加一个引用计数。


cv::Mat的函数列表

类别函数解释
构造函数cv::Mat默认构造函数
cv::Mat(int rows, int cols, int type)指定类型的二维数组
cv::Mat(int rows, int cols, int type, const Scalar&s)指定类型的二维数组,并指定初始化值
cv::Mat(int rows, int cols int type, void* data, size_t step=AUTO_STEP)指定类型的二维数组,并指定预先存储的数据
cv::Mat(cv::Size sz, int type)指定类型的二维数组(大小由sz指定)
cv::Mat(cv::Size sz, int type, const Scalar&s)指定类型的二维数组,并指定初始化值(大小由sz指定)
cv::Mat(cv::Size sz, int type, void* data, size_t step = AUTO_STEP)指定类型的二维数组,并指定预先存储的数据(大小由sz指定)
cv::Mat(int ndims, const int* sizes, int type)指定类型的多维数组
cv::Mat(int ndims, const int* sizes, int type, const Scalar&s)指定类型的多维数组,并指定初始化值
cv::Mat(int ndims, const int* sizes, int type, void* data, size_t step=AUTO_STEP)指定类型的多维数组,并指定预先存储的数据
复制构造函数cv::Mat(const Mat&mat)复制构造函数
cv::Mat(const Mat&mat, const cv::Range&rows, cv::Range&cols)只从指定的行列中复制数据的复制构造函数
cv::Mat(const Mat&mat, const cv::Rect&roi)只从感兴趣的区域中复制数据的复制构造函数
cv::Mat(const Mat&mat, const cv::Range* ranges)服务与n维数组的,从泛华的感兴趣区域中复制数据的复制构造函数
cv::Mat(const cv::MatExpr&expr)从其它矩阵的线性代数表述中生成型矩阵的复制构造函数
模板构造函数cv::Mat(const cv::Vec<T,n>&vec, bool copyData=true)构造一个如同cv::Vec所指定的数据类型为T,大小为n的一维数组
cv::Mat(const cv::Matx<T,m,n>&vec, bool copyData=true)构造一个如同cv::Matx所指定的数据类型为T,大小为mxn的二维数组
cv::Mat(const std::vector&vec, bool copyData=true)构造STL的vector所指定的数据类型为T,大小为vector元素数的一维数组
静态方法cv::Mat::zeros(rows, cols, type)构造一个大小为rows*cols,数据类型为type指定类型的,值全为0的矩阵
cv::Mat::ones(rows, cols, type)构造一个大小为rows*cols,数据类型为type指定类型的,值全为1的矩阵
cv::Mat::eye(rows, cols, type)构造一个大小为rows*cols,数据类型为type指定类型的单位矩阵

1.1.3、访问数组

①、通过模板函数at<>来实现

这个函数的工作方式式先将at<>特化到矩阵所包含的数据类型,然后使用你所想要的数据的行和列的位置访问元素,例如:

Mat grayIm(600,800,CV_8UC1);  //创建600x800的矩阵,8位单通道
Mat colorIm(600,800,CV_8UC3); //创建600x800的矩阵,8位3通道

//遍历所有像素,并设置像素值
for(int i=0; i<grayIm.rows; i++)
{
	for(int j=0; j<grayIm.cols; j++)
    {
        grayIm.at<uchar>(i,j) = (i+j)%255;
    }
}

//遍历所有像素,并设置像素值
for(int i=0; i<colorIm.rows; i++)
{
	for(int j=0; j<colorIm.cols; j++)
    {
    	Vec3b pixel;
        //注意:openCV通道顺序是BGR,而非RGB
        pixel[0]=i%255;
        pixel[1]=j%255;
        pixel[2]=(i+j)%255;
        colorIm.at<Vec3b>(i,j) = pixel;
    }
}
namedWindow("Display1");
namedWindow("Display2");
imshow("Display1",grayIm);
imshow("Display2",colorIm);

注:使用cv::Mat::eye()和cv::Mat::ones()的时候,如果要求创建的是一个多通道数组,就只有第一通道会被设置为1,其余通道保存位0;


②、使用模板函数ptr<>来实现

为了访问二维数组,可以使用C风格的指针来指定某一行,这个工作由cv::Mat类的成员函数ptr<>来完成(数组中的数据是按行连续组织的,因此不可以通过这种方式访问一个指定的列)。

函数接收一个整型参数来指示希望指向的行,返回一个和矩阵原始数据类型相同的数据指针(比如CV_32FC3,将返回float*)。因此,给定一个数据类型为float三通道的矩阵mtx,结构体mtx.ptr<Vec3f>(3)将会返回第三行指向第一个元素第一个通道的指针。例如:

// 遍历所有像素,并设置像素值
for(int i=0; i<grayIm.rows; i++)
{
	//获取第i行首像素的指针
    uchar* p = grayIm.ptr<uchar>(i);
    //对第i行的每个像素(byte)操作
    for(int j=0; j<grayIm.cols; j++)
    	p[j] = (i+j)%255;
}

// 遍历所有像素,并设置像素值
for(int i=0; i<colorIm.rows; i++)
{
	//获取第i行首像素的指针
    Vec3b* p = colorIm.ptr<Vec3b>(i);
    //对第i行的每个像素(byte)操作
    for(int j=0; j<colorIm.cols; j++)
    {
    	p[j][0] = i%255;
        p[j][1] = j%255;
        p[j][2] = (i+j)%255;
    }
}

附1:使用C风格指针式存取方式的一些问题:如果想要访问一个数组中所有东西,可能需要一次性迭代一行,因为这些行在数组中可能是连续的,但也有可能不连续。成员函数isContinuous()将告诉你数组是否被连续的打包。如果是连续的,你就可以通过获取第一行第一个元素的指针,然后在整个数组中遍历,仿佛它是一个巨大的一维数组。

附2:有两种方式可以获取一个执行矩阵mtx的数据区域的指针,一种是使用ptr<>成员函数,另一种是直接使用数据指针data,然后使用数组step来计算地址,后者更接近于C语言中所做的操作,效率最高,但是操作复杂。


③、使用cv::Mat内嵌的迭代器机制

迭代器说明
cv::MatConstIterator<>一个用于只读数组的迭代器
cv::MatIterator<>一个用于非只读数组的迭代器

cv::Mat的成员函数begin()和end()会返回这种类型的对象,因为迭代器具有足够的智能来处理连续的内存区域和非连续的内存区域,所以这种用法非常方便,不管是在哪一种维度的数组中都非常有效。例如:

MatIterator_<uchar> grayIt, grayEnd;
for(grayIt = grayIm.begin<uchar>(),grayEnd = grayIm.end<uchar>(); grayIt != grayEnd; grayIt++)
{
	*grayIt = rand()%255;
}

// 遍历所有像素,并设置像素值
MatIterator<Vec3b> colorIt, colorEnd;
for(colorIt = colorIm.begin<Vec3b>(),colorEnd = colorIm.end<Vec3b>();colorIt != colorEnd; colorIt++)
{
	(*colorIt)[0] = rand()%255; //Blue
    (*colorIt)[1] = rand()%255; //Green
    (*colorIt)[2] = rand()%255; //Red
}

④、cv::Mat区块访问

实例描述
m.row(i);m中第i行数组
m.col(j);m中第j行数组
m.rowRange(i0, i1);m中第i0行到第i1-1行(左闭右开)所构成的数组
m.rowRange(cv::Range(i0, i1));m中第i0行到第i1-1行(左闭右开)所构成的数组
m.colRange(j0, j1);m中第j0列到第j1-1列(左闭右开)所构成的数组
m.colRange(cv::Range(j0, j1));m中第j0行到第j1-1列(左闭右开)所构成的数组
m.diag(d);m中偏移为d的对角线所组成的数组(d=0为主对角线,d>0向上偏移,d<0向下偏移)
m(cv::Range(i0, j1), cv::Range(j0, j1));m中从点(i0, i1)到点(j0, j1)所包含数据构成的数组
m(cv::Rect(i0, i1, w, h))m中从点(i0, i1)到点(i0+w-1, i1+h-1)所包含数据构成的数组
m(ranges);m中依据ranges[0]到ranges[ndim-1]所索引区域构成的数组

1.1.4、矩阵表达式:代数和cv::Mat

矩阵表达式可用的运算操作

示例描述
m0 + m1; m0 - m1;矩阵的加法和减法
m0 + s; m0 - s; s + m0; s - mo;矩阵和单个元素的加和减
-m0;矩阵取负
s * m0; m0 * s;Scaling of a matrix by a singleton
m0.mul(m1); m0/m1;按元素将m0和m1相乘,按元素将m0和m1相除
m0 * m1;m0和m1进行矩阵乘法
m0.inv(method);对m0矩阵求逆(默认使用DECONP_LU)
m0.t();对m0矩阵求转置
m0>m1; m0>=m1; m0==m1; m0<=m1; m0<m1;按元素进行比较,返回元素只有0到255的uchar类型矩阵
m0&m1; m0|m1; m0^m1; m0&s; m0|s; m0^s;矩阵和矩阵之间或者矩阵和单个元素之间按位进行的逻辑操作
min(m0,m1); max(m0,m1); min(m0,s); max(m0,s);矩阵和矩阵之间或者矩阵和单个元素之间按元素去最大或最小值
cv::abs(m0)对m0按元素取绝对值
cv::norm(m0)对m0按元素求范数
cv::mean(m0)对m0按元素求均值
cv::sum(m0)对m0按元素求和

1.1.5、饱和转换

在OpenCV中,常常有一些操作有溢出的风险。为了处理这个问题,OpenCV实现了一个称为饱和转换(saturation casting)的构造。这意味着OpenCV在进行算术和其他操作的时候会自动检查是否上溢出和下溢出。在这些情况选,这个库函数会将结果值转换为相对最小或者最大的可行值。

这一操作的模板函数是cv::staturate_cast<>(),例如:

uchar& Vxy = mo.at<uchar>(x,y);
Vxy = cv::saturate_cast<ucahr>((Vxy-128)*2+128);

众所周知,无符号整型的范围是0~255。假设Vxy=0,(Vxy-128)*+128<0,所以Vxy下溢出了,cv::saturate_cast<>会将其转换为可取范围内的最小值0;


1.1.6、cv::Mat成员函数

示例描述
m1 = m0.clone();从m0进行完全复制,该复制将复制所有的数据元素
m0.copyTo(m1);将m0复制给m1,如果有必要,将给m1重分配内存空间(等同于m1=m0.clone())
mo.copyTo(m1, mask);和m0.copyTo(m1)一样,但是只复制mask所指示的区域
m0.convertTo(m1, type, scale, offset);转换m0中的元素类型(比如CV_32F)并且在尺度变换(默认为1)和增加偏置(默认为0)之后赋值给m1
m0.assignTo(m1, type);只在内部使用(集成在convertTo中)
m0.setTo(s, mask);设置m0所有元素为s,如果存在mask,则只对mask指示区域进行操作
m0.reshape(chan, rows);改变二维数组的有效形状,chan和rows变量可能为0,表示不做更改
m0.push_back(s);在末尾增加一个m*1大小的数组
m0.push_back(m1);向mn大小的矩阵m0增加k行并且复制到m1中,m1大小必须是kn
m0.pop_back(n);从m*n大小的矩阵中移除n行(默认是1)
m0.locateROI(size, offset);将m0的全尺寸写入cv::Size变量size,如果mo只是一个大矩阵的一块小区域,还会写入一个Point类型的offset
m0.adjustROI(t, b, l ,r);通过四个值t(上)、b(下)、l(左)、r(右)调整ROI范围
m0.total();计算数组序列的元素的数目(不包含通道)
m0.isContinuous();如果m0的行之间没有空隙(行连续),将返回true
m0.elemSize();返回m0的位长度(比如三通道浮点矩阵将返回12)
m0.elemSize1();返回m0最基本元素的位长度(比如三通道浮点矩阵将返回4)
m0.type();返回m0元素的类型(比如CV_32FC3)
m0.depth();返回m0通道中的元素类型(比如CV_32F)
m0.channels();返回m0的通道数目
m0.size();以cv::Size返回m0的大小
m0.empty();如果数组没有元素,将返回true(比如m0.total==0或者m0.data==NULL

1.1.7、cv::Mat和cv::Mat_<>的区别

cv::Mat已经拥有了表示基本数据类型的能力,但是它在创建的时候还是会指明其基于的类型。cv::Mat_<>这个实例模板其实是从cv::Mat集成来的一个类,并且使这个类的特化,这样就简化了接口和其他需要模板化的成员函数。

使用cv::Mat创建的矩阵在访问独立元素时需要指定矩阵的类型:

cv::Mat m(10, 10, CV_32FC2);
m.at<Vec2f>(i0, i1) = cv::Vec2f(x, y);

使用cv::Mat_<>创建的矩阵在访问独立元素时不需要指定矩阵的类型:

cv::Mat_<Vec2f> m(10,10);
m.at(i0,i1) = cv::Vec2f(x, y);
//or
m(i0, i1) = cv::Vec2f(x, y);

推荐使用特化模板cv::Mat_<>的方式创建矩阵,不仅可以极大的简化代码,而且避免一些编译器检测类型匹配的错误;


1.2、稀疏数据类cv::SparesMat

cv::SparesMat类在数组非0元素非常少的情况下使用,稀疏矩阵只存储有数据的部分,可以节约大量的内存。

cv::SparesMatcv::Mat在很多方面都类似,它们的定义十分相似,支持大多数相同的操作,并且可以包含相同的数据类型。但从内部来说,它们数据的组织方式有着非常大的不同;cv::Mat使用接近C风格的数组,cv::SparesMat使用哈希表存储非0元素,这个哈希表会自动维护,所以当数组中的非0数据变得太多以致无法高效进行查找的时候,表也会自动增长。


1.2.1、访问稀疏数组中的元素

稀疏数组提供四种访问机制:cv::SparseMat::ptr()cv::SparseMat::ref()cv::SparseMat::value()cv::SparseMat::find()


①、 cv::SparseMat::ptr()

cv::SparseMat::ptr()方法有几种变体,最简单的如下所示:

uchar* cv::SparseMat::ptr(int i0, bool createMissing, size_t* hashval=0)
  • i0:请求元素的索引;
  • createMissing:表示这个元素是否应该被创建(如果它事先不存在于数组中);当调用cv::SparseMat::ptr()时,如果这个元素已经在数组中定义,它将直接返回指向这个元素的指针,如果createMissing参数为真,这个元素将会被创建,并且一个合理的非0指针将指向这个新的元素
  • hashval:如果hashval参数是默认的NULL,哈希key将被计算,如果主动提供一个key,它将会被使用。

cv::SparseMat的数据存储结构为哈希表,查找哈希表中的对象需要两步:
第一,计算哈希key;
第二:在key所指向的列表中进行查找,一般来说,这个列表将会很短(理想状态下只有一个元素),所以基础运算的时间主要耗费在查找和计算哈希key

还有一些cv::SparseMat::ptr()的变体允许使用两个或者三个索引,也有第一个参数是一个整型数组指针的版本(比如const int* idx),它要求这个索引的元素数量和被访问的数组的维度一样多。

但是,不管是什么变体,cv::SparseMat::ptr()都会返回一个无符号字符型的指针(uchar*),一般需要再次转换为正确的类型。


②、cv::SparseMat::ref()

访问器模板函数cv::SparseMat::ref()用于返回一个指向数组中特定元素的引用。这个函数类似于cv::SparseMat::ptr(),可以接受一个、两个或者三个索引数组,并且同样支持一个可选的用于查找的哈希值。

因为这是一个模板函数,所以必须指定对象的类型。如果你的数组是CV_32F类型,就可以这样调用cv::SparseMat::ref()

a_sparse_mat.ref<float>(i0, i1) += 1.0f;

③、cv::SparseMat::value()

cv::SparseMat::value()cv::SparseMat::ref()彼此独立,它将返回一个值,而不是值的引用。因此,这个方法也叫“只读方法”。


④、cv::SparseMat::find()
访问器函数cv::SparseMat::find()类似于cv::SparseMat::ref()cv::SparseMat::value(),不同的是会返回一个请求对象的指针。和cv::SparseMat::ptr()不一样,cv::SparseMat::find()的指针类型是由模板指定的,所以不需要再次转换。


当然还可以通过迭代器访问稀疏数组的元素。与密集数组类型一样,迭代器通常是模板化的,模板化的迭代器是cv::SparseMatIteator_<>cv::SparseMatConstIterator_<>(const形式的begin()和end()将返回const的迭代器)。也有非模板化的迭代器cv::SparseMatIteatorcv::SparseMatConstIt erator,它们将返回一个非模板的SparseMat::begin()SparseMat::end().

实例:打印一些稀疏矩阵的元素

// 创建一个10x10的稀疏矩阵,带有少量的非0元素
int size[] = {10,10};
cv::SparseMat sm(2, size, CV_32F);

for(int i=0; i<10; i++) //初始化
{
	int idx[2];
	idx[0] = size[0]*rand();
	idx[1] = size[1]*rand();
	sm.ref<float>(idx) += 1.0f
}

//输出非0元素
cv::SparseMatConstIterator_<float> it = sm.begin<float>();
cv::SparseMatConstIterator_<float> it_end = sm.end<float>();

for(; it != it_end; it++)
{
	const cv::SparseMat::Node* node = it.node();
	printf("(%3d,%3d) %f\n",node->idx[0], node->idx[1], *it);
}

node返回一个指向被迭代器索引指向的稀疏矩阵的实际数据区域,它将返回一个l类型为cv::SparseMat::Node的对象,其定义如下:

struct Node
{
	size_t hashval;
	size_t next;
	int idx[cv::MaAX_DIM];
}

这个结构体即包含指向对应元素的哈希值的索引(注意,元素索引是一个int数组),还包含元素对应的哈希值。


1.2.2、稀疏数组中的特有元素

如前所述,稀疏矩阵支持许多稠密矩阵相同的操作。此外,还有一些方法是稀疏矩阵所独有的,如下表:

示例描述
cv::SparseMat sm;跳过初始化,创建一个稀疏矩阵
cv::SparseMat sm(3, sz, CV_32F);创建一个由sz指定维度大小的三通道稀疏浮点矩阵
cv::SparseMat sm(sm0);从稀疏矩阵sm0复制一个稀疏矩阵
cv::SparseMat(m0, try1d);从已有的稠密矩阵m0创建一个稀疏矩阵。如果布尔变量try1d为真,它将转换m0到一维稀疏矩阵(如果稠密矩阵是nx1或者1xn大小的)
cv::SparseMat(&old_sparse_mat);从一个2.1版本之前的C风格稀疏矩阵CvSparseMat创建一个新的稀疏矩阵
CvSparseMat* old_sm = (cv::SparseMat* ) sm;转换操作将创建一个2.1版本之前的C风格稀疏矩阵CvSparseMat对象并且所有数据都会被复制到新对象中,最后返回对象的指针
size_t n = sm.nzcount();返回sm中的非0元素数量
size_t h = sm.hash(i0);返回一维稀疏矩阵中索引i0所指向数据的哈希值
size_t h = sm.hash(i0, i1);返回二维稀疏矩阵中索引i0i1所指向数据的哈希值
size_t h = sm.hash(i0, i1, i2);返回三维稀疏矩阵中索引i0,i1,i2所指向数据的哈希值
size_t h = sm.hash(idx);返回多维稀疏矩阵中索引idx数组所指向数据的哈希值
sm.ref<float>(i0) = f0;设置一维稀疏矩阵中索引i0所指向元素的值为f0
sm.ref<float>(i0, i1) = f0;设置二维稀疏矩阵中索引i0i1所指向元素的值为f0
sm.ref<float>(i0, i1, i2) = f0;设置三维稀疏矩阵中索引i0,i1,i2所指向元素的值为f0
sm.ref<float>(idx) = f0;设置多维稀疏矩阵中索引idx数组所指向元素的值为f0
f0 = sm.value<float>(i0);设置一维稀疏矩阵中索引i0所指向元素的值为f0
f0 = sm.value<float>(i0, i1);设置二维稀疏矩阵中索引i0i1所指向元素的值为f0
f0 = sm.value<float>(i0, i1, i2);设置三维稀疏矩阵中索引i0,i1,i2所指向元素的值为f0
f0 = sm.value<float>(idx);设置多维稀疏矩阵中索引idx数组所指向元素的值为f0
p0 = sm.find<float>(i0);将一维稀疏矩阵中索引i0所指向的元素赋值给p0
p0 = sm.find<float>(i0, i1);将二维稀疏矩阵中索引i0i1所指向的元素赋值给p0
p0 = sm.find<float>(i0, i1, i2);将三维稀疏矩阵中索引i0,i1,i2所指向的元素赋值给p0
p0 = sm.find<float>(idx);将多维稀疏矩阵中索引idx所指向的元素赋值给p0
sm.erase(i0, i1, &hashval);移除二维稀疏矩阵中索引为(i0, i1)的元素
sm.erase(i0, i1, i2, &hashval);移除三维稀疏矩阵中索引为(i0, i1, i2)的元素
sm.erase(idx, &hashval);移除多维稀疏矩阵中索引为(idx)的元素
cv::SparseMatIterator<float> it = sm.begin<float>()创建一个浮点型稀疏矩阵迭代器it并指向其第一个元素
cv::SparseMatIterator<float> it_end = sm.end<uchar>();创建一个无符号字符型稀疏矩阵迭代器it_end并将其初始化指向数组sm的最后一个元素的后一个元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝勒里恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值