404频道

学习笔记

本文在学习saltstack的过程中编写,内容比较基础,方便使用时查阅命令。

安装

为了方便起见,直接采用yum的安装方式,centos源中并没有salt,需要手工添加一下。

CentOS 7

安装master

1
2
rpm -Uvh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/7/x86_64/e/epel-release-7-5.noarch.rpm
yum install salt-master

修改/etc/salt/master配置文件,在其中指定salt文件根目录位置,默认路径为/srv/salt/。

1
2
3
file_roots:
base:
- /svr/salt/

salt在安装的时候已经创建了systemctl命令启动程序需要的service文件,位于/usr/lib/systemd/system/salt-master.service,重启systemctl restart salt-master.service生效。

CentOS 6.5

安装minion

1
2
rpm -Uvh http://ftp.linux.ncsu.edu/pub/epel/6/i386/epel-release-6-8.noarch.rpm
yum install salt-minion

修改/etc/salt/minion配置文件,在其中指定master主机的地址

1
master: 192.168.204.128

执行service salt-minion restart对服务进行重启。

连通性测试

执行salt-key -L命令可以看到已认证和未认证的minion,执行salt-key -a 192.168.204.149可接收minion。

在master主机中执行salt '*' test.ping可测试连接的minion主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  stats  salt-key -L
Accepted Keys:
Denied Keys:
Unaccepted Keys:
192.168.204.149
Rejected Keys:

➜ stats salt-key -a 192.168.204.149
The following keys are going to be accepted:
Unaccepted Keys:
192.168.204.149
Proceed? [n/Y] y
Key for minion 192.168.204.149 accepted.

➜ stats salt '*' test.ping
192.168.204.149:
True

state

可以通过预先定义好的sls文件对被控主机进行管理,这里演示一个简单的文件复制的例子,该例子可以将master主机上的vimrc文件复制到目标主机上。

在master主机的/svr/salt/edit目录下新建vim.sls文件,文件内容如下:

1
2
3
4
5
6
/etc/vimrc:
file.managed:
- source: salt://edit/vimrc
- mode: 644
- user: root
- group: root

另外在edit目录下需要存在一个空的init.sls,以确保state.sls可以找到该目录下的sls文件。同时该目录下还需要存在要复制的vimrc文件。

执行salt '*' state.sls edit.vim即可以执行该命令。

如果将vim.sls更改为init.sls文件,执行salt '*' state.sls edit命令即可。

常用命令

salt ‘192.168.204.149’ cmd.run ‘free -m’

salt ‘192.168.204.149’ sys.list_modules 列出minion支持哪些模块,默认已经支持很多模块

salt ‘192.168.204.149’ cp.get_file salt://test_file /root/test_file 将master主机file_roots目录下的文件复制到minion任意目录下,该命令不可以将master主机任意目录下的文件进行复制

salt ‘192.168.204.149’ cp.get_dir salt://test_dir/ /root/ 实验未成功

salt ‘*’ file.mkdir dir_path=/root/test_dir user=root group=root mode=700 在minion主机上创建目录s

参考文章

*saltstack官方文档

*《python自动化运维技术与最佳实践》

前段时间我的工作有了一个比较大的变动,我的工作地点从济南到了北京,离开了待了9年的济南,离开了温馨的家,离开了我的亲人,独自一人开启了北漂模式。

本文不打算叙述面试细节问题,具体的面试细节我自己在印象笔记中整理过,但不打算放出来。本文仅选取有代表性的几家公司来叙述。

之所以有如此大的变动,最大的因素是我的个人技术的发展遇到了瓶颈。我近几年对个人发展的定位是在技术上能够更上一层楼,尽量不走管理路线,还是以踏踏实实学技术为主要任务。可是在济南工作已经慢慢被推上了管理的岗位,我怕会逐渐脱离技术,直到完全走上了管理的岗位,这跟我对技术非常感兴趣的初衷是有些违背的。

工作这几年,技术的进步主要来自于自己业余时间的学习,工作中越来越学不太着东西,不是因为工作中不需要牛逼的技术,而是没有人和精力去研究新技术,济南缺乏这个环境,而且是非常匮乏。我已经深深感觉到济南的IT行业未来会逐渐缩小,实际上目前济南的圈子就是很小的,稍微有点规模的公司也就那么几家而已。

我自从开始工作就以互联网公司为目标,可以济南没有一家真正意义上的互联网公司,用的技术也都不咋地,工作五年后,有了一定的技术积累,家庭也算稳定了,是时候出来闯闯了,不能在济南的安逸环境中,像水煮青蛙般等待着济南IT业的下滑。

这次来北京找工作的目标非常明确,互联网公司,最好是规模能够稍微大些的,BAT更好,不想加入A轮的公司,我需要的是成熟的互联网公司的环境和技术,来洗刷我已经在传统行业奋斗了多年的旧习。

我是裸辞的,因为毕竟面试是需要去北京的,在职请假面太麻烦,且不能够全身心的找工作。我给自己找工作的期限为一个月的时间,我最终上班的时间是在20天多点的时间。

在辞职后的第一周我在家里边休息边看了一遍《STL源码剖析》,之前一直觉得没必要看此书,结果看起来效果还不错,比我想象的要简单的多的多。另外,我制作了自己的简历,包括了pdf版本和markdown版本,并在拉勾网上投递了几份简历。

就这样第一周过去了,而我没有收到任何的面试通知,第二周我必须加紧开始找工作了。首先在100offer上申请拍卖了我的简历,之前一直关注100offer,微信公众账号和知乎上经常看到100offer的文章,感觉是个靠谱的平台,事实证明确实是一个靠谱的平台。另外,恰巧我在微信公共账号“余晟以为”的文章看到了怎样写简历的文章,就跟作者聊了一会,并且作者在twitter上推荐了我的简历,我的博客有史以来日pv达到了600多,这是我没有想到的,在这里非常感谢余晟的无私帮助。

100offer上拍卖后没多久就收到了二个面试通知,周三上午就赶到了北京参加面试。上午参加的一家游戏公司的面试,用C++做后台的业务逻辑处理,其实我并不喜欢游戏类工作,来面试也仅仅是为了了解北京面试的流畅增加一些资历而已。两天后收到了该公司的offer,而我理所当然是拒绝了。

周四上午参加了一个创业团队的面试,面试官年龄跟我差不多,问的问题非常多,非常杂,最后还有一个我最讨厌的逻辑题,一直面试到下午一点。这家公司的面试是通过twitter上看到我的简历联系我的,我来的目的其实也是出于学习和了解行业。面试完后跟团队成员一起吃了个饭,令我感动的是我面试完已经一点钟了,而大家都在等着我一起吃饭,团队的成员都是做技术的码农,还是非常好相处的。如果我已经有了几年的工作经历,或许我会选择这样一家公司。

下午参加了我目前所在公司一点资讯的面试,面试流程还是非常nice的,感觉跟我心目中的互联网公司基本吻合,虽然面试的时候问的问题过于简单些,令我有点怀疑公司的真实技术实力。另外还问了一堆我并不深入的web技术问题,令我非常捉急。

没有面试的时候,我就直接回济南了,毕竟在自己家里比北京不知舒服多少倍。

第三周的时候,我已经开始有些慌了。该用的能有面试机会的方式我都用了,而却收不到面试的邀请了。我用到了100offer、拉勾网、内推、jobdeer、同学内推的方式。难道是我简历写的太水了,可是我已经很难改进自己的简历了,我不想将简历写的夸张了。

我开始反思原因。我发现互联网用到的我擅长的C++技术的公司非常少,互联网追求的是短平快,C++并不具备开发速度快的特点,因此并不受互联网公司欢迎。像BAT类的公司用C++技术还是比较多的,因为做到一定程度会深究程序的性能,而这是C++擅长的。而BAT类的公司,我并没有任何优势,我虽有几年工作经验,但是都是在传统行业,互联网行业的经验却为0。很多大公司的hr在看到我的简历后直接就给pass掉了,压根没有面试的机会。

我找同学内推了百度的简历,另外在拉勾网上也投了一些百度的简历。在我入职之前的几天百度的hr妹子给我电话沟通说一周之内会给我面试通知,最终的结果是一周后百度hr妹子给我打电话让我去面试,看来大公司的流程真是复杂,连面试都得排队,而那天是我入职的第一天。没办法,只能委婉的拒绝了,hr倒是很爽快的挂掉了电话。

目前,我已经入职有三周的时间了,工作已经趋于稳定。我也搬到了公司附近,离公司就几分钟的路程,毕竟就自己一个人,没必要离公司太远,上下班太折腾。晚上一般会在公司待到十点以后,毕竟回去了也没太有什么事情。周末会抽时间回济南跟家人团聚,或者家人来北京,不期望因为工作的原因而对家庭有损伤。工作方面的内容还算满意,能涉及到很多新技术,对个人的成长还不错,只是组内的人员较少,沟通交流的机会不够多,通过招聘慢慢就会解决了。由于用到的很多技术都不够熟悉,自己俨然变成了一个菜鸟,有大量的技术需要学习。同事也还比较给力,大家对工作也很认真负责,团队的凝聚力也符合我的预期。

总结来看,这段找工作的经历虽有很多失误的地方,错误的对行业的需求进行了估计,以为C++程序员很抢手,事实并不是如此,但结果还算满意。现在自己一个人在北京奋斗,期望通过自己的努力能够有个好的收货。要想写的东西很多,很多都一笔带过了,非技术类的文章写起来还是挺费脑力的。

最近在开发程序的过程中遇到了一个getaddrinfo函数的问题,令我感到非常奇怪。

程序中调用了librdkafka库,当程序选择用-static方式链接所有库时程序会在librdkafka库中某个函数core dump,但是选择动态链接系统库(包括libpthread、libdl、libz、libm、libc等)时程序却能正常运行。

每次程序都回core dump在getaddrinfo函数中,经过搜索发现有人跟我遇到同样的问题,但是却没有解决方案。

我这里实验了文中提到了例子,在静态链接的时候确实会报错,动态链接却非常正常,编译选项为g++ -o test_getaddrinfo test_getaddrinfo.cpp -lpthread -Wall -static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
include <stdio.h>
#include <netdb.h>
#include <pthread.h>
#include <unistd.h>

void *test(void *)
{
struct addrinfo *res = NULL;
fprintf(stderr, "x=");
int ret = getaddrinfo("localhost", NULL, NULL, &res);
fprintf(stderr, "%d ", ret);
return NULL;
}

int main()
{
for (int i = 0; i < 512; i++)
{
pthread_t thr;
pthread_create(&thr, NULL, test, NULL);
}
sleep(5);
return 0;
}

发现程序在链接的时候会提示如下警告:

1
2
3
4
/tmp/cc0WILtn.o: In function `test(void*)':
test_getaddrinfo.cpp:(.text+0x49): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/lib/gcc/x86_64-redhat-linux/4.8.3/../../../../lib64/libpthread.a(libpthread.o): In function `sem_open':
(.text+0x685b): warning: the use of `mktemp' is dangerous, better use `mkstemp'

从网上查看有该警告的人还是非常多的,都是在-static方式链接glibc库时遇到的,但是没有发现很好的解决方案。该问题的原因
产生估计是glibc在静态链接时调用libnss库存在问题,因此不提倡静态链接方式。

我看到了两种解决方案:

方案一:用newlib或uClibc来代替glibc来静态链接,这种方案我没有去尝试是否可行。

方案二:用--enable-static-nss重新编译glibc。我试了一下问题仍然存在。

我之所以采用静态链接的方式,是因为开发机器和运行机器的glibc版本不一致造成的。我尝试将libc.so相关文件复制运行机器上,并让程序链接我复制过去的文件,ldd查看可执行文件没有错误,但是当运行程序时会报如下错误:

1
./xxx: relocation error: /home/kuring/lib/libc.so.6: symbol _dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference

最终,我放弃了静态链接的方式,采用了动态链接方式来暂时解决了问题。如果你知道解决方案,请告诉我。

参考文章

这是一篇拿来主义的文章,所有的安装步骤仅为互联网上查找,网络上的教程各种凌乱,这里根据我的实践情况进行了更改,本文仅记录了我的安装过程,由于不同环境可能导致安装步骤不甚相同。

MAC OS X 10.10

php

Mac OSX 10.10的系统自带了php、php-fpm,省去了安装的麻烦,可以执行php -v查看php的版本。这里需要简单地修改下php-fpm的配置,否则运行php-fpm会报错。

1
2
sudo cp /private/etc/php-fpm.conf.default /private/etc/php-fpm.conf
vim /private/etc/php-fpm.conf

修改php-fpm.conf文件中的error_log项,默认该项被注释掉,这里需要去注释并且修改为error_log = /usr/local/var/log/php-fpm.log。如果不修改该值,运行php-fpm的时候会提示log文件输出路径不存在的错误。

如果系统中存在多个php-fpm.conf,不知道需要编辑哪一个,可以执行php-fpm -t命令查看php-fpm要读取的配置文件。

通过php-fpm -D来启动php-fpm,可以通过lsof -Pni4 | grep LISTEN | grep php命令来查看php-fpm是否监听在9000端口。

nginx

这里为了简单,直接采用了brew的方式安装。执行

1
brew install nginx

nginx的配置文件位于/usr/local/etc/nginx/nginx.conf,默认只能解析html文件,需要配置后才能调用php-fpm解析php文件。下面内容为该修改后的文件全部有效内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
worker_processes  1;
events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
root /Users/kuring/www; // 页面存放路径
location / {
index index.html index.htm index.php;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
include servers/*;

然后执行nginx命令即可启动,默认监听的端口为8080,在浏览器中输入http://127.0.0.1:8080即可看到nginx的初始界面。nginx要想监听1024以下端口还需要进一步的配置,8080端口既能满足我需求,不再更改。

mysql

1
brew install mysql

在启动mysql之前可对mysql的配置文件进行更改,我这里需要更改mysql的编码方式,将所有的编码方式都更改为utf8,防止乱码问题的发生。mysql的配置文件为my.cnf,我的位于/usr/local/Cellar/mysql/5.6.25/my.cnf,对文件添加如下内容,有些选项不存在,可手动添加。

1
2
3
4
5
6
7
8
[mysqld]
character-set-server=utf8

[client]
default-character-set=utf8

[mysqld_safe]
default-character-set=utf8

输入mysqld命令即可启动mysql,启动mysql后输入mysql_secure_installation命令对mysql进行配置,可以设置root用户的密码。

通过mysql -uroot -p命令连接到mysql后,输入status命令可查看刚才更改的编码是否生效。

由于不需要长期使用mysql,这里不设置mysql自启动命令。

CentOS6

我首先采用的方案为完全用普通用户安装,尝试失败后采用root安装依赖库普通用户编译程序的方案。

普通用户安装依赖库

我首选选择在普通用户kuring下进行安装和运行整个web环境。因此不能使用yum安装方式,必须采用源码安装的方式。通过普通用户安装,最麻烦的地方就在于需要安装很多的依赖库,而依赖的库的安装可能又有需要的库,且库之间存在版本问题。

首先普及几个小知识:

bash查找命令的先后顺序为:

  1. alias别名
  2. shell中的关键字,如if等
  3. shell中的函数
  4. shell内置命令,如echo等
  5. $PATH环境变量,PATH中的匹配顺序为从前向后的。

程序查找lib库的先后顺序为:

  1. 编译程序时指定的链接库路径,g++编译器可以通过-Wl,-rpath,路径来指定链接库的路径。
  2. 环境变量LD_LIBRARY_PATH指定的搜索路径。
  3. /etc/ld.so.conf指定的路径。
  4. 默认的系统动态库搜索路径,如/usr/lib64、/usr/local/lib64等。

很多程序采用pkg-config程序来检查库的版本号,pkg-config命令依赖于动态链接库对应的.pc文件,这些.pc文件一般位于系统的/usr/local/lib/pkgconfig目录下。为了能够将安装完成的库通过pkg-config找到对应的.pc文件,需要将.pc文件所在的路径/home/kuring/local/lib/pkg-config设置到环境变量PKG_CONFIG_PATH中。

安装php依赖的libxml2库时提示找不到libtool、autoconf和automake,首先安装libtool。执行./configure --prefix=/home/kuring/local;make; make install将其安装到当前用户的local目录下。

用同样的步骤安装autoconf,执行./configure --prefix=/home/kuring/local;make; make install

为了能够将安装的程序起作用,需要将/home/kuring/local目录添加到PATH环境变量中,在.bash_profile文件中添加PATH=$PATH:$HOME/local/bin语句,并执行source ~/.bash_profile

安装之前需要先安装libxml2库,下载地址采用git clone git://git.gnome.org/libxml2的方式下载。在执行sh autogen产生configure配置文件的过程中,发现提示

1
2
./configure: line 13094: syntax error near unexpected token `LZMA,liblzma,'
./configure: line 13094: ` PKG_CHECK_MODULES(LZMA,liblzma,'

经过发现是由于找不到PKG_CHECK_MODULES造成的,正常情况下该函数定义在aclocal.m4文件,而该情况下aclocal.m4文件中并不存在该函数。之所以不存在是由于aclocal命令找不到pkg.m4文件造成的,可以通过aclocal --print命令查看查找的pkg.m4文件的路径。我这里的解决思路为直接从其他机器上复制一个pkg.m4文件过来。

在产生了configure命令后,执行./configure --prefix=/home/kuring/local命令后发现提示找不到Python.h命令的错误。

鉴于遇到了如此之多的错误,本着不浪费生命的原则还是采用yum来安装依赖库吧。

php

这里的mysql直接采用了yum命令安装的。

在执行php的./configure命令后提示libxml2找不到错误,直接yum install libxml-devel命令安装libxml-devel即可。然后执行./configure --enable-fpm --prefix=/home/kuring/php5.5 --with-mysqli=/usr/bin/mysql_config;make; make install;。在编译php的时候要加上php-fpm选项来安装php-fpm命令。

安装后配置~/.bash_profile文件的$PATH环境变量的值为:PATH=$HOME/bin:$HOME/php5.5/bin:$HOME/php5.5/sbin:$PATH。

此时即可通过php-fpm -D命令来启动php-fpm命令了。

安装完成后通过phpinfo()函数查看里面有MySQLi的选项,但是实际程序运行的时候居然不支持mysqli的一些力函数,说明mysqli的扩展安装不成功。在/home/kuring/php5.5/include/php/ext/mysqli目录中找到了对应的.h文件,却没有找到mysqli.so的动态链接库文件。大概是由于在编译php时mysql的路径配置有些问题造成的,因为mysql是通过yum安装的,路径比较乱一些。

为了能够产生mysqli.so文件,采用单独编译的方式,在php的源码目录中已经包含了mysqli的源码,进入mysqli源码目录下执行phpize;./configure --prefix=/home/kuring/php5.5/mysqli --with-php-config=/home/kuring/php5.5/bin/php-config --with-mysqli=/usr/bin/mysql_config;make;make install;。将mysqli.so文件安装到了/home/kuring/php5.5/lib/php/extensions/no-debug-non-zts-20121212目录下,不知道为什么目录末尾还要加个这么长的文件夹名,直接将文件复制到上一级目录下。

在/home/kuring/php5.5目录下没有找到php.ini文件,通过php --ini命令查看php的配置文件路径为/home/kuring/php5.5/lib,直接从php的源码文件中复制一个php.ini文件到该目录下。并将php.ini中的增加如下内容:

1
2
extension_dir = "/home/kuring/php5.5/lib/php/extensions"
extension=mysqli.so

再运行程序,发现mysqli的系列函数已经支持了,好一段折腾。

php-fpm

执行cp $HOME/php5.5/etc/php-fpm.conf.default $HOME/php5.5/etc/php-fpm.conf来增加配置文件。

nginx

首先安装pcre库,该库为正则表达式库。下载后通过

下载源码后执行./configure --prefix /home/kuring/nginx;make;make install;即可安装完成。

修改nginx的配置文件为如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
worker_processes  1;
events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
root /home/kuring/www;
location / {
index index.html index.htm index.php;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
include servers/*;

常见操作

nginx

  • nginx -s stop:关闭
  • nginx -t:检测nginx的配置是否正确

mysql

  • mysqld_safe:启动mysql
  • mysqladmin shutdown -u root -p:关闭mysql
  • create user kuring identified by ‘kuring_pass’:mysql创建用户(我尝试过几次,每次创建的用户密码都为空)
  • drop user kuring:删除一个用户
  • grant all privileges on . to ‘root‘@’%’ identified by ‘root’ with grant option :允许mysql的root用户通过远程登录

创建用户的操作

使用create user kuring identified by 'kuring_pass'命令创建用户kuring。默认创建完成的用户在本机无法登陆,但是远程却可以登陆。 这是因为mysql数据库中的user表中存在一条记录造成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use mysql;
select user,host,password from user;

// 在表中存在一条用户名为空的记录
+---------+-----------+-------------------------------------------+
| User | host | password |
+---------+-----------+-------------------------------------------+
| root | localhost | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| root | 127.0.0.1 | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| root | ::1 | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| | localhost | |
| root | % | *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B |
| report1 | % | *884CAA4D6FA1C3F7E4849C8DAF1B5B37FCB3EC0B |
+---------+-----------+-------------------------------------------+

// 将mysql中的为空的记录删除掉,这样就可以通过创建的用户连接了

在mysql命令行中执行grant all privileges on kuring_db.* to kuring identified by 'kuring_pass'命令可以给刚创建的用户对数据库的权限。

修改mysql用户密码

mysql将用户名和密码存放到了mysql数据库的user表中,在mysql命令行中执行use mysql;update user set password=password("new password") where user="username";flush privileges;即可更新相应用户的密码。

php-fpm

  • php-fpm -D:启动php-fpm,如果需要指定php.ini文件,可以使用-c参数
  • php-fpm -t:检查php-fpm的配置文件
  • kill -USR2: 重启php-fpm
  • kill -INT: 停止php-fpm

php

  • php –ini:显示php.ini文件路径

参考文章

  1. 运用Autoconf和Automake生成Makefile的学习之路

我在软件行业工作已经有五个年头了,在现在这家公司已经有两个年头了。虽然身为公司的研发部经理可以参与公司的一些决定,但是没有绝对的话语权,对于公司的很多决定我深知是错误的,虽然后来也证明是错误的,但是我仍然无能为力。这里总结一下在公司中遇到的问题。

宁可招聘一个技术水平高的也不愿招聘三个技术水平低的。在工作中能够非常有力的证明这一点,三个刚工作的技术人员,尤其对于C++这样门槛稍微高一些,需要工作经验来弥补C++中坑的语言,三个C++技术人员远没有一个高水平的工作效率高,因为三个菜鸟需要将大牛踩过的坑全部踩一遍,踩过多少坑就代表走了多少弯路。

兴趣是最大的老师。我带过不少人,很多都是新人,我给他们制定了学习计划,期望他们能够在业余时间多学习,但实际上哪有几个人能够充分利用业余时间的。我就非常怀疑他们对技术的兴趣问题,如果他们对技术不感兴趣那为什么要加入该行业,为没有兴趣的工作而工作就是自己对自己耍流氓。如果他们对技术感兴趣,那只能说明他们业余时间中有更大的诱惑。

在招聘中不要过于在意金钱,便宜无好货在招聘行业中仍然非常适用。在招聘中千万不要吝惜给员工的那点钱,因为一千块钱而错失一个好的员工是非常不值得的。

盈利模式决定了公司对产品的态度。我所在的软件行业属于传统的软件行业,传统软件行业的盈利模式为销售,由于软件具有可复制性的特点,因此只要一套产品卖的越多就赚的越多。对于传统软件行业的产品使用者很多情况下就是几个人,至少跟互联网产品的用户数量不在一个量级。使用的人数决定了传统软件行业的用户体验可以做的很烂,技术水平可以不用那么高,只要能用就行,慢点无所谓,只要能卖出去就行了。身为一个技术人员,一个对技术有追求的技术人员,这令我非常反感,我做技术我不能对技术无所谓,我讨厌听到无所谓这样的字眼。

一定要明确公司的定位,明白什么时候应该干什么,什么应该干,野心太大也是问题。公司处于成长阶段提出了今年营业额比去年增长10倍的目标,我听到之后就是嗤之以鼻,这压根就是不可能的任务,而事实证明这也根本不可能完成,实际上当年营业额仅比去年增长了一倍。

一家公司一定要有自己的明确产品线,要抵住外界的诱惑。公司的产品线本来是非常明确的,后来由于客户需求和各种方面的原因,开始考虑疯狂扩展产品,这就造成了本来人手就紧蹙的情况下,没有时间去改善现有的系统,不得不去研发新的产品。自己没有的产品甚至跟客户合作或者完全购买别人的产品,导致公司很多人都在考虑跟其他公司合作的事宜。结果可想而知,新产品的销售并不理想,旧有的产品升级维护的也开始变慢。ps:我是非常讨厌在技术上跟其他公司之间考虑合作的问题,因为这从本质上讲并没有产生任何的社会价值,技术上必然涉及到接口的问题,只要是接口必然会有很多细节问题,这些往往会出现技术人员扯皮的问题,一个问题你可以解决他也可以解决,但是谁都不愿意解决,你说烦不烦。

技术人员后来要么转行要么做管理了。在济南技术人员就这两种出路吧,没见过多少大龄的程序员,很多情况下写着写着程序突然发现自己转为公司的中层了,比如我,并逐渐参与公司的事务。很多对程序不感兴趣的,可能就直接换个行业或者转行做销售了。

有些人再怎么培养也成不了高手。在工作我发现,有些人即使有了几年的工作经验,对公司的产品也非常了解,但是在解决问题的时候总是找不到点子上,占了一大堆资源,最后解决起问题来即慢又绕弯路,还留下一堆bug。对于这部分人,我想说也许这个行业不适合你。

领导千万不可三天两头一个想法,这在员工看来就是一个不靠谱的领导。谁都不愿意追随一个拿着自己当猴耍的领导,一会一个想法只能说明领导不够成熟,不适合做领导。跟随杰出的人,为杰出的人工作。

搞公司最好不要搞施工太久的。公司很多做工程的都在为现场的情况忙碌,一个点架设完毕后往往还需要耗费大量的时间来维护,维护对于公司而言牵涉到精力太大,尽量避免需要整天跟客户打交道和整天维护的业务。

专科生是很难撑起一家科技企业。虽然我不完全认同学历就能决定能力,但学历跟能力之间是成正比关系的。我的朋友中有专科生在工作几年后可以做专业的视频教程,并且业余时间写过几部玄幻小说。由于学习经历的不同这就造就了科班出身的程度不同,自然能力之间是有差异的。虽然中国的大学教育跟工作很脱节,但是在工作中还是能够跟大学教育挂起钩来的。学历跟素质之间也是成正比关系的,这里的素质体现在工作中就包括了工作中的责任心,工作态度等方面,这里就不展开了,要展开的话我可以举出非常多活生生的例子。因此,我非常不提倡在公司招聘中招聘专科生。我发现在很多情况下,很多专科人员是连普通话都不会的,操着各种方言或各种被普通话的方言。基本上能不能说普通话也是一个断定人素质的标准,扩展到其他行业同样适用。

也许本文的观点有些偏激,没错我就是一个偏激的IT工程师,就酱。

在C++98中有左值和右值的概念,不过这两个概念对于很多程序员并不关心,因为不知道这两个概念照样可以写出好程序。在C++11中对右值的概念进行了增强,我个人理解这部分内容是C++11引入的特性中最难以理解的了。该特性的引入至少可以解决C++98中的移动语义和完美转发问题,若你还不清楚这两个问题是什么,请向下看。

温馨提示,由于内容比较难懂,请仔细看。C++已经够复杂了,C++11中引入的新特性令C++更加复杂了。在学习本文的时候一定要理解清楚左值、右值、左值引用和右值引用。

移动构造函数

首先看一个C++98中的关于函数返回类对象的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class MyString {
public:
MyString() {
_data = nullptr;
_len = 0;
printf("Constructor is called!\n");
}

MyString(const char* p) {
_len = strlen (p);
_init_data(p);
cout << "Constructor is called! this->_data: " << (long)_data << endl;
}

MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
cout << "Copy Constructor is called! src: " << (long)str._data << " dst: " << (long)_data << endl;
}

~MyString() {
if (_data)
{
cout << "DeConstructor is called! this->_data: " << (long)_data << endl;
free(_data);
}
else
{
std::cout << "DeConstructor is called!" << std::endl;
}
}

MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
cout << "Copy Assignment is called! src: " << (long)str._data << " dst" << (long)_data << endl;
return *this;
}

operator const char *() const {
return _data;
}

private:
char *_data;
size_t _len;

void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
};

MyString foo()
{
MyString middle("123");
return middle;
}

int main() {
MyString a = foo();
return 1;
}

该例子在编译器没有进行优化的情况下会输出以下内容,我在输出的内容中做了注释处理,如果连这个例子的输出都看不懂,建议再看一下C++的语法了。我这里使用的编译器命令为g++ test.cpp -o main -g -fno-elide-constructors,之所以要加上-fno-elide-constructors选项时因为g++编译器默认情况下会对函数返回类对象的情况作返回值优化处理,这不是我们讨论的重点。

1
2
3
4
5
6
Constructor is called! this->_data: 29483024 // middle对象的构造函数
Copy Constructor is called! src: 29483024 dst: 29483056 // 临时对象的构造,通过middle对象调用复制构造函数
DeConstructor is called! this->_data: 29483024 // middle对象的析构
Copy Constructor is called! src: 29483056 dst: 29483024 // a对象构造,通过临时对象调用复制构造函数
DeConstructor is called! this->_data: 29483056 // 临时对象析构
DeConstructor is called! this->_data: 29483024 // a对象析构

在上述例子中,临时对象的构造、复制和析构操作所带来的效率影响一直是C++中为人诟病的问题,临时对象的构造和析构操作均对堆上的内存进行操作,而如果_data的内存过大,势必会非常影响效率。从程序员的角度而言,该临时对象是透明的。而这一问题正是C++11中需要解决的问题。

在C++11中解决该问题的思路为,引入了移动构造函数,移动构造函数的定义如下。

1
2
3
4
5
6
MyString(MyString &&str) {
cout << "Move Constructor is called! src: " << (long)str._data << endl;
_len = str._len;
_data = str._data;
str._data = nullptr;
}

在移动构造函数中我们窃取了str对象已经申请的内存,将其拿为己用,并将str申请的内存给赋值为nullptr。移动构造函数和复制构造函数的不同之处在于移动构造函数的参数使用*&&*,这就是下文要讲解的右值引用符号。参数不再是const,因为在移动构造函数需要修改右值str的内容。

移动构造函数的调用时机为用来构造临时变量和用临时变量来构造对象的时候移动语义会被调用。可以通过下面的输出结果看到,我们所使用的编译参数为g++ test.cpp -o main -g -fno-elide-constructors --std=c++11

1
2
3
4
5
6
Constructor is called! this->_data: 22872080 // middle对象构造
Move Constructor is called! src: 22872080 // 临时对象通过移动构造函数构造,将middle申请的内存窃取
DeConstructor is called! // middle对象析构
Move Constructor is called! src: 22872080 // 对象a通过移动构造函数构造,将临时对象的内存窃取
DeConstructor is called! // 临时对象析构
DeConstructor is called! this->_data: 22872080 // 对象a析构

通过输出结果可以看出,整个过程中仅申请了一块内存,这也正好符合我们的要求了。

C++98中的左值和右值

我们先来看下C++98中的左值和右值的概念。左值和右值最直观的理解就是一条语句等号左边的为左值,等号右边的为右值,而事实上该种理解是错误的。左值:可以取地址,有名字的值,是一个指向某内存空间的表达式,可以使用&操作符获取内存地址。右值:不能取地址,即非左值的都是右值,没有名字的值,是一个临时值,表达式结束后右值就没有意义了。我想通过下面的例子,读者可以清楚的理解左值和右值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lvalues:
//
int i = 42;
i = 43; // i是左值
int* p = &i; // i是左值
int& foo();
foo() = 42; // foo()返回引用类型是左值
int* p1 = &foo(); // foo()可以取地址是左值

// rvalues:
//
int foobar();
int j = 0;
j = foobar(); // foobar()是右值
int* p2 = &foobar(); // 编译错误,foobar()是右值不能取地址
j = 42; // 42是右值

C++11右值引用和移动语义

在C++98中有引用的概念,对于const int &m = 1,其中m为引用类型,可以对其取地址,故为左值。在C++11中,引入了右值引用的概念,使用*&&*来表示。在引入了右值引用后,在函数重载时可以根据是左值引用还是右值引用来区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void fun(MyString &str)
{
cout << "left reference" << endl;
}

void fun(MyString &&str)
{
cout << "right reference" << endl;
}

int main() {
MyString a("456");
fun(a); // 左值引用,调用void fun(MyString &str)
fun(foo()); // 右值引用,调用void fun(MyString &&str)
return 1;
}

在绝大多数情况下,这种通过左值引用和右值引用重载函数的方式仅会在类的构造函数和赋值操作符中出现,被例子仅是为了方便采用函数的形式,该种形式的函数用到的比较少。上述代码中所使用的将资源从一个对象到另外一个对象之间的转移就是移动语义。这里提到的资源是指类中的在堆上申请的内存、文件描述符等资源。

前面已经介绍过了移动构造函数的具体形式和使用情况,这里对移动赋值操作符的定义再说明一下,并将main函数的内容也一起更改,将得到如下输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
MyString& operator=(MyString&& str) { 
cout << "Move Operator= is called! src: " << (long)str._data << endl;
if (this != &str) {
if (_data != nullptr)
{
free(_data);
}
_len = str._len;
_data = str._data;
str._len = 0;
str._data = nullptr;
}
return *this;
}

int main() {
MyString b;
b = foo();
return 1;
}

// 输出结果,整个过程仅申请了一个内存地址
Constructor is called! // 对象b构造函数调用
Constructor is called! this->_data: 14835728 // middle对象构造
Move Constructor is called! src: 14835728 // 临时对象通过移动构造函数由middle对象构造
DeConstructor is called! // middle对象析构
Move Operator= is called! src: 14835728 // 对象b通过移动赋值操作符由临时对象赋值
DeConstructor is called! // 临时对象析构
DeConstructor is called! this->_data: 14835728 // 对象b析构函数调用

在C++中对一个变量可以通过const来修饰,而const和引用是对变量约束的两种方式,为并行存在,相互独立。因此,就可以划分为了const左值引用、非const左值引用、const右值引用和非const右值引用四种类型。其中左值引用的绑定规则和C++98中是一致的。

非const左值引用只能绑定到非const左值,不能绑定到const右值、非const右值和const左值。这一点可以通过const关键字的语义来判断。

const左值引用可以绑定到任何类型,包括const左值、非const左值、const右值和非const右值,属于万能引用类型。其中绑定const右值的规则比较少见,但是语法上是可行的,比如const int &a = 1,只是我们一般都会直接使用int &a = 1了。

非const右值引用不能绑定到任何左值和const右值,只能绑定非const右值。

const右值引用类型仅是为了语法的完整性而设计的, 比如可以使用const MyString &&right_ref = foo(),但是右值引用类型的引入主要是为了移动语义,而移动语义需要右值引用是可以被修改的,因此const右值引用类型没有实际意义。

我们通过表格的形式对上文中提到的四种引用类型可以绑定的类型进行总结。

引用类型/是否绑定 非const左值 const左值 非const右值 const右值 备注
非const左值引用
const左值引用 全能绑定类型,绑定到const右值的情况比较少见
非const右值引用 C++11中引入的特性,用于移动语义和完美转发
const值引用 没有实际意义,为了语法完整性而存在

下面针对上述例子,我们看一下foo函数绑定参数的情况。

如果只实现了void foo(MyString &str),而没有实现void fun(MyString &&str),则和之前一样foo函数的实参只能是非const左值。

如果只实现了void foo(const MyString &str),而没有实现void fun(MyString &&str),则和之前一样foo函数的参数即可以是左值又可以是右值,因为const左值引用是万能绑定类型。

如果只实现了void foo(MyString &&str),而没有实现void fun(MyString &str),则foo函数的参数只能是非const右值。

强制移动语义std::move()

前文中我们通过右值引用给类增加移动构造函数和移动赋值操作符已经解决了函数返回类对象效率低下的问题。那么还有什么问题没有解决呢?

在C++98中的swap函数的实现形式如下,在该函数中我们可以看到整个函数中的变量a、b、c均为左值,无法直接使用前面移动语义。

1
2
3
4
5
6
7
template <class T> 
void swap ( T& a, T& b )
{
T c(a);
a=b;
b=c;
}

但是如果该函数中能够使用移动语义是非常合适的,仅是为了交换两个变量,却要反复申请和释放资源。按照前面的知识变量c不可能为非const右值引用,因为变量a为非const左值,非const右值引用不能绑定到任何左值。

在C++11的标准库中引入了std::move()函数来解决该问题,该函数的作用为将其参数转换为右值。在C++11中的swap函数就可以更改为了:

1
2
3
4
5
6
7
template <class T> 
void swap (T& a, T& b)
{
T c(std::move(a));
a=std::move(b);
b=std::move(c);
}

在使用了move语义以后,swap函数的效率会大大提升,我们更改main函数后测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() { 
// move函数
MyString d("123");
MyString e("456");
swap(d, e);
return 1;
}

// 输出结果,通过输出结果可以看出对象交换是成功的
Constructor is called! this->_data: 38469648 // 对象d构造
Constructor is called! this->_data: 38469680 // 对象e构造
Move Constructor is called! src: 38469648 // swap函数中的对象c通过移动构造函数构造
Move Operator= is called! src: 38469680 // swap函数中的对象a通过移动赋值操作符赋值
Move Operator= is called! src: 38469648 // swap函数中的对象b通过移动赋值操作符赋值
DeConstructor is called! // swap函数中的对象c析构
DeConstructor is called! this->_data: 38469648 // 对象e析构
DeConstructor is called! this->_data: 38469680 // 对象d析构

右值引用和右值的关系

这个问题就有点绕了,需要开动思考一下右值引用和右值是啥含义了。读者会凭空的认为右值引用肯定是右值,其实不然。我们在之前的例子中添加如下代码,并将main函数进行修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void test_rvalue_rref(MyString &&str)
{
cout << "tmp object construct start" << endl;
MyString tmp = str;
cout << "tmp object construct finish" << endl;
}

int main() {
test_rvalue_rref(foo());
return 1;
}

// 输出结果
Constructor is called! this->_data: 28913680
Move Constructor is called! src: 28913680
DeConstructor is called!
tmp object construct start
Copy Constructor is called! src: 28913680 dst: 28913712 // 可以看到这里调用的是复制构造函数而不是移动构造函数
tmp object construct finish
DeConstructor is called! this->_data: 28913712
DeConstructor is called! this->_data: 28913680

我想程序运行的结果肯定跟大多数人想到的不一样,“Are you kidding me?不是应该调用移动构造函数吗?为什么调用了复制构造函数?”。关于右值引用和左右值之间的规则是:

如果右值引用有名字则为左值,如果右值引用没有名字则为右值。

通过规则我们可以发现,在我们的例子中右值引用str是有名字的,因此为左值,tmp的构造会调用复制构造函数。之所以会这样,是因为如果tmp构造的时候调用了移动构造函数,则调用完成后str的申请的内存自己已经不可用了,如果在该函数中该语句的后面在调用str变量会出现我们意想不到的问题。鉴于此,我们也就能够理解为什么有名字的右值引用是左值了。如果已经确定在tmp构造语句的后面不需要使用str变量了,可以使用std::move()函数将str变量从左值转换为右值,这样tmp变量的构造就可以使用移动构造函数了。

而如果我们调用的是MyString b = foo()语句,由于foo()函数返回的是临时对象没有名字属于右值,因此b的构造会调用移动构造函数。

该规则非常的重要,要想能够正确使用右值引用,该规则必须要掌握,否则写出来的代码会有一个大坑。

完美转发

前面已经介绍了本文的两大主题之一的移动语义,还剩下完美转发机制。完美转发机制通常用于库函数中,至少在我的工作中还是很少使用的。如果实在不想理解该问题,可以不用向下看了。在泛型编程中,经常会遇到的一个问题是怎样将一组参数原封不动的转发给另外一个函数。这里的原封不动是指,如果函数是左值,那么转发给的那个函数也要接收一个左值;如果参数是右值,那么转发给的函数也要接收一个右值;如果参数是const的,转发给的函数也要接收一个const参数;如果参数是非const的,转发给的函数也要接收一个非const值。

该问题看上去非常简单,其实不然。看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

using namespace std;

void fun(int &) { cout << "lvalue ref" << endl; }
void fun(int &&) { cout << "rvalue ref" << endl; }
void fun(const int &) { cout << "const lvalue ref" << endl; }
void fun(const int &&) { cout << "const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T t) { fun(t); }

int main()
{
PerfectForward(10); // rvalue ref

int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref

const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref

return 0;
}

在上述例子中,我们想达到的目的是PerfectForward模板函数能够完美转发参数t到fun函数中。上述例子中的PerfectForward函数必然不能够达到此目的,因为PerfectForward函数的参数为左值类型,调用的fun函数也必然为void fun(int &)。且调用PerfectForward之前就产生了一次参数的复制操作,因此这样的转发只能称之为正确转发,而不是完美转发。要想达到完美转发,需要做到像转发函数不存在一样的效率。

因此,我们考虑将PerfectForward函数的参数更改为引用类型,因为引用类型不会有额外的开销。另外,还需要考虑转发函数PerfectForward是否可以接收引用类型。如果转发函数PerfectForward仅能接收左值引用或右值引用的一种,那么也无法实现完美转发。

我们考虑使用const T &t类型的参数,因为我们在前文中提到过,const左值引用类型可以绑定到任何类型。但是这样目标函数就不一定能接收const左值引用类型的参数了。const左值引用属于左值,非const左值引用和非const右值引用是无法绑定到const左值的。

如果将参数t更改为非const右值引用、const右值也是不可以实现完美转发的。

在C++11中为了能够解决完美转发问题,引入了更为复杂的规则:引用折叠规则和特殊模板参数推导规则。

引用折叠推导规则

为了能够理解清楚引用折叠规则,还是通过以下例子来学习。

1
2
3
4
5
6
7
8
9
10
typedef int& TR;

int main()
{
int a = 1;
int &b = a;
int & &c = a; // 编译器报错,不可以对引用再显示添加引用
TR &d = a; // 通过typedef定义的类型隐式添加引用是可以的
return 1;
}

在C++中,不可以在程序中对引用再显示添加引用类型,对于int & &c的声明变量方式,编译器会提示错误。但是如果在上下文中(包括使用模板实例化、typedef、auto类型推断等)出现了对引用类型再添加引用的情况,编译器是可以编译通过的。具体的引用折叠规则如下,可以看出一旦引用中定义了左值类型,折叠规则总是将其折叠为左值引用。这就是引用折叠规则的全部内容了。另外折叠规则跟变量的const特性是没有关系的。

1
2
3
4
A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&

特殊模板参数推导规则

下面我们再来学习特殊模板参数推导规则,考虑下面的模板函数,模板函数接收一个右值引用作为模板参数。

1
2
template<typename T>
void foo(T&&);

说白点,特殊模板参数推导规则其实就是引用折叠规则在模板参数为右值引用时模板情况下的应用,是引用折叠规则的一种情况。我们结合上文中的引用折叠规则,

  1. 如果foo的实参是上文中的A类型的左值时,T的类型就为A&。根据引用折叠规则,最后foo的参数类型为A&。
  2. 如果foo的实参是上文中的A类型的右值时,T的类型就为A&&。根据引用折叠规则,最后foo的参数类型为A&&。

解决完美转发问题

我们已经学习了模板参数为右值引用时的特殊模板参数推导规则,那么我们利用刚学习的知识来解决本文中待解决的完美转发的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

using namespace std;

void fun(int &) { cout << "lvalue ref" << endl; }
void fun(int &&) { cout << "rvalue ref" << endl; }
void fun(const int &) { cout << "const lvalue ref" << endl; }
void fun(const int &&) { cout << "const rvalue ref" << endl; }

//template<typename T>
//void PerfectForward(T t) { fun(t); }

// 利用引用折叠规则代替了原有的不完美转发机制
template<typename T>
void PerfectForward(T &&t) { fun(static_cast<T &&>(t)); }

int main()
{
PerfectForward(10); // rvalue ref,折叠后t类型仍然为T &&

int a;
PerfectForward(a); // lvalue ref,折叠后t类型为T &
PerfectForward(std::move(a)); // rvalue ref,折叠后t类型为T &&

const int b = 8;
PerfectForward(b); // const lvalue ref,折叠后t类型为const T &
PerfectForward(std::move(b)); // const rvalue ref,折叠后t类型为const T &&

return 0;
}

例子中已经对完美转发的各种情况进行了说明,这里需要对PerfectForward模板函数中的static_cast进行说明。static_cast仅是对传递右值时起作用。我们看一下当参数为右值时的情况,这里的右值包括了const右值和非const右值。

1
2
3
4
5
6
7
// 参数为右值,引用折叠规则引用前
template<int && &&T>
void PerfectForward(int && &&t) { fun(static_cast<int && &&>(t)); }

// 引用折叠规则应用后
template<int &&T>
void PerfectForward(int &&t) { fun(static_cast<int &&>(t)); }

可能读者仍然没有发现上述例子中的问题,“不用static_cast进行强制类型转换不是也可以吗?”。别忘记前文中仍然提到一个右值引用和右值之间关系的规则,如果右值引用有名字则为左值,如果右值引用没有名字则为右值。。这里的变量t虽然为右值引用,但是是左值。如果我们想继续向fun函数中传递右值,就需要使用static_cast进行强制类型转换了。

其实在C++11中已经为我们封装了std::forward函数来替代我们上文中使用的static_cast类型转换,该例子中使用std::forward函数的版本变为了:

1
2
template<typename T>
void PerfectForward(T &&t) { fun(std::forward<T>(t)); }

对于上文中std::move函数的实现也是使用了引用折叠规则,实现方式跟std::forward一致。

引用

  1. 《深入理解C++11-C++11新特性解析与应用》
  2. C++11 标准新特性: 右值引用与转移语义
  3. 如何评价 C++11 的右值引用(Rvalue reference)特性?
  4. C++11 完美转发
  5. C++ Rvalue References Explained
  6. 详解C++右值引用 (对C++ Rvalue References Explained的翻译)

最近粗读了一遍《大型网站技术架构-核心原理与案例分析》,并对其中的内容通过思维导图的形式进行了整理。本书的所讲解的内容均为大型网站中涉及到的问题及相关技术,但并未展开深入讨论相关技术的解决办法,非常适合入门。下面我将我的思维导图以图片的形式贴出来,并提供XMind编辑的.xmid格式的文件。

下载

大型网站技术架构读书笔记

问题描述

无线wifi的essid支持英文和中文,中文的编码在802.11协议并没有规定,对于802.11协议而言仅将essid看作是二进制。而中文又存在多种编码方式,最常见的就是GB18030(我这里直接用GB18030代替了GB系列的字符集)和UTF-8了。

iwlist程序通过命令iwlist wlan0 scanning可以在终端上正常显示UTF-8编码的essid,对于其他编码的中文仍然是乱码,这也就非常容易理解了。因为具体的essid能否将中文正常显示在终端屏幕上跟essid的编码和当前终端环境的编码是否能够匹配有关,如果essid的编码和当前终端环境的编码均为UTF-8,则essid可以在屏幕上正常显示。如果当前网络中的可以搜索到的essid即包含了GB18030编码又包含了UTF-8编码,则打印在终端上的essid必然会有乱码的情况出现。

airodump-ng程序问题

对于airodump-ng程序而言,即时是essid的编码和终端编码一致也会出现某些中文字符乱码的问题,这一点比较奇怪。比如“免费”中的“免”字是乱码,“费”却能正常显示。通过这一现象有理由怀疑airodump-ng对essid做了某些处理。

经过查看源码发现,在airodump-ng.c文件中存在三处如下类似代码,作用为将essid中的ascii值在(126,160)之间的转换为”.”。看来airodump-ng程序并没有考虑到中文的情况,仅将ascii中无法显示的字符做了转换。将程序中的三处代码注释后就可以正常显示了。具体三处代码可以通过搜索’.’来查找。

1
2
3
4
5
6
7
8
9
for( i = 0; i < n; i++ )
{
c = p[2 + i];
if( c == 0 || ( c > 126 && c < 160 ) )
{
c = '.'; //could also check ||(c>0 && c<32)
}
st_cur->probes[st_cur->probe_index][i] = c;
}

NetworkManager

通过实践发现,GNOME和KDE桌面下的查看无线网络连接的ssid是可以正常显示的,即可以正常显示GB18030,又可以正常显示UTF-8编码的essid。则可以推测,在桌面环境下的搜索网络的程序肯定对编码做了某些处理,顺着这个思路,就可以查找GNOME或KDE的代码了。

在GNOME的源码中看到了network-manager-applet,该程序即为桌面上查看无线网络连接的小控件。在applet-device-wifi.c文件中看到了如下代码,其中的nm_utils_ssid_to_utf8函数即为将其他编码转换为UTF-8编码的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static char *
get_ssid_utf8 (NMAccessPoint *ap)
{
char *ssid_utf8 = NULL;
const GByteArray *ssid;

if (ap) {
ssid = nm_access_point_get_ssid (ap);
if (ssid)
ssid_utf8 = nm_utils_ssid_to_utf8 (ssid);
}
if (!ssid_utf8)
ssid_utf8 = g_strdup (_("(none)"));

return ssid_utf8;
}

nm_utils_ssid_to_utf8函数定义在NetworkManager工程中的nm-utils.c文件中。该函数的代码如下,该函数具体功能可以查看代码中的注释,已经非常详细了。其中以g_开头的函数是glib库中的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
char *
nm_utils_ssid_to_utf8 (const GByteArray *ssid)
{
char *converted = NULL;
char *lang, *e1 = NULL, *e2 = NULL, *e3 = NULL;

g_return_val_if_fail (ssid != NULL, NULL);

if (g_utf8_validate ((const gchar *) ssid->data, ssid->len, NULL))
return g_strndup ((const gchar *) ssid->data, ssid->len);

/* LANG may be a good encoding hint */
g_get_charset ((const char **)(&e1));
if ((lang = getenv ("LANG"))) {
char * dot;

lang = g_ascii_strdown (lang, -1);
if ((dot = strchr (lang, '.')))
*dot = '\0';

get_encodings_for_lang (lang, &e1, &e2, &e3);
g_free (lang);
}

converted = g_convert ((const gchar *) ssid->data, ssid->len, "UTF-8", e1, NULL, NULL, NULL);
if (!converted && e2)
converted = g_convert ((const gchar *) ssid->data, ssid->len, "UTF-8", e2, NULL, NULL, NULL);

if (!converted && e3)
converted = g_convert ((const gchar *) ssid->data, ssid->len, "UTF-8", e3, NULL, NULL, NULL);

if (!converted) {
converted = g_convert_with_fallback ((const gchar *) ssid->data, ssid->len,
"UTF-8", e1, "?", NULL, NULL, NULL);
}

return converted;
}

nm_utils_ssid_to_utf8该函数位于libnm-util.so.1动态库中,可通过nm -D /usr/lib64/libnm-util.so.1 | grep nm_utils_ssid_to_utf8命令查看导出表中存在该函数。但是系统中并不存在该函数的头文件libnm-util.h,给该库的调用增加了不少难度。可以通过将相关头文件引入到该工程编译的方式来完成,但是可能会牵涉到的头文件比较多,比较繁琐。

我这里直接采用了将NetworkManager中相关代码抓取出来的思路,并将其封装成类的形式以方便调用。具体代码可以参照demo中的例子。

glib

glib是GTK底层调用的核心库,跟glibc是没有关系的,虽然名字中仅差一个字母。为了调用该库需要在编译的时候添加*pkg-config --cflags --libs glib-2.0*信息,以引入需要的头文件和要链接的库。

相关下载

文中用到的软件源码和程序demo

引用

题目一 Single Number

Given an array of integers, every element appears twice except for one. Find that single one.
Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

题目二 Single Number II

Given an array of integers, every element appears three times except for one. Find that single one.
Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

题目一分析及解答

针对题目一,一看就能看出是考察异或操作的特点,并迅速写出了解答方法。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int singleNumber(int A[], int n) {
int result = 0;
for (int i = 0; i < n; i++)
{
result ^= A[i];
}
return result;
}
};

题目二分析及解答

要想实现时间复杂度为O(n),空间复杂度为O(1)的算法,还是跟题目一一样需要充分利用位操作特性,但是并没有直接可用的位操作特性可以完成,于是想到肯定是各种位操作的组合操作,但是并没有继续向下想到具体的算法。本质上该题目就是模拟一个三进制的操作,当一个位的最大值为2,当为3时直接清0。

参照网上的算法,利用一个int类型的数组来模拟一个三进制数,每个int值的最大值为3,当然这样存在一定空间上的浪费。算法需要将A中的每个值通过移位运算获取到该位的状态,并将值添加到用来模拟三进制的int数组中相应的位置,最后将模拟三进制int数组中的值为3的更改为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int singleNumber(int A[], int n) {
int count[32] = {0};
int result = 0;
for (int i = 0; i < 32; i++) {
for (int j = 0; j < n; j++{
if ((A[j] >> i) & 1) {
count[i]++;
}
}
result |= ((count[i] % 3) << i);
}
return result;
}
};

另外,还有上述算法的改进算法,更为节省空间,效率更高,但是确实不容易理解和记忆,属于下次仍然无法记忆的算法类型。这里仅提供代码,不再给出解释,自己领悟。

1
2
3
4
5
6
7
8
9
10
11
12
int singleNumber(int A[], int n) {
int ones = 0, twos = 0, threes = 0;
for (int i = 0; i < n; i++) {
twos |= ones & A[i];
ones ^= A[i];// 异或3次 和 异或 1次的结果是一样的
//对于ones 和 twos 把出现了3次的位置设置为0 (取反之后1的位置为0)
threes = ones & twos;
ones &= ~threes;
twos &= ~threes;
}
return ones;
}

前段时间参加了牛客网的答题活动,共两套试题,每套题目3个算法题,我只做了每套题的前两道。最近想查看之前做的题目的答案,却发现非常不方便,特此将我做过的4道题目记录一下,算法的思路就不再解释了。

题目一 奇数位上都是奇数或者偶数位上都是偶数

给定一个长度不小于2的数组arr。 写一个函数调整arr,使arr中要么所有的偶数位上都是偶数,要么所有的奇数位上都是奇数上。 要求:如果数组长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1),下标0,2,4,6…算作偶数位,下标1,3,5,7…算作奇数位,例如[1,2,3,4]调整为[2,1,4,3]即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
void oddInOddEvenInEven(vector<int>& arr, int len) {
int odd = 1;
int even = 0;
while (odd < len && even < len)
{
if (arr[odd] % 2 == 0)
{
while (arr[even] % 2 == 0)
{
even += 2;
}
if (even < len)
{
int tmp = arr[even];
arr[even] = arr[odd];
arr[odd] = tmp;
}
else
{
break;
}
}
else
{
odd += 2;
}
}
}
};

题目二 求正数数组的最小不可组成和

给定一个全是正数的数组arr,定义一下arr的最小不可组成和的概念: 1,arr的所有非空子集中,把每个子集内的所有元素加起来会出现很多的值,其中最小的记为min,最大的记为max; 2,在区间[min,max]上,如果有一些正数不可以被arr某一个子集相加得到,那么这些正数中最小的那个,就是arr的最小不可组成和; 3,在区间[min,max]上,如果所有的数都可以被arr的某一个子集相加得到,那么max+1是arr的最小不可组成和; 举例: arr = {3,2,5} arr的min为2,max为10,在区间[2,10]上,4是不能被任何一个子集相加得到的值中最小的,所以4是arr的最小不可组成和; arr = {3,2,4} arr的min为2,max为9,在区间[2,9]上,8是不能被任何一个子集相加得到的值中最小的,所以8是arr的最小不可组成和; arr = {3,1,2} arr的min为1,max为6,在区间[2,6]上,任何数都可以被某一个子集相加得到,所以7是arr的最小不可组成和; 请写函数返回arr的最小不可组成和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
int getFirstUnFormedNum(vector<int>& arr, int len) {
set<int> res;
for (int i=0; i<len; i++)
{
set<int> tmp = res;
for (set<int>::iterator iter = res.begin(); iter != res.end(); iter++)
{
tmp.insert(*iter + arr[i]);
}
res = tmp;
res.insert(arr[i]);
}

set<int>::iterator iter = res.begin();
int before = *iter;
iter++;
for (; iter != res.end(); iter++)
{
if (*iter - before > 1)
{
return before + 1;
}
before = *iter;
}
return before + 1;
}
};

题目三 最大的LeftMax与rightMax之差绝对值

给定一个长度为N的整型数组arr,可以划分成左右两个部分: 左部分arr[0..K],右部分arr[K+1..arr.length-1],K可以取值的范围是[0,arr.length-2] 求这么多划分方案中,左部分中的最大值减去右部分最大值的绝对值,最大是多少? 例如: [2,7,3,1,1] 当左部分为[2,7],右部分为[3,1,1]时,左部分中的最大值减去右部分最大值的绝对值为4; 当左部分为[2,7,3],右部分为[1,1]时,左部分中的最大值减去右部分最大值的绝对值为6; 最后返回的结果为6。 注意:如果数组的长度为N,请尽量做到时间复杂度O(N),额外空间复杂度O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
int getMaxABSLeftAndRight(vector<int> vec, int len) {
if (len == 0)
{
return 0;
}

// find the max in array
int max = vec[0];
for (int i=1; i<(int)vec.size(); i++)
{
if (vec[i] > max)
{
max = vec[i];
}
}

// compare the head and tail in array
if (vec[0] < vec[len - 1])
{
return max - vec[0];
}
return max - vec[len - 1];
}
};

题目四 按照左右半区的方式重新组合单链表

给定一个单链表的头部节点head,链表长度为N。 如果N为偶数,那么前N/2个节点算作左半区,后N/2个节点算作右半区; 如果N为奇数,那么前N/2个节点算作左半区,后N/2+1个节点算作右半区; 左半区从左到右依次记为L1->L2->…,右半区从左到右依次记为R1->R2->…。请将单链表调整成L1->R1->L2->R2->…的样子。 例如: 1->2->3->4 调整后:1->3->2->4 1->2->3->4->5 调整后:1->3->2->4->5 要求:如果链表长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
public:
void relocateList(struct ListNode* head) {
if (head == NULL || head->next == NULL)
{
return ;
}

// use one loop, find the right head
ListNode *right_head = head;
ListNode *node = head;
while (node != NULL)
{
if (node->next == NULL)
{
break;
}
if (node->next->next == NULL)
{
right_head = right_head->next;
break;
}
right_head = right_head->next;
node = node->next->next;
}

ListNode *left_node = head;
ListNode *right_node = right_head;
while (left_node->next != right_head)
{
ListNode *tmp = left_node->next;
left_node->next = right_node;
right_node = right_node->next;
left_node->next->next = tmp;
left_node = left_node->next->next;
}

left_node->next = right_node;
}
};
0%