代码简洁精炼,美观,可读性好,高效率,高复用,可移植性好,高内聚,低耦合,没有冗余,不符合这些原则,必须特别说明。
规范性,代码有规可循。特殊排版、特殊语法、特殊指令,必须特别说明。
a)系统头文件与用户头文件包含区分开,不交叉。
b)系统头文件,稳定的目录结构,应采用包含子路径方式。#include”sub/test.h”
c)系统头文件应用:#include
d)自定义同文件应用:#include "xxx.h"
e)只引用需要的头文件。
f)将所有#include的文件名视为大小写敏感。能使用声明的情况下,使用声明。
a)头文件命名为.h,内联文件命名为.inl;C++文件命名为*.cpp
b)文件名用不要用无意义的名称:例如XImage.cpp,命名方式在系统中应该统一。
c)头文件除了特殊情况,应使用#ifdef控制块。
d)头文件#endif应采用行尾注释。
e)头文件,首先是包含全局函数,其次是宏定义代码块,然后是全局变量,全局常量,类型定义,类定义,内联部分。
f)cpp文件,包含指令,宏定义,全局变量,函数定义。
a)文件应包含文件头注释和内容。
b)函数体类体之间原则上用1个空行,特殊情况下可用一个或者不需要空行。
文件头、控制块,#include部分、宏定义部分、class部分、全局常量部分、全局变量部分、函数和函数之间,用一个空行。'
作者,文件名称,文件说明,生成日期(可选),提供模板
a)关键函数必须写上注释,说明函数的用途,提供注释模板。
b)特别函数参数,需要说明参数的目的,由谁负责释放等等。
c)除了特别情况,注释写在代码之前,不要放到代码行之后。
d)对每个#else或#endif给出行末注释。
e)关键代码注释,包括但不限于:赋值,函数调用,表达式,分支等等。
f)尚未实现完整的代码,或者需要进一步优化的代码,应加上 // TODO ...或者同等含义的注释。
g)调试的代码,加上注释 // DEBUG或者同等含义的注释
h)需要引起关注的代码,加上注释 // NOTE ...或者同等含义的注释
i)对于较大的代码块结尾,如for,while,do等,可加上 // end for|while|do或者同等含义的注释。
a)同一性:在编写一个子模块或派生类的时候,要遵循其基类或整体模块的命名风格,保持命名风格在整个模块中的同一性。
b)标识符组成:标识符采用英文单词或其组合,应当直观且可以拼读,可望文知意,用词应当准确,避免用拼音命名。
c)最小化长度 && 最大化信息量原则:在保持一个标识符意思明确的同时,应当尽量缩短其长度。
d)避免过于相似:不要出现仅靠大小写区分的相似的标识符,例如"i"与"I","function"与"Function"等等。
e)避免在不同级别的作用域中重名:程序中不要出现名字完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但容易使人误解。
f)正确命名具有互斥意义的标识符:用正确的反义词组命名具有互斥意义的标识符,如:"minValue" 和 "maxValue","getName()" 和"setName()" ….
g)避免名字中出现数字编号:尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。
a)类的函数名称应采用首字母小写类似handleXXX命名,例如:handleColor;不推荐采用例如 HandleColor;除了标准c风格代码,标准模板库,不推荐用下划线,例如,handle_color。容易与系统函数标准函数重名,导致不能目视而知其作用范围。
b)函数参数比较多时,应考虑用结构代替,参数不能超过6个。
c)如果不能避免函数参数比较多,应在排版上可考虑相似含义的参数占用一行,参数名竖向对齐。甚至每个参数一行。
a)变量命令方式应采用驼峰命令方式。例如handleColor;不同类型变量打头方式见表 1,当类型叠加时,应以最能够表现变量类型的打头方式,例如int类型的指针,应采用p打头;指针类型的数组,应采用a打头,数组的指针因采用p打头。
表 1 变量类型打头表
类型 | 打头 | 备注 |
---|---|---|
int | n | 如nValue |
bool | b | |
long | l | 如lValue |
float | f | |
double | d | |
std::string/QString | str | |
std::list/QList | lst | 如lstValues |
std::vector/QVector | vec | |
std::map/QMap | map | |
枚举 | e | |
指针 | p | |
引用 | r | |
数组 | a |
b)不同作用范围的变量应在类型前面添加前缀加下划线,不同作用范围的前缀见表 2。
表 2变量作用范围前缀表
类型 | 前缀 | 备注 |
---|---|---|
成员变量 | m_ | 如m_nValue |
全局变量 | g_ | 如g_lValue |
静态变量 | s_ | 如s_pValuePtr,静态全局和静态成员也应s为前缀 |
a)类名和结构体首字母大写,例如MyClass,MyStruct。
b)类和对象名应是名词。
c)枚举定义以E_打头,且应全大写,使用下划线分割不同的引文单词,如E_TYPE_STUDENT_INFO,各枚值名字应为枚举类型名打头,例如E_TYPE_STUDENT_INFO_NUMBER。
d)宏定义以DEF_打头,且应全大写,使用下划线分割不同的引文单词,如DEF_TYPE_ID。
a)对于移植的或者开源的代码,可以沿用原有风格,不用C++的命名规范。
a)每一行开始处的缩进只能用4个空格,不能用Tab,输入内容之后统一用空格。
b)在代码行的结尾部分不能出现多余的空格。
c)不要在"::","->","."前后加空格。
d)不要在",",";"之前加空格。
a)类,结构,枚举,联合:大括号另起一行
b)函数体的{需要新起一行,在{之前的缩进应与上一级对齐。
c)除了特别情况,函数体内不能出现两个空行。
d)除了特别情况,函数体内不能宏定义指令,除非该宏定义仅能在该函数内使用。
e)在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
a)"if"、"for"、"while"、"do"、"try"、"catch" 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加 "{ }" 。这样可以防止书写和修改代码时出现失误。
b)"if"、"for"、"while"、"do"、"try"、"catch" 的括号和表达式,括号可紧挨关键字,这样强调的是表达式。
a)一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
b)多行变量定义,为了追求代码排版美观,可将变量竖向对齐。
c)代码行最大长度宜控制在一定个字符以内,能在当前屏幕内全部可见为宜。建议80个。
a)不要用分号结束宏定义。
b)能使用函数来解决的问题,不要使用宏,方便调式。
a)定义指针和引用时*和&紧跟变量名。
b)尽量避免使用浮点数,除非必须。
c)用typedef简化程序中的复杂语法。
d)避免定义无名称的类型。例如: enum TState { EIdle, EActive };
e)少用union,如果一定要用,则采用简单数据类型成员。
f)用enum取代(一组相关的)常量。
g)不要使用魔鬼数字。
h)尽量用引用取代指针。
i)定义变量完成后立即初始化,勿等到使用时才进行,在需要使用到变量的时候才定义变量。
j)如果有更优雅的解决方案,不要使用强制类型转换。例如dynamic_cast,尽量使用多态。
a)避免在表达式中用赋值语句。
b)避免对浮点类型做等于或不等于判断。
c)不能将枚举类型进行运算后再赋给枚举变量。
d)在循环过程中不要修改循环计数器。
e)检测空指针,用 if( p )
f)检测非空指针,用 if( ! p )
a)引用类型作为返回值:函数必须返回一个存在的对象。
b)引用类型作为参数:调用者必须传递一个存在的对象。
a)除开void函数,构造函数,析构函数,其它函数必须要有返回值。在函数的实现中,每个分支必须显示返回return。
b)当函数返回引用或指针时,用文字描述其有效期。
a)内联函数应将函数体放到类体外。
b)只有简单的函数才有必要设计为内联函数,复杂业务逻辑的函数不要这么做。
c)虚函数不要设计为内联函数。
a)只读取该参数的内容,不对其内容做修改,用常量引用。
b)修改参数内容,或需要通过参数返回,用非常量引用。
c)简单数据类型用传值方式。
d)复杂数据类型用引用或指针方式。
e)输入参数排在前面,输出参数排在后面,默认参数除外。
f)除通用库函数,尽量不使用默认参数。
a)按照 public, protected, private 的顺序分块。哪一块没有,就直接忽略。
b)每一块中,按照下面顺序排列
1)typedef,enum,struct,class 定义的嵌套类型
2)常量
3)构造函数
4)析构函数
5)成员函数,含静态成员函数
6)数据成员,含静态数据成员
a)构造函数的初始化列表,应和类里成员变量的顺序一致。
b)初始化列表中的每个项,应独占一行。
c)避免出现用一个成员初始化另一个成员。
d)构造函数应初始化所有成员,尤其是指针。
e)不要在构造函数和析构函数中抛出异常。
接口类的虚函数应设计为纯虚函数。
a)如果类可以继承,则应将类析构函数设计为虚函数。
b)如果类不允许继承,则应将类析构函数设计为非虚函数。
c)如果类不能被复制,则应将拷贝构造函数和赋值运算符设计为私有的。
d)如果为类设计了构造函数,则应有析构函数。
a)尽量避免使用mutable(mutex除外)和volatile。
b)尽量避免使用公有成员变量。
a)努力使类的接口少而完备。
b)尽量使用常成员函数代替非常量成员函数,const函数
c)除非特别理由,绝不要重新定义非虚函数。
d)如果是子类型重写父类的虚函数,应该在函数声明后面添加override,让编译器来检查是否重新定义非虚函数。C++11
e)不想被子类重写的虚函数,函数声明后面添加final。C++11
a)继承必须满足IS-A的关系,HAS-A应采用包含。
b)虚函数不要采用默认参数。
c)除非特别需要,应避免设计大而全的虚函数,虚函数功能要单一。
d)除非特别需要,避免将基类强制转换成派生类。
a)释放内存完成后将指针赋空,避免出现野指针。
b)使用指针前进行判断合法性,应考虑到为空的情况的处理,除非明显的组合模式。
c)使用数组时,应先判断索引的有效性,处理无效的索引的情况。
d)卫句风格:先处理所有可能发生错误的情况,再处理正常情况。
a)头文件中使用前向声明代替头文件包含。Class M;
b)尽量在for循环之前,先写计算估值表达式。
c)尽量避免在循环体内部定义对象。
d)避免对象拷贝,尤其是代价很高的对象拷贝。
e)避免生成临时对象,尤其是大的临时对象。
f)注意大尺寸对象数组。
第6章 信息的度量和作用
1. 信息熵
* 数学表达
当我们面对一条内容丰富的消息时,我们常常会感叹“这条消息的信息量太大了”。然而,长久以来,如何以科学、客观的方式准确量化这种“信息量”一直是一个难题。直至1948年,这一挑战得到了里程碑式的解决。在这一年,著名的信息论先驱克劳德·香农(Claude Shannon)在其开创性的论文《通信的数学原理》(A Mathematic Theory of Communication)中提出了“信息熵”的概念。这一概念的引入不仅为信息的度量提供了坚实的数学基础,而且使得我们能够量化评估信息在通信系统中的实际作用。信息熵的提出,标志着信息论这一新兴学科的诞生,也为后续的信息科学和技术的发展奠定了坚实的基础。
“信息熵”(Entropy),单位是比特(Bit),一个比特是一位二进制数(0或者1)。
之所以称之为‘信息熵’,是因为这一概念与物理学中热力学领域的‘熵’有着异曲同工之妙,两者都揭示了一种秩序与混乱、确定性与不确定性的度量。在热力学中,熵代表着系统的混乱程度,是系统微观状态数量的度量,而信息熵则描述了信息的不确定性和随机性。两者虽然在不同的学科领域中发展,但它们的本质都是对某种‘无序’或‘不确定性’的量化。这种跨学科的联动真令人感叹。
* 理解NLP中的信息熵概念
目前中文常用汉字的数量大约为7000个。在理想化的假设下,若每个汉字在文本中出现的概率完全相等,则根据信息论的原理,我们大约需要13比特(bit)的编码长度来唯一标识并表达一个汉字,因为 约等于7000。然而,这种假设在实际情况中并不成立,因为每个汉字在实际文本中的使用频率是显著不同的。
事实上,据统计,前10%的最常用汉字占据了常用文本中超过95%的出现频率。这意味着,在实际应用中,如果我们根据汉字的出现频率进行编码,那么编码效率将大大提高。因此,在不考虑上下文信息的情况下,仅从每个汉字单独出现的频率来看,我们大约只需要9比特来编码并表达一个汉字,这显著减少了所需的存储空间或传输带宽。
进一步地,当我们将上下文信息纳入考虑时,编码效率还能得到进一步的提升。这是因为上下文信息可以帮助我们预测某个汉字在特定语境下出现的可能性,从而减少编码时的冗余。因此,在充分考虑上下文信息的情况下,我们可能只需要大约5比特就能有效地表达一个汉字,这进一步体现了自然语言处理中信息编码的复杂性和精妙性。
2. 消除不确定性
2.1 条件熵
*数学表达
为有效减少某一事件(如“飞往三亚的机票价格是否高昂?”)的不确定性,最为有效的途径是通过外部渠道引入新的信息。这些信息可以直接针对目标事件本身(例如,“确实昂贵,当前单价已达到4000元”),也可以是与该事件相关联的其他相关事件或数据(例如,“据悉,计划前往三亚的旅客数量激增”)。通过利用这些“相关联的信息”,我们可以有效地降低目标事件的不确定性。这一策略的数学可行性可通过引入条件熵(conditional entropy)的概念加以证明。
* 理解NLP中的条件熵概念
为了让大家对前述的概率概念有更深入的理解,我们可以借助在前几章详细探讨的统计语言模型来进行类比和思考。在这个框架下,我们可以将变量 视为当前需要预测的单词,而变量 则代表该单词之前的单词,即上下文中的前驱词。在前面章节中,我们通过实验已得出结论:在考虑单词间依赖关系的情况下,二元模型(即考虑当前单词与前一个单词之间关系的模型)相较于一元模型(仅考虑单词自身出现频率的模型)能够提供更准确的预测结果。
2.2 互信息
*数学表达
两个随机事件 与 互信息,本质上就等于,信息熵 与条件熵 的差值(大家细细品)。如果 与 完全相关,那么两者的互信息就等于 ;如果 与 完全不相关,则互信息就等于0。
* 理解NLP中的互信息概念
在阐述自然语言处理领域中机器翻译的挑战时,作者特别提到了互信息在处理一词多义问题上的重要性。众所周知,英语中存在许多常见词汇,它们拥有众多截然不同的含义,这些含义在语境中往往毫无关联。以“布什总统”为例,其中的“bush”一词,在英文中既可以指代人名,即美国前总统“布什”,也可以指代自然界的“灌木丛”。这两个含义在日常使用中均极为普遍。
早期,一些机器翻译软件在处理这类词汇时,由于缺乏足够的上下文理解能力,曾将“布什总统”错误地翻译为“灌木丛总统”,这样的翻译结果显然荒诞不经,引起了广泛的笑话。
为了纠正这一错误,科学家们最初尝试通过制定复杂的语法规则来区分这些词汇的不同含义。然而,他们很快发现,这种方法存在着极大的局限性。因为,无论是哪种语法规则,都难以完全覆盖语言中所有可能出现的特殊情况。更糟糕的是,每当遇到新的特殊语境或用法时,就需要添加新的语法规则,这导致语法规则库不断膨胀,变得庞大而难以管理。
显然,这种依靠语法规则来解决一词多义问题的方法并不实际。于是,研究者们转变了思路,开始利用互信息的概念来解决这一难题。互信息能够量化词汇与其上下文之间的关联性,通过分析文本中各个词汇之间的统计关系,机器翻译系统能够更准确地理解词汇在特定语境下的具体含义。比如首先,我们通过分析海量的文本数据,识别出与“布什总统”这一词组共同出现且互信息值较高的词汇,如“总统”、“美国”、“国会”、“华盛顿”等。这些词汇与“布什总统”形成了强烈的关联性,为我们提供了明确的上下文线索。同样地,我们也运用相同的方法,找出了与“灌木丛”关联紧密且互信息值显著的词汇,如“土壤”、“植物”、“野生”等,这些词汇共同勾勒出了“灌木丛”的自然环境背景。通过这种方法,我们能够更准确地理解词汇在特定上下文中的含义,从而为机器翻译提供更加精确的指导。
实践证明,这种基于互信息的方法在处理一词多义问题上取得了显著成效,有效地解决了机器翻译中的这一难题。
3. 相对熵
* 数学表达
相对熵(也称为KL散度)和互信息是信息论中两个核心概念,它们在衡量信息的不确定性、相似性以及变量间的关系方面起着重要作用。相对熵主要用于度量两个概率分布函数之间的差异,而互信息则是衡量两个随机变量之间共享的信息量。
1)当两个函数在定义域内的所有取值都完全相同时,它们之间的相对熵等于零。这意味着从信息论的角度来看,这两个函数在描述数据或概率分布时没有差异。
2)相对熵是一种量化两个函数(通常是概率分布或概率密度函数)之间差异程度的度量。当相对熵的值越大时,表示这两个函数之间的差异越大;相反,如果相对熵的值越小,那么这两个函数之间的差异也就越小。
3)对于概率分布或概率密度函数,只要它们的取值都大于零(即没有零概率事件),我们就可以使用相对熵来度量这两个随机分布之间的差异。这种度量方式在信息论和机器学习中非常重要,因为它可以帮助我们理解不同分布之间的相似性或差异性。
*理解NLP中的相对熵概念
在文本查重(如学术论文查重)这一场景中,相对熵的应用尤为显著。其基本原理在于通过对比两篇文本的词频分布(即概率分布函数),来评估它们之间的相似性。当两篇文本的相似度极高时,它们所包含的词汇及其频率分布将趋于一致,从而导致这两篇文本之间的相对熵值趋近于0。基于这一原理,我们可以有效地判断是否存在抄袭现象。
此外,在机器翻译领域,相对熵同样展现出其独特的价值。通过计算翻译模型的输出分布与参考翻译的分布之间的相对熵,我们可以量化评估翻译结果的质量。较低的相对熵值意味着翻译模型的输出与参考翻译更为接近,从而反映出更高的翻译质量。这为研究者和工程师提供了一个有力的工具,用于优化翻译模型,提升NLP应用的性能。
4. 引用:《数学之美》