热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《Linux设备驱动开发详解A》一一3.5Linux下的C编程特点

本节书摘来华章计算机出版社《Linux设备驱动开发详解A》一书中的第3章,第3.5节,作者:宋宝华更多章节内容可以访问云栖社区“华章计算机

本节书摘来华章计算机出版社《Linux设备驱动开发详解 A》一书中的第3章,第3.5节,作者:宋宝华 更多章节内容可以访问云栖社区“华章计算机”公众号查看。1

3.5 Linux下的C编程特点

3.5.1 Linux编码风格
Linux有独特的编码风格,在内核源代码下存在一个文件Documentation/CodingStyle,进行了比较详细的描述。
Linux程序的命名习惯和Windows程序的命名习惯及著名的匈牙利命名法有很大的
不同。
在Windows程序中,习惯以如下方式命名宏、变量和函数:

#def?ine PI 3.1415926 /* 用大写字母代表宏 */
int minValue, maxValue; /* 变量:第一个单词全小写,其后单词的第一个字母大写 */
void SendData(void); /* 函数:所有单词第一个字母都大写 */

这种命名方式在程序员中非常盛行,意思表达清晰且避免了匈牙利法的臃肿,单词之间通过首字母大写来区分。通过第1个单词的首字母是否大写可以区分名称属于变量还是属于函数,而看到整串的大写字母可以断定为宏。实际上,Windows的命名习惯并非仅限于Windows编程,许多领域的程序开发都遵照此习惯。
但是Linux不以这种习惯命名,对于上面的一段程序,在Linux中它会被命名为:

#def?ine PI 3.1415926
int min_value, max_value;
void send_data(void);

在上述命名方式中,下划线大行其道,不按照Windows所采用的用首字母大写来区分单词的方式。Linux的命名习惯与Windows命名习惯各有千秋,但是既然本书和本书的读者立足于编写Linux程序,代码风格理应与Linux开发社区保持一致。
Linux的代码缩进使用“TAB”。
Linux中代码括号“{”和“}”的使用原则如下。
1)对于结构体、if/for/while/switch语句,“{”不另起一行,例如:

struct var_data {int len;char data[0];
};if (a == b) {a = c;d = a;
}for (i &#61; 0; i <10; i&#43;&#43;) {a &#61; c;d &#61; a;
}

2&#xff09;如果if、for循环后只有1行&#xff0c;不要加“{”和“}”&#xff0c;例如&#xff1a;

for (i &#61; 0; i <10; i&#43;&#43;) {a &#61; c;
}
应该改为&#xff1a;
for (i &#61; 0; i <10; i&#43;&#43;) a &#61; c;

3&#xff09;if和else混用的情况下&#xff0c;else语句不另起一行&#xff0c;例如&#xff1a;

if (x &#61;&#61; y) {...
} else if (x > y) {...
} else {...
}

4&#xff09;对于函数&#xff0c;“{”另起一行&#xff0c;譬如&#xff1a;

int add(int a, int b)
{return a &#43; b;
}

在switch/case语句方面&#xff0c;Linux建议switch和case对齐&#xff0c;例如&#xff1a;

switch (suff?ix) {
case &#39;G&#39;:
case &#39;g&#39;:mem <<&#61; 30;break;
case &#39;M&#39;:
case &#39;m&#39;:mem <<&#61; 20;break;
case &#39;K&#39;:
case &#39;k&#39;:mem <<&#61; 10;/* fall through */
default:break;
}

内核下的Documentation/CodingStyle描述了Linux内核对编码风格的要求&#xff0c;内核下的scripts/checkpatch.pl提供了1个检查代码风格的脚本。如果使用scripts/checkpatch.pl检查包含如下代码块的源程序&#xff1a;

for (i &#61; 0; i <10; i&#43;&#43;) {a &#61; c;
}

就会产生“WARNING: braces {} are not necessary for single statement blocks”的警告。
另外&#xff0c;请注意代码中空格的应用&#xff0c;譬如“for?(i?&#61;?0; ?i?在工程阶段&#xff0c;一般可以在SCM软件的服务器端使能pre-commit hook&#xff0c;自动检查工程师提交的代码是否符合Linux的编码风格&#xff0c;如果不符合&#xff0c;则自动拦截。git的pre-commit hook可以运行在本地代码仓库中&#xff0c;如Ben Dooks完成的一个版本&#xff1a;

#!/bin/sh
#
# pre-commit hook to run check-patch on the output and stop any commits
# that do not pass. Note, only for git-commit, and not for any of the
# other scenarios
#
# Copyright 2010 Ben Dooks, if git rev-parse --verify HEAD 2>/dev/null >/dev/null
thenagainst&#61;HEAD
else# Initial commit: diff against an empty tree objectagainst&#61;4b825dc642cb6eb9a060e54bf8d69288fbee4904
f?igit diff --cached $against -- | ./scripts/checkpatch.pl --no-signoff -

3.5.2 GNU C与ANSI C
Linux上可用的C编译器是GNU C编译器&#xff0c;它建立在自由软件基金会的编程许可证的基础上&#xff0c;因此可以自由发布。GNU C对标准C进行一系列扩展&#xff0c;以增强标准C的功能。
1.?零长度和变量长度数组
GNU C允许使用零长度数组&#xff0c;在定义变长对象的头结构时&#xff0c;这个特性非常有用。例如&#xff1a;

struct var_data {int len;char data[0];
};

char data[0]仅仅意味着程序中通过var_data结构体实例的data[index]成员可以访问len之后的第index个地址&#xff0c;它并没有为data[]数组分配内存&#xff0c;因此sizeof(struct var_data)&#61;sizeof(int)。
假设struct var_data的数据域就保存在struct var_data紧接着的内存区域中&#xff0c;则通过如下代码可以遍历这些数据&#xff1a;

struct var_data s;
...
for (i &#61; 0; i

GNU C中也可以使用1个变量定义数组&#xff0c;例如如下代码中定义的“double x[n]”&#xff1a;

int main (int argc, char *argv[])
{int i, n &#61; argc;double x[n];for (i &#61; 0; i }

2.?case范围
GNU C支持case x…y这样的语法&#xff0c;区间[x,y]中的数都会满足这个case的条件&#xff0c;请看下面的代码&#xff1a;

switch (ch) {
case &#39;0&#39;... &#39;9&#39;: c -&#61; &#39;0&#39;;break;
case &#39;a&#39;... &#39;f&#39;: c -&#61; &#39;a&#39; - 10;break;
case &#39;A&#39;... &#39;F&#39;: c -&#61; &#39;A&#39; - 10;break;
}

代码中的case &#39;0&#39;... &#39;9&#39;等价于标准C中的&#xff1a;

case &#39;0&#39;: case &#39;1&#39;: case &#39;2&#39;: case &#39;3&#39;: case &#39;4&#39;:
case &#39;5&#39;: case &#39;6&#39;: case &#39;7&#39;: case &#39;8&#39;: case &#39;9&#39;:

3.?语句表达式
GNU C把包含在括号中的复合语句看成是一个表达式&#xff0c;称为语句表达式&#xff0c;它可以出现在任何允许表达式的地方。我们可以在语句表达式中使用原本只能在复合语句中使用的循环、局部变量等&#xff0c;例如&#xff1a;

#def?ine min_t(type,x,y) \
(&#xff5b;type _ _x &#61;(x);type _ _y &#61; (y); _ _x<_ _y? _ _x: _ _y; })int ia, ib, mini;
float fa, fb, minf;mini &#61; min_t(int, ia, ib);
minf &#61; min_t(float, fa, fb);

因为重新定义了_ xx和 _y这两个局部变量&#xff0c;所以用上述方式定义的宏将不会有副作用。在标准C中&#xff0c;对应的如下宏则会产生副作用&#xff1a;

def?ine min(x,y) ((x) <(y) ? (x) : (y))

代码min(&#43;&#43;ia,&#43;&#43;ib)会展开为((&#43;&#43;ia) <(&#43;&#43;ib) ? (&#43;&#43;ia): (&#43;&#43;ib))&#xff0c;传入宏的“参数”增加
两次。
4.?typeof关键字
typeof(x)语句可以获得x的类型&#xff0c;因此&#xff0c;可以借助typeof重新定义min这个宏&#xff1a;

#def?ine min(x,y) ({ \const typeof(x) _x &#61; (x); \const typeof(y) _y &#61; (y); \(void) (&_x &#61;&#61; &_y); \_x <_y ? _x : _y; })

我们不需要像min_t(type,x,y)那个宏那样把type传入&#xff0c;因为通过typeof(x)、typeof(y)可以获得type。代码行(void) (&_x &#61;&#61; &_y)的作用是检查_x和_y的类型是否一致。
5.?可变参数宏
标准C就支持可变参数函数&#xff0c;意味着函数的参数是不固定的&#xff0c;例如printf()函数的原
型为&#xff1a;

int printf( const char *format [, argument]... );

而在 GNU C中&#xff0c;宏也可以接受可变数目的参数&#xff0c;例如&#xff1a;

#def?ine pr_debug(fmt,arg...) \printk(fmt,##arg)

这里arg表示其余的参数&#xff0c;可以有零个或多个参数&#xff0c;这些参数以及参数之间的逗号构成arg的值&#xff0c;在宏扩展时替换arg&#xff0c;如下列代码&#xff1a;

pr_debug("%s:%d",f?ilename,line)

会被扩展为&#xff1a;

printk("%s:%d", f?ilename, line)

6.?标号元素
标准C要求数组或结构体的初始化值必须以固定的顺序出现&#xff0c;在GNU C中&#xff0c;通过指定索引或结构体成员名&#xff0c;允许初始化值以任意顺序出现。
指定数组索引的方法是在初始化值前添加“[INDEX] &#61;”&#xff0c;当然也可以用“[FIRST ... LAST] &#61;”的形式指定一个范围。例如&#xff0c;下面的代码定义了一个数组&#xff0c;并把其中的所有元素赋值为0&#xff1a;

unsigned char data[MAX] &#61; { [0 ... MAX-1] &#61; 0 };

下面的代码借助结构体成员名初始化结构体&#xff1a;

struct f?ile_operations ext2_f?ile_operations &#61; {llseek: generic_f?ile_llseek,read: generic_f?ile_read,write: generic_f?ile_write,ioctl: ext2_ioctl,mmap: generic_f?ile_mmap,open: generic_f?ile_open,release: ext2_release_f?ile,fsync: ext2_sync_f?ile,
};

但是&#xff0c;Linux 2.6推荐类似的代码应该尽量采用标准C的方式&#xff1a;
s&#96;Javascript
truct f?ile_operations ext2_f?ile_operations &#61; {

.llseek &#61; generic_f?ile_llseek,.read &#61; generic_f?ile_read,.write &#61; generic_f?ile_write,.aio_read &#61; generic_f?ile_aio_read,.aio_write &#61; generic_f?ile_aio_write,.ioct &#61; ext2_ioctl,.mmap &#61; generic_f?ile_mmap,.open &#61; generic_f?ile_open,.release &#61; ext2_release_f?ile,.fsync &#61; ext2_sync_f?ile,.readv &#61; generic_f?ile_readv,.writev &#61; generic_f?ile_writev,.sendf?ile &#61; generic_f?ile_sendf?ile,

};

7.?当前函数名
GNU C预定义了两个标识符保存当前函数的名字&#xff0c;_ _FUNCTION_ _保存函数在源码中的名字&#xff0c;_ _PRETTY_FUNCTION_ _保存带语言特色的名字。在C函数中&#xff0c;这两个名字是相同的。

void example()
{
printf("This is function:%s", _?_FUNCTION_?_);
}

代码中的_ _FUNCTION_ _意味着字符串“example”。C99已经支持_ _func_ _宏&#xff0c;因此建议在Linux编程中不再使用_ _FUNCTION_ _&#xff0c;而转而使用_ _func_ _&#xff1a;

void example(void)
{
printf("This is function:%s", _?_func_?_);
}

8.?特殊属性声明
GNU C允许声明函数、变量和类型的特殊属性&#xff0c;以便手动优化代码和定制代码检查的方法。要指定一个声明的属性&#xff0c;只需要在声明后添加_?_attribute_?_ (( ATTRIBUTE ))。其中ATTRIBUTE为属性说明&#xff0c;如果存在多个属性&#xff0c;则以逗号分隔。GNU C支持noreturn、format、section、aligned、packed等十多个属性。
noreturn属性作用于函数&#xff0c;表示该函数从不返回。这会让编译器优化代码&#xff0c;并消除不必要的警告信息。例如&#xff1a;
# def?ine ATTRIB_NORET _?_attribute_?_((noreturn)) ....
asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;
format属性也用于函数&#xff0c;表示该函数使用printf、scanf或strftime风格的参数&#xff0c;指定format属性可以让编译器根据格式串检查参数类型。例如&#xff1a;
asmlinkage int printk(const char * fmt, ...) _?_attribute_?_ ((format (printf, 1, 2)));
上述代码中的第1个参数是格式串&#xff0c;从第2个参数开始都会根据printf()函数的格式串规则检查参数。
unused属性作用于函数和变量&#xff0c;表示该函数或变量可能不会用到&#xff0c;这个属性可以避免编译器产生警告信息。
aligned属性用于变量、结构体或联合体&#xff0c;指定变量、结构体或联合体的对齐方式&#xff0c;以字节为单位&#xff0c;例如&#xff1a;

struct example_struct {

char a;
int b;
long c;

} _?_attribute_?_((aligned(4)));

表示该结构类型的变量以4字节对齐。
packed属性作用于变量和类型&#xff0c;用于变量或结构体成员时表示使用最小可能的对齐&#xff0c;用于枚举、结构体或联合体类型时表示该类型使用最小的内存。例如&#xff1a;

struct example_struct {

char a;
int b;
long c _?_attribute_?_((packed));

};

编译器对结构体成员及变量对齐的目的是为了更快地访问结构体成员及变量占据的内存。例如&#xff0c;对于一个32位的整型变量&#xff0c;若以4字节方式存放&#xff08;即低两位地址为00&#xff09;&#xff0c;则CPU在一个总线周期内就可以读取32位&#xff1b;否则&#xff0c;CPU需要两个总线周期才能读取32位。
9.?内建函数
GNU C提供了大量内建函数&#xff0c;其中大部分是标准C库函数的GNU C编译器内建版本&#xff0c;例如memcpy()等&#xff0c;它们与对应的标准C库函数功能相同。
不属于库函数的其他内建函数的命名通常以_ _builtin开始&#xff0c;如下所示。
内建函数_ _builtin_return_address (LEVEL)返回当前函数或其调用者的返回地址&#xff0c;参数LEVEL 指定调用栈的级数&#xff0c;如0表示当前函数的返回地址&#xff0c;1表示当前函数的调用者的返回地址。
内建函数_ _builtin_constant_p(EXP)用于判断一个值是否为编译时常数&#xff0c;如果参数EXP 的值是常数&#xff0c;函数返回1&#xff0c;否则返回0。
例如&#xff0c;下面的代码可检测第1个参数是否为编译时常数以确定采用参数版本还是非参数版本&#xff1a;
def?ine test_bit(nr,addr) \

(_?_builtin_constant_p(nr) ? \
constant_test_bit((nr),(addr)) : \
variable_test_bit((nr),(addr)))

内建函数_ _builtin_expect(EXP, C)用于为编译器提供分支预测信息&#xff0c;其返回值是整数表达式EXP的值&#xff0c;C的值必须是编译时常数。
Linux内核编程时常用的likely()和unlikely()底层调用的likely_notrace()、unlikely_notrace() 就是基于_ _builtin_expect(EXP, C)实现的。
#def?ine likely_notrace(x) __builtin_expect(!!(x), 1)
#def?ine unlikely_notrace(x) __builtin_expect(!!(x), 0)
若代码中出现分支&#xff0c;则即可能中断流水线&#xff0c;我们可以通过likely()和unlikely()暗示分支容易成立还是不容易成立&#xff0c;例如&#xff1a;

if (likely(!IN_DEV_ROUTE_LOCALNET(in_dev)))

if (ipv4_is_loopback(saddr))goto e_inval;

在使用gcc编译C程序的时候&#xff0c;如果使用“-ansi –pedantic”编译选项&#xff0c;则会告诉编译器不使用GNU扩展语法。例如对于如下C程序test.c&#xff1a;

struct var_data {
int len;
char data[0];
};

struct var_data a;

直接编译可以通过&#xff1a;

gcc -c test.c

如果使用“-ansi –pedantic”编译选项&#xff0c;编译会报警&#xff1a;

gcc -ansi -pedantic -c test.c

test.c:3: warning: ISO C forbids zero-size array &#39;data&#39;
**3.5.3 do { } while(0) 语句**
在Linux内核中&#xff0c;经常会看到do {} while(0)这样的语句&#xff0c;许多人开始都会疑惑&#xff0c;认为do {} while(0)毫无意义&#xff0c;因为它只会执行一次&#xff0c;加不加do {} while(0)效果是完全一样的&#xff0c;其实do {} while(0)的用法主要用于宏定义中。
这里用一个简单的宏来演示&#xff1a;
#def?ine SAFE_FREE(p) do{ free(p); p &#61; NULL;} while(0)
假设这里去掉do...while(0)&#xff0c;即定义SAFE_DELETE为&#xff1a;
#def?ine SAFE_FREE(p) free(p); p &#61; NULL;
那么以下代码&#xff1a;

if(NULL !&#61; p)
SAFE_DELETE(p)
else
.../ do something /

会被展开为&#xff1a;

if(NULL !&#61; p)
free(p); p &#61; NULL;
else
.../ do something /

展开的代码中存在两个问题&#xff1a;
1&#xff09;因为if分支后有两个语句&#xff0c;导致else分支没有对应的if&#xff0c;编译失败。
2&#xff09;假设没有else分支&#xff0c;则SAFE_FREE中的第二个语句无论if测试是否通过&#xff0c;都会执行。
的确&#xff0c;将SAFE_FREE的定义加上{}就可以解决上述问题了&#xff0c;即&#xff1a;
#def?ine SAFE_FREE(p) { free(p); p &#61; NULL;}
这样&#xff0c;代码&#xff1a;

if(NULL !&#61; p)

SAFE_DELETE(p)

else

... /* do something */

会被展开为&#xff1a;

if(NULL !&#61; p)
{ free(p); p &#61; NULL; }
else

... /* do something */

但是&#xff0c;在C程序中&#xff0c;在每个语句后面加分号是一种约定俗成的习惯&#xff0c;那么&#xff0c;如下代码&#xff1a;

if(NULL !&#61; p)

SAFE_DELETE(p);

else

... /* do something */

将被扩展为&#xff1a;
if(NULL !&#61; p)
{ free(p); p &#61; NULL; };
else
... / do something /

这样&#xff0c;else分支就又没有对应的if了&#xff0c;编译将无法通过。假设用了do {} while(0)语句&#xff0c;情况就不一样了&#xff0c;同样的代码会被展开为&#xff1a;

if(NULL !&#61; p)

do{ free(p); p &#61; NULL;} while(0);

else

... /* do something */

而不会再出现编译问题。do{} while(0)的使用完全是为了保证宏定义的使用者能无编译错误地使用宏&#xff0c;它不对其使用者做任何假设。
**3.5.4 goto语句**
用不用goto一直是一个著名的争议话题&#xff0c;Linux内核源代码中对goto的应用非常广泛&#xff0c;但是一般只限于错误处理中&#xff0c;其结构如&#xff1a;

if(register_a()!&#61;0)

goto err;

if(register_b()!&#61;0)

goto err1;

if(register_c()!&#61;0)

goto err2;

if(register_d()!&#61;0)

goto err3;

...

err3:

unregister_c();

err2:

unregister_b();

err1:

unregister_a();

err:

return ret;

这种将goto用于错误处理的用法实在是简单而高效&#xff0c;只需保证在错误处理时注销、资源释放等&#xff0c;与正常的注册、资源申请顺序相反。



推荐阅读
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 如何提高PHP编程技能及推荐高级教程
    本文介绍了如何提高PHP编程技能的方法,推荐了一些高级教程。学习任何一种编程语言都需要长期的坚持和不懈的努力,本文提醒读者要有足够的耐心和时间投入。通过实践操作学习,可以更好地理解和掌握PHP语言的特异性,特别是单引号和双引号的用法。同时,本文也指出了只走马观花看整体而不深入学习的学习方式无法真正掌握这门语言,建议读者要从整体来考虑局部,培养大局观。最后,本文提醒读者完成一个像模像样的网站需要付出更多的努力和实践。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • EzPP 0.2发布,新增YAML布局渲染功能
    EzPP发布了0.2.1版本,新增了YAML布局渲染功能,可以将YAML文件渲染为图片,并且可以复用YAML作为模版,通过传递不同参数生成不同的图片。这个功能可以用于绘制Logo、封面或其他图片,让用户不需要安装或卸载Photoshop。文章还提供了一个入门例子,介绍了使用ezpp的基本渲染方法,以及如何使用canvas、text类元素、自定义字体等。 ... [详细]
  • svnWebUI:一款现代化的svn服务端管理软件
    svnWebUI是一款图形化管理服务端Subversion的配置工具,适用于非程序员使用。它解决了svn用户和权限配置繁琐且不便的问题,提供了现代化的web界面,让svn服务端管理变得轻松。演示地址:http://svn.nginxwebui.cn:6060。 ... [详细]
author-avatar
全球时_尚热门焦点吧
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有