基础技能树-20 字符串

小说:大连天健棋牌游戏大厅作者:邓密通更新时间:2019-01-22字数:33681

“昊天宗一门双斗罗,唐天、唐昊两兄弟,曾经令整个魂师界为之颤抖。唐天是你大伯,他比你父亲足足大了十五岁,在六十岁那年,顺利突破了九十级,进入封号斗罗的层面。而你父亲获得封号斗罗,却是在十三年前,那时候,他才不过四十四岁。所以我才会说,他是当今天下,最年轻的封号斗罗,也是我的偶像。”

92游戏中心手机版

如果是平日,林风完全可以凭借刀的速度阻断身前区域,此时不能,毒娘子攻入,整个人向后退去。
奇为兵者之王,同样是大忌,如果运用不得当,最后只会被其所害,狂狮此时陷入极度的矛盾之中,冲,冲不进去,而且和两边失去了联系,如果自己这个时候带着人退,一旦两侧的人冲进来,等于是一头钻进对方的长矛、弯刀之下。

“只因林风从开门小道人衣着看出一些不妥,心里觉得奇怪,所以才会派人前去查探一番。”

基础技能树-20 字符串


本节内容

  • 开篇
  • Go字符串和C char*的差异
  • 为什么实现为不可变类型
  • 拼接字符串实现方式[付费阅读]
  • 转换性能优化[付费阅读]

开篇

之前接触了汇编,从反汇编来看,站在汇编的角度基本上没有什么数据结构一说,只有纯数字,两大类,第一类是有多长,1字节2字节8字节。第二类所有的东西全是字节。要么传地址,地址也是一些整数,要不就是把内存中一些字节从寄存器搬到内存或者从内存搬到寄存器或者做一些简单的数学运算。所以说站在汇编的角度事情往往变得很简单直接。这有个好处就是我们可以抛开很复杂的抽象理论去研究计算机本质是怎样工作的。

那么有个问题是我们编程时不可能所有的东西全部用数字来表达,我们肯定需要用些其它的抽象概念来表达。当然我们知道从本质上来说,不管是字符串也好还是其它对象也好类型也好,反正都是由一些数字构成的一些数学模型。当我们学习这些抽象概念的时候,我们必须要有一种途径把它还原出去原始的状态。

今天开始我们研究一些除了基本类型以外的复合类型。从字面上来说复合类型就是由多个基本类型构成的,第一个内存是连续的,或者是用指针指向另外的内存。我们管这种由多个独立内存块构成的数据结构通常称之为复合类型。当然在不同的语言里对于复合类型的定义可能不太一样。比如说c语言里默认没有字符串的概念,我们把某个字节数组以某个结束符标记的字节数组称之为字符串。没有一尘不变的东西。所有的东西都是由其它数据结构来模拟完成的。

整个内存可以看做成超大号的字节数组,其中地址就是数组的序号,所以说不管你类型多么的复杂归根结底它是在数组之上进行抽象的。因为我们可以把整个虚拟地址空间看做成字节数组。在这字节数组之上通过一些关联逻辑把它构想成一个复杂的数据结构。那么我们把这些数据结构最终还原成具体的内存布局。学习c语言都知道我们研究一个对象的时候首先需要知道它的内存布局结构。因为这关系到我们进行代码优化,选择什么样的算法。因为我们要知道当我们传递一个对象的时候究竟复制什么东西,如果是简单的整数那把整数复制过去,那复合结构呢?是复制一块呢还是把两块全复制,这个有很大差别的,如果是由指针指向的,是复制本身还是连指针指向的内存块一起复制。如果只复制本身,那么是不是有另外的指针也指向那块内存块。那么这时候就会存在数据竞争问题,所以对于一个复合类型来说,我们必须要知道它究竟在内存中什么样的,它是怎么构成的,这个对于我们选择什么样的数据结构或者说进行什么样的优化有直接关系。

我们常见的复合结构有很多,从简单的学起。比如说字符串、数组。

Go字符串和C char*的差异

接下来我们需要搞清楚一个字符串在内存中究竟什么样子?

c语言中字符串在内存中什么样子,*char代表字符串,一个字节数组加个一个结束符。

go语言底层也是字节数组,字节数组保存数据但没有结束信息,它单独会构建一个头信息,头信息开始位置是个指针指向字节数组起始位置,后面用一个长度表达有多长。

所以说这地方有很大的差别在于c语言字符串是连续的内存块,go语言里很显然是个复合结构,由两块内存组成的。一个是字符串头信息,一个是数据体。那么当我们复制字符串的时候,我们得知道我们到底复制的是哪部分。

go语言判断字符串的长度,sizeof处理的是返回类型的长度,很显然所有类型长度都是固定的,大多时候这种复合结构返回类型长度只有头信息。我们返回实例长度和返回类型的长度不是一回事。

go语言一个字符串标准内容是一个指针加上一个长度的标准信息,由两个字段构成,所以字符串长度是16字节。至于指针指向什么地方那是实例的属性和类型无关。类型里面只看到一个指针。一个类型和一个实例是有差别的。

例子:

$ cat test.go
package main

import (
    "unsafe"
)

func main() {
    s := "abcdefg"
    println(unsafe.Sizeof(s), len(s))
}

unsafe.Sizeof(s)返回的是字符串类型的长度,len(s)返回的是实例的长度。

输出

16 7

查看结构

$ go build -o test -gcflags "-N -l" test.go
$ gdb test
$ l
$ b 24
$ r
$ ptype s #输出字符串类型信息,看到字符串标准类型是由一个指针加上一个整数组成的。至于指针指向什么地方是实例的内容。
$ x/2xg &s #查看s的实例信息,第一块是指针,第二块是长度,字符串地址是在栈上面
$ x/s 0x0000000000472288 #这个是底层字节数组
$ info files #对比地址指向rodata里,字面量abcdefg保存在rodata里。
$ p/x $rsp
$ p/x &s #字符串地址是在栈上面

那么字符串标准的结构是指针和长度7,然后指针指向字节数组,字节数组在rodata里。这就是字符串的结构。那么我们动态构建一个字符串信息,字节数组会不会在rodata里?

package main

import (
    "unsafe"
    "strings"
)

func main() {
    s := strings.Repeat("a", 3)
    println(unsafe.Sizeof(s), len(s))
}
$ go build -o test -gcflags "-N -l" test.go
$ gdb test
$ l
$ b 25
$ r
$ p/x &s #字符串对象是在堆上
$ p/x $rsp #用rsp对比
$ x/2xg &s #查看字符串内容,第一块是地址不属于rodata,是托管堆的地址,第二块是长度。

我们知道预编译的地址基本上都是短地址,这时候动态构建实际上是在堆上分配的,为什么把这个数据分配在堆上而不分配在栈上。我们知道这个字符串是由strings.Repeat函数来生成的,函数返回局部变量的时候有两种可能,一种这个函数被内联了直接在当前栈桢分配,如果不能内联想返回一个失效栈桢里面的数据是不安全的,那么只能把这个数据扔到堆上。第二种原因是很多语言对于字符串特殊处理,在大部分语言字符串都是一种很特殊的数据类型。

为什么实现为不可变类型

我们知道在很多语言里,字符串都有几个特征,一是所有字符串都是不可变类型,一旦生成了字符串以后你不能对这个字符串进行修改。修改的前提你要转换或者重新拼一个新的字符串出来。很多语言都有这样的限制,那么为什么要把字符串实现成不可变类型,我们知道像python、go、java、c#字符串是不可变的。

池化共享

我们知道任何语言里面的优化基本原则都是基于一定统计的基础,我们知道字符串这个类型是我们日常工作当中使用频率非常高的数据类型,而且这种类型有个特点是不定长的,还有对于字符串处理往往非常复杂,如果我们设计这种数据结构怎么样实现呢?核心问题是提高它的处理效率。我们会尽可能的把它缓存起来,比如说多个人使用同一个字符串的时候或者字符串在不同函数中传递的时候,我们希望要么复制要么不可变。那么字符串被大量引用的情况下最好的方式是它是数据安全的、不可变的。好处是不管引用多少次,我只保留一个副本。当你对这个进行修改的时候你就创建一个新的。这是一个理由我们可以把它进行池化操作,尤其像动态语言python中所有的名字空间里面都是用字符串来实现的。这些字符串会被大量的使用,如果每次都创建新的对象那内存管理就什么都不用干了全变成零零碎碎的了。

除此之外很多语言对字符串做池化处理来减少相同字符串在内存当中的副本数量,运行时会尽一切可能对字符串做出各种各样的优化处理,不同的语言有不同的做法。

map[hashcode]

假如你自己写个算法你要比较两个字符串是否相等,你觉得什么方式最快,简单的做法直接比较hashcode,当我们每个字符串生成了以后,这个字符串如果是不可变的情况下,生成完了之后我可以立即计算它的hashcode,我把hashcode作为这个字符串对象的一部分,那么接下来只要判断他们的hashcode是否相同,如果相同也有可能不一样因为有哈希碰撞问题。所以你会注意到在很多语言里,每个字符串都会生成后立即把hashcode计算出来。因为这两个hashcode不同的话这两个字符串肯定不一样,如果hashcode相同,再去比较他们字符这样来避免哈希碰撞问题。前三个条件可以减少我们低效率操作。这也是让字符串变成不可变类型的原因。

安全性

安全性比如说很少见的语言有这样一个做法,比如我们调用sql操作,当我们把select语句扔进去的时候,如果这个字符串类型是只读的可以避免这个字符串对象被修改,因为一旦字符串被修改了以后它和原来的字符串就不属于同一块,那么可以验证字符串在中途是否变更过来避免一些非安全的注入。这个在一些很少见的DSL语言见过,大部分通用编程语言我们没有见过类似这样的东西。有些像sql引擎它会做这样的操作,你提交的sql语句它会验证你的sql语句是否被修改过,最好的办法是这个字符串对象是只读的。因为这块内存是只读的情况下你对这块内存访问会触发一些安全机制,那么不可写肯定是没有办法修改你的信息的,你没有办法注入非安全的东西,因为很多时候它是支持插件机制的。但这种东西比较少见,只是在一些引擎或者DSL里面见过。

当前文章:http://0477auto.com/forum.php?mod=viewthread&tid=28786

发布时间:2019-01-22 00:41:47

机子麻将打牌技巧 美女斗地主单机版 集结号3d手机版官网下载 广西快三计划软件 850网络棋牌的输赢规律 手游棋牌老输钱 亲朋爽翻捕鱼手游 四川长牌手机版下载

编辑:王纯马侯

我要说两句: (0人参与)

发布