PEP8 中文试译

这篇文章是两年前写的,现在回头来看当初尝试的好多翻译真是好蠢,不过弃之可惜,先留着。

本文仅代表个人认知、观点、经验,May be Stupid!

什么是PEP

PEP是 Python Enhancement Proposal 的缩写,翻译过来就是 Python增强建议书

PEP8

译者:本文基于 2013-08-02 最后修改的 PEP8 版本翻译,若要查看英文原文,请参考PEP8

简介

本文档给出的编码约定,来源于 Python 主发行版标准库中的代码。Python 的 C 语言实现所使用的 C 语言风格指南,请参考PEP7

本文档与 PEP 257(文档字符串规范)都来自于 Guido1Python Style Guido 论文原文,另外有来自 Barry's style guide 的补充。

随着 Python 语言自身的改变,本指南也在持续演进,新的编码约定被认同,而旧的矣被废弃。

许多项目都有一套专有的编码风格指南,当冲突发生时,应以项目编码规范为优先。

愚蠢的一致性就像没有脑子的妖怪

Guido 的一个核心观点认为,相比于被编写,代码更多的是被阅读。这篇指南意在提高代码的可读性并使之在广袤的 Python 编码中保持风格一致。就像 PEP 20 所表述的,"可读性当被重视2".

风格指南即一致性指南。本文档中描述的一致性是重要的,一个项目内代码的一致性则更重要一些,而一个模块或方法中代码的一致性则是最重要的。

但最终要的是:知道什么时候去打破一致性 — 风格指南并不总是适用。当存在不确定性时,做出你最好的抉择。你可以看看别人的代码是怎么写的,选择一种看起来最好的,并及时发问!

特别注意:不要为了遵守本 PEP 而破坏代码的向后兼容性!

当以下情况发生时,也是忽略某个风格指南的好理由:

  • 当遵守指南会降低代码可读性,甚至对于那些依循 PEP 去阅读代码的人也是这样时。
  • 当遵守指南会与其他部分的代码风格背离时 — 当然也许这是一个修正某些混乱代码的机会。
  • 当那些并没有遵循指南的旧代码已无法修改时。
  • 当你的代码需要与旧版本的 Python 保持兼容,而旧版本的 Python 不支持指南中提到的特性时。

代码布局

缩进

每次缩进使用 4 个空格。

续行3应该与被圆括号、方括号、花括号包裹起来的其他元素对齐,或者使用悬挂式缩进。当使用悬挂式缩进时,应该遵循这些注意事项:第一行不能有参数,应该使用进一步的缩进来将续行与其他行区分开。

符合本约定的代码:

# Aligned with opening delimiter
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# More indentation included to distinguish this from the rest.
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

不符合本约定的代码:

# Arguments on first line forbidden when not using vertical alignment
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# Further indentation required as indentation is not distinguishable
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

可选的符合约定的代码:

# Extra indentation is not necessary.
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

结尾的方括号/圆括号/花括号应该被放置在多行内容的最后一行的第一个非空字符的正下方4,如下所示:

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

或者被放置在多行内容的起始行的第一个字符的正下方5,如下所示:

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

制表符还是空格?

空格是首选的缩进方式。

为了保持一致性,在使用了制表符作为缩进的代码中,应该保持使用制表符。

Python 3 不支持空格缩进与制表符缩进混用。

Python 2 中的混用缩进代码也应该被转换为统一使用空格。

当使用 -t 选项来调用 Python 2 命令行工具时,运行混用缩进的代码会报出警告,当使用 -tt 选项时,运行混用缩进的代码会报出错误。强力建议使用这两个选项。

单行最大长度

将所有的行限制在 79 个字符以内。

对于那些具有很少的结构约束(例如文档字符串、注释)的代码段来说,最大行长度应该在在 72 个字符以内。

限制代码编辑窗口的宽度使并排编辑多个文件成为可能,并且在使用代码审核工具时,可以很好的在两个相邻列中显示不同的代码版本。

很多工具中的默认换行设置破坏了代码的可视结构,使其更难被理解。某些编辑器在换行时会在行尾放置标记字符,若限制代码的最大的长度,可以在这些最大宽度只有80个字符的编辑器中避免换行。而一些基于 Web 的工具也许根本不会提供动态自动换行功能。

一些团队更喜欢较长的单行代码。如果某个团队对单行代码长度的问题达成了共识,并且由该团队专门维护其代码的话,在将文档字符串与注释保持在 72 个字符以内的前提下,将名义上的单行代码的最大长度从 80 个字符提升到 100 个也是可以的(有效的将实际字符最大长度提高到了 99 个)。

Python 标准库是保守的,选择了将单行代码长度限制在 79 个字符以内(文档字符串/注释 72 个字符以内)。

最为推荐的长行换行方式是在圆括号、方括号、花括号内的 Python 隐式行续6。相比于使用反斜杠来转义续行,应该优先使用将长行放置于圆括号内来隐式续行的方式。

而某些时候反斜杠也是适于使用的。例如,较长的with语句不能使用隐式行续,就需要使用反斜杠了:

with open('/path/to/some/file/you/want/to/read') as file_1, \
        open('/path/to/some/file/being/written', 'w') as file_2:
    file_2.write(file_1.read())

另一个案例是,assert 语句中也需要反斜杠。

确保使用适当的行续缩进。在二元操作符两端,换行的推荐位置是在操作符之后,而不是操作符之前。以下是一些例子:

class Rectangle(Blob):

    def __init__(self, width, height,
                 color='black', emphasis=None, highlight=0):
        if (width == 0 and height == 0 and
                color == 'red' and emphasis == 'strong' or
                highlight > 100):
            raise ValueError("sorry, you lose")
        if width == 0 and height == 0 and (color == 'red' or
                                           emphasis is None):
            raise ValueError("I don't think so -- values are %s, %s" %
                             (width, height))
        Blob.__init__(self, width, height,
                      color, emphasis, highlight)

空白行

使用两个空白行来分隔顶级函数定义、类定义。

使用单个空白行来分隔类内的方法定义。

额外的空白行可以被(尽量少的)用来分隔几组相关的函数。在一堆相关的单行代码之间,空白行应该被省略。

在函数中(尽量少的)使用空白行来区分逻辑代码块。

Python 将 control-L (也就是, ^L) 换行符认作空白符。在许多工具中都将 control-L 识别做分页符,可以使用其来分页。但是注意,在某些编辑器或基于Web的代码浏览器中,control-L 是不会识别作换行符的,会被做为其他字符显示。

源文件编码

在 Python 的核心发布版中,应该主要使用 UTF-8 编码(或者在 Python 2 中使用 ASCII)。

在 Python 2 中使用 ASCII ,在 Python 3 中使用 UTF-8 时不应该在文件中进行编码声明。

在标准库中,往往只有以测试为目的的代码或包含非 ASCII 编码字符的作者名的注释中,才会使用非默认编码。否则,则推荐使用 \x, \u, \U, \N 等转义字符来在字符串文本中表示非 ASCII 字符。

在 Python 3.0 与更高级的 Python 版本中,对 Python 标准库的源文件编码作出了如下规定7:Python 标准库中的所有标示符必须仅使用 ACSII 编码的字符,在任何可能的时候都使用英文书写(在许多情况下,缩写名词和技术术语使用的是非英文)。另外,字符串文本与注释也必须使用 ASCII 编码。唯一的例外,是测试非 ASCII 编码特性的测试案例,与作者名的书写。对于非拉丁字符的作者名,应该将其翻译为拉丁字母书写。

推荐那些面向全球范围内开发者、用户的开源项目也遵循上述规定。

导入

  • import语句通常应该独立成行,例如:

    #符合约定的代码:     
    import os
    import sys
    
    #不符合本约定的代码:  
    import os,sys

    但这样的import也是的合理的:

    from subprocess import Popen, PIPE
  • Import 语句总应该被放到放到源码文件的最前端,即在模块注释与文档字符串之后,全局变量与常量定义之前。

    多条 Import 语句总应该遵循这样的顺序书写:

    1. 标准库的导入
    2. 相关第三方库导入
    3. 本地应用/库的相关导入

    在每组 import 语句应该使用空白行分隔。

    Put any relevant all specification after the imports.8

  • 建议使用绝对导入形式的 import 语句,它不仅更易读,并且在配置错误(例如某个包中的目录以 sys.path 结尾时)时有更良好的导入行为(至少有更好的报错):

    import mypkg.sibling
    from mypkg import sibling
    from mypkg.sibling import example

    相比于绝对导入,清晰的相对导入其实也是可以接受的,特别是当使用绝对导入需要处理不必要的复杂包布局时。

    from . import sibling
    from .sibling import example

    Python 标准库代码应该避免复杂的包布局,并且总是使用绝对导入。

    Python 3 中不应该使用相对导入,并且 Python 3 中该功能已被移除。

  • 当在某个包含类的模块中导入类时,这样的书写方式是合理的:

    from myclass import MyClass
    from foo.bar.yourclass import YourClass

    但如果这样的书写方式引起类名冲突,则请这样书写:

    import myclass
    import foo.bar.yourclass

    并使用 myclass.MyClassfoo.bar.yourclass.YourClass 来对其进行引用。

  • 通配符导入 (from <module> import *) 应该被禁止,因为这样做会导致在被导入的命名空间中存在哪些命名对象变得不清晰,迷惑读者与其他自动化工具。不过要使用通配符导入,也有站得住脚的理由:需要将内部 API 重新发布为公共 API(例如,使用纯 Python 重写一个可选加速模块的借口,而事先你并不知道这个接口将被重写)。

    当以这样的方式重发布命名时,下述的编码指南依然适用。

表达式与语句中的空白符

小问题

在以下情况中避免使用额外的空格:

  • 在圆括号、方括号、花括号内

    #符合约定的代码
    spam(ham[1], {eggs: 2})
    #不符合约定的代码
    spam( ham[ 1 ], { eggs: 2 } )
  • 在逗号、分号、冒号之前:

    #符合约定的代码
    if x == 4: print x, y; x, y = y, x
    #不符合约定的代码
    if x == 4 : print x , y ; x , y = y , x
  • 在函数调用的参数列表的左括号前

    #符合约定代码
    spam(1)
    #不符合约定的代码
    spam (1)
  • 在切片或索引的左方括号前

    #符合约定的代码
    dict['key'] = list[index]
    #不符合约定的代码
    dict ['key'] = list [index]
  • 在赋值(或其他)操作符两侧的多余一个的空格

    #符合约定的代码
    x = 1
    y = 2
    long_variable = 3
    #不符合约定的代码
    x             = 1
    y             = 2
    long_variable = 3

其他建议

  • 总是在下列二元操作符的两端使用单个空格:赋值操作符 (=),参数赋值 (+=, -= 等),比较操作符 (==, <, >, !=, <>, <=, >=, in, not in, is, is not),布尔操作符 (and, or, not)。

  • 加入使用了多个具有不同优先级的操作符,考虑在低优先级的操作符两侧使用空格。请自行判断,无论怎样,不要使用多余一个空格,并且保持二元操作符两端的空格数量一致。

    #符合约定的代码
    i = i + 1
    submitted += 1
    x = x*2 - 1
    hypot2 = x*x + y*y
    c = (a+b) * (a-b)
    #不符合约定的代码
    i=i+1
    submitted +=1
    x = x * 2 - 1
    hypot2 = x * x + y * y
    c = (a + b) * (a - b)
  • 不要在指示关键字参数或参数默认值的 = 符号两端使用空格。

    #符合约定的代码
    def complex(real, imag=0.0):
        return magic(r=real, i=imag)
    #不符合约定的代码
    def complex(real, imag = 0.0):
        return magic(r = real, i = imag)
  • 不建议使用符合语句(在一个物理行中存在多条语句)。

    #符合建议的代码
    if foo == 'blah':
        do_blah_thing()
    do_one()
    do_two()
    do_three()
    #不符合建议的代码
    if foo == 'blah': do_blah_thing()
    do_one(); do_two(); do_three()
  • 虽然有时把较短小的 if/for/while 语句放在同一物理行内也是可以的,但千万不要对多子句的语句也这样做,同时也是为了避免折叠长行。

    #不符合约定的代码
    if foo == 'blah': do_blah_thing()
    for x in lst: total += x
    while t < 10: t = delay()
    #绝对不要这样写
    if foo == 'blah': do_blah_thing()
    else: do_non_blah_thing()
    
    try: something()
    finally: cleanup()
    
    do_one(); do_two(); do_three(long, argument,
                                 list, like, this)
    
    if foo == 'blah': one(); two(); three()

注释

与代码相矛盾的注释不如没有,请时刻保持注释随代码更新。

注释应该是完整的句子。短语注释或整句注释都应当以大写字母开头,除非该注释以首字母小写的标示符开头。

段小注释后的句号可以省略,而在注释块中,每个完整的句子都应该以句号结尾。

在句尾句号之后,应该跟上两个空白符。

当撰写英文注释时,参考《英文写作指南》9

非英语程序员也请使用英文书写注释,除非你 120% 的保证,不会有不使用你的语言的人阅读你的代码。

块注释

通常快注释用来阐述跟在其后的代码,并且与代码一样重要。块注释中的每一行都应该以一个 # 符号加一个空白符开头(除非是注释块内的缩进行)。

块注释内的自然段以 # 开头的空行分割。

行内注释

尽量少的使用行内注释。

行内注释应该与语句在同一行内,与语句之间以至少两个空白符分割,并且以一个 # 符号加一个空白符开头

行内注释不是非常必要,并且当注释语义显而易见时会分散阅读者的注意力。例如,不要撰写这样的注释:

x = x + 1                 # Increment x

但某些时候,行内注释也是很有用的

x = x + 1                 # Compensate for border

文档字符串

关于编写好文档字符串的约定,请参考 PEP 257

请为所有的公开模块、类和方法编写文档字符串。对于非公开的方法,文档字符串不是必须的,但是仍应该为其撰写注释,表明其用途。方法注释应写在方法定义行之下。

PEP 257 描述了良好文档字符串的编写规范。注意,以 """ 结尾的文档字符串应该以该符号单独成行,最好前面还有一行空白行。 例如:

"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.

"""

对于单行的文档字符串来说,将 """ 放在该行的末尾也是可以的。

版本注记

如果你的代码中需要有版本变更标记,请像这样书写。

__version__ = "$Revision: 70b79ccd671a $"
# $Source$

以上内容请

These lines should be included after the module's docstring, before any other code, separated by a blank line above and below.


  1. Python 之父 Guido Van Rossum 

  2. Readability counts,参见 PEP 20 

  3. 译者注:将一个逻辑行分为多个物理行时除第一行之外的行 

  4. 译者注:例如下例中']'被放到'4'的正下方 

  5. 译者注:例如下例中')'被放到'r'的正下方 

  6. 译者注:即无需转义,在括号内直接换行 

  7. 参见 PEP 3131 

  8. 译者注:这句没有翻出来 

  9. 译者注:作者是小威廉·斯特伦克(William Strunk, Jr.)和E.B.怀特(Elwyn Brooks White),因此此书也被称 Strunk & White 

个人站搭建手册

这里是我购买 VPS,然后在上面搭建这个网站的笔记,供参考。

VPS 的选择

请看这里 速度更快的海外 VPS

搭建一个 VPN,翻阅长城

参照了 DigitalOcean 的教程,使用了 SoftEther 来创建了 VPN 服务。

另外,根据 SoftEther 的官方 L2TP/IPsec VPN Server 教程,要使用 SoftEther 的 L2TP/IPsec VPN 服务,需要在 iptables 中开启 UDP 5004500 端口。除此之外,为了使 L2TP 能正常连接,还需要开启 UDP 1701 端口。

安装 LEMP

为什么是 LEMP?
''因为 Nginx 是 EngineX''

安装 MySQL

如果买的是内存为 512M 的实例,需要做一个 SWAP 文件并挂载,否则 MySQL 无法安装成功。

dd if=/dev/zero of=/swapfile bs=1M count=1024
mkswap /swapfile
swapon /swapfile
echo 'swapon /swapfile' >> /etc/rc.d/rc.local

添加用户,并安装 MySQL ,我使用的使 percona 的 rpm 包。

useradd -s /sbin/nologin mysql

MySQl 安装好后,删除其中的匿名用户

mysql> delete from mysql.user where user='';
mysql> flush privileges;

源码安装 PHP

安装依赖包

yum install -y gcc gcc-c++ autoconf cmake libjpeg-devel libpng-devel freetype-devel libxml2-devel zlib-devel glibc-devel glib2-devel bzip2-devel curl-devel openssl-devel pcre-devel libmcrypt-devel

下载安装 php

tar zxf php-5.5.13.tar.gz
cd php-5.5.13
./configure --prefix=/usr/local/php --disable-fileinfo --enable-fpm --enable-ftp --with-gd --with-mysql --with-curl --with-mcrypt --with-zlib --enable-zip --with-jpeg-dir --with-png-dir --with-openssl --enable-mbstring --with-pdo-mysql
make
make install
ln -s /usr/local/php/bin/php /usr/bin/php

复制 php 配置文件

cp -f php.ini-production /usr/local/php/lib/php.ini
mv /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf

添加 php 的 bin 目录到 PATH

PATH=$PATH:$HOME/bin:/usr/local/php/bin/

源码安装 Memcached

安装 Memcached

useradd -M -s /sbin/nologin memcached
yum -y install libevent-devel

tar zxf memcached-1.4.20.tar.gz
cd memcached-1.4.20
./configure && make && make install

ln -s /usr/local/bin/memcached /usr/bin/memcached
mkdir /var/run/memcached
cp scripts/memcached.sysv /etc/init.d/memcached
chkconfig memcached on

添加 Memcached 配置文件,创建 ''/etc/sysconfig/memcached'' 文件并加入以下内容

PORT="11211"
USER="memcached"
MAXCONN="256"
CACHESIZE="32"
OPTIONS="-l 127.0.0.1"

启动 Memcached

service memcached start

安装 PHP 的 Memcached 扩展

tar zxf libmemcached-1.0.18.tar.gz
cd libmemcached-1.0.18
./configure && make && make install

tar zxf memcached-2.2.0.tgz
cd memcached-2.2.0
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config --disable-memcached-sasl
make && make install

安装 Nginx

tar zxf nginx-1.6.0.tar.gz
cd nginx-1.6.0
./configure --with-http_stub_status_module --with-http_ssl_module --prefix=/opt/nginx
make
make install

修改一些 nginx 常用设置:

error_log  logs/error.log;
pid        logs/nginx.pid;

两个方便的函数

在 ~/.bash_profile 中添加两个方便的函数,用来重载 Nginx 和 php-fpm

export PATH

# Reload Nginx
function rex()
{
    kill -HUP `cat /opt/nginx/logs/nginx.pid`
}

# Reload php-fpm
function rep()
{
    killall php-fpm
    /usr/local/php/sbin/php-fpm
}

配置 Nginx 支持 WordPress 固定链接

待完成

静态文件 CDN 加速等

使用 upyuan 参考:

http://www.84tt.com/web/2013/04/718.html
http://www.meshuo.com/archives/2280.html

使用单指针域实现双向链表

数学基础

离散数学中的异或运算 a⊕b,具有以下性质:

  • a⊕b = b⊕a
  • a⊕a = 0
  • a⊕0 = a
  • a⊕(a⊕b) = (a⊕a)⊕b = b
  • (a⊕b)⊕b = a⊕(b⊕b) = a

利用异或运算的这些性质,我们可以只用一个指针域,来实现一个双向链表。

单指针域双向链表的逻辑结构

下图是一个具有五个节点的双向链表的逻辑结构示意图,没有头结点。

SignlePointerDoublyLinkedList

其中每个节点的后半部分表示指针域,存储了它的前驱节点的指针与后继借点的指针的异或值。我们从最左边的节点开始遍历,就可以使用 0 ^ (0^P1) = P1 来得到下一个节点的指针(请注意,此处 0^P1 是一个整体,直接从节点的指针域中获得的)。继续往右走,又可以使用 P0 ^ (P0^P3) 来得到 P3,并以此类推。从右节点开始遍历也是同理(如:(P4^0) ^ 0 = P4)。

因此,按照上面的数据结构,就可以只使用一个指针域来实现双向链表了。

代码实现

#include <stdio.h>
#include <stdlib.h>

#define OK 1
#define ERROR 0

typedef int Status;

typedef struct DLink
{
    int data;
    unsigned long link;
} DLink;

// Initialize a SPDLL
Status CreateList(DLink **L, DLink **R, int *Elements, int n);
// Traverse from Left to Right
Status DispL(DLink *L);
// Traverse from Right to Left
Status DispR(DLink *R);

int main()
{
    DLink *L, *R;
    int Elements[] = {1, 2, 3, 4, 5};
    CreateList(&L, &R, Elements, 5);
    printf("Traverse from Left to Right: ");
    DispL(L);
    printf("\n");
    printf("Traverse from Right to Left: ");
    DispR(R);
    printf("\n");
}

// Initialize a SPDLL
Status CreateList(DLink **L, DLink **R, int *Elements, int n)
{
    DLink *pre, *q;
    int i;
    unsigned long l = 0, r = 0;

    // L 是指向头结点的指针
    (*L) = (DLink *)malloc(sizeof(DLink));
    (*L)->data = Elements[0];

    pre = *L;
    for (i=1; i<n; i++)
    {
        // 这是生成的新节点
        q = (DLink *)malloc(sizeof(DLink));
        // 为新节点的数据域赋值
        q->data = *(Elements+i);
        /* l 为 pre 前驱节点的指针地址经强制转换的长整型值,
        q 为 pre 的后继节点的地址,
        因此将 l ^ (unsigned long)q 赋值给 pre 的指针域 */
        pre->link = l ^ (unsigned long)q;
        // 然后将分别将 l 与 pre 向前移动一个身位 :D
        l = (unsigned long)pre;
        pre = q;
    }
    // 最后,再将末尾节点的指针域赋值为次末尾节点与0的异或值即可
    pre->link = (unsigned long)l ^ r;
    /* R 是指向末尾节点的指针,如果要从右向左遍历这个链表的话,
    需要从尾指针开始,因此每个单指针双向非循环链表都需要一个头指针和尾指针 */
    (*R) = pre;
    return OK;
}

// Traverse from Left to Right
Status DispL(DLink *L)
{
    unsigned long l = 0, next;
    DLink *p = L;
    // 首先将首节点的值直接输出
    printf("%d ", p->data);
    while (1)
    {
        // 由当前节点 p 的前驱节点与 p 的指针域异或得到下一个节点的地址
        next = l ^ ((unsigned long)(p->link));
        l = (unsigned long)p;
        if (next == 0) break;
        p = (DLink *)next;
        printf("%d ", p->data);
    }
    return OK;
}

// Traverse from Right to Left
// 从右往左遍历,需要从尾指针开始,因此每个单指针双向非循环链表都需要一个头指针和尾指针
Status DispR(DLink *R)
{
    unsigned long r = 0, prior;
    DLink *p = R;
    printf("%d ", p->data);
    while (1)
    {
        prior = ((unsigned long)(p->link)) ^ r;
        r = (unsigned long)p;
        if (prior == 0) break;
        p = (DLink *)prior;
        printf("%d ", p->data);
    }
    return OK;
}

执行结果

# gcc ./SinglePointDoubleLinkList.c -o ./SinglePointDoubleLinkList
# ./SinglePointDoubleLinkList
Traverse from Left to Right: 1 2 3 4 5
Traverse from Right to Left: 5 4 3 2 1

在函数内部使用 malloc 分配内存

错误的例子

下面这段程序中,main 函数中的 printf 语句会正确的打印 5 吗?

不会

#include <stdio.h>
#include <stdlib.h>

void ChangeN(int *p);

int main(void)
{
    // 定义了一个 int 类型的指针
    int *i;
    // 该函数试图给指针 i,分配一段内存并赋一个初值
    ChangeN(i);
    // 打印 i 指向的地址空间的值
    printf("%d\n", *i);
}

void ChangeN(int *p)
{
    p = malloc(sizeof(int));
    *p = 5;
}

执行结果

# gcc ./int.c -o ./int
# ./int
Segmentation fault

拙计

  1. main 函数中,指针变量 i 被声明之后,还只是一个空指针,并没有被分配空间
  2. 指针 p 作为形参,代码执行进入函数内后,p 依旧是一个空指针,而且与 i 不是同一个
  3. 函数 ChangeN 在其内部新申请了一块内存空间,并将这块内存空间的地址赋予了指针变量 p 中,但函数外的指针 i 并没有获得该地址。因此在 ChangeN 函数执行结束后,指针 p 被销毁了,他指向的地址空间也无从找寻了,i 还依旧是那个空指针,肯定就已经无法访问到 p 原本指向的空间了。对上面的程序稍加修改,就能很好的说明这个问题

例子二

#include <stdio.h>
#include <stdlib.h>

void ChangeN(int *p);

int main(void)
{
    int *i;
    ChangeN(i);
    // 修改1, 改为打印 i 指向的地址
    printf("i is located at %p\n", i);
}

void ChangeN(int *p)
{
    p = malloc(sizeof(int));
    *p = 5;
    printf("***** Start ChangeN *****\n");
    // 修改2,打印函数内 p 分配空间后,指向的空间地址
    printf("p is located at %p\n", p);
    printf("*p = %d\n", *p);
    printf("***** End ChangeN *****\n");
}

例子二执行结果

# gcc ./int_1.c -o int_1
# ./int_1
***** Start ChangeN *****
p is located at 0x1110010
*p = 5
***** End ChangeN *****
i is located at (nil)

正确的例子

正确的在函数内为外部的变量分配内存空间的方法也为:

  1. 在函数外部申请指针变量 i
  2. 将函数的形参设为指向指针的指针 p
  3. 将指针变量 i 的地址作为实参传递给函数
  4. 在函数内申请内存空间后,将地址赋给 *p
#include <stdio.h>
#include <stdlib.h>

// 将函数的形参声明为指向指针的指针
void ChangeN(int **p);

int main(void)
{
    int *i;
    // 将指针变量的变量地址传给函数
    ChangeN(&i);
    printf("%d\n", *i);
}

void ChangeN(int **p)
{
    // 为指针 p 指向的指针分配内存空间
    *p = malloc(sizeof(int));
    // 为指针 p 指向的指针赋值
    **p = 5;
}

执行结果

# gcc ./int_2.c -o ./int_2
# ./int_2
5

Getopt versus Getopts

I wrote a Chinese version of this post months ago to interpret some basic differce between bash command getopt and getopts. Now I am writing the English version to try to help me get a Bash Scripting related job on freelancer. 😀

Overview

Both getopt and getopts are used to get and parse the options and arguments of your shell script. In other word, if you write a shell script, now you want to add some user-defined options(eg. ‘–help’) to you new software, use this two commands.

What is the difference

  • getopts is a bash builtin command, getopt is an external software
  • getopts has a simple syntax, getopt is more complex but powerfull
  • getopts does not support long parameters like --help, getopt does support
  • getopts dose not reorder your parameters but getopt dose (will explain detailed below)
  • getopts is be constructed to do getopt‘s job simpler and quicker

Get closer to getopt

  • a simple getopt example
      #!/bin/bash
    
      ARGS=`getopt -o "ao:" -l "arg,option:" -n "getopt.sh" -- "$@"`
    
      eval set -- "${ARGS}"
    
      while true; do
          case "${1}" in
              -a|--arg)
              shift;
              echo -e "arg: specified"
              ;;
              -o|--option)
              shift;
              if [[ -n "${1}" ]]; then
                  echo -e "option: specified, value is ${1}"
                  shift;
              fi
              ;;
              --)
              shift;
              break;
              ;;
          esac
      done
    
  • execution results
      # ./getopt.sh -a
      arg: specified
      # ./getopt.sh -a -o Apple
      arg: specified
      option: specified, value is Apple
      # ./getopt.sh --arg --option Apple
      arg: specified
      option: specified, value is Apple
    
  • the same functions implemented by getopts
      #!/bin/bash
    
      while getopts "ao:" OPT; do
          case ${OPT} in
              "a")
              echo -e "a: specified, short for arg"
              ;;
              "o")
              echo -e "o: specified, short for option, value is ${OPTARG}"
              ;;
          esac
      done
    
  • execution results
      # ./getopts.sh -a -o Apple
      a: specified, short for arg
      o: specified, short for option, value is Apple
    
  • the parameters reorder ability of getopt
      #!/bin/bash
    
      echo "${@}"
      ARGS=`getopt -o "ao:" -l "arg,option:" -n "getopt_2.sh" -- "$@"`
      eval set -- "${ARGS}"
      echo "${@}"
    
      while true; do
          case "${1}" in
              -a|--arg)
              shift;
              echo -e "arg: specified"
              ;;
              -o|--option)
              shift;
              if [[ -n "${1}" ]]; then
                  echo -e "option: specified, value is ${1}"
                  shift;
              fi
              ;;
              --)
              shift;
              break;
              ;;
          esac
      done
    
      echo "${@}"
    
  • execution results
      # ./getopt_2.sh -o Apple Orange Banana
      -o Apple Orange Banana
      -o Apple -- Orange Banana
      option: specified, value is Apple
      Orange Banana
      # ./getopt_2.sh Orange Banana -o Apple
      Orange Banana -o Apple
      -o Apple -- Orange Banana
      option: specified, value is Apple
      Orange Banana
    

How dose getopt reorder the parameters but getopts not

Just like the last example above, getopt rearranged your parameters’ sequence. The meaning is that you can write parameters with - or -- behind or ahead other plain parameters(eg. Orange) when use getopt, but you cannot do this with getopts. The underlying reason is getopts put your parameters into the while loop directly, but getopts deal your parameters with the command set - ${ARGS} before put them into the while loop.

Another import thing your should notice is, the variable ${@} is ‘cleaned up’ after processed by set - ${ARGS}. You can still use ${1}, ${2} to get all parameters which are not led by - or --.