当前位置: 首页 > news >正文

<C++>详解string类

文章目录

  • 1. STL简介
  • 2. 标准库里的string类
  • 3. string类的常用接口说明
    • 3.1 string类对象的常见构造
    • 3.2 string类对象的容量操作
    • 3.3 string类对象的访问及遍历操作
    • 3.4 string类对象的修改操作
    • 3.5 string类非成员函数
  • 3. 深浅拷贝
    • 3.1 浅拷贝
    • 3.2 深拷贝
    • 3.3 传统写法、现代写法的string类对比
    • 3.4 拓展:写实拷贝
  • 4. string类模拟实现

1. STL简介

STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架

image-20220719131725287
今天我们讲的是容器中的string
之后的博客会围绕这六大组件进行讲解

2. 标准库里的string类

标准库类型string表示可变长的字符序列

string类的文档介绍
总结:

  1. string是表示字符串的字符串类

  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。

  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;

  4. 不能操作多字节或者变长字符的序列。

在使用string类时,必须包含#include <string>以及using namespace std;

3. string类的常用接口说明

3.1 string类对象的常见构造

cplusplus网站上的string说明

image-20220712171158837

image-20220712171714112

void test1()
{
	// 初始化
	//std::string s;
    
	// string();
	string s1;// 构造空的string类对象s1

	// string (const char* s);
	string s2("hello world");// 用C格式字符串构造string类对象s2(C格式指传的字符串默认以\0结束)

	// 拷贝构造
	// string (const string& str);
	string s3(s2);// 拷贝构造s3
	string s4 = s2;// 拷贝构造s4

	// string (const char* s, size_t n);
	string s5("https://www.baidu.com", 6);// 输出https: // 从头开始到第6个字符
	cout << s5 << endl;

	// string (size_t n, char c);
	string s6(10, 'x');// xxxxxxxxxx
	cout << s6 << endl;

	// string (const string& str, size_t pos, size_t len = npos);
	string s7(s2, 6, 3);// wor
	cout << s7 << endl;
	// 缺省参数len默认的npos是size_t类型的最大值,这里的作用是默认输出pos位置后的所有字符
	string s8(s2, 6);// world
	cout << s8 << endl;

}

3.2 string类对象的容量操作

一般用于扩容

image-20220712201935257

image-20220712202008947

清理

image-20220718125924789

void test6()
{
	//string s("hello world");
	//cout << s.length() << endl;// 返回字符串有效字符长度
	//cout << s.size() << endl;//  size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()
	//cout << s.max_size() << endl;// 整型最大值
	//cout << s.capacity() << endl;// 容量大小

	
	// reserve和resize
	//string s;
	s.reserve(1000);// reserve提前开够空间,提高插入数据的效率,避免增容带来的开销
	s.resize(1000)// resize:括空间+初始化。capacity变,size也变
	//s.resize(1000, 'x');// 先把前1000个空间存x,再执行以下程序
	//size_t sz = s.capacity();
	//cout << "making s grow:\n";
	//for (int i = 0; i < 1000; ++i)
	//{
	//	s.push_back('c');
	//	if (sz != s.capacity())
	//	{
	//		sz = s.capacity();
	//		cout << "capacity changed: " << sz << '\n';
	//	}
	//}

	string s1("hello");
	//s1.reserve(100);// capacity变成100+
	s1.resize(100);// capacity变成100+,size变成100(hello\0\0\0\0……共95个\0)
	// vs下它们都不会缩容
	s1.reserve(10);// capacity不变,size不变
	s1.resize(10);// capacity不变,size会变成10,之后使用会覆盖数据
}

总结:

  1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
  2. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。

3.3 string类对象的访问及遍历操作

遍历方法:

  1. 下标+[]
  2. 迭代器
  3. 范围for

image-20220712173347107

void test1()
{
	string s1("hello");
	const string s2("hello");
	// at和[]功能类似
	// 区别在于越界时
	//s1[6];// 断言报错
	//s2[6];
	//s1.at(6);// 抛异常
	//s2.at(6);

	s1[0] = 'x';
	//s2[0] = 'x'; 代码编译失败,因为const类型对象不能修改
	s1.at(0) = 'y';
}
void test2()
{
	// 遍历string的每个字符
	string s1("hello");

	// 方法一:[下标]
	// char& operator[] (size_t pos);// 这里的引用返回是为了支持修改返回对象
	// const char& operator[] (size_t pos) const;
	for (size_t i = 0; i < s1.size(); i++)
	{
		// s1.operator[](i)
		cout << s1[i] << " ";
	}
	cout << endl;

	// 方法二:迭代器(跟指针挺像)
	string::iterator it = s1.begin();
	while (it != s1.end())//end()指向最后一个数据的下一个位置(在这指向\0)
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// 方法三:范围for(本质上是迭代器)
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
}

迭代器分四种:正向迭代器、反向迭代器、const正向迭代器、const反向迭代器

void func(const string& s)
{
	string::const_iterator it = s.begin();// 可读不可写
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//string::const_reverse_iterator rit = s.rbegin();// 可读不可写
	auto rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << " ";
		++rit;// 变了方向,还是++
	}
	cout << endl;
}

void test5()
{
	// 迭代器
	string s1("hello");
	
	// 正向迭代器
	string::iterator it = s1.begin();// 可读可写
	while (it != s1.end())//end()指向最后一个数据的下一个位置(在这指向\0)
	{
		cout << *it << " ";
		//(*it) += 1;// 可写
		++it;
	}
	cout << endl;

	// 反向迭代器
	string::reverse_iterator rit = s1.rbegin();// 可读可写
	while (rit != s1.rend())//end()指向最后一个数据的下一个位置(在这指向\0)
	{
		cout << *rit << " ";
		++rit;// 变了方向,还是++
	}
	cout << endl;

	func(s1);
}

3.4 string类对象的修改操作

尾插单个字符

image-20220712202434567

尾插字符串

image-20220712202525025

image-20220712202725792

任意位置插入

image-20220712200306984

删除

image-20220712201053439

转换成c类型字符串

image-20220712203516254

查找

image-20220712205405141

倒着找

image-20220712205736836

取字符串

image-20220712205515404

// 增删
void test()
{
	//string s;
	 尾插
	//s.push_back('x');// 尾插字符
	//s.append("hello");// 尾插字符串
	//string str("world");
	//s.append(str);
	//s.push_back('x');
	//cout << s << endl;
	 +=是尾插最方便的
	//s += 'x';
	//s += "hello";
	//s += str;
	//cout << s << endl;

	 头插
	//string s1("hello");
	//s1.insert(0, 1, 'x');
	//s1.insert(s1.begin(), 'y');
	//cout << s1 << endl;
	//s1.insert(0, "sort ");
	//cout << s1 << endl;

	 任意位置插入
	//s1.insert(3, 1, 'x');
	//s1.insert(s1.begin()+3, 'y');
	//cout << s1 << endl;

	// 删除
	string s("hello world");
	cout << s << endl;

	s.erase(s.begin());// 头删
	cout << s << endl;

	s.erase(s.begin()+4);// 删第四个位置
	cout << s << endl;

	s.erase(3, 2);// 删第三个位置开始的两个,即第四第五个
	cout << s << endl;

	s.erase(3);// 删第三个位置后的所有字符
	cout << s << endl;

}
// 找,取
void test7()
{
	string s1("hello world");
	string s2("string");

	// C++98
	s1.swap(s2);// 效率高。交换指针指向
	swap(s1, s2);// 效率低。深拷贝交换

	cout << s1 << endl;
	cout << s1.c_str() << endl;

	// 要求取出文件的后缀
	string file("string.cpp.zip");
	size_t pos = file.find('.');
	//size_t pos = file.rfind('.');// 倒着找
	if (pos != string::npos)
	{
		//string suffix = file.substr(pos, file.size() - pos);
		string suffix = file.substr(pos);// 取pos位置后的字符串
		cout << file << "后缀:" << suffix << endl;
	}
	else
	{
		cout << "没有后缀";
	}

	string url1("https://cplusplus.com/reference/string/string/rfind/");
	string url2("https://www.baidu.com/");
	string& url = url1;

	// 协议 域名 uri
	string protocol;
	size_t pos1 = url.find("://");
	if (pos1 != string::npos)
	{
		protocol = url.substr(0, pos1);
		cout << "protocol:" << protocol << endl;
	}
	else
	{
		cout << "非法url" << endl;
	}
	string domain;
	size_t pos2 = url.find('/', pos1 + 3);// 从pos1+3的位置开始找
	if (pos2 != string::npos)
	{
		domain = url.substr(pos1+3, pos2-(pos1+3));
		cout << "domain:" << domain << endl;
	}
	else
	{
		cout << "非法url" << endl;
	}
	string uri = url.substr(pos2 + 1);
	cout << "uri:" << uri << endl;
}

3.5 string类非成员函数

+是传值返回,要少用

image-20220718125116496

输出

image-20220718125616074

输入

>>遇到空格或换行就停止

image-20220718125529780

getline遇到换行才停止

image-20220718125640571

3. 深浅拷贝

3.1 浅拷贝

默认生成的拷贝构造函数,就只能实现浅拷贝。

image-20220713165709694

image-20220713165931369

浅拷贝的危害:

  1. 指向同一块空间,修改数据会互相影响

  2. 析构时这块空间会被释放两次,程序崩溃

3.2 深拷贝

我们要自己实现深拷贝。

再开一块空间,安置s2

image-20220713170840683

image-20220713171610769

3.3 传统写法、现代写法的string类对比

class string
{
public:
	string(const char* str = "")
	{
		if (nullptr == str)
			str = "";
		str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
    
	// s2(s1)
	// 传统写法:自己干活
	/*string(const string& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}*/
	// 现代写法:让别人干活,然后窃取别人的劳动成果
	string(const string& s)
		: _str(nullptr)
	{
		string tmp(s._str);// tmp出函数之后会被析构,要先让_str置空,否则交换之后无法释放空间
		swap(_str, tmp._str);
	}
    
    
    // 传统写法:自己干活
    /*string& operator=(const string& s)
	{
		if (this != &s)
		{
			char* pStr = new char[strlen(s._str) + 1];
			strcpy(pStr, s._str);
			delete[] _str;
			_str = pStr;
		}
		return *this;
	}*/
   
	// 现代写法一:让别人干活,然后窃取别人的劳动成果,最后再让别人把地擦了再走(析构掉原对象)
	// 这里能直接析构,因为swap之后tmp是一定能直接释放的
    /*
	string& operator=(const string& s)
	{
		if (this != &s)
		{
			string tmp(s._str);
			swap(_str, tmp._str);
		}
		return *this;
	}
	*/
	// 现代写法2:在现代写法1的基础上,加了剥削要从娃娃抓起的思想(doge)
	// 传值传参,暗中调用拷贝构造,之后直接窃取成果
    // 自己给自己赋值也没事,因为调用时已经深拷贝了,木已成舟
	string& operator=(string s)
	{
		swap(_str, s._str);
		return *this;
	}
	
	~string()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
    
private:
	char* _str;
};

3.4 拓展:写实拷贝

为了减少深拷贝后又不使用的现象,造成的深拷贝资源浪费。

我们引入了写实拷贝:本质是浅拷贝+引用计数,解决析构问题;使用的时候再深拷贝,解决使用问题(其实还得深拷贝,只不过在赌你不用它)

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源 。有多少个使用者用就记多少,没人用再彻底析构,释放资源

4. string类模拟实现

库里面是用class string,为了区分,我们能把string写成大写String;或者增加一层命名空间,在命名空间里写string,不过在用的时候要记得调用命名空间里的string类

以下用的是第一种写法class String

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
// 先实现一个简单的string,只考虑资源管理深浅拷贝的问题
// 暂且不考虑增删查改
//class String
//{
//public:
//	String(const char* str)
//		: _str(new char[strlen(str)+1])// \0 // 在堆上开辟一段空间,把字符串拷贝过去,在堆上实现后续操作
//	{
//		strcpy(_str, str);
//	}
//
//	// s2(s1)
//	String(const String& s)
//		: _str(new char[strlen(s._str) + 1])
//	{
//		strcpy(_str, s._str);
//	}
//
//	// 不能直接拷贝的原因:空间不够大/空间浪费
//	// s1 = s3;
//	String& operator=(const String& s)
//	{
//		if (&s != this)// 防止自己给自己赋值:先把自己杀了再复活自己导致的随机值;或浪费资源
//		{
//			//delete[] _str;
//			//_str = new char[strlen(s._str) + 1];
//			//strcpy(_str, s._str);
//			
//			// 防止new失败,_str已经没了
//			char* tmp = new char[strlen(s._str) + 1];
//			strcpy(tmp, s._str);
//			delete[] _str;
//			_str = tmp;
//		}
//		return *this;
//	}
//
//	~String()
//	{
//		if (_str)
//		{
//			delete[] _str;
//		}
//	}
//
//	// const char* c_str(const string* const this) 
//	const char* c_str() const 
//	{
//		return _str;
//	}
//
//	char& operator[](size_t pos)
//	{
//		assert(pos < strlen(_str) + 1);// 要加一吗?我觉得要加一
//		return _str[pos];
//	}
//
//	size_t size()
//	{
//		return strlen(_str);
//	}
//
//private:
//	char* _str;
//};

// 考虑增删查改和使用
class String
{
public:
	typedef char* iterator;
	typedef const char* const_iterator;// 返回值也不能修改

	iterator begin()
	{
		return _str;
	}

	iterator end()
	{
		return _str + _size;
	}

	const_iterator begin() const
	{
		return _str;
	}

	const_iterator end() const
	{
		return _str + _size;
	}

	void swap(String& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

	//String()
	//	: _size(0)
	//	, _capacity(0)
	//{
	//	_str = new char[1];
	//	_str[0] = '\0';
	//}
	// 提供全缺省
	// "\0"("\0\0")  ""("\0")  '\0'(字符,ascii值是0,把0给str,错)
	String(const char* str = "")// 空字符串,默认常量字符串
		: _size(strlen(str))
		, _capacity(_size)
	{
		_str = new char[_capacity + 1];// 初始化列表的初始化顺序是按照声明顺序的
		strcpy(_str, str);
	}

	// s2(s1)
	// 传统写法:自己干活
	/*String(const String& s)
		: _size(s._size)
		, _capacity(s._capacity)
	{
		_str = new char[_capacity + 1];
		strcpy(_str, s._str);
	}*/

	// 现代写法:让别人干活,然后窃取别人的劳动成果
	String(const String& s)
		: _str(nullptr)
		, _size(0)
		, _capacity(0)
	{
		String tmp(s._str);// tmp出函数之后会被析构,要先让_str置空,否则交换之后无法释放空间
		swap(tmp);
	}


	 不能直接拷贝目标内容的原因:原空间不够大/空间浪费
	 s1 = s3;
	// 传统写法:自己干活
	//String& operator=(const String& s)
	//{
	//	if (&s != this)
	//	{
	//		//delete[] _str;
	//		//_str = new char[strlen(s._str) + 1];
	//		//strcpy(_str, s._str);

	//		// 防止new失败,_str已经没了
	//		char* tmp = new char[s._capacity + 1];
	//		strcpy(tmp, s._str);
	//		delete[] _str;
	//		_str = tmp;
	//		_size = s._size;
	//		_capacity = s._capacity;
	//	}
	//	return *this;
	//}

	// 现代写法1:让别人干活,然后窃取别人的劳动成果,最后再让别人把地擦了再走(析构掉原对象)
	// 这里能直接析构,因为swap之后tmp是一定能直接释放的
	 s1 = s3;
	/*String& operator=(const String& s)
	{
		if (this != &s)
		{
			String tmp(s._str);
			swap(tmp);
		}
		return *this;
	}*/

	// 现代写法2:让别人干活,然后窃取别人的劳动成果,最后再让别人把地擦了再走(析构掉原对象)
	// 传值传参,暗中调用拷贝构造,之后直接窃取成果
	String& operator=(String s)
	{
		swap(s);
		return *this;
	}


	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
	}

	// const char* c_str(const string* const this) 
	const char* c_str() const
	{
		return _str;
	}

	char& operator[](size_t pos)
	{
		assert(pos < _size);// 要加一吗?如果是只允许操作有效字符,就不用+1
		return _str[pos];
	}
	const char& operator[](size_t pos) const
	{
		assert(pos < _size);// 要加一吗?如果是只允许操作有效字符,就不用+1
		return _str[pos];
	}

	size_t size() const
	{
		return _size;
	}

	size_t capacity() const
	{
		return _capacity;
	}

	void reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

	// 三种情况:大于_capacity;大于_size  小于_capacity;小于_size
	void resize(size_t n, char ch = '\0')
	{
		// 删除部分数据,保留前n个    
		if (n < _size)
		{
			_size = n;
			_str[_size] = '\0';
		}
		else
		{
			if (n > _capacity)
			{
				reserve(n);		
			}
			// 填充
			for (size_t i = _size; i < n; i++)
			{
				_str[i] = ch;
			}
			_size = n;
			_str[_size] = '\0';
		}
	}

	void push_back(char ch)
	{
		/*if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';*/
		insert(_size, ch);
	}

	String& operator+=(char ch)
	{
 		push_back(ch);
		return *this;
	}

	String& operator+=(const char* str)
	{
		append(str);
		return *this;
	}

	void append(const char* str)
	{
		/*size_t len = _size + strlen(str);
		if (len > _capacity)
		{
			reserve(len);
		}
		strcpy(_str + _size, str);
		_size = len;*/
		insert(_size, str);
	}

	String& insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		// 移动
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		_str[pos] = ch;
		_size++;

		return *this;
	}

	String& insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}

		size_t end = _size + len;
		while (end > pos + len -1)
		{
			_str[end] = _str[end - len];
			--end;
		}
		strncpy(_str + pos, str, len);
		_size += len;

		return *this;
	}

	String& earse(size_t pos, size_t len = npos)
	{
		assert(pos < _size);
		if (len == npos || len + pos >= _size)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			size_t begin = pos + len;
			while (begin <= _size)
			{
				_str[begin - len] = _str[begin];
				++begin;
			}
			_size -= len;
		}
		return *this;

	}

	size_t find(char ch, size_t pos = 0)
	{
		for (;pos < _size; ++pos)
		{
			if (_str[pos] == ch)
			{
				return pos;
			}
		}
		return npos;
	}

	size_t find(char* str, size_t pos = 0)
	{
		const char* p = strstr(_str + pos, str);
		if (p ==nullptr)
		{
			return npos;
		}
		return p - _str;
	}

	void clear()
	{
		_str[0] = '\0';
		_size = 0;
	}


private:
	char* _str;
	size_t _size;// 有效字符的个数(不计\0)
	size_t _capacity;// 存储有效字符的空间

	//const static size_t npos = -1;// 这样也能通过,但这本质是声明
	const static size_t npos;

};

const  size_t String::npos = -1;

ostream& operator<< (ostream& out, const String& s)
{
	//out << s.c_str();
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

istream& operator>> (istream& in,  String& s)
{
	//char ch;
	in >> ch;// cin>> 拿不到空格或换行
	//ch = in.get();
	//while (ch != ' ' && ch != '\n')
	//{
	//	s += ch;
	//	ch = in.get();
	//}
	//return in;

	// 清理缓冲区
	s.clear();
	char ch;
	//in >> ch;// cin>> 拿不到空格或换行
	ch = in.get();
	char buff[128] = { '\0'};
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)//要留一个位置装\0
		{
			s += buff;
			memset(buff, '\0', 128);
			i = 0;
		}
		ch = in.get();
	}
	s += buff;
	return in;
}

bool operator<(const String& s1, const String& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const String& s1, const String& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator>(const String& s1, const String& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) > 0;
}
bool operator<=(const String& s1, const String& s2)
{
	return !(s1 > s2);
}
bool operator>=(const String& s1, const String& s2)
{
	return !(s1 < s2);
}
bool operator!=(const String& s1, const String& s2)
{
	return !(s1 == s2);
}

相关文章:

  • (一) springboot详细介绍
  • (一)UDP基本编程步骤
  • (附源码)spring boot公选课在线选课系统 毕业设计 142011
  • 新作文杂志新作文杂志社新作文编辑部2022年第8期目录
  • d的nan讨论4
  • Python 运算符和表达式
  • 【LeetCode】2022 8月 每日一题
  • AcWing-1-递归实现指数型枚举
  • 易基因|文献科普:DNA甲基化测序揭示DNMT3a在调控T细胞同种异体反应中的关键作用
  • 基于springboot小型车队管理系统毕业设计源码061709
  • 大数据ClickHouse进阶(一):ClickHouse使用场景和集群安装
  • js面向对象之封装,继承,多态,类的详解
  • 永久免费H5直播点播播放器SkeyeWebPlayer.js实现webrtc流播放
  • JavaScript-HelloWorld、浏览器控制台使用、数据类型
  • Centos部署Docker
  • Apache的80端口被占用以及访问时报错403
  • ECMAScript入门(七)--Module语法
  • hadoop集群管理系统搭建规划说明
  • Javascript基础之Array数组API
  • JavaScript设计模式系列一:工厂模式
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • NSTimer学习笔记
  • WordPress 获取当前文章下的所有附件/获取指定ID文章的附件(图片、文件、视频)...
  • 电商搜索引擎的架构设计和性能优化
  • 工程优化暨babel升级小记
  • 面试遇到的一些题
  • 微信开放平台全网发布【失败】的几点排查方法
  • 问题之ssh中Host key verification failed的解决
  • 我的业余项目总结
  • PostgreSQL 快速给指定表每个字段创建索引 - 1
  • 阿里云移动端播放器高级功能介绍
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • #NOIP 2014# day.2 T2 寻找道路
  • (2)MFC+openGL单文档框架glFrame
  • (C语言)fgets与fputs函数详解
  • (二)学习JVM —— 垃圾回收机制
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (每日持续更新)jdk api之FileFilter基础、应用、实战
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • (转)微软牛津计划介绍——屌爆了的自然数据处理解决方案(人脸/语音识别,计算机视觉与语言理解)...
  • **Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .gitignore
  • .gitignore文件---让git自动忽略指定文件
  • .net Application的目录
  • .NET Core/Framework 创建委托以大幅度提高反射调用的性能
  • .NET开源项目介绍及资源推荐:数据持久层 (微软MVP写作)
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .sh 的运行
  • @cacheable 是否缓存成功_Spring Cache缓存注解
  • @RequestMapping-占位符映射
  • [ solr入门 ] - 利用solrJ进行检索