中文版本由空白的贝塔君
整理发布
第五章 字符串处理
SystemVerilog语言本身提供了许多字符串操作。然而,经验表明,内置方法不足以满足工作中的字符串处理任务,svlib
提供了进一步的操作集来帮助满足这些需求。
在大多数情况下,字符串操作有两种不同的形式,用户可以自由选择更适合自己需要的形式。
- 第一种形式是关于字符串变量的简单函数,通常(但不总是)返回字符串结果。这些函数在svlib包中定义,名称都以str_开头。第二种形式是
Str
类对象的方法(注意大写的S
)。Str
类是SystemVerilog字符串的wrapper
,通过引用传递字符串,并使一些操作更方便。
对比使用简单函数,使用Str
对象必须在所有操作之前构造对象。不过通过Str
对象的许多操作的效率和便利性通常收益是利大于弊的。程序员可以自由选择对他们来说最方便的方法。如果只需要对一个字符串执行一个操作,那么pkg级函数可能是最方便的。如果要对同一个字符串执行许多连续操作,最好创建一个Str
对象来进行处理。
5.1 Str
类
5.1.1 处理Str对象和成员的方法
static function Str Str::create(string s = "");
function void set (string s);
function string get ();
function Str copy ();
function int len ();
前文提到过,用户不能直接通过new
函数创建对象,必须使用Str::create
方法。当然,创建对象是可以无视参s
。
对象创建以后,随时可以使用set
方法更新字符串成员。而get
方法则返回对象保存的字符串。len
方法则返回字符串长度。copy
函数则返回一个新的对象,并且它的内容与调用的对象一致。
5.1.2 枚举类型
typedef enum {NONE, LEFT, RIGHT, BOTH} side_enum;
typedef enum {START, END} origin_enum;
这两个枚举用于指定某些方法的各种可选行为。ide_enum
用于指定字符串的哪一侧将参与各种操作,特别是trim
和pad
。origin_enum
用于指定在range
和replace
操作时从字符串的哪端计数。START
指定字符串最左端,END
指定最右端。这些选项的细节将在后面的小节中展开。
5.1.3 在Str对象的字符串后面拼接一个字符串
function void append(string s);
这个函数通过使用简单的字符串连接,将指定的字符串拼接到一个Str
对象的字符串成员后面,从而修改该对象的现有字符串内成员。
5.1.4 查找子字符串
function int first (string substr, int ignore=0);
function int last (string substr, int ignore=0);
first()
在对象的字符串内容中搜索字符串子str的第一次出现的位置。它返回子字符串的最左边字符在原始字符串中的位置。如果搜索失败(在原始字符串中没有出现子字符串),则函数返回-1。这个方法的搜索是精确的文字匹配,不使用通配符或正则表达式匹配。
参数ignore
指定搜索从哪里开始。默认值(ignore=0
)将扫描整个字符串,并返回第一个匹配项。如果ignore
大于零,搜索将从指定的字符位置开始。不管ignore
的值是多少,成功匹配后的返回值都是匹配在原始字符串中的绝对起始位置。
last
的行为方式类似,但它从字符串的最右端开始扫描,因此,如果查找的子字符串在原始字符串中出现多次,它将返回最后一个可能的匹配结果。最后,ignore
参数指定在字符串最右端的要忽略的字符数——它的作用等效于这部分字符不存在。
「注意」:Str
类的first
和last
方法提供了一个简单快速的子字符串搜索方法。在第六章中,使用正则表达式匹配可以更灵活地进行搜索匹配,但这种灵活性的代价是参数配置增加和速度下降。在大多数情况下,是利大于弊的,正则表达式是首选。
5.1.5 切割和连接操作
function string sjoin (qs elements);
function qs split (string splitset="", bit keepSplitters=0);
「注意」:svlib
内部定义了类型名qs
,表示“queue of strings”,但用户代码不能调用它。如果你需要一个类型名来表示字符串队列,你应该自己定义类型名,能完全兼容(类型等效)qs。另外,也可以简单地声明字符串队列的变量,并使用它们作为参数和结果变量。
sjoin
方法(不使用join
作为名称,是因为和SystemVerilog关键字冲突)使用Str
对象的内容作为“joiner
”,将字符串队列中的元素组装成单个字符串。例如,它可以方便地创建逗号分隔的列表。
split
方法获取Str
对象的现有字符串(保持不变),并使用单个字符分割标记("splitter
")将其分割成字符串队列。参数splitset
是一个字符串,但它被视为一组单独的字符;对象的字符串变量被分割,分割的位置是出现splitset
中字符的位置。如果splitset
是一个空字符串,那么对象的字符串会被分割后的字符串队列的每个元素都将是单个字符。
如果keepsplitter
为true
(1)且splitset
不是空字符串,则拆分字符将作为结果队列的单个成员出现在其对应的位置。如果keepsplitter
为false
(默认值),拆分字符将不会出现在结果中。
「注意」:从svlib的0.5版开始,Regex类中有一个新的split方法(见第6章)。它提供了比这里的Str::split方法灵活得多的功能,在大多数情况下是首选方法。
5.1.6 提取子字符串和替换操作
function string range (int p, int n, origin_enum origin=START);
function void replace(string rs, int p, int n, origin_enum origin=START);
range
提供了比SystemVerilog原生字符串的substr
操作的更通用和统一的方法。当其中一个边界超出字符串时,它的表现会更加正常。在第5.3节中,详细地介绍了如何使用p
、n
和origin
参数指定字符串的一个切片的详细信息。range
只返回指定的子字符串,返回类型为SystemVerilog的字符串类型。
replace以
完全相同的方式指定子字符串,然后用rs
替换该子字符串,并修改Str对象的内容。replace
非常灵活,有时可以单独使用。例如:
- 通过传入空的
rs
参数,删除指定子字符串通过下面的方式可以实现在尾部添加一个字符串
s.replace(append_string, 0, 0, Str::END);
- 通过下面的方式可以实现在开头添加一个字符串
s.replace(prefix_string, 0, 0, Str::START);
传入的rs
字符串的长度没有限制,不需要和被替换的字符串长度一致。
5.1.7 在字符串的开头和结尾删除或添加空白字符
function void trim (side_enum side=BOTH);
function void pad (int width, side_enum side=BOTH);
trim
删除字符串的开头或者结尾的所有空白字符,它会修改Str对象的现有内容。参数side指定要修剪字符串的哪一端。如果side是Str::LEFT
,则从字符串的左端删除空白;RIGHT
删除尾随空格;BOTH
删除两端的空格。最后,如果指定了NONE
,就不会产生任何效果。
空白字符包括任何空格、制表符、换行符、回车符和不间断空格(ASCII码160)。
如果字符串完全由空格组成,并且side
参数不是NONE
,则结果将是一个空字符串。
pad
会在开头或者结尾添加空白字符(使用空格字符),使结果字符串的长度正好是width
。如果字符串已经大于width
,则不进行任何操作。如果side
为NONE
,则字符串不变。否则,将根据需要在指定的字符串末尾添加空格。如果side
为BOTH
,则在两边添加相同数量的空格(必要时在右侧添加一个额外的空格)。此方法对于以表格格式打印的文本对齐非常有用。
5.1.8 删除字符串中不想要的字符
function void strip (string chars = " \t\n\13\14\15\240\177");
strip
删除Str对象中以字符形式出现的所有字符。默认情况下是删除所有空白字符,但您可以指定一个包含您想要删除的任何字符的字符串。
5.1.9 将字符串转换为systemverilog的标准字符串
function void quote ();
此方法会更新对象,对字符串的进行转义处理。使用转义字符,如"和\n,将特殊字符(反斜杠,双引号,控制字符等)替换为等价字符。在需要的地方使用更通用的\xNN表示法。最后,整个字符串由一对字符串引号(")包围。结果总是一个完整的、合法的SystemVerilog字符串。
这个函数是用来编写SystemVerilog的,用于生成SystemVerilog源代码。在以逗号分隔值(CSV)等格式写入文件时,也很有用。
5.2 包级字符串函数
function string str_sjoin(qs elements, string joiner);
function string str_trim(string s, Str::side_enum side=Str::BOTH);
function string str_pad(
string s, int width, Str::side_enum side=Str::BOTH);
function string str_quote(string s);
function string str_replace(
string s, string rs,
int p, int n, Str::origin_enum origin=Str::START);
function string strip (
string s, string chars = " \t\n\13\14\15\240\177");
如果只想进行简单的操作,创建一个对象其实是很不方便的。因此,svlib
提供了一些字符串操作作为包级函数,作为类方法的替代。这些函数执行的操作与Str类的相应方法完全相同。在方法内部,都用参数string s填充Str对象,然后再执行操作,并最终返回对应的结果。这些方法性能开销很小,因为库维护了一个Str对象池,专门用于此类操作。
5.3 指定字符串范围
svlib
使用单一且一致的方式指定子字符串范围(字符串的切片)。它显式地在Str
的方法range和replace(以及相应的包级函数str_range和str_replace)中使用,也在其他地方隐式地使用。它的设计是为了降低SystemVerilog的自带的字符串类型的substr操作的复杂性。
5.3.1 起点的定义
不根据字符数指定字符串范围,因为这会导致在处理零长度字符串切片时出现奇怪的不连续。字符串切片的边界是根据字符之间的位置指定的。为了说明这一点,考虑5个字符的字符串“Hello”:
使用这种方法,我们有一种一致的方式来指定子字符串的边界位置,使用参数p
,将origin
参数指定为Str::START
(默认值)。通过这种方式可以直观地理解,负的,或者大于字符串的长度,所代表的位置。
也可以根据字符串的Str::END
(最右边的位置)指定边界。在下例中,修改了对不同p
参数值的定义,p
从右(结束)字符边界向左计算:
我们直接定义了p
的超出范围值时的意义。因此,如果将origin
指定为Str::END
,我们就可以指定字符串的末尾部分,而不必关心字符串的确切长度。
5.3.2 长度参数n的定义
在为字符串范围建立了起点之后,现在需要考虑希望获取的切片长度。这个参数n的解释不受原始值的任何影响。它指定从p指定的边界移动多远,以找到我们的子字符串的第二个边界。n为正表示向右移动。负值表示向左移动。
5.3.3 最终范围的定义
origin
、n
和p
三者指定了字符范围。例如,如果我们要调用函数str_range(.s("Hello"), .p(3), .n(4), .origin(Str::START))
,它将指定下面图表中阴影代表的范围:
p=3
, origin=START
指定了起始位置3;n=4
指定了从p指定的位置右侧开始的四个字符位置。然而,其中两个字符位置不在原来的5个字符的字符串中,因此范围操作的结果是两个字符的字符串“lo”。
5.3.4 一些例子
下面是各种情况的一些例子。