C++ 自定义String类

  • 思考
  • 类的声明
  • _CompareLength
  • _SetString
  • _MoveString
  • _UnSafeMoveString
  • 构造函数和析构函数
  • 重载 = 操作符
  • 重载+=操作符
  • 重载+操作符
  • 重载 [] 中括号
  • 重载==操作符
  • 重载输入输出流
  • 完整代码

想要实现高级的 String 类还挺复杂的,C++ 和 C# 不一样,许多模块都需要自己来解决内存问题,并且如何提高内存空间的利用效率,调用函数是否触发拷贝构造函数,触发拷贝构造函数时又该如何避免重复的内存释放。

代码已经写好了并测试修正(但不排除个别案例没有测试…)

思考

首先定需求,要实现一个什么样的 String,然后设计实现方案,要怎么是实现这个 String,接着是怎么去优化这个 String,最后不断测试不断解决 bug。

需求方面可以直接用std::string来思考要实现什么代码,同时也可以学习到哪些操作是不合法的
比如构造字符串:

string s1;
string s2 = string(); // string s2();是不合法的
string s3 = "s3";
string s4(string("s4"));

其中,

string s1 = "s1";

由于太久没写面向对象了,最初,我以为这一行的实现是:触发 “= (char*)” 生成临时的 string temp,再将 string temp 的内存拷贝到 string s1,实现后才记起来这行等价于 string s1(“s1”),此时s1还未初始化,编译器会默认使用构造函数生成s1

类的声明

思考:如果要实现一个自定义的字符串,那么可以用字符数组模拟,由于这个是 C++,就不用结构体实现,使用一个类封装字符数组,再使用成员函数对其操作。
这个字符串类主要实现:
1.储存字符串的字符数组 char* _str,初始值设为 NULL
2.可以引用字符数组,c_str(),用法与 string.c_str() 一致,是const修饰
3.字符串长度 _size
4.length() 获得长度
5.限制字符串最大长度,必要的话发生字符串过长时抛出异常
6.支持 s[i] = char 和 char = s[i],要防止下标越界
7.支持 cin 和 cout 输入输出字符串,字符串输入要防止字符串长度超过最大长度
8.构造函数能实现字符串的实例化,并提高性能,避免不必要的拷贝操作(实现 move 操作,只转移内存所有权,不拷贝内存)
9.析构函数要释放申请的内存
10.实现赋值,s1 = s2, s3 = “s3”, s4 = move(s3)
11.实现 += 操作,s1 += ‘1’, s2 += “123”, s3 += s2
12.实现 + 操作,调用了 += 即可,但是要注意内存效率(还是 move)
13.实现 == 判断

mystring.h

#pragma once
#include <iostream>
#include <string.h>
#include <istream>
#include <ostream>
#include <iomanip>
//#include <stdexcept>
//#include <cassert> class String {private:char* _str = NULL; // 储存字符串size_t _size = 0; // 字符串长度static constexpr size_t _LIMIT_STRING_SIZE = 2048; // TODO: 限制字符串最大字符长度bool _SetString(const char* str); // 写入字符数组bool _MoveString(char* &str); // 移动字符数组bool _UnSafeMoveString(char*& str, size_t& size); // 不安全移动字符数组const bool _CompareLength(size_t& size); // 判断目标大小是否超出字符串的最大长度
public:// 默认构造函数String();// 传入 const char* 的拷贝构造函数String(const char* str);// 传入 const String& 的拷贝构造函数String(const String& other);// 传入 String&& 的 move 拷贝构造函数String(String &&other) noexcept;// 析构函数virtual ~String();// 重载 [] 操作符//char operator[](size_t index);char& operator[](size_t index);// 重载输入输出流friend std::istream& operator >> (std::istream& cin, String& str);friend std::ostream& operator << (std::ostream& cout, String& str);//返回自身地址String* GetAddress() { return this; }// 返回克隆String GetCopy() { return *this; }// 返回本身String& GetOriginal() { return *this; }// 返回字符串本身的字符数组地址,加上const防止被修改const char* c_str() { return _str; }// 返回字符串长度const size_t length() { return _size; }// 重载 = 操作符String& operator =(const char* str);String& operator =(char* &&str);String& operator =(const String& other);String& operator =(String &&other) noexcept; // TODO: Unsafe// 重载 + 操作符, 返回的是临时对象,不需要使用引用const String operator +(const char ch);const String operator +(const char* str);const String operator +(const String& other);// 重载 += 操作符String& operator +=(const char ch);String& operator +=(const char* str);String& operator +=(const String& other);// 重载 == 操作符const bool operator ==(const char* str);const bool operator ==(const String& other);
};

上面的内容应该把大部分的东西都涵盖,有一些的声明和实现已经做了测试改进,有可能存在没测出来的bug和未写好的功能,欢迎指点

_CompareLength

判断目标大小是否超出字符串的最大长度,调用频率比较高
由于不修改类成员,因此加上const

// 判断目标大小是否超出字符串的最大长度
const bool String::_CompareLength(size_t& size)
{return size + 1 > String::_LIMIT_STRING_SIZE;
}

_SetString

_SetString函数 是用来拷贝字符数组,因为在函数执行构成中拷贝字符数组过于频繁,因此为了降低代码量,对其进行了封装。当然不一定是每个拷贝都需要调用该函数。
为了保证安全性,我加入了空指针判断和最大字符串长度判断,如果开发者可以保证传入的数据合法,可以不使用它们。

// 写入字符数组
// String._str <- char* str
inline bool String::_SetString(const char* str)
{// 传入空指针if (str == NULL) {_size = 0;_str = NULL;return false;}else {size_t size = strlen(str);// 超出字符串最大限制长度if (_CompareLength(size)) {throw std::bad_alloc();return false;}// 释放原空间if (_str != NULL) {delete[] _str;_str = NULL;}// 拷贝字符数组_size = size;_str = new char[_size + 1];strcpy(_str, str);_str[_size] = '\0';}return true;
}

_MoveString

_MoveString 函数是在 _SetString 函数的基础上进行改进,_SetString 函数可以拷贝字符数组,但是有些情况下会造成空间浪费。
比如有一个 String s,我们需要把一个 new 出来的变量 char* str “仅拷贝” 到 s 中后立即 delete[] str,那么调用 _SetString 函数后,new了新空间并拷贝字符数组,再去释放 str 的内存空间,这就直接造成了一种空间效率降低,这是一种多余的操作,因为我们可以直接令 s 的字符数组指针指向str。
上面这段话其实就是在说 std::move 思想的重要性,std::move 可以将一个左值引用强制转为右值引用,只是转移状态没有转移内存

下面就是对 move 的一个实现,由于之前没用过 move 函数,我希望能通过用 _str = std::move(str) 来转移 str 的内容,但可能因为move作用于对象,所以最后仍然需要str = NULL,最后我用回了_str = str。

// 移动字符数组
// String._str <- move(char*& str)
inline bool String::_MoveString(char* &str)
{// 传入空指针if (str == NULL) {_size = 0;_str = NULL;return false;}else {size_t size = strlen(str);// 超出字符串最大限制长度if (_CompareLength(size)) {throw std::bad_alloc();return false;}// 释放原空间if (_str != NULL) {delete[] _str;_str = NULL;}// 移动地址_size = size;_str = str;  // TODO: _str = std::move(str);_str[_size] = '\0'; // TODO: 可能上面的strlen就炸了str = NULL; // 将原字符数组的地址变为NULL}return true;
}

_str[_size] = ‘\0’; 只是防止出现某种问题,但是可能在这行之前 strlen 函数就炸了

_UnSafeMoveString

_MoveString 的不安全写法

// 不安全移动字符数组
// (String._str, String._size) <- move(char*& str, size_t&)
inline bool String::_UnSafeMoveString(char* &str, size_t& size)
{// 释放原空间if (_str != NULL) {delete[] _str;_str = NULL;}// 移动地址_str = str;_size = size;str = NULL; // 将原字符数组的地址变为NULLsize = 0; // 将原长度变为0return true;
}

构造函数和析构函数

// 默认构造函数
String::String() {}// 传入 const char* 的拷贝构造函数
String::String(const char* str)
{_SetString(str);
}// 传入 const String& 的拷贝构造函数
String::String(const String& other)
{_SetString(other._str);
}// 传入 String&& 的 move 拷贝构造函数
String::String(String &&other) noexcept
{if (_str != NULL) {delete[] _str;_str = NULL;}_MoveString(other._str);//std::cout << (other._str == NULL); // 验证 move 是否实现
}// 析构函数
String::~String()
{if (_str != NULL) {delete[] _str;_str = NULL;}
}

测试代码,注释表示触发的函数,-> 表示触发的先后。
匿名函数 CreateTempString 表示生成一个临时的String()。
匿名函数 CreateMoveString 表示生成一个只有右值引用状态的String(),目的是触发测试 String::String(String&&) 拷贝构造函数。

(测试内容是模仿 string,可以先用 string 写,然后文本替换成 String 类。)

(PS:在优化代码之前,我的代码烂到会在拷贝构造函数中进入无限递归的死循环,简而言之,不想一行代码触发2个以上的构造函数,需要精心研磨、不断优化)

int main(){auto CreateTempString = []()->String {return String("temp");};auto CreateMoveString = []()->String {return std::move(String("move"));};String s1; //String()String s2 = String(); // String()String s3 = "s3"; // String(const char* str)String s4(String("s4")); // String(const char* str)String* s5 = new String(); // String()String* s6 = new String("s6"); // String(const char* str)String s7(*s6); // String(const String& s)delete s5; // ~String:String()delete s6; // ~String:String(const char* str)// 删除s6不会修改s7中的内容String s8 = String(String("s8")); // String(const char* str)String s9 = s8; // String(const String& s)String s10 = CreateTempString(); // String(const char* str)String s11 = CreateMoveString(); // String(const char* str) -> String(String &&other) -> ~String:String(const char* str)// s11: ~String:String(String &&other)// s10: ~String:String(const char* str)// s9: ~String:String(const String &s)// s8: ~String:String(const char* str)// s7 ~String:String(const String &s)// s4 ~String:String(const char* str)// s3 ~String:String(const char* str)// s2 ~String:String()// s1 ~String:String()return 0;
}

重点提一下 s11,
CreateMoveString 返回一个 std::move(String) 的临时变量并且返回的是一个右值。
String&& 表示右值引用,并且只能绑定右值,适合用在临时变量和即将销毁的变量。
这两个组合一下,就能触发 String::String(String &&other) 拷贝构造函数,并且 s11 的内存效率比 s10 高,原理可以看 _MoveString。

String&& 参考博客:https://blog.csdn.net/Yeluorag/article/details/52423343

下面的代码等价于上面的,这样写就是少了几个if判断,但是一些代码的安全和准确就要由程序员来判断。

// 传入 String&& 的 move 拷贝构造函数
String::String(String &&other) noexcept
{if (_str != NULL) {delete[] _str;_str = NULL;}_size = other._size;_str = other._str;other._size = 0;other._str = NULL;
}

重载 = 操作符

待测试项1

String s1;
String s2 = s1;

待测试项2

String s3;
s3 = s1;

在构造函数的部分我没有写第二种的测试,是因为这部分涉及 = 赋值操作。
代码1中,此时 s2 未被实例化,也就是还没分配内存,因此会触发拷贝构造函数,等价于 String s2(s1)
代码2中,s3 已被实例化/触发默认构造函数,s3=s1只会触发赋值操作。

知识补充1:
那么如果代码中没有重载 = 操作符,VS 会提示 s3=s1 位置 “尝试引用已删除的函数”,因此必须重载 = 操作符。而触发 “尝试引用已删除的函数” 的情况有:

  • 含有非静态的const成员变量
  • 含有非静态的reference成员变量
  • 含有不能被拷贝的基类
  • 含有用户定义的移动构造函数或者移动赋值函数
  • 含有不能被拷贝的成员变量

应该是最后两种符合条件。

知识补充2:
如果返回值是在重载函数里临时定义的变量,则返回值类型后面不能加&,如果返回值是不会被回收内存的*this、this、函数中引用调用的参数,则返回值类型后面可以加&。赋值是返回自身,因此函数返回值类型就是String&

同样的模仿string的操作,于是就有下面的代码:

// 重载 = 操作符,直接拷贝字符数组
String& String::operator =(const char* str)
{// 检查自赋点if (_str != str) {// 写入字符数组_SetString(str);}return *this;
}// 重载 = 操作符,move字符数组
String& String::operator =(char* &&str)
{// 检查自赋点if (_str != str) {// move字符数组_MoveString(str);}return *this;
}// 重载 = 操作符,直接拷贝类中的字符数组
String& String::operator =(const String& other)
{// 检查自赋点if (this != &other) {// 写入字符数组_SetString(other._str);}return *this;
}// 重载 = 操作符,传入 String&& 的 move 赋值函数
String& String::operator =(String&& other) noexcept
{// 检查自赋点if (this != &other) {_MoveString(other._str);}return *this;
}

测试 String& String::operator =(const char* ):

int main()
{String s12 = "s12"; // String(const char* str)String s13; // String()s13 = "s13"; // operator =(const char* str)// s13: ~String:String()// s14: ~String:String(const char* str)return 0;
}

测试 String& String::operator =(char* &&):
只有传入右值才会触发

int main()
{char* s16 = new char[4]{ "s16" };String s17 = s16; // String(const char* str)std::cout << "s16:" << (s16 != NULL) << "\ts17:" << s17 << std::endl; // s16:1   s17 : s16String s18; // String()s18 = s16; // operator =(const char* str)std::cout << "s16:" << (s16 != NULL) << "\ts18:" << s18 << std::endl; // s16 : 1   s18 : s16s18 = std::move(s16); // operator =(char* &&str)std::cout << "s16:" << (s16 != NULL) << "\ts18:" << s18 << std::endl; // s16 : 0   s18 : s16if (s16 != NULL) {std::cout << "s16 != NULL" << std::endl;delete[] s16;}// s18: ~String:String()// s17: ~String:String(const char* str)return 0;
}

测试 String& String::operator =(const String&) 和 String& String::operator =(String&&):

int main()
{String s19; // String()s19 = CreateTempString(); // String(const char* str) -> operator =(String &&other) -> ~String:String(const char* str)String s20; // String()s20 = CreateTempString(); // String(const char* str) -> operator =(String && other) -> ~String:String(const char* str)// s20: ~String:String()// s19: ~String:String()return 0;
}

下面是错误写法,会进入无限递归的死循环,我很自然的以为 *this=String(…) 可以实现我想要的拷贝,但是会不断触发第三个拷贝构造而进入死循环递归

//重载 = 操作符
//错误写法,会进入无限递归的死循环
String& String::operator =(const char* str)
{*this = String(str);return *this;
}String& String::operator =(const String& other)
{*this = String(other);return *this;
}String& String::operator =(String&& other) noexcept
{*this = String(other);return *this;
}

重载+=操作符

// 重载 += 操作符
// (String) Elem1 += (char) Elem2
String& String::operator +=(const char ch)
{if (_str == NULL) {throw std::exception("NullPointerException", NULL);}size_t t_size = _size + 1;// 超出字符串最大限制长度if (_CompareLength(t_size)) {throw std::runtime_error("Null Pointer");}char* t_str = new char[t_size + 1];strcpy(t_str, _str);  // 拷贝原字符数组t_str[t_size - 1] = ch; // 拷贝新字符t_str[t_size] = '\0';_UnSafeMoveString(t_str, t_size);return *this;
}// 重载 += 操作符
// (String) Elem1 += (char[]) Elem2
String& String::operator +=(const char* str)
{if (_str == NULL) {throw std::runtime_error("Null Pointer");}if (str == NULL) {throw std::runtime_error("Null Pointer");}size_t t_size = strlen(str) + _size;// 超出字符串最大限制长度if (_CompareLength(t_size)) {throw std::bad_alloc();}char* t_str = new char[t_size + 1];// 拷贝旧新字符数组strcpy(t_str, _str); // 拷贝原字符数组strcpy(t_str + _size, str); // 拷贝新字符数组t_str[t_size] = '\0';_UnSafeMoveString(t_str, t_size);return *this;
}// 重载 += 操作符
// (String) Elem1 += (String) Elem2
String& String::operator +=(const String& other)
{if (_str == NULL) {throw std::runtime_error("Null Pointer");}if (other._str == NULL) {throw std::runtime_error("Null Pointer");}*this += other._str;return *this;
}

测试 String& operator +=(const char ch);

int main()
{String s21 = "s2" ; // String(const char* str)s21 += '1'; // operator +=(const char ch)std::cout << s21 << std::endl; // s21// ~String:String(const char* str)return 0;
}

测试 String& operator +=(const char* str);

int main()
{String s22 = "s"; // String(const char* str)s22 += "22"; // operator +=(const char* str)std::cout << s22 << std::endl; // s22//~String:String(const char* str)return 0;
}

测试 String& operator +=(const String& other);

int main()
{String s23 = "s23"; // String(const char* str)String s24 = "s24"; // String(const char* str)s23 += s24; // operator +=(const String& other) -> operator +=(const char* str)std::cout << s23 << std::endl; // s23s24// s24 ~String:String(const char* str)// s23 ~String:String(const char* str)return 0;
}

重载+操作符

+ 操作函数会返回一个临时变量,那么返回值类型后面就不能加 &,并且这个临时变量是对象,返回后马上被编译器析构掉,为了提高内存效率,将返回值套上 move 函数转为右值,那么就不会触发拷贝内存的构造函数,触发的是移动内存所有权的 move 构造函数。

// 重载 + 操作符
// (String) Elem1 + (char) Elem2
const String String::operator +(const char ch)
{String _temp(*this);_temp += ch;return std::move(_temp);
}// 重载 + 操作符
// (String) Elem1 + (char*) Elem2
const String String::operator +(const char* str)
{String _temp(*this);_temp += str;return std::move(_temp);
}// 重载 + 操作符
// (String) Elem1 + (String) Elem2
const String String::operator +(const String& other)
{String _temp(*this);_temp += other;// return _temp; // 会触发拷贝构造return std::move(_temp);
}

测试

int main()
{String s25 = "s25"; // String(const char* str)String s26 = s25 + '6'; // operator +(const char ch) -> String(const String& s) // -> operator +=(const char ch) -> String(String &&other) -> ~String:String(const String& s)std::cout << s26 << std::endl; // s256String s27 = s25 + "s27";// operator +(const char* str) -> String(const String& s)// -> operator +=(const char* str) -> String(String&& other) -> ~String:String(const String & s)std::cout << s27 << std::endl; // s25s27String s28 = s26 + s27;// operator +(const String& other) -> String(const String& s)// -> operator +=(const String& other) -> operator +=(const char* str)// -> String(String&& other) -> ~String:String(const String & s)std::cout << s28 << std::endl; // s256s25s27String s29; // String()s29 = s26 + s27;// operator +(const String& other) -> String(const String& s)// -> operator +=(const String& other) -> operator +=(const char* str)// -> String(String&& other) -> ~String:String(const String & s)// -> operator =(const String & other) -> ~String:String(String && other)std::cout << s29 << std::endl; // s256s25s27//s29: ~String:String()//s28: ~String:String(String&& other)//s27: ~String:String(String&& other)//s26: ~String:String(String&& other)//s25: ~String:String(const char* str)return 0;
}

重载 [] 中括号

// 重载 [] 操作符
// (char)Elem1 = (String) Elem2[index]
// (String)Elem1[index] = (char) Elem2
char& String::operator [](size_t index)
{if (index >= _size)    {throw;}return _str[index];
}

函数返回值可以作为左值,因此必须加&,并且不能加 const。
测试

int main()
{String s30 = "s30";const char* s31 = s30.c_str();std::cout << s31[0] << s31[1] << s31[2] << std::endl; // s30std::cout << (s31[3] == '\0') << std::endl; // 1std::cout << s30[0] << s30[1] << s30[2] << std::endl; // s30std::cout << (s30[3] == '\0') << std::endl; // 程序中断return 0;
}

重载==操作符

// 重载 == 操作符
// (String) Elem1 == (char[]) Elem2
const bool String::operator ==(const char* str)
{if (_str == NULL || str == NULL){return false;}if (_str == str) {return true;}if (_size != strlen(str))return false;return (strcmp(_str, str) == 0);
}// 重载 == 操作符
// (String) Elem1 == (String) Elem2
const bool String::operator ==(const String& other)
{if (*this == other) {return true;}return (_str == other._str);
}

重载输入输出流

// cin >> (String) Elem
std::istream& operator >>(std::istream& cin, String& str)
{// new一个字符数组,将字符数组move到String中char* t_str = new char[String::_LIMIT_STRING_SIZE];cin >> std::setw(String::_LIMIT_STRING_SIZE) >> t_str;str._MoveString(t_str);//if (t_str == NULL)std::cout << "NULL"; // 检测move函数是否实现return cin;
}// 重载输出流
// cout << (String) Elem
std::ostream& operator <<(std::ostream& cout, String& str)
{// 检查字符数组非NULL指针if (str.c_str() != NULL) {cout << str.c_str();}return cout;
}

完整代码

完整的代码我丢在我的github了,需要自取
https://github.com/Yundi339/MyC/tree/master/mystring

不足之处
1.String s 初始化,我设置了字符数组为NULL,string默认应该是字符数组="",这块我特意没改。
2.String 默认最大长度为2048,照理来说,字符串应该有两个长度,一个是已分配元素长度_size,另一个是当前可储存最大长度_maxsize,两个都是可变长变量。
s1+=s2:
如果相加长度不超过s1._maxsize 则 s2 可以直接拷贝到s1;
如果相加长度超过s1._maxsize,就要new一个新字符数组,字符数组长度可以是相加长度的1.5倍,然后拷贝s1和s2并释放s1,接着s1=move(s3)

以上就是我写的自定义String类全部内容,欢迎大佬们指点。

C++ 自定义String类相关推荐

  1. C++ 自定义string类 重载相关运算符

    c++提供的string类 c++提供的string类可以做很多事情,它本质上是也是类,它的很多运算符都是通过重载进行实现的. 今天我们自己来简单来实现MyString类来模拟 string类能做得一 ...

  2. c++string 加引号_C++|引用计数与shared_ptr智能指针(以实现String类为例)

    C++ 中,动态内存的管理是通过一对运算符来完成的,new 用于申请内存空间,调用对象构造函数初始化对象并返回指向该对象的指针.delete接收一个动态对象的指针,调用对象的析构函数销毁对象,释放与之 ...

  3. 如何实现一个具备基本功能函数的C++的自定义String字符串类

    在这篇文章,我们创造一个自定义的C++字符串类,它具备基本的string操作功能. 为什么要创建自己的字符串类呢?其中的一个原因是,当你想要让自己的字符串类型区别于一般的string时,你就需要自定义 ...

  4. 掌握 ASP.NET 之路:自定义实体类简介

    发布日期 : 5/24/2005| 更新日期 : 5/24/2005 Karl Seguin Microsoft Corporation 摘要:有些情况下,非类型化的 DataSet 可能并非数据操作 ...

  5. C++知识点42——下标运算符[]的重载及string类的实现

    一.下标运算符的重载 1.概念 如果一个类表示容器,那么要重载下标运算符[],下标运算符必须是成员函数.下表访问运算符通常要有一个const版本和一个非const版本.如果不定义const版本,那么c ...

  6. 13.6 Thread类自定义线程类

    package cn.chen.thread; /** * 线程:* 多线程:* 一个java程序只是有两个线程:* 一个是主线程负责main方法代码执行,一个是垃圾回收器线程,负责* 创建线程的方式 ...

  7. RocketMQ-初体验RocketMQ(11)-过滤消息_自定义Java类筛选消息

    文章目录 概述 集群信息 项目结构 生产者 自定义类 消费者 测试结果 概述 RocketMQ-初体验RocketMQ(10)-过滤消息_SQL92表达式筛选消息 通过SQL92的方式,消费者可以过滤 ...

  8. java kafka 设置分区_Java kafka如何实现自定义分区类和拦截器

    Java kafka如何实现自定义分区类和拦截器 2.producer配置文件指定,具体的分区类 // 具体的分区类 props.put(ProducerConfig.PARTITIONER_CLAS ...

  9. kettle中java组件_kettle系列-[KettleUtil]kettle插件,类似kettle的自定义java类控件

    该kettle插件功能类似kettle现有的定义java类插件,自定java类插件主要是支持在kettle中直接编写java代码实现自定特殊功能,而本控件主要是将自定义代码转移到jar包,就是说自定义 ...

  10. 自定义工具类:工具类介绍

    自定义工具类 自定义注解 自定义注解 package com.learn.domain.poi;import java.lang.annotation.ElementType; import java ...

最新文章

  1. 何时使用margin和padding?
  2. 大多数人都不知道,人类基因组正在衰败
  3. 4键电子手表说明书_电子手表怎么调(电子手表的四个键的功能各是什么)
  4. 【Android 逆向】函数拦截 ( GOT 表数据结构分析 | 函数根据 GOT 表进行跳转的流程 )
  5. 解决Extjs中textarea不支持keyup事件的问题
  6. OpenGL text rendering文字渲染的实例
  7. 2022年移动应用趋势洞察白皮书
  8. Centos-检查文件系统并尝试修复-fsck
  9. java中 resource_Java中如何获取resource的源码分析
  10. 小白记事本--链表--loading
  11. TeeChart Pro FOR delphi 年底稳定版
  12. 重读《从菜鸟到测试架构师》-- 构建的过程
  13. 每个c语言程序文件的编译错误被分为什么,已打印中央电大C语言考试题库(c语言小题+编程)...
  14. linux忽略大小写 grep,linux grep不区分大小写查找字符串方法
  15. gRPC快速入门(三)——Protobuf应用示例
  16. 齐博x1 换服务器如何转移网站
  17. 国内镜像下载python文件
  18. 创业分享:创业的过程就是坚持的过程
  19. Google学术搜索 学术论文搜索的利器
  20. CPU锁频率在0.78 GHz

热门文章

  1. 计算机应用基础练习题库与答案
  2. 计算机语言echo off什么意思,批处理文件的@echo off是什么意思?
  3. 博饼游戏奖项积分设置
  4. pythonQQ连连看秒杀脚本
  5. JJ斗地主记牌器java开发,【欢乐斗地主记牌器制作】遇到两个问题
  6. python优化网站_利用python做seo优化
  7. 区间多目标优化算法IP-MOEA
  8. EXP-00091 Exporting questionable statistics问题解决
  9. 影响摄像头移动侦测灵敏度因素
  10. python实现海康sdk二次开发,移动侦测事件(一)