版本:redis - 5.0.4
参考资料:redis设计与实现
文件:src下的sds.c sds.h

  • 一、c语言中的字符串 vs SDS
  • 二、sds.h
    • 1、sdshdr
      • header
      • sds的存储
      • 操作宏定义
    • 2、static inline 函数
    • 3、SDS API
  • 三、sds.c
    • 1、sdsIncrLen
    • 2、sdsull2str
    • 3、sdscatvprintf
    • 4、sdssplitlen
    • 5、sdsnewlen

一、c语言中的字符串 vs SDS

c中无字符串,只有字符数组

//c语言中定义一个字符串
//申请一段连续数组空间,s指向首地址,每个单元存储一个字符,'\0'为结束标志
// {'h','e','l','l','o','\0'}
char* s = "hello"
//sds
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; //已使用的数组长度,不包括零结束标志uint8_t alloc; //总长度,不包括零结束标志unsigned char flags; //表示选择哪种sds headerchar buf[];//用于存储字符串
};
  1. 获取字符串长度需要运算:没有字段存储字符串长度,需要运算。申请空间大小≠字符串长度。
  2. 非二进制安全:C语言中,以空字符‘\0’作字符串结束标志。
    • sds延续了这种做法,兼容部分c字符串函数。
    • 字符串里不能包含空字符。sds以处理二进制的方式处理数据。
  3. 不可修改:sds同样用字符数组存储字符串。字符串修改时,字符串增长或缩短,内存需要重分配
    • 空间预分配:字符串增长时并需要空间扩展时,若增长后的 len < 1M,则再分配同样大小的avail空间,也就是说buf数组的实际长度变为len*2 + 1字节(一字节存储空字符)。若增长后的len >= 1M,则再分配1M大小的avail空间,buf数组的实际长度变为len + 1M + 1byte;
    • 惰性空间释放:字符串缩短时,并不释放多出来的字节,而是记录下来,等待将来使用。
c字符串 sds
获取字符串长度的复杂度为 O(N) 获取字符串长度的复杂度为 O(1)
API 是不安全的,可能会造成缓冲区溢出 API 是安全的,不会造成缓冲区溢出
修改字符串长度 N 次必然需要执行 N 次内存重分配 修改字符串长度 N 次最多需要执行 N 次内存重分配 ,内存分配次数减少,可动态扩容
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有 <string.h> 库中的函数 可以使用一部分 <string.h> 库中的函数

二、sds.h

1、sdshdr

header

sdshdr(sds header)用于控制字符串,存储了字符数组buf,字符串长度len(实际使用的的长度),总长度alloc(数组长度减去存放空字符的1的字节)。

由于字符串长度不同,所以表示长度的len和alloc所需要的大小也不同,有四种:

  • 1字节 uint8_t
  • 2字节 uint16_t
  • 4字节 uint32_t
  • 8字节 uint64_t
    关于 size_t 的更多知识

由此产生了五种header ,通过选择合适的header,用来节省空间。并且header里需要存储一个flag,表明选择了哪种header。flag占一个字节,8位,而五种header只需用三位表示就行,所以还有五位没有用。

 __attribute__((packed)) :编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。__attribute__可以设置函数属性、变量属性和类型属性使用格式为:__attribute__ ((attribute-list));要求放于声明的尾部“;”之前。

关于 __attribute__的更多内容

typedef char *sds;/* Note: sdshdr5 is never used, we just access the flags byte directly.* However is here to document the layout of type 5 SDS strings. *//*注意: 永远不会使用 sdshdr5, 我们只需直接访问标志字节。但是, 这里是文件类型 5 SDS 字符串的布局。*/
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; //3位存header类型, (高)5位存字符长度char buf[];//字符最长为2^5 = 31//所以sdshdr5 没有alloc,
};struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; //已使用的数组长度uint8_t alloc; //总长度不包括零结束标志unsigned char flags; //表示选择哪种sds headerchar buf[];//用于存储字符串
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* used */uint16_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};//一共五种header 用三位表示
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
sds的存储


由于在一个结构体里面,而且关闭了自动对齐,此时的header内的各个部分都是紧紧挨在一起的,没有多余空间。
在一开始申请空间时,buf只是一个标志,证明那有一个数组,但并不给其分配空间,只给其他的部分分配空间。

操作宏定义
/**
* 宏定义中的##是将两个符号连接成一个,如T=5时,sdshdr和T合成sdshdr5
* 将该地址赋给了一个新变量sh,得到了一个指向header的指针。
* T是type(哪一种header),s是字符串指针(一个字符串的首地址)
* sizeof(struct sdshdr##T)是一个header大小(不包括buf),
* (s)-(sizeof(struct sdshdr##T))就是首地址减header的大小,即header的首地址。
*/
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));//得到header的首地址
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))//f为flag, f左移三位,得到高五位,就是字符长度
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

2、static inline 函数

下面,在.h文件中定义了一些函数,这些函数是static inline的。
inline:把函数指定为内联函数。

#define SDS_TYPE_MASK 7//掩码
#define SDS_TYPE_BITS 3//得到s的长度
static inline size_t sdslen(const sds s) {unsigned char flags = s[-1];//flag和掩码相与,只留后三位,判断类型//根据类型,得到header,获取长度switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->len;case SDS_TYPE_16:return SDS_HDR(16,s)->len;case SDS_TYPE_32:return SDS_HDR(32,s)->len;case SDS_TYPE_64:return SDS_HDR(64,s)->len;}return 0;
}//得到剩余的空间
static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);return sh->alloc - sh->len;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);return sh->alloc - sh->len;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);return sh->alloc - sh->len;}}return 0;
}//设置一个新长度
static inline void sdssetlen(sds s, size_t newlen) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:{//s-1的位置就是flagunsigned char *fp = ((unsigned char*)s)-1;*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);}break;case SDS_TYPE_8:SDS_HDR(8,s)->len = newlen;break;case SDS_TYPE_16:SDS_HDR(16,s)->len = newlen;break;case SDS_TYPE_32:SDS_HDR(32,s)->len = newlen;break;case SDS_TYPE_64:SDS_HDR(64,s)->len = newlen;break;}
}//长度增加:添加字符串后,长度增加
static inline void sdsinclen(sds s, size_t inc) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:{unsigned char *fp = ((unsigned char*)s)-1;unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);}break;case SDS_TYPE_8:SDS_HDR(8,s)->len += inc;break;case SDS_TYPE_16:SDS_HDR(16,s)->len += inc;break;case SDS_TYPE_32:SDS_HDR(32,s)->len += inc;break;case SDS_TYPE_64:SDS_HDR(64,s)->len += inc;break;}
}/* sdsalloc() = sdsavail() + sdslen() */
//总空间大小
static inline size_t sdsalloc(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->alloc;case SDS_TYPE_16:return SDS_HDR(16,s)->alloc;case SDS_TYPE_32:return SDS_HDR(32,s)->alloc;case SDS_TYPE_64:return SDS_HDR(64,s)->alloc;}return 0;
}//设置总空间大小
static inline void sdssetalloc(sds s, size_t newlen) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:/* Nothing to do, this type has no total allocation info. */break;case SDS_TYPE_8:SDS_HDR(8,s)->alloc = newlen;break;case SDS_TYPE_16:SDS_HDR(16,s)->alloc = newlen;break;case SDS_TYPE_32:SDS_HDR(32,s)->alloc = newlen;break;case SDS_TYPE_64:SDS_HDR(64,s)->alloc = newlen;break;}
}

3、SDS API

最后声明了一些操作:

//创建一个(定长),包含给定字符串init的sds
sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);sds sdsempty(void);//创建一个空sdssds sdsdup(const sds s);//复制sds
void sdsfree(sds s);//释放sds的header
sds sdsgrowzero(sds s, size_t len);//用空字符将s扩展到给定长//把t(的len个字符)加到s之后
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);
sds sdscatsds(sds s, const sds t);//把t(的len个字符),复制到s中,并覆盖原来的
sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);//将ap按照给定格式fmt打印后的字符串,添加到s中
sds sdscatvprintf(sds s, const char *fmt, va_list ap);//如果__GNUC__被宏定义过,则执行第一部分,否则执行第二部分
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)__attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endifsds sdscatfmt(sds s, char const *fmt, ...);//类似于sdscatprintf,但只处理打印格式标识符//format (archetype, string-index, first-to-check)
//archetype:哪种风格;
//string-index:传入函数的第几个参数是格式化字符串;
//first-to-check:从函数的第几个参数开始按格式化字符串的规则进行检查。sds sdstrim(sds s, const char *cset);//从左右两端删除除指定字符串cset外的全部字符
void sdsrange(sds s, ssize_t start, ssize_t end);//截取字符串
void sdsupdatelen(sds s);//以第一个空格为界,得到字符串长度,重置
void sdsclear(sds s);//清空sds字符串
int sdscmp(const sds s1, const sds s2);//比较字符串sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);//用sep分割字符串s分割后存在数组中返回,数组元素个数为count
void sdsfreesplitres(sds *tokens, int count);//释放数组tokens的count个元素
sds *sdssplitargs(const char *line, int *argc);//将line分割,分割后的元素数为argcvoid sdstolower(sds s);//变小写
void sdstoupper(sds s);//变大写sds sdsfromlonglong(long long value);//把long long类型转换为sds
sds sdscatrepr(sds s, const char *p, size_t len);//处理p中的所有不可打印字符后,增加到s后
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);//s中的from字符换为to字符//将argv数组中的内容用sep拼接成一个字符串,(放在sds)
sds sdsjoin(char **argv, int argc, char *sep);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);/* Low level functions exposed to the user API */
sds sdsMakeRoomFor(sds s, size_t addlen);//给末尾增大空间
void sdsIncrLen(sds s, ssize_t incr);//增加或减少长度
sds sdsRemoveFreeSpace(sds s);//紧缩空间
size_t sdsAllocSize(sds s);//sds的得到全部大小
void *sdsAllocPtr(sds s);//得到header地址

三、sds.c

主要是一些对字符串的增加删除,空间申请释放

1、sdsIncrLen

assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,
然后通过调用 abort 来终止程序运行。
已放弃使用assert()的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。

/*sds 字符串长度增加,空余位置减少,在字符串末尾设空字符。
此函数用于用户调用 Sdsmakecomfor ()修改字符串长度后, 写完当前字符串的内容, 最后需要设置新的长度。
注意: 可以使用负增量来修剪字符串。*/
void sdsIncrLen(sds s, ssize_t incr) {unsigned char flags = s[-1];size_t len;switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {unsigned char *fp = ((unsigned char*)s)-1;unsigned char oldlen = SDS_TYPE_5_LEN(flags);assert((incr > 0 && oldlen+incr < 32) || (incr < 0 && oldlen >= (unsigned int)(-incr)));*fp = SDS_TYPE_5 | ((oldlen+incr) << SDS_TYPE_BITS);len = oldlen+incr;break;}......//此处省略部分相似代码,类似的还有 SDS_TYPE_8, SDS_TYPE_16, SDS_TYPE_32, SDS_TYPE_64default: len = 0; /* Just to avoid compilation warnings. */}s[len] = '\0';
}

2、sdsull2str

//把unsigned long long类型的值转为字符串并存储
int sdsull2str(char *s, unsigned long long v) {char *p, aux;size_t l;/* Generate the string representation, this method produces* an reversed string. */p = s;do {*p++ = '0'+(v%10);//这样从个位放起,结果是反的,所以最后要反转字符串v /= 10;} while(v);/* Compute length and add null term. */l = p-s;*p = '\0';/* Reverse the string. */p--;while(s < p) {aux = *s;*s = *p;*p = aux;s++;p--;}return l;
}

3、sdscatvprintf

type va_arg(va_list argptr, type);
void va_end(va_list argptr);
void va_start(va_list argptr, last_parm);
任何可变长度的元素被访问之前,必须先用va_start()初始化改变元指针argptr。
初始化argptr后,经过对va_arg()的调用,以作为下一个参数类型的参数类型,返回参数。
最后取完所有参数并从函数返回之前。必须调用va_end()。由此确保堆栈的正确恢复。
其实就是堆栈中,使用指针,遍历堆栈段中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程.

/*
把参数按指定格式输出,添加到s中
*/
sds sdscatvprintf(sds s, const char *fmt, va_list ap) {va_list cpy;char staticbuf[1024], *buf = staticbuf, *t;size_t buflen = strlen(fmt)*2;//先用尝试静态缓冲区staticbuf,如果大小不够,再用堆分配if (buflen > sizeof(staticbuf)) {buf = s_malloc(buflen);if (buf == NULL) return NULL;} else {buflen = sizeof(staticbuf);}//先用,如果大小不够,申请两倍大的空间while(1) {buf[buflen-2] = '\0';va_copy(cpy,ap);//把cpy按照fmt格式输出到buf中,可接收的最大字符数量为buflenvsnprintf(buf, buflen, fmt, cpy);va_end(cpy);if (buf[buflen-2] != '\0') {//长度超出if (buf != staticbuf) s_free(buf);//释放bufbuflen *= 2;buf = s_malloc(buflen);//申请两倍大的空间if (buf == NULL) return NULL;continue;}break;}//把buf的值,加到s后面,返回新下标t = sdscat(s, buf);if (buf != staticbuf) s_free(buf);return t;
}

4、sdssplitlen

 /*count:分隔后的元素个数*/
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count) {int elements = 0, slots = 5;//elements实际元素数;默认元素slots个数(数组默认大小)为5long start = 0, j;sds *tokens;//分隔后的结果存储在此数组中if (seplen < 1 || len < 0) return NULL;tokens = s_malloc(sizeof(sds)*slots);if (tokens == NULL) return NULL;if (len == 0) {*count = 0;return tokens;}for (j = 0; j < (len-(seplen-1)); j++) {//数组大小不够,重新申请两倍空间(快,但浪费空间)if (slots < elements+2) {sds *newtokens;slots *= 2;newtokens = s_realloc(tokens,sizeof(sds)*slots);if (newtokens == NULL) goto cleanup;tokens = newtokens;}//分割元素if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {tokens[elements] = sdsnewlen(s+start,j-start);//找到对应字符串,分割并存储if (tokens[elements] == NULL) goto cleanup;//如果为null,释放elements++;start = j+seplen;j = j+seplen-1; /* skip the separator */}}//把最后剩下的存储tokens[elements] = sdsnewlen(s+start,len-start);if (tokens[elements] == NULL) goto cleanup;elements++;*count = elements;return tokens;cleanup:{int i;for (i = 0; i < elements; i++) sdsfree(tokens[i]);s_free(tokens);*count = 0;return NULL;}
}

5、sdsnewlen

 /*
使用init和initlen指定的内容创建一个新的sds字符串。
init = null, 则字符串将以零字节初始化。
init = SDS _ NOINIT, 则缓冲区不初始化,sds字符串始终为空终止;
您可以使用 printf () 打印字符串, 因为字符串的末尾有一个隐式 \ 0。
该字符串是二进制安全的, 可以包含\ 0 字符的中间, 因为长度存储在sds头中。*/
sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;//获取信息char type = sdsReqType(initlen);/* Empty strings are usually created in order to append. Use type 8* since type 5 is not good at this. */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. *///申请空间sh = s_malloc(hdrlen+initlen+1);//填充header信息if (init==SDS_NOINIT)init = NULL;else if (!init)memset(sh, 0, hdrlen+initlen+1);if (sh == NULL) return NULL;s = (char*)sh+hdrlen;fp = ((unsigned char*)s)-1;switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}}//初始化字符串if (initlen && init)memcpy(s, init, initlen);//拷贝添加字符串s[initlen] = '\0';return s;
}

redis源码解析(二)——SDS(简单动态字符串)相关推荐

  1. 结合redis设计与实现的redis源码学习-2-SDS(简单动态字符串)

    上一次我们学习了redis的内存分配方式,今天我们来学习redis最基本的数据结构SDS,在redis的数据库里,包含字符产值的简直对在底层都是由SDS实现的. SDS的基本数据结构是sdshdr结构 ...

  2. redis源码注释二:简单字符串sds.c sds.h

    1. sds(Simple Dynamic String)简介 sds(Simple Dynamic String)简单动态字符串. redis没有直接用char*,而是使用sds替代char*.为什 ...

  3. c语言追加字符串_Redis源码解析二--简单动态字符串

    Redis 简单动态字符串 1.介绍 Redis兼容传统的C语言字符串类型,但没有直接使用C语言的传统的字符串(以'0'结尾的字符数组)表示,而是自己构建了一种名为简单动态字符串(simple dyn ...

  4. Redis源码解析——前言

    今天开启Redis源码的阅读之旅.对于一些没有接触过开源代码分析的同学来说,可能这是一件很麻烦的事.但是我总觉得做一件事,不管有多大多难,我们首先要在战略上蔑视它,但是要在战术上重视它.除了一些高大上 ...

  5. Redis源码解析(15) 哨兵机制[2] 信息同步与TILT模式

    Redis源码解析(1) 动态字符串与链表 Redis源码解析(2) 字典与迭代器 Redis源码解析(3) 跳跃表 Redis源码解析(4) 整数集合 Redis源码解析(5) 压缩列表 Redis ...

  6. Redis源码解析——双向链表

    相对于之前介绍的字典和SDS字符串库,Redis的双向链表库则是非常标准的.教科书般简单的库.但是作为Redis源码的一部分,我决定还是要讲一讲的.(转载请指明出于breaksoftware的csdn ...

  7. Redis源码解析——字典基本操作

    有了<Redis源码解析--字典结构>的基础,我们便可以对dict的实现进行展开分析.(转载请指明出于breaksoftware的csdn博客) 创建字典 一般字典创建时,都是没有数据的, ...

  8. Redis源码解析——内存管理

    在<Redis源码解析--源码工程结构>一文中,我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库.在linux系统中,Redis默认使用jemalloc库.当然用户可以指定 ...

  9. 【深度学习模型】智云视图中文车牌识别源码解析(二)

    [深度学习模型]智云视图中文车牌识别源码解析(二) 感受 HyperLPR可以识别多种中文车牌包括白牌,新能源车牌,使馆车牌,教练车牌,武警车牌等. 代码不可谓不混乱(别忘了这是职业公司的准产品级代码 ...

  10. Redis内部数据结构详解之简单动态字符串(sds)

    本文所引用的源码全部来自Redis2.8.2版本. Redis中简单动态字符串sds数据结构与API相关文件是:sds.h, sds.c. 转载请注明,本文出自:http://blog.csdn.ne ...

最新文章

  1. python cookbook 2字符串 (1)
  2. RAC8——scan ip的理解
  3. 用户报告性能缓慢的解决流程
  4. 51nod 1027 大数乘法
  5. JVM 类型的生命周期学习
  6. 配置基于python的VIM环境
  7. 锐捷官方提供122套实验题.
  8. 自动事务_JDBC进阶(二)事务编程
  9. 主进程退出后子进程还会存在吗?_[docker]从一个实例,一窥docker进程管理
  10. 撕掉单词书,每天花10分钟做这件事,英语水平暴涨!
  11. OpenCV编程简介
  12. Oracle 11g R1(11.1) Joins表连接
  13. .net开发mvc架构分析
  14. 2019艾肯VST声卡ProDriveIII-3.1.0最新驱动下载
  15. 批量下载魔兽replays录像文件
  16. ubuntu 20.04安装输入法,微信,QQ,亲测使用很方便。
  17. toStdString()
  18. arduino串口绘图_Arduino IDE开发环境串口绘图仪(Serial Plotter)使用方法简介
  19. python怎么进入虚拟环境_Python 中如何使用 virtualenv 管理虚拟环境
  20. [转] Carmack 谈 d3d 与 ogl, 定位专业应用的OpenGL, 专注娱乐应用的DirectX, 未来:OpenGL、DirectX并行发展...

热门文章

  1. 【初级C语言】表达式和基本语句(布尔型与0比较,浮点型与0比较,switch语句,提高循环语句的效率)
  2. Django Web框架教学笔记-1
  3. GRIB2 资料处理
  4. 图灵 | 一站式图应用平台
  5. 【最新】CUDA Toolkit版本及可用PyTorch对应关系(参考官网)
  6. Java自学之路——构造器(Constructor)
  7. 2018最新的web前端开发框架排行总结
  8. 软件设计师2010上午题基础知识(易错整理)
  9. 股票及炒股票的基础知识
  10. java并发编程(并发编程的三个问题)