cocos2d-x-SpriteFrameCache

| 分类 cocos2d-x  | 标签 cocos2d-x 源码学习 

本文为xiedantibu原创,转载请注明作者及出处!

###目录 —-     SpriteFrame
    SpriteFrameCache
    SpriteFrameCache与TextureCache
    plist文件格式
    COCOS2D-X常用宏
    C相关方法学习


####SpriteFrame-精灵帧

SpriteFrame我们查看下其源码,发现其持有一个Texture2D *_texture以及相关的纹理Name,以及纹理偏移量等。那SpriteFrame的主要作用其实也就是为了保存从plist文件中读取的纹理信息,配合SpriteFrameCache实现精灵帧缓存的作用。下面是SpriteFrame的相关源码:

构造SpriteFrame

static SpriteFrame* create(const std::string& filename, const Rect& rect);
static SpriteFrame* create(const std::string& filename, const Rect& rect, bool rotated, const Vec2& offset, const Size& originalSize);//很少用

static SpriteFrame* createWithTexture(Texture2D* pobTexture, const Rect& rect);//通过纹理Texture2D以及Rect创建精灵帧
static SpriteFrame* createWithTexture(Texture2D* pobTexture, const Rect& rect, bool rotated, const Vec2& offset, const Size& originalSize);

以下为初始化精灵帧的主要方法

bool initWithTexture(Texture2D* pobTexture, const Rect& rect);
bool initWithTextureFilename(const std::string& filename, const Rect& rect);
bool initWithTexture(Texture2D* pobTexture, const Rect& rect, bool rotated, const Vec2& offset, const Size& originalSize)

通过精灵帧获取设置纹理

Texture2D* getTexture(void);
void setTexture(Texture2D* pobTexture); 相关属性

    Vec2 _offset;//偏移量
    Size _originalSize;原始大小
    Rect _rectInPixels;//像素RECT
    bool   _rotated;//是否旋转
    Rect _rect;
    Vec2 _offsetInPixels;//像素单位偏移量
    Size _originalSizeInPixels;
    Texture2D *_texture;//持有的纹理指针
    std::string  _textureFilename;//纹理名字 ---         <a id='SpriteFrameCache' name='SpriteFrameCache'> </a> ####[SpriteFrameCache](#top). ---

SpriteFrameCache也就是精灵帧缓存,其主要的工作流程是读取一张大的纹理图片,同时通过plist文件,将这张大图片上的小图片的相关信息,也就是截取小图片的纹理信息保存在精灵帧中。

Map<std::string, SpriteFrame*> _spriteFrames;//保存精灵帧的啊
ValueMap _spriteFramesAliases;//format值为3, Zwoptex导出的格式,在format为3的情况下,保存key为aliases的精灵帧,在format其他情况下没有赋值该值
std::set<std::string>*  _loadedFileNames;//大图片的名字,也就是plist的名字

获取单例的SpriteFrameCache对象。sharedSpriteFrameCache方法已经弃用

static SpriteFrameCache* getInstance(void);

销毁SpriteFrameCache对象,purgeSharedSpriteFrameCache已经弃用

void SpriteFrameCache::destroyInstance()
{
	CC_SAFE_RELEASE_NULL(_sharedSpriteFrameCache);
}

初始化_spriteFrames等属性,在getInstance中回调init方法

bool SpriteFrameCache::init(void)
{
	_spriteFrames.reserve(20);//设置所需要保存的帧的数量,默认数量为20
	_spriteFramesAliases.reserve(20);//设置所需要保存aliases的精灵帧的数量,默认为20
	_loadedFileNames = new std::set<std::string>();
	return true;
}

_spriteFrames是cocos2d::Map<K,V>类型,cocos2d::Map<K,V>是使用std::unordered_map作为底层结构的关联式容器。而std::unordered_map是一个存储键值对的关联式容器,它可以通过它们的键快速检索对应的值。使用unordered_map,键通常是唯一的,而值则与这个键对应。

在unordered_map内部,元素是无序,它们是根据键的哈希值来存取的,存取的时间复杂度是常量,超级快。(参考自:http://www.cocoachina.com/bbs/read.php?tid=202255)

将精灵帧添加到_spriteFrames中

void addSpriteFramesWithFile(const std::string& plist);//自动创建的纹理Texture2D
void addSpriteFramesWithFile(const std::string& plist, const std::string& textureFileName);//需要传入纹理名字,通过名字创建纹理
void addSpriteFramesWithFile(const std::string&plist, Texture2D *texture);//直接传入一个纹理 以上三个方法其中2,3方法类似,都是直接创建Texture2D,调用addSpriteFramesWithDictionary方法,关于方法1,则需要遍历plist文件,获取纹理名字,来创建纹理。下面就来看看第一个方法的具体实现:

void SpriteFrameCache::addSpriteFramesWithFile(const std::string& pszPlist)
{
if (_loadedFileNames->find(pszPlist) == _loadedFileNames->end())//通过查找_loadedFileNames来判断是否该plist已经将精灵帧添加到精灵帧缓存中
   {
    std::string fullPath = FileUtils::getInstance()->fullPathForFilename(pszPlist);//获取该plist的完整路径
    ValueMap dict = FileUtils::getInstance()->getValueMapFromFile(fullPath);//将该plist文件转换成ValueMap

    string texturePath("");//初始化纹理名字路径
    if (dict.find("metadata") != dict.end())//在dict中查找是否含有metadata的key
    {
        ValueMap& metadataDict = dict["metadata"].asValueMap();//将metadata的value转换成ValueMap类型
        texturePath = metadataDict["textureFileName"].asString();//查找metadataDict中是否含有textureFileName这个key,如果有把她的value转换成string类型
    }
    if (!texturePath.empty())//判断路径是否为空
    {
        texturePath = FileUtils::getInstance()->fullPathFromRelativeFile(texturePath.c_str(), pszPlist);//获取纹理其完整路径
    }
    else
    {
        texturePath = pszPlist;//如果路径为空,则直接把plist的路径赋值给他
        size_t startPos = texturePath.find_last_of("."); //找到其后缀名.的位置
        texturePath = texturePath.erase(startPos);//抹除.后面的全部内容
        texturePath = texturePath.append(".png");//添加上.png,这里要看清是png类型,所以如果没有加密的plist文件,需要确保纹理图片是png,且名字要一样
    }
    Texture2D *texture = Director::getInstance()->getTextureCache()->addImage(texturePath.c_str());//终于找到这一步,和方法2,3一样,将纹理图片名字传给纹理缓存,创建纹理
    if (texture)
    {
        addSpriteFramesWithDictionary(dict, texture);//这方法是干货,将plist的内容,以及纹理Texture2D传过去,为啥要Texture2D呢?因为精灵帧需要啊.真正的处理plist文件,保存精灵帧的地方
        _loadedFileNames->insert(pszPlist);//将该纹理名字插入_loadedFileNames中,方便前面的判断
    }
    else
    {
        CCLOG("cocos2d: SpriteFrameCache: Couldn't load texture");
    }
  }
}    

在上面我们看到addSpriteFramesWithFile三个方法都调用了addSpriteFramesWithDictionary这方法,可见他的重要性,在看这代码之前我们得先了解下plist文件,看完了也得看代码了:

void SpriteFrameCache::addSpriteFramesWithDictionary(ValueMap& dictionary, Texture2D* texture)
{
//typedef std::unordered_map<std::string, Value> ValueMap

ValueMap& framesDict = dictionary["frames"].asValueMap();//获取key为frames的values
int format = 0;//plist的格式类型
if (dictionary.find("metadata") != dictionary.end())//查找key为metadata
{
    ValueMap& metadataDict = dictionary["metadata"].asValueMap();
    format = metadataDict["format"].asInt();//获取format的类型
}
for (auto iter = framesDict.begin(); iter != framesDict.end(); ++iter)//迭代framesDict
{
    ValueMap& frameDict = iter->second.asValueMap();//获取value值
    std::string spriteFrameName = iter->first;//获取key值,也就是图片帧的名字
    SpriteFrame* spriteFrame = _spriteFrames.at(spriteFrameName);//查找在_spriteFrames是否有记录相同名字的图片
    if (spriteFrame)//在_spriteFrames中已经含有spriteFrameName的帧,则调过继续
    {
        continue;
    }
    
    if(format == 0) 
    {
      。//格式为0,Flash版本的具体逻辑省虑。。  
    } 
    else if(format == 1 || format == 2) //TexturePacker的主要文件格式
    {
        Rect frame = RectFromString(frameDict["frame"].asString());//将String类型转换成Rect类型
        bool rotated = false;
        if (format == 2)
        {
            rotated = frameDict["rotated"].asBool();
        }

        Vec2 offset = PointFromString(frameDict["offset"].asString());//将String类型转换成Vec2
        Size sourceSize = SizeFromString(frameDict["sourceSize"].asString());//将string类型转换成Size
        spriteFrame = new SpriteFrame();//创建一个精灵帧
        spriteFrame->initWithTexture(texture,
            frame,
            rotated,
            offset,
            sourceSize
            );//初始化一个精灵帧,将一系列参数传入
    } 
    else if (format == 3)//Zwoptes1.0.2以后的主要文件格式
    {
        Size spriteSize = SizeFromString(frameDict["spriteSize"].asString());
        Vec2 spriteOffset = PointFromString(frameDict["spriteOffset"].asString());
        Size spriteSourceSize = SizeFromString(frameDict["spriteSourceSize"].asString());
        Rect textureRect = RectFromString(frameDict["textureRect"].asString());
        bool textureRotated = frameDict["textureRotated"].asBool();
        ValueVector& aliases = frameDict["aliases"].asValueVector();//获取aliases的值,返回的是一个数组,具体查看下文plist-3文档
        for(const auto &value : aliases) {
            std::string oneAlias = value.asString();
            if (_spriteFramesAliases.find(oneAlias) != _spriteFramesAliases.end())
            {
            }
            _spriteFramesAliases[oneAlias] = Value(spriteFrameName);//将key为oneAlias,value为Value(spriteFrameName)赋值到_spriteFramesAliases
        }
        spriteFrame = new SpriteFrame();
        spriteFrame->initWithTexture(texture,
                        Rect(textureRect.origin.x, textureRect.origin.y, spriteSize.width, spriteSize.height),
                        textureRotated,
                        spriteOffset,
                        spriteSourceSize);
    }

    // add sprite frame
    _spriteFrames.insert(spriteFrameName, spriteFrame);//将图片纹理名字,已经精灵帧插入到_spriteFrames中
    spriteFrame->release();
}

除了通过plist文件插入精灵帧外,还可以直接将精灵帧插入

	void addSpriteFrame(SpriteFrame *frame, const std::string& frameName);//通过传入精灵帧,以及纹理名字。如果在_spriteFrames已经传在frameName的精灵帧,那原先的精灵帧将被覆盖

有添加插入精灵帧,当然也就有移除精灵帧,我们在插入的过程中其实我们知道,精灵帧主要是插入到_spriteFrames,移除当然也就是移除_spriteFrames里的东西,当然在此过程中也就需要移除_loadedFileNames。否则我下次通过plist插入不进来精灵帧啊,当然在次过程中不能忘了_spriteFramesAliases,这仅仅只在format == 3的时候保存了纹理name.下面大体的看下移除的方法:

void removeSpriteFrames();//移除全部的精灵帧,只有把上诉三个属性全部clear就可以
void removeUnusedSpriteFrames();//移除没有使用的精灵帧,什么叫没有用的精灵帧呢??SpriteFrame的引用计数为1,那就是没有使用啊。这样就可以通过_spriteFrames.erase(toRemoveFrames)移除没有使用的了。
void removeSpriteFrameByName(const std::string& name);//根据名字移除,那直接下面两个方法不就可以_spriteFrames.erase(key)。_spriteFramesAliases.erase(key);具体的还是见代码好啊
void removeSpriteFramesFromTexture(Texture2D* texture);//通过迭代比较精灵帧的纹理是否和texture相等,来移除对应的纹理精灵
void removeSpriteFramesFromFile(const std::string& plist);//根据plist文件移除精灵,好好想下,移除的步骤其实,就是得知道纹理图片的名字,所以肯定是得通过ValueMap把plist的纹理图片找出来,移除图片,所以当前方法会用到下面的方法
void removeSpriteFramesFromDictionary(ValueMap& dictionary);//这方法也就是在上面方法将的,通过ValueMap把图片名字取出来,移除啊

移除精灵帧主要通过两种途径,要不就是根据纹理名字erase。要不就是通过比较精灵帧中的纹理是否和我将要移除的纹理一样来erase。
根据名字从精灵帧缓冲中获取精灵帧,当然也就很轻松了啊。。。

	SpriteFrame* getSpriteFrameByName(const std::string& name);

####SpriteFrameCache vs. TextureCache — 以下摘抄自:http://www.cocoachina.com/bbs/read.php?tid=200359
SpriteFrameCache精灵框帧缓存。顾名思义,这里缓存的是精灵帧SpriteFrame,它主要服务于多张碎图合并出来的纹理图片。这种纹理在一张大图中包含了多张小图,直接通过TextureCache引用会有诸多不便,因而衍生出来精灵框帧的处理方式,即把截取好的纹理信息保存在一个精灵框帧内,精灵通过切换不同的框帧来显示出不同的图案。 跟TextureCache功能一样,将SpriteFrame缓存起来,在下次使用的时候直接去取。不过跟TextureCache不同的是,如果内存池中不存在要查找的图片,它会提示找不到,而不会去本地加载图片。

 * TextureCache时最底层也是最有效的纹理缓存,缓存的是加载到内存中的纹理资源,也就是图片资源。
 * SpriteFrameCache精灵框帧缓存,缓存的时精灵帧。
 * SpriteFrameCache是基于TextureCache上的封装。缓存的是精灵帧,是纹理指定区域的矩形块。各精灵帧都在同一纹理中,通过切换不同的框帧来显示出不同的图案。

####plist文件格式 — 以下摘抄自:http://zengrong.net/post/1981.htm

  1. 什么是plist文件格式?

     这是一种人类可读的串行化对象文件,由苹果公司发明,最早用于NeXTSTEP系统。详情看这里:Plist(http://zh.wikipedia.org/wiki/Plist) 。cocos2d-x 从 cocos2d-iphone 发展而来,因此在引擎中大量使用了这种文件格式。
    
  2. cocos2d-x在哪些地方使用了plist格式?

    *.图像纹理定义文件:将多个纹理拼在一张大图上,使用 CCSpriteFrameCache 可以载入这类plist文件;
    *.Label纹理定义文件:作用与图像纹理定义文件类似,只不过处理的是自己,面向 CCLabelAtlas;
    *.帧动画定义:定义一个或多个动画中,使用哪些纹理,使用 CCAnimationCache 可以载入这类plist文件;
    
  3. 图像纹理定义文件格式说明

    cocos2d-x中的纹理定义格式,是以Zwoptex生成的格式为标准的。
    Zwoptex生成的格式,有4种主要不同的版本:
       *.format值为0,代表Flash版本;
       *.format值为1,Zwoptex 0.4b以前支持;
       *.format值为2,Zwoptex 1.0以后支持,与format1的区别在于支持旋转;
       *.format值为3,属性名称进行了大幅修改,Zwoptes1.0.2之后支持。
    这3种格式的plist文件,cocos2d-x都能支持,具体的解析代码在CCSpriteFrameCache::addSpriteFramesWithDictionary。
    TexturePacker生成的for cocos2d plist格式与Zwoptex生成的format为2的格式相同。
    
  4. format为0的plist文件

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>texture</key>
    	<dict>
    		<key>width</key>
    		<integer>256</integer>
    		<key>height</key>
    		<integer>128</integer>
    	</dict>
    	<key>frames</key>
    	<dict>
    		<key>grossini.png</key>
    		<dict>
        		<key>x</key>
        		<integer>103</integer>
        		<key>y</key>
        		<integer>1</integer>
        		<key>width</key>
        		<integer>51</integer>
        		<key>height</key>
        		<integer>109</integer>
        		<key>offsetX</key>
        		<real>0</real>
        		<key>offsetY</key>
        		<real>-1</real>
        		<key>originalWidth</key>
        		<integer>85</integer>
        		<key>originalHeight</key>
        		<integer>121</integer>
    		</dict>
    	</dict>
    </dict>
    </plist>		
    
  5. format为2的plist文件

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    	<dict>
    		<key>frames</key>
    		<dict>
        		<key>parts-dragon_leg_l.png</key>
        		<dict>
            		<key>frame</key>
            		<string>{ {866,2},{152,254} }</string>
            		<key>offset</key>
            		<string>{0,0}</string>
            		<key>rotated</key>
            		<false/>
            		<key>sourceColorRect</key>
            		<string>{ {0,0},{152,254} }</string>
            		<key>sourceSize</key>
            		<string>{152,254}</string>
        		</dict>
    		</dict>
    		<key>metadata</key>
    		<dict>
        		<key>format</key>
        		<integer>2</integer>
        		<key>realTextureFileName</key>
        		<string>dragon.png</string>
        		<key>size</key>
        		<string>{1024,2048}</string>
        		<key>smartupdate</key>
        		<string>$TexturePacker:SmartUpdate:26d1d28da42d49170ab3142654fea750:1/1$</string>
        		<key>textureFileName</key>
        		<string>dragon.png</string>
    		</dict>
    	</dict>
    </plist>
    
  6. format为3的plist文件

	<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
	<plist version="1.0">
	<dict>
		<key>frames</key>
		<dict>
    		<key>armyLevelBg.png</key>
    		<dict>
        		<key>aliases</key>
        		<array/>
        		<key>spriteColorRect</key>
        		<string>{ {0, 0}, {61, 36} }</string>
        		<key>spriteOffset</key>
        		<string>{0, -0}</string>
        		<key>spriteSize</key>
        		<string>{61, 36}</string>
        		<key>spriteSourceSize</key>
        		<string>{61, 36}</string>
        		<key>spriteTrimmed</key>
        		<true/>
        		<key>textureRect</key>
        		<string>{ {195, 2}, {36, 61} }</string>
        		<key>textureRotated</key>
        		<true/>
    		</dict>
		</dict>
		<key>metadata</key>
		<dict>
    		<key>version</key>
    		<string>1.6.0</string>
    		<key>format</key>
    		<integer>3</integer>
    		<key>size</key>
    		<string>{256, 512}</string>
    		<key>name</key>
    		<string>army_zw.zwd</string>
    		<key>textureFileName</key>
    		<string>army_zw_cocos2d.png</string>
    		<key>premultipliedAlpha</key>
    		<false/>
		</dict>
	</dict>
	</plist>

####COCOS2D-X常用宏.

 #define CC_SWAP(x, y)//交互x,y的值
 #define CCRANDOM_MINUS1_1()//生成一个 -1 到 1的随机数
 #define CCRANDOM_0_1() // 生成一个 0 到 1的随机数
 #define CC_DEGREES_TO_RADIANS(__ANGLE__) // 角度转弧度
 #define CC_RADIANS_TO_DEGREES(__ANGLE__) //弧度转角度
 #define kRepeatForever //定时器永远执行
 #define CC_BLEND_SRC  //GL_ONE表示使用0.0作为因子,实际上相当于不使用这种颜色参与混合运算
 #define CC_BLEND_DST  //GL_ONE_MINUS_SRC_ALPHA表示用1.0减去源颜色的alpha值来作为因子
 #define CC_NODE_DRAW_SETUP() //getGLProgram()->use(); getGLProgram()->setUniformsForBuiltins(_modelViewTransform);设置gl的状态,以及设置模型视图矩阵以及投影矩阵
 #define CC_DIRECTOR_END()/// 停止 director 并从内存中移除, 从父级移除 CCGLView
 #define FLT_EPSILON //趋0最小float
 #define DISALLOW_COPY_AND_ASSIGN(TypeName) //用来限制类型复制、拷贝和默认的一些转换操作等对class类型变量的误操作,也可以提高性能,减少编译器自作主张为我们生成一些无法预料的代码  
 #define CC_CONTENT_SCALE_FACTOR()//缩放因子
 #define CC_RECT_PIXELS_TO_POINTS(__rect_in_pixels__) //RECT的转换,主要是除以CC_CONTENT_SCALE_FACTOR()
 #define CC_RECT_POINTS_TO_PIXELS(__rect_in_points_points__)//RECT的转换 主要通过*CC_CONTENT_SCALE_FACTOR()
 #define CC_POINT_PIXELS_TO_POINTS(__pixels__)//点的转换 主要是除以CC_CONTENT_SCALE_FACTOR()
 #define CC_POINT_POINTS_TO_PIXELS(__points__)//点的转换 主要通过*CC_CONTENT_SCALE_FACTOR()
 #define CC_SIZE_PIXELS_TO_POINTS(__size_in_pixels__)//SIZE的转换 
 #define CC_SIZE_POINTS_TO_PIXELS(__size_in_points__)
 #define CHECK_GL_ERROR_DEBUG()//检测opengl的错误
 #define CC_INCREMENT_GL_DRAWS(__n__)//batches绘制次数
 #define CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(__drawcalls__, __vertices__)//batches绘制次数,以及vertices绘制个数

/**
以下CC_CALLBACK_* 都是std::bind的宏定义,由于std::bind的占位符的数量不一样,也就形成了CC_CALLBACK_0,CC_CALLBACK_1等形式。
__target__表示执行这个回调函数的具体对象;##__VA_ARGS__是可变参数宏,std::bind是在C++ 11里新加入的成员。可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。调用bind的一般形式为: auto newCallback = bind(callback,arg_list); 其中,newCallback是一个可调用对象,arg_list是可以用逗号分隔的参数列表,至于是啥参数,那就看callback函数里有啥参数啦。也就是说,当我们调用newCallback时,newCallback会调用函数callback,并传递参数arg_list给callback. 

有一个函数callback,如:int callback(int one,char two,double three);
下面我们用bind来调用callback:
auto newCallback = bind(callback,_1,_2,1.5);
int x = newCallback(10,'h');  //这句相当于:int x = callback(10,'h',1.5);

“_1″是一个占位符对象,用于表示当函数callback通过函数newCallback进行调用时,函数newCallback的第一个参数在函数callback的参数列表中的位置。第一个参数称为”_1″, 第二个参数为”_2″,依此类推,有意思吧。至于‘1.5’是指默认参数,它处于_1和_2的后面,所以它就是double类型的参数了.
*/ 
 #define CC_CALLBACK_0(__selector__,__target__, ...)//std::bind(&__selector__,__target__, ##__VA_ARGS__)
#define CC_CALLBACK_1(__selector__,__target__, ...) //std::bind(&__selector__,__target__, std::placeholders::_1, ##__VA_ARGS__)
#define CC_CALLBACK_2(__selector__,__target__, ...) //std::bind(&__selector__,__target__, std::placeholders::_1, std::placeholders::_2, ##__VA_ARGS__)
#define CC_CALLBACK_3(__selector__,__target__, ...) //std::bind(&__selector__,__target__, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, ##__VA_ARGS__)

####C相关方法学习.

+. void *memcpy(void *dest, const void *src, size_t n):从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中.

+. strcpy和memcpy主要有以下3方面的区别:

* 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。  
* 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度  
* 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

+. erase函数的原型如下:

* string& erase ( size_t pos = 0, size_t n = npos );
* iterator erase ( iterator position );
* iterator erase ( iterator first, iterator last );  

+. 也就是说erase有三种用法:

* erase(pos,n); 删除从pos开始的n个字符,比如erase(0,1)就是删除第一个字符
* erase(position);删除position处的一个字符(position是个string类型的迭代器)
* erase(first,last);删除从first到last之间的字符(first和last都是迭代器)

青春是一场大雨,即使感冒了,还盼回头再淋一次...image...微笑永远是一个人身上最好看的东西...


PREVIOUS     NEXT