首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
269 阅读
2
从零开始配置 vim(15)——状态栏配置
159 阅读
3
nvim番外之将配置的插件管理器更新为lazy
145 阅读
4
2018总结与2019规划
137 阅读
5
PDF标准详解(五)——图形状态
116 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
读书笔记
elisp
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
392
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
90
篇与
的结果
2025-02-11
Emacs 折腾日记(十二)——变量
本文是依据 emacs lisp 简明教程 而来在此之前我们已经了解了elisp中的全局变量和函数中的局部变量,也了解了elisp中各种数据类型。这一篇主要谈谈elisp中各种变量的生命周期和作用域let 绑定的变量使用let绑定的变量只在let范围内有效,如果是多层嵌套的let,只有最里层的那个变量是有效的,用 setq 改变的也只是最里层的变量,而不影响外层的变量。比如(progn (setq foo "I'm global variable!") (let ((foo 5)) (message "foo value is: %S" foo) ;; ==> "foo value is: 5" (let (foo) (setq foo "I'm local variable!") (message foo)) ;; ==> "i’m local variable!" (message "foo value is still: %S" foo)) ;; ==> "foo value is still: 5" (message foo)) ;; ==> "i’m global variable!"这个有点像C/C++中的{} 定义的语句块中的变量只在当前 {} 内有效,当内部变量与外部变量重名的时候只影响{}内,而不影响外部。我们可以给出这样的c++代码int n = 0; printf("n = %d\n", n); { int n = 10; printf("n = %d\n", n); } printf("n = %d\n", n);elisp中的let两边的括号就有点像这里的 {},出了这个范围定义的变量就无效了。但是定义变量使用的是栈空间,而程序的栈空间是有大小限制的,一旦超过这个范围就会发生栈溢出。在C/C++程序中一般发生在递归层数过大。我们可以在编译时修改这个栈空间的大小。在elisp中也有变量控制递归层数,它就是 max-specpdl-size 但是在最新的29中已经将它弃用,新版的emacs可以使用 max-lisp-eval-depth 来限制specpdl堆栈的大小,该堆栈主要用于 存储动态变量绑定和 unwind-protect 激活等。buffer-local 变量顾名思义,它的值只在当前buffer中生效,在其他buffer中可能是另外的值。它有点像之前介绍的vim中的setlocal变量,仅在当前缓冲区内生效。该特性常用于实现缓冲区特定的配置和行为。声明一个 buffer-local 的变量可以用 make-variable-buffer-local 或用 make-local-variable。这两个函数的区别在于前者是在所有缓冲区都创建一个 buffer-local 的变量。而后者只在声明时所在的缓冲区内产生一个局部变量,而其它缓冲区仍然使用的是全局变量。一般来说推荐使用 make-local-variable。下面来举例说明它们的区别,在举例之前介绍例子中用到的函数或者宏with-current-buffer, 它的使用方式是(with-current-buffer buffer body)其中 buffer 可以是一个缓冲区对象,也可以是缓冲区的名字。它的作用是使其中的 body 表达式在指定的缓冲区里执行。default-value 可以访问符号所对应的全局变量下面是使用 make-local-variable 创建buffer-local变量的例子(setq foo "i'm a global variable!") (make-local-variable 'foo) foo ;; ==> "i'm a global variable!" (setq foo "i'm buffer-local variable in scratch buffer") foo ;; ==> "i'm buffer-local variable in scratch buffer" (with-current-buffer "*Messages*" (progn (message "%s" foo) ;; ==> "i'm a global variable!" (setq foo "i'm buffer-local variable in message buffer") (message "%s" foo))) ;; ==> "i'm buffer-local variable in message buffer" (default-value 'foo) ;; ==> i'm buffer-local variable in message buffer上述代码因为message buffer 中未定义foo的buffer-local 变量,所以它修改的是全局变量的值,我们使用 default-value 发现全局变量的值被修改了。(setq foo "i'm a global variable!") (make-variable-buffer-local 'foo) foo (setq foo "i'm buffer-local variable in scratch buffer") foo (with-current-buffer "*Messages*" (progn (message "%s" foo) (setq foo "i'm buffer-local variable in message buffer") (message "%s" foo))) (default-value 'foo) ;; ==> "i'm a global variable!"前面的结果都一样,但是我们关注一下最后输出的全局的 foo 变量,它的值没有被修改,而使用 make-local-variable 的时候它被修改了。这是因为 make-variable-buffer-local 会在每一个缓冲区内都创建一个 buffer-local 的拷贝,所以后面在 message 缓冲区中修改的是缓冲区内自己的 buffer-local 变量而不影响全局变量的值,而 make-local-variable则不同,它会在 message 缓冲区中为foo也创建一个buffer-local变量。message 缓冲区中修改的是 buffer-local 变量,不影响全局的foo变量。一般来说根据实际情况选择使用哪种,我并不是资深的elisp开发者,以我浅薄的认知,一般使用 make-local-variable情况较多,它影响面较小,仅仅影响当前缓冲区。而make-variable-buffer-local 影响面较大,一旦使用它设置了local-buffer 变量,那么在后面其他缓冲区中想要使用 setq 设置全局的值就没那么简单了。这种一般是需要隐藏起来的核心变量,例如某些功能依靠这个变量来驱动,一旦修改了可能导致后面的代码运行行为不准确。可能会使用 make-variable-buffer-local 定义,不让用户自己随便修改全局的值。我们可以使用 setq-default 来设置全局变量的值。这里的例子就不给出了,各位读者可以根据上面的例子稍加修改就可以了。测试一个变量是不是 buffer-local 可以用 local-variable-p。这里我们再介绍一个新的函数 get-buffer。它需要一个buffer的名称作为参数,返回这个buffer的对象,如果未找到对应的buffer,则返回nil。(setq foo 5) (make-local-variable 'foo) (local-variable-p 'foo) ;; ==> t (local-variable-p 'foo (get-buffer "*Messages*")) ;; ==> nil如果要在当前缓冲区里得到其它缓冲区的 buffer-local 变量的值可以用 buffer-local-value(setq foo "i'm a global variable!") (make-local-variable 'foo) (setq foo "i'm buffer-local variable in scratch buffer") (with-current-buffer "*Messages*" (buffer-local-value 'foo (get-buffer "*scratch*"))) ;; ==> "i'm buffer-local variable in scratch buffer"变量的作用域在之前我们已经介绍过几种变量,分别是使用 setq 或者 defvar 定义的变量,它们是全局变量,即使在一些语法块或者函数中定义的,在外围也可以正常访问使用 let 或者 let* 定义的变量,只在 let 语法块中有效使用 make-local-variable 或者 make-variable-buffer-local 定义的buffer-local 变量,在当前buffer中有效但是函数参数列表的变量生命周期与我们平常在C/C++、Java、Python等语言中有些不同。作用域(scope)是指变量在代码中能够访问的位置。emacs lisp 这种绑定称为 indefinite scope。indefinite scope 也就是说可以在任何位置都可能访问一个变量名。而 lexical scope(词法作用域)指局部变量只能作用在函数中和一个块里(block)。比如 let 绑定和函数参数列表的变量在整个表达式内都是可见的,这有别于其它语言词法作用域的变量。先看下面这个例子(defun foo(x) (getx)) (defun getx() x) (message "%s" (foo "hello,x")) ;; ==> "hello,x"我们可以看到最终成功输出了结果,而根据之前学习C/C++的经验,在C、C++等语言中,这样的代码是无法执行成功的,因为在getx中并未定义x的值。但是在elisp 中,foo函数执行期间,x变量的都是有效的且可以正常访问到的。当然,在elisp中,let也具有这一效果,在let的语法块中,定义的变量总是有效的。例如(let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)在let语句块中 x 是一直有效的,如果脱离let,在最外层调用 getx 将会得到一个x未定义的错误需要注意的是,上面的例子无法再使用 C-x C-e 来一条条的执行了,这个时候需要使用 eval-buffer 来执行整个缓冲区的代码。emacs 从 24.1 版本开始,引入了 lexical binding 这一特性,在代码中如果启用这个属性,那么它将采用C/C++ 等普通编程语言的那种 lexical scope 的作用形式。官方文档中提到使用 lexical binding 特性可以提高代码的运行效率,并且鼓励使用。可以在代码文件最开始的位置添加这么一个注释 -*- lexical-binding: t -*- 来开启这一特性。上面的代码只要加上这一特性就能得到不一样的结果;; -*- lexical-binding: t -*- (let ((x "hello, x")) (message "%s" (getx))) (defun getx() x)这个时候执行将会得到一个错误信息 "Symbol’s value as variable is void: x",x这个符号是一个未定义的变量。生存期是指程序运行过程中,变量什么时候是有效的。全局变量和 buffer-local 变量都是始终存在的,前者只能当关闭emacs 或者用 unintern 从 obarray 里除去时才能消除。而 buffer-local 的变量也只能关闭缓冲区或者用 kill-local-variable 才会消失。而对于局部变量,elisp 使用的方式称为动态生存期:只有当绑定了这个变量的表达式运行时才是有效的。在 emacs lisp 简明教程 中举了一个闭包的例子,在elisp中并不支持闭包,它采用的是与普通编程语言一样的变量生存周期,这里我就不列出来了。JavaScript等语言中是支持闭包的,有兴趣的读者可以去看看JavaScript中的闭包。其他函数一个符号如果值为空,直接使用可能会产生一个错误。可以用 boundp 来测试一个变量是否有定义。这通常用于 elisp 扩展的移植(用于不同版本或 XEmacs)。对于一个 buffer-local 变量,它的缺省值可能是没有定义的,这时用 default-value 函数可能会出错。这时就先用 default-boundp 先进行测试。使一个变量的值重新为空,可以用 makunbound。要消除一个 buffer-local 变量用函数 kill-local-variable。可以用 kill-all-local-variables 消除所有的 buffer-local 变量。但是有属性 permanent-local 的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的,比如输入法。(setq foo "I'm local variable!") foo ; ==> "I'm local variable!" (boundp 'foo) ; ==> t (default-boundp 'foo) ; ==> t (with-current-buffer "*Messages*" (boundp 'foo)) ; ==> t (makunbound 'foo) ; ==> foo foo ; This will signal an error (boundp 'foo) ; ==> t (default-boundp 'foo) ; ==> t (kill-local-variable 'foo) ; ==> foo (with-current-buffer "*Messages*" (boundp 'foo)) ; ==> t上面的例子需要注意以下几点这里只是使变量的值为空,并没有消除这个变量的符号。所以在执行makunbound 之后,关于foo 是否绑定的测试都是tkill-local-variable 只是消除了foo作为buffer-local变量,并没有影响到全局变量,所以在messages-buffer中测试它仍然是有效的变量变量命名习惯对于变量的命名,有一些习惯,这样可以从变量名就能看出变量的用途:hook 一个在特定情况下调用的函数列表,比如关闭缓冲区时,进入某个模式时。function 值为一个函数functions 值为一个函数列表flag 值为 nil 或 non-nilpredicate 值是一个作判断的函数,返回 nil 或 non-nilprogram 或 -command 一个程序或 shell 命令名form 一个表达式forms 一个表达式列表。map 一个按键映射(keymap)
2025年02月11日
26 阅读
0 评论
0 点赞
2025-01-21
Emacs折腾日记(十一)——求值规则
截至到现在,我觉得我自己的elisp水平有了一定的提高,希望各位读者借助之前的文章也能有一些收获。现在已经可以尝试写一点elisp的程序了,但是如果想深入了解一下 lisp 是如何工作的,不妨先花些时间看看 lisp 的求值过程。对于我这样一个日常使用C/C++的程序员来说,习惯了C/C++的语法和写法,初次见到lisp这样使用括号并且主要是S-表达式的语言,开始总会有点不习惯,但是在尝试自己写了这么些文章之后,对lisp有那么一点感觉。这篇我想就着 求值规则 这篇文章以及自己的一些理解来尝试梳理一下自己是如何理解elisp表达式的。S表达式要理解S表达式,我们先从如何解析四则运算开始。在之前我鸽了一个系列就是使用C来实现C语言解析器的系列。在那个系列中提到,一个普通的4则运算最终会生成一个抽象语法树,例如 a * b - (c + d) 最终可以生成如下的抽象语法树 - / \ * + / \ / \ a b c d二叉树的每个节点,或者是叶节点,或者有2个子节点,叶节点可以用来存储数据。而每颗子树的根节点存储操作符,或者说表示要对数据进行的操作,而如果操作符需要一个或者多个操作数,那么可以对抽象语法树进行调整。可以用上面的图来表示树的话有些麻烦了,后来发明了点对表示法, 如果只关心叶子节点,每颗子树的根节点采用.来表示,那么这颗二叉树可以表示为 ((a . b) . (c . d)) 。看到这里各位读者想到了什么呢?cons cell。S表达式是点对表示法的形式定义:原子 -> 数字 | 符号 S表达式 -> 原子 | (S表达式 . S表达式)所以,S表达式或者是原子,或者是递归的由其他S表达式构成的点对。虽然抽象语法树可以使用这种点对来描述,但是语法树大了,点的数量大了,其实也挺麻烦的,所以lisp中有一些简单的写法。回顾一下之前学习列表和cons cell的知识,简化也就得到了列表,例如'((a . b) . (c . d)) ⇒ ((a . b) c . d) '((a . b) . (c . (d . nil))) ⇒ ((a . b) c d)如果我们考虑这颗树的根节点,并且采用先序遍历的方式访问,结果仍然采用点对来表示,那么将得到这样的结果 (- (* a b) (+ c d))。 这样我们得到了计算这个四则运算的lisp代码,这个它可以作为列表,也可以让lisp解释器来执行。到此为止,各位读者应该理解了S表达式。它其实就是对应了一颗语法树。现在看到S表达式也不那么恐惧了,解释器如何执行它似乎也慢慢的清晰起来了呢S表达式的求值理解了S表达式,再回过头来看看它的求值过程。所有的表达式可以分为三种:符号、列表和其它类型。我们来分别说明最简单的就是自求值表达式,前面说过数字、字符串、向量都是自求值表达式。还有两个特殊的符号 t 和 nil 也可以看成是自求值表达式。第二种表达式是符号。符号的求值结果就是符号的值。如果它没有值,就会出现 void-variable 的错误。第三种表达式是列表表达式。而列表表达式又可以根据第一个元素分为函数调用、宏调用和特殊表达式(special form)三种。根据上面对S表达式的理解,这里的第一个元素也就是放在语法树的每颗子树的根节点上,表示对它的子节点进行的操作,例如上面的加减乘除,或者使用car之类的函数。而它的子节点可以是一颗语法树,也可以是简单的值,对应在elisp中的话,就是这个操作可以针对上面两种自求值表达式或者符号值,也可以是另一个S表达式。整个求值过程就是不断的求子树然后使用根节点来对子树进行操作,例如针对上面的二叉树可以写下这么一段伪代码来实现求值float calc-ast(ast* pRoot) { switch(pRoot->eOprType) { case function: //函数调用 return function(calc-ast(pRoot->left), calc-ast(pRoot->right)); case math: // 数学计算 return calc-ast(pRoot->left) + calc-ast(pRoot->right); //这里以加法为例 .... default: calc-ast(pRoot->left); calc-ast(pRoot->right); break } }第一个元素如果是一个特殊表达式时,它的参数可能并不会全求值。这些特殊表达式通常是用于控制结构或者变量绑定。每个特殊表达式都有对应的求值规则。这个就根据具体的语法来定,例如 and 和 or 这些操作符具有短路的特性。本文内容到此就结束了,本文比较简单,算是对之前的一个总结,对lisp有一个大概的了解。最后,本文可能是年前最后一篇文章了,在这里提前祝各位读者新年快乐!
2025年01月21日
39 阅读
0 评论
0 点赞
2025-01-20
Emacs 折腾日记(十)——elisp符号
符号是有名字的对象,这么说可能有点抽象。我们先来回忆一下C/C++中关于符号的内容。C/C++ 最终被编译成机器码直接执行,在机器码中不存在变量名称,函数名称等字符,它只有一串地址。但是在写C/C++代码的时候有变量名,函数名,类名,对象名等等名称。编译器是如何做到将符号和地址关联起来的呢?答案是,编译器在编译阶段会提供一个符号表,符号表如何实现的我也不太清楚,但是它做到了关联一个地址和字符串符号的作用。在Windows平台的 vs下,debug版本一般会生成与exe同名的pdb文件,这个是调试文件,它里面保存了一些调试信息,包括符号表。这里的符号与C/C++中的符号表中的符号类似,可以通过它来找到具体的变量。可以理解成elisp提供了这么一种操作符号表的功能。首先必须知道的是符号的命名规则。符号名字可以含有任何字符。与之对应的,一般的编程语言在定义变量的时候有些特殊符号不能用,而且不能以数字开头,有些关键字也不能作为变量名。而elisp中没有这些限制,只是在使用特殊符号的时候需要使用转义字符。(symbolp '+1) ;; ==> nil (symbolp '\+1) ;; ==> t (symbol-name '+1) ;; error (symbol-name '\+1) ;; ==> "+1"上面的代码 symbolp 是在判断一个对象是否是一个符号的函数,symbol-name 用于取符号的名称,它会返回给定符号的名称的字符串。上面的代码说明了,elisp中符号没有什么特别要求,只是对于特定的字符需要使用转义字符。与c/c++中类似,elisp中的符号名也是区分大小写的。符号创建在C/C++中符号表由编译器来创建和操作。而elisp中则提供了操作符号表的方式。符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray。从这个名字可以看出这个表是用数组类型,事实上它是一个向量。对于一个新的符号,解释器会首先取字符串的hash值,根据hash值来放入数组对应的位置。同时我们也将这种保存符号的数据结构称之为obarray。也就是说obarray不仅是一个保存符号的变量,也是一种结构。我们也可以在符号上建立这么一个结构用来保存符号对应的属性。也可以作为参数传入emacs 相关函数中当 elisp 读入一个符号时,通常会先查找这个符号是否在 obarray 里出现过,如果没有则会把这个符号加入到 obarray 里。这样查找并加入一个符号的过程称为是 intern。intern 函数可以查找或加入一个名字到 obarray 里,返回对应的符号。默认是全局的obarray,也可以指定一个 obarray。intern-soft 与 intern 不同的是,当名字不在 obarray 里时,intern-soft 会返回 nil,而 intern 会加入到 obarray里。(setq foo (make-vector 10 0)) ;; ==> [0 0 0 0 0 0 0 0 0 0] (intern-soft "abc" foo) ;; ==> nil (intern "abc" foo) ;; ==> abc (intern-soft "abc") ;; ==> abc (intern-soft "abc") ;; ==> nillisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #:(intern-soft "abc") ;; ==> nil 'abc (intern-soft "abc") ;; ==> abc '#:abcd ;; ==> abcd (intern-soft "abcd") ;; ==> nil可以使用 untern 从obarray中删除对应的符号,如果成功删除,会返回t,如果没有对应的符号,则会返回 nil(intern "abc") ;; ==> abc (intern-soft "abc") ;; ==> abc (unintern "abc") ;; ==> t (intern-soft "abc") ;; ==> nil(setq foo 1) (intern-soft "foo") (unintern "foo") (intern-soft "foo") (1+ foo) ;; error通过setq,我们让elisp将foo这个变量放入到obarray中,后续使用unintern 删除这个变量后再使用foo的时候就会报错,foo是一个空变量与hash-table一样,obarray 也提供一个mapatom 函数来遍历整个obarray,例如下面是一个计算所有符号数量的例子(setq count 0) (defun count-sys(s) (setq count (1+ count))) (mapatoms 'count-sys) count ;; ==> 95733 (length obarray) ;; ==> 15121这里我们看到,数组的长度小于符号的数量。这根hash-table的实现是一样的。各位读者在学习hash-table的时候应该了解过,hash-table 中 hash值不同的元素存储在数组的不同位置,相同的元素通过链表进行串联,一般的hash-table在内存中的结构如下图.符号的组成在计算机中,所有的内容都是使用二进制来进行存储的,我们人为的将二进制数据划分为代码和数据。如果单纯的给出一个内存的地址,如何知道它是数据还是代码呢?例如在C/C++中定义了一个int类型的变量a,为什么在后续使用a(1) 这样的语句会报错呢?又例如一个函数指针 pfn,使用 *pfn = 1 这样也会报错呢?编译器怎么知道哪个地址存的是变量,哪个地址存的是函数指针呢?还是通过符号表来解决,符号表中针对每个符号名称都会给定它的类型,例如这个符号对应的地址是一个整数,或者指针,又或者是函数指针。符号名称类比C/C++ 中的符号表,elisp中每个符号都可以有4个组成部分,一个是符号名称,它可以类比到符号表中的名称,可以用symbol-name 来访问。它返回一个符号的字符串名称,关于使用的例子在最开始已经给出了。符号值第二个组成部分是符号值,可以类比成普通变量的值,可以通过set函数来设置,通过 symbol-value 来获取。(set 'abc "i am abc") ;; ==> "i am abc" (symbol-value 'abc) ;; ==> "i am abc" abc ;; ==> "i am abc"set 以及 symbol-value 需要提供一个符号,表示对哪个符号进行操作。解释器执行第一行代码的时候未发现 abc 这个符号,那么它会将abc放入符号表中,然后这个符号就可以作为普通变量来使用了。最后一行代码我们直接将它作为普通变量那样使用,直接对它进行求值,发现它也可以获取到具体的值我们使用 setq 也可以达到这样的效果(setq val 123) ;; ==> 123 (symbol-value 'val) ;; ==> 123 val ;; ==>123 (set 'val 1234) ;; ==> 1234 (symbol-value 'val) ;; ==> 1234 val ;; ==> 1234从上面的代码中发现,setq 直接使用变量名来对变量进行赋值,而set 则需要对符号进行quote操作。我们可以将setq 看做是一个宏(至于宏是什么,会在后面进行介绍),也就是 set quote,自动将后面的符号进行quote操作。但是setq只能对全局的obarray中的符号进行赋值,如果我们想放到指定的obarray中进行,此时就不能使用setq 了(setq foo (make-vector 10 0)) (set (intern "value" foo) 123) (symbol-value 'value) ;; ==> error (symbol-value (intern-soft "value" foo)) ;; ==> 123set 和 symbol-value 没有直接的参数来指定符号所在的obarray,如果想要使用自定义的obarray,那么就需要借助 intern、intern-soft、这样可以指定obarray 的函数来进行辅助操作。如果一个符号的值已经有设置过的话,则 boundp 测试返回 t,否则为 nil。对于 boundp 测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误(intern "null") (boundp 'null) ;; ==> nil null ;; error (set 'null 123) (boundp 'null) ;; ==> t null ;;==> 123函数第三个组成部分是函数,它可以类比成函数指针, 它可以用 symbol-function 来访问,用 fset 来设置。在之前一篇文章中,有知乎大牛指出我的问题,根据大牛的描述,在绑定lambda表达式时将函数部分绑定到符号的函数部分,使用funcall 调用的时候是在取函数部分的内容执行。这里详细了解符号相关的知识之后上述表达就很容易理解了。(setq foo (make-vector 10 0)) (fset (intern "abc" foo) (lambda (name) message "hello,%s" name)) (funcall (intern-soft "abc" foo) "Emacs") ;; ==> error上述的代码会报告一个错误,因为这里我们使用的obarray 是自定义的foo,它里面没有message这个符号,当然我们可以使用 intern来获取全局的 message 函数,并将它放入到foo中。这里的代码可以这么改(fset (intern "abc" foo) (lambda (name) (funcall (intern-soft "message") "hello,%s" name))) (funcall (intern-soft "abc" foo) "Emacs") ;; ==> "hello,Emacs"类似的,可以用 fboundp 测试一个符号的函数部分是否有设置。(fboundp 'message) (fboundp (intern-soft "message" foo)) ;; ==> nil (fset (intern "message" foo) (symbol-function 'message)) (fboundp (intern-soft "message" foo)) ;; ==>t属性列表第4个组成部分是属性列表,关于这部分我暂时还没想到该怎么用C/C++进行类比,如果非要一个类比的话,可以用这个类比。C/C++的编译器在看待变量的时候是将变量转变成对应的内存地址,操作变量实际上就是在操作变量所对应的内存。从CPU的角度来讲,CPU并没有规定哪些内存是只读的,哪些是数据,哪些是代码。编译器是如何做的呢?答案应该是编译器会在符号表中对各个符号做一些标记,例如const型变量所对应的内存不能修改。具体编译器是如何实现我也不太清楚,先这么生搬硬套吧,至少在了解elisp的符号这块,这么理解可能会稍微具体一点elisp中的属性列表,用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型。属性名和值可以是任意的 lisp 对象,但是通常名字是符号,可以用 get 和 put 来访问和修改属性值,用 symbol-plist 得到所有的属性列表:(put (intern "abc" foo) 'doc "this is abc") (get (intern-soft "abc" foo) 'doc) ;; ==> "this is abc" (symbol-plist (intern-soft "abc" foo)) ;; ==> (doc "this is abc")符号的属性列表在内部表示上是用(prop1 value1 prop2 value2 ...) 的形式, 在存取上有点像C/C++中的map,但是在elisp中并不是所谓的map结构。另外还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表,在上一段代码的基础之上(也就是设置了符号 abc 的 doc 属性的前提下),使用如下代码来进行测试(plist-get (symbol-plist (intern-soft "abc" foo)) 'doc) ;; ==> "this is abc" (plist-put (symbol-plist (intern-soft "abc" foo)) 'foo 69) (get (intern-soft "abc" foo) 'foo) ;; ==> 69 (setq my-plist '(doc "this is abc")) ;; ==> "this is abc" (plist-put my-plist 'foo 89) (plist-get my-plist 'doc) ;; ==> "this is abc" (plist-get my-plist 'foo) (get (intern-soft "abc" foo) 'foo) ;; ==> 69从上面的代码来看,plist-get 和 plist-put 需要一个额外的属性列表的操作表示要操作的属性列表,但是它也可以通过传入符号的真实属性列表直接来操作符号的属性列表。
2025年01月20日
23 阅读
0 评论
0 点赞
2025-01-15
Emacs 折腾日记(九)——elisp 数组与序列
elisp 中序列是数组和列表的统称,序列的共性是内部数据有一个先后的顺序,它与C/C++ 中有序列表类似。elisp 中的数组包括向量、字符串、char-table 和布尔向量,它们的关系如下:在之前一章中已经介绍了序列中的一种类型——列表,本篇将介绍序列中的另外一种数据类型——数组数组简介与C/C++ 中的数组类似,elisp中的数组有如下特征在创建之初给定长度之后不允许后期修改长度数组中的每个元素都可以通过索引来获取,并且获取的算法时间复杂度为O(1)数组是自求值的数组中的的元素可以通过 aref 来获得,并且通过aset 来设置值根据上图,向量是数组中的一种。字符串也是特殊的数组,它是内部全部都是字符的数组(虽然elisp中没有字符这种数据类型)。教程中没有介绍 char-table 和 bool-vector,所以这里我也不打算介绍,后面要是真遇到了再看。测试函数测试函数是用同名带p的函数来进行测试,例如 sequencep 来测试是否是一个序列,stringp 测试是否是一个字符串, vectorp 测试是否是一个向量,arrayp 测试是否是一个数组。char-table-p 和 bool-vector-p 分别测试对象是否是 char-table、bool-vector(arrayp [1 2 3]) ;; ==> t (vectorp [1 2 3]) ;; ==> t (stringp [?A ?B ?C]) ;; ==> nil (stringp "ABC") ;; ==>t (vectorp "ABC") ;; ==> nil (arrayp "ABC") ;; ==> t通过上面的测试发现字符串和向量是不同的类型,但是字符串也是一种数组上述代码创建了一个向量,然后判断向量是否是一个数组。在elisp中向量也是数组的一种,所以这里返回t序列的通用函数在字符串中提到过,可以使用 length 来获取字符串的长度。其实它是一个序列的函数,它可以获取序列的长度。对于列表来说,它只能获取真列表的长度,对于点列表它会报错,而对于循环列表则会陷入死循环。它的算法应该是跟C/C++ 中获取链表的长度的算法一样,根据最后一个节点的next指针域是否为空来进行判断。对于点列表和循环列表,可以使用 safe-length 来获取,从名称上看,它是一个安全的获取长度的函数。(length [1 2 3 4]) ;; ==> 4 (safe-length '(1 2 3 4)) ;; ==> 4 (safe-length '(1 2 3 . 4)) ;; ==>3这里不要疑惑为什么第二个参数表达式返回的结果会是3,表达式中列表真正的表达形式应该是'(1 (2 (3 . 4)))虽然写法上使用 (1 2 3 . 4) 比较清爽干净也容易理解,但是要时刻记住它真正的形式应该是多个cons cell组成。插一个题外话,不知道各位读者还记不记得当初在学数据结构时,学到的如何判断环形链表的算法。那个算法被叫做两个指针跑步法,脱胎于小学时学的一道数学题;“在一个环形跑道,小明以每秒1米的速度匀速跑,小华以每秒2米的速度匀速跑,多久之后小明落后小华一圈”。这个算法也是这样的,一个慢指针每次往后移动一个节点,一个快指针一次移动两个节点,下一次两个指针能相遇,那么它就是一个环形列表。根据这个算法我们也可以提供一个lisp版本的判断环形列表的代码(defun circle-list-p (list) (and (consp list) (circle-list-p-1 (cdr list) (cdr (cdr list))))) (defun circle-list-p-1 (slow fast) (if (or (null slow) (null fast)) nil (if (not (consp slow)) nil (if (eq (car fast) (car slow)) t (circle-list-p-1 (cdr slow) (cdr (cdr fast))))))) (circle-list-p '(1 2 3 4)) ;; ==> nil (circle-list-p '#1=(1 2 . #1#)) ;; ==> t获取序列的第n个元素可以使用 elt,但是对于已知数据类型最好使用对应的函数,例如针对列表应该使用 nth,数组使用 aref。一来该对象是何种数据类型更加直观,二来省去了 elt 内部类型判断的操作。copy-sequence 在前面已经提到了。不过同样 copy-sequence 不能用于点列表和环形列表。对于点列表可以用 copy-tree 函数。环形列表就没有办法复制了。 好在这样的数据结构很少用到。数组操作创建向量可以使用 vector 函数,或者使用[], 来包裹一组数据,后者是向量的读入语法(vector 1 2 3) ;; ==> [1 2 3] (setq foo '(a b)) [foo] ;; ==> [foo] (vector 'foo) ;; ==> [foo] (vector foo 1 2 3) ;; ==> [(a b) 1 2 3]上述代码中我们使用两个方式分别构造一个向量。采用vector的时候会对其中的每个符号进行求值。而使用[]来构造时则没有进行求值,等效于使用 quote使用 make-vector 可以生成元素相同的向量(make-vector 9 "foo") ;; ==> ["foo" "foo" "foo" "foo" "foo" "foo" "foo" "foo" "foo"]fillarray 可以将数组的每个元素使用对应值进行填充(fillarray [1 2 3 4] 'foo) ;; ==> [foo foo foo foo]aref 和 aset 可以访问和设置数组中对应索引的元素。但是需要注意数组的长度,如果传入索引超过数组长度则会报错。可以使用 vconcat 可以将多个序列合并成一个向量,这里可以传入非向量,例如传入列表。针对列表仅限真列表。(vconcat [1 2 3] [3 4 5]) ;; ==> [1 2 3 3 4 5] (vconcat [1 2 3] '(3 4 5)) ;; ==> [1 2 3 3 4 5] (vconcat [1 2 3] '(4 . 5)) ;; ==> error将向量转化成列表可以使用 append函数(append [a b]) ;; ==> [a b] (append [a b] nil) ;; ==> (a b) (append [a b] '(c)) ;; ==> (a b c) (append [a b] "cd") ;; ==> (a b . "cd") (append [a b] "cd" nil) ;; ==> (a b "cd")在列表那一章中,append是将列表的最后一个节点的cdr替换为对应参数。在这里它可以将序列的元素转化为列表,但是需要注意,转换时同样需要两个参数。它会将第一个参数转化为列表,然后执行列表中添加元素的操作
2025年01月15日
19 阅读
0 评论
0 点赞
2025-01-13
Emacs 折腾日记(八)——CONS CELL和列表
本篇我们来介绍emacs lisp中的第一种复核结构——列表类型。cons cell从概念上讲 cons cell 非常简单,就是两个有顺序的元素。第一个元素叫 CAR、第二个元素叫 CDR。CAR 和 CDR 名字来自于 Lisp。根据 emacs lisp 简明教程 上的说法:它最初在IBM 704机器上的实现。在这种机器有一种取址模式,使人可以访问一个存储地址中的“地址(address)”部分和“减量(decrement)”部分。CAR 指令用于取出地址部分,表示(Contents of Address part of Register),CDR 指令用于取出地址的减量部分(Contents of the Decrement part of Register)。cons cell 也就是 construction of cells。至于历史出处我们并不需要特别关心,也不用掌握,我们只需要掌握相关用法即可。其实我们可以将它想象成一个有两个抽屉的柜子,有一个抽屉叫 car 另一个叫cdr 。具体里面放什么东西没有限制,可以放基本的数据类型,也可以同样的放入这么一个柜子。首先使用 cons 来构建一个cons cell。例如(setq my-cons (cons 1 "hello")) ;; ⇒ (1 . "hello") (setq my-cons (cons 1 nil)) ;; ⇒ (1)因为一个cons cell 包含 car 和 cdr 两个元素,所以一般我们传入的时候需要两个参数。但是第二个参数可以为nil。根据emacs在mini-buffer上的输出,其实还可以使用另一种方式来构建一个cons cell(setq my-cons '(1 . "hello"))我们看到在前面的代码中我们带了一个单引号,这个单引号用于表示符号(symbol)或字面量(literal)。具体来说,它的作用是防止后面的表达式被求值。Lisp的语句是一个S-表达式,在解释器读到到一个S-表达式的时候会尝试对这个S-表达式求值。在出现括号的表达式的时候,会将括号内第一个元素作为函数进行调用,而将其他元素作为参数。如果不加引号,那么上面的代码就变成了(setq my-cons (1 . "hello"))这个表达式的含义就是调用1 这个函数,传入 "hello" 参数,并将函数的返回值设置成变量 my-cons 的值。因为没有这么一个函数,所以它执行会报错。这里的引号就是 quote 函数,它用来表示对后面的内容不求值,仅仅作为一个符号传入。上面的代码也可以改成(setq my-cons (quote (1 . "hello")))再举一个例子(setq my-cons '(a . b));; error上述的代码会报错,虽然我们指定了 (a . b) 是一个符号,是一个cons cell,但是对于里面的 a 和 b 却没有指定,因此解释器会尝试解释 a和b,然后发现a和b未定义,所以也会报错,我们可以使用单引号单独的指定a、b都是符号,或者给a、b变量设定值。虽然都不报错,但是它们的含义却是不同的。(setq my-cons ('a . 'b)) (let ((a 1) (b 2)) (setq my-cons '(a . b)))cons cell还有一个特殊的值,那就是 nil 它表示一个空的 cons cell。它可以使用如下形式来给出nil '()空表并不是一个真正的 cons cell , 但是为了编程方便,还是可以通过 car 和 cdr 来取值,结果都是空。(car '()) ;; ⇒ nil (cdr '()) ;; ⇒ nil (car nil) ;; ⇒ nil列表lisp的全程是 List Processing ,列表处理,从这点上看列表在lisp中的比重非常重,非常重要。列表可以看作一个特殊形式的cons cell。在上面的介绍中,cons cell有两个元素,car和cdr,列表第一个元素是car,其余的是cdr。以此规律往下递归。我们可以使用 list 函数来构建(list 1 2 3) ;; ⇒ (1 2 3)也可以使用上面的 quote 来构造'(1 2 3) ;; ⇒ (1 2 3)二者定义的时候有什么区别呢?quote 方式是直接将内容作为一个列表,而list 函数则是先解释执行后面的代码,再将结果构建成列表,下面是二者不同的一个例子(list (+ 1 2 ) 3) ;; ⇒ (3 3) '((+ 1 2) 3) ;; ⇒ '((+ 1 2) 3)再来看一个例子'(a b c) ;; ⇒ (a b c) (list a b c) ;; ⇒ error, 因为a b c都未定义,无法解释执行如果要使用 list 来生成类似于 (a b c) 这样的列表,关键点在于要告诉解释器a b c 它们不需要解释执行,可以使用 quote 来做到这点(list 'a 'b 'c) ;; ⇒ (a b c)测试函数可以使用 consp 来判断一个对象是否是cons cell。使用 listp 来判断对象是否是列表,但是我们说列表是特殊的 cons cell 所以使用 consp 来检测列表,也会返回真(consp '(1 2 3)) ⇒ t除此之外,elisp 将cons cell也视为一种特殊的列表,因此下面的代码也返回t(listp (cons 1 2)) ;; ⇒ t但是nil 或者 '() 它们不是cons cell 也不是 list,所以判断它们都会返回 nil(consp nil) (consp '())深入理解 cons cell 和列表上面提到我们可以使用 cons 和 list 来分别构造一个 cons cell 和列表,但是它们构造一个新的,不影响之前的,例如(setq my-cons (cons 1 2)) (cons my-cons my-cons) my-cons ;; ⇒ (1 . 2)同时 cons 也可以在列表前增加一个元素,例如(setq foo '(a b)) (cons 'x foo) ;; ⇒ (x a b) foo ;; ⇒ (a b)从上面返回的结果来看,cons 会创建一个新的列表,并且在新列表的最前面加上指定元素,但是它不会修改原有的列表。cons 会返回新元素,不修改老元素还可以理解,因为它本来就是用来构建新的 cons cell 的。那么还有一个问题需要解释,为什么这样一个用来构建cons cell的函数会用来添加列表元素呢?要回答这个问题,我们可以需要回归到列表的本质了。先看这么一个例子'(1 . (2 . (3 . nil))) ;; ⇒ (1 2 3)我们执行它,发现它会返回一个列表,从这个例子上看,列表本身就是一个cons cell。它是一个特殊的cons cell 。按照列表最后一个 cdr 来区分的话,可以分成三类:第一类就是上述例子这样的,它的最后一个cdr是nil,它也被叫做真列表第二类,既不是cons cell也不是nil,这种被称之为点列表第三类,最后一个cdr 指向之前一个cons cell'(1 . #1=(2 3 . #1#)) ; => (1 2 3 . #1)这个是教程中给出的环形列表的表示形式,它比较复杂。但是它的结构与当初学过的数据结构中的环形链表类似。'(1 . (2 . (3 . 4))) ;; ⇒ (1 2 3 . 4)上述代码是第二类列表的形式,它的最后一个cdr 是 4,既不是cons cell 也不是nil。上述的代码中也可以看出来,并不是说有 . 的都是 cons cell,没有. 的就是列表。还是以前面的抽屉来类比,第二个抽屉里放的是nil或者其他基本数据类型,那么它就是一个 cons cell。如果放的是另外一个同样类型的抽屉,那么它就是一个列表。用数据结构中的概念来类比的话,cons cell是一个不带指针的结构体,而列表就是一个带有指向自身结构体类型的指针域。即使它只有一个这种结构的对象也是一个列表的节点。(cons 1 nil) ;; ⇒ (1)上述代码就是这样的,第二节点域指向空,没有指向下一个节点,虽然只有一个节点,但它也是一个列表。我个人的理解是,不应该严格区分cons cell 和列表,就像C/C++中的struct 和list,struct是组成list的基础,而list中每个节点又都是一个struct, 所以前面使用 consp 和 listp 无法区分cons cell 和 lisp。而. 则可以看作是分隔符,分隔数据域和指针域的数据,指针域同样可以放入其他类型的数据,也可以放入 cons cell列表的操作函数添加列表元素如果希望修改原始列表可以使用 push ,与栈操作类似,它是将当前值添加到列表头,例如(setq foo '(a b)) (push 'x foo) foo ;; ⇒ (x a b)在列表前面添加元素使用 cons ,在列表后面添加元素可以使用 append。(setq foo '(a b)) (append foo '(x)) ;; ⇒ (a b x) foo ;; ⇒ (a b)(setq foo '(a b)) (append foo 'x) ;; ⇒ (a b . x) foo ;; ⇒ (a b)(setq foo '(a . b)) (append foo 'x) ;; error foo与cons 类似,它同样不修改原始列表的值。 用上面C/C++结构体和链表的类比话术来说的话,它的作用是将第一个参数的最后一个节点的指针域的空指针替换成第二个参数。上面的第一个例子,原本列表应该是 (a . (b . nil)) 它的最后一节点的指针域就是 nil,它被替换成了 (x), 可以写成 (x . nil) 。最后的结果就是 (a . (b . (x . nil))) 它是一个真列表,(a b x) 。第二个例子,还是先将列表展开 (a . (b . nil)) ,将nil替换成 x ,最后的结果就是 (a . (b . x)) 第三个例子,使用cdr 取出来的最后一个例子并不是空,所以它会报错与C中链表类似,采用头插法的速度要比使用尾插法快得多。即使用 cons 速度要比使用 append 快获取列表元素列表就是一个个cons cell 串起来组成的,可以使用 car 和 cdr 来获取元素,我们可以自己尝试仿照着C中对链表的操作来写一个函数获取列表中任意位置的元素(defun my-get-list-item(lst index) (let ((i 0)) (while (and (cdr lst) (< i index)) (setq lst (cdr lst)) (setq i (+ i 1))) (if (<= index i) (car lst) nil))) (my-get-list-item '(0 1 2 3 4 5) 2) ;; ⇒ 2当然也可以使用递归来完成(defun my-get-list-item(lst index) (if (or (not lst) (= 0 index)) (car lst) (my-get-list-item (cdr lst) (1- index)))) (my-get-list-item '(0 1 2 3 4 5) 2)递归版本相对于上面的循环来说要简单的多,代码量也少。递归版本中当列表为空或者当前索引为0时,停止递归并返回。利用空列表表的car 和 cdr 都是空这个特性,来将两种不同的情况使用同一操作进行处理。条件不满足时对cdr进行递归处理。虽然可以自己写这样的算法来取列表的第n个元素,但是elisp中也提供的对应的操作函数。使用 nth 来获取第n个元素,使用 nthcdr 来获取第n次调用cdr 的结果,也就是获取包含第n个元素的子列表(nth 2 '(0 1 2 3 4 5)) ;; ⇒ 2 (nthcdr 2 '(0 1 2 3 4 5)) ;; ⇒ (2 3 4 5)同时还提供了 last 来返回从右往左数第n个元素的子列表。和 butlast 来返回last之外的其它列表元素。(last '(0 1 2 3 4 5) 3) ;; ⇒ (3 4 5) (butlast '(0 1 2 3 4 5) 3) ;; ⇒ (0 1 2)利用这些函数可以实现取某一范围的子列表(defun my-get-sub-items (lst start end) (if (nthcdr start lst) (butlast (nthcdr start lst) (- (length lst) end)))) (my-get-sub-items '(0 1 2 3 4 5) 2 5) ;; ⇒ (2 3 4)上面的代码比较简单,首先使用 nthcdr 来取start后面的内容,然后使用 butlast 来去掉 end 后面的内容。不知道各位读者还记不记得 length 这个函数,前面我们用它来获取字符串的长度,这里我们用它来获取列表的长度。设置列表元素一般情况下,我们可以放心的递归和对列表进行操作,因为上述的一些函数都不会修改原列表的值,在递归或者循环的过程中我们使用的是产生的临时列表。但是有时候会希望修改列表的值,例如在将列表作为栈来使用的时候,就需要出栈和压栈的操作。设置元素的值,可以使用 setcar 和 setcdr 这两个函数。如果我想设置任意索引位置的值该怎么办呢?可以配合使用 nthcdr 和 setcar。(setq foo '(a b c)) (setcar foo 'x) foo ;; ⇒ (x b c) (setq foo '(a b c)) (setcdr foo '(x y)) foo ;; ⇒ (a x y) (setq foo '(a b c)) (setcdr foo 'x) foo ;; ⇒ (a . x) (setq foo '(a b c)) (setcar (nthcdr 1 foo) 'x) ;; ⇒ x foo ;; ⇒ (a x c)前面提到使用 push 在表头添加元素,这里再介绍一个 pop 函数,它用来删除表头元素,它们两个配合使用就能组成一个栈的数据结构(setq foo '(a b c)) (push 'x foo) ;; ⇒ foo (pop foo)列表排序将列表从尾到头进行反转可以使用 reverse ,例如(setq foo '(a b c)) (reverse foo) ;; ⇒ (c b a) foo ;; ⇒ (a b c)我们可以看到,reverse也是不修改原始的列表,而是返回一个新的列表。如果想要修改原始列表可以使用 nreverse(setq foo '(a b c)) (reverse foo) ;; ⇒ (c b a) foo ;; ⇒ (a)为什么这里foo 指向了列表的最后一个元素呢?使用当初学习C/C++链表操作时掌握的知识很好解释,原本foo指向的是列表头,但是反转之后,原来的链表头就变成最后一个元素,而没有修改foo指针指向的情况下,它就是指向链表的最后一个元素(这个原因是我猜的,不知道对不对)。我们还可以对列表进行排序,可以使用sort 函数进行排序,它接收一个列表,并且接收一个排序方式的函数。例如(setq foo '(3 4 5 1 2 0)) (sort foo '<) ;; ⇒ (0 1 2 3 4 5) foo ;; ⇒ (0 1 2 3 4 5)这里的 '< 是一个排序函数,有点像C++ 11 标准里面的 sort 函数,它可以传入一个函数用来表示排序时比较大小的一个过程。而且这里我们并不需要在这个时候调用 < 这个函数,所以先使用 quote 。在后续真正执行排序要比较大小的时候会调用它。这里我们可以自己定义比较函数,比如这里我们按照字符串长度进行排序(defun strlen-cmp (str1 str2) (< (length str1) (length str2))) (setq foo '("hello" "emacs" "aaa" "bbbbbb")) (sort foo 'strlen-cmp) ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb") foo ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb") ;; 这里也可以使用lambda表达式 (setq foo '("hello" "emacs" "aaa" "bbbbbb")) (sort foo (lambda (str1 str2) (< (length str1) (length str2)))) ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb") foo ;; ⇒ ("aaa" "hello" "emacs" "bbbbbb")这里我们发现sort 已经将修改了原始列表,如果想要保留原始列表,可以使用 copy-sequence(setq foo '(3 4 5 1 2 0)) (let ((temp (copy-sequence foo))) (sort temp '<)) ;; ⇒ (0 1 2 3 4 5) foo ;; ⇒ (3 4 5 1 2 0)还有像 nconc 和 append 功能相似,但是它会修改除最后一个参数以外的所有的参数,nbutlast 和 butlast 功能相似,也会修改参数。这些函数都是在效率优先时才使用。总而言之,以 n 开头的函数都要慎用遍历列表前面我们已经使用 car 和 cdr 能做到遍历列表,这里再介绍一下专门用来遍历的函数 mapc 和 mapcar 。它们都可以遍历列表中的所有元素,它们的第一个参数是一个函数,每次遍历到一个元素的时候会调用这个函数并将元素作为参数传入这个函数。C++中没有提供这样的函数,但是也有类似的操作。例如使用 foreach 获取每个元素,然后根据元素来执行操作。(setq foo '(0 1 2 3 4)) (mapc '1+ foo) ;; ⇒ (0 1 2 3 4) foo ;; ⇒ (0 1 2 3 4) (setq foo '(0 1 2 3 4)) (mapcar '1+ foo) ;; ⇒ (1 2 3 4 5) foo ;; ⇒ (0 1 2 3 4)这两个遍历函数的区别就是,是否使用返回值来构建新的列表,其中 mapcar 会根据返回值构建新的列表,而 mapc 则返回原列表。我们发现无论是哪个函数都无法修改原始列表,要修改原始列表当然也有方法,我能想到的一个方法就是循环,然后配合 setcar 和 ntdcdr 根据索引来设置。好了,本篇的内容就到此为止了。本篇按照 emacs lisp 简明教程 的内容修改而来的。原教程还有好多其他数据结构的操作,但是我作为初学者还是希望本篇内容专注在列表上,至于教程中涉及的其他操作或者数据结构,等后面学到了再了解也不迟。
2025年01月13日
25 阅读
0 评论
0 点赞
2024-12-17
Emacs折腾日记(四)——elisp控制结构
目前我们接着学习elisp相关语法,这里我是按照 elisp 简明教程 来进行学习。与其说这是我自己写得教程到不如说是在这个上面做得注释。目前我不知道这样是否侵犯相关的知识产权。目前就先这样继续学习,继续写记录吧。闲话少说,进入本篇的正题,关于elisp的控制结构。一般编程语言都有三种控制结构:顺序结构、条件结构、循环结构。elisp同样有这三种控制结构。顺序结构和复合语句一般默认elisp的语句是顺序执行的,例如下面的代码(setq name "Emacs") (message "hello, %s" name)它先执行前面的 setq 语句,先给变量name 定义并赋值为 Emacs 。后面接着执行第二行代码,调用message 函数来输出一段文字。在其他语言一般都有一个复合语句。它是有多个语句共同组成的,例如 C/C++中使用{} 来将多个语句整合成一条复合语句。针对C/C++ 我们在很多地方会用到复合语句。例如如果 if , while 等语句后只需要一条语句,那么可以直接使用一条语句,例如下面的代码// 这么写代码不太正规但是符合语法规范,也能编译过 int main() { int i = 0; while(i++ < 10) printf("%d\n", i); //打印1到10,这么10个数字 return(0); }但是如果在循环或者if条件成立后,执行多条语句,就需要使用复合语句,也就是用大括号括起来。那么在elisp中也有这样的操作,在条件和循环语句中需要执行不止一条语句,也需要使用复合语句。elisp 中符合语句使用 progn 来包含一组语句组成复合语句,它的语法规则是(progn statement1 statement2 ... statement3)例如我们将上面的代码用 progn 包装一下(progn (setq name "Emacs") (message "hello, %s" name)) ;; => "hello, Emacs"使用 progn 包装的复核语句可以使用 C-x C-e 也就是 eval-last-sexp 来同时执行里面的两个子语句。如果我们将它们分开写,则使用 eval-last-sexp 做不到这点,它只能一条条的执行# 条件语句 我们使用 if 和 cond来表示条件分支,if的语法如下(if condition then else)需要注意的是 这里的 then 和 else 并不是关键字,而是对应的语句,也就说紧跟着if条件的语句表示条件成立时执行的代码,下一条则是条件不成立时执行的代码。例如我们使用下面的代码来获取两个数的最大值(defun get-max(a b) (if (> a b) a b)) (get-max 3 4) ; => 4与 C/C++ 的函数不同,elisp 函数的返回值不需要使用 return 或者其他的关键字特意指出,它是将函数最后执行的语句的返回值作为函数的返回值,这里当 a > b 时条件成立,执行 a 然后结束函数,也就是这个时候函数的最后一个语句是 a ,函数返回 a 的值。否则执行 b ,此时函数的最后执行的语句就是 b ,这个时候函数就返回 b 的值而 cond 有点像 C/C++ 中的 switch ,它的语法如下(cond (case1 do-when-case1) (case2 do-when-case2) ... (t do-when-none-meet))它的语法特点是,它与 switch 类似,由一堆 case 和 default 组成。每个case 都使用一对 () 来区分,最后可以使用 t 来表示未匹配到前面的 case 时执行的语句,类似于default语句。这里我们使用当初学习C/C++ switch 语法时的经典代码来作为示例(defun score-report (score) (cond ((>= score 90) "优秀") ((>= score 80) "良好") ((>= score 60) "及格") (t "不及格"))) (score-report 75); => 及格我们可以看到,cond 语句的使用比 switch 更为的灵活,switch case 只能进行整型变量的相等比较,而 cond 可以进行其他变量类型的不同形式的条件判断,它只是在形式上更像 switch,但是在使用的范围上更像 if-else if-else。另外 elisp 简明教程中 提供了一个使用 cond 计算 斐波那契数列的例子(defun fib(n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ; => 55因为 elisp 中使用 setq 来进行赋值操作,所以它里面的= 就是数学意义上比较相等的操作符,而 其他语言中的 == 在lisp中无效。这里如果写成 == 将会报错。上面的例子也很好理解 当 n 等于 0时返回0,等于 1 时返回1,否则返回 fib(n - 1) + fib(n - 2) 使用 C/C++ 的话可能更容易理解int fib(int n) { if(i == 0) return 0; else if (i == 1) return 1; else return fib(n - 1) + fib (n - 2) }循环结构循环使用 while 关键字,它的语法结构如下(while condition body)我们可以将上述循环打印的C代码使用 elisp 实现(setq i 0) (while (< i 10) (progn (message "%d" i) (setq i (+ i 1))))我们执行完代码之后使用 switch-buffer,切换到 *message* ,可以看到它打印了从0到9的数据。上面的斐波那契数列的例子我们可以使用 while 来实现(defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (let ((first 1) (second 1) (third 1)) (setq n (- n 2)) (while (> n 0) (progn (setq third (+ first second)) (setq first second) (setq second third) (setq n (- n 1)))) third)))) (fib 10) ; => 55因为 elisp 中没有提供 += ++ 这样算术运算符,所以我们需要使用 setq 来赋值。下面还有一个计算阶乘的例子(defun factorial (n) (let ((res 1)) (while (> n 1) (setq res (* res n)) (setq n (- n 1))) res)) (factorial 10) ; => 3628800我们也可以提供一个递归的版本(defun factorial (n) (if (= n 1) 1 (* (factorial (- n 1)) n))) (factorial 10) ; => 3628800到此为止,本篇就结束了。本篇涉及到的elisp 代码其实也不算复杂,如果能熟练掌握一门编程语言的话,到此为止的代码应该不算太难理解。在编写这些示例代码的时候我觉得还好,主要注意括号的匹配,算法什么的就是照搬C/C++中一些经典写法就差不多了。但是即使上面的代码并不多,代码量并不大,我也能明显感觉到上述代码在阅读上不那么直观。
2024年12月17日
26 阅读
0 评论
0 点赞
2021-05-31
动态内存与智能指针
c/c++语言的一大特色是在于可以动态的进行内存管理,而这也是它的难点所在。程序出现问题,原因经常在动态内存管理这块,比如分配内存后没有及时释放,或者当前线程提前释放了其他线程也会使用的内存。而c++11中新增的智能指针能在一定程度上解决这些问题动态内存与智能指针在c++中动态内存的管理是通过一对运算符来完成的: new和delete ,new为对象分配空间并返回一个指向该对象的指针。delete 接受一个动态对象的指针,销毁对象并释放相关内存动态内存的管理十分困难,有时候会忘记释放内存,这种情况下会产生内存泄漏。有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。为了更容易也更安全的使用动态内存,新的标准提供了两种智能指针类型来管理动态对象。shared_ptr 类类似于vector 智能指针也是模板。创建智能指针时,必须提供额外的信息——指针可以指向的类型。智能指针的用法与普通指针类似。解引用一个智能指针返回它指向的对象,箭头运算符可以返回对象中的成员shared_ptr<string> p = new string; if(nullptr != p && p->empty()) { *p = "hello world"; //字符串为空的时候,将一个新值赋予string }最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回此对象的 shared_ptr。shared_ptr<int> p3 = make_shared<int>(42); //初始化p3 指向一个值为42的int类型 shared_ptr<string> p4 = make_shared<string>(10, '9'); //p4指向一个值为 "9999999999" 的string shared_ptr<int> p5 = make_shared<int>(); //指向一个值初始化的int,即,值为0当shared_ptr 进行拷贝和赋值操作时,每个shared_ptr 都会记录有多少个其他的shared_ptr 指向相同的对象auto p = make_shared<int>(42); //此时p指向的对象只有一个引用值 auto q = p; //此时p指向的对象有两个引用者我们可以认为每一个shared_ptr 都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr 赋一个新值或者shared_ptr 被销毁时,他所关联的计数器就会递减。当指向一个对象的最后一个shared_ptr 被销毁时,shared_ptr 类就会自动销毁此对象。shared_ptr 并不是万能的,也会出现内存泄漏的问题。这种情况一般出现在容器中。如果在容器中存放了shared_ptr ,然后重新排列了容器,从而不需要某些元素。在这种情况下应该确保使用earse删除某些不再需要的shared_ptr 元素直接管理内存相对与智能指针直接使用new 和 delete很容器出错。当内存耗尽时,new会失败,会抛出一个bad_alloc 异常。我们可以改变使用new的方式来阻止它抛出异常int *p1 = new int; //如果分配失败则会抛出异常 int *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针我们称这种形式的new为定位new。定位new允许我们传递额外的参数给到new,在此例子中我们传递一个标准库中的nothrow 对象,告知它在内存不足的时候不要抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针动态对象的生命周期一直到它被手动释放为止。这样就给使用者造成了一个额外的负担,调用者必须记得释放内存。使用new和delete 管理动态内存存在三个常见问题:忘记delete内存。造成内存泄漏问题使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检出这种错误同一块内存多次释放坚持只使用智能指针就可以避免所有这些问题。对于一块内存只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它shared_ptr 和 new 结合使用接受指针参数的智能指针构造函数是 explicit 的。因此,我们不能将一个内置指针隐式转化为智能指针,必须使用直接初始化的方式shared_ptr<int> p1 = new int(1024); //错误,这里需要将int* 隐式转化为shared_ptr<int> 类型 shared_ptr<int> p2(new int(1024)); //正确默认情况下一个用来初始化智能指针的普通指针必须指向使用new创建的动态内存(malloc 创建的需要自定义释放操作),因为智能指针默认采用delete来释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上面,但是为了这样做,必须提供自己的操作来代替delete不要混合使用普通指针和智能指针。void process(shared_ptr<int> ptr) { // 进入到函数中时,ptr 所在的引用计数加1 } //函数结束时, ptr 所在对象的引用计数减 1 shared_ptr<int> p(new int(42)); //引用计数为1 process(p); //在函数内部,引用计数加1,变为2 //执行完成后,引用计数减1,变为1,此时对象不会被销毁 *p = 100; //可以进行赋值,此时对象还未被销毁 int* p1 = new int(42); process(shared_ptr<int>(p1)); //进入函数后,由于p1 自身不是智能指针,所以在函数结束之后,智能指针的计数为0,会销毁对应的对象 *p1 = 100; //错误,此时对象已被销毁 智能指针定义了一个get函数用来返回一个普通的指针,此函数是为了这样一种情况而设计的:我们需要像不能使用智能指针的代码传递一个内置指针,但这段代码中不能使用delete来销毁这个指针所指向的对象我们不能将get返回的指针再绑定到另一个智能指针上。智能指针和异常当发生异常时,普通的指针如果在异常发生之后进行delete操作,那么资源回收操作可能会被中断,而智能指针不会void f() { shared_ptr<int> sp(new int(24)); //即使后面发生异常,sp指针在函数结束时计数变为0,对象被释放 } void f() { int* p = new int(24); //这里发生异常的话,后面的delete 不会被执行,可能发生内存泄漏 delete p; }有些资源由于提供的是c函数级别的接口,因此需要手动进行释放,就会存在与动态内存一样的问题,忘记释放资源。这种情况我们也可以使用智能指针的技术来自动管理资源。例如我们的socket程序,在最后需要调用shutdown 和 closesocket来关闭void clear_socket(socket* sk) { shutdown(*sk); closesocket(*sk); } socket s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); shared_ptr<socket> ps(&s, clear_socket); //链接服务器 //程序推出后会自动调用clear_socket 来释放socket资源智能指针可以提供对动态分配的内存安全而有方便的管理,但是这建立在正确使用的前提下。为了方便的使用智能指针,我们必须坚持一些基本原则:不使用相同的内置指针初始化多个智能指针不delete get函数返回的指针不使用get初始化或者reset另一个指针指针如果使用get返回的指针,记住当最后一个对应的智能指针被销毁后,你的指针就变为无效了如果使用智能指针管理的资源不是new分配的,记住传递给它一个删除器unique_ptrunique_ptr 拥有它所指向的对象。某一个时刻只能有一个 unique_ptr 指向一个给定的对象。当unique_ptr 被销毁时,它所指向的对象也会被销毁unique_ptr 不支持拷贝操作,没有类似 make_shared 的操作。unique_ptr<string> p1(new string("hello world")); unique_ptr<string> p2(p1); //错误:unique_ptr 不支持拷贝 unique_ptr<string> p3; p3 = p1; //错误:unique_ptr 不支持赋值虽然不能拷贝和赋值unique_ptr ,但是可以调用release或者reset将指针的所有权从一个(非const)unique_ptr 转移给另一个unique_ptrreset 成员接受一个可选的指针参数,令unique_ptr 重新指向给定的指针。如果unique_ptr 不为空,它原来指向的对象被释放。release会切断unique_ptr 和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或者给另一个智能指针赋值,如果我们不用另一个智能指针保存release返回的指针,则需要手工释放指针指向的资源p2.release(); //错误,p2指向的资源不会正常释放 auto p = p2.release(); delete p;不能拷贝unique_ptr 的规则又一个例外: 我们可以拷贝或者赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptrunique_ptr<int> clone(int p){ return unique_ptr<int>(new int(p)); }还可以返回一个局部对象的拷贝:unique_ptr<int> clone(int p) { unique_ptr<int> ret(unique_ptr<int>(p)); return ret; }类似于shared_ptr, unique_ptr 默认情况下使用delete 释放它指向的对象。我们也可以重载一个unique_ptr 中默认的删除器。但是unique_ptr 管理删除器的方式与shared_ptr 不同。重载一个unique_ptr 中删除器会影响到unique_ptr 类型以及如何构造该类型的对象。与与重载关联容器的比较运算相似,我们必须在尖括号中unique_ptr 指向类型之后提供删除容器类型。在创建或者reset 一个这种unique_ptr 类型的对象时,必须提供一个指定类型的可调用对象weak_ptrweak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。将一个shared_ptr绑定到一个 weak_ptr。不会改变shared_ptr 的引用计数。一旦最后一个指向对象的shared_ptr 被销毁,对象就会被释放。即使有weak_ptr 指向对象,对象还是会被销毁由于对象可能不存在,所以不能直接使用weak_ptr 来访问对象,需要先调用lock函数,此函数检查weak_ptr 指向的对象是否仍然存在。如果存在,lock返回一个指向共享对象的shared_ptr。只要此shared_ptr 存在,它所指向的对象也会一直存在.if(shared_ptr<int> np = wp.lock()) { //在if中np 和wp 共享对象 }动态数组new 和数组标准库提供了一个可以管理new 分配的数组的unique_ptr 版本,为了用一个unique_ptr 管理动态数组,我们必须在对象类型后面跟一对方括号:unique_ptr<int[]> up(new int[10]); up.release(); //自动用delete[] 销毁其指针shared_ptr 不直接支持管理动态数组。如果希望使用shared_ptr 管理动态数组,必须提供自己定义的删除器:shared_ptr<int> sp(new int[10], [](int* p){delete[] p;}); sp.reset();shared_ptr 未定义下标运算符,因此我们通过shared_ptr 访问动态数组时需要使用get获取到内置指针,然后用它来访问数组元素** allocator 类当分配一块大内存时,我们通常计划在这块内存上按需求构造对象,这种情况下使用new分配时会立即执行对象的构造操作,会造成一定的开销string *const p = new string[n]; //构造n个空白的string delete[] p;上述代码在new 的同时已经调用了n次string 的构造函数。但是我们可能不需要n个string对象,少量的即可满足。 这样我们就可能创建一些永远也用不到的对象。而且对于那些要使用的对象,我们也在初始化之后立即赋予了它们新值,每个被使用的元素被赋值了两次,第一次是在默认初始化的时候,第二次是在赋值时。标准库中定义了allocator 类可以帮助我们将内存分配和对象构造分离开来。allocator<string> alloc;//可以分配string的allocator对象 auto const p = alloc.allocate(n); //分配n个未初始化的stringallocator 分配的内存是未构造的。我们按照需要在此内存中构造对象。成员函数construct接受一个指向将要被构造的内存的指针,同时可以接受额外参数作为构造对象时的参数。auto q = p; //q 指向下一个将要被构造的位置 alloc.construct(q++); //构造一个空字符串 alloc.construct(q++, 10, 'c'); //构造一个'cccccccccc'的字符串 alloc.construct(q++, "hi"); //构造一个 "hi" 字符串当我们使用完对象后必须调用destroy 来销毁它们while(q != p) { alloc.destroy(--q); }这里要注意我们只能对真正构造了的元素进行destroy操作destroy之后这些内存并没有完全交换给系统,还需要调用deallocate 来完成alloc.deallocate();
2021年05月31日
22 阅读
0 评论
0 点赞
2021-05-16
关联容器
之前介绍过标准库中的顺序容器,顺序容器是元素在内存中按照一定顺序进行排列的,都是按线性结构进行排列。除了顺序容器外,c++中还有关联容器。与顺序容器不同的是,关联容器中元素是按照关键字来保存和访问的。与之相对的顺序容器是按它们在容器中的位置来顺序的保存和访问的。关联容器支持高效的查找和访问。两个主要的关联容器类型是map和set。标准库提供8种关联容器,这8个容器见的不同体现在3个维度或者是一个set,或者是一个map或者要求不重复的关键字,或者允许重复关键字按顺序保存元素或者无序保存允许重复关键字的容器都包含单词 multi ,不保持关键字按顺序存储的容器的名字都以单词unordered 开头这8中容器分别是 map、set、multimap、multiset、unordered_map、unordered_set、unordered_multimap、unordered_multiset关联容器概述关联容器不支持顺序容器中的位置相关操作,例如 push_back、push_front。原因是关联容器是按照关键字存储的,这些操作对关联容器没有意义对于map、multimap、set、multiset 关键字类型必须定义元素的比较方法。默认情况下标准库使用关键字类型的< 运算符来比较两个关键字在介绍关联容器的操作之前,我们需要了解名为pair的标准库类型。一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,需要提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样pair<string, string> anon; pair<string, size_t> word_count; pair<string, vector<int>> line;初始化时,会调用类型的默认构造函数进行初始化,也可以为每个成员提供初始化器:pair<string, string> author{"James", "Joyce"};pair 的数据成员是public的,两个成员分别命名为first和second。我们用普通的成员访问符号来访问它们。关联容器的操作关联容器定义了额外的类型别名key_type: 此容器类型的关键字类型mapped_type: 每个关键字关联的类型:只适用与mapvalue_type: 对于set,与key_value 相同;对于map,为 pair<const key_type, mapped_type>set<string>::value_type v1; //v1 是一个string set<string>::key_value v2; //v2 是一个string map<string, int>::value_type v3; //v3 是一个pair<const string, int> map<string, int>::key_type v4; //v4 是一个string map<string, int>::mapped_type v5; //v5 是一个int我们使用作用域运算符来提取一个类型的成员。当解引用一个关联容器的迭代器的时候,会得到一个类型为容器的value_type 的值的引用。对map而言,value_type 是一个pair 类型,其first成员保存const的关键字,second成员保存值auto map_it = word_count.begin(); cout << map_it->first; cout << " " << map_it->second; map_it->first = "new key"; //错误 first是const类型 ++map_it->second;set 的迭代器是const的。关联容器中键值是无法通过迭代器进行修改的。只能通过迭代器读,每次修改键值都会导致容器中元素的重新排序。因此不允许通过迭代修改键值我们通常不能对关联容器使用泛型算法。关键字是const这一特性意味着不能将关联容器传递给修改或者重排容器元素的算法。关联容器可以使用只读取元素的算法。但是很多这类算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行快速查找,因此对其使用泛型算法几乎总是一个坏主意关联容器中有一个find的成员,我们可以使用find算法来根据关键字查找元素。关联容器的insert成员可以向容器中添加一个元素或者元素范围。因为set和map无法包含关键字重复的元素,因此插入已存在的元素对容器没有任何影响vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8}; //ivec 有8个元素 set<int> set2; set2.insert(ivec.cbegin(), ivec.cend()); //set2 现在有4个元素 set2.insert({1, 3, 5, 7, 1, 3, 5, 7}); //set2 现在有8个元素对一个map执行insert操作时,需要记住元素类型是pair。通常在insert的参数列表中创建一个pair//向word_count 插入word的4种方法 word_count.insert({word, 1}); word_count.insert(make_pair(word, 1)); word_count.insert(pair<string, size_t)>(word, 1)); word_count.insert(map<string, size_t>::value_type(word, 1));insert 的返回值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已经存在于容器中,则insert什么也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器,且bool值为true对于允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器。这里无须返回一个bool值,因为insert总是向这类容器中加入一个新元素关联容器定义了三个版本的erase。可以通过传入一个迭代器或者一个迭代器对来删除一个元素或者一个元素范围。如果指定的元素被删除,函数返回void关联容器提供一个额外的erase函数,它允许传入一个key_type 参数,删除所有匹配给定关键字的元素,返回实际删除元素的数量。对于map和unordered_map 容器提供了下标运算,下标中填入key_type的值,得到value_type,如果关键字不在map中,会为它创建一个元素并插入map中。使用容器的find访问关联容器,传入key_type,如果能找到对应值,返回一个指向对应元素的迭代器,否则返回一个指向容器end()位置的迭代器的使用容器的count方法,传入key_type,返回容器中相同关键字元素的个数对于map使用find代替下标操作,使用下标如果未找到对应关键字则会插入一个新的元素,这样会产生未知行为。如果我们只想知道一个给定关键字是否在map中,而不想改变map,这种情况下应该使用find。对于不允许存储重复关键字的关联容器来说,通过关键字查找元素只会找到一个,而对于允许重复关键字的关联容器来说,会返回第一个元素的迭代器,而相同关键字的元素会相邻存储。在遍历所有相同关键字的元素时,可以首先使用find找到第一个元素的迭代器,然后使用count找到公有多少个元素。最后循环递增迭代器即可访问到所有相同关键字的元素string search_item("Alain de Botton"); auto enteris = authors.count(search_item); auto iter = authors.find(search_item); while(enteris) { cout << iter->second << endl; ++iter; --enteris; }除了上述方法,还可以使用 lower_bound和 upper_bound;lower_bound 指向第一个对应的元素,upper_bound 指向匹配的最后一个元素的下一个位置。如果未找到对应元素,则二者指向同一个迭代器,指向一个不影响排序的关键字插入位置for(auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg) { cout << beg->second << endl; }解决此问题的最后一个方法是直接使用容器的equal_range函数。该函数返回一个pair,保存的是两个迭代器。指向的位置与 lower_bound 和 upper_bound 相同解决此问题的最后一个方法是直接使用容器的equal_range函数。该函数返回一个pair,保存的是两个迭代器。指向的位置与 lower_bound 和 upper_bound 相同for(auto pos = authors.equal_range(search_item); pos.first != pos.end; ++pos.first) { cout << pos.first->second << endl; }无序容器新标准中定义了4种无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的 == 运算符。在关键字类型的元素没有明显的序关系时,无序容器是非常有用的。对于自定义类型的关键字,无法直接在无序容器中使用,还需要提供该类型的hash操作。
2021年05月16日
25 阅读
0 评论
0 点赞
2021-05-10
泛型算法
好久没有更新博客了,最近一直想把我以前的老笔记本换成 Arch + dwm 的样式来使用。现在基本已经弄完了。后面会考虑将我的心得发出来。从0开始一点点的增加自己需要的功能确实很繁琐但是也挺有趣的。闲话就到这里,这篇文章继续记录我学习c++ 11的内容。这篇主要是泛型算法相关的内容标准容器自身提供的操作少之又少,在多数情况下可能希望对容器进行其他操作,例如排序、删除指定元素等等。标准库容器中并未针对每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法,它们实现了一组经典算法的公共接口,可以使用于不同类型的元素和多种容器类型。也就是相同一组算法可以处理多种容器类型概述之所以是泛型的,主要是这些通用算法不依赖于具体的容器类型,所有相同算法采用相同的接口迭代器的存在使得算法不依赖于具体的容器类型,但是算法依赖于元素类型的相关操作,例如我们可以简单的使用下面的代码来说明这个bool find(beg, end, val) { for(auto iter = beg; iter != end; ++iter) { if(*iter == val) { return true; } } return false }上述代码并不限定于只能使用某种类型的容器,只要容器中迭代器支持递增操作,并且元素本身支持比较运算即可。泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作,最多也就只会修改迭代器所指向的元素的值。对容器自身没有影响。算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器中移动元素。但是永远不会直接添加或者删除元素(当然插入迭代器例外)初识泛型算法除了极少数例外,标准库算法都是对一个范围内的元素进行操作。我们将此元素范围称之为输入范围,接受输入范围的算法总是使用前两个参数来表示此范围。两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。理解基本算法的方法就是了解它们是否读取元素、改变元素或者重新排列元素顺序只读算法一些算法只会读取输入范围内的元素,而从不改变元素。find就是这样一个算法。一些常见的只读算法如下:find:查找容器中出现某个元素的位置,需要容器中元素类型实现 == 操作count: 返回容器中出现某个元素的次数,同样需要容器中元素类型实现 == 操作accumulate: 计算某个迭代器范围内,所有元素的和,需要容器中元素类型实现 + 操作equal: 比较两个序列中元素值是否完全相同,它接受三个参数,前两个表示一个容器的迭代器范围。最后一个参数代表第二个容器的起始位置一般来说对于只读取而不改变元素的算法,通常最好使用cbegin和cend 获取const版本的迭代器。那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。写容器元素的算法这类算法需要确保,容器原大小不能小于我们要求算法写入的元素数目。由于算法不会执行容器操作,因此它们自己不可能改变容器大小一种保证算法有足够元素空间来容纳输出数据的方式是使用插入迭代器,插入迭代器是一种向容器中添加元素的迭代器拷贝算法接受3个迭代器,前两个表示一个源容器的范围,第三个迭代器是目的容器的起始位置的迭代器。同样的源容器的长度不能超过目的容器的长度定制操作很多算法都会比较输入序列中的元素,默认情况下,这类算法使用元素类型的< 或者 == 运算符来完成比较操作。标准库还为这些算法定义了额外的版本,允许我们提供自已定义的操作来代替默认运算符。例如sort 算法默认使用元素类型的 < 运算符,但是可以使用sort的重载版本,额外定义比较的规则向算法传递参数标准库中可以接受的比较函数一般返回一个bool值,表示是否小于或者是否相等。函数接受一个参数或者两个参数。在c++新标准中将这个函数叫做谓词,接受一个参数的函数被成为一元谓词,接受两个参数的函数叫做二元谓词。vector<string> words; //初始化 words //...... bool isShorter(const string& s1, const string& s2) { return s1.size() < s2.size(); } sort(words.cbegin(), words.cend(), isShorter);lambda 表达式在介绍lambda 表达式之前,需要先介绍可调用对象这个概念可调用对象:对于一个对象或者一个表达式,如果可以对其使用调用运算符,则称它是可调用的;例如,e是一个可调用对象,则我们可以编写代码e(args) ,其中args是一个逗号分割的一个或者多个参数的列表到目前为止,我们只接触了函数和函数指针这两类可调用对象,还有其他两种可调用对象:重载了函数调用运算符的类,以及lambda表达式。一个lambda 表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数,定义形式如下capture list -> return type {function body}capture list 捕获列表,是一个lambda 所在函数中定义的局部变量的列表。parameter list 函数的参数列表return type 是函数返回值类型function body 是函数体,需要执行的具体代码段与普通函数不同的是 lambda 必须使用尾置返回来指定返回类型我们可以忽略参数列表和返回值,但是必须包含捕获列表和函数体auto f = [] {return 42;};如果lambda 表达式中没有明确指定返回类型,函数体中包含任何单一 return 语句之外的内容,则返回voidlambda 的调用方式和普通函数的调用方式一样,都是调用运算符cout << f() << endl;lambda 表达式不能有默认参数[] (const string& str1, const string& s2) { return s1.size() < s2.size(); }; vector<string> words; stable_sort(words.begin(), words.end(), [](const string& s1, const string& s2){ return s1.size() < s2.size(); });lambda 表达式一般出现在一个函数中,使用其局部变量,但是它只能访问那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指明将会使用这些变量。捕获列表指引lambda 在其内部包含访问局部变量所需的信息[sz](const string& s){ return a.size() >= sz; }lambda 捕获和返回与参数传递类似,变量捕获的方式可以是值或者引用。void func1() { size_t v1= 42; auto f = [v1]{return v1;}; v1 = 0; auto j = f(); //j 是42, 因为在定义lambda的时候传入的是v1的拷贝,后续v1 的改变不影响捕获中v1 的值 }被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值void func2() { size_t v1 = 42; auto f = [&v1](){return v1;}; v1 = 0; auto j = f(); //j 是0,f保存v1的引用,而非拷贝 }引用捕获与返回引用有着相同的限制,必须保证调用在调用lambda表达式时,是存在的。捕获的都是函数的局部变量,如果lambda 在函数结束之后执行,捕获的引用指向的局部变量已经消失。可以在函数中返回一个lambda表达式,此时返回的lambda 中不应该包含引用捕获使用引用捕获的时候需要注意,在一次或者多次调用lambda表达式的时候应该保证引用的对象仍然有效,同时需要保证对象的值是我们所期待的。因此在使用lambda的时候尽量减少捕获变量的数量,同时尽量不使用引用捕获除了显式列出我们希望使用的所来自所在函数的变量外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或者=,表示采用引用捕获或者值捕获我们也可以混合使用隐式捕获和显式捕获,混合使用时,捕获列表的第一个元素必须是一个&或者=。当混合使用了显式捕获和隐式捕获时。显式捕获的变量必须与使用隐式捕获不同的方式。当使用值捕获的时候,默认情况下lambda表达式是不能改变其值的,如果希望改变一个被捕获的变量的值,就必须在参数列表后加上关键字 mutablevoid f3() { size_t v1 = 42; auto f = [v1] ()mutable{return ++v1;}; v1 = 0; auto j = f(); // j = 43 }一个引用捕获的变量是否可以修改依赖于此引用指向的是一个const 类型还是一个非const类型void fnc4() { size_t v1 = 42; auto f2 = [&v1]{return ++v1;}; v1 = 0; auto j = f2(); //j = 1 }// 错误,由于有除return之外的其他语句,因此编译器推断lambda 表达式返回void,但是返回了具体值 transform(v1.begin(), v1.end(), vi.begin(), [](int i){ if(i < 0) return -i; else return i; }); //正确,只有return语句,编译器可以推断出返回int类型 transform(v1.begin(), v1.end(), vi.begin(), [](int i){ return (i < 0)? -i : i; }); //正确,明确指定了返回int类型 transform(v1.begin(), v1.end(), vi.begin(), [](int i)->int{ if(i < 0) return -i; else return i; });参数绑定lambda 表达式适用于只在一两个地方使用的简单操作,如果多个地方都需要使用到相同的操作,就需要写上相同的lambda表达式。这个时候最好的办法是定义一个函数。在需要进行捕获的情况下使用函数就不是那么容易了。例如有的泛型算法只传递一个参数,但是我们在函数中需要两个参数。这种情况下就需要用到参数绑定标准库中定义了一个bind函数。可以将bind作为一个函数适配器。它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表auto newCaller = bind(callable, arg_list);其中 callable 是一个可调用对象,返回的newCaller 是一个新的可调用对象,而arg_list 中的参数可能包含形如 _n 的名字,其中n是一个整数。这些参数是“占位符”。表示 newCaller 的参数。它们占据了传递给newCaller的参数位置。数值n表示生成的可调用对象中参数的位置。_1为newCaller的第一个参数,_2 为第二个参数。以此类推auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));此时调用生成一个可调用对象,将check_size 的第二个参数绑定到sz的值,当find_if 对words中的string调用这个对象的时候,这些对象会调用check_size 将给定的string 和 sz 传递给它,因此 find_if 可以有效的对输入序列中的每一个string调用check_size 实现string与 sz的比较_n 都定义在一个名为 placeholders 的命名空间中,而这个命名空间本身定义在std命名空间中。每次在使用_n 这样的名字时,都需要声明这个命名空间。using std::placeholders::_1; using std::placeholders::_2;每个占位符都必须提供单独的using声明,这样比较麻烦。可以使用另一种不同形式的 using 语句using namespace std::placeholders;我们可以使用bind 给可调用对象中参数重新排序,例如f是一个可调用对象,它有5个参数auto g = bind(f, a, b, _2, c, _1);生成的新的可调用对象g接受两个参数,分别是 _2, _1。在调用g时相当于void g(_1, _2) { f(a, b, _2, c, _1); }当我们执行 g(x, y) 最终会执行 f(a, b, y, c, x)在执行时会将 bind 中传入的参数拷贝到原来的函数参数中,如果想向原函数传递引用,可以使用标准库中的 ref函数auto g = bind(f, ref(a), b, _2, c, _1)上述代码中,在执行g的时候会向f中拷贝a的引用。_1, _2 本身在传值的时候可以传入引用再谈迭代器除了之前介绍的迭代器,标准库还定义了几种额外的迭代器:插入迭代器:这些迭代器被绑定到一个容器上,可以用来向容器插入元素流迭代器:这些迭代器绑定到流中,可以用来遍历所有关联的IO流反向迭代器:这些迭代器向后而不是向前移动,除了 forward_list 之外的标准库容器都有迭代器移动迭代器:这些专用迭代器不是拷贝其中的元素,而是移动它们。插入迭代器插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。插入迭代器有三种类型,差异在于元素插入的位置:back_iterator: 创建一个使用push_back 的迭代器front_iterator: 创建一个使用push_front 的迭代器inserter: 创建一个使用insert 的迭代器iostream 迭代器虽然iostream并不是容器,但是标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator 读取输入流,ostream_iterator 向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以使用泛型算法从流对象读取数据以及向其写入数据。istream_iterator<int> in(cin), eof; accumulate(in, eof, 0); // 从标准输入中读取整数,并计算它们的和 ostream_iterator<int> out(cout); copy(vec.begin(), vec.end(), out); //将vector中的数据拷贝到ostream流中,也就是输出vector 中的元素istream_iterator 允许使用懒惰求值,即只在需要时进行数据读取泛型算法结构任何算法最基本的特性是它要求其迭代器提供哪些操作。算法要求的迭代器操作可以分为5个迭代器类型:输入迭代器:只读不写;单遍扫描,只能递增输出迭代器:只写不读;单遍扫描,只能递增前向迭代器:可读写,多遍扫描,只能递增双向迭代器:可读写,多遍扫描,可递增递减随机访问迭代器:可读写,多变扫描,支持全部迭代器运算5 类迭代器类似容器,迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。输入迭代器可以读取序列中的元素。一个输入迭代器必须支持:用于比较两个迭代器的相等和不想等运算符用于推进迭代器的前置和后置递增运算符用于读取元素的解引用运算符,解引用只会出现在赋值运算符的右侧箭头运算符输出迭代器可以看作是输入迭代器功能上的补集,只写而不读元素,输出迭代器必须支持用于推进迭代器的前置和后置递增运算解引用运算符,只出现在赋值运算符的左侧前向迭代器可以读写元素,这类迭代器只能在序列中沿一个方向移动。前向迭代器支持所有输入和输出迭代器的操作。双向迭代器可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置的递减运算符。随机访问迭代器提供在常量时间内访问序列中任意元素的能力。除了支持双向迭代器的所有功能外,还支持:用于比较两个迭代器相对位置关系的运算符 (<、<=、>和>=)迭代器和一个整数值的加减运算(+、+=、-、-=),计算结果是迭代器在序列中前进或者后退给定整数个元素后的位置用于两个迭代器上的减法运算符,得到两个迭代器的距离下标运算符 iter[n] 与 *(iter[n]) 等价算法形参模式大多数算法具有如下4种形式之一:alg(beg, end, other, args)alg(beg, end, dest, other, args)alg(beg, end, beg2, other, args)alg(beg, end, beg2, end2, other, args)其中alg 是算法名字,beg和 end表示算法所操作的输入范围,几乎所有算法都接受一个输入范围。是否有其他参数依赖于要执行的操作。dest参数表示算法可以写入的目的位置的迭代器。算法假定按其需要写入数据,不管写入多少个元素都是安全的。如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已经存在的元素内。更常见的情况是,dest被绑定到一个插入迭代器或者是一个ostream_iterator。接受单独的beg2 或者 beg2和end2的算法用这些迭代器表示第二个输入范围,这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算算法命名规范除了参数规范,算法还遵循一套命名和重载。这些规则处理诸如:如何提供一个操作代替默认的 < 或者 == 运算以及算法是将输出数据写入到一个序列还是一个分离的目的位置等问题接受谓词参数来代替 < 或者== 运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。一个版本用元素自身的运算符来比较元素,另一版本接受一个额外的谓词参数来代替 <或者==unique(beg, end); unique(beg, end, comp); //使用comp函数比较元素接受一个元素值的算法通常有另一个不同名版本,该版本接受一个谓词,代替元素值,接受谓词参数的算法都有附加的_if 后缀find(beg, end, val); find_if(beg, end, pred); //pred 是一个函数,查找第一个令pred返回真的元素默认情况下,重排元素的算法将重排后的元素写回给指定的输入序列。这些算法还提供了另一个版本,将元素写到一个指定的输出目的位置。这类算法都在名字后加一个_copyreverse(beg,end); reverse(beg,end,dest); //将元素按逆序拷贝到dest一些算法还提供_copy和_if 版本,这些版本接受一个目的位置迭代器和一个谓词remove_if(v1.beg(), v1.end(), [](int i){return i % 2}); remove_if(v1.beg(), v1.end(), back_inserter(v2), [](int i){return i % 2});特定容器算法与其他容器不同,链表定义了几个成语啊函数形式的算法。,它们定义了独有的sort、merge、remove、reverse和unique。这些链表中定义的算法的性能比通用版本要高的多。与通用版本中的不同,链表中的特有操作会改变容器。
2021年05月10日
19 阅读
0 评论
0 点赞
2021-04-11
顺序容器
所有容器类都共享公共的接口,不同容器按不同的方式进行扩展,这个公共接口使得学习容器更加容器。我们基于这种容器所学习的内容也都适用于其他容器。每种容器都提供了不同的性能和功能权衡一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器的位置相对应。顺序容器概述所谓的顺序容器是指,在内存中数据存储有一定顺序。数据结构中的顺序容器有:可变数组、队列、数组、链表、栈。c++ 标准库中的顺序容器提供了快速顺序访问元素的能力。但是这些容器在一下方面都有不同的性能折中向容器中添加或者删除元素的代价非顺序访问容器中元素的代价标准库中顺序容器主要有:vector:可变大小的数组。支持快速随机访问,在尾部之外插入或者删除元素可能会很慢dque:双端队列,支持快速随机访问,在头尾位置插入/删除元素速度很快list:双向连标,只支持双向顺序访问,在list中任何位置进行插入删除操作速度都很快forward_list: 单向链表,只支持单向顺序访问,在链表任何位置插入删除元素速度很快array: 固定大小的数组,支持快速随机访问,不能添加或者删除元素string: 与vector容器类似,但专门用于保存字符,随机访问快。在尾部插入与删除速度快c++ 标准库中的容器是经过精心优化设计过的。性能通常会是同类数据结构中最好的。现代c++ 程序应该使用标准库容器,而不是原始的数据结构(如内置数组)通常使用vector 是最好的选择,除非你有很好的理由选择其他容器一下是一些选择容器的基本原则:除非你有很好的理由选择其他容器,否则使用vector如果你的程序有很多小的元素,且额外开销很严重,否则不要使用list或者forward_list如果程序要求随机访问元素,应该使用vector 或者deque如果程序要求在容器中间插入或者删除元素,应该使用list或者forward_list如果程序要在头尾位置插入或者删除元素,但是不会在中间位置插入删除元素,则应该使用deque如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素,则: 6.1 首先确定是否真的需要在容器中间位置添加元素。当处理输入数据时通常很容易向vector中添加数据,然后再调用标准库的sort函数,来重排元素,避免在中间位置添加元素 6.2 如果必须在中间位置插入元素考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector中如果你不确定该使用哪种容器,可以在程序中只使用vector 和list公共的操作,不使用下标操作,使用迭代器,避免随机访问容器库概述迭代器迭代器是访问容器中元素的公共接口所有迭代器都通过解引用运算符来实现这个操作。标准库中的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。部分容器的迭代器也定义了递减运算符,用于从一个元素移动到上一个元素一个迭代器范围是由一对迭代器来表示的。两个迭代器分别指向同一个容器的元素或者尾元素之后的位置。如果两个迭代器满足以下两个条件,则这两个迭代器构成一个迭代器范围:它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置我们可以通过反复递增begin 来到达end位置,换句话说,end不在begin之前如果两个迭代器构成一个迭代器范围,则:如果begin和end相等,则范围为空如果begin和end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素我们可以对begin递增若干次,使得begin== end容器类型成员每个容器都定义了多个类型,我们已经使用了其中的3种:size_type、iterator、const_iterator除了正向的迭代器,容器库还提供了反向遍历容器的迭代器,反向迭代器就是一种反向遍历容器的迭代器。与正向迭代器相比各种操作的含义也都发生了颠倒。例如对一个反向迭代器执行++操作,会得到上一个元素。剩下的就是类型别名了。通过类型别名,我们可以在不了解容器中元素类型的情况下使用它,如果需要元素类型可以使用容器的value_type ,如果需要元素类型的一个引用可以使用reference或者const_reference。begin 和 end 成员begin 和 end 操作生成一个指向容器中第一个元素和尾元素之后位置的迭代器范围。begin和end有多个版本。带r的版本返回反向迭代器,带c的版本返回const型迭代器容器定义和初始化可以将一个容器初始化为另一个容器的拷贝将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器,或者拷贝由一个迭代器对指定的元素范围为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。而且新容器和原容器中的元素类型也可以不同,只要能将拷贝的元素转化为要初始化的容器的元素类型即可在新标准中我们对一个容器进行列表初始化标准库array在定义之处就应该给出具体的大小,而且后续不允许修改它的大小我们不能直接对内置数组执行拷贝或者对象赋值操作,但是array对象允许赋值和swap我们直接使用 = 运算符来将一个容器赋值为另一个容器的拷贝,但是要求容器类型完全相同,array也支持这种操作顺序容器(array除外)还定义了一个assign的成员,assign操作用参数所指定的元素替换左边容器中的所有元素list<string> name; vector<const char*> oldstyle; name = oldstyle; //错误 = 要求两侧容器类型完全相同 name.assign(oldstyle);可以使用swap交换两个容器中的内容,要求两个容器类型完全相同。除了array外,swap不对任何元素进行拷贝、删除或者插入操作,可以保证在常量时间内完成容器大小操作每个容器都有三大与大小相关的操作。size: 返回容器中元素数目empty: 当容器中元素数量为0时,返回true,否则返回falsemax_size: 返回一个大于或者等于该类型容器所能容纳的最大元素数的值关系运算符除了无序容器外的所有容器都支持关系运算符关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素比较的过程与比较string大小的过程类似如果两个容器具有相同大小且所有元素都两辆对应相等,则两个容器相等。否则两个容器不等如果两个容器大小不同,但较小容器中每个元素都等于较大容器中对应元素。则较小容器大于较大容器如果两个容器都不是另一个容器的前缀自序列,则它们的比较结果取决于第一个不相等的元素的比较结果容器的相等运算符实际上是使用元素的==运算符实现比较的。而其他关系是使用元素的< 运算符顺序容器的操作向顺序容器中添加元素push_back:将内容追加到容器尾部push_front: 将内容添加到容器的首部insert: 在容器的特定位置插入0个或者多个元素,返回插入元素位置的迭代器emplace_back、emplace_front、emplace: 这些函数是直接在容器内部进行元素构造,而上述函数是将内容进行拷贝。从效率上讲emplace 函数会高一些使用这些操作时必须记得不同类型的容器使用不同的元素分配策略,而这些策略直接影响性能。访问顺序容器每个顺序容器中都有一个front 函数,返回容器内第一个元素的引用。而除了forward_list 之外的所有顺序容器都有一个back成员函数。另外可以使用at来访问随机位置的元素记住,这些访问函数返回的都是引用删除元素pop_front: 删除首元素pop_back: 删除尾元素erase: 可以从容器中删除指定位置的元素,可以传入一个范围,删除指定范围内的元素特殊的 forward_list 操作在对forward_list 进行增删操作的时候,需要找到对应位置的前驱节点,而单向链表无法很容器的找到一个节点的前驱节点。因此对单向链表,提供了类似 insert_after、emplace_after、erase_after等操作改变容器大小可以使用resize 来增大或者缩小容器大小,如果是缩小容器大小,则指向被删除元素的迭代器、引用、指针都会失效容器操作可能使迭代器失效在向容器中添加元素后:如果容器是vector或者string,且存储空间被重新分配,则指向容器的迭代器、指针都会失效。如果存储空间未重新分配,指向插入位置之前的迭代器、指针、引用仍然有效,但是指向插入位置之后元素的迭代器、指针和引用将会失效对于deque,插入到首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效对于list和forward_list,指向容器的迭代器、指针和引用仍然有效删除一个元素后,指向原来被删除元素的迭代器、指针和引用都会失效。对于forward_list 和list来说,指向容器其他位置的迭代器、引用和指针仍然有效对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素以外的其他元素的迭代器、引用和指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效。但是其他迭代器、引用和指针不受影响,如果删除首元素,这些也不会受到影响对于vector和string,指向被删除元素之前元素的迭代器、引用和指针仍然有效删除元素时尾后迭代器总是会失效使用insert插入元素后可以保存返回的迭代器,然后用该迭代器进行迭代可以保证迭代器有效不要保存end返回的迭代器vector 容器是如何增长的为了支持快速随机访问,vector 将元素连续存储。如果往容器中添加一个新元素时,发现容器空间已经不够了,就需要重新分配空间。并将已有元素逐一拷贝到新的内存空间中,然后添加新元素。为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间vector和string也提供了一些成员函数,允许我们与它的实现中内存分配部分互动。capacity: 告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素reserve: 允许我们同志容器它应该准备保存多少个元素一般来讲,vector 的实现采用的策略似乎是在每次需要分配新内容空间时将当前容量翻倍额外的string操作除了顺序容器共同的操作之外,string类还提供了一些额外的操作。这些操作中的大部分要么是提供string类和C风格字符串之间的互相转换,要么是增加了允许我们用下标代替迭代器的版本。substr: 返回一个string,它是原始string的一部分或者全部的拷贝可以使用insert、erase、assign 来改变字符串的内容append可以在字符串尾部添加一个新字符串; replace 进行查找替换搜索操作标准库中提供了6个不同的搜索函数,每个函数有4中不同形式的重载版本。每个搜索操作都返回string::size_type 值,表示匹配发生位置的下标。如果搜索失败返回一个名为string::npos 的static成员s.find(arg): 查找字符串中第一次出现某个字符串的位置s.rfind(arg): 查找字符串中最后一次出现某个字符串的位置s.find_first_of(arg): 在s中查找arg中任意一个字符第一次出现的位置s.find_last_of(arg): 在s中查找arg中任意一个字符最后一次出现的位置s.find_first_not_of(arg): 在s中查找第一个不在arg 中的字符s.find_last_not_of(arg): 在s中查找最后一个不在arg 中的字符compare 函数compare函数用于比较两个大小字符串,与C标准库中的strcmp类似数值转化to_string: 将数值数据转化为字符串stod: 将字符串转化为doublestof: 将字符串转化为floatstoi: 将字符串转化为intstol: 将字符串转化为longstoul: 将字符串转化为 unsigned longstoll: 将字符串转化为 long longstoull: 将字符串转化为 unsigned long longstold: 将字符串转化为 long double容器适配器适配器是标准库提供的一组概念,能使某种事物的行为看起来像另一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像另一种事物一样标准库提供了三种适配器: stack、queue、priority_queue(优先级队列)所有的适配器都要求容器具有添加和删除元素的能力。stack 只要求类型容器具有 push_back、pop_back 操作因此可以使用除了array 和 forward_list 之外的任何容器类型来进行构造queue 要求容器类型具有 back、push_back、front、pop_back 因此它可以构造在list 或者deque之上但不能基于vector 构造; priority_queue 要求容器类型具有push_back、front、pop_back之外还要求容器具有随机访问的能力,所以它必须构造在vector之上
2021年04月11日
31 阅读
0 评论
0 点赞
1
2
3
...
9