worldsite / blog.sc Goto Github PK
View Code? Open in Web Editor NEWBlogging soul chat, stay cool. via: https://blog.sc
Blogging soul chat, stay cool. via: https://blog.sc
一个用于创建可复用、可聚合的web组件的js库,只提供mvc中的v层。
Javascript的xml语法扩展,React将jsx编译成React构造器的方法。支持嵌套的子组件,方便直观。
搜索(线性和二分查找)、排序(冒泡、选择排序)、递归函数(阶乘、斐波那契数列)、时间复杂度(线性、二次和常量)
团队编写代码需要保持统一的代码格式,但是每个人总有各自的习惯,通过工具会更加方便管理。clang-format是基于clang的一个命令行工具,能够自动格式化C/C++/ObjectC代码,支持多种代码风格:Google、Chromium、LLVM、Mozilla、Webkit等,也支持自定义风格(通过编写clang-format文件)。
ffi 是Foreign Function Interface的简称,是一款nodejs的addon。
可以让大家在nodejs中调用c风格的c++动态链接库
ffi调用起来非常简单,可以看下官网的这个例子
var ffi = require('ffi-napi');
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2
// You can also access just functions in the current process by passing a null
var current = ffi.Library(null, {
'atoi': [ 'int', [ 'string' ] ]
});
current.atoi('1234'); // 1234
只要传入dll的路径,方法名称,返回值和参数类型,就可以得到一个关于dll中对应方法的代理对象。
接着在这个代理对象上执行对应的方法即可。
node-ffi的原理可以参见下图
实际上node-ffi是在libffi之上套了一层壳,将libffi的方法封装成了nodejs的addon
而libffi又调用了系统的API,打开特定的dll(POSIX下dlopen方法/Winodws下LoadLibraryEx方法),并获取对应的方法的地址(POSIX下dlsym方法/Winodws下GetProcAddress方法)。
获得方法对象之后,就可以封装对应的参数,在dll中执行,并获得对应的返回值
ffi相较于单独编写nodeaddon的方式简便了不少。
但是也会存在一定的问题。
因此ffi也不是银弹,需要大家在项目中根据实际情况进行考虑
参考文档:
作为一个IT从业者,一直想挑选一个合适的平台来写博客,希望有一个高度定制化的界面,展示自己独一无二的个性和工作、技能等。折腾过WordPress、hexo,自行搭建服务等一系列尝试后,间断性的码字总是被服务搭建到期而终止。最终发现,基于issuess的方式就是我想要的博客平台。
作为全球最大的代码托管平台,又被微软收入麾下,其可靠程度是非常高的,基本不用担心存放在里面的数据会丢失。
Github issues 提供了非常方便快捷的编辑能力,尤其是贴图。它支持通过拖拽、粘贴、选择的方式上传图片,图片会存放在 user-images.githubusercontent.com 这个地方,且支持外链——这也意味着我们可以很方便地把 issue 的内容转载到其他的平台。
在 Github issues 里面,可以为某条 issue 添加点赞、爱心等互动标签(Reactions),也可以设置分类标签(Labels),更可以给 issue 添加评论(Comment)。
Github 提供了一套满足了绝大部分需求的 API,囊括了 REST 和 GraphQL 的调用方式,这才是 Github 能够成为我们博客平台的大杀器。
使用 Github issues 作为博客平台,也就是相当于管理后端。我们在管理后端里面撰写文章,设置标签,回复评论,然后通过 API 调用把数据传送给客户端。
管理后端直接用现成的 Github issues 页面,那么客户端则使用 Github 为开发者免费提供的静态页面部署服务 Github pages。要使用这个服务,只需要开通一个仓库,然后在仓库的 Settings 里面找到 Github pages 并打开即可,默认会以 Master 分支的根目录作为静态资源目录,我们只需要把客户端的静态资源直接放置在这里就好。
开通了 Github pages 以后,便可以通过其提供的 URL 直接在浏览器里访问到博客了,而博客的数据则完全加载自 Github API。通过已授权的接口,还允许提交评论等功能。
总结一下,Github issues 提供了一个博客平台所需的的各项基本能力,与 Github 的可靠性, API 的全面性,Github pages 的便捷性结合在一起,都非常适合作为一个博客平台来使用。
下一步,我将一步一步学习如何基于Issues搭建个性化的博客平台。欢迎一起来学习、交流。
前端开发,使用CSS进行布局是工作的核心。在做ReactNativeDesktop的实践中,逐步学习了CSS的布局方式。本文将汇总相关的基础知识,以供回顾和总结。
传统的布局方式是通过盒模型,使用display(文档流布局)+position(定位布局)+float(浮动布局)来控制。
按照文档的顺序一个个显示,块元素独占一行,行内元素共享一行。
使元素脱离文档流,浮动起来。
通过position属性来定位。
通过上述盒模型的三种布局方式有一些缺陷,比如我们不能只使用一个属性来实现垂直居中布局,所以就产生了第四种布局方式:flex 布局。可以简便、完整、响应式地实现各种页面布局,Flex即Flexible Box的缩写,意为弹性布局,为盒模型提供最大的灵活性。任何一个容器都可以指定为flex布局,行内元素也可以通过inline-flex属性值来使用flex布局。inline-flex 和 inline-block 一样,对设置了该属性值的元素的子元素来说是个 display:flex 的容器,对外部元素来说是个 inline 的块。
在 flex 中,最核心的概念就是容器和轴,所有的属性都是围绕容器和轴设置的。其中,容器分为父容器和子容器。轴分为主轴和交叉轴(主轴默认为水平方向,方向向右,交叉轴为主轴顺时针旋转 90°)
在使用 flex 的元素中,默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis),主轴开始的位置称为 main start,主轴结束的位置称为 main end。同理,交叉轴开始的位置称为 cross start,交叉轴结束的位置称为 cross end。在使用 flex 的子元素中,占据的主轴空间叫做 main size,占据的交叉轴空间叫做 cross size。
这里需要强调,不能先入为主认为宽度就是 main size,高度就是 cross size,这个还要取决于你主轴的方向,如果你垂直方向是主轴,那么项目的高度就是 main size。
实现 flex 布局需要先指定一个容器,任何一个容器都可以被置顶为 flex 布局,这样容器内部的元素就可以使用 flex 来进行布局。简单说来,如果你使用块元素如 div,你就可以使用 flex,而如果你使用行内元素,你可以使用 inline-flex。
需要注意的是:当时设置 flex 布局之后,子元素的 float、clear、vertical-align 的属性将会失效。
设置了 flex 属性的容器可以通过设置其属性值来设置容器的子元素的排列规则:
flex-direction:设置子元素的排列方向,row(默认值,主轴为水平方向,起点为左端)、row-reverse(主轴为水平方向,起点为右端)、colum(主轴为垂直方向,起点在上沿)、colum-reverse(主轴为垂直方向,起点在下沿)
flex-wrap:设置是否换行,让弹性盒子元素必要时换行显示,不设置默认为不换行(如果容器装不下子元素会相应的收缩);nowrap(默认不换行)、wrap(换行第一行在上)、wrap-reverse(换行第一行在下)
flex-flow:flex-direction 和 flex-wrap 属性的复合属性;默认为row nowrap
justify-content:设置子元素在横轴上的排列对齐方式;flex-start(默认值,左对齐)、flex-end(右对齐)、center(居中)、space-between(两端对齐,子元素间隔相等)
align-items:设置子元素在纵轴上的排列,flex-start(交叉轴的起点对齐)、flex-end(交叉轴的终点对齐)、center(交叉轴的中心点对齐)、baseline(子元素的第一行文字的基线对齐)、stretch(默认值;子元素未设置高度height或设置为auto将占满整个容器高度;如果设置则此属性值无效)
align-content:当有多行子元素时每行子元素之间的排列方式;当只有一行子元素时即flex-wrap属性值为no-wrap时,该属性没有效果;stretch(默认值;子元素默认排列,相当于只设置了flex-wrap: wrap,每行子元素间有一定的间隔)、flex-start(子元素从纵轴的起点开始排列,且行间没有间距)、flex-end(与纵轴的终点对齐,且行间没有间距)、center(与纵轴的中点对齐,行间无间距)、space-between(与纵轴两端对齐,轴线之间的间隔平均分布)、space-around(每行子元素的间隔都相等,且每行子元素之间的间隔比子元素到父元素的距离大一倍)
order:设置某个子元素的排序先后顺序,数值越小,排列越靠前,默认为0
flex-grow:设置子元素的放大比例,默认为0;0时即使存在剩余空间也不放大;1时等分剩余空间;即按照所有子元素的值求和后等分,然后乘以各自的系数值
flex-shrink:子元素缩小比例,默认为1;1时即默认是如果父元素装不下子元素,如果不换行的话那么子元素都按照一样的比例缩小;该属性取负值无效;如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小;如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小
flex-basis:子元素的理想宽度,即子容器在不伸缩情况下的原始尺寸,主轴为横向时代表宽度,主轴为纵向时代表高度;默认值为auto即子元素的本来大小;跟长度width差不多意义,可以设为一个长度值,定义了子元素初始占据的宽度;设置子元素在被放进一个flex容器之前的大小,也就是子元素理想或假设的大小,但是flex-basis并不能保证其实际呈现的大小;但当父元素装不下子元素时,子元素会按照flex-shrink来进行相应比例的缩小,如果没有设置flex-shrink默认情况下每个子元素的压缩率都是一样的;当父元素有空余空间时,设置了flex-basis的子元素并不会自动扩大,而是保持flex-basis的属性值设置的大小
flex:flex-grow、flex-shrink和flex-basis的组合;默认值为0 1 auto;有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)
aligin-self:设置单个子元素的纵轴排列方式;允许单个项目有与其他项目不一样的纵轴排列方式,可覆盖align-items属性;默认值为auto,即跟父元素设置的 align-items 的值指定的纵轴排列方式一样;可能取6个值,设置的值可覆盖掉父元素设置的align-items指定的排列方式;
网格布局,实现二维布局方式。可以让我们摆脱现在布局中存在的文档流限制,换句话说,你的结构不需要根据设计稿从上往上布置了。这也意味着您可以自由地更改页面元素位置。这最适合你在不同的断点位置实现你最需要的布局,而不再需要为响应你的设计而担心HTML结构的问题。
和 table 布局不同的是,grid 布局不需要在 HTML 中使用特定的标签布局,所有的布局都是在 CSS 中完成的,你可以随意定义你的 grid 网格。
没有 HTML 结构的网格布局有助于使用流体、调整顺序等技术管理或更改布局。通过结合 CSS 的媒体查询属性,可以控制网格布局容器和他们的子元素,使用页面的布局根据不同的设备和可用空间调整元素的显示风格与定位,而不需要去改变文档结构的本质内容。
网格线组成了网格,他是网格的水平和垂直的分界线。一个网格线存在行或列的两侧。我们可以引用它的数目或者定义的网格线名称。
网格轨道是就是相邻两条网格线之间的空间,就好比表格中行或列。在网格中其分为grid column和grid row。每个网格轨道可以设置一个大小,用来控制宽度或高度。
网格单元格是指四条网格线之间的空间。所以它是最小的单位,就像表格中的单元格。
网格区域是由任意四条网格线组成的空间,所以他可能包含一个或多个单元格。相当于表格中的合并单元格之后的区域。
使用 grid 布局很简单,通过display属性设置属性值为 grid 或 inline-grid 或者是 subgrid(该元素父元素为网格,继承父元素的行和列的大小) 就可以了。
网格容器中的所有子元素就会自动变成网格项目(grid item),然后设置列(grid-template-columns)和 行(grid-template-rows)的大小,设置 grid-template-columns 有多少个参数生成的 grid 列表就有多少个列。
注:当元素设置了网格布局,column、float、clear、vertical-align属性无效。
如果没有设置 grid-template-columns,那么默认只有一列,宽度为父元素的 100%。设置了 grid-template-columns 的话,设置了几个参数,就有几列(不超过 grid item 的个数),然后设置的 grid-template-row 参数就是每一列的高度(超出列数的高度无效)。
例如:grid-template-columns: 1fr 1fr 2fr; 生成3列
css fr 单位是一个自适应单位,fr单位被用于在一系列长度值中分配剩余空间,如果多个已指定了多个部分,则剩下的空间根据各自的数字按比例分配。fr 是基于网格容器可用空间来计算的(flex 也是一样),所以我们可以和其他单位混合使用,如果需要的话。
例如:grid-template-rows: minmax(100px,200px) minmax(50px,200px),将第一行的高度设置为 minmax(100px,200px),第二行的高度设置为minmax(50px,200px)
如果容器总高度设置为 300px,这时每一列的高度要怎么算呢?总高度是小于第一列高度的最大值和第二列高度的最大值之和的,这样就是先用 总高度 300px - 第一列最小高度 100px - 第二列最小高度 50px = 150px。第一列高度:第一列最小高度 100px + 150px/2 = 175px;第二列高度:第一列最小高度 50px + 150px/2 = 125px
重复行或者列:repeat() 可以创建重复的网格轨道。这个适用于创建相等尺寸的网格项目和多个网格项目。repeat接受两个参数:第一个参数定义网格轨道应该重复的次数,第二个参数定义每个轨道的尺寸。
间距:grid-column-gap创建列与列之间的距离;grid-row-gap创建行与行之间的距离;grid-gap 是 grid-row-gap 和 grid-column-gap两个属性的缩写
我们可以通过表格线行或者列来定位 grid item,grid-row 是 grid-row-start 和 grid-row-end 的简写。grid-column 是 grid-column-start 和 grid-column-end 的简写。如果只提供一个值,指定了 grid-row-start 和 grid-column-start 的值。如果提供两个值,第一个值是 grid-row-start 或者 grid-column-start 的值,第二个值是 grid-row-end 或者 grid-column-end 的值,两者之间必须要用/隔开。这四个值可以用 grid-area 缩写,分别对应 grid-row-start、grid-column-start、grid-row-end、grid-column-end。
和 excel 中的合并单元行/列是相同的(这个需要设置在 grid item 中),grid-column-start、grid-column-end、grid-row-start、grid-row-end,也可以使用 grid-row 和 grid-column 简写的形式,关键词 span 后面紧随数字,表示合并多少个列或行,/ 前面是从第几行/列开始。
在 grid 中,是可以自定义网格线的名称的,然后使用定义好的网格线来进行布局,[col1-start] 网格线名称一定要使用 [] 括住。
什么是网格区域? 网格区域(grid-area)是一个逻辑空间,主要用来放置一个或多个网格单元格(Grid Cell)。他是由四条网格线(Grid line),网格区域每边一条,四边相交组织的网格轨道(Grid Track)。简单点理解,网格区域是有四条网格线交织组成的网格空间,这个空间中可能是一个网格单元格,也可能是多个网格单元格。
定义网格区域 在CSS Grid Layout中定义网格区域有两种方式,一种是通过网格线来定义,另一种是通过grid-template-areas来定义。接下来看看两种定义网格区域的方法在具体使用过程中有何不同。
网格线定义网格区域:使用网格线定义网格区域的方法非常的简单,首先依赖于 grid-template-columns 和 grid-template-rows 显式定义网格线,甚至是由浏览器隐式创建网格线,然后通过 grid-area 属性通过取网格线,组成网格线交织区域,那么这个区域就是所讲的网格区域。在使用 grid-area 属性调用网格线,其遵循的规则是 grid-area: row-start/ column-start / row-end / column-end。
grid-template-areas 定义网格区域:在 CSS Grid Layout 中还可以通过 grid-template-areas 属性来定义网格区域的名称,然后需要放在对应网格区域的元素,可以通过 grid-area 属性来指定。而且重复区域可以使用同一个名称来实现跨区域。另外对于空的轨道区域,可以使用点号 . 来代表
垂直居中真的是已经被讲烂了,这个方式是有很多的,但就是看的太多会导致最后一个都没有记住,所以最好是每一种情况只记住一个最佳实践。
对于居中,不需要背什么“x 种方式实现 xx”这样的例子,我们只需要了解其原理即可写出符合要求的 css。
水平、垂直居中,比较喜欢用绝对定位的方法实现,其次就是使用 table 布局,因为自带垂直居中。如果是单行的行内元素使用 line-height 等于 height,对于多行元素的垂直居中,大部分都是使用 table 元素(求推荐更好的布局),当然还有 flex 和 grid 布局
一般水平居中还是比较容易的,一般都是先看子元素是固定宽度还是宽度未知;如果是固定宽度,这种方式是绝对定位居中,除了使用 margin,我们还可以使用 transform;宽度未知,将子元素设置为行内元素,然后父元素设置 text-align: center。
多个块状元素,上面的方式即使子元素不止一个也想实现水平居中也是有效的,(宽度固定不固定都可,不固定的话就不需要设置宽度,会被自动撑开,但是要考虑到撑爆的情况)。当然也可以使用我们刚刚介绍的 flex,我们只需要让子元素在主轴上的对齐方式设置为居中就可以。
单行行内元素,只需要将子元素的行高等于高度就可以了。多行的行内元素,因为给每一个子元素都设置了 line-height,但是试了很多方法,要不是没有效果,要不然就是又局限性,提到最多的是使用 table-cell 的方式(但是貌似这个方法也有一点弊端,那就是其子元素的表现形式和行内元素类似,子元素不能独占一行);还有一个方法是设置一个空的行内元素,使其 height:100%,display:inline-block,vertical-align: middle; 并且 font-size:0。但是这样方式的原理我还不是很清楚,只是知道要设置一个空元素,高度和父元素相等,并且设置垂直居中的属性。但是,这只是用与所有的行内元素的宽度和不超过父元素的宽度的情况。另一个一劳永逸的方法就是 flex。
经常有看到设计稿是图片和文字垂直居中的,那么怎么才能让图片和文字垂直居中呢?只需要给图片一个 vertical-align: middle; 属性就可以。
我们要实现水平或者垂直居中,应该从两方面下手:元素自带居中的效果或者强制让其显示在中间。
所以我们先考虑,哪些元素有自带的居中效果,最先想到的应该就是 text-align:center 了,但是这个只对行内元素有效,所以我们要使用 text-align:center 就必须将子元素设置为 display: inline; 或者 display: inline-block;;
接下来我们可能会想既然有 text-align 那么会不会对应也有自带垂直居中的呢,答案是有的 vertical-align:,我一直不是很喜欢使用这个属性,因为十次用,9.9 次都没有垂直居中,一度让我怀疑人生。现在貌似也搞得不是很清楚,看了 张鑫旭的文章 居然看得也不是很懂,笑哭。目前就在 table 中设置有效,因为 table 元素 的特性,打娘胎里面带的就是好用。还有一种可以有效的方式是前面提到的空元素的方式,不过感觉多设置一个元素还不如使用 table。
还有一只设置垂直居中的是将行内元素的 line-height 和 height 设置为相同(只适用于单行行内元素)
固定宽度或者固定高度的情况个人认为设置水平垂直居最简单,可以直接使用绝对定位。使用绝对定位就是子元素相对于父元素的位置,所以将父元素设置 position:reletive 对应的子元素要设置 position:absolute,然后使用 top:50%;left:50%,将子元素的左上角和父元素的中点对齐,之后再设置偏移 margin-top: 1/2 子元素高度;margin-left: 1/2 子元素宽度;。这种方式也很好理解。
上面的绝对定位方法只要将 margin 改为 transform 就可以实现宽度和高度未知的居中(兼容性啊兄弟们!(ಥ_ಥ))transformX:50%;transformY:50%;
其实我还真是第一次听说圣杯布局这种称呼,看了下这个名字的由来,貌似和布局并没有什么关系,圣杯布局倒是挺常见的三栏式布局。两边顶宽,中间自适应的三栏布局。
这个布局方式的关键是怎么样才能使得在伸缩浏览器窗口的时候让中间的子元素宽度改变。可以适应浏览器的宽度变化使用百分比设置宽度再合适不过,所以我们要将中间子元素的宽度设置为 100%,左边和右边的子元素设置为固定的宽度。
这里我们要注意的是,中间栏要在放在文档流前面以优先渲染。
将其三个元素的宽度和高度设置好,然后都设置为 float:left。
我们可以看出,现在三个子元素是在一排显示的,因为我们给中间的子元素设置的宽度是 100%,并且中间的子元素在文档流的最前面,最先被渲染。
那么我们要使得三个元素在同一排显示。接下来我们要将 .left 和 .right 向上提。实际上我们是使用 margin-left 为 负值来实现的,我们将 .left 的 margin-left 设置为 -100%(负的中间子元素的宽度),这样,左边的元素就会被“提升”到上一层。
然后就是右边子元素了,只需要设置 margin-left 设置为负的自身的宽度。
现在中间的子元素被遮挡了,只要使得中间的子元素显示的宽度刚好为左边元素和右边元素显示中间的宽度就可以。同时我们还必须保证是使用的半分比的布局方式。
这样的话有一种方式可以即使中间的宽度减少,又可以使中间的宽度仍然使用 100%,那就是设置父元素的 padding 值,将父元素的 padding-left 设置为左边子元素的宽度,将父元素的 padding-right 设置为右边子元素的宽度。
中间的子元素确实是在中间了,那么我们只需要设置相对位置,将左边的子元素和右边的子元素向两边移动就好。
双飞翼布局是为了解决圣杯布局的弊端提出的,如果你跟我一起将上面的圣杯布局的代码敲了一遍,你就会发现一个问题,当你将浏览器宽度缩短到一定程度的时候,会使得中间子元素的宽度比左右子元素宽度小的时候,这时候布局就会出现问题。所以首先,这提示了我们在使用圣杯布局的时候一定要设置整个容器的最小宽度。
圣杯布局和双飞翼布局解决问题的方案在前一半是相同的,也就是三栏全部float浮动,但左右两栏加上负margin让其跟中间栏div并排,以形成三栏布局。
不同在于解决”中间栏div内容不被遮挡“问题的思路不一样:圣杯布局,为了中间div内容不被遮挡,将中间div设置了左右padding-left和padding-right后,将左右两个div用相对布局position: relative并分别配合right和left属性,以便左右两栏div移动后不遮挡中间div。
双飞翼布局,为了中间div内容不被遮挡,直接在中间div内部创建子div用于放置内容,在该子div里用margin-left和margin-right为左右两栏div留出位置。
所以只是一个小小的改动,在我们将中间元素宽度调到比两边元素小的时候,也是可以正常显示,但是如果总宽度小于左边元素或者右边元素的时候,还是会有问题。
JavaScript是web的编程语言,定义了网页的行为(HTML定义网页内容,css描述网页布局)。
JavaScript 是一种轻量级的脚本编程语言,可以插入HTML页面编码。ECMA-262 是 JavaScript 标准的官方名称。
JavaScript是世界上最流行的脚本语言,因为你在电脑、手机、平板上浏览的所有的网页,以及无数基于HTML5的手机App,交互逻辑都是由JavaScript驱动的。
JavaScript是一种运行在浏览器中的解释型的编程语言。
HTML 中的脚本必须位于 <script> 与 </script> 标签之间。脚本可被放置在 HTML 页面的 和 部分中。您可以在 HTML 文档中放入不限数量的脚本。通常的做法是把函数放入 部分中,或者放在页面底部。这样就可以把它们安置到同一处位置,不会干扰页面的内容。
也可以把脚本保存到外部文件中。外部文件通常包含被多个网页使用的代码。外部 JavaScript 文件的文件扩展名是 .js。使用外部文件,可在 <script> 标签的 "src" 属性中设置该 .js 文件。外部脚本不能包含 <script> 标签
JavaScript 没有任何打印或者输出的函数,但是可以使用window.alert() 弹出警告框、document.write() 方法将内容写到 HTML 文档中、 innerHTML 写入到 HTML 元素、console.log() 写入到浏览器的控制台。
JavaScript是字母大小写敏感的,使用unicode字符集,涵盖了所有字符包括标点符号等。常用驼峰命名规则。
JavaScript不会执行注释,但是可以提高代码可读性。可以类似C++的单行注释或者多行注释。
固定值,如3.14;数字字面量可以是整数或者小数或者科学计数;字符串字面量可以使用单引号或者双引号;表达式字面量用于计算;数组字面量定义一个数组;对象字面量定义一个对象;函数字面量定义一个函数;通常恒定不可变
用于存储数值;使用var定义变量,使用等号为变量赋值;变量通过变量名访问,通常可变。
变量均为对象,声明一个变量时,即创建一个新的对象
使用分号分隔,用于发送命令,告诉宿主环境执行任务。语句通常以一个语句标识符开始,并执行该语句。语句标识符作为保留关键字,不能作为变量名使用。例如function、return等。
标签:break+标签可以跳出任何代码块,break+不带标签、continue用于循环中使用
函数是可以反复调用执行的语句块,多由事件驱动。使用function,包裹在花括号中。可以使用return增加返回值。函数内部定义的变量为局部变量,在函数运行以后被删除。
数字、字符串、数组、对象等
function isArray(myArray) {
return myArray.constructor.toString().indexOf("Array") > -1;
}
function isDate(myDate) {
return myDate.constructor.toString().indexOf("Date") > -1;
}
可访问变量、对象、函数的集合,变量在作用域内保持生命周期。
HTML 事件是发生在 HTML 元素上的事情。当在 HTML 页面中使用 JavaScript 时, JavaScript 可以触发这些事件。HTML 事件可以是浏览器行为,也可以是用户行为,如HTML加载完成、按钮被点击等。
事件可以用于处理表单验证,用户输入,用户行为及浏览器动作:
可以使用多种方法来执行 JavaScript 事件代码:
使用单个字符串来描述、匹配一系列符合某个句法规则的字符串搜索模式。搜索模式可用于文本搜索和文本替换。
/正则表达式主体/修饰符(可选)
在 JavaScript 中,正则表达式通常用于两个字符串方法 : search() 和 replace()
正则表达式修饰符:
正则表达式模式:方括号用于查找某个范围内的字符
JavaScript 中,函数及变量的声明都将被提升到函数的最顶部。JavaScript 中,变量可以在使用后声明,也就是变量可以先使用再声明。JavaScript 初始化不会提升,只有声明的变量会提升,初始化的不会。
为了避免这些问题,通常我们在每个作用域开始前声明这些变量,这也是正常的 JavaScript 解析步骤,易于我们理解。
JavaScript 严格模式(strict mode)即在严格的条件下运行。"use strict" 指令在 JavaScript 1.8.5 (ECMAScript5) 中新增。它不是一条语句,但是是一个字面量表达式,在 JavaScript 旧版本中会被忽略。"use strict" 的目的是指定代码在严格条件下执行。严格模式下你不能使用未声明的变量。
严格模式通过在脚本或函数的头部添加 "use strict"; 表达式来声明。
"严格模式"体现了Javascript更合理、更安全、更严谨的发展方向,包括IE 10在内的主流浏览器,都已经支持它,许多大项目已经开始全面拥抱它。另一方面,同样的代码,在"严格模式"中,可能会有不一样的运行结果;一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。掌握这些内容,有助于更细致深入地理解Javascript,让你变成一个更好的程序员。
所有的编程语言,包括 JavaScript,对浮点型数据的精确度都很难确定。
现有工程是基于C++开发的,这里主要讨论C++库,以及基于C++的UWP移植和方案处理。
UWP是微软近些年大力打造的一个支持多设备、多CPU架构、多语言等可以运行在Win10系统的一个通用平台。在这个平台上微软加强了安全性、软件规范性,同时软件必须要经过微软商店的评审,很大程度上保证了软件的质量。
在此平台上,微软摈弃了历史悠久的Win32运行时库,转而提供一个全新的Windows运行时库,被称之为WinRT。WinRT的强大之处在于,它并不限制某一种语言,就目前来说它支持的语言有以下几种
同时,我们还可以使用多种语言来创建我们自己的WinRT库,来满足软件模块复用的需求。对于C++来说,共有三种模块可选择
C++/WinRT,该方案支持完全以C++17的语法来编写代码以及调用WinRT的API,但是如果想要创建一个新的WinRT运行时组件,那么就需要把Lyra的导出类和接口都定义为一个IDL文件,然后由必要的WinRT工具生产相应的实现类来进行导出功能。工程量依然巨大和复杂。C++/CX WinRT,该方案则需要使用CX的语法来将功能导出,同样工程量不小。
UWP模块,该方案可以直接使用现有的C++代码,导出方式也和传统的Win32DLL一致,需要注意的是,某些Win32函数将不再支持,因此,移植的工作就是查看哪些函数不能在UWP环境下运行,然后在WinRT中找到替代方案来代替它。
DISM全称是Deployment Image Servicing and Management,一个命令行的部署映像服务和管理工具。针对特殊的渠道版本,需要附带自定义配置信息,可以利用Preinstall Apps Using DISM中的Use custom data files来实现。
DISM会在App的安装目录下生成的Custom.data文件,文件的格式可以自定义,文件名和内容生成无法修改,后续用户通过商店升级APP后,这个文件也不会丢失。直到用户卸载App时被一起移除,开发者可以将文件放到Appx/microsoft.system.package.metadata目录下(microsoft.system.package.metadata为隐藏目录),来调试自定义配置的定制版本功能。
参考:https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/web-view
可以通过使用InvokeScriptAsync来调用或注入脚本到webview,通过ScriptNotify 事件从 webview获取信息。
string functionString = String.Format("document.getElementById('nameDiv').innerText = 'Hello, {0}';", nameTextBox.Text); await webView1.InvokeScriptAsync("eval", new string[] { functionString });
UWP开发涉及很多UI布局文件,为便于协作和防止xaml文件的冲突,建议安装xaml styler插件,统一xaml格式,减少文件修改的冲突发生。安装后修改XAML Styler配置,Keep first attribute on same line设置为true,Format XAML on save改为false
最近Visual Studio 2022 Preview更新后,启动打开项目后,莫名奇妙的直接崩溃,不显示任何异常提示
解决方案是增加环境变量如下:
为避免将来的博客内容杂乱无章,首先限定博客的主题**,以将编程、生活日记、工具wiki等分离拆开。
本博客主要记录编程相关的内容,比如:
这将是未来的工作技能名片。
python修改文件后,仅需要保存,执行 python demo.py 即可。而C/C++修改文件后,需要执行 gcc demo.c -o demo 编译生成方可
print "hello world!"
我们来分析下python的执行流程,以 python demo.py 为例:
而编译型语言会先将代码文件编译成机器码,生成可执行文件;所以脚本解释型语言速度会慢一点。
不同于C/C++的强类型,python支持赋值时确定数据类型的动态语义。这种方式更加符合人类的直观思维,所见即所得(WYSIWYG原来也可以在语言中体现)。
num = 4
print num
python中以缩进来决定语句的层次关系和逻辑块。
a = 1
b = 2
if a == b
print 'hello world!'
python具体而分,包含模块、语句、表达式、和对象构成。程序由模块组成,模块包含语句,语句包含表达式,表达式建立并处理对象。
# regex:coding[:=]\s*([-\w.]+)
#coding=utf-8
"标准模块脚本的写作范式,作为脚本的文档"
# 引入模块
import time
print time.__doc__
# 单行注释
desc = "全局变量" #后备注释
def hello():
"""
多行注释
"""
return "hello world"
# main
if __name__ == "__main__":
print hello()
类似C++,不能包含python的关键字;大小写敏感
python内置万能钥匙,这些彩蛋可以方便的让我们去学习和掌握了解各个模块。
这得具体情况具体分析。
1、如果只是写一段web页面图片轮播,或是跑马灯效果等等之类简单的功能。那不需要保护。
2、如果是精心设计一个绚丽的特效,如果想要保护这段自己付诸幸苦实现的特效代码不被他人随意拿去使用,那应该保护这段JS代码!
3、如果页面上有重要的功能是用JS代码管控的,比如交易逻辑、帐号密码信息、个人隐私、甚至有与远程服务器或数据库的通信等等,那么相关的js代码非常应该被保护、应该做JS代码防盗保护!
否则可能引起被黑客分析、攻击等严重问题。安全相关的事情,从来都要防患于未然、不可心存侥幸。除非它对你毫不重要。
1、打包&压缩
有人认为打包、压缩就是对JS代码的保护。确实,打包在一定程度上可以起码些许保护作用,好像看起来是如此。但打包、压缩的目的并不是为了保护JS代码,而是为了使用方便、减小代码体积,方便使用、便于传输。比如模块化的编程可能产生200个JS文件,如果使用时逐一用“script src”进行引用……这是种折磨,不管是对于代发人员,还是网络加载(浏览器也会生气!x_x)。
类似Webpacket、Gulp进行打包,可以将这些多个JS合成到一个文件,并且可能会进行回车、换行、空格的删除,以实现代码压缩,也有一些简单混淆操作:把长变量名改为统一风格的短变量名等。然后,最终生成的一个文件。代码总量减小了、可读性差了、使用方便了。同时让有些人认为这也实现了JS代码保护。其实、实际上、当然的代码并没有被保护:可读性依旧,只是代码量大了一些而已,只要稍稍耐心的读代码,会发现,代码依然是很易理解的,没有多少安全性可言。
2、混淆&加密
前端JS代码的保护,必需要混淆和加密共用。
单独的JS源代码加密,是行不通的,更不可能有所谓的JS不可逆加密。因为代码在浏览器端执行时,必须转解密还原成原始代码,才能被浏览器的JS引擎识别和运行。在解密后,会存在完整的原始JS代码。这是非常不安全的存在,有多种方法可将原始的JS代码显示出来。
JS代码混淆被不少开发人员认为是不够高端的JS代码保护方式,听起来不如JS源码加密更具安全性。事实上,混淆也有多个级别。比如比较低端的字符搜索和串替换、随机插入伪僵尸代码、字符串十六进制化等等。而也有高端的手法,会先进行语法分析、词法分析,重建语法树,相当于已经实现了一个JS引擎,在引擎中处理代码,那么,就可以在其中任意一步进行自由度极高的操作,比如在语法树中插入新的语法结构、比如可以将字符串全部提取并进行加密、可以对变量进行整体有规则化的重定义使无意义化等等。这样就可以实现真正的代码重建。这样重建的JS代码安全性,将会有一个质的提升。
当正真的混淆和加密联合使用,可以实现真正的JS代码安全保护。JS混淆中融入JS加密,JS加密中又嵌入JS混淆。这样保护后的代码,即使在客户端执行环境中被逆向还原,得到的也是大量含义不明的函数、代码、字符串。特别重点是:代码已经经过了重建,这时逆向得到的也是分离后的重建的无意义JS代码、大量的僵尸代码、混淆的字符串、不明含意的变量。可读性与原始代码相比……天壤之别。
固执的人或许还会说:没有破解不了的保护方案,只要我认真、用心、用时的分析,还是能分析出原始代码含意的,确实,可能如此。
但是,原本的代码,可能只需要读10分钟,而从这样保护后的JS代码读取原始含意,可能需要……10个月。而这时候,我们的JS代码可能已经更新到下一版了。
JS代码保护的目的已经达到了,不是吗?
毫无疑问,Python 是当下最火的编程语言之一。使用C/C++做开发将近10年,尝试下学习其他语言,来体验不一样的开发体验。
Python的相关核心概念可以提前了解一下,然后在整个学习过程中反复品味和更新。从而掌握一门语言最核心的通用原理,万般皆通。
Python系列学习将从如下几个方面进行学习和讲解:
超文本标记语言
超文本:使用html编写的文档,文件格式.html
HTML文档结构
文档类型
文档根元素
文档头部
文档主题
双标签与单标签
标题:h1~h6
段落:p
链接:a
图像:img
列表:ul+li,ol+li
表格:table+thead+tbody+tr+td
表单:form+label+input+button
框架:iframe
通用:div/span
容器:header/nav/main/article/section/footer
header
nav
main
article
section
aside
footer
设置HTML元素在文档中的布局和显示方式(外观)
元素属性: style="color: red"
元素标签:<style>p{color:red;} </style>
外部资源:
样式规则:
选择器:标签选择器、类选择器
样式声明
样式选择器:
id选择器:id是唯一的,#id
class选择器:.class
标签选择器:p
优先级:标签<class<id<javascript
一切皆盒子,一切布局的基础,页面上的一切可见元素均可看作盒子。
盒子默认都是块级元素:独占一行,支持宽高设置。
盒子模型可以设置六个样式:宽高、背景、内外边距、边框
width:水平方向
height:垂直方向
background-color:背景,默认透明
margin:外边距,当前盒子与其他盒子之间的位置与关系,默认透明,只有宽高属性
border:边框,位于内外边距之间,是可见元素,不透明,除宽度外,还可以设置样式与前景色
padding:内边距,内容与边框之间的填充区域,默认透明,只有宽高属性
其中margin与padding的设置,属性按顺时针上右下左排列,可设方式为:上-右-下-左,上-左右-下,上下-左右,上下左右。
根据可见性可以分为两类:
可见:width,height,border
透明:background,padding,margin
id选择器:#id{...}
class选择器:.class{...}
标签选择器:标签{}
属性选择器:选择器[id=""]{...}
群组选择器:地位平等,同时设置多个选择器样式;选择器1,选择器2 {...}
相邻选择器:选择器+* {...},加号标识选中相邻,*作为通配符,命中相邻的一个
兄弟选择器:相邻兄弟选择器,平级的所有元素;选择器~ * {....}
伪类选择器:使用冒号
子元素选择器:根据位置进行选择;选择器 空格 : first-child/last-child/nth-child/nth-last-child(元素索引,从1开始) {...}, 空格也可以改为具体的选择器;
类型选择器:选择器 类型: first-of-type/last-of-type/nth-of-type {...},只要选中类型,就要使用带有type的
关注点是位数,用nth-child
既关注位置,又关注类型,用nth-of-type
background-color:背景色,覆盖边框区域
background-image:背景图,url(image-path),默认会水平垂直重复
background-repeat:重复方向,no-repeat(不重复),repeat-x(水平重复),repeat-y
background-position:背景定位,left center,30px 50px,10% 20%
background-attachment:滚动方式,scroll,fixed(固定)
background-img:多背景图设置,css3新增
background-size:设置背景图尺寸,css3新增
background-clip:设置背景图绘制区域,css3新增
background:简写,顺序为:背景色 背景图 重复方向 背景定位 滚动方式
padding会撑大盒子
宽度分离:增加中间层
box-sizing:计算盒子宽高计算方式,改为边框+padding;box-sizing: border-box;content-box则恢复默认计算方式
同级塌陷:垂直方向同级排列时,上边盒子margin-bottom与下边盒子margin-top会重叠
嵌套传递:子元素的margin会向父元素传递;子元素的margin转为父元素的padding来解决
自动挤压:margin-left: auto,自动定位到右侧
浏览器交出页面布局的权限,交给用户,即将元素从文档流中脱离出来。脱离后必然是一个块,可以设置宽高等。
流动布局:又称文档流/标准流,元素排列顺序与元素在html文档中的编写顺序一致的,从上到下,从左往右
脱离文档流的手段:
浮动:将元素在水平方向上自由移动,垂直仍在文档流中
float:left,right;clear:both/left/right
浮动元素:让元素脱离文档流,在垂直方向仍然在文档流中,在水平方向可以向左右两边自由浮动
浮动元素对前面的元素无影响,只会影响后面的元素
浮动元素之间可以理解为在水平方向组成了一个文档流,按照规则进行排列
子元素(区块)与父级包含块(区块):
子元素浮动,父区块塌陷,高度丢失:浮动后脱离文档流,父区块无法再包裹住子元素
给父区块设置高度:可以但是pass,太不灵活
父区块跟着一块浮动:pass,需要很多级别均设置float
子元素同级增加一个元素:设置 clear: both父元素增加一个overflow,专用来清浮动:overflow:hidden
伪元素:老教材中有,pass
清空浮动以overflow来处理
定位:将元素在页面重新排列
静态定位:static,文档流定位,流动布局
相对定位:relative,元素仍在文档流
只是相对它原来的位置发生偏移
{position: relative; left:30px;right:20px}
后边元素依旧认为它在原来位置,然后进行后续布局
绝对定位:absolute,元素脱离文档流
参照物:相对于离他最近的,具有定位属性的父级元素进行定位;循环后的最顶级的参照物为body
{position: absolute; left:0;right:0}
固定定位:fixed,不在文档流中
网页拆分为上中下(通用头部/底部+内容区),然后内容区拆分为左中右。
头部:链接+导航
底部:
主体的布局,常用方式是双飞翼布局、圣杯布局。
双飞翼布局:左右固定,中间自适应;优先展示中间,以提高渲染优先级
DOM结构
主体:放到wrap容器中实现宽度分离,以规避padding撑大盒子;默认大小由内容决定
左侧
右侧
圣杯布局:杯体+两个耳朵把手
DOM结构
主题:不需要wrap,盒子大小改为边框计算方式{box-sizing: border-box}
左侧
右侧
原生表格的DOM结构如下:
table:表格
caption:标题
thead:表头,tr+th
tbody:主体,tr+td
tfoot:页脚,tr+td
rowspan/colspan:合并单元格
CSS控制表格的样式:
box-shadow:阴影
tfoot尽量直接写在tbody中
hack方式的伪元素:通过css向页面添加元素
table:after {}
table:before {}
表格布局=>定位(position)=>浮动(float)=>弹性(flex)=>网格(Grid)
布局的传统解决方案,基于盒状模型,依赖 display
属性 + position
属性 + float
属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
Flex 是 Flexible Box的缩写, 意思是"弹性布局", 用来为盒状模型提供最大的布局灵活性
任何一个容器都可以设置为Flex
布局模式
/* 块元素可以设置为Flex容器 */
.container {
display: flex;
}
/*内联元素也可以设置为Flex*/
span {
display: inline-flex;
}
/* WebKit内核浏览器,如Safari, 需要加前缀*/
div {
display: -webkit-flex; /*Safari*/
display: lfex;
}
float
, clear
, vertical-align
属性全部失去意义, 没有效果了Flex容器:简称容器,采用flex
布局的元素
Flex项目:简称项目
Flex容器中的所有成员(子元素)会自动成为该容器的成员,称为flex项目
flex项目都支持宽高设置, 哪怕它之前是内联元素,类似于浮动元素
主轴:水平轴,横轴,x轴
main start
: 起始位置
main end
: 结束位置
main size
: 单个项目占据的主轴空间
交叉轴: 垂直轴,坚轴,y轴
cross start
: 起始位置cross end
: 结束位置cross size
: 单个项目占据的交叉轴空间order:定义项目排列顺序,索引越小越靠前,默认为0
flex-grow:定义项目的放大比例,默认为0不放大,即有剩余空间时也不放大项目
flex-shrink:项目缩小比例,默认为1,即如果空间不足,则自动缩小项目来填充
flex-basis:定义项目在主轴占据的空间,默认为auto,即项目原始大小
flex:是flex-grow,flex-shrink,flex-basis的简写,默认为0 1 auto,后两个属性可选
align-self:个性化定制某个项目的对齐方式,可以覆盖容器的aligin-items属性,默认为auto
Grid是第一个专门为解决布局问题而生的CSS模块
display: grid
grid-template-columns/grid-template-rows:设置行列大小
grid-column/grid-row:将它的子元素放入网格
Grid Container:网格容器,设置了display:grid
的元素,是所有grid item的直接父项
Grid Item:网格元素,容器的直接子元素,间接的子元素不是哦
Grid Line:网格线,分界线组成网格结构,可以是垂直的,也可以是水平的,并位于列或行的任意一侧
Grid Track:网格轨道,两个相邻网格线之间的空间,可以想象成网格的列或者行
Grid Cell:网格单元,两个相邻的行和相邻的列网格线之间的空间,是网格的一个单元
Grid Area:网格区域,四个网格线包围的总空间,网格区域可以由任意数量的网格单元组成
display:将元素定义为grid容器,并为其内容建立新的网格格式化上下文
grid:生成一个块级网格
inline-grid:生成一个行级网格
subgrid:如果容器本身就说一个grid项目(即嵌套网络),可以使用这个属性表示想从父节点获取它的行/列大小,而不是指定自己的大小
column/float/clear/vertical-align对容器没有影响
grid-template-columns/grid-template-rows:使用以空格分隔的多个值来定义网格的行和列,表示轨道的大小,空格代表网格线
grid-template-areas:通过引用grid-area属性指定的网格区域的名称来定义网格模板,重复网格区域的名称导致内容扩展到这些单元格。点号代表一个空单元格,提供了网格结构的可视化
grid-template:定义了grid-template-rows/columns/areas的简写
grid-column-gap/grid-row-gap:指定网格线的大小,即设置列/行之间的间距宽度
grid-gap:grid-row-gap和grid-column-gap的缩写
justify-items:沿着横轴对齐网格内的元素
align-items:沿着纵轴对齐
justify-content:网格的总大小可能小于容器的大小,如果所有项目都使用像素值这样的非弹性单位设置大小,则可能出现此种情况。此时可以设置网格容器内的网格的对齐方式,沿着横轴对齐网格
aligin-content:同上,验证纵轴对齐网格
grid-auto-columns/grid-auto-rows:指定自动生成的网格轨道(即隐式网格轨道)的大小。
grid-auto-flow:如果你存在没有显示指明放置在网格上的 grid item,则自动放置算法会自动放置这些项目。 而该属性则用于控制自动布局算法的工作方式。
grid:在单个属性中设置所有以下属性的简写:grid-template-rows,grid-template-columns,grid-template-areas,grid-auto-rows,grid-auto-columns和grid-auto-flow。 它同时也将 sets grid-column-gap 和 grid-row-gap 设置为它们的初始值,即使它们不能被此属性显示设置。
grid-column-start/grid-column-end/grid-row-start/grid-row-end:指定元素网格线的起始/终止位置
grid-column/grid-row:上述属性的简写
grid-area:给网格进行命名,也可以作为grid-row-start + grid-column-start + grid-row-end + grid-column-end 的简写形式
justify-self:沿着行轴对齐grid item 里的内容(与之对应的是 align-self, 即沿列轴对齐)。 此属性对单个网格项内的内容生效。
align-self:沿着列轴对齐grid item 里的内容(与之对应的是 justify-self, 即沿行轴对齐)。 此属性对单个网格项内的内容生效。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
<title>简单的三列布局</title>
</head>
<body>
<header class="header">头部</header>
<aside class="left">左边栏</aside>
<main class="content">主体内容区</main>
<aside class="right">右边栏</aside>
<footer class="footer">底部</footer>
</body>
</html>
/* style.css */
body {
/*设置body元素采用网格布局*/
display: grid;
/*行模板: 第一行60px,第二行750px, 第三行60px*/
grid-template-rows: 60px 750px 60px;
/*列模板: 第一列200px,第2列自动扩展, 第3列200px*/
grid-template-columns: 200px auto 200px;
/*设置行间距:10px*/
grid-row-gap: 10px;
/*设置列间距: 10px*/
grid-column-gap: 10px;
}
.header {
/*网格区域命名*/
grid-area: my-header;
/*参考背景色*/
background-color: lightgreen;
}
.footer {
/*网格区域命名*/
grid-area: my-footer;
/*参考背景色*/
background-color: lightgreen;
}
.left {
/*网格区域命名*/
grid-area: my-left;
/*参考背景色*/
background-color: lightblue;
}
.right {
/*网格区域命名*/
grid-area: my-right;
/*参考背景色*/
background-color: lightblue;
}
.content {
/*网格区域命名*/
grid-area: my-content;
/*参考背景色*/
background-color: coral;
}
/*设置网格区域: 非常直观*/
body {
/*网格区域名称相同, 意味着合并*/
grid-template-areas:
"my-header my-header my-header"
"my-left my-content my-right"
"my-footer my-footer my-footer"
}
@media (max-width: 1050px) {
}
Submit a DMCA takedown for any pages.
Please commit a comment to submit a DMCA takedown notice. We will receive emails during office hours and delete the information on your submission request. You also can send an mail to [email protected] for more help.
Thanks to everyone in the open source community for giving us so much knowledge and learning opportunities.
Thank you very much!
Yours sincerely, Ant Team.
版权报告, 请在这里提交DMCA删除要求。
请在下方留言提交一个DMCA版权举报通知,我们将时刻接收您的举报邮件,并尽快到达办公室后第一时间删除您举报的信息。您也可以发邮件到[email protected]通知删除。
感谢开源社区的每一个人,给了我们如此丰富多彩的知识,提供了如此有价值的学习机会。
非常感谢~~~
此致,蚂蚁团队。
鸣谢!
对象、类、方法和构造函数、继承
libuv是一个高性能的事件驱动的I/O库,提供了跨平台的API。libuv是node.js的官方御用库,拥有卓越的系统编程性能,已被更多的如Rust语言等所使用。libuv提供了一个跨平台的抽象,实现不同平台的内核事件通知机制,如FreeBSD-kqueue、Linux-(e)poll、Windows-IOCP。
libuv使用异步的事件驱动编程风格,核心是提供一个event-loop,以及基于I/O和其他事件的通知回调函数。同时提供了一些核心工具,如定时器、非阻塞的网络支持、异步文件系统、子进程等。
异步编程在现代编程语言中越来越占主流地位,例如界面编程通过异步来实现非阻塞的交互体验,网络请求的异步处理等。最直观的理解方式就是观察者模式,我们关注每个事件,并对事件做出反应。libuv负责将来自系统的事件收集起来,或者监视其他来源的事件,用户注册相关的事件回调后就可以在事件发生时收到回调。
系统编程中最经常处理的一般是输入和输出,而不是一大堆的数据处理。传统的输入/输出函数(例如read,fprintf)都是阻塞式的,一个read操作返回前程序什么也做不了。例如文件写入数据,网络读取数据等,这些操作耗费的时间相比于cpu的处理时间差得太多。对于高性能的的程序来说,将无法忍受。
一个标准的解决方案是多线程,每一个阻塞的I/O操作都被分配到各个子线程中(或者是使用线程池),当某个线程一旦阻塞,处理器就可以调度处理其他需要cpu资源的线程。
libuv使用异步非阻塞的方案,大多数现代的操作系统都提供了基于事件通知的子系统。例如一个socket的read调用会发生阻塞,但是我们可以请求系统监视socket事件的到来,并将事件放到事件队列中,这样通过程序检查事件来及时获取数据并回调。异步是相对于时间或者空间来讲的,非阻塞的方式下我们无需等待数据瞬间返回而自由的处理我们的其他事情。例如我们使用微信,发给A一个消息后可以看个新闻,A回复消息后会提醒我们,我们再来查看。
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int main() {
auto loop = malloc(sizeof(uv_loop_t));
uv_loop_init(loop);
uv_run(loop, UV_RUN_DEFAULT);
uv_loop_close(loop);
free(loop);
}
同步阻塞式编程,在执行返回后可以返回代表结果的错误码(成功为0我们也统称为错误码的一种)。但是对于异步执行,会在执行失败后,给回调函数传递一个状态参数。libuv的错误码信息被定义为UV_E常量。通常小于0代表出现了错误,但是UV_EOF指示读取到文件等的末端,需要特殊处理。
libuv工作在我们监听了相关的特定事件,通常需要创建对应的I/O设备、定时器、进程等handle来实现。handle是不透明的数据结构,通过其uv_type_t的type来指定handle的使用目的。
初始化handle:
uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)
/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;
在异步操作中,handle上有许多与之关联的request,request是短暂性的对象,通常只维持在一个回调函数的时间,对应着handle上的一个IO操作。request用来在初始函数和回调函数直接进行上下文的传递,例如uv_udp_t代表一个udp的socket,对每一个向socket的写入操作,都会新建和回调一个uv_udp_send_t。
/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;
我们来思考下,为何要创建多个request? 是的,同一个request的话我们需要保证request的读写是线程安全的、原子安全的等等等。在使用asio的过程中,我尝试过使用一个request,但是我是将所有数据处理完成后才返回的,并保证了序列性从而保证request数据的安全性。对于libuv这样一个通用性的库,显而易见需要更加灵活的方式来返回数据,所以就需要多个request来保证数据的安全。
#include <stdio.h>
#include <uv.h>
int64_t counter = 0;
void wait_for_a_while(uv_idle_t* handle) {
counter++;
if (counter >= 10e6)
uv_idle_stop(handle);
}
int main() {
uv_idle_t idler;
uv_idle_init(uv_default_loop(), &idler);
uv_idle_start(&idler, wait_for_a_while);
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
uv_loop_close(uv_default_loop());
}
基于回调函数的异步编程,我们需要在调用处和回调函数之间传递一些上下文等特定的信息。所有的handle和request都有一个data域,用来存储信息并传递。uv_loop_t也有一个相似的data域。
传递一个上下文特定信息,这是一个c语言库中很常见的模式。想象一下如何实现C和C++的对象直接的互调操作? 最简单的是把C++的对象当作上下文,在所有的相关操作中附加带上。
文件读写可以通过uv_fs_*函数族和结构体完成,在线程池中调用系统的阻塞函数,在程序交互时通知在事件循环中注册的监视器。如果监视器callback为null,则自动执行阻塞同步。open和close是一次性执行的采用了同步处理,对任务和多路I/O的快速I/O采用异步来提升性能。
由于文件系统和磁盘的调度策略,写入成功的数据并不一定在磁盘上。
关于更多的文件操作API,可以查阅官方文档,这里就不再详细赘述,使用方式都是类似的。
讲到文件,就不得不提流。熟悉Asio的同学,可能对其中的流处理比较印象深刻。在libuv中,最基础的I/O操作是流uv_stream_t。TCP套接字、UDP套接字、管道对文件I/O和IPC来讲都可以看作是stream的子类。
数据的离散单元式uv_buffer_t,包含了指向数据的开始地址指针和长度,我们需要管理的式实际数据,需要自己分配和回收内存。类似asio的stream需要我们附加到自己的string上。nodejs使用自己的内存分配Smalloc,将buffer与v8的对象关联起来。
现代操作系统会提供相关的API来监视文件夹、文件的变化,例如inotify-linux、fsevents-darwin、kqueue-bsd、ReadDirectoryChanges-windows、eventPorts-solaris等。libuv包括了类似的文件监视库:uv_fs_event_start。具体可以参考API文档。
libuv的网络编程接口比BSD的socket便捷很多,因为都是非阻塞的,且原理都是一样的。libuv提供了覆盖了烦人啰嗦的底层任务的抽象函数。
libuv基于stream实现tcp:
libuv未提供stream形式的实现,而是提供了一个uv_udp_t句柄接收和uv_udp_send_t句柄发送。
libuv提供了一个异步的DNS解决方案,提供自己的uv_getaddrinfo,在回调函数中可以像使用正常的socket操作一样。
uv_interface_addresses可以获得系统的网络接口信息,在服务器准备绑定IP地址时很有用,可以查看哪些端口是否被占用等。
虽然做web编程,了解事件循环的方法即可。但是在nodejs实现前后端的统一后,我们还是有必要了解下处理器完成任务的单元:线程。线程更多是在内部使用,用来在执行系统调用时伪造异步的假象。libuv使用线程使得程序可以异步的执行一个阻塞的任务,使用线程池来保证大量阻塞API的调用。
libuv的线程API与pthread-POSIX API使用方法和语义上近似,在不同的系统平台上由于句法和语义表现都不太相似,libuv支持了有限数量的线程API。
只有一个主线程,主线程只有一个event-loop。不会有其他与主线程交互的线程,除非使用uv_async_send。
libuv的互斥量与pthread存在一一映射。递归调用互斥量函数在某些系统平台上支持,但是BSD上会报错,例如:
uv_mutex_lock(mut);
uv_thread_create(threadid, entry, (void*)mut);
uv_mutex_lock(mut);
可以用来等待其他线程初始化一些变量然后释放mut锁。
读写锁是更细粒度的实现机制,两个线程可以同时从共享区中读取数据。以读模式占用读写锁时,无法再以写模式拥有。以写模式占用锁时,其他读写都不可再拥有。
libuv提供了很多的子进程管理函数,跨平台支持stream/pipe完成进程间通信。在unix中的共识是一个进程只做一件事情并做到最好。因此进程通常通过创建子进程来完成不同的任务。一个多进程通过消息通信的模型,比多线程共享内存的模式会比较容易理解很多。
spawning child processes:uv_spawn,进程的命令行参数argv按照惯例,要比实际参数多一个,最后一个设置为NULL。
更多进程相关的内容,请搜索相关的文章来学习,这里只简单提一下。在真正需要时再行查找即可。
libuv提供了非常多的控制event-loop的方法,你能通过使用多loop来实现很多有趣的功能。你还可以将libuv的event loop嵌入到其它基于event-loop的库中。比如,想象着一个基于Qt的UI,然后Qt的event-loop是由libuv驱动的,做着加强级的系统任务。
uv_timer_t timer_req;
uv_timer_init(loop, &timer_req);
uv_timer_start(&timer_req, callback, 5000, 2000);
我们可以使用uv_timer_stop(&timer_req)来停止定时器,且在回调函数中可以安全使用。
可以用于执行一些优先级低的任务,例如可以向开发者发送程序的性能情况,以便于分析。
在使用uv_queue_work的时候,你通常需要给工作线程传递复杂的数据。解决方案是自定义struct,然后使用uv_work_t.data指向它。一个稍微的不同是必须让uv_work_t作为这个自定义struct的成员之一(把这叫做接力棒)。这么做就可以使得,同时回收数据和uv_wortk_t。
通常在使用第三方库的时候,需要应对他们自己的IO,还有保持监视他们的socket和内部文件。在此情形下,不可能使用标准的IO流操作,但第三方库仍然能整合进event-loop中。所有这些需要的就是,第三方库就必须允许你访问它的底层文件描述符,并且提供可以处理有用户定义的细微任务的函数。但是一些第三库并不允许你这么做,他们只提供了一个标准的阻塞IO函数,此函数会完成所有的工作并返回。在event-loop的线程直接使用它们是不明智的,而是应该使用libuv的工作线程。当然,这也意味着失去了对第三方库的颗粒化控制。
libuv的uv_poll简单地监视了使用了操作系统的监控机制的文件描述符。从某方面说,libuv实现的所有的IO操作,的背后均有uv_poll的支持。无论操作系统何时监视到文件描述符的改变,libuv都会调用响应的回调函数。
libuv提供了一个跨平台的API来加载共享库,用来实现插件、扩展、模块系统。
文字终端长期支持非常标准化的控制序列。它经常被用来增强终端输出的可读性。例如grep --colour。libuv提供了跨平台的,uv_tty_t抽象(stream)和相关的处理ANSI escape codes 的函数。这也就是说,libuv同样在Windows上实现了对等的ANSI codes,并且提供了获取终端信息的函数。
未完待续......
场景:将现有Win32平台Dll移植到UWP平台,供采用C#开发的Win Phone APP使用,而该DLL还依赖其他C++静态、动态库。
在Visual Studio中新建项目,模板选择Visual C++/Windows/通用页面,包含如下几个工程类型:
其中Dll和静态库可以被空白应用和运行时组件使用,并且是语言相关的,不能跨语言调用。运行时组件可以被空白应用使用,与语言无关,不管是C++还是C#应用均可调用。我们可以通过如下步骤来实现Win32到UWP的移植:
请下载Universal Windows Platform (UWP) app samples,参考微软官方方案的示例。
win32工程依赖关系为:app 依赖a.dll,a.dll 链接c++.lib,a.dll 依赖c.dll。
整体的工程关系转换为:a.dll -> a_rt.lib,c++.lib -> c++_rt.lib,c.dll -> c_rt.lib。其中通用Window版组件加_rt以作区分,Windows 运行时组件(通用 Windows)外壳定义为 shell_rt.dll。
新的依赖关系:app 依赖shell_rt.dll,shell_rt.dll 链接a_rt.lib、c++_rt.lib、c_rt.lib, 并且shell_rt.dll 负责重新封装a.dll的接口。app 可由 C++ 或 C# 开发。
注意: 创建 Windows 运行时组件(通用 Windows) 工程时,必须保证工程内的最外层命名空间名字和最终生成的dll名字(包括winmd文件)完全一致,这也是官方的要求。
通过阅读 官方文档 得知在不重新创建工程的情况下将现有工程转换为UWP工程的方法,如下:
打开DLL项目的项目属性
将配置改为所有配置
打开C/C++常规选项,将使用Windows运行时扩展设置为是(/Zw),这将启用组件扩展(C++/CX);注意此步骤只能用于C++项目,C语言项目需将此设置为否;如果C++项目中包含C文件,可以单独将C文件设置为否
在解决方案资源管理器中,选择项目节点,打开快捷菜单,然后选择重定向SDK版本目标,点击确定
在解决方案资源管理器中,选择项目节点,打开快捷菜单,然后选择卸载项目
在解决方案资源管理器中,选择卸载的项目节点,然后选择编辑项目文件,找到WindowsTargetPlatformVersion元素并将其替换为如下元素,然后关闭vcxproj文件,然后选择重新加载项目
<AppContainerApplication>true</AppContainerApplication>
<ApplicationType>Windows Store</ApplicationType>
<WindowsTargetPlatformVersion>10.0.10156.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.10156.0</WindowsTargetPlatformMinVersion>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
经过以上步骤处理,项目将被标识为通用Windows项目。
总结来讲,就是修改平台工具集、使用Windows运行扩展、运行库。
工程转换完成后,需要处理编译问题。由于编译问题各式各样,哪里有问题就改哪里。
例如可能需要增加如下宏定义,以解决环境问题:
#ifdef _M_ARM
#define WINAPI_FAMILY WINAPI_FAMILY_PHONE_APP
#endif
UWP 不支持fopen,CreateFile此类操作。用来替换的是CreateFile2,用法和CreateFile类似。但该API只能处理特殊目录,例如程序安装目录、图片、文档、视频等。对于磁盘中任意的目录,都没有操作权限。因此,对于期望可操作任意目录文件的需求,只能放弃使用CreateFile2,改用以下UWP组件中的磁盘操作类:
其它相关类请在类名上按F12打开对象浏览器查看。
看过类里的函数之后可以发现大部分函数都有Asyn后缀,带Asyn后缀的函数均为异步函数,Windows不希望UI线程及其它某些线程因为同步调用导致响应迟钝。C++中异步函数的调用方式大致为:
// 使用 task 和以下命名空间中的类时需开启 /ZW 选项,即开启 C++/CX 支持
#include <collection.h>
#include <ppltasks.h>
using namespace concurrency;
using namespace Platform;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;
// 从一个文件对象获取其目录对象
void Test(StorageFile ^file)
{
create_task(file->GetParentAsync()).then([this, file](StorageFolder ^parentFolder)
{
if(parentFolder != nullptr)
{
// do something
}
});
}
线程A调用Test函数,通过create_task创建一个task对象,并将一个lamda函数(位于then()中,[this, file]中声明的变量可在函数中使用)作为委托传递给task对象的then方法,并继续向下执行并退出Test函数。task中的file->GetParentAsyn()操作实际由线程B调用,待函数返回后,再将结果交由线程A执行委托函数。
^ 这个符号读作hat,这里用来声明句柄对象。String ^str;这里的str就是一个String的句柄类型,初始值或无对象指向时为nullptr,释放时可以使用delete str 也可以让作用域控制自动释放。可以简单的理解为类似智能指针。
异步方法虽然可以避免对线程A的阻塞,但实际使用中并不方便。因为,大部分情况下,我们都会为耗时的网络或磁盘操作专门开启线程处理,而不是直接使用UI线程操作。因此如果都使用这种异步方式,在某些场景下,代码会写的很反人类,例如下面这个比较完整的文件读取操作:
// 头文件、命名空间省略,变量判断、异常处理省略
void ReadBytesFromFile(String ^strFilePath)
{
// 根据文件路径获取文件对象;
create_task(StorageFile::GetFileFromPathAsync(strFilePath)).then([](StorageFile ^file)
{
if (file != nullptr)
{
// 以读写的方式打开文件;
create_task(file->OpenAsync(FileAccessMode::ReadWrite)).then([](IRandomAccessStream ^stream)
{
if (stream != nullptr)
{
auto buf = ref new Buffer(10); // 读取10个字节;
create_task(stream->ReadAsync(buf, 10, InputStreamOptions::None)).then([buf](IBuffer ^buffer)
{
// buf 和 buffer 中包含读取到的数据;
});
}
});
}
});
}
昔日Win32的一个CreateFile操作,在这里变的无比繁琐。而且,上面传入一个String路径打开文件的方式因为权限文件,并不可行。
在系统中,除几个个别目录(安装目录、图片目录、视频目录等)在app配置权限后可用于直接操作权限外,app是无法直接使用任意字符串路径进行文件操作的。正确的方式应该是:
如果需要在某目录下新建文件,则应该使用FolderPicker获取StorageFolder对象,将对象加入权限列表,再使用该StorageFolder对象创建文件。
考虑到在做代码移植时,调整某些线程的同异步模式将会导致原有框架结构变的混乱,因此,出现了下面的用法:
// 将file文件中偏移5开始的10个字符写入到偏移2开始的位置;
auto taskOpen = create_task(file->OpenAsync(FileAccessMode::ReadWrite));
if(taskOpen.wait() == canceled)
return false;
IRandomAccessStream ^stream = taskOpen.get();
stream->Seek(5);
auto buffer = ref new Buffer(10);
auto taskRead = create_task(stream->ReadAsync(buffer, 10, InputStreamOptions::None));
if(taskRead.wait() == canceled)
return false;
auto data = ref new Array<byte>(buffer->Length);
auto reader = DataReader::FromBuffer(buffer);
reader->ReadBytes(data);
stream->Seek(2);
auto taskWrite = create_task(stream->WriteAsync(buffer));
if(taskWrite.wait() == canceled)
return false;
auto taskFlush = create_task(stream->FlushAsync());
if(taskFlush.wait() == canceled)
return false;
这种 task.wait() 的调用方式并不能应用到所有线程。参见ppltask.h文件的task_status _Wait();函数及其中的_IsNonBlockingThread函数内部实现。请自行调试实验各类线程调用wait()中的_IsNonBlockingThread函数时的返回情况。
(经过验证,以上这种写文件的写法效率较低,在频繁调用时尤为明显,包括前面列出的对GetFileFromPathAsync 的调用)
Platform::Array<unsigned char> ^UnsignedChar2Array(unsigned char *pBuffer, unsigned int uSize)
{
return ref new Platform::Array<unsigned char>(pBuffer, 10);
}
std::wstring PlatformString2StdWstring(Platform::String ^str)
{
return std::wstring(str->Data());
}
std::string Unicode2Utf8(Platform::String ^str)
{
std::wstring wstrTemp(str->Data());
std::string strUtf8;
int iUtf8Len = ::WideCharToMultiByte(CP_UTF8, 0, wstrTemp.c_str(), wstrTemp.length(), NULL, 0, NULL, NULL);
if (0 == iUtf8Len)
return "";
char* pBuf = new char[iUtf8Len + 1];
memset(pBuf, 0, iUtf8Len + 1);
::WideCharToMultiByte(CP_UTF8, 0, wstrTemp.c_str(), wstrTemp.length(), pBuf, iUtf8Len, NULL, NULL);
strUtf8 = pBuf;
delete[] pBuf;
return strUtf8;
}
using namespace Windows::Storage::Streams;
IBuffer ^UnsignedChar2Buffer(unsigned char *pBuffer, unsigned int uSize)
{
DataWriter writer;
writer.WriteBytes(Platform::ArrayReference<uint8>(pBuffer, uSize));
return writer.DetachBuffer();
}
void Buffer2UnsignedChar(IBuffer ^buffer, unsigned char **pBuffer, unsigned int *uSize)
{
DataReader ^reader = DataReader::FromBuffer(buffer);
*uSize = buffer->Length;
*pBuffer = new uint8[*uSize];
reader->ReadBytes(Platform::ArrayReference<uint8>(*pBuffer, *uSize));
}
UWP 组件的接口不同于Win32 DLL 的导出接口,UWP 的接口是一个winmd 文件,包含语言无关类型信息MetaData(元数据)。使用组件时只需要 xxx.dll 和 xxx.winmd 两个文件,不需要头文件。
在导出接口时,首先需要最外层有一个和库文件名相同的命名空间名,导出的类需要声明成如下格式(需带public ref sealed声明 ):
namespace test // 组件名为test.dll
{
public ref class CInterface sealed
{
}
}
因为接口可能被跨语言使用,因此下面这种接口参数的写法就要避免:
void Func(Platform::String ^*pStr);
这种写法只能被C++使用,如果C#调用的话,会出现崩溃。不过,Platform::Array^ *这种写法倒是没有问题。int、int *诸如此类,都是可以的,int *对于C# 的调用,使用out进行修饰。
// C++方式的接口导出函数声明
void Func1(int *pParam);
void Func2(const Platform::Array<unsigned char>^ inArray);
void Func3(Platform::Array<unsigned char>^ *outArray);
// C#看到的接口声明
// void Func1(out int pParam);
// void Func2(byte[] inArray);
// void Func3(out byte[] outArray);
// C#方式的接口调用
Int32 param = 0;
Func1(out param);
byte[] inArray;
Func2(inArray);
byte[] outArray;
Func3(out outArray);
如果接口需要传递回调函数,需要封装成类,可以从接口导出一个interface 修饰的类:
namespace test
{
public interface ICallback
{
public:
virtual void func() = 0;
}
public ref class CInterface sealed
{
void RegCallback(ICallback ^callback)
{
// 对于callback我做了一层回调封装映射,由此处的ICallback ^ 类型与内部原有的C++ 回调形成映射关系(中间过渡)
// 避免C++/CX 语法深入内部
}
}
}
// C# 使用时
class CCallback : test.ICallback
{
public void func()
{
// do something
}
}
CCallback callback = new CCallback();
test.CInterface inter = new test.CInterface();
inter.RegCallback(callback);
我们可以将第三方库转为UWP模块项目,工程文件由原Win32的工程文件转换得来。skia项目除外,skia是由gn构建工具生成的。生成工程编译后,我们可以创建一个UWP的demo来调用测试。
在传统的Win32程序中,窗口和消息循环是分割的,我们可以使用 CreateWindow 来创建多个窗口,而多个窗口是共用一个消息循环,由于消息循环的独立性,我们的消息循环的代码通常像下面这样:
MSG msg = { 0 };
while (::GetMessage(&msg, NULL, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
即使没有Win32的窗口,消息循环也可以正常运行起来。同时,所有窗口共用的消息循环是运行在一个线程中的,也就是常说的主线程(UI线程)。而在UWP程序中,微软对窗口,消息循环进行了整合:
上图中,CoreApplication代表着整个UWP的进程,该UWP程序中,可以拥有多个CoreApplicationView,每一个CoreApplicationView是一个窗口和一个消息循环(消息分发)(CoreDispatcher)的结合,而且每一个CoreApplicationView独占一个线程。
也就是说,上图中的两个CoreApplicationView分别跑在不同的线程当中。
因此,UWP程序中,窗口和消息循环是不可分割的,是由UWP框架决定的,我们无法变更。
故而,google base库的MessagePump的Win32的实现模式(消息循环独立),在UWP中将不可复用。
参考SDL2对UWP的支持模式,对于MessagePump,初步设想为:MessagePump使用 CoreApplication.run 来实现,意味着必然会创建一个窗口,对于执行顺序问题,参考SDL2的处理方式,以回调函数的方式把创建窗口的操作延后。
skia目前找到的资料以及示例,Dx绘制的最小单元是一个CoreWindow。
UWP的窗口是CoreWindow,但是不支持子窗口,目前未知如何将一个CoreWindow设置为另外一个CoreWindow的子窗口方法。通过上边讲述的CoreWindow对应一个消息循环,所以子窗口在UWP是不存在的,不然我们的消息循环就没有问题了。
另外,CoreWindow不支持窗口的标题隐藏,在自绘窗口的情况下,会有移植问题;并且多个CoreWindow会在任务栏显示多个图标,不符合Win32的窗口视觉设计。
所以对Win32的窗口,移植到UWP应该是一个控件,例如为image。但是skia最小绘制单元为CoreWindow,不能对单独一个控件进行画面更新提交,可以通过Dx接口获取到刷新完成后的画面,然后使用UWP方法将图片刷新到image控件中。
UWP原生菜单不支持超出窗口大小,这种情况和Win32的菜单窗口行为是不同的,需要特殊注意。
UWP项目中,xaml后缀问题提供了大量的工具箱控件,但是并不能支持win32的所有事件。需要使用AddHandler来为控件添加事件响应。
例如:Button本来是监听不到鼠标左键的按下和抬起事件。哪怕你监听PointerPressedEvent事件,并在相应的事件处理函数中判断左键也不行,但是用AddHandler函数把PointerPressedEvent再添加一遍就可以监听到。
skin文件夹必须在uwp项目所在文件夹内,skin下所有文件必须添加到vs项目中,而且必须作为内容,不然appx包里就不会有当前文件。
如果所依赖的库在当前工程中,只需要右击引用添加即可;如果不在工程中,将依赖库以现有项的方式添加到工程中,并将属性页的文件内容改为是。
开发的前提是dll中不使用windows10已弃用的API,应该程序所需要的所有dll都需要放在appx目录下,debug模式默认目录下,除了configuration\debug\c++uwp_called_win32dll\appx里面有一个exe,在configuration\Debug\c++uwp_called_win32dll下也有一个exe
这个页面我将记录阅读《JavaScript高级程序设计》第三版的点滴记录,既记录基础语法,又包含相关的理解
最初的作用是在客户端实现表单的验证等,而今具备了与浏览器窗口及其内容等几乎所有方面的交互能力,并且通过node.js等可以实现更多native的能力。
Javascript已经发展成为一门功能全备的编程语言,能够处理复杂的计算和交互,拥有闭包、匿名函数和元编程等特性。一个完整的实现包含如下部分:
HTML中插入js的方法是使用<script>元素,惯例会将标签放在元素中,现代Web应用为渲染优化会放在元素中。
文档模式我们现在均默认选择标准模式:
<!DOCTYPE html>
语言的核心就是语法、操作符、数据类型、内置功能等,通过这些基础设施来解决现实问题。
ECMAScript的一切变量、函数名、操作符等均区分大小写,函数名、变量名不能是关键字如typeof,标识符采用驼峰大小写格式。
可以保存任何类型的数据,仅是一个用于保存值的占位符而已。定义时使用var操作符,未初始化时会存储一个特殊值undefined。
我们可以理解为是一个void*,用于存储任意类型的数据的地址
通过var定义的为局部变量,省略var则定义为全局变量。
6种数据类型:undefined、null、boolean、number、string、object,可以使用typeof操作符来确认类型
函数在ECMAScript中是对象,不是一种数据类型;但是也确实有一些特殊的属性,因此区分函数和其他对象是有必要的
null:只有一个值;如果在C++中实现,可以认为指向了一个全局构造的对象地址,如base::EmptyString();null指向一个空对象的指针,所以typeof(null)===object;
undefined:只有一个值,使用var声明变量但未对其初始化的值;注意与空对象指针是不一样的,即便undefined继承自null所以null==undefined 为true(注意==操作符为了比较目的,实际做了操作数的转换),但是typeof两者却是不同的
对未声明的变量,只能使用typeof检测其类型,并且会返回undefined;怀疑是否在执行时构造了一个临时变量?
我们应该要尽量做到每一个声明的对象均给与初始化,以便于区分未声明的对象
在编程中的条件判断中,建议全部显式转换为boolean,从而保证整个流程的准确无歧义且易于阅读
在编程中,保证一个变量的类型不被修改(虽然是合法的),这样更加便于理解和清晰明了(请原谅我喜欢强类型的编程模式)
浮点数值的最高精度是17位小说,但是算术计算精度却有舍入误差,永远不要测试浮点数值
如果计算值超过Number.MAX_VALUE则返回Infinity,小于Number.MIN_VALUE则返回-Infinity,且不能再参与计算,可以通过isFinite()函数检测返回值
NaN非数值是一个特殊的数值,表示本来要返回数值的操作数未返回数值,例如除以0
任何涉及NaN的操作都返回NaN,NaN与任何值都不相等(包括NaN本身);可以通过isNaN()函数检测,任何不能被转换为数值的值都会返回true,如isNaN("blue")=NaN,isNaN(object)时会先调用对象的valueOf()方法,确定其返回值是否可以转换为数值,如果不能则基于返回值调用toString()方法,再测试返回值
数值转换Number():boolean转为0和1,数字值直接返回,null转为0,undefined返回NaN,字符串如果只包含数字转为十进制;字符串忽略前导0,字符串包含有效浮点格式转为对应浮点数值,字符串包含有效十六进制转为相同大小十进制,字符串为空转为0,字符串非上述情况转为NaN;对象调用valueOf()返回值进行Number()调用,若上述返回NaN则再调用toString方法再调用Number()
数值转换parseInt():转换字符串,查看是否符合数值模式,忽略字符串前的空格,找到第一个非空格字符;若第一个字符非数字和负号,返回NaN;对空字符串会返回NaN;解析后续字符直到遇到结束或者遇到非数字字符;注意:小数点不是有效的数字字符哦;可以识别十进制、十六进制,第二个参数指定基数可以传递按照多少进制解析,我们要求明确指定基数
数值转换parseFloat():与parseInt类似,只是增加了对第一个小数点的解析支持;且始终忽略前导0,只解析十进制格式,十六进制字符串会返回0
length属性:获取字符串长度,返回字符串中16位字符的数目,如果包含双字节字符则不会精确返回字符数目
ECMAScript中的字符串是不可变的,要改变变量保持的字符串需要先销毁原来的字符串然后用另一个包含新值的字符串填充该变量
字符转换toString:数值、布尔、对象、字符串均有toStirng方法,但是null和undefined没有;数值调用时可以传递基数,默认情况以十进制格式返回数值的字符串表示
字符转换String():将任意类型转为字符串,包括null和undefined;如果有toString,调用并返回;null返回"null",undefined返回“undefined”
一元递增递减:对任何值都适用,且变为数值变量;有效数字字符串先转为数字值再执行增减;不包含有效数字字符时返回NaN;false变为0再执行;true变1再执行;对象先调用valueOf执行增减,如果为NaN执行toString后执行增减
一元加减:加相当于转换为数值,减相当于转为数值的负数
位操作符:按内存中的数值的位来操作数值;数值的位操作是64位的数值转换为32位然后执行位操作后再转换为64位;特殊的NaN和Infinity被当作0来处理;对非数值先使用Number转为数值后再执行位操作,得到一个数值的结果
位非NOT:~,返回数值反码,操作数的负值减1
位与AND:&,位对齐,每一位全为1取1反之取0
位或OR:|,位对齐,每一位全为0取0反之取1
位异或XOR:^,位对齐,每一位只有一个1取1反之取0
左移:<<,所有位向左移动指定位数,以0补足空位;不会影响符号位
有符号的右移:>>,保留符号位
无符合右移:>>>,所有32位右移;负数以补码右移会返回很大的值
逻辑非:!,任意类型均返回一个布尔值,对象返回false,空字符串返回true,非空字符串返回false,0返回true,非0和infinity返回false,null返回true,NaN返回true,undefined返回true;两个!!相当于模拟Boolean()
逻辑与:&&,相当于短路操作;有一个操作数不是布尔时,返回就不一定时布尔;第一个操作数是对象,返回第二个操作数;第二个为对象,第一个求值为true时返回第二个对象;两个操作数据均是对象返回第二个;有一个为null返回null;有一个为NaN返回NaN;有一个为undefined返回undefined
逻辑或:||,有一个操作数不是布尔值,就不一定返回布尔;第一个为对象,返回第一个;第一个操作数求值为false返回第二个;两个对象返回第一个;两个都是null返回null;两个都是NaN返回NaN;两个都是undefined返回undefined;短路操作,第一个操作数求值为true就不执行第二个
工作中,我们在某些情况下需要区分(const int *p;) (int const *p;) (int *const p;)三者,那么他们的区别是什么呢?
首先,我们来看下边的两个模板定义:
template <typename R, typename Receive, typename... Args>
bool Invoke(R (Receive::*method)(Args...), Args... args) {
using Functor = FunctorAdapter<R, Receive, Args...>;
return Invoke<Functor>(method, args...);
}
template <typename R, typename Receive, typename... Args>
bool Invoke(R (Receive::*method)(const Args...), Args &&... args) {
using Functor = FunctorAdapter<R, Receive, const Args...>;
return Invoke<Functor>(method, args...);
}
当method的参数为指针时,需要如何定义才能匹配上述两个模板调用呢? 这时候我们就需要使用 const type* const ,如下:
virtual void Method(const std::int32_t *const &) {}
int* p定义了一个普通的指针,const指针有如下几种情况:
*p = 2; // 错误,实际内存值无法通过p被修改,所以*p不能作为左值,
p = nullptr; // 正确,p可以被修改
*p = nullptr; // 正确,*p指向的内存可以通过p修改
p = nullptr; // 错误,p是const的,无法修改,不能作为左值
*p = nullptr; // 错误
p = nullptr; // 错误
const在类型之后:修饰的是const前的类型,即变量的实际值不可变
const在指针之后:修改的是const前的指针,则指针不可变
const在类型之前:等同于在类型之后
不可变,则不能作为左值
int i = 0;
const int& r1 = i; // 正确
const int& r2 = 3; // 正确
const int& r3 = i * 5; // 正确
int& r4 = r1 * 2; // 错误
int& r5 = 3; // 错误
术语中我们讲的常量引用,其实指的是引用常量
static const 和 const static是一样的
微软提供了一种将win32程序直接转成UWP的方法--Desktop Bridge
UWP发布过程:Pre-processing --> Certification --> Release --> Publishing
DesktopAppConverter.exe -Setup -BaseImage D:\UWP\BaseImage-15063-UPDATE.wim -Verbose
DesktopAppConverter.exe -Installer E:\App\AppSetup.exe -InstallerArguments "/S" -Destination D:\App\App -PackageName "0C72C7CD.535318B65018D" -AppId "App" -AppDisplayName "APP" -PackageDisplayName "App" -AppDescription "App" -Publisher "CN=7C12DEB1-3990-4000-BC36-CFF184F703CC" -PackagePublisherDisplayName "App" -Version 3.0.41.0 -MakeAppx -Verbose
DesktopAppConverter.exe -Installer d:\App\win32\AppSetup.exe -InstallerArguments "/S" -Destination D:\App\output -PackageName "0C72C7CD.535318B65018D" -AppId "App" -AppDisplayName "App" -PackageDisplayName "App" -AppDescription "App" -Publisher "CN=7C12DEB1-3990-4000-BC36-CFF184F703CC" -PackagePublisherDisplayName "App" -Version 3.0.41.0 -Verbose
图片压缩,如果D:\App\output\0C72C7CD.535318B65018D\PackageFiles\Assets\AppLargeTile.scale-400.png文件大小超过200kb,可以使用pngout进行优化
手动打包:不要再desktop bridge环境里
"C:\Program Files (x86)\Windows Kits\10\bin\x86\makeappx.exe" pack /d D:\app\output\0C72C7CD.535318B65018D\PackageFiles /p D:\app\output\0C72C7CD.535318B65018D_S05.appx /l
打开C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools,在cmd运行VsDevCmd.bat,然后执行如下操作
cd "C:\Program Files (x86)\Windows Kits\10\bin\x86"
MakeCert.exe -r -h 0 -n "CN=7C12DEB1-3990-4000-BC36-CFF184F703CC" -eku 1.3.6.1.5.5.7.3.3 -pe -sv my.pvk my.cer
pvk2pfx.exe -pvk my.pvk -spc my.cer -pfx my.pfx
cd "C:\Program Files (x86)\Windows Kits\10\bin\x86"
signtool.exe sign -f my.pfx -fd SHA256 -v .\0C72C7CD.535318B65018D_S03.appx
Before you submit your app for certification and listing in the Windows Store, use the Windows App Certification Kit to test your app and make sure it's ready to go.
"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\appcertui.exe"
本文中,我们将要学习PHP;初学者的学习的路线如下:
学习总是容易疲惫,交叉学习可以保持大脑的清晰。接下来我们来学习下PHP的基础语法,然后利用开源的框架,开发一个企业网站。
PHP是一种创建动态交互性网站的强有力的服务器端脚本语言。代码在服务器上执行,以纯html返回给浏览器,代码文件以".php"作为文件扩展名。跨平台和面向对象是其最大最重要的特性。PHP是B/S体系结构,安全性高,内嵌Zend加速引擎性能稳定快速。
PHP被唱衰很长时间了,但是目前以PHP架构的网站依旧占据着互联网的一大部分,并且PHP在不断的更新迭代,相信知识永远不会过时。PHP的扩展库很多,例如对MySql、bzip2等的支持;我们在需要的时候查询相关的文档即可,这里不做列举,以免造成学习的恐惧。
学习环境可以使用AppServ等集成开发环境,生产环境现在也有了宝塔等类似的环境。我们不在环境上浪费过多时间,网上很多。开发工具也根据个人喜好即可,这里我们选择VSCode。
/*index.php*/
<?php
echo "Hello World!!!";
?>
本系列学习将从如下几个方面进行学习和讲解:
编程的终极捷径就是:coding、coding、coding
python是一种优美的语言
Ptyhon之禅,阐述了Python语言的核心**。我们来品味一下:
import this
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Python之禅 by Tim Peters
优美胜于丑陋(Python 以编写优美的代码为目标)
明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)
简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)
复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)
扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)
间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)
可读性很重要(优美的代码是可读的)
即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)
包容所有错误,除非你确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码)
当存在多种可能,不要尝试去猜测
而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)
虽然这并不容易,因为你不是 Python 之父(这里的 Dutch 是指 Guido )
做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)
如果你无法向人描述你的方案,那肯定不是一个好方案;反之亦然(方案测评标准)
命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)
简单看了下python的介绍,发现如下两个与C/C++相区分的关键点:对象+引用。如何来理解呢?
对象是包含了一系列数据和方法的实体
这里的对象可以理解为C++中的对象,python实现时类似于将每一个数据都通过内置对象类型做了一层包装。例如整形的5,在python中是一个interger对象,其值为数字5。
变量在python中类似于C++中的函数地址,指向了数据的对象。例如 t = 5 ,可以理解为t指向了一个对象,这个对象的值为5,t是这个对象的引用。
当我们后边将要学习python的基础内置数据类型时,会更加深刻的理解这两点。这样做的好处是什么呢?
理解上边阐述的概念,那么我们可以思考下:自己来设计python的数据类型时会怎么做呢?
我们使用C++来实现时,自然而然就想到了引用计数。这也是工作中,我们管理内存的常用方式。
import sys
print sys.getrefcount(5)
通过如上的阐述,我们可以想象到python中一切都可以用C++中的类指针方式实现。
所以,python中不用再考虑指针的问题了。
综上,在现实世界中,一切均可抽象为对象。在微观世界中,我们可以理解为一切皆由最小的元子组成;如果不停的微观化,是不是就可以重塑这个世界? 人抽象为各种元子组成,感情也是元子的一种属性及方法;如果抓住了最核心的元子,造人也许就是真的造人了!!!
回到对象本身,可以明了python的数据组成部分:身份(id查看唯一标识符,可以取到内存地址)、类型(type)、值(属性)
转自:https://mp.weixin.qq.com/s/vkOH79E8XBtbxSLo_AGFcg
翻译原文链接在这里:https://zhuanlan.zhihu.com/p/427778091
干货开始:
这本书的副标题是:45ish Simple Rules with Specific Action items for better C++ ,这本书是由公司大佬推荐的, 个人认为有必要掌握一下这45条最佳实践, 可以很大程度上提升代码的可读性和健壮性, 而且这本书也不长, 翻译起来也会比较简单,比很多人推荐的Effective C++ 要容易读的多, 抽了一个十一的尾巴就翻译完了,easy~ 翻译的过程中去掉了欧美人写书时喜欢带上的口水话,从而让文章更加精炼。++我翻译的时候会带上一些自己的理解,这部分我用下划线标出来了, 注意区分++。
我以一个训练者的目标希望各位能够:
最佳实践实际上就是说:1. 减少常见的错误;2. 能够更快定位错误 3. 提升运行性能
为什么要最佳实践? 因为你的项目并不是一个特殊的项目
如果你或者你同事在用C++编程,他们往往是在乎性能的,否则他们会用一些其它的语言。我经常去一些公司他们都告诉我他们的项目是特殊的,因为他们想要快速完成功能。
警告:他们都在出于相同的原因做着相同的决定。很少有例外, 例外者其实都是那些已经遵循这本书的组织。
你需要一个单独的指令去跑所有的测试,如果你没有自动测试, 没有人会跑这些测试的:
Ctest 是一个Cmake下用来测试的runner, 它可以很好地利用 Cmake的 add_test 特性。你需要很熟悉测试工具、搞清楚他们是怎么做的、并且从他们中选择一个。
没有自动化的测试, 这本书的其它部分就是没有意义的,在重构代码的时候,如果你不能验证你没有破坏现有的代码, 你就不能使用这些最佳实践。
Oleg Rabaev 说过:
==如果一个部分是很难测试的, 那它肯定没被设计好。如果一个组成部分是容易测试的, 那就意味它是有被很好的设计的。反过来说,一个好的设计,应该是一个容易被测试的设计。==
没有自动化的测试, 那就很难去保证代码的质量。在我生涯中的一些C++ 项目中,我的C++项目支持各种操作系统、各架构的组合。当你开始在不同平台、架构上组合不同的编译器的时候, 那就很有可能出现在一个平台能够使用在另外一个平台上不能使用的情况。为了解决这个问题, 使用带有持续测试的持续构建工具吧。
有许多警告你也许都没有在使用, 大多数警告实际上都是有好处的。-Wall 并不是GCC和Clang提供的所有warning, -Wextra仍然只是warning中的冰山一角。
强烈考虑使用 -Wpedantic(GCC/Clang) 以及 /permissive(msvc) , 这些编译选项能够进制语言拓展项,并且让你更加接近于C++ 标准, 你今天开启越多的warning, 你以后就越容易迁移到其它平台。
静态分析工具可以在不编译和运行你的代码的情况下来分析你的代码, 你的编译器实际上就是这么一个工具并且是你的第一道代码质量防线。许多这种工具都是免费的并且是开源的, CPPCheck 和clang-tidy是两种流行并且免费的工具, 大多数IDE和编辑器都能够支持。
sanitizers是集成在gcc clang msvc下的一种运行时分析工具。如果你比较熟悉Valgrind, sanitizers提供了相似的功能但快了好几个数量级。
Address sanitizer, UB Sanitizer, Thread sanitizer 可以找到很多像魔法一样的问题, 在写这本书的时候,msvc正在支持越来越多的sanitizer, gcc和clang 拥有更多对sanitizer的支持。
John Reghr 推荐在开发的时候永远开启ASan( Address Sanitizer) 和UBSan(Undefined Behaviour Sanitizer) 。
当类似于内存溢出的错误发生时, sanitizer 将会给你一个报告告诉你什么情况下会导致程序失败, 通常还会给出修复问题的建议。
你可以用如下的命令开启ASan 和UBSan:
gcc -fsanitize=address,undefined <filetocompile>
在C++中,一个问题往往有很多解决方案, 对于哪种方案是最优的有若干个观点存在。从被人的项目里面复制粘贴代码是非常容易的, 使用你觉得最满意的的方案去解决问题也是非常容易的(但这两种都是要避免的), 要注意了:
如果解决方案似乎很复杂很大,停下。这时候是一个很好的时间去走走然后好好思考这个方案。当你结束了散步, 把你的设计方案和同事讨论一下,或者用橡皮鸭思考方法讲出来(++所谓橡皮鸭方法就是你对着一个橡皮鸭一五一十地说出你的思考过程和逻辑,从中可以察觉到自己的思维漏洞等++) 如果你还没有发现一个直接的解决方法?去twitter上面问问吧。最关键的其实是不要盲目地去用你自己觉得满意的方案去解决问题, 要多停一段时间多思考。随着年龄的增长,我花在编程的时间越来越少,但是思考的时间却越来越多, 最后我实现解决方案的速度比以前越来越快,并且越来越简单。
这部分只是提醒你我们可以对C++的各个方面进行推理, 它并不是一个黑盒,也并不是一个魔法。
如果你有问题, 你通常可以很简单地去写个实验代码,它将自己回答你的这些问题。
Bjarne Stroustrup 在"The C++ Programming Language" 的第三版里面讲到:
==C++ 是一个通用的编程语言但是偏向于系统编程, 它比C更好, 支持数据抽象, 支持面向对象, 支持泛型编程。==
你必须明白C++ 是一种多科目语言, 它很好的支持了当下的所有编程范式:
考虑到C++并不是一个纯面向对象的语言, 你必须掌握一些其它技术才能更好地使用C++,最好学会一门函数式语言比如Haskell.
许多大佬说过这很多次了, 让一个对象修饰为const 有两个好处:
它迫使我们去思考这个对象的的初始化和生命周期, 这会影响程序的性能。可以向代码的读者传达这是一个常量的意义 另外,如果它是一个static对象,编译器现在可以自由地将它移到内存的常量区,这可能会影响优化器的行为。
使用# define的日子早就一去不复返了, constexpr应该成为你新的默认选择。很不幸的是, 人们总是过份高估constexpr的复杂程度, 所以就让我们把它拆解成最简单的东西说起吧~
如果你看到像这样的一些东西:
在编译时期,数据就已经知道是static const 对象了:
static const std::vector<int> angles{-90,-45,0,45,90}
那这种情况就需要把它变成:
static constexpr std::array<int,5> angles{-90,-45,0,45,90}
static constexpr 在这里可以保证对象不会在每次这个函数/声明遇到时都会重新初始化。由于static修饰了这个变量, 它将存在于整个程序的生命周期, 而且我们知道它不会被初始化两次。
这两个代码的区别有三:
我其实并不是一个永远使用auto的人, 但是让我来问你一个问题:std::count 函数返回的类型是什么?
我的回答是:我不在乎。
const auto result = std::count( /* stuff */ );
使用auto 避免没有必要的转换和数据丢失, 同样的场景经常在range-for 循环中出现,再举个栗子:
C++
有可能发生代价很高的转换
const std::string value = get_string_value();
get_string_value()的返回类型是什么?如果它是一个std::string_view或者是一个const char *, 我们将会有一个潜在的高代价的类型转化。
不可能有高代价的性能转化的写法:
// 避免类型转化
const auto value = get_string_value();
另外, auto作为返回值实际上可以大幅度简化泛型代码:
// C++98 template usage
template<typename Arithmetic>
Arithmetic divide(Arithmetic numerator, Arithmetic denominator) {
return numerator / denominator;
}
这个代码强制我们分子的类型和分母的类型都是同一种类型,非常难受。但在C++98 里面怎么实现分子分母不同类型呢?
template<typename Numerator, typename Denominator>
/*what's the return type*/
divide(Numerator numerator, Denominator denominator) {
return numerator / denominator;
}
可以看到,我们没有办法提供返回值的类型, C++98没有提供这种问题的解决方法,但是C++11通过尾置返回类型可以做到:
// use trailing return type
template<typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator)
-> decltype(numerator / denominator)
{
return numerator / denominator;
}
但是在C+14里面, 我们还可以把返回类型给省去,
// use trailing return type
template<typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator){
return numerator / denominator;
}
我会用几个例子来说明这一点:
当循环时, int和 std::size_t 的问题
for (int i = 0; i < container.size(); ++i) {
// 糟糕,i 实际上不是int类型,是size_t类型,有类型不一致的问题
}
当循环时, 容器类型不一致的问题:
for (auto itr = container.begin();itr != container2.end();++itr) {
// 哦,我们大多数人都有过这种经历
}
使用ranged-for的例子
for (const auto &element : container) {
// 消除了以上的两种问题
}
注意:永远不要在用ranged-for的时候修改容器自身
不使用auto会让你更容易不经意间犯一些错误:
意外的类型转换:
for (const int value : container_of_double){
// 意外的转换,很有可能会报warning
}
意外的继承切片问题
for (const Base value : container_of_Derived){
// 意外的发生了继承切片问题
}
正确做法
for (const auto &value : container){
// 不会发生意外的问题
}
优先考虑:
算法能够传达更多的含义并且能够符合”const 一切" 的**, 在C++20 中, 我们有ranges, 这会使得算法用起来更加舒服。
使用函数的方式并且配合使用算法, 这会使C++ 的代码读起来更想是一个句子。
比如,检查一个container内是否有一个大于12的数:
const auto has_value = std::any_of(begin(container), end(container), greater_than(12));
在极少数情况,编译器的静态分析工具可以能够提示你有现有的算法能够使用。
模板能够表现 C++ 中的DRY原则(Dont repeat yourself) , 模板它可能是复杂的,令人生畏的, 并且是图灵完备的, 但是它们也不必如此。15年前, 业界似乎有一个盛行的态度是:“模板就不是给正常人写的"。
幸运的是, 这种观点放在今天越来越不正确了, 现在我们用更多的工具:concepts, generic lanbdas 等等。
我们将在接下来写一个简单的例子。假设我们想要写一个函数它可以除任意两个值:
// 除两个double 类型的数
double divide(double numerator, double denominator){
return numerator / denominator;
}
// 你不希望分子的类型被提升到double类型:
float divide ( float numerator, float denominator){
return numerator / denominator;
}
// 当然,你还想两个int类型的相除
int divide ( int numerator, int denominator){
return numerator / denominator;
}
// template 就是为了这种情况而设计的:
// 最基础的template
template<typename T>
T divide(T numerator, T denominator){
return numerator / denominator;
}
// 大多数的例子都用T来表示, 就像我刚才做的那样,但是不要这么做,给你的类型一个有意义的名字:
template<typename Arithmetic>
Arithmetic divide(Arithmetic numerator, Arithmetic denominator){
return numerator / denominator;
}
如果你发现自己正在选中一块代码并且复制它, 停下!
后退一步并且再看下这些代码:
我发现这个简单的条例可以对我的代码质量有着最直接的影响, 如果我们即将在当前的函数中进行粘贴一段代码, 那就考虑使用lambda 。C++14 的lambda 配合上泛型参数(也叫auto),可以让你更加容易写出可复用的代码还不要处理template 语法。
在正确的情况下,没有析构函数总是更好的。空的析构函数会损失一部分性能,并且:
如果你需要提供一个自定义的析构函数,那你必须同时对其它特殊成员函数进行 =delete, =default,或者实现它们。这个规则一开始叫“三法则”, 在C++11后变成了“五法则”
// 特殊的成员函数
struct S {
S(); // default constructor
// does not affect other special member functions
// If you define any of the following, you must deal with
// all the others.
S(const S &); // 拷贝构造
S(S&&); // 移动构造
S &operator=(const S &); // 拷贝赋值
S &operator=(S &&); // 移动赋值
};
当你不知道怎么去处理它们的时候,=delete 对于这些特殊的成员函数来说是一个非常安全的处理方法.
当你在声明带有虚函数的基类时,你也应该遵循”五法则“:
struct Base {
virtual void do_stuff();
// because of the virtual function we know this class
// is intended for polymorphic use, therefore our
// tools will tell us to define a virtual destructor
virtual ~Base() = default;
// and now we need to declare the other special members
// a good safe bet is to delete them, because properly and safely
// copying or assigning an object via a reference or pointer
// to a base class is hard / impossible
S(S&&) = delete;
S(const &S) = delete;
S &operator=(const S &) = delete;
S &operator=(S &&) = delete;
};
struct Derived : Base {
// We don't need to define any of the special members
// here, they are all inherited from `Base`.
}
现在我们知道有很多未定义的行为是很难追踪的, 在接下来的几节中我将给出一些例子。最重要的事情是你需要理解,未定义行为的存在会破坏你整个程序。
一个符合规范的实现在运行一个格式良好的程序时,应该产生可以观测的行为,相同的程序和相同的输入应该产生与之相对应的行为。
但是, 如果一个程序中包含着一个未定义的行为, 这段代码对执行输入的程序也就没有要求。(甚至对第一个未定义操作之前的操作也没有要求)
如果你有未定义的行为, 整个程序的执行就会变得很诡异。
int Class::member() {
if (this == nullptr) {
// removed by the compiler, it would be UB
// if this were ever null
return 42;
} else {
return 0;
}
}
严格意义上说,这并不是对未定义行为的校验。但是这个校验不可能会失败, 如果this等于nullptr, 那你将会处于一个UB行为的状态中。人们过去经常经常这么做,但这永远是一个UB行为。你不能从一个对象的生命周期以外去访问一个对象。从理论上来说, this=null的唯一可能就是你在调用一个null对象的成员。
int get_value(int &thing) {
if (&thing == nullptr) {
// removed by compiler
return 42;
} else {
return thing;
}
}
不要尝试它,这是一个UB行为, 永远认为引用所指向的是一个存在的对象, 在你设计API时可以合理使用这一点。
这个问题可以用以下一系列的例子来阐明, 从这个开始:
enum class Values {
val1,
val2
};
std::string_view get_name(Values value) {
switch (value) {
case val1: return "val1";
case val2: return "val2";
}
}
如果你开启了所有warning, 你将会得到一个"not all code paths return a value" 的警告,这从技术上说是正确的。
我们可以调用 get_name(static_cast(15)) , 这并不违法任何C++的规定, 但函数不返回任何值这一点是个UB行为。
你可以尝试这样修复代码:
enum class Values {
val1,
val2
};
std::string_view get_name(Values value) {
switch (value) {
case val1: return "val1";
case val2: return "val2";
default: return "unknown";
}
}
但是呢这样会引入一个新的问题:
enum class Values {
val1,
val2,
val3, // 这里增加一个值
};
std::string_view get_name(Values value) {
switch (value) {
case val1: return "val1";
case val2: return "val2";
default: return "unknown";
}
// 编译器不会诊断出来val3没有处理
}
实际上,我们更倾向于这样的代码:
enum class Values {
val1,
val2,
val3, // 这里增加一个值
};
std::string_view get_name(Values value) {
switch (value) {
case val1: return "val1";
case val2: return "val2";
} // 没有处理的enum 值这里会有warning
return "unknown"
}
C++11 引入了带作用域的枚举值, 目的是为了解决很多从C继承过来的问题。
C++98 enums
enum Choices {
option1 // value in the global scope
};
enum OtherChoices {
option2
};
int main() {
int val = option1;
val = option2; // no warning
}
enum Choices 和OtherChoices它们俩很容易被搞混, 并且它们引入了全局命名空间下的标识符。
在这些枚举类中的值都是带有限定作用域的,并且更加强类型。
C++11 scoped enumeration
enum class Choices {
option1
};
enum class OtherChoices {
option2
};
int main() {
int val = option1;
int val2 = Choices::option1;
Choices val = Choices::option1;
val = OtherChoices::option2;
}
这个带enum class的版本没有花太多功夫就可以让它们不那么容易被搞混, 并且它们的标识符现在是带作用域的,并不是全局的。
enum Struct和 enum class 其实是等价的,只是逻辑上enum struct更加合理, 因为它的成员是public公开的。
SFINAE 是一种很难读懂的代码(++不懂SFINAE的参考知乎的这个文章文章) , if constexpr 没有SFINAE那么灵活,但是当你能用它的时候就尽量用它.
我们来看一下之前在Prefer auto in Many Cases的文章中举的相除的例子:
template<typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator)
{
return numerator / denominator;
}
好, 那当我们要做整数相除的时候, 我们现在想要加一个不同的行为该怎么做呢?在C++17以前, 我们会使用SFINAE("Substitution Failure is not An Error") 这个特性。基本意思就是说, 如果一个函数无法编译, 那它就会从重载解析中删除。举个例子:
SFINAE 版本的删除
#include <stdexcept>#include <type_traits>#include <utility>
template <typename Numerator, typename Denominator,
std::enable_if_t<std::is_integral_v<Numerator> &&
std::is_integral_v<Denominator>,int> = 0>
auto divide(Numerator numerator, Denominator denominator) {
// is integer division
if (denominator == 0) {
throw std::runtime_error("divide by 0!");
}
return numerator / denominator;
}
template <typename Numerator, typename Denominator,
std::enable_if_t<std::is_floating_point_v<Numerator> ||
std::is_floating_point_v<Denominator>,
int> = 0>
auto divide(Numerator numerator, Denominator denominator) {
// is floating point division
return numerator / denominator;
}
C++17 的 if constexpr 语法可以简化这个代码:
#include <stdexcept>#include <type_traits>#include <utility>
template <typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator) {
if constexpr (std::is_integral_v<Numerator> && std::is_integral_v<Denominator>) {
// is integral division
if (denominator == 0) {
throw std::runtime_error("divide by 0!");
}
}
return numerator / denominator;
}
注意,if constexpr块中的代码在语法上仍然必须是正确的. if constexpr 跟#define是不一样的。
Concepts比起SFINAE会给你带来更好的报错信息以及更快的编译时间, 另外还会比SFINAE有更好的可读性。我们继续构建我们上一章的例子 , 这是上一章用if constexpr 写出来的代码:
#include <stdexcept>#include <type_traits>#include <utility>
template <typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator) {
if constexpr (std::is_integral_v<Numerator> && std::is_integral_v<Denominator>) {
// is integral division
if (denominator == 0) {
throw std::runtime_error("divide by 0!");
}
}
return numerator / denominator;
}
用concept我们可以把它分解成两个不同的函数。Concepts可以在许多不同的场景下使用, 这个版本在函数声明以后用了一个简单的requires从句:
#include <stdexcept>#include <type_traits>#include <utility>
// overload resolution will pick the most specific version
template <typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator) requires
(std::is_integral_v<Numerator>&& std::is_integral_v<Denominator>) {
// is integral division
if (denominator == 0) {
throw std::runtime_error("divide by 0!");
}
return numerator / denominator;
}
template <typename Numerator, typename Denominator>
auto divide(Numerator numerator, Denominator denominator) {
return numerator / denominator;
}
这个版本用了concepts作函数参数, C++20 甚至还有“auto concept" , 这是个隐式模板函数。
#include <stdexcept>#include <concepts>
auto divide(std::integral auto numerator,
std::integral auto denominator) {
// is integer division
if (denominator == 0) {
throw std::runtime_error("divide by 0!");
}
return numerator / denominator;
}
auto divide(auto numerator, auto denominator){
return numerator / denominator;
}
尽可能地将代码移出模板之外, 使用其它函数或者使用基类都可以,编译器仍然可以内联它们。(并不是不用模板,而是模板内的代码尽可能移出去)
去模板化可以提高编译速度并且减少二进制文件的大小,二者都很有用。它还可以消除模板膨胀。
每次函数模板实例化时都会生成一个新lambda函数
template<typename T>
void do_things()
{
// this lambda must be generated for each
// template instantiation
// 这个lambda函数在每个模板实例化的时候都会被生成一次
auto lambda = [](){ /* some lambda that doesn't capture */ };
auto value = lambda();
}
与之相对应的是
auto some_function(){ /* do things */
template<typename T>
void do_things()
{
auto value = some_function();
}
现在只编译了一个版本的内部逻辑,编译器决定它们是否应该内联。在基类和模板派生类中会用到相似的技巧。
与“去模板化"相同的道理, 这是在异常处理中的一个DRY(Dont't repeat yourself) 原则。如果你有很多异常类型需要处理, 你可能会写出如下代码:
void use_thing() {
try {
do_thing();
} catch (const std::runtime_error &) {
// handle it
} catch (const std::exception &) {
// handle it
}
}
void use_other_thing() {
try {
do_other_thing();
} catch (const std::runtime_error &) {
// handle it
} catch (const std::exception &) {
// handle it
}
}
Lippincott 函数提供了一种中心化的异常处理套路:
void handle_exception() {
try {
throw; // re-throw exception already in flight
} catch (const std::runtime_error &) {
} catch (const std::exception &) { }
}
void use_thing() {
try {
do_thing();
} catch (...) {
handle_exception();
}
}
void use_other_thing() {
try {
do_other_thing();
} catch (...) {
handle_exception();
}
}
这个小技巧不是啥新东西, 它早在C++98时就可以使用了。
对全局状态进行推理是很难的, 任何非const 的static值或者std::shared_ptr 都可能是一个潜在的全局状态, 你永远不知道谁可能会更新这个值或者它是否是线程安全的。
当一个函数改变了一个全局状态时,它会导致不易察觉的并且很难去追溯的bug, 另一个函数要么会依赖这个变化,要么会受到它的负面影响。
你的接口是你的第一道防线, 如果你提供了一个很容易用错的接口, 你的用户就会错误地使用它。如果你提供了一个很难用错的接口, 你的用户就会很难去把它用错。但这是C++, 他们总能找到办法的。
设计一个很难用错的接口有时会导致代码的冗长, 你必须选择哪一个是最重要的, 是正确的代码还是短的代码?
你是否接受一个空指针?它是否是个可选参数?如果一个nullptr传入你的函数会发生什么?如果一个异常范围的值传入你的函数会发生什么?有些开发者会在内部接口和外部接口里面做个区分, 他们允许一些不安全的API在内部接口中使用。
不过你能保证外部的使用者永远不调用内部的API嘛?你能保证内部使用者永远不误用API嘛?
[nodiscard] 是一个C++的特性,它告诉编译器如果返回值被放弃了则需要警告,
它可以用在函数上:
[[nodiscard]] int get_value();
int main()
{
// warning, [[nodiscard]] value ignored
get_value();
}
可以用在类型上:
struct [[nodiscard]] ErrorCode{};
ErrorCode get_value();
int main()
{
// warning, [[nodiscard]] value ignored
get_value();
}
考虑一下POSIX socket的API
socket(int, int, int);
每个参数代表:
Rectangle(int, int, int, int); 这个函数可能是指(x,y,width, height), 也有可能是指(x1, y1, x2, y2), 或者不太可能但是仍然有可能出现的是含义是(width, height, x, y)
那你认为下面这个API怎么样呢?
强类型API
struct Position {、
int x;
int y;
};
struct Size {
int width;
int height;
};
struct Rectangle {
Position position;
Size size;
};
Rectangle(Position, Size);
这可以延伸出来其它的具有操作符重载的组合语句:
// Return a new rectangle that has been
// moved by the offset amount passed in
Rectangle operator+(Rectangle, Position);
避免bool类型的参数
这章的预发布读者指出, steve Maguire 在他的《编写可靠的代码》的第五章中说过:"要让代码在调用时易于理解“。在C++11中, enum class 提供给了你一种很容易的方法去添加强类型,从而避免布尔类型的参数, 这会使你的API更难用错。
考虑以下代码:
参数顺序不明显
struct Widget {
// this constructor is easy to use wrong, we
// can easily transpose the parameters
Widget(bool visible, bool resizable);
}
与如下代码相比:
强类型带作用域的枚举值
struct Widget {
enum struct Visible { True, False };
enum struct Resizable { True, False };
// still possible to use this wrong, but MUCH harder
Widget(Visible visible, Resizable resizable);
}
返回一个裸指针会让读者和使用者很难去思考明白它的所有权。选择引用智能指针, 非归属指针包装器(?++没明白,原文是non owning pointer wrapper++) ,或者考虑可选引用(++没明白是啥,原文是optional reference++)。
返回一个裸指针的函数示例
int *get_value();
谁拥有这个返回值?是我吗?当我用完它,我是否需要去delete掉这个指针?
或者考虑一种更坏的情况, 如果这个内存使用malloc来分配的,我是不是需要调用free来释放它?
这是个指向单个int值的指针,还是一个int数组?
这个代码有太多问题了, 甚至用[[no discard]]都不能帮助我们
栈对象(非动态分配的作用域在本地的对象)对于优化器更加友好、缓存更加友好、并且可能被优化器完全删除。正如Björn Fahller所言, ”假设任何指针间接指向都是一次cache miss”
用最简单的话来说:
ok,用栈并且这可以被优化
std::string make_string() {
return "Hello World";
}
不好, 使用了堆
std::unique_ptr<std::string> make_string() {
return std::make_unique<std::string>("Hello World");
}
OK
void use_string() {
// This string lives on the stack
std::string value("Hello World");
}
非常糟糕的写法, 用了堆还内存泄露
void use_string() {
// The string lives on the heap
std::string *value = new std::string("Hello World");
}
记住,std::string本身也会在内部分配内存, 并且用的是堆, 如果你的目标是不用任何堆, 你需要用一些其它方法来打到。其实我们的目标是不分配不必要的堆。
总体来说, 用new 创建的对象(或者用make_unique或者make_shared创建的对象) 都是堆对象, 并且拥有动态存储周期(Dynamic Storage Duration), 在本地作用域下创建的对象都是栈对象, 并且具有自动存储周期(Automactic Storage Duration)
你已经避免使用堆并且使用智能指针来管理内存资源了对吧。再进一步, 在少数一些你需要用堆的情况下,请确保使用 std::make_unique<>()(C++14),在很少见的情况下你需要共享对象的所有权,这时候使用std::make_shared<>()(c++11)
优先以这种顺序选择你的容器:
std::array
一个固定大小的分配在栈上的连续容器, 数据的多少必须在编译时期知道, 你必须拥有足够的栈空间去承载数据。这个容器可以帮助我们优先使用栈而不是堆。已知的位置以及内存连续性会让std::array<>是一个“负成本抽象"(negative cost abstraction)", 因为编译器知道数据的大小和位置, 它可以用额外的一系列优化手段来优化。
std::vector
一个动态大小分配在堆上的连续容器, 尽管编译器不知道数据最终会驻留在哪里, 但他知道元素的在内存中是紧密布局的。内存的连续性给了编译器更多的优化空间并且对缓存更加友好。
几乎任何其它事情都需要评论和解释原因, 对于小型的容器, 带有线性搜索的map可能比std::map性能要更好。但是别对这点太痴迷了, 如果你需要kv查找, 用std::map并且评估一下它是否有你想要的性能表现和特性。
尽管编译器继续提升,优化器也在继续解决这些类型的复杂性, 这些仍然有很有可能去增加编译时间和运行时间的开销。C++14 的lambda, 具有广义的捕获表达式功能, 能够做到和std::bind同样的事情。
用std::bind去改变参数的顺序
#include <functional>
double divide(double numerator, double denominator) {
return numerator / denominator;
}
auto inverted_divide = std::bind(divide, std::placeholders::_2,std::placeholders::_1);
用lambda去改变参数的顺序
#include <functional>
double divide(double numerator, double denominator) {
return numerator / denominator;
}
auto inverted_divide = [](const auto numerator, const auto denominator) {
return divide(denominator/numerator)
}
如果你在正在转向”现代C++", 请跳过Cpp11版本, Cpp14修复了许多Cpp11的漏洞。
其中语言层面的特性包括:
库层面特性包括:
Initializer List在C++里面是一个重载项, Initializer Lists被用于直接初始化数值。 initializer_list被用于向函数或者构造器传入一个value list。
接下来举几个例子讲一下initializer_list的一些异常行为(摘自本书作者的youtube上的讲解视频:
auto f(int i, int j, int k){
return std::initializer_list<int>{i, j, k};
}
int main() {
int argc = 1;
for (int i: f(argc+1, argc+2, argc+3)){
std::cout << i << ",";
}
}
最终的结果实际上是个UB行为, 因为上面这段代码的函数f等价于
auto f(int i, int j, int k){
return std::initializer_list<int>{i, j, k};
}
auto f(int i, int j, int k){
const int __a[] = {i, j, k};
return std::initializer_list<int>{__a, __a+3}; // pointer local
}
因此同理,下面这段代码是没有办法编译成功的(因为unique_ptr不能转让所有权):
#include <vector>#include <memory>
std::vector<std::unique_ptr<int>> data{
std::make_unique<int>(40), std::make_unique<int>(2)
};
裸make file或者visual studio的项目文件让上面列出的每个东西都很棘手并难以去实现。使用build tool工具去帮助你维护在不同平台和编译器之间的可移植性。对待你的build script就想对待你的其它code一样, 它们也有自己的一套best practise, 并且非常容易就写出一个不易维护的build sciprt, 就像写出一个不可维护的C++代码一样。在使用cmake --build的情况下, Build generators同时也可以帮助抽象和简化你的持续集成环境, 这样无论你在用什么平台开发,都可以做出正确的事情。
最近几年开发者对C++的包管理工具表现出了浓厚的兴趣, 有两个成为了其中最著名的包管理工具:
使用一个包管理工具是绝对有好处的, 包管理工具可以提高可以提高可移植性并且降低开发人员的管理成本。
对于减少构建时间带来的痛苦,有以下一些很实用的建议:
使用IDE
我所观察到的使用现代IDE最令人惊喜的一个特性就是:IDE对你的代码做了实时分析。实时分析就意味着你在键入代码的时候编译器就知道它是否要编译,因此你会花上更少的时间去等待构建。
在你的平台上你至少要支持两种编译器。每个编译器都会做不同的分析并且以一种略微不同的方式来实现标准。如果你使用Visual Studio,你应该能够在clang和cl.exe之间灵活切换。你还可以使用WSL并且开启远程linux 构建。
如果你使用linux系统,你应该能够在GCC和Clang之间能够灵活切换。
注意, 在macOS上,确保你在使用的编译器是你想使用的那个,因为gcc 命令很可能是一个苹果公司安装的clang的软连接
你的想象力限制了你能够构建的测试用例, 你是否有尝试恶意调用API?你有故意去传入一些格式错误的数据给你的输入吗?你是否处理一些未知或者未经信任的数据来源?
在所有可能的情况组合中为所有可能的函数调用生成所有可能的输入是不可能的, 很幸运的是,有工具来为我们解决这些问题。
模糊测试
模糊测试工具能够生成各种长度的随机字符串,这种测试能够约束你用合适的方法去处理这些数据, 模糊测试工具分析那些从你的测试执行过程中生成出来的覆盖率数据并且使用那些信息去删除多余的测试并且产生新且特殊的测试用例。
理论上来说,如果给它足够的时间,模糊测试可以对你需要测试的代码最终达到100%的代码覆盖率。结合AddressSanitizer, 它可以成为寻找你代码中bug的强有力的工具。在一个很有趣的文章里面描述了模糊测试工具和AddressSainitzer的组合是如何在小于6个小时内发现OpenSSL的安全漏洞的。
变异测试
编译测试是通过修改你代码里的条件和变量来进行测试工作的,举个例子:
bool greaterThanFive(const int value) {
return value > 5; // comparison
}
void tests() {
assert(greaterThanFive(6));
assert(!greaterThanFive(4));
}
编译测试能够修改你的常量5或者>运算符, 所以你的code变成:
bool greaterThanFive(const int value) {
return value < 5; // mutated
}
任何能够继续通过的测试用例都属于"存活下来的变异测试用例", 这就预示着要么你的代码存在bug要么这是一个有缺陷的测试。
如果你想变得更强你就要持续不断地学习, 世面上有许多你可以在你的C++学习上使用在资源。(++若干年后, 你就会发现,你变禿了,也变强了++)
本部分我们来详述基础的数据结构,如栈、队列、字典、元组、树、链表等。
“把学习带到现实中,让孩子用自己的力量创造改变,可以直接提升他们的幸福感和竞争力。”
这是“全球孩童创意行动”的发起人——Kiran Sethi在TED演讲时说的一句话,这个行动旨在引导中小学生主动寻找现实问题,并创造性地解决它。这种能力对于今天的孩子来说,可谓至关重要,世界经济论坛今年发布了“2020年人才市场最看重的10项技能”,就把“Complex Problem Solving(解决复杂问题)”放在了第一位。
而世界上所有注重创新教育的国家和地区,也都极其注重训练孩子解决现实问题的能力。具体如何训练呢?欧美学校都钟爱一个名为“设计思维”(Design Thinking)的创造力训练方法。
“设计思维”发源于设计界,后来被各行各业借鉴,斯坦福大学设计学院把它归纳成一套科学方法论后,迅速风靡全球高校和中小学。它一共分为下图这5个步骤,引导孩子们以“人的需求”为中心,通过团队合作解决问题,获得创新。
同理心(Empathy):收集对象的真实需求
定义(Define):分析收集到的各种需求,提炼要解决的问题
头脑风暴(Ideate):打开脑洞,创意点子越多越好
原型制作(Phototype):把脑子中的想法动手制作出来
测试(Test):优化解决方案
有人把这种方法类比为一本菜谱,告诉你烧菜的步骤、烧的时间等,虽然每个人用它炒出来的东西都不一样,但只要跟着这本菜谱仔细做,一般都不会做得太难吃。本文将把5大步骤逐一介绍,形成一份详细的“设计思维攻略”,也可称为“创造性解决问题攻略”,值得为孩子收藏。
▋一、学会用“同理心”思考问题,而非“同情心”
设计思维的第一步,是建立“同理心”,这是一种设身处地体会他人感受的思考方式,和“同情心”有本质不同,比如有个人掉到山洞了,有“同情心”的人会说,“你好可怜。”但有“同理心”的人会说,“洞里这么冷,你一定不好受。”
一个经典的同理心练习是:对着别人,用你左手的食指和右手的食指,摆一个“人”字。你发现了什么呢?——你得摆一个“入”字。
有2个常见方法,可以帮助孩子快速进入“同理心”模式:
方法1:角色扮演
了解一个人最好的方法,就是成为那个人。“角色扮演”让人们得以亲身体验对象的处境,用感同身受,代替主观臆测。这种思考,比先入为主的“我以为”,要更深刻。
举个例子。我们经常鼓励孩子参与公益,帮助社会上有需要的人,却极少引导孩子思考“我给别人的帮助,真的是别人需要的吗?”比如灾区的人们更需要食物和被褥,我们却一厢情愿给人家寄去旧衣服(有些甚至都没有消毒干净),到头来也只是感动了自己。
怎么给特殊群体更好的帮助呢?离我们很近的香港理工大学的学生就曾面临这个问题,他们要为盲人做设计,动手之前,他们做了一个“一小时盲人体验”——蒙上眼睛,户外探索一小时。期间,学生们互相搀扶着上下楼梯,偶尔会碰上树木,在几个空间里探索前行,聆听声音,触摸不同的材质等。
整个过程中,不时有学生发出呼救声,但慢慢地,他们安静下来,把自己当成盲人,去思考一些问题,并决定,“与其设计一些东西帮助他们,不如设计一些东西让他们享受正常生活。”下图是其中一位学生,Kevin Chan设计的“盲人用的跑步机”:提供了一个安全的跑步地带,模仿户外的情景。
“亲身体验”的经历,也更能激发孩子们解决问题的动力。
比如印度排名前十的河沿小学的五年级学生,有一堂课叫“儿童权益”,学习这门课时,学生们就被要求制作庙里所用的香,连续工作八个小时,以体验童工的境遇。
当孩子连续工作了两个小时后,他们后背酸痛,连续工作一天后,他们的心态改变了——自发走上街头,用切身感受说服城市里的每一个人:童工制度必须被禁止!他们和企业家、工头诚恳对话,表达自己这一愿景,后来成功说服大量作坊停止采用童工。
方法2:采访
采访是记者的基本功,但从小掌握采访的技巧,能让孩子更高效地收集自己想要的信息。而采访最看重的,就是提问的技巧。如何提问呢?最基本的原则是“5个W,1个H”:
Who?谁?
Doing What?做什么?
When?什么时候?
Where?在哪儿?
Why?为什么?
How did it look?How did you feel?长什么样子?你什么感受?……
两个常用的技巧是:
寻找不一致,用得体的方式指出人们说话、做事前后不一致的地方;
注意语言之外的信号,比如肢体语言和情绪。
采访的技巧,美国男孩Logan La Plante就玩得很溜。他从小在家上学,但学习面和知识深度比同龄人领先很多,奥秘就是他经常拜访一些网络科技公司,免费在那里工作,采访专业人士;晚上则请一个不同行业的陌生人喝咖啡,接触不同领域的知识……
▋二、学会发现真实问题,并且重新定义它
爱因斯坦曾说,“如果只给我一个小时拯救地球,我会花59分钟找准核心问题,然后用1分钟解决它。(If I were given one hour to save the planet, I would spend 59 minutes defining the problem and one minute resolving it.)”设计思维的第二步,就是要“定义正确的问题”。
它的意思是,明确问题到底出在哪里,并用一句很精简的话告诉别人,“你想如何解决什么问题”。有一个常用句式——“How might we……”(我们该如何……),下图就是个很好的例子:
我们该(如何),为(谁),做点(什么),好解决(什么问题)
我们该(如何),为(会员),填平他们在文化和技术上的差距,好(促进大家的交流)
再举个例子。美国“天赋教育”(相当于我们的尖子班)有一道经典的领导力训练题:
“假设在2097年,你和你的宇宙飞船机组成员正执行一次百年宇宙飞行的使命。着陆时,却遇到了问题:你们降落在错误的着陆点上。在测量设备全毁的情况下,你将如何率领团队走出绝境?”
套用“How might we”句式,问题就从“我如何率领团队走出绝境”,聚焦成了“我如何收集尽可能多资料,尽快确定地理位置,联系救援”。
▋三、学会提出更多更有创意的解决方法
上面两步——“同理心——定义”是“发现问题”,从“头脑风暴”开始,孩子进入“解决问题”阶段。
所谓“头脑风暴”,要求孩子尽可能多地写下脑海中一闪而过的创意点子,不拒绝任何疯狂的想法。但它可不是让孩子坐在那里苦思冥想,下面是一些常见的练习,能让我们打破惯有思维的局限。
方法1:黄金60秒
看看下图左上角这坨蓝色,你会联想到什么呢?云朵?水杯图案?水渍?……给自己60秒,然后写下你想到的所有可能。
“头脑风暴”经常会设置一个很短的时间,给人们紧迫感。少年商学院曾受新加坡国家设计中心邀请,带领**孩子前往参加设计思维工作坊,期间,学生们就被要求,根据采访的收获,在30分钟内想出100个方法,解决他们锁定的对象正在面临的问题……头脑风暴后,又要快速筛选创意,几十个创意经过小组讨论和筛选,只留下了1-2个,成为设计方案的核心。
方法2:自由联想
我们曾分享过现在很流行的思维工具——思维导图的使用指南(点此获取),其中有一张“树状图”就特别适合用来收集发散的想法,比如,设置中心词为“月圆”,你能想到什么呢?写在旁边。
方法3:强制类比
和“自由联想”不同,“强制类比”选中的事物往往看上去八竿子打不着,但越是天差地别,摆到一起还一定要找出相同点时,结果越是有趣。思维导图中的“双重气泡图”就很适合用来做这类梳理,比如下图,把灯泡和球鞋放在了一起:
强制类比能给我们很多灵感,比如锯子借鉴了茅草边的锯齿,雨伞借鉴了香菇的形状。
▋四、学会试错:把脑子里的想法“拿”出来试错优化
最后两个步骤——“原型制作”和“测试”,经常是结伴出现的。
所谓“原型制作”,就是动手把脑子中的想法制作成一个看得见摸得着的实体模型。不用拘泥于哪种特定的工具,只要孩子喜欢,乐高、橡皮泥、现正流行的一款游戏“Minecraft(我的世界)”都可以。
众所周知,欧美学校非常注重培养孩子动手的习惯和能力,这不仅为让孩子检验自己构想的方案是不是具有真实的可操作性,更能帮助孩子打破“完美主义”,在实践中找到优化解决方案的新思路,而不是纸上谈兵。
(今年暑假,少年商学院受到斯坦福设计学院姊妹院校——德国波茨坦大学设计学院的邀请,带领**孩子前往参加工作坊。上图为学院里的工具墙)
它的目的,是做“测试”,看看对方的问题是不是真的得到了解决。这不可能一蹴而就,不断试错的过程,也在培养孩子的耐心和抗挫折能力。
▋收获创造力,更收获创造力自信
这5个步骤是一个环环相扣的紧密的循环。比如下面这6个初中生,就运用设计思维,用6天5晚的时间,改造了ATM(自动取款机):
1、同理心:收集需求
他们首先请教了银行行长,了解“客户怎么使用ATM?他们最经常投诉的问题是什么?”、“ATM盗窃最经常发生在哪个国家?”这类专业问题;再到ATM生产基地实地考察,询问专业设计师,“互联网金融这么发达,支付宝早就代替了银行,对ATM业务会有影响吗?”
然后走上街头,采访刚使用完ATM的路人,但并不顺利,每5个人,只有1个人愿意接受采访。
2、定义:找准问题
根据上一阶段收集来的需求,他们列表区分开“我们认为他们需要的”和“他们实际需要的”。
然后再筛选出这些具体的核心问题。
3、头脑风暴:发现解决方案
对于孩子来说,用创意写满一页纸,其实不难。但有时创意没了极限,他们就请设计师给自己“泼冷水”,再次反思——“这些是你们喜欢的功能,还是用户真正需要的功能?”
5、公开展示:收集优化建议
最后,6人小组带着自己做好的模型站上了演讲台,向坐在台下的ATM设计师、银行高管演示自己的方案:他们的ATM穿了一层自动感应的光电玻璃,人操作的时候,透明玻璃变为磨砂,保护隐私;玻璃外墙还可以卖广告位,不仅让排队的人不无聊,还能为银行带来二次收入……
这6个学生是少年商学院在2014年于广州开展的“设计思维工作坊之ATM大作战”的同学。工作坊开展之前,他们大都连银行卡都没用过,更不了解ATM了,但最后他们却通过设计思维流程,非常有同理心地提出了改造ATM机的方案,让ATM制造业上市公司首席设计师和银行高管都为其点赞。其中一位同学后来把这段学习经历分享给了她申请美国高中的面试官,她现在已在心仪的高中就读。还有一位同学说,工作坊结束后回到家,“看见什么都想改造一下,很多东西都存在问题,但大人们习以为常了。”
就像文章开头说的,让孩子主动发现并解决现实中的问题,能让孩子收获“幸福感”。非常幸运的是,少年商学院获全球两大设计思维泰斗—— 美国斯坦福大学设计学院(d.school)创办人和德国波茨坦大学d.school创办人的授权与认可,向**少年普及原汁原味的设计思维方法与课程。
(少年商学院教研总监Evan与斯坦福大学d.school创办人David Kelley)
而这个过程给我们的最大的体会就是——**孩子从不缺乏创造力,缺乏的只是展示创造力的机会和自信。在这方面的提升上,“设计思维”是绝佳工具。
这个世界原本属于一群高喊“知识就是力量”的理性思考族群,但现在,它将属于一些有高的感性能力的人——他们富有创造力、具有同理心、能观察趋势以及讲述故事的动情表达能力。
——Daniel Pink,纽约时报撰稿人、美国前副总统戈尔演讲撰稿人
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。
其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。其特性如下:
lua标识符与其他C++类似,区分大小写,保留字及lua内部全局变量多以下划线+大写字母组成(如_VERSION)。
1 | true | false | nil | |
2 | while | for | repeat | |
3 | do | end | util | return |
4 | if | else | elseif | then |
5 | break | goto | ||
6 | and | not | or | |
7 | function | in | local |
默认情况下,变量总是全局的。不需要声明,赋值即创建;访问未初始化全局变量也不会出错,只是结果为 nil ;删除全局变量只需要将其赋值为nil即可。
Lua是动态类型,变量不需要类型定义,只需要变量赋值。值可以存储在变量中,作为参数传递或者结果返回。
lua中有8种基本类型:nil、boolean、number、string、userdata、function、thread、table。
数据类型 | 描述 |
---|---|
nil | 一个无效值,如一个没有赋值的变量 对于全局变量和table,执行删除作用 做比较时需要加上双引号 |
boolean | true或者false nil看作是false,数字0为true |
number | 双精度实浮点数,double 默认类型可以修改luaconf.h里的定义 |
string | 字符串,单双引号标识 可以使用[[]]表示一块多行字符串,如赋值为xml等时很有用 字符串进行算术操作,会尝试转为一个数字 字符串连接使用两个点".." #用来计算字符串长度,放在字符串前 |
function | 由c或者lua编写的函数 函数被看作第一类值(First-Class value) 可以以匿名函数的方式通过参数传递 |
userdata | 用户自定义数据,存储在变量种,代表任意应用程序或者C/C++语言库所创建的数据结构及类型,可以将任意C/C++的任意数据类型的数据(通常是struct或者指针)存储在lua变量种 |
thread | 执行的独立线路,执行协同程序 在lua中最主要的线程是协同程序(coroutine),和线程差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。 |
table | 表,关联数组,索引可以是数字、字符串或者表类型,通过构造表达式创建,如{}创建一个空表 lua的表默认初始化索引从1开始,不同于C++从0开始 不固定长度大小,新增数据自动增加长度,未初始化的table都是nil 支持[]和.点号两种索引方式 |
可以使用type函数来测试变量的类型和值的类型:
print(type(10.1*5))
print(type(print))
线程和协程的区别:线程可以同时多个运行,协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起时才能暂停。
使用前必须代码中声明,创建变量。编译程序执行代码前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。
lua有三种变量类型:全局、局部、表中的域。语句块或者函数中的均为全局变量,local显式声明方为局部变量,局部作用域从声明位置开始到所在语句块结束。应尽可能使用局部变量,以避免命名冲突,且访问局部变量速度更快。
赋值时改变一个变量的值和改变表域的最基本方法,可以多变量同时赋值,用逗号隔开依次给左侧变量赋值。先计算右侧的值然后赋给左侧变量。
循环结构是在一定条件下反复执行某段程序的流程结构,被反复执行的程序被称为循环体。能否继续重复,决定循环的终止条件。循环语句是由循环体及循环的终止条件两部分组成的。
循环类型 | 描述 |
---|---|
while循环 | 条件为true时重复执行,先检查后执行 |
for循环 | 重复执行次数在for中控制 |
repeat-until | 重复执行,直到指定条件为真 |
循环嵌套 | while do end; for do end; repeat until |
流程控制语句通过程序设定一个或多个条件语句来设定。在条件为 true 时执行指定程序代码,在条件为 false 时执行其他指定代码。
语句 | 描述 |
---|---|
if语句 | 一个布尔表达式作为条件判断,后紧跟其他语句 |
if-else语句 | if假执行else |
if嵌套 | 在if-else-if中使用if-else-if |
函数是对语句和表达式进行抽象的主要方法。既可以用来处理一些特殊的工作,也可以用来计算一些值。Lua 提供了许多的内建函数,你可以很方便的在程序中调用它们,如print()函数可以将传入的参数打印在控制台上。
Lua 编程语言函数定义格式如下:
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
lua支持多返回值,用逗号隔开;可变参数类似C语言三个点号,{...}表示可变长参数组成的数组表,也可以用select("#", ...)来获取可变参个数;select(n, ...) 返回n到select(“#”, ...)的参数。
序号 | 类型 | 操作符 | 描述 |
---|---|---|---|
1 | 算术运算符 | +、-、*、/、%、-(负号)、^(乘幂, A^2=100) | |
2 | 关系运算符 | ==、~=、>、<、>=、<= | |
3 | 逻辑运算符 | and、or、not | |
4 | 其他运算符 | 两个点号(..连接字符串),#一元操作符返回字符串长度 |
字符串由数字、字母、下划线组成。可以使用单引号、双引号、[[]]三种形式定义。不能直接显示的字符,可以使用\转义。
格式字符串可能包含以下的转义码:
为进一步细化格式, 可以在%号后添加参数. 参数将以如下的顺序读入:
数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。
迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。在 Lua 中迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。
table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil。Lua table 是不固定大小的,你可以根据自己需要进行扩容。Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用"format"来索引table string。
模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。
Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。
-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}
-- 定义一个常量
module.constant = "这是一个常量"
-- 定义一个函数
function module.func1()
io.write("这是一个公有函数!\n")
end
local function func2()
print("这是一个私有函数!")
end
function module.func3()
func2()
end
return module
Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。
-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
print(module.constant)
module.func3()
对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。
require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。
当然,如果没有 LUA_PATH 这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以),例如把 "~/lua/" 路径加入 LUA_PATH 环境变量里:
#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"
#更新环境变量参数,使之立即生效
source ~/.profile
如果找过目标文件,则会调用 package.loadfile 来加载模块。否则,就会去找 C 程序库。搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。搜索的策略跟上面的一样,只不过现在换成搜索的是 so 或 dll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。
Lua和C是很容易结合的,使用 C 为 Lua 写包。与Lua中写包不同,C包在使用以前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制。Lua在一个叫loadlib的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。
一般情况下我们期望二进制的发布库包含一个与前面代码段相似的 stub 文件,安装二进制库的时候可以随便放在某个目录,只需要修改 stub 文件对应二进制库的实际路径即可。将 stub 文件所在的目录加入到 LUA_PATH,这样设定后就可以使用 require 函数加载 C 库了。
在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作。因此 Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。
有两个很重要的函数来处理元表:
__index 元方法是 metatable 最常用的键。当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键。__index 元方法查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 __index 返回结果。
Lua 查找一个表元素时的规则,其实就是如下 3 个步骤:
__newindex 元方法用来对表更新,__index则用来对表访问 。当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。
Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。
-- coroutine_test.lua 文件
co = coroutine.create(
function(i)
print(i);
end
)
coroutine.resume(co, 1) -- 1
print(coroutine.status(co)) -- dead
print("----------")
co = coroutine.wrap(
function(i)
print(i);
end
)
co(1)
print("----------")
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
coroutine在底层实现就是一个线程;当create一个coroutine的时候就是在新线程中注册了一个事件;
当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件;coroutine.running() 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 corouting 的线程号。
Lua I/O 库用于读取和处理文件。分为简单模式(和C一样)、完全模式:
程序运行中错误处理是必要的,在我们进行文件操作,数据转移及web service 调用过程中都会出现不可预期的错误。如果不注重错误信息的处理,就会造成信息泄露,程序无法运行等情况。
我们可以使用两个函数:assert 和 error 来处理错误。assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出;error函数终止正在执行的函数,并返回message的内容作为错误信息(error函数永远都不会返回),error会附加一些错误位置的信息到message头部。
Lua中处理错误,可以使用函数pcall(protected call)来包装需要执行的代码。
pcall接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值true或者或false, errorinfo。
if pcall(function_name, ….) then
-- 没有错误
else
-- 一些错误
end
pcall以一种"保护模式"来调用第一个参数,因此pcall可以捕获函数执行中的任何错误。通常在错误发生时,希望落得更多的调试信息,而不只是发生错误的位置。但pcall返回时,它已经销毁了调用桟的部分内容。Lua提供了xpcall函数,xpcall接收第二个参数——一个错误处理函数,当错误发生时,Lua会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用debug库来获取关于错误的额外信息了。
Lua 提供了 debug 库用于提供创建我们自定义调试器的功能。Lua 本身并未有内置的调试器,但很多开发者共享了他们的 Lua 调试器代码。
Lua 采用了自动内存管理。 这意味着你不用操心新创建的对象需要的内存如何分配出来, 也不用考虑在对象不再被使用后怎样释放它们所占用的内存。Lua 运行了一个垃圾收集器来收集所有死对象 (即在 Lua 中不可能再访问到的对象)来完成自动内存管理的工作。 Lua 中所有用到的内存,如:字符串、表、用户数据、函数、线程、 内部结构等,都服从自动管理。
Lua 实现了一个增量标记-扫描收集器。 它使用这两个数字来控制垃圾收集循环: 垃圾收集器间歇率和垃圾收集器步进倍率。 这两个数字都使用百分数为单位 (例如:值 100 在内部表示 1 )。
垃圾收集器间歇率控制着收集器需要在开启新的循环前要等待多久。 增大这个值会减少收集器的积极性。 当这个值比 100 小的时候,收集器在开启新的循环前不会有等待。 设置这个值为 200 就会让收集器等到总内存使用量达到 之前的两倍时才开始新的循环。
垃圾收集器步进倍率控制着收集器运作速度相对于内存分配速度的倍率。 增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。 不要把这个值设得小于 100 , 那样的话收集器就工作的太慢了以至于永远都干不完一个循环。 默认值是 200 ,这表示收集器以内存分配的"两倍"速工作。
如果你把步进倍率设为一个非常大的数字 (比你的程序可能用到的字节数还大 10% ), 收集器的行为就像一个 stop-the-world 收集器。 接着你若把间歇率设为 200 , 收集器的行为就和过去的 Lua 版本一样了: 每次 Lua 使用的内存翻倍时,就做一次完整的收集。
LUA中最基本的结构是table,所以需要用table来描述对象的属性。lua 中的 function 可以用来表示方法。那么LUA中的类可以通过 table + function 模拟出来。至于继承,可以通过 metetable 模拟出来(不推荐用,只模拟最基本的对象大部分时间够用了)。Lua 中的表不仅在某种意义上是一种对象。像对象一样,表也有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值的对象(table)代表两个不同的对象;一个对象在不同的时候也可以有不同的值,但他始终是一个对象;与对象类似,表的生命周期与其由什么创建、在哪创建没有关系。
-- 元类
Shape = {area = 0}
-- 基础类方法 new
function Shape:new (o,side)
o = o or {}
setmetatable(o, self)
self.__index = self
side = side or 0
self.area = side*side;
return o
end
-- 基础类方法 printArea
function Shape:printArea ()
print("面积为 ",self.area)
end
-- 创建对象
myshape = Shape:new(nil,10)
myshape:printArea()
-- Meta class
Shape = {area = 0}
-- 基础类方法 new
function Shape:new (o,side)
o = o or {}
setmetatable(o, self)
self.__index = self
side = side or 0
self.area = side*side;
return o
end
-- 基础类方法 printArea
function Shape:printArea ()
print("面积为 ",self.area)
end
-- 创建对象
myshape = Shape:new(nil,10)
myshape:printArea()
Square = Shape:new()
-- 派生类方法 new
function Square:new (o,side)
o = o or Shape:new(o,side)
setmetatable(o, self)
self.__index = self
return o
end
-- 派生类方法 printArea
function Square:printArea ()
print("正方形面积为 ",self.area)
end
-- 创建对象
mysquare = Square:new(nil,10)
mysquare:printArea()
Rectangle = Shape:new()
-- 派生类方法 new
function Rectangle:new (o,length,breadth)
o = o or Shape:new(o)
setmetatable(o, self)
self.__index = self
self.area = length * breadth
return o
end
-- 派生类方法 printArea
function Rectangle:printArea ()
print("矩形面积为 ",self.area)
end
-- 创建对象
myrectangle = Rectangle:new(nil,10,20)
myrectangle:printArea()
Lua 数据库的操作库有一个开源的LuaSQL,支持的数据库有:ODBC, ADO, Oracle, MySQL, SQLite 和 PostgreSQL。LuaSQL 可以使用 LuaRocks 来安装可以根据需要安装你需要的数据库驱动。
require "luasql.mysql"
--创建环境对象
env = luasql.mysql()
--连接数据库
conn = env:connect("数据库名","用户名","密码","IP地址",端口)
--设置数据库的编码格式
conn:execute"SET NAMES UTF8"
--执行数据库操作
cur = conn:execute("select * from role")
row = cur:fetch({},"a")
--文件对象的创建
file = io.open("role.txt","w+");
while row do
var = string.format("%d %s\n", row.id, row.name)
print(var)
file:write(var)
row = cur:fetch(row,"a")
end
file:close() --关闭文件对象
conn:close() --关闭数据库连接
env:close() --关闭数据库环境
5.2 版本之后,require 不再定义全局变量,需要保存其返回值。
本部分我们将来学习PHP的语言的基础知识。
<?php
echo "XML风格"; // 其他标记风格请自行google,这里统一使用此种风格
?>
php支持8种原始类型:4种标量类型(boolean、integer、float/double、string),2种复合类型(array、object),2种特殊类型(resource、null)。PHP种数据类型是根据变量使用的上下文运行时决定的。可以强制转换以及判断类型, 如settype, is_bool。
数组是一组类型相同的变量的集合,数组的每个数据称为一个元素,包括索引和值两部分。索引可以由数字或者字符串组成。
资源是一种特殊变量,又称作句柄,保持外部资源的一个引用,通过专门的函数来创建和使用。
值不变的量,定义后脚本任何地方都不能改变。define,defined
变量通过一个变量名来定义,系统对程序种的每一个变量分配一个存储单元。PHP变量使用前无需声明,变量标识符区分大小写。
加减乘除、取余、递增递减与C++类似。
英文句号,连接两个字符串。
赋值操作从右而左
对二进制位从低位到高位对齐后的运算,包含按位与或异或非,左右移位。
$err = @(5 / 0);
表达式是PHP的基本元素和重要组成部分,使用分号来区分表达式和语句。
函数就是将一些重复使用到的功能写到一个独立的代码块种,需要时单独调用。
function hi() {
}
不同于其他语言的是,包含变量函数和函数的引用。
本文将迁移至Rapid C++: CMake与VisualStudio工程配置映射
本文整理了工作中常用的CMake与VisualStudio工程配置的映射关系,便于基于现有VS工程迁移到CMake,以及保持开源项目与现有项目的编译兼容性。
工作中的项目工程使用VisualStudio2019进行编译,而开发机已经升至最新的VisualStudio2022,故而会包含相关版本上的兼容讨论。工程配置以VisualStudioCommunity2022Preview版本为参考。
参考CMake Wiki.
PROJECT(main)
CMAKE_MINIMUM_REQUIRED(VERSION 3.15)
SET(CMAKE_SOURCE_DIR .)
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
AUX_SOURCE_DIRECTORY(. DIR_SRCS)
ADD_EXECUTABLE(main ${DIR_SRCS})
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /machine:x86")
参考Windows C++ project property page reference.
在使用cmake生成的visual studio工程中,WIndowsSDKVersion总是为本机的最新SDK版本号,对于想要控制版本号或者使用最新版本均十分不便。
查阅了现有文档,以及官方文档均无相关配置项。
可参考官方相关讨论:https://gitlab.kitware.com/cmake/cmake/-/issues/21403
在核查cmGlobalVisualStudio14Generator的代码时发现cmake有一个针对sdk版本过滤的逻辑,而visual studio默认未设置版本时是否可以自动设置为10.0呢?是的,VisualStudio会帮我们自动设置为10.0 latest installed version
具体的方式是:
cmake -G "Visual Studio 17 2022" -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM=10.0
但是,这样是否有什么后遗症? 目前还未验证,待调试所有参数完毕后测试验证下编译效果
CMAKE_MINIMUM_REQUIRED(VERSION 3.15)
使用.bat脚本调用cmake,可以指定比较复杂的cmake.exe命令的参数
:: ${ProjectRoot}/build/vs2017-x64.bat
@echo off
::build directory
:: it should be similar name with cmake generator name
set BUILD_DIR=vs2017-x64
:: platform
:: x86 or x64
set BUILD_PLATFORM=x64
:: cl.exe compiler version
set BUILD_COMPILER=v142
:: create directory if not exist
if not exist %BUILD_DIR% md %BUILD_DIR%
cd %BUILD_DIR%
:: run cmake by specifing:
:: - generator
:: - installation directory
:: - CMakeLists.txt location
cmake -G "Visual Studio 12 2017 Win64" -DCMAKE_INSTALL_PREFIX=D:/target/%BUILD_PLATFORM%/%BUILD_COMPILER%
:: run build by specifying config and target
:: note: this may fail, and please open .sln and do manual compilation and installation
cmake --build . --config Release --target INSTALL
:: go back to old folder
cd ..
:: stuck to show build messages
pause
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
message(STATUE "64bit")
else()
message(STATUE "32bit")endif()
if(CMAKE_CL_64)
message(STATUS "MSVC 64bit")
else()
message(STATUS "MSVC 32bit")
endif()
if(WIN32)
message(STATUS "----- This is Windows.")
elseif(UNIX)
message(STATUS "----- This is UNIX.") #Linux下输出这个
elseif(APPLE)
message(STATUS "----- This is APPLE.")
elseif(ANDROID)
message(STATUS "----- This is ANDROID.")
endif(WIN32)
if (CMAKE_SYSTEM_NAME MATCHES "Windows")
message(STATUS "----- OS: Windows")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
message(STATUS "----- OS: Linux")
elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
message(STATUS "----- OS: MacOS X")
elseif(CMAKE_SYSTEM_NAME MATCHES "Android")
message(STATUS "----- OS: Android")endif()
测试发现,如果在CMAKE_MINIMUM_VERSION()后立即使用CMAKE_SYSTEM_NAME,Linux下得到结果为空,Android下得到为Android。看起来是Android的toolchain中进行了设定。
set(PROJECT_LIB_DIR, "path")
在vs平台下,会自动把path和path/$(Configuration)添加到库搜索目录。
修改CMAKE_C_FLAGS、CMAKE_CXX_FLAGS变量
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /we4013 /we4431")
error LNK2026: 模块对于 SAFESEH 映像是不安全的
fatal error LNK1281: 无法生成 SAFESEH 映像
解决办法是:
if (CMAKE_SYSTEM_NAME MATCHES "Windows")
#message("inside windows")
# add SAFESEH to Visual Studio. copied from http://www.reactos.org/pipermail/ros-diffs/2010-November/039192.html
#if(${_MACHINE_ARCH_FLAG} MATCHES X86) # fails
#message("inside that branch")
# in VS2013, there is: fatal error LNK1104: cannot open file "LIBC.lib"
# so, we have to add /NODEFAULTLIB:LIBC.LIB
# reference: https://stackoverflow.com/questions/6016649/cannot-open-file-libc-lib
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
set (CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
#endif()
endif (CMAKE_SYSTEM_NAME MATCHES "Windows")
link_directories() 这句话必须在add_executable()之前写 不然找不到库目录
或者先add_executable() 再 target_link_directories(XXX PRIVATE some direcotry)
设置debug模式下编译出的库文件,相比于release模式下,多带一个字母"d"作为后缀。
创建目录:file(MAKE_DIRECTORY ${SO_OUTPUT_PATH})
和某个target绑定的文件拷贝,使用add_custom_command:add_custom_command(TARGET your_target PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${MY_SO_NAME} ${SO_OUTPUT_PATH}/)
和target无关的,或者说对于所有target而言都需要做文件拷贝,用execute_process
foreach(lib_name_pth ${LIBS_TO_COPY})
message(STATUS "--- ${lib_name_pth}")
execute_process(COMMAND ${CMAKE_COMMAND} -E copy ${lib_name_pth} ${SO_OUTPUT_PATH})
endforeach()
get_filename_component(SO_OUTPUT_PATH_ABS ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${ANDROID_ABI} ABSOLUTE)
foreach(loop_var arg1 arg2 arg3)
message(STATUS "--- ${loop_var}")
endforeach(loop_var)
foreach(loop_var ${SNPE_LIB_ALL})
message(STATUS "--- ${loop_var}")
endforeach(loop_var)
通过设定CMAKE_C_COMPILER和CMAKE_CXX_COMPILER来做到。
注意:project()命令必须在设定编译器之后出现,否则编译器的设定不起作用,将使用系统默认编译器。
if (UNIX)
message(STATUS "----- This is Linux.")
set(CMAKE_C_COMPILER "gcc-4.9")
set(CMAKE_CXX_COMPILER "g++-4.9")
endif()
project(gamma)
注:前一种方法是在单个CMakeLists.txt中设定。对于跨平台编译,则应当避免污染根CMakeLists.txt,应该为每个平台分别使用cmake cache script。而在cache script中需要设定的变量,都应该是缓存变量。
set(CMAKE_C_COMPILER gcc CACHE STRING "C compiler")
set(CMAKE_CXX_COMPILER g++ CACHE STRING "C++ compiler")
set(PLATFORM_NAME "SigmaStar" CACHE STRING "")
如果是自己项目中的源码基于cmake构建,其中利用add_library()创建的库目标,可以直接用来作为可执行目标、动态库或静态库的依赖库直接使用。
而如果是别人直接丢过来的库和头文件、没有用cmake封装一次呢?显然我们不应该在Visual Studio的项目属性中手动添加,手写一个导入库的cmake,在add_library()命令中指定关键字IMPORTED,再用set_target_properties()命令来设定导入库目标的头文件目录、库目录、库文件名字:
add_library(rock SHARED IMPORTED GLOBAL)
set_target_properties(rock PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "inc" #PUBLIC头文件目录
IMPORTED_IMPLIB "rock.lib" #Windows平台上dll库的.lib库所在位置
IMPORTED_LOCATION "rock.dll" #dll库的.dll所在位置,或者.so库的位置,或者静态库的位置
)
其中GLOBAL关键字,是为了让全局可见。例如通过add_subdirectory()添加了mpbase库,里面是上述方式添加的库,但是上级CMakeLists.txt要确保能使用这个库,就需要指定GLOBAL关键字。
P.S. 实践发现,如果库文件所在目录很长(超过256个字符),或者添加的导入库对应的库文件有多个,它们的名字会被拼接起来,在CMake+Ninja的NDK开发环境下直接报错说路径太长。因此,导入库并不是一个好的实践。
问题来自StackOverFlow上某网友的提问:Compile error CMAKE with CUDA on Visual Studio C++
解决步骤:
add_definitions(-DUSE_OPENCV)
add_definitions(-DLANDMARK_VERSION=2.1.33)
相当于传递给C/C++编译器:
#define USE_OPENCV
#define LANDMARK_VERSION 2.1.33
error: Android 5.0 and later only support position-independent executables (-fPIE)
问题出现在:连接一个静态库到一个可执行程序,并在android6.0上运行
解决办法:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE -pie")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fPIE -pie")
问题出现场景:编译动态库libaisf_bodyattr_processor.so的时候,它依赖于静态库libarcsoft_bsd.a,但是libarcsoft_bsd.a库编译时没有指定fPIC编译选项。
在编译静态库的时候,全局设定:set(CMAKE_POSITION_INDEPENDENT_CODE ON)
在编译静态库的时候,设定
add_library(lib1 lib1.cpp)
set_property(TARGET lib1 PROPERTY POSITION_INDEPENDENT_CODE ON)
使用场景满足的条件:
此时如果继续在CMakeLists.txt中“一把梭”,各种设定都写在单个文件中,可以执行就不够高了。每个依赖写成FindXXX.cmake,则后续直接使用find_package(XXX)很方便,定位排查依赖项问题、可移植性都得到了增强。
FindXXX.cmake基本步骤
例子1:单个头文件和单个库文件
# provides `milk_LIBRARIES` and `milk_INCLUDE_DIRS` variable
# usage: `find_package(milk)`
include(FindPackageHandleStandardArgs)
set(milk_ROOT_DIR ${PROJECT_SOURCE_DIR}/third_party/milk CACHE PATH "Folder contains milk")
set(milk_DIR ${milk_ROOT_DIR})
find_path(milk_INCLUDE_DIRS
NAMES milk.h
PATHS ${milk_DIR}
PATH_SUFFIXES include include/x86_64 include/x64
DOC "milk include"
NO_DEFAULT_PATH)
# find milk.libfind_library(milk_LIBRARIES
NAMES milk
PATHS ${milk_DIR}
PATH_SUFFIXES lib lib64 lib/x86_64 lib/x86_64-linux-gnu lib/x64 lib/x86
DOC "milk library"
NO_DEFAULT_PATH)
find_package_handle_standard_args(milk DEFAULT_MSG milk_INCLUDE_DIRS milk_LIBRARIES)
if (milk_FOUND)
if (NOT milk_FIND_QUIETLY)
message(STATUS "Found milk: ${milk_INCLUDE_DIRS}, ${milk_LIBRARIES}")
endif ()
mark_as_advanced(milk_ROOT_DIR milk_INCLUDE_DIRS milk_LIBRARIES)
else ()
if (milk_FIND_REQUIRED)
message(FATAL_ERROR "Could not find milk")
endif ()
endif ()
例子2:同时存在Debug和Release版本的库
希望在调用find_package(xxx)之后,Visual Studio或XCode等IDE能自动切换debug和release的库。则需要为debug库的路径添加debug字段,为release库添加optimized字段。
set(LEMON_LIBRARIES
debug "${LEMON_DIR}/lib/debug/lemon.lib"
optimized "${LEMON_DIR}/lib/release/lemon.lib" )
考虑到硬编码不是一个好的方案,库文件可能放在lib、lib64、lib/Release等目录中,应当先用find_library()进行查找,然后再set库文件变量LEMON_LIBRARIES。
多个库的find_library写法
对于依赖库中的多个库,自然的想法是使用foreach()来处理每个库文件。
考虑到find_library(lemon_lib_name)会产生缓存变量lemon_lib_name,这会导致再次调用find_library(lemon_lib_name)时不再查找。需要unset(${lemon_lib_name} CACHE)该缓存变量来确保查找成功。直接给出完整例子:
include(FindPackageHandleStandardArgs)
set(LEMON_ROOT_DIR ${PROJECT_SOURCE_DIR}/third_party/lemon CACHE PATH "Folder contains lemon")
set(LEMON_DIR ${CEVA_ROOT_DIR})
set(LEMON_DIR ${LEMON_ROOT_DIR})
set(LEMON_LIBRARY_COMPONENTS lemon_core lemon_extra)
foreach(lemon_component ${LEMON_LIBRARY_COMPONENTS})
unset(LEMON_LIBRARIES_DEBUG CACHE)
find_library(LEMON_LIBRARIES_DEBUG
NAMES ${lemon_component}
PATHS ${LEMON_DIR}
PATH_SUFFIXES lib lib/debug lib/debug
DOC "lemon library component ${lemon_component} debug"
NO_DEFAULT_PATH)
unset(LEMON_LIBRARIES_RELEASE CACHE)
find_library(LEMON_LIBRARIES_RELEASE
NAMES ${lemon_component}
PATHS ${LEMON_DIR}
PATH_SUFFIXES lib lib/release lib/Release
DOC "lemon library component ${lemon_component} release"
NO_DEFAULT_PATH)
list(APPEND LEMON_LIBRARIES
debug ${LEMON_LIBRARIES_DEBUG}
optimized ${LEMON_LIBRARIES_RELEASE}
)
endforeach()
find_package_handle_standard_args(LEMON DEFAULT_MSG LEMON_LIBRARIES)
if (LEMON_FOUND)
if (NOT LEMON_FIND_QUIETLY)
message(STATUS "Found LEMON: ${LEMON_LIBRARIES}")
endif ()
mark_as_advanced(LEMON_ROOT_DIR LEMON_LIBRARIES)
else ()
if (LEMON_FIND_REQUIRED)
message(FATAL_ERROR "Could not find lemon")
endif ()
endif ()
例子3:找dll
注意:CMAKE_FIND_LIBRARY_SUFFIXES的使用:CMake find_library matching behavior?
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
实际上,CMAKE_FIND_LIBRARY_SUFFIXES影响最大的就是find_library()命令了。譬如zlib安装目录下,同时存在动态库和静态库,分别是libz.a和libz.so,而find_library()的行为是“找到一个就不再找了”,因此如果没有很好的设定CMAKE_FIND_LIBRARY_SUFFIXES,就会导致找不到想要的库。默认情况下是找到动态库,然而windows下还需要手动拷贝DLL。。。麻烦。
可以通过备份原有的CMAKE_FIND_LIBRARY_SUFFIXES的值,改掉它的值,find_library()之后再改回原来的值,这样就支持了 静态库/动态库 分别查找的设定。。(用于魔改cmake自带的FindZLIB.cmake)
CMake find module to distinguish shared or static library
cmake_minimum_required(VERSION 3.2)
message(STATUS "CMAKE_C_FLAGS = " ${CMAKE_C_FLAGS})
message(STATUS "CMAKE_C_FLAGS_DEBUG = " ${CMAKE_C_FLAGS_DEBUG})
message(STATUS "CMAKE_C_FLAGS_MINSIZEREL = " ${CMAKE_C_FLAGS_MINSIZEREL})
message(STATUS "CMAKE_C_FLAGS_RELEASE = " ${CMAKE_C_FLAGS_RELEASE})
message(STATUS "CMAKE_C_FLAGS_RELWITHDEBINFO = " ${CMAKE_C_FLAGS_RELWITHDEBINFO})
message(STATUS "CMAKE_CXX_FLAGS = " ${CMAKE_CXX_FLAGS})
message(STATUS "CMAKE_CXX_FLAGS_DEBUG = " ${CMAKE_CXX_FLAGS_DEBUG})
message(STATUS "CMAKE_CXX_FLAGS_MINSIZEREL = " ${CMAKE_CXX_FLAGS_MINSIZEREL})
message(STATUS "CMAKE_CXX_FLAGS_RELEASE = " ${CMAKE_CXX_FLAGS_RELEASE})
message(STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO = " ${CMAKE_CXX_FLAGS_RELWITHDEBINFO})
message(STATUS "CMAKE_EXE_LINKER_FLAGS = " ${CMAKE_EXE_LINKER_FLAGS})
message(STATUS "CMAKE_EXE_LINKER_FLAGS_DEBUG = " ${CMAKE_EXE_LINKER_FLAGS_DEBUG})
message(STATUS "CMAKE_EXE_LINKER_FLAGS_MINSIZEREL = " ${CMAKE_EXE_LINKER_FLAGS_MINSIZEREL})
message(STATUS "CMAKE_EXE_LINKER_FLAGS_RELEASE = " ${CMAKE_EXE_LINKER_FLAGS_RELEASE})
message(STATUS "CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO = " ${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO})
message(STATUS "CMAKE_MODULE_LINKER_FLAGS = " ${CMAKE_MODULE_LINKER_FLAGS})
message(STATUS "CMAKE_MODULE_LINKER_FLAGS_DEBUG = " ${CMAKE_MODULE_LINKER_FLAGS_DEBUG})
message(STATUS "CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL = " ${CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL})
message(STATUS "CMAKE_MODULE_LINKER_FLAGS_RELEASE = " ${CMAKE_MODULE_LINKER_FLAGS_RELEASE})
message(STATUS "CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO = " ${CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO})
message(STATUS "CMAKE_SHARED_LINKER_FLAGS = " ${CMAKE_SHARED_LINKER_FLAGS})
message(STATUS "CMAKE_SHARED_LINKER_FLAGS_DEBUG = " ${CMAKE_SHARED_LINKER_FLAGS_DEBUG})
message(STATUS "CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL = " ${CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL})
message(STATUS "CMAKE_SHARED_LINKER_FLAGS_RELEASE = " ${CMAKE_SHARED_LINKER_FLAGS_RELEASE})
message(STATUS "CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO = " ${CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO})
message(STATUS "CMAKE_STATIC_LINKER_FLAGS = " ${CMAKE_STATIC_LINKER_FLAGS})
message(STATUS "CMAKE_STATIC_LINKER_FLAGS_DEBUG = " ${CMAKE_STATIC_LINKER_FLAGS_DEBUG})
message(STATUS "CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL = " ${CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL})
message(STATUS "CMAKE_STATIC_LINKER_FLAGS_RELEASE = " ${CMAKE_STATIC_LINKER_FLAGS_RELEASE})
message(STATUS "CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO = " ${CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO}
检查链接到的重名函数
场景:A库的代码中定义了函数play(),B库的代码中也定义了函数play(),但是这两个play()函数的实现不同,并且被可执行目标C同时链接。
链接器默认是找到一个符号就不再查找,因此默认能链接并且可以运行,只不过运行结果不是所期待的。
容易查到,Linux下gcc对应的链接器中可以使用--whole-archive和--no-whole-archive参数来包含静态库中的所有符号。
如果是gcc,则使用gcc -Wl --whole-archive someLib --no-whole-archive。
如果是Visual Studio,则需要>=2015 update2的版本中才支持/WHOLEARCHIVE选项,VS2013要哭泣了。
因而,在CMakeLists.txt中,可以设定链接器的全局设定:
if(CMAKE_SYSTEM_NAME MATCHES "Windows")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /WHOLEARCHIVE")
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--whole-archive")
endif()
缺点:
TODO: 对于单个target,如何设定?
if (CMAKE_SYSTEM_NAME MATCHES "Windows")
set_target_properties(inter
PROPERTIES LINK_FLAGS
"/WHOLEARCHIVE:gender /WHOLEARCHIVE:smile"
)
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
# 不起作用
set_target_properties(inter
PROPERTIES LINK_FLAGS
"-Wl,--whole-archive gender
-Wl,--whole-archive smile"
)
endif()
或者:
set(MYLIB -Wl,--whole-archive mytest -Wl,--no-whole-archive)
target_link_libraries(main ${MYLIB})
实际上:
gcc的链接器ld:通过-Wl, --whole-archive lib_name -Wl, --no-whole-archive能每次分别对一个静态库加载所有的member(函数等
Visual Studio的链接器:VS2017(1900)之后才支持-WHOLEARCHIVE来实现同样功能;
PC Clang的链接器:使用lld作为链接器(比如Xcode现在用肯定是lld),用-Wl,-force_load ${lib}来做到只导入一个静态库中的所有member
NDK的链接器:怎么说现在用的NDK也是17b起步了,默认编译器是Clang,gcc暂时没考虑;虽然编译器很早就(可以)切换到Clang了,但链接器目前还是用的gcc的ld,因此NDK的链接阶段检查重复符号应该用ld的检查方式;即使是用当前(2020-01-26 02:16:34)最新的NDK也就是NDK21,手动传入ANDROID_LD=lld后,NDK切换到的链接器lld和MacOS上与AppleClang搭配的lld也还是不一样,链接阶段查重复符号仍然需要传gcc的ld的那一套参数:-Wl, --whole-archive lib_name -Wl, --no-whole-archive,但好处是报错界面更加友好直观了:
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set设定变量并且设定PARENT_SCOPE参数。当项目中代码变多,就可能需要分成多个目录存放。每个目录下放一个CMakeLists.txt,写出它要处理的文件列表,然后暴露给外层CMakeLists.txt,使外层CMakeLists.txt保持清爽结构。
set(hello_srcs
${CMAKE_CURRENT_SOURCE_DIR}/hello.cpp)
set(hello_private_incs
${CMAKE_CURRENT_SOURCE_DIR}/hello.h)
set(hello_srcs ${hello_srcs} PARENT_SCOPE)
set(hello_private_incs ${hello_private_incs} PARENT_SCOPE)
包括两步:
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0")
VS2019开始,需要用-A参数指定是32位程序,还是64位程序。以前的用法不行了
cmake -G "Visual Studio 16 2019" -A Win32 ..\..
cmake -G "Visual Studio 16 2019" -A x64 ..\..
include_directories("inc" "src")
add_library(rock src/rock.cpp)
target_include_directories(rock PRIVATE "3rdparty/spdlog")
用法有坑,其中target_include_directories()设置的目录不会生效。经验:只使用其中一种设定include目录的方式。
这需要编译器的编译选项中开启exception的支持。现在NDK开发,主流的做法是Android Studio + gradle + cmake做构建,需要在build.gradle中设定-fexceptions:
externalNativeBuild {
cmake
{// cppFlags '-std=c++11 -fexceptions'
// arguments '-DANDROID_PLATFORM=android-21',
// '-DANDROID_TOOLCHAIN=clang',
// '-DCMAKE_BUILD_TYPE="Release',
// '-DANDROID_ARM_NEON=ON',
// '-DANDROID_STL=c++_shared'
// cppFlags '-std=c++11'
// arguments '-DANDROID_TOOLCHAIN=clang',
// '-DANDROID_STL=c++_static'
cppFlags "-std=c++11 -fexceptions"
}
}
但有时候,发现上述设定后并不生效;尝试删除.externalBuild目录重新构建,仍然报exception无法处理。后来发现,问题出在CMakeLists.txt加载的.cmake脚本中(用的代码框架是其它部门同事写的,所以不是很熟悉),他给手动设定了这么一句:
SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions -fno-short-enums -Werror=non-virtual-dtor")
去掉-fno-exception即可。
目前遇到的有两种:
当然,还可以使用mark_as_adances来设定,则默认在cmake-gui中不可见
NDK开发中,CMake+Ninja构建,如果文件名超过260个字符会失败。这个限制略蛋疼
ninja: error: Stat(../../deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarc_net_sgl.a;E:/dbg/myy/landmark_demo/deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarcsoft_face_detection.a;E:/dbg/myy/landmark_demo/deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarcsoft_landmark_tracking.a;E:/dbg/myy/landmark_demo/deps/arcsoft_landmark/2.1.12021.33/libs/Android/armeabi-v7a/libarcsoft_videooutline.a): Filename longer than 260 characters
比如我想要定制一些安全的编译选项,发现需要区分msvc,gcc和clang。容易直接想到CMAKE_C_COMPILER和CMAKE_CXX_COMPILER。但考虑到-G Xcode和命令行下分别输出,得到的结果并不都是clang,这就很蛋疼。
使用CMAKE_CXX_COMPILER_ID比较方便,像上面提到的case会做合并输出AppleClang。而如果是NDK-r17c则输出Clang。
常见的平台下对应输出:
更多结果看官方文档 CMAKE__COMPILER_ID
简单有效的两种方式:
function(afq_list_append __string __element)
set(${__string} "${${__string}} ${__element}" PARENT_SCOPE)
endfunction()
本质上,当使用cmake ..,或cmake -G "Visual Studio 15 2017 Win64" ../..类似命令时,是执行“pre-make”,相当于是makefile的生成器,可以说对于你的项目代码来说并没执行编译链接,更没有安装。
实际使用经验:cmake生成了这些cache文件后,可以打开Visual Studio编译,或执行make编译(Linux下)。但这些都是native tool。通用的方式则是用cmake包装好的接口:
# 执行编译(如果是可执target,则包括链接过程)
cmake --build . --config Release
# 执行某个target的编译(如果是可执target,则包括链接过程)
cmake --build . --config Release --target xx
# 执行安装
cmake --install . --prefix d:/lib/openblas/clang-cl/x64 -v
在cmake里混合编译C/C++与汇编代码,通过enable_language(ASM_)可以做到。
例如x86(32位)的MASM(Visual Studio支持的一种汇编语法):enable_language(ASM_MASM)
此外往往还需要给Visual Studio设置SEH:
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /SAFESEH:NO /NODEFAULTLIB:LIBC.LIB")
汇编文件则和C/C++文件一起,正常添加为target的依赖即可:add_executable(run src/main.cpp src/CalcSum_.asm)
有些依赖库只提供.pc文件,甚至已经配置了CMake脚本但是安装后还是只有.pc而没有XXXConfig.cmake或xxx-config.cmake。并且不仅是Linux,Windows上也这样。
这就不得不在CMake中尝试去加载.pc文件。原理是,cmake里面封装了对pkg-config工具的兼容,可以认为是一个插件,用这个插件去加载.pc文件。实际测试发现Linux和Windows都可以用。
使用:先找到xx.pc文件,然后分成目录和文件前缀两部分,在cmake中配置
举例1:Ubuntu 16.04下用apt安装openblas并在CMake中用pkg-config方式配置:
cmake_minimum_required(VERSION 3.15)
project(cmake_pkg_config_example)
set(ENV{PKG_CONFIG_PATH} /usr/lib/pkgconfig)
find_package(PkgConfig)
pkg_search_module(OBS REQUIRED blas-openblas)
message(STATUS "=== OBS_LIBRARIES: ${OBS_LIBRARIES}")
message(STATUS "=== OBS_INCLUDE_DIRS: ${OBS_INCLUDE_DIRS}")
举例2:Windows 10下用CMake配置Pangolin安装中配置的zlib
cmake_minimum_required(VERSION 3.15)
project(cmake_pkg_config_example)
#指定pkg-config.exe绝对路径
set(PKG_CONFIG_EXECUTABLE "D:/soft/pkg-config/bin/pkg-config.exe")
#指定zlib.pc所在目录
set(ENV{PKG_CONFIG_PATH} "D:/lib/pangolin/share/pkgconfig")
find_package(PkgConfig)
message(STATUS "--- PKG_CONFIG_FOUND: ${PKG_CONFIG_FOUND}")
message(STATUS "--- PKG_CONFIG_VERSION_STRING: ${PKG_CONFIG_VERSION_STRING}")
pkg_search_module(ZLIB REQUIRED zlib)
message(STATUS "=== ZLIB_LIBRARIES: ${ZLIB_LIBRARIES}")
message(STATUS "=== ZLIB_INCLUDE_DIRS: ${ZLIB_INCLUDE_DIRS}")
#[[
第一种注释方式
#]]
#[===============[
第二种注释方式
#]===============]
在cmake脚本中指定其中任意一种:
CMAKE_MODULE_PATH是在CMake脚本中用户可以自行修改的变量。
CMAKE MODULE DIRECTORY是什么,官方文档没明确说。其实说的应该是cmake安装后的Modules目录,例如/usr/local/share/cmake/Modules
IWYU 是 google 的开源项目,用来移除不必要的头文件。
cmake 3.13 开始提供的命令。低版本cmake无法使用
用CMake构建NDK项目时,会传入toolchain的cmake脚本文件android.toolchain.cmake给CMake。这个文件中会做若干设定,其中就包括include路径。
我遇到的情况是,自己手动修改CMAKE_C_FLAGS和CMAKE_CXX_FLAGS时,覆盖了它们原有的(android.toolchain.cmake修改后的)值,导致asm/types.h找不到。
# 我的错误设定:
set(CMAKE_C_FLAGS "${MY_CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${MY_CMAKE_CXX_FLAGS}")
# 正确做法应该是追加内容而非修改:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MY_CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MY_CMAKE_CXX_FLAGS}")
P.S. 排查方法:由于我是基于ninja构建的(cmake+ndk的组合下,现在通常用ninja),通过对比”能正常构建的工程“和”提示asm/types.h找不到的工程“之间${CMAKE_BINARY_DIR}目录下的rules.ninja和build.ninja来发现问题所在。
.lib是导入库,里面存访对外可见(暴露)的符号(函数、变量)。.dll应该搭配一个.lib导入库才能使用。
如果是自己的源码生成的dll共享库,则在CMakeLists.txt一开始,添加:set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)则可以导出所有的符号。
CMake linking against shared library on windows: error about not finding .lib file
而如果只想导出一部分符号,则可以为每个函数分别指定导出规则。
在Windows下,Visual Studio中,如果用了动态库(例如opencv、zlib等),需要把dll放到PATH环境变量中,使得运行时能找到dll。
而其实Windows下的PATH查找,是会在CMAKE_BINARY_DIR目录下查找的。如果不想改PATH环境变量,也不希望每次都要手动拷贝dll,包括清掉build目录后重新构建时也不想手动拷贝,那么可以用cmake命令来搞。
举个例子,调用zlib库执行文本压缩解压,用到了zlib1.dll,其中executable target名字是demo。
zlib的二进制包下载:https://nsis.sourceforge.io/mediawiki/images/b/bb/Zlib-1.2.8-win64-AMD64.zip
zlib的调用示例代码:https://blog.csdn.net/yuhuqiao/article/details/82188963
cmake中拷贝zlib1.dll的写法:
# each time the `demo` target is built, we copy zlib1.dll if it is changed.
add_custom_command(TARGET demo
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${ZLIB_DLL}
${CMAKE_BINARY_DIR}/)
tar命令是从cmake 3.2开始支持的内置命令,以解压doctest.zip到项目根目录为例:
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_SOURCE_DIR}/doctest.zip
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
有些基于cmake的项目,CMakeLists.txt写的很复杂很庞大。可以列出所有target,帮助理清思路。
列出makefile中的所有target:
cd build
cmake --build . --target help
实际上,makefile里诸如"all"和"clean"这样的target,并不是我们感兴趣的。还是shell大法拼凑一下吧:
cd ~/work/my_project
mkdir build && cd build && cmake ..
make -j4 > log.txt 2>&1
grep 'Built target' log.txt | awk '{print $4}'
EXISTS可以判断,同时适用于文件和目录。
举例:查找当前目录下是否存在doctest目录,并且检查doctest目录下是否存在doctest_fwd.h和doctest.cpp文件:
if (EXISTS "${CMAKE_SOURCE_DIR}/doctest"
AND EXISTS "${CMAKE_SOURCE_DIR}/doctest/doctest_fwd.h"
AND EXISTS "${CMAKE_SOURCE_DIR}/doctest/doctest.cpp")
message(STATUS "--- doctest source code ready !")
else()
message(STATUS "--- extracting doctest source code from zip...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xzf ${CMAKE_SOURCE_DIR}/doctest.zip
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
endif()
message(STATUS "--- CMAKE_CXX_COMPILER is: ${CMAKE_CXX_COMPILER}")
include(CMakePrintHelpers)
cmake_print_variables(CMAKE_CXX_COMPILER)
用IS_DIRECTORY命令判断。例如shadow中判断各个backend,如果是目录,则添加subdirectory,写法如下:
foreach (backend_dir ${backends_dir})
if (IS_DIRECTORY ${backend_dir})
add_subdirectory(${backend_dir})
endif()
endforeach()
https://stackoverflow.com/a/56490614/2999096
从cmake3.15开始,可以用CMAKE_MSVC_RUNTIME_LIBRARY和MSVC_RUNTIME_LIBRARY设定。
可以全局设定:set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreadedDebug)
针对单个目标设定:
add_executable(foo foo.c)
set_property(TARGET foo PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
候选值有4种:
首先确保有符号信息(例如CMAKE_BUILD_TYPE设定为Debug)。
其次是设定如下几个变量中的其中一个(多个也行但没必要,看你需求):
例如我编译的是可执行目标,那么:
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
对于可执行目标,并且依赖于静态库或动态库,懒人用法:
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
set(CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
注意:ASAN似乎对vector等容器的支持不够好。对于vector,预先分配多少内存,似乎ASAN并不知道,导致vector被clear后再使用,(做下标访问一段时间后)出现的segfault,没被ASAN检测到。
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32")
假设.sln目录在 build/vs2019-x64 下,则默认的可执行文件生成目录是 build/vs2019-x64/Debug 或 build/vs2019-x64/Release;而如果文件放在这一默认生成目录下的话无法被读取到(不加前缀的情况),需要放在 build/vs2019-x64 目录下才能读到。
以至于,代码里经常要根据 _MSC_VER 或者 ANDROID 等平台相关的宏,设定不同的路径,例如:
#if _MSC_VER
const char* image_path = "E:/share/to_zcx/ncnn_vk_dbg/build/vs2019-x64/ncnn_input_cpu.bmp";
#elif ANDROID
const char* image_path = "ncnn_input_cpu.bmp";
#endif
这让代码不整洁,也增加了出错的可能。
可以通过上面两截图中的方式,在项目属性中修改可执行文件的生成目录和启动目录,但仍然不方便;最好的办法还是在 CMakeLists.txt 里设定:
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_SOURCE_DIR}/bin>)
add_executable(testbed hello.cpp)
set_target_properties(testbed PROPERTIES
VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")
https://stackoverflow.com/questions/47175912/using-cmake-how-to-stop-the-debug-and-release-subdirectories
https://stackoverflow.com/questions/41864259/how-to-set-working-directory-for-visual-studio-2017-rc-cmake-project
从cmake3.17开始,文档里正式说明支持CMAKE_FIND_DEBUG_MODE这一cmake变量,设定为TRUE则打印find_package/find_program/find_file等函数的打印过程
set(CMAKE_FIND_DEBUG_MODE TRUE)
find_package(...)
set(CMAKE_FIND_DEBUG_MODE FALSE)
实际上据网友反馈,稍早的版本也可以用这一变量,只不过文档里当时没写。
另外,设定CMAKE_FIND_DEBUG_MODE变量为TRUE,等价于调用cmake时候指定--debug-find参数。
如果你是cmake-GUI方式构建,菜单栏也可以选择输出debug信息:
场景:命令行方式调用cmake,指定了很多option和cache variable的值,希望把这些option和cache variable放在.txt文件中,然后通过cat option.txt方式传给cmake。
结论:option.txt里的写法,-D之后不能有空格,否则无法生效。
例如,正确写法是:
-DUSE_X1=ON
-DUSE_X2=ON
-DUSE_X3=ON
错误写法是
-D USE_X1=ON
-D USE_X2=ON
-D USE_X3=ON
可以用如下 CMakeLists.txt 验证结论:
cmake_minimum_required(VERSION 3.15)
project(x)
option(USE_X1 "USE X1?" OFF)
option(USE_X2 "USE_X2?" OFF)
option(USE_X3 "USE_X3?" OFF)
if (USE_X1)
message(STATUS "USE_X1: ON")
else()
message(STATUS "USE_X1: OFF")
endif()
if (USE_X2)
message(STATUS "USE_X2: ON")
else()
message(STATUS "USE_X2: OFF")
endif()
if (USE_X3)
message(STATUS "USE_X3: ON")
else()
message(STATUS "USE_X3: OFF")
endif()
cmake `cat options.txt`
cmake G:/dev/opencv-build/mock -G "Visual Studio 16 2019" -A x64 -DCMAKE_INSTALL_PREFIX=install $(type G:/dev/opencv-build/mock/options.txt)
powershell是基于ksh语法修改而来(而不是M的别的其他的语法);‘(cat options.txt)`在bash里也能用;
两个``符号,英文正式名字叫做backtick,而不是"reverse quote"。
常用主要有如下几个不响应target的全局设定:
target_compile_definitions(): 目标添加编译器编译选项,例如target_compile_definitions(shadow_jni PRIVATE -DUSE_STB -DUSE_ARM)
target_include_directories():目标添加包含文件,例如target_include_directories(shadow_jni PRIVATE "shadow_jni/body_detection" "shadow_jni/util" ${SNPE_INC_DIR})
target_link_directories():目标添加链接库查找目录,例如target_link_directories(shadow_jni PRIVATE ${SNPE_LIB_DIR})
target_link_libraries():目标添加链接库,例如target_link_libraries(shadow_jni ${log-lib} ${graph-lib} ${SNPE_LIB})
https://stackoverflow.com/questions/66485987/what-is-the-bash-reverse-quote-equivalent-in-powershell
https://stackoverflow.com/questions/434038/whats-the-cmd-powershell-equivalent-of-back-tick-on-bash
本文整理了部分C/C++的框架和库,分享给大家。后续会附加相关库的评测和使用指南,持续关注哦~~~
Apache C++ Standard Library : 是一系列算法,容器,迭代器和其他基本组件的集合
ASL : Adobe源代码库提供了同行的评审和可移植的C++源代码库。
Boost : 大量通用C++库的集合。
BDE : 来自于彭博资讯实验室的开发环境。
Cinder : 提供专业品质创造性编码的开源开发社区。
Bxxomfort : 轻量级的,只包含头文件的库,将C++ 11的一些新特性移植到C++03中。
Dlib : 使用契约式编程和现代C++科技设计的通用的跨平台的C++库。
EASTL : EA-STL公共部分
ffead-cpp : 企业应用程序开发框架
Folly : 由Facebook开发和使用的开源C++库。
JUCE : 包罗万象的C++类库,用于开发跨平台软件
libphenom : 用于构建高性能和高度可扩展性系统的事件框架。
LibSourcey : 用于实时的视频流和高性能网络应用程序的C++11 evented IO
LibU : C语言写的多平台工具库
Loki : C++库的设计,包括常见的设计模式和习语的实现。
MiLi : 只含头文件的小型C++库
openFrameworks : 开发C++工具包,用于创意性编码。
Qt : 跨平台的应用程序和用户界面框架
Reason : 跨平台的框架,使开发者能够更容易地使用Java,.Net和Python,同时也满足了他们对C++性能和优势的需求。
ROOT : 具备所有功能的一系列面向对象的框架,能够非常高效地处理和分析大量的数据,为欧洲原子能研究机构所用。
STLport : 是STL具有代表性的版本
STXXL : 用于额外的大型数据集的标准模板库。
Ultimate++ : C++跨平台快速应用程序开发框架
Windows Template Library : 用于开发Windows应用程序和UI组件的C++库
Yomm11 : C++11的开放multi-methods.
Boost.Asio : 用于网络和底层I/O编程的跨平台的C++库。
libev : 功能齐全,高性能的时间循环,轻微地仿效libevent,但是不再像libevent一样有局限性,也修复了它的一些bug。
libevent : 事件通知库
libuv : 跨平台异步I/O。
libco : 协程,微信支持8亿用户同时在线的底层IO库。功能强大
ntyco : 纯c版的协程框架,代码短小精悍,适合嵌入工程。
libgo : golang风格的并发框架,C++11实现协程库
ACE : C++面向对象网络变成工具包
Casablanca : C++ REST SDK
cpp-netlib : 高级网络编程的开源库集合
Dyad.c : C语言的异步网络
libCurl : 多协议文件传输库
Mongoose : 非常轻量级的网络服务器
Muduo : 用于Linux多线程服务器的C++非阻塞网络库
net_skeleton : C/C++的TCP 客户端/服务器库
WAFer : 基于C语言的超轻型软件平台,用于可扩展的服务器端和网络应用。 对于C编程人员,可以考虑node.js
Onion : C语言HTTP服务器库,其设计为轻量级,易使用。
POCO : 用于构建网络和基于互联网应用程序的C++类库,可以运行在桌面,服务器,移动和嵌入式系统。
RakNet : 为游戏开发人员提供的跨平台的开源C++网络引擎。
Tufao : 用于Qt之上的C++构建的异步Web框架。
WebSocket++ : 基于C++/Boost Aiso的websocket 客户端/服务器库
ZeroMQ : 高速,模块化的异步通信库
f-stack : 腾讯开源的协议栈,基于DPDK的高性能用户态协议栈。
NtyTcp : 单线程的协议栈的,基于netmap,DPDK,rawSocket的实现。
LWIP : 针对 RAM 平台的精简版的 TCP/IP 协议栈实现。
mTCP : 针对多核系统的高可扩展性的用户空间 TCP/IP 协议栈。
4.4BSD : * nix的协议栈是源于4.4BSD的。
Nginx : 一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。
Lighttpd : 一款开源 Web 服务器软件,安全快速,符合行业标准,适配性强并且针对高配置环境进行了优化。
Libmicrohttpd : GNU软件下的简单c库的Web服务器。API简单,快速。
shttpd : 基于Mongoose的Web服务器框架。
CivetWeb : 提供易于使用,强大的,C/C++嵌入式Web服务器,带有可选的CGI,SSL和Lua支持。
CppCMS : 免费高性能的Web开发框架(不是 CMS).
Crow : 一个C++微型web框架(灵感来自于Python Flask)
Kore : 使用C语言开发的用于web应用程序的超快速和灵活的web服务器/框架。
libOnion : 轻量级的库,帮助你使用C编程语言创建web服务器。
QDjango : 使用C++编写的,基于Qt库的web框架,试图效仿Django API,因此得此名。
Wt : 开发Web应用的C++库。
C++ Standard Library : 是一系列类和函数的集合,使用核心语言编写,也是C++ISO自身标准的一部分。
Standard Template Library : 标准模板库, STL
C POSIX library : POSIX系统的C标准库规范
ISO C++ Standards Committee : C++标准委员会
FMOD : 易于使用的跨平台的音频引擎和音频内容的游戏创作工具。
Maximilian : C++音频和音乐数字信号处理库
OpenAL : 开源音频库—跨平台的音频API
Opus : 一个完全开放的,免版税的,高度通用的音频编解码器
Speex : 免费编解码器,为Opus所废弃
Tonic : C++易用和高效的音频合成
Vorbis : Ogg Vorbis是一种完全开放的,非专有的,免版税的通用压缩音频格式。
lisequence : 用于表示和分析群体遗传学数据的C++库。
SeqAn : 专注于生物数据序列分析的算法和数据结构。
Vcflib : 用于解析和处理VCF文件的C++库
Wham : 直接把联想测试应用到BAM文件的基因结构变异。
bzip2 : 一个完全免费,免费专利和高质量的数据压缩
doboz : 能够快速解压缩的压缩库
PhysicsFS : 对各种归档提供抽象访问的库,主要用于视频游戏,设计灵感部分来自于Quake3的文件子系统。
KArchive : 用于创建,读写和操作文件档案(例如zip和 tar)的库,它通过QIODevice的一系列子类,使用gzip格式,提供了透明的压缩和解压缩的数据。
LZ4 : 非常快速的压缩算法
LZHAM : 无损压缩数据库,压缩比率跟LZMA接近,但是解压缩速度却要快得多。
LZMA : 7z格式默认和通用的压缩方法。
LZMAT : 及其快速的实时无损数据压缩库
miniz : 单一的C源文件,紧缩/膨胀压缩库,使用zlib兼容API,ZIP归档读写,PNG写方式。
Minizip : Zlib最新bug修复,支持PKWARE磁盘跨越,AES加密和IO缓冲。
Snappy : 快速压缩和解压缩
ZLib : 非常紧凑的数据流压缩库
ZZIPlib : 提供ZIP归档的读权限。
Boost.Compute : 用于OpenCL的C++GPU计算库
Bolt : 针对GPU进行优化的C++模板库
C++React : 用于C++11的反应性编程库
Intel TBB : Intel线程构件块
Libclsph : 基于OpenCL的GPU加速SPH流体仿真库
OpenCL : 并行编程的异构系统的开放标准
OpenMP : OpenMP API
Thrust : 类似于C++标准模板库的并行算法库
HPX : 用于任何规模的并行和分布式应用程序的通用C++运行时系统
VexCL : 用于OpenCL/CUDA 的C++向量表达式模板库。
Bcrypt : 一个跨平台的文件加密工具,加密文件可以移植到所有可支持的操作系统和处理器中。
BeeCrypt : 快速的加密图形库,功能强大,接口方便。
Botan : C++加密库
Crypto++ : 一个有关加密方案的免费的C++库
GnuPG : OpenPGP标准的完整实现
GnuTLS : 实现了SSL,TLS和DTLS协议的安全通信库
Libgcrypt : 基于GnuPG的加密图形库。
Libmcrypt : 线程安全,提供统一的API。
LibreSSL : 免费的SSL/TLS协议,属于2014 OpenSSL的一个分支
LibTomCrypt : 一个非常全面的,模块化的,可移植的加密工具
libsodium : 基于NaCI的加密库,固执己见,容易使用
Nettle : 底层的加密库
OpenSSL : 一个强大的,商用的,功能齐全的,开放源代码的加密库。
Tiny AES128 in C : 用C实现的一个小巧,可移植的实现了AES128ESB的加密算法
hiberlite : 用于Sqlite3的C++对象关系映射
hiredis : 用于Redis数据库的很简单的C客户端库
LevelDB : 快速键值存储库
LMDB : 符合数据库四大基本元素的嵌入键值存储
MySQL++ : 封装了MySql的C API的C++ 包装器
RocksDB : 来自Facebook的嵌入键值的快速存储
SQLite : 一个完全嵌入式的,功能齐全的关系数据库,只有几百KB,可以正确包含到你的项目中。
Redis : 一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库.
MongoDB : 一个基于分布式文件存储的数据库
Boost.Test : Boost测试库
Catch : 一个很时尚的,C++原生的框架,只包含头文件,用于单元测试,测试驱动开发和行为驱动开发。
CppUnit : 由JUnit移植过来的C++测试框架
CTest : CMake测试驱动程序
GoogleTest : 谷歌C++测试框架
ig-debugheap : 用于跟踪内存错误的多平台调试堆
libtap : 用C语言编写测试
MemTrack : 用于C++跟踪内存分配
MicroProfile : 跨平台的网络试图分析器
minUnit : 使用C写的迷你单元测试框架,只使用了两个宏
Remotery : 用于web视图的单一C文件分析器
UnitTest++ : 轻量级的C++单元测试框架
C++ B-Tree : 基于B树数据结构,实现命令内存容器的模板库
Hashmaps : C++中开放寻址哈希表算法的实现
Cocos2d-x : 一个跨平台框架,用于构建2D游戏,互动图书,演示和其他图形应用程序。
Grit : 社区项目,用于构建一个免费的游戏引擎,实现开放的世界3D游戏。
lrrlicht : C++语言编写的开源高性能的实时#D引擎
PolyCode : C++实现的用于创建游戏的开源框架(与Lua绑定)。
bgfx : 跨平台的渲染库
Cairo : 支持多种输出设备的2D图形库
Horde3D : 一个小型的3D渲染和动画引擎
magnum : C++11和OpenGL 2D/3D 图形引擎
Ogre 3D : 用C++编写的一个面向场景,实时,灵活的3D渲染引擎(并非游戏引擎)
OpenSceneGraph : 具有高性能的开源3D图形工具包
Panda3D : 用于3D渲染和游戏开发的框架,用Python和C++编写。
Skia : 用于绘制文字,图形和图像的完整的2D图形库
urho3d : 跨平台的渲染和游戏引擎。
Boost.GIL : 通用图像库
CImg : 用于图像处理的小型开源C++工具包
FreeImage : 开源库,支持现在多媒体应用所需的通用图片格式和其他格式。
GDCM : Grassroots DICOM 库
ITK : 跨平台的开源图像分析系统
Magick++ : ImageMagick程序的C++接口
OpenCV : 开源计算机视觉类库
tesseract-ocr : OCR引擎
VIGRA : 用于图像分析通用C++计算机视觉库
VTK : 用于3D计算机图形学,图像处理和可视化的开源免费软件系统。
gettext : GNU gettext
IBM ICU : 提供Unicode 和全球化支持的C、C++ 和Java库
libiconv : 用于不同字符编码之间的编码转换库
frozen : C/C++的Jason解析生成器
Jansson : 进行编解码和处理Jason数据的C语言库
jbson : C++14中构建和迭代BSON data,和Json 文档的库
JeayeSON : 非常健全的C++ JSON库,只包含头文件
JSON++ : C++ JSON 解析器
json-parser : 用可移植的ANSI C编写的JSON解析器,占用内存非常少
json11 : 一个迷你的C++11 JSON库
jute : 非常简单的C++ JSON解析器
ibjson : C语言中的JSON解析和打印库,很容易和任何模型集成
libjson : 轻量级的JSON库
PicoJSON : C++中JSON解析序列化,只包含头文件
Qt-Json : 用于JSON数据和 QVariant层次间的相互解析的简单类
QJson : 将JSON数据映射到QVariant对象的基于Qt的库
RepidJSON : 用于C++的快速JSON 解析生成器,包含SAX和DOM两种风格的API
YAJL : C语言中快速流JSON解析库
Boost.Log : 设计非常模块化,并且具有扩展性
easyloggingpp : C++日志库,只包含单一的头文件。
Log4cpp : 一系列C++类库,灵活添加日志到文件,系统日志,IDSA和其他地方。
templog : 轻量级C++库,可以添加日志到你的C++应用程序中
btsk : 游戏行为树启动器工具
Evolving Objects : 基于模板的,ANSI C++演化计算库,能够帮助你非常快速地编写出自己的随机优化算法。
Caffe : 快速的神经网络框架
CCV : 以C语言为核心的现代计算机视觉库
mlpack : 可扩展的C++机器学习库
OpenCV : 开源计算机视觉库
Recommender : 使用协同过滤进行产品推荐/建议的C语言库。
SHOGUN : Shogun 机器学习工具
sofia-ml : 用于机器学习的快速增量算法套件
Armadillo : 高质量的C++线性代数库,速度和易用性做到了很好的平衡。语法和MatlAB很相似
blaze : 高性能的C++数学库,用于密集和稀疏算法。
ceres-solver : 来自谷歌的C++库,用于建模和解决大型复杂非线性最小平方问题。
CGal : 高效,可靠的集合算法集合
CML : 用于游戏和图形的免费C++数学库
Eigen : 高级C++模板头文件库,包括线性代数,矩阵,向量操作,数值解决和其他相关的算法。
GMTL : 数学图形模板库是一组广泛实现基本图形的工具。
GMP : 用于个高精度计算的C/C++库,处理有符号整数,有理数和浮点数。
GStreamer : 构建媒体处理组件图形的库
LIVE555 Streaming Media : 使用开放标准协议(RTP/RTCP, RTSP, SIP) 的多媒体流库
libVLC : libVLC (VLC SDK)媒体框架
QtAV : 基于Qt和FFmpeg的多媒体播放框架,能够帮助你轻而易举地编写出一个播放器
SDL : 简单直控媒体层
SFML : 快速,简单的多媒体库
Box2D : 2D的游戏物理引擎。
Bullet : 3D的游戏物理引擎。
Chipmunk : 快速,轻量级的2D游戏物理库
LiquidFun : 2D的游戏物理引擎
ODE : 开放动力学引擎-开源,高性能库,模拟刚体动力学。
ofxBox2D : Box2D开源框架包装器。
Simbody : 高性能C++多体动力学/物理库,模拟关节生物力学和机械系统,像车辆,机器人和人体骨骼。
MOOS-Ivp : 一组开源C++模块,提供机器人平台的自主权,尤其是自主的海洋车辆。
MRPT : 移动机器人编程工具包
PCL : 点云库是一个独立的,大规模的开放项目,用于2D/3D图像和点云处理。
Robotics Library : 一个独立的C++库,包括机器人动力学,运动规划和控制。
RobWork : 一组C++库的集合,用于机器人系统的仿真和控制。
ROS : 机器人操作系统,提供了一些库和工具帮助软件开发人员创建机器人应用程序。
FFTW : 用一维或者多维计算DFT的C语言库。
GSL : GNU科学库。
ChaiScript : 用于C++的易于使用的嵌入式脚本语言。
Lua : 用于配置文件和基本应用程序脚本的小型快速脚本引擎。
luacxx : 用于创建Lua绑定的C++ 11 API
SWIG : 一个可以让你的C++代码链接到JavaScript,Perl,PHP,Python,Tcl和Ruby的包装器/接口生成器
V7 : 嵌入式的JavaScript 引擎。
V8 : 谷歌的快速JavaScript引擎,可以被嵌入到任何C++应用程序中。
Cap'n Proto : 快速数据交换格式和RPC系统。
cereal : C++11 序列化库
FlatBuffers : 内存高效的序列化库
MessagePack : C/C++的高效二进制序列化库,例如 JSON
ProtoBuf : 协议缓冲,谷歌的数据交换格式。
ProtoBuf-c : C语言的协议缓冲实现
SimpleBinaryEncoding : 用于低延迟应用程序的对二进制格式的应用程序信息的编码和解码。
Thrift : 高效的跨语言IPC/RPC,用于C++,Java,Python,PHP,C#和其它多种语言中,最初由Facebook开发。
libvpx : VP8/VP9编码解码SDK
FFMpeg : 一个完整的,跨平台的解决方案,用于记录,转换视频和音频流。
libde265 : 开放的h.265视频编解码器的实现。
OpenH264 : 开源H.364 编解码器。
Theora : 免费开源的视频压缩格式。
Expat : 用C语言编写的xml解析库
LibXml2 : Gnome的xml C解析器和工具包
LibXml++ : C++的xml解析器
PugiXML : 用于C++的,支持XPath的轻量级,简单快速的XML解析器。
RapidXML : 试图创建最快速的XML解析器,同时保持易用性,可移植性和合理的W3C兼容性。
TinyXML : 简单小型的C++XML解析器,可以很容易地集成到其它项目中。
TinyXML2 : 简单快速的C++CML解析器,可以很容易集成到其它项目中。
TinyXML++ : TinyXML的一个全新的接口,使用了C++的许多许多优势,模板,异常和更好的异常处理。
Xerces-C++ : 用可移植的C++的子集编写的XML验证解析器
本文摘自https://time.geekbang.org/column/article/245905,请支持正版付费
C++的高效、灵活和强大本文将不再赘述,使用现代的C++特性,再加上标准库、第三方库,C++几乎无所不能。但是,C++ 也有自己的“阿喀琉斯之踵”,那就是语言复杂、学习曲线陡峭、开发周期长、排错 / 维护成本高。
所以,C++ 不能完全适应现在的快速开发和迭代的节奏,最终只能退到后端、底层等领域。要想充分发挥 C++ 的功力,就要辅助其他的语言搭建混合系统,尽量扬长避短,做好那最关键、最核心的部分,这样才能展现出它应有的价值。由于当前的操作系统、虚拟机、解释器、引擎很多都是用 C 或者 C++ 编写的,所以,使用 C++,可以很容易地编写各种底层模块,为上层的 Java、Go 等语言提供扩展功能。不过,今天我不去说这些大型语言,而是讲两种轻便的脚本语言:Python 和 Lua,看看 C++ 怎么和它们俩实现无缝对接:以 C++ 为底层基础,Python 和 Lua 作为上层建筑,共同搭建起高性能、易维护、可扩展的混合系统。
Python 应该是除了 JavaScript 以外最流行的一种脚本语言了,一直在 TIOBE 榜单里占据前三名的位置。而且,在新兴的大数据、人工智能、科学计算等领域,也都有着广泛的应用。很多大公司都长期招聘 Python 程序员,就是看中了它的高生产率。
Python 本身就有 C 接口,可以用 C 语言编写扩展模块,把一些低效耗时的功能改用 C 实现,有的时候,会把整体性能提升几倍甚至几十倍。但是,使用纯 C 语言写扩展模块非常麻烦,那么,能不能利用 C++ 的那些高级特性来简化这部分的工作呢?很多人都想到了这个问题,于是,就出现了一些专门的 C++/Python 工具,使用 C++ 来开发 Python 扩展。其中,最好的一个就是pybind11。
pybind11 借鉴了“前辈”Boost.Python,能够在 C++ 和 Python 之间自由转换,任意翻译两者的语言要素,比如把 C++ 的 vector 转换为 Python 的列表,把 Python 的元组转换为 C++ 的 tuple,既可以在 C++ 里调用 Python 脚本,也可以在 Python 里调用 C++ 的函数、类。
pybind11 名字里的“11”表示它完全基于现代 C++ 开发(C++11 以上),所以没有兼容旧系统的负担。它使用了大量的现代 C++ 特性,不仅代码干净整齐,运行效率也更高。
下面,我们看看怎么用 pybind11,让 C++ 来辅助 Python,提升 Python 的性能。
pybind11 是一个纯头文件的库,但因为必须结合 Python,所以首先要有 Python 的开发库,然后再用 pip 工具安装。
pybind11 支持 Python2.7、Python3 和 PyPy,这里用的是 Python3:
apt-get install python3-dev
apt-get install python3-pip
pip3 install pybind11
pybind11 充分利用了 C++ 预处理和模板元编程,把原本无聊重复的代码都隐藏了起来,展现了“神奇的魔法”——只需要短短几行代码,就可以实现一个 Python 扩展模块。具体怎么实现呢?
实际上,你只要用一个宏“PYBIND11_MODULE”,再给它两个参数,Python 模块名和 C++ 实例对象名,就可以了。
#include <pybind11/pybind11.h> // pybind11的头文件
PYBIND11_MODULE(pydemo, m) // 定义Python模块pydemo
{
m.doc() = "pybind11 demo doc"; // 模块的说明文档
} // Python模块定义结束
代码里的 pydemo 就是 Python 里的模块名,之后在 Python 脚本里必须用这个名字才能 import。
第二个参数“m”其实是 pybind11::module 的一个实例对象,封装了所有的操作,比如这里的 doc() 就是模块的说明文档。它只是个普通的变量,起什么名字都可以,但为了写起来方便,一般都用“m”。
假设这个 C++ 源文件名是“pybind.cpp”,现在你就可以用 g++ 把它编译成在 Python 里调用的模块了,不过编译命令比较复杂:
g++ pybind.cpp \ #编译的源文件
-std=c++11 -shared -fPIC \ #编译成动态库
`python3 -m pybind11 --includes` \ #获得包含路径
-o pydemo`python3-config --extension-suffix` #生成的动态库名字
稍微解释一下。第一行是指定编译的源文件,第二行是指定编译成动态库,这两个不用多说。第三行调用了 Python,获得 pybind11 所在的包含路径,让 g++ 能够找得到头文件。第四行最关键,是生成的动态库名字,前面必须是源码里的模块名,而后面那部分则是 Python 要求的后缀名,否则 Python 运行时会找不到模块。
编译完后会生成一个大概这样的文件:pydemo.cpython-35m-x86_64-linux-gnu.so,现在就可以在 Python 里验证了,使用 import 导入,然后用 help 就能查看模块说明:
$ python3
>>> import pydemo
>>> help(pydemo)
刚才的代码非常简单,只是个空模块,里面什么都没有,现在,我们来看看怎么把 C++ 的函数导入 Python。
你需要用的是 def() 函数,传递一个 Python 函数名和 C++ 的函数、函数对象或者是 lambda 表达式,形式上和 Python 的函数也差不多:
namespace py = pybind11; // 名字空间别名,简化代码
PYBIND11_MODULE(pydemo, m) // 定义Python模块pydemo
{
m.def("info", // 定义Python函数
[]() // 定义一个lambda表达式
{
py::print("c++ version =", __cplusplus); // pybind11自己的打印函数
py::print("gcc version =", __VERSION__);
py::print("libstdc++ =", __GLIBCXX__);
}
);
m.def("add", // 定义Python函数
[](int a, int b) // 有参数的lambda表达式
{
return a + b;
}
);
} // Python模块定义结束
这样我们就非常轻松地实现了两个 Python 函数,在 Python 里可以验证效果:
import pydemo # 导入pybind11模块
pydemo.info() # 调用C++写的函数
x = pydemo.add(1,2) # 调用C++写的函数
pybind11 也支持函数的参数、返回值使用标准容器,会自动转换成 Python 里的 list、dict,不过你需要额外再包含一个“stl.h”的头文件。下面的示例代码演示了 C++ 的 string、tuple 和 vector 是如何用于 Python 的:
#include <pybind11/stl.h> // 转换标准容器必须的头文件
PYBIND11_MODULE(pydemo, m) // 定义Python模块pydemo
{
m.def("use_str", // 定义Python函数
[](const string& str) // 入参是string
{
py::print(str);
return str + "!!"; // 返回string
}
);
m.def("use_tuple", // 定义Python函数
[](tuple<int, int, string> x) // 入参是tuple
{
get<0>(x)++;
get<1>(x)++;
get<2>(x)+= "??";
return x; // 返回元组
}
);
m.def("use_list", // 定义Python函数
[](const vector<int>& v) // 入参是vector
{
auto vv = v;
py::print("input :", vv);
vv.push_back(100);
return vv; // 返回列表
}
);
}
因为都是面向对象的编程语言,C++ 里的类也能够等价地转换到 Python 里面调用,这要用到一个特别的模板类 class_,注意,它有意模仿了关键字 class,后面多了一个下划线。
我拿一个简单的 Point 类来举个例子:
class Point final
{
public:
Point() = default;
Point(int a);
public:
int get() const;
void set(int a);
};
使用 pybind11,你需要在模板参数里写上这个类名,然后在构造函数里指定它在 Python 里的名字。导出成员函数还是调用函数 def(),但它会返回对象自身的引用,所以就可以连续调用,在一句话里导出所有接口:
py::class_<Point>(m, "Point") // 定义Python类
.def(py::init()) // 导出构造函数
.def(py::init<int>()) // 导出构造函数
.def("get", &Point::get) // 导出成员函数
.def("set", &Point::set) // 导出成员函数
;
对于一般的成员函数来说,定义的方式和普通函数一样,只是你必须加上取地址操作符“&”,把它写成函数指针的形式。而构造函数则比较特殊,必须调用 init() 函数来表示,如果有参数,还需要在 init() 函数的模板参数列表里写清楚。pybind11 的功能非常丰富,我们不可能一下子学完全部的功能,刚才说的这些只是最基本,也是非常实用的功能。除了这些,它还支持异常、枚举、智能指针等很多 C++ 特性,你可以再参考一下它的文档,学习一下具体的方法,挖掘出它的更多价值。如果你在工作中重度使用 Python,那么 pybind11 绝对是你的得力助手,它能够让 C++ 紧密地整合进 Python 应用里,让 Python 跑得更快、更顺畅,建议你有机会就尽量多用。
接下来我要说的第二个脚本语言是小巧高效的 Lua,号称是“最快的脚本语言”。
你可能对 Lua 不太了解,但你一定听说过《魔兽世界》《愤怒的小鸟》吧,它们就在内部大量使用了 Lua 来编写逻辑。在游戏开发领域,Lua 可以说是一种通用的工作语言。
Lua 与其他语言最大的不同点在于它的设计目标:不追求“大而全”,而是“小而美”。Lua 自身只有很小的语言核心,能做的事情很少。但正是因为它小,才能够很容易地嵌入到其他语言里,为“宿主”添加脚本编程的能力,让“宿主”更容易扩展和定制。
标准的 Lua(PUC-Rio Lua)使用解释器运行,速度虽然很快,但和 C/C++ 比起来还是有差距的。所以,你还可以选择另一个兼容的项目:LuaJIT(https://luajit.org/)。它使用了 JIT(Just in time)技术,能够把 Lua 代码即时编译成机器码,速度几乎可以媲美原生 C/C++ 代码。
不过,LuaJIT 也有一个问题,它是一个个人项目,更新比较慢,最新的 2.1.0-beta3 已经是三年前的事情了。所以,我推荐你改用它的一个非官方分支:OpenResty-LuaJIT(https://github.com/openresty/luajit2)。它由 OpenResty 负责维护,非常活跃,修复了很多小错误。
git clone [email protected]:openresty/luajit2.git
make && make install
和 Python 一样,Lua 也有 C 接口用来编写扩展模块,但因为它比较小众,所以 C++ 项目不是很多。现在我用的是 LuaBridge,虽然它没有用到太多的 C++11 新特性,但也足够好。
LuaBridge 是一个纯头文件的库,只要下载下来,把头文件拷贝到包含路径,就能够直接用:
git clone [email protected]:vinniefalco/LuaBridge.git
我们先来看看在 Lua 里怎么调 C++ 的功能。
和前面说的 pybind11 类似,LuaBridge 也定义了很多的类和方法,可以把 C++ 函数、类注册到 Lua 里,让 Lua 调用。
但我不建议你用这种方式,因为我们现在有 LuaJIT。它内置了一个 ffi 库(Foreign Function Interface),能够在 Lua 脚本里直接声明接口函数、直接调用,不需要任何的注册动作,更加简单方便。而且这种做法还越过了 Lua 传统的栈操作,速度也更快。
使用 ffi 唯一要注意的是,它只能识别纯 C 接口,不认识 C++,所以,写 Lua 扩展模块的时候,内部可以用 C++,但对外的接口必须转换成纯 C 函数。
下面我写了一个简单的 add() 函数,还有一个全局变量,注意里面必须要用 extern "C"声明:
extern "C" { // 使用纯C语言的对外接口
int num = 10;
int my_add(int a, int b);
}
int my_add(int a, int b) // 一个简单的函数,供Lua调用
{
return a + b;
}
然后就可以用 g++ 把它编译成动态库,不像 pybind11,它没有什么特别的选项:
g++ lua_shared.cpp -std=c++11 -shared -fPIC -o liblua_shared.so
在 Lua 脚本里,你首先要用 ffi.cdef 声明要调用的接口,再用 ffi.load 加载动态库,这样就会把动态库所有的接口都引进 Lua,然后就能随便使用了:
local ffi = require "ffi" -- 加载ffi库
local ffi_load = ffi.load -- 函数别名
local ffi_cdef = ffi.cdef
ffi_cdef[[ // 声明C接口
int num;
int my_add(int a, int b);
]]
local shared = ffi_load("./liblua_shared.so") -- 加载动态库
print(shared.num) -- 调用C接口
local x = shared.my_add(1, 2) -- 调用C接口
在 ffi 的帮助下,让 Lua 调用 C 接口几乎是零工作量,但这并不能完全发挥出 Lua 的优势。
因为和 Python 不一样,Lua 很少独立运行,大多数情况下都要嵌入在宿主语言里,被宿主调用,然后再“回调”底层接口,利用它的“胶水语言”特性去粘合业务逻辑。
要在 C++ 里嵌入 Lua,首先要调用函数 luaL_newstate(),创建出一个 Lua 虚拟机,所有的 Lua 功能都要在它上面执行。
因为 Lua 是用 C 语言写的,Lua 虚拟机用完之后必须要用函数 lua_close() 关闭,所以最好用 RAII 技术写一个类来自动管理。可惜的是,LuaBridge 没有对此封装,所以只能自己动手了。这里我用了智能指针 shared_ptr,在一个 lambda 表达式里创建虚拟机,顺便再打开 Lua 基本库:
auto make_luavm = []() // lambda表达式创建虚拟机
{
std::shared_ptr<lua_State> vm( // 智能指针
luaL_newstate(), lua_close // 创建虚拟机对象,设置删除函数
);
luaL_openlibs(vm.get()); // 打开Lua基本库
return vm;
};
#define L vm.get() // 获取原始指针,宏定义方便使用
在 LuaBridge 里,一切 Lua 数据都被封装成了 LuaRef 类,完全屏蔽了 Lua 底层那难以理解的栈操作。它可以隐式或者显式地转换成对应的数字、字符串等基本类型,如果是表,就可以用“[]”访问成员,如果是函数,也可以直接传参调用,非常直观易懂。
使用 LuaBridge 访问 Lua 数据时,还要注意一点,它只能用函数 getGlobal() 看到全局变量,所以,如果想在 C++ 里调用 Lua 功能,就一定不能加“local”修饰。
给你看一小段代码,它先创建了一个 Lua 虚拟机,然后获取了 Lua 内置的 package 模块,输出里面的默认搜索路径 path 和 cpath:
auto vm = make_luavm(); // 创建Lua虚拟机
auto package = getGlobal(L, "package"); // 获取内置的package模块
string path = package["path"]; // 默认的lua脚本搜索路径
string cpath = package["cpath"]; // 默认的动态库搜索路径
你还可以调用 luaL_dostring() 和 luaL_dofile() 这两个函数,直接执行 Lua 代码片段或者外部的脚本文件。注意,luaL_dofile() 每次调用都会从磁盘载入文件,所以效率较低。如果是频繁调用,最好把代码读进内存,存成一个字符串,再用 luaL_dostring() 运行:
luaL_dostring(L, "print('hello lua')"); // 执行Lua代码片段
luaL_dofile(L, "./embedded.lua"); // 执行外部的脚本文件
在 C++ 里嵌入 Lua,还有另外一种方式:提前在脚本里写好一些函数,加载后在 C++ 里逐个调用,这种方式比执行整个脚本更灵活。
具体的做法也很简单,先用 luaL_dostring() 或者 luaL_dofile() 加载脚本,然后调用 getGlobal() 从全局表里获得封装的 LuaRef 对象,就可以像普通函数一样执行了。由于 Lua 是动态语言,变量不需要显式声明类型,所以写起来就像是 C++ 的泛型函数,但却更简单:
string chunk = R"( -- Lua代码片段
function say(s) -- Lua函数1
print(s)
end
function add(a, b) -- Lua函数2
return a + b
end
)";
luaL_dostring(L, chunk.c_str()); // 执行Lua代码片段
auto f1 = getGlobal(L, "say"); // 获得Lua函数
f1("say something"); // 执行Lua函数
auto f2 = getGlobal(L, "add"); // 获得Lua函数
auto v = f2(10, 20); // 执行Lua函数
只要掌握了上面的这些基本用法,并合理地划分出 C++ 与 Lua 的职责边界,就可以搭建出“LuaJIT + LuaBridge + C++”的高性能应用,运行效率与开发效率兼得。比如说用 C++ 写底层的框架、引擎,暴露出各种调用接口作为“业务零件”,再用灵活的 Lua 脚本去组合这些“零件”,写上层的业务逻辑。小结
好了,今天讲了怎么基于 C++ 搭建混合系统,介绍了 Python 和 Lua 这两种脚本语言。
Python 很“大众”,但比较复杂、性能不是特别高;而 Lua 比较“小众”,很小巧,有 LuaJIT 让它运行速度极快。你可以结合自己的实际情况来选择,比如语言的熟悉程度、项目的功能 / 性能需求、开发的难易度,等等。
今天的内容也比较多,简单小结一下要点:
所谓服务端渲染,就是将原先运行在客户端的 JavaScript 框架的渲染过程放在服务器上,渲染出静态的 HTML 和 CSS 的过程。
为什么说它很重要呢?
我们都想提升网站的加载速度 —— 而服务端渲染技术就是这么一个可以加速页面渲染的工具。下面我们来看一看页面渲染的关键步骤:浏览器渲染页面的关键步骤是接受服务端发送的关键数据并渲染成可视化的页面,如果我们能更快地将这些关键资源分发到浏览器,那么页面就可以更快地渲染好并呈现给用户。
浏览器渲染你的应用的速度,取决于你如何构建它。浏览器收到的第一个东西是 HTML 文档。文档包括了指向各种资源的引用 —— 如图片、CSS 和 JavaScript 代码。在浏览器解析 HTML 文档时,它知道要去获取并下载这些资源。所以,即使浏览器已经下载好了 HTML,也还是会在 CSS 解析完成之后才会开始渲染页面。
一旦 CSS 解析结束,浏览器便进一步地渲染页面。也就是说,只需要 HTML 和 CSS,浏览器就能渲染页面。我们都知道浏览器就擅长这个,所以这个过程是很快的。
现在,最后的一步是 JavaScript。HTML 文档解析结束后,浏览器会下载你的 JavaScript 文件。如果文件很大,或者网络状况很差,那下载时间会很长,而且浏览器还得解析 JavaScript。在硬件配置较差的设备上,这部分的时间消耗会很可观。并且,如果你的首次渲染依赖于 JavaScript,你还会多次地看到慢的加载过程。JavaScript 应被看作 HTML 和 CSS 的一个增强,因为它的加载是可以延迟的。然而,情况并不总是这么简单。有的网站需要一些严重依赖于 JavaScript 的功能 —— 这类网站使用了 JavaScript 框架。
上面提到的 JavaScript 框架的渲染过程其实也是可以优化的,比如可以在服务端提前完成其渲染过程,再将生成好的 HTML 下发至浏览器。
所以,用户几乎能立刻看到你之前在服务端提前生成好的 HTML 页面,同时 JavaScript 在后台启动执行。这不一定能让你的页面比非服务端渲染的版本加载的要快,但它确实能在 JavaScript 还在后台下载的过程中给用户一些能看到的内容 —— 这挺好的。
在我们进一步讨论之前,让我们来看看一些来自不同网站的统计信息。
根据一项关于 JavaScript 情况的调查,我们可以看到,Next 排在了 Nuxt 和 Gatsby 之前。在本文中,我们不会考虑 Express 等,因为我们只关注用于服务端渲染的 JavaScript 框架。
根据下面的统计,我们可以看到用户对与 JavaScript 相关的后端框架的关注度。可以看出,NextJS 比起 NuxtJS 和 GatsbyJS,用户的使用量和兴趣都是最高的。
依据这些 GitHub 仓库,我们可以看到开发者越来越被 NextJS 所吸引 —— 不管是 Watch、Fork 还是 Star 数,NextJS 都来得高一些。但 Gatsby 在开发者之间也差不多受欢迎。
Next 是快速成长的 React 框架之一,尤其是在服务端渲染方面。作者把 NextJS 称作一个轻量级框架。我个人认为把它看作一个平台或者是起始模板更合适。
如果你在构建一个简单的应用,我估计 NextJS 会过犹不及。
如果你打算把一个服务端的应用集成到 NextJS 应用中,我不建议你立即就开始这么做,因为实际上是做不到的 —— 工作量太大了。
NuxtJS 是在 VueJS 上层构建的高层次框架,能帮助你构建适用于生产环境的 Vue 应用。
如果你使用自定义的库来构建应用,使用 Nuxt 可能会颇具挑战性。
如果你第一次使用 Nuxt,同时你的 Vue 应用的开发计划排期比较紧,你可能会遇到一些问题。
应用调试会很痛苦 —— 这是开发者社区中的常见问题。
Gatsby 也是一个基于 React 的、GraphQL 驱动的静态站点生成器。更简单地说,Gatsby 是一个静态站点生成器。这是什么意思呢?静态站点的意思是我们放在服务器上的东西是由 Gatsby 生成的静态的 HTML 文件。这与许多网站的工作方式不同。
需要指出的是,静态站点并不意味着不可互动和非动态。我们可以在 Gatsby 服务器上的 HTML 文件中加载 JavaScript,发起 API 请求、进行交互、构建丰富大型的网站等,尽管它们是静态的。
如果你要和 WordPress 一起使用 Gatsby,你需要换用很多 WordPress 内置的功能 —— 比如,你不能使用主题的自定义布局。
由于 Gatsby 站点是静态的,所以每个改动都需要一次新的部署。
基于上面的优缺点和调查,我们可以得出结论:NextJS 是未来最好的服务端渲染框架。然而,如果我们看看前端开发的未来,我们可以看到,Vue 在实际项目开发中也表现得不错。基于上面的各项因素,我建议你学习和使用 NextJS。
感谢阅读。
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。
GitHub 不仅是维护代码的好工具,而且也是学习和成长的好去处。身为一个软件开发者,我一直在寻找有用的 GitHub 仓库,从中学习,以及找到灵感。以下是我最喜欢的 10 个仓库。
GitHub stars: 92.4k
如果你想要做些什么,并且想要获得一些关于如何实现它的指导,这是一个很好的资源。通过浏览列表,你还会发现很多真正有趣的事情。
地址:https://github.com/danistefanovic/build-your-own-x
GitHub stars: 92.1k
一个区别是,软件工程师和软件开发人员更容易掌握算法和数据结构。但是,不管你的背景是什么,这个仓库提供了很多不同的算法,一个详尽的数据结构列表,还有你可能在软件工程面试中遇到的一些典型问题的答案。
地址:https://github.com/trekhleb/javascript-algorithms
GitHub stars: 72.8k
不管你是一个从事编程的人,还是一个已经在业界自学的开发者, OSSU 的课程为所有想要学习计算机科学的人提供了大量的免费学习资源。
地址:https://github.com/ossu/computer-science
GitHub stars: 72.8k
超过 100 个代码段,涵盖了 JavaScript 中的各种内容,从典型的算法,到你可能会发现自己需要完成的常见任务。非常值得一看。
地址:https://github.com/30-seconds/30-seconds-of-code
GitHub stars: 52.6k
有没有想过要学习如何使用特定的语言 / 技术开发适当的应用?这就是为你准备的 GitHub 仓库!这超出了典型的“ to-do”应用程序, RealWorld 的示例使整个“Medium-style”应用程序更加丰富,包括了所有的钟声、哨声和最佳实践。
地址:https://github.com/gothinkster/realworld
GitHub stars: 170k
听起来确实如此。大量的免费编程书籍可以帮助你的知识和理解更上一层楼。
地址:https://github.com/EbookFoundation/free-programming-books
GitHub stars: 118k
如果你正在寻找高级软件工程(或更高的)职位,那么拥有设计大型系统的能力是很有价值的,很多大型技术公司都希望你具备这一能力。这也是一个重要技巧,如果你打算为你正在做的任何工作构建任何大型系统的话。这份指南提供了大量的信息来帮助你做好准备。
地址:https://github.com/donnemartin/system-design-primer
GitHub stars: 92.2k
Python 内建了一系列不同的库、框架和技术的列表。对那些想要学习一种新的编程语言或仅仅想要提高对已有 Python 的了解的人来说,这是一个极好的指南。
地址:https://github.com/vinta/awesome-python
GitHub stars 58.6k
我总是找不到足够的最佳实践指南。所以当发现这个 GitHub 仓库时,我觉得必须把它包括进去。自学的坏处之一就是,你并不总是从最佳实践开始。因此,拥有这些详细的指南可以帮助你快速提高技能。
地址:https://github.com/goldbergyoni/nodebestpractices
GitHub stars: 47.8k
像我们之前看到的 Python 清单一样,这个 GitHub 仓库包含大量不错的机器学习资源。
地址:https://github.com/josephmisiti/awesome-machine-learning
原文链接:
RND,可以简单看做是 ReactNative 在桌面系统上的实现,Native 部分采用C++开发。RND 的 JS Framework 基于 RN 0.57.8,node.js 运行时 8.1.2。
npm install -g react-native-cli
react-native init demo --version 0.57.8
npm install --save @suhao/react-node-desktop
react-native clone-demo
react-native run-demo
react-native package
在 flex 容器中默认存在两条轴,水平主轴(main axis) 和垂直的交叉轴(cross axis),这是默认的设置,当然你可以通过修改使垂直方向变为主轴,水平方向变为交叉轴,这个我们后面再说。
在容器中的每个单元块被称之为 flex item,每个项目占据的主轴空间为 (main size), 占据的交叉轴的空间为 (cross size)。
这里需要强调,不能先入为主认为宽度就是 main size,高度就是 cross size,这个还要取决于你主轴的方向,如果你垂直方向是主轴,那么项目的高度就是 main size。
实现 flex 布局需要先指定一个容器,任何一个容器都可以被置顶为 flex 布局,这样容器内部的元素就可以使用 flex 来进行布局。简单说来,如果你使用块元素如 div,你就可以使用 flex,而如果你使用行内元素,你可以使用 inline-flex。
需要注意的是:当时设置 flex 布局之后,子元素的 float、clear、vertical-align 的属性将会失效。
<View style={{width:50,height:50,backgroundColor:'red'}}></View>
在React Native中,您并不需要学习什么特殊的语法来定义样式。仍然是使用JavaScript来写样式。所有的核心组件都接受名为style的属性。这些样式名基本上是遵循了web上的CSS的命名,只是按照JS的语法要求使用了驼峰命名法,例如将background-color改为backgroundColor。
style属性可以是一个普通的JavaScript对象。这是最简单的用法,因而在示例代码中很常见。你还可以传入一个数组——在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承。 比如:
<Text style={[styles.welcome,{fontSize:30}]}>
Hello,RN
</Text>
实际开发中组件的样式会越来越复杂,我们建议使用StyleSheet.create来集中定义组件的样式。比如像下面这样:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
button: {
backgroundColor:'black'
}
});
常见的做法是按顺序声明和使用style属性,以借鉴CSS中的“层叠”做法(即后声明的属性会覆盖先声明的同名属性)。
大多数组件在创建时就可以使用各种参数来进行定制。用于定制的这些参数就称为props(属性)。
以常见的基础组件Image为例,在创建一个图片时,可以传入一个名为source的prop来指定要显示的图片的地址,以及使用名为style的prop来控制其尺寸。
自定义的组件也可以使用props。通过在不同的场景使用不同的属性定制,可以尽量提高自定义组件的复用范畴。只需在render函数中引用this.props,然后按需处理即可。
我们使用两种数据来控制一个组件:props和state。props是在父组件中指定,而且一经指定,在被指定的组件的生命周期中则不再改变。 对于需要改变的数据,我们需要使用state。
一般来说,你需要在constructor中初始化state(译注:这是ES6的写法,早期的很多ES5的例子使用的是getInitialState方法来初始化state,这一做法会逐渐被淘汰),然后在需要修改时调用setState方法。
大多数组件在创建时就可以使用各种参数来进行定制。用于定制的这些参数就称为props(属性)。
假如我们需要制作一段不停闪烁的文字。文字内容本身在组件创建时就已经指定好了,所以文字内容应该是一个prop而文字的显示或隐藏的状态(快速的显隐切换就产生了闪烁的效果)则是随着时间变化的,因此这一状态应该写到state中。
典型的场景是在接收到服务器返回的新数据,或者在用户输入数据之后。你也可以使用一些“状态容器”比如Redux来统一管理数据流(译注:但我们不建议新手过早去学习redux)。
props是properties的缩写,您可以把任意类型的数据传递给子组件,如此子组件的默认数据源就是props。在使用中可以通过this.props.data调用props属性,但是切记不能用this.props.xxx修改属性,这会导致props错乱。可以把props当做const类型来看待。
state是组件的内部状态属性,主要用来存储组件自身需要的数据。 除了初始化时可能由props来决定,之后就完全由组件自身去维护。 组件中由系统定义了setState方法,每次调用setState时都会更新组件的状态,触发render方法重新渲染界面。 需要注意的是render方法是被异步调用的,这可以保证同步的多个setState方法只会触发一次render,这样做是有利于提高性能的。
React Native 的一个很大的技术特点就是它虽然用JavaScript语言开发,但是界面用原生端控件来渲染,从而获得平台上的组件本来就具备的所有特性和原生开发出来的界面一致的体验。
RN 中的原生组件通常包含原生端实现(用i平台原生语言开发)和JS端的载体(将原生控件的属性和方法暴露给JS端)两个部分。
原生组件使得RN具备很大的灵活性,RN官方自带的组件库不能满足需求的时候,可以随时将用原生语言开发的控件(包括开源的)封装成JS端可以使用的组件,这也使得RN 开源社区蓬勃发展,催生了各种开源RN组件,封装了各种高级组件,是RN官方组件的有益补充,降低了使用门槛,提高了开发效率。
React Native官方封装了一些API用来调用原生端能力,但是毕竟无法满足各种各样的需求。RN提供的原生模块特性为你提供了一种便捷的途径来将原生端的所有能力暴露给JS端调用。当RN官方还不支持某个你需要的原生特性时,你就可以用原生模块来自己实现该特性的封装。
RN原生模块的实现一般只需要编写原生端代码,暴露一些方法(支持回调,不支持直接返回值,因为跨语言通信是异步的)给JS端,JS端简单调用这些方法就可以了。有时为了使用方便,我们也可以将某个原生模块在JS端对应的实现一个类,封装里面的方法,这样调用方法的时候就可以利用IDE的代码自动补全功能,提高便利性,也可以同时封装一些额外的逻辑,比如参数校验等等。
python中一切皆对象,内置的对象类型有:数字、字符串、列表、字典、元组、文件、集合、布尔型、None等;编程单元类型有函数、模块、类,与实现相关的类型有编译的代码堆栈跟踪,还有套接字对象等。
(学会使用help万能函数,来探索功能哦)
下面,我们先来学习下基本的数据类型,也是核心数据类型。Python的基本数据类型有interge、boolean、string(字符串也称为序列)、list、tuple、dict,可以分为可变(list/dict)和不可变两类(interge/string/tuple)。
变量之赋值一切皆为引用,动态类型但是却是固定类型,不可动态转换
$ str = "abc" #动态类型
$ str + 1 #固定类型, ERROR: 不可动态转换
$ print 1 == 1 #print, 条件语句
包括整数、浮点数、复数、十进制数、有理分数、集合等。常支持加减乘除,双星号的乘方等。。。
import math
import random
用来记录文本信息,是python的一种序列。序列包含元素并从左到右或者其他的可定义明了的顺序,可以根据相对位置存取。严格意义上,字符串是单个字符的字符序列,其他的还包含列表、元组。
首先讲下编码,在C++中有宽字节字符、窄字节字符、CP_ACP、CP_UTF8等,文件还有各种格式,如果不遵循统一的规则,或者接入各种第三方模块却未有明确约束,会深陷泥潭不可自拔。
在python中,默认的文件编码是ascii的,默认文件中是不能输入中文的;但是我们可以在文件开始加入utf-8标识来解决。这在C++中是依赖系统的文件属性,太不程序员了。。。
UTF-8是unicode的一种标准的实现。
请动手google搜索下如下两个概念,以及其区别哦。。。
ASCII码
Unicode编码标准
python强制类型转换是失效的,如int("abc1222")是错误的
a = "中文"
#输出6,注意文件增加coding=utf-8也依旧输出6
print len(a)
#输出2
print len(u"中文");
b = a.decode('utf-8')
print len(b)
单引号,双引号,三引号:
三引号用于多行注释,单双引号可以用于嵌套引号
格式化%,format方法{偏移量占位},{name}
Python中进行模式匹配,需要导入re模块,包含了类似搜索、分割、替换等。
import re
match = re.match('hello[ \t]*(.*)[ \t]*world', 'Hello Python world')
match.group(1)
可存储任意类型对象,位置相关的,有序的集合,通过偏移来索引,支持嵌套,可变的类型。大小不固定,通过偏移量可以修改列表的大小。列表也是序列的一种,故支持各种序列操作。
最为强大的是,列表没有固定类型的约束。由于python中存储的元素可以看作是指针,故而可以在C++中理解为各种对象的指针void*
a = [1,2,3]
print a[-1]
b = [[1,2,3],[4,5,6]]
print b
["%s" % d for d in xrange(10)]
[(x,y) for x in range(2) for y in range(2)]
dict([(x,y) for x in range(2) for y in range(2)])
b=a,则b为列表的同一个引用
元组是一个有序的集合,通过偏移来取数据,属于不可变对象,不能原地修改无排序修改等操作
优点:保证数据安全,传递给一个不熟悉的方法或者数据接口时,可以保证方法和接口不会改变我们的数据,以免引发未知问题
a = (1,2,3)
a[1:3]
dir(a)
b = list(a)
b[0] = 5
type(b)
a = tuple(b)
print a
集合没有顺序的概念,不能使用切片和索引操作
字典是无序,不能通过偏移存取,只能通过键来存取。
Lua 是一门脚本语言,它有着一个重要的特性就是:它很容易嵌入其它语言。现在,越来越多的 C++ 服务器和客户端融入了脚本的支持,尤其在网游领域,脚本语言已经渗透到了方方面面,比如你可以在你的客户端增加一个脚本,这个脚本将会帮你在界面上显示新的数据,亦或帮你完成某些任务,亦或帮你查看别的玩家或者 NPC 的状态。对于游戏测试来说,大量繁杂的工作都可以由 Lua 代劳,因此在测试领域它也是一把利器。
关于 Lua 的一些重要基础知识:
Lua 有重要之重要的概念,就是栈。Lua 与别的语言交互以及交换数据,是通过栈完成的。其实简单的解释一下,你可以把栈想象成一个箱子,你要给他数据,就要按顺序一个个的把数据放进去,当然,Lua 执行完毕,可能会有结果返回给你,那么 Lua 还会利用你的箱子,一个个的继续放下去。而你取出返回数据呢,要从箱子顶上取出,如果你想要获得你的输入参数呢?那也很简单,按照顶上返回数据的个数,再按顺序一个个的取出,就行了。不过这里提醒大家,关于栈的位置,永远是相对的,比如 -1 代表的是当前栈顶,-2 代表的是当前栈顶下一个数据的位置。栈是数据交换的地方,一定要有一些栈的概念。
首先下载 Lua,一般情况下,很容易就可以通过 make macosx 或者 make linux 来编译。这样会生成 liblua.a 静态库文件,我们随后要连接这个库。
首先,我们要在开发工具中连接 liblua.a,然后要将 Lua 源代码文件的位置添加到编译器的头文件搜索目录列表里面去,这样我们的编译器才能找到 Lua 的头文件。
我们需要创建一个 C++ 的主程序,以便同 Lua 进行通信。
cpp 程序如下:
#include <iostream>
#include <lua.hpp>
extern "C" {
static int lua_cpp_function(lua_State* L) {
double arg = luaL_checknumber(L, 1);
lua_pushnumber(L, arg*0.5);
return 1;
}
}
int main(int argc, const char * argv[]) {
lua_State *L;
L = luaL_newstate();
cout << ">> 载入(可选)标准库,以便使用打印功能" << endl;
luaL_openlibs(L);
cout << ">> 载入文件,暂不执行" << endl;
if (luaL_loadfile(L, "luascript.lua")) {
cerr << "载入文件出现错误" << endl;
cerr << lua_tostring(L, -1) << endl;
lua_pop(L,1);
}
cout << ">> 从 C++ 写入数据 cppvar" << endl;
lua_pushnumber(L, 1.1);
lua_setglobal(L, "cppvar");
cout << ">> 执行 lua 文件" << endl << endl;
if (lua_pcall(L,0, LUA_MULTRET, 0)) {
cerr << "执行过程中出现错误" << endl;
cerr << lua_tostring(L, -1) << endl;
lua_pop(L,1);
}
cout << ">> 从 Lua 读取全局变量 luavar 到 C++" << endl;
lua_getglobal(L, "luavar");
double luavar = lua_tonumber(L,-1);
lua_pop(L,1);
cout << "C++ 从 Lua 读取到的 luavar = " << luavar << endl << endl;
cout << ">> 从 C++ 执行 Lua 的方法 myfunction" << endl;
lua_getglobal(L, "myluafunction");
lua_pushnumber(L, 5);
lua_pcall(L, 1, 1, 0);
cout << "函数返回值是:" << lua_tostring(L, -1) << endl << endl;
lua_pop(L,1);
cout << ">> 从 Lua 执行 C++ 的方法" << endl;
cout << ">>>> 首先在 Lua 中注册 C++ 方法" << endl;
lua_pushcfunction(L,lua_cpp_function);
lua_setglobal(L, "cppfunction");
cout << ">>>> 调用 Lua 函数以执行 C++ 函数" << endl;
lua_getglobal(L, "myfunction");
lua_pushnumber(L, 5);
lua_pcall(L, 1, 1, 0);
cout << "函数返回值是:" << lua_tonumber(L, -1) << endl << endl;
lua_pop(L,1);
cout << ">> 释放 Lua 资源" << endl;
lua_close(L);
return 0;
}
其次,是 lua 文件,我们将它命名为 luascript.lua
print("Hello from Lua")
print("Lua code is capable of reading the value set from C++", cppvar)
luavar = cppvar * 3
function myluafunction(times)
return string.rep("(-)", times)
end
function myfunction(arg)
return cppfunction(arg)
end
运行 cpp 文件,结果如下:
>> 载入(可选)标准库,以便使用打印功能
>> 载入文件,暂不执行
>> 从 C++ 写入数据 cppvar
>> 执行 lua 文件
Hello from Lua
Lua code is capable of reading the value set from C++ 1.1
>> 从 Lua 读取全局变量 luavar 到 C++
C++ 从 Lua 读取到的 luavar = 3.3
>> 从 C++ 执行 Lua 的方法 myfunction
函数返回值是:(-)(-)(-)(-)(-)
>> 从 Lua 执行 C++ 的方法
>>>> 首先在 Lua 中注册 C++ 方法
>>>> 调用 Lua 函数以执行 C++ 函数
函数返回值是:2.5
>> 释放 Lua 资源
lua_State *L;
L = luaL_newstate();
luaL_openlibs(L); // 载入(可选)标准库,以便使用打印功能
if (luaL_loadfile(L, "luascript.lua")) { // 载入文件,暂不执行
cerr << "载入文件出现错误" << endl;
cerr << lua_tostring(L, -1) << endl;
lua_pop(L,1);
}
上述代码创建 lua_State 并载入标准库,同时载入代码 luascript.lua
lua_pushnumber(L, 1.1);
lua_setglobal(L, "cppvar");
if (lua_pcall(L,0, LUA_MULTRET, 0)) {
cerr << "执行过程中出现错误" << endl;
cerr << lua_tostring(L, -1) << endl;
lua_pop(L,1);
}
在 C++ 中通过 lua_setglobal 在 Lua 中设置一个全局变量 cppvar。因为 C++ 和 Lua 通过 lua_State 中的堆栈来交换数据,所以要先 push 数据到堆栈,然后调用 lua_setglobal,这样就将数据赋给相应的值。
设置完全局的 cppvar 之后,执行 lua_pcall 来运行我们的 Lua 代码文件,之后,Lua 就可以使用 cppvar 变量。在 Lua 代码中,还创建了新的全局变量 luavar 可以供 C++ 访问。
lua_getglobal(L, "luavar");
double luavar = lua_tonumber(L,-1);
lua_pop(L,1);
cout << "C++ 从 Lua 读取到的 luavar = " << luavar << endl << endl;
要从 Lua 中读取数据,我们先要使用 lua_getglobal 将数据放到栈顶,然后将栈顶数据通过 lua_tonumber 转为 double,然后通过 lua_pop 将其移除出栈顶。
lua_getglobal(L, "myluafunction");
lua_pushnumber(L, 5);
lua_pcall(L, 1, 1, 0);
cout << "函数返回值是:" << lua_tostring(L, -1) << endl << endl;
lua_pop(L,1);
由了前面的过程,这个比较容易理解了:首先我们通过 lua_getglobal 取得方法名称,即将其放到栈顶,然后通过 lua_pushnumber 给参数赋值,然后通过 lua_pcall 执行。
执行之后,再去栈顶获取返回结果,然后 lua_pop 将其移除。
lua_pushcfunction(L,l_cppfunction);
lua_setglobal(L, "cppfunction");
lua_getglobal(L, "myfunction");
lua_pushnumber(L, 5);
lua_pcall(L, 1, 1, 0);
cout << "函数返回值是:" << lua_tonumber(L, -1) << endl << endl;
lua_pop(L,1);
这一段先是使用 lua_pushcfunction 来将 C++ 的方法 l_cppfunction 传递给 Lua,然后通过 lua_setglobal 给予其在 Lua 中的方法名称 cppfunction,接下来执行就很简单了。
lua_close(L);
以上就是一个简单的 Lua 与 C++ 交互的简单例子,算是入门的基础吧。我们可以看到,C++ 和 Lua 能够很自由的进行通信,而我们也可以很方便的修改 Lua 文件代码来实现对 C++ 程序流程的直接控制,对游戏开发、调试有很大的好处。
转自:https://indienova.com/indie-game-development/lua-as-script-with-cpp-development/
本文整理了工作中常用的批处理命令,汇总后可以便于查找和提高工作效率。
批处理是一系列DOS命令,通常用于自动执行重复性任务。批处理也称为批处理脚本,就是对某个对象进行批量处理,批处理文件扩展名为bat/cmd。批处理类似于Unix系统中的shell脚本。
批处理的常见规则如下:
@ #关闭单行回显
echo off #从下一行开始关闭回显
@echo off #从本行开始关闭回显,一般批处理第一行都是这条命令
echo on #从下一行开始打开回显
echo #显示当前回显状态
echo. #输出一个回车换行空白行
echo %errorlevel% #每个命令运行结束可以用这个命令行格式查看返回码,默认值为0,执行出错会设置为1
dir #显示当前目录中的文件和子目录
dir /a #显示当前目录中的文件和子目录,包含隐藏文件和系统文件
dir c: /a:d #显示当前目录中的目录
dir c: /a:-d #显示当前目录中的文件
dir c: /b/p #b显示文件名,p分页显示
dir *.exe /s #显示当前目录和子目录的所有exe文件
cd /d D:\
md d:\a\b\c #如果不存在将自动创建中级目录
rd abc #删除当前目录里的abc子目录,要求为空目录
rd /s/q d:\temp #删除文件夹及其子文件夹和文件,q指示为安静模式
del d:\test.txt #删除指定文件不能是隐藏、系统、只读文件
del /q/a/f d:\temp\*.* #删除temp文件夹里的所有文件,包含隐藏、只读、系统文件,不包含子目录
del /q/a/f/s d:\temp\*.* #同上,同时删除子文件夹里的文件,不包含子目录
copy c:\t.txt d:\b.bak #复制文件并重命名
copy con test.txt #从屏幕上等待输入,按ctrl+z结束输入,输入内容存为text.txt文件;con-屏幕,prn-打印机,nul-空设备
copy 1.txt + 2.txt 3.txt #合并1.txt和2.txt的内容保存到3.txt,如果不指定3.txt则存到1.txt
copy 1.txt + #复制文件到自己,实际是修改文件日期
date #显示当前日期,/t控制不提示输入新日期,输入时按回车可以略过输入
time #显示当前时间,/t控制不提示输入新时间,输入时按回车可以略过输入
:label #行首为:表示该行是标签行,标签行不执行操作
goto lable #跳转到指定的标签那一行
find "abc" c:\test.txt #find查找命令,在txt文件里查找含abc字符串的行,如果找不到将errorlevel返回码设为1
find /i "abc" c:\test.txt #查找,忽略大小写
find /c "abc" c:\test.txt #显示含abc的行的函数
more c:\test.txt #逐屏显示文件内容
& #顺序执行多条命令,而不管命令是否执行成功
&& #顺序执行多条命令,当遇到执行出错的命令后将不再执行后续命令
find "ok" c:\test.txt && echo 成功
|| #顺序执行多条命令,当碰到执行正确的命令后将不执行后续命令
find "ok" c:\test.txt || echo 不成功
| #管道命令
dir *.* /s/a | find /c ".exe" #先执行dir命令对其输出结果执行后边的find命令
> #清除文件中原有的内容后再写入
>> #追加内容到文件末尾而不会清除原有内容
type c:\test.txt >prn #屏幕上不显示文件内容,转向输出到打印机
echo hello world > con #在屏幕上显示,实际上所有输出默认都为con
copy C:\test.txt f: >nul #拷贝文件,不显示复制成功提示;如果f盘不存在会显示出错信息
copy c:\test.txt f: >nul 2>nul#拷贝文件,不显示复制成功提示,也不显示出错信息
echo ^^W ^> ^W > c:\test.txt #生成的文件内容为 ^W > W, ^和>是控制命令,要输出到文件需要在前面加个^符号
@echo off
:: 不等待输入直接修改当前日期
echo 2022-09-01 > temp.txt
data < temp.txt
del temp.txt
%0 #批处理文件本身
%1 #第一个参数
%2 #第二个参数
%9 #第九个参数
%* #从第一个参数开始的所有参数
%~1 #删除引号,扩充%1
%~f1 #将%1扩充到一个完全合格的路径名
%~d1 #仅将 %1 扩充到一个驱动器号
%~p1 #仅将 %1 扩充到一个路径
%~n1 #仅将 %1 扩充到一个文件名
%~x1 #仅将 %1 扩充到一个文件扩展名
%~s1 #扩充的路径指含有短名
%~a1 #将 %1 扩充到文件属性
%~t1 #将 %1 扩充到文件的日期/时间
%~z1 #将 %1 扩充到文件的大小
%~$PATH : 1 #查找列在 PATH 环境变量的目录,并将 %1扩充到找到的第一个完全合格的名称。如果环境变量名未被定义,或者没有找到文件,此组合键会扩充到空字符串
set #显示目前所有可用的变量,包括系统变量和自定义的变量
echo %SystemDrive% #显示系统盘盘符。系统变量可以直接引用
set p #显示所有以p开头的变量,要是一个也没有就设errorlevel=1
set p=aa1bb1aa2bb2 #设置变量p,并赋值为 = 后面的字符串,即aa1bb1aa2bb2
echo %p% #显示变量p代表的字符串,即aa1bb1aa2bb2
echo %p:~6% #显示变量p中第6个字符以后的所有字符,即aa2bb2
echo %p:~6,3% #显示第6个字符以后的3个字符,即aa2
echo %p:~0,3% #显示前3个字符,即aa1
echo %p:~-2% #显示最后面的2个字符,即b2
echo %p:~0,-2% #显示除了最后2个字符以外的其它字符,即aa1bb1aa2b
echo %p:aa=c% #用c替换变量p中所有的aa,即显示c1bb1c2bb2
echo %p:aa=% #将变量p中的所有aa字符串置换为空,即显示1bb12bb2
echo %p:*bb=c% #第一个bb及其之前的所有字符被替换为c,即显示c1aa2bb2
set p=%p:*bb=c% #设置变量p,赋值为 %p:*bb=c% ,即c1aa2bb2
set /a p=39 #设置p为数值型变量,值为39
set /a p=39/10 #支持运算符,有小数时用去尾法,39/10=3.9,去尾得3,p=3
set /a p=p/10 #用 /a 参数时,在 = 后面的变量可以不加%直接引用
set /a p=”1&0″ #”与”运算,要加引号。其它支持的运算符参见set/?
set p= #取消p变量
set /p p=请输入 #屏幕上显示”请输入”,并会将输入的字符串赋值给变量p,注意可以用来取代 choice 命令
这个比较复杂,请对照 for/? 来看
for %%i in (c: d: e: f:) do echo %%i
依次调用小括号里的每个字符串,执行 do 后面的命令
注意%%i,在批处理中 for 语句调用参数用2个%
默认的字符串分隔符是"空格键","Tab键","回车键"
for %%i in (*.txt) do find "abc" %%i
对当前目录里所有的txt文件执行 find 命令
for /r . %%i in (*.txt) do find "abc" %%i
在当前目录和子目录里所有的.txt文件中搜索包含 abc 字符串的行
for /r . %%i in (.) do echo %%~pni
显示当前目录名和所有子目录名,包括路径,不包括盘符
for /r d:mp3 %%i in (*.mp3) do echo %%i>>d:mp3.txt
把 d:mp3 及其子目录里的mp3文件的文件名都存到 d:mp3.txt 里去
for /l %%i in (2,1,8) do echo %%i
生成2345678的一串数字,2是数字序列的开头,8是结尾,1表示每次加1
for /f %%i in ('set') do echo %%i
对 set 命令的输出结果循环调用,每行一个
for /f "eol=P" %%i in ('set') do echo %%i
取 set 命令的输出结果,忽略以 P 开头的那几行
for /f %%i in (d:mp3.txt) do echo %%i
显示 d:mp3.txt 里的每个文件名,每行一个,不支持带空格的名称
for /f "delims=" %%i in (d:mp3.txt) do echo %%i
显示 d:mp3.txt 里的每个文件名,每行一个,支持带空格的名称
for /f "skip=5 tokens=4" %%a in ('dir') do echo %%a
对 dir 命令的结果,跳过前面5行,余下的每行取第4列
每列之间的分隔符为默认的"空格"
可以注意到 dir 命令输出的前5行是没有文件名的
for /f "tokens=1,2,3 delims=- " %%a in ('date /t') do (
echo %%a
echo %%b
echo %%c
)
对 date /t 的输出结果,每行取1、2、3列
第一列对应指定的 %%a ,后面的 %%b 和 %%c 是派生出来的,对应其它列
分隔符指定为 - 和"空格",注意 delims=- 后面有个"空格"
其中 tokens=1,2,3 若用 tokens=1-3 替换,效果是一样的
for /f "tokens=2* delims=- " %%a in ('date /t') do echo %%b
取第2列给 %%a ,其后的列都给 %%b
title 标题 #设置cmd窗口标题
cls #清屏
ver #显示系统版本
vol #显示卷标
label #显示卷标,同时提示输入新卷标
pause #暂停命令
rem #注释命令,注释不执行操作
:: #注释命令,注释不执行操作
start #批处理中调用外部程序的命令,否则等外部程序完成后才继续执行剩下的指令
call #批处理中调用另外一个批处理的命令,否则剩下的批处理指令将不会被执行
choice #选择命令,让用户输入一个字符,从而选择运行不同的命令,返回码errorlevel为1234……
@ECHO OFF
Rd "%WinDir%\system32\test_permissions" >NUL 2>NUL
Md "%WinDir%\System32\test_permissions" 2>NUL||(Echo 请使用右键管理员身份运行!&&PAUSE >NUL&&EXIT)
Rd "%WinDir%\System32\test_permissions" 2>NUL
@echo off
echo ----------------------------------------------------
echo Press any key to delete all files with ending:
echo *.aps *.idb *.ncp *.obj *.pch *.tmp *.sbr
echo Visual c++/.Net junk
echo ----------------------------------------------------
del /F /Q /S *.aps *.idb *.ncp *.obj *.pch *.sbr *.tmp *.bsc *.ilk *.res *.ncb *.opt *.suo *.manifest *.dep
mklink /D E:\dev\intermediate "F:\cache\build\dev"
cd /d "%~dp0"
注意这里的~dp是变量扩充,变量%0是批处理的路径。
习惯于windows的操作,学习下linux的基础操作。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.