字符串参数的模板函数推导问题

国庆长假期间又翻了翻 《C++ Primer》,看到模板函数特化,就想起来以前遇到的一个问题。这个问题我曾经在 TopLanguage 讨论组请教过(链接),今天翻出来又仔细想了想,做一个总结吧。

困惑起源于以字符串作为参数,如何匹配到特化的模板函数。代码如下,其中注释部分是该句对应的输出(使用 VS2008 编译器,一会儿再讨论 g++ 的问题):

#include <iostream>
#include <typeinfo>
using namespace std;

template<typename T>
void foo(const T& t)
{
  cout << "foo: generic " << typeid(t).name() << endl;
}

template<>
void foo<const char *>(const char * const& t)
{
  cout << "foo: special " << typeid(t).name() << endl;
}

template<typename T>
void bar(const T t)
{
  cout << "bar: generic " << typeid(t).name() << endl;
}

template<>
void bar<const char *>(const char * t)
{
  cout << "bar: special " << typeid(t).name() << endl;
}

int main()
{
  char str[] = "hello";
  const char con_str[] = "hello";
  const char * const p = "hello";
  foo("hello");                                  // foo: generic char const [6]
  foo(static_cast<const char * const>("hello")); // foo: special char const *
  foo(static_cast<const char *>("hello"));       // foo: special char const *
  foo(str);                                      // foo: generic char const [6]
  foo(con_str);                                  // foo: generic char const [6]
  foo(p);                                        // foo: special char const *
  bar("hello");                                  // bar: special char const *
  bar(str);                                      // bar: generic char *
  bar(con_str);                                  // bar: special char const *
  bar(p);                                        // bar: special char const *
  cout << typeid("hello").name() << endl;        // char const [6]
  cout << typeid(str).name() << endl;            // char [6]
  cout << typeid(con_str).name() << endl;        // char const [6]
  cout << typeid(p).name() << endl;              // char const *
  return 0;
}

首先让我奇怪的问题是,第一个 foo 函数调用 foo("hello"),为什么实际调用的不是特化的 foo 函数?

其实这个例子是有起源的,《C++ Primer》第四版 Section 16.6.1 的最后给出这样一个例子:

// define the general compare template
template <class T>
int compare(const T& t1, const T& t2) { /* ... */ }

int main() {
    // uses the generic template definition
    int i = compare("hello", "world");
    // ...
}

// invalid program: explicit specialization after call
template<>
int compare<const char*>(const char* const& s1,
                         const char* const& s2)
{ /* ... */ }

并解释说:

This program is in error because a call that would match the specialization is made before the specialization is declared. When the compiler sees a call, it must know to expect a specialization for this version. Otherwise, the compiler is allowed to instantiate the function from the template definition.

那么我认为作者暗含的意思里有,compare("hello", "world") 这个函数调用是 match 特化的 compare 函数的。但是从我们给出的第一段代码输出来看,并不是这个样子的,所以我谨慎地怀疑,《C++ Primer》给出的这个例子是有错的。虽然这段程序的确有错,但是即使将特化函数提到前面,compare("hello", "world") 仍然不会调用该特化函数。

请教了别人、书本和标准之后,下面我试着对上面每句的输出做一下解释(当然,可能有错,请指正):

1.   foo("hello");                                  // foo: generic char const [6]

"hello"具有类型 char const [6],由于 foo 模板使用的是引用参数,因此数组实参不会被转换成指针,而是追求一个较为精确的匹配,因此编译器实例化一个 void foo<char const [6]>(const char (& t)[6]) 模板函数(VS2008),这也是为什么我们能看到参数的类型输出是 char const [6];

2.   foo(static_cast<const char * const>("hello")); // foo: special char const *

"hello"被 cast 成了 const char * const 类型,自然与特化的函数 void foo<const char *>(const char * const& t) 能够精确匹配,因此调用的是特化的 foo;

3.   foo(static_cast<const char *>("hello"));       // foo: special char const *

"hello"被 cast 成了 const char * 类型,虽然少了一个 const,但是 C++ 标准中有这样的说法:

14.8.2.3
If the orignial A is a reference type, A can be more cv-qualified than the deduced A

这种 cv-qualifier 并不影响推导,最终仍然是匹配到特化的 foo 函数;

4.   foo(str);                                      // foo: generic char const [6]

str 和 "hello" 也是仅仅相差一个 cv-qualifier,也不影响推导,其结果与 1 是一致的;

5.   foo(con_str);                                  // foo: generic char const [6]

con_str 和 "hello" 的类型一样,显然其结果与 1 应是一致的;

6.   foo(p);                                        // foo: special char const *

p 的类型其实就是 2 中参数被 cast 之后的类型,显然其结果应该与 2 一致;

7.   bar("hello");                                  // bar: special char const *

乍一看就有些奇怪,为什么把模板参数换成值(而不是引用),特化的情况就与 foo 不同了呢?C++ 标准中有这样的规定:

14.8.2.3
If A is not a reference type:
-- If P is an array type, the pointer type produced by the array-to-pointer standard conversion (4.2) is used in place of P for type deduction;

因此,这里 "hello" 原本是一个数组类型,由于模板的参数不是引用类型,所以 "hello" 的类型被转换为指针类型 char const * 参加推导,正好与特化的 bar 函数匹配;

8.   bar(str);                                      // bar: generic char *

由于模板参数不是引用类型,没有 const 限定的 str 无法匹配特化的 bar,因此编译器实例化一个 void bar<char *>(char * t) 模板函数;

9.   bar(con_str);                                  // bar: special char const *

由于 con_str 与 "hello" 的类型一样,因此其结果与 7 是一致的;

10.   bar(p);                                        // bar: special char const *

这里 p 的类型本身就是特化函数的参数类型,显然要被推导为调用特化函数。

解释完了字符串参数的模板函数推导问题,下面来讨论一下 g++ 和 VS2008 的不同。上面同样的代码,使用 g++ 编译之后,输出是这个样子的:

foo: generic A6_c
foo: special PKc
foo: special PKc
foo: generic A6_c
foo: generic A6_c
foo: special PKc
bar: special PKc
bar: generic Pc
bar: special PKc
bar: special PKc
A6_c
A6_c
A6_c
PKc

当然,需要解释的是 g++ 内部对符号的字面做了一些变化,我们可以使用 c++filt demangle 这些符号:

$ c++filt [-t] A6_c PKc Pc
char [6]
char const *
char *

与 VS2008 的输出相比,我有一个疑问,为什么 g++ 没有为 const char [6] 输出正确的 const 类型名呢?

还有,我们提到了第 1 种情况下,编译器为 foo("hello") 调用实例化了一个 void foo<char const [6]>(const char (& t)[6]) 类型的函数。假如我们提供了一个类似的特化函数,那么 foo("hello") 会调用该特化函数;但是,使用 g++ 编译器时,特化函数的类型必须是 void foo<char [6]>(const char (& t)[6]) 而不是 void foo<char const [6]>(const char (& t)[6]),这让我感觉非常奇怪。只有不提供模板参数时,比如 void foo(const char (& t)[6]),两个编译器才能都推导出调用特化函数。

需要验证的话,您可以尝试在第一段代码中增加下面两个特化函数,再在两个编译器上编译那段代码:

template<>
void foo<char [6]>(const char (& t)[6])
{
  cout << "foo: special<char [6]> " << typeid(t).name() << endl;
}

template<>
void foo<char const [6]>(const char (& t)[6])
{
  cout << "foo: special<char const [6]> " << typeid(t).name() << endl;
}

将文本文件读入数组-C语言实现

要求:使用 C 语言将文本文件的每一行读入为数组的一个元素,返回一个 char ** 指针。

由于行长度和文本文件行数均未知,相当于二维 char 数组的两维长度都未定义。由于 getline 函数可以自动扩充 char 数组长度,我最初的想法是使用 getline 得到每行,然后每次对 char ** 进行 realloc,直到读完整个文件。

但是这种做法并不好,首先 getline 是 glibc 的扩展,而不是 C 语言的标准函数,使用除 gcc 以外的编译器是不一定能编译通过的;其次,每次对 char ** 指针进行 realloc 显得代码很 ugly。可以使用 fgets 替代 getline,但是就要自己来控制一维 char 数组的长度。

后来想想,换了一种思路,首先将整个文件读入内存,然后根据 '\n' 的个数来计算文件的行数,作为二维数组的长度,然后将所有的 '\n' 替换成 '\0',并将每一行的指针赋给二维 char 数组,代码如下:

char ** text_2_array(const char *filename)
{
  char *p, **array;
  int lines;
  if(filename == NULL) return NULL;

  FILE *fp = fopen(filename, "r");
  if(fp == NULL) return NULL;

  /* Get file size. */
  fseek(fp, 0L, SEEK_END);
  long int f_size = ftell(fp);
  fseek(fp, 0L, SEEK_SET);

  /* Allocate space for file content. */
  char *buf = (char *) calloc(f_size, sizeof(char));
  if(buf == NULL) return NULL;

  fread(buf, sizeof(char), f_size, fp);
  fclose(fp);

  /* Get number of lines. */
  for(p=strchr(buf, '\n'), lines=1; p!=NULL; p=strchr(p, '\n'), lines++) {
    if(*p == '\n') p++;
  }

  /* Allocate space for array; split file buffer to lines by change '\n' to
     '\0'. */
  array = (char **) calloc(lines+1, sizeof(char*));
  array[0] = buf;
  for(p=strchr(buf, '\n'), lines=1; p!=NULL; p=strchr(p, '\n')) {
    if(*p == '\n') *p++ = '\0';
    if(p != NULL) array[lines++] = p;
  }
  /* Add a terminate NULL pointer. */
  array[lines] = NULL;
  return array;
}

其实读文本文件入数组这个功能在很多语言中是很简单的操作,比如 PHP 的 file 函数,或者 Bash 的 (`cat filename`),都可以直接实现这个功能。但是对 C 这种更低级的语言来说,貌似就没那么简单了。我想要了解的是,除了我上面提到的两种思路,有没有更简单或者直接的方法来解决这个问题?比如一些我不熟悉的函数,或者一些 trick。

豆瓣好友统计图标

自从 Feedburner 订阅数统计图标成为博客装逼工具之后,各种各样的统计图标层出不穷,比如我也使用的 Twitter Counter。但是我一直没发现我认为很有装逼范儿的豆瓣提供好友数统计图标,因此我就使用豆瓣的 API 自己搞了一个。其实我主要是觉得在博客侧栏放豆瓣图书列表太多了,而一个小“豆”字也没啥意思,搞个好友数图标就挺好玩了。

我想没准你也会感兴趣,所以我把这个服务发布了出来。豆瓣好友统计图标的主页在:

http://solrex.org/douyou/

下面是直接从主页拷贝过来的内容,您也可以到我的博客侧栏看看效果。

介绍

呃——我觉得写主页比写主程序还费劲。简单来说,这个东西就跟 Feedburner 的订阅数统计图标类似,利用豆瓣提供的 API 抓取你的豆瓣好友数量,并做成一个小图片出来让你可以放在自己的博客上秀一秀。比如下面就是我的豆瓣好友统计图标:

豆瓣

你还可以移步到我的博客右侧栏,看看豆瓣好友统计图标和其它统计图标共存的状况。本统计图标一天更新一次,因此统计数并不完全实时,这是为了减轻服务器负载,请理解。

这个小项目完全是出于兴趣写成的,因此很简陋且维持在可用的水平上,我也没有更优化它的想法。在我服务器能承受的情况下我会尽量维持它,但本人不对服务的有效性和可用性做出任何承诺。

我觉得这个服务本身应该由豆瓣提供,如果你是豆瓣的工作人员,觉得这个站点有趣并想在豆瓣中加入此服务的话,欢迎你和我联系,我将无偿提供所有的代码,仅仅希望在对应产品中加上一个 Thanks to 到我的链接。

生成图片

输入豆瓣 UID:
(豆瓣用户 UID,英文或数字,非登录 email 地址)

(若没有即刻显示请稍等后多提交一次,服务器抓取信息可能有延迟。不知道自己的 UID 的话,可以登录豆瓣,查看自己的设置->username项。)

豆瓣

您可以把下面这段代码嵌入到您的博客或者主页中来显示豆瓣好友统计:

<a href="http://www.douban.com/people/solrex" title="豆瓣好友统计"><img src="http://solrex.org/douyou/dc/solrex" style="border: 0pt none ;" alt="豆瓣" height="26" width="88"></a>

JPerf Single Jar with UDP BW Unit Fixed

JPerf is the GUI frond-end of IPerf, a TCP and UDP bandwidth performance measurement tool which allows the tuning of various parameters and UDP characteristics.

The official JPerf release (2.0.2 version) has some flaws. First, it mistakenly uses bytes/sec as the unit of UDP bandwidth, which should be bits/sec according to IPerf man-page:

-b, --bandwidth #[KM]
       for  UDP,  bandwidth  to  send  at  in  bits/sec (default 1 Mbit/sec,
       implies -u)

Second, starting it from command line is error prone. The command to start it (jperf.sh) is:

java -classpath jperf.jar:lib/forms-1.1.0.jar:lib/jcommon-1.0.10.jar:lib/jfreechart-1.0.6.jar:lib/swingx-0.9.6.jar net.nlanr.jperf.JPerf

We can see that all jar paths in classpath are relative paths. So if we create a symbol link to the jperf.sh script, e.g. /usr/bin/jperf -> /opt/jperf-2.0.2/jperf.sh. Then calling /usr/bin/jperf will result in some errors like:

Exception in thread "main" java.lang.NoClassDefFoundError: net/nlanr/jperf/JPerf
Caused by: java.lang.ClassNotFoundException: net.nlanr.jperf.JPerf
at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
Could not find the main class: net.nlanr.jperf.JPerf. Program will exit.

This error can be fixed by resolving the real path of the symbol link, as I reported.

However, a better way to solve this problem is to pack all libs JPerf needed(i.e. forms*.jar, jcommon*.jar, jfreechart*.jar, swingx*.jar) to a single jar, and add a proper "Manifest". Then we will be able to start JPerf with a much simpler command:

java -jar jperf.jar

And finally, I gave a try to solve the above 2 flaws and put my work (deb/jar/src packets) on my site. You can find them here .

在 Ubuntu 9.04 上安装 Kscope

Kscope 是我很喜欢的 Linux 平台上的代码查看工具,因为我不会用 Emacs,vim + ctags 又用得不熟,看看小程序还可以,看大项目就傻眼了。以前也尝试过 Source-Navigator(这个项目N年没更新,06年时候我装都装不上,08年底居然又复活了,有空了再去试试)、Eclipse、Kdevelop、CodeBlocks,总之都没有 Kscope 用着最舒服。Kscope 让我欣赏的特点主要有:

1. 它号称是代码编辑环境(source-editing environment),而不是IDE。我不用在建立 Kscope 项目时烦心地去选择项目类型、编译器、编译选项等等。编译我有 Makefile,我就是找个工具看看代码,用得着那么麻烦吗。 建立 Kscope 项目时只需要干两件事:选择项目名、项目保存地址和添加源文件。

2. 它不会在源文件目录下建立一堆乱七八糟的文件,影响市容。我记得 Eclipse、CodeBlocks 等都会把项目信息保存在源文件目录下,而 Kscope 的项目保存位置可以自己选,比如我一般都保存在 workspace/kscope 目录下面,这样对要查看的源文件目录没有任何影响。因此 Kscope 的项目和源文件基本没关系,我可以添加任何位置的源文件到某个项目中去。

3. 它不会去读非指定类型的文件。这是针对 Eclipse 来说的,每次在 Eclipse 项目中搜索时,一堆 .svn 目录中文件的结果让我感觉非常闹心,两年没用不知道现在的 Eclipse 是不是更智能点儿了,但是 Eclipse 改不了的毛病就是慢和吃内存。

4. 它支持代码查看的基本功能。其实我最常用的也就那么几个功能:语法高亮、同时打开多文件、整个项目中搜索字符串、查找函数定义位置和引用、项目文件列表+搜索。在这些条上据说 Windows 下的 SourceInsight 做得更好,但我没用过没有发言权。

简而言之,Kscope 与其它工具比就是快、简单、省心。但是时代在变革呀,转眼到了 KDE4 的时代,而 Kscope 仍然停留在 KDE3.5 上。现在的 Ubuntu 9.04 的依赖关系里,居然已经撤掉了 Kscope,在 9.04 上 sudo apt-get install kscope,会得到这样的消息:E: Couldn't find package kscope,真是让人丧气。

其实 Kscope 之所以不能安装,主要原因是它依赖于 Kate 的两个库:libkateinterfaces.so.0 和 libkateinterfaces.so.0,只需要从 KDE3.5 的 Kate 中提取出来这两个库安装到系统中后,Kscope 就可以正常运行了。Ubuntu 9.04 的依赖关系中虽然找不到 Kscope,但是 Ubuntu 的软件仓库中还有 Kscope 的包,我们可以手动下载安装。下面这个脚本的功能就是自动安装 kscope 到 Ubuntu 9.04,稍微修改一下也可以用于在其它 KDE4 桌面系统中安装 Kscope,或者解决 Kscope 无法运行的问题。您也可以从这里下载到该脚本:

#!/bin/bash
# This script helps you install Kscope on Ubuntu 9.04.
# You can also use it to fix "Kscope doesn't run in KDE4" bug.

echo "Determining machine hardware name... "
MACHINE=`uname -m`
case "$MACHINE" in
  i386 | i586 | i686)
    ARCH="i386"
    ;;
  x86_64)
    ARCH="amd64"
    ;;
  *)
    ARCH="i386"
    ;;
esac

# If Kscope is not installed, install it.
which kscope &> /dev/null
if [ $? -ne 0 ]; then
  echo "Installing kscope..."
  sudo apt-get install kscope || \
  wget http://archive.ubuntu.com/ubuntu/pool/universe/k/kscope/kscope_1.6.0-1_${ARCH}.deb && \
  sudo dpkg -i kscope_*.deb || \
  sudo apt-get -fy install || \
  echo "Oops, some error happens..."
fi

kscope -v &> /dev/null
if [ $? -eq 0 ]; then
  echo "Kscope works fine."
  exit
fi

echo "Downloading KDE3 libraries needed by kscope..."
wget http://ftp.debian.org/debian/pool/main/k/kdebase/kate_3.5.9.dfsg.1-6_${ARCH}.deb
dpkg -x kate_3*.deb kate

echo "Installing KDE3 libraries..."
sudo cp kate/usr/lib/libkateinterfaces.so.0.0.0 /usr/local/lib/
sudo cp kate/usr/lib/libkateutils.so.0.0.0 /usr/local/lib
sudo ln -s /usr/local/lib/libkateinterfaces.so.0.0.0 /usr/local/lib/libkateinterfaces.so.0
sudo ln -s /usr/local/lib/libkateutils.so.0.0.0 /usr/local/lib/libkateutils.so.0
sudo ldconfig

echo "Finished."

vasprintf 会将空间分配到栈上吗?

由于提交过几次 Linux Fetion 的 bug 和 patch,Linux Fetion 的开发者邀请我加入了 Linux Fetion GUI 的维护者团队中。

昨天晚上和今天下午,我和邓东东(DDD)一直在调试一个 Linux Fetion 在 64 位电脑上的段错误 BUG。这是一个非常奇怪的 BUG,其表现为在 64 位电脑上(Ubuntu 9.04)运行 Linux Fetion 在登录成功后会经常出现 Segmentation Fault。DDD 确定该 BUG 存在于 Libfetion 库中,并且和读取联系人信息的函数有关。Libfetion 论坛上一直有人抱怨类似问题,但是在 DDD 的 64 位虚拟机上却无法重现此 BUG(他的 libc 是 2.7 版本的)。

由于 DDD 仍然不愿意公开 libfetion 库的源代码,我只好等每次他修改库文件之后发给我再调试。经过了好几个小时的努力,今天下午我发现,该 BUG 的主要成因非常有可能是:子函数中本应被动态分配到堆(heap)上的空间被分配(或误写)到了栈(stack)上,子函数返回调用者之后指向子函数栈内容的指针非法。

由于该动态分配的空间是使用 vasprintf 自动分配的,从 DDD 给我的部分代码来看指针传递出问题的可能性不大。那么我想,是不是 vasprintf 函数并不能保证动态分配的空间在 heap 上呢?希望对此有了解的朋友指点一下,谢谢!

$ uname -a
Linux Slytherin 2.6.28-12-generic #43-Ubuntu SMP Fri May 1 19:31:32 UTC 2009 x86_64 GNU/Linux
$ gcc -v
Using built-in specs.
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.3.3-5ubuntu4' --with-bugurl=file:///usr/share/doc/gcc-4.3/README.Bugs --enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr --enable-shared --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --enable-nls --with-gxx-include-dir=/usr/include/c++/4.3 --program-suffix=-4.3 --enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc --enable-mpfr --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.3.3 (Ubuntu 4.3.3-5ubuntu4)
$ ll /lib/libc.so.6
lrwxrwxrwx 1 root root 11 2009-04-13 10:26 /lib/libc.so.6 -> libc-2.9.so

PS:我在写这篇博文过程中搜索了一下 Wikipedia,发现这样一段话:

int asprintf(char **ret, const char *format, ...)

asprintf automatically allocates enough memory to hold the final string. It sets *ret to a pointer to the resulting string, or to an undefined value if an error occurred (GLibc is notable in being the only implementation that doesn't always set *ret to NULL on error).

那么是不是分配失败导致了错误的发生呢?但是如果分配失败,为什么子函数返回前的指针的确指向一段在栈上的字符串呢?

多余的逗号?

晚上看了两页 The Art of Unix Programming,其中提到了一个我以前一直感觉困惑的地方:

在我看过的 C/C++ 语言程序代码中,为什么有的列表初始化时在最后元素后会加逗号“,”,而有的不会?
例如:int[] a = { 1, 2, 3, };

书中的原话倒不是讨论逗号该不该加,而是说到了这样做能带来的好处:

A good example is C accommodating an extra comma at the end of an array initializer list, which makes both editing and machine generation of array initializers much easier.
-- The Art of Unix Programming (TAOUP) Ch8.3.1

哦,虽然我一直体会到这样做的好处(尤其当列表成员又臭又长且要经常修改时),也晓得这样做不会引起编译错误,但我经常是在代码 stable 之后将最后的逗号去掉——原因无它,不确定这样做是不是没有问题,那么还是尽量避免吧。今天忽然看到 TAOUP 提到这个,我就好奇:到底是 C/C++ 标准允许这样做呢?还是编译器的实现大部分支持这样做?于是就查了一下。

结果让我很开心,C/C++ 标准中就允许这样做:

initializer:
    assignment-expression
    { initializer-list }
    { initializer-list , }

-- ISO/IEC 9899:1999 (C99) Ch6.7.8 §1

initializer-clause:
    assignment-expression
    { initializer-list ,opt }
    { }

-- ISO/IEC 14882:1998 (C++98) Ch8.5 §1

K&R 中也用非常简短的一句话提到了这个特性:

A list may end with a comma, a nicety for neat formatting.
-- The C Programming Language (K&R) Appendix 8.7

这意味着(C/C++ 语言中)在元素列表最后加上一个逗号是一件非常安全的事情,看来我以后不必再考虑删除列表最后那个逗号了,这样能省却我很多麻烦。

延伸阅读:在其它编程语言中,是否支持这样做呢?Arrays: additionnal commas 这篇文章进行了一个很有意思的讨论。

应用程序打包技术之四(exe篇)

1. 应用程序打包技术之一(源代码篇)
2. 应用程序打包技术之二(deb篇)
3. 应用程序打包技术之三(rpm 篇)
4. 应用程序打包技术之四(exe篇)

exe 是 Windows 下通用应用程序的后缀,因此也是 Windows 下安装程序的后缀,我就以 exe 篇来命名这篇文章吧。

我们在 Windows 下写程序,尤其是带有图形界面的应用程序,往往不仅需要一个可执行的 exe 文件,还需要一些辅助的 dll 或者资源文件。一般情况下,我们可以采用打包整个目录的方式来发布软件(也是目前很多所谓“绿色软件”的做法),当然我们也可以发布传统的 Windows 安装文件。这样用户不需要额外的解压缩软件,双击执行安装文件就可以安装软件。

HM NIS Edit

将 Win 下应用程序打包成安装文件的方法有很多,这里我们主要介绍一个开源的安装文件制作工具 NSIS(Nullsoft Scriptable Install System)。比如我们耳熟能详的软件 WiNamp 和 eMule 的安装文件,就是使用 NSIS 生成的。它的主页在http://nsis.sourceforge.net,您可以从它的主页下载到最新版本。

NSIS 是一个基于脚本的安装文件制作工具,因此我们必须写好一个脚本提供给它执行。总是要学一些“微语言”真是麻烦的事情,幸好我们还有 HM NIS Edit,一个开源的 NSIS 脚本编辑工具。使用 HM NIS Edit,我们可以一步一步地按照向导生成 NSIS 所需的脚本。因此,我们这篇文章其实主要是介绍 HM NIS Edit 的使用,和 NSIS 的部分脚本格式。使用 NSIS 脚本生成安装文件那一步,实在是太简单不过了,点一下鼠标或者敲一个命令而已。

假设我们要为 foo 程序生成一个安装文件,在打包之前您应该将 foo 的可执行程序、所需 dll 和资源文件等放在一个文件夹 foo 中。比如假设 foo 程序的目录是这样的。

D:>tree /F foo
D:FOO
│  foo.dll
│  foo.exe
│  licence.txt
│
└─image
        foo.ico

下面我们就可以使用 HM NIS Edit 来创建 NSIS 脚本了。启动 HM NIS Edit,在 File 菜单中选择 New script from wizard 脚本生成向导,点击 Next 下一步,进入程序信息页。

Application name 程序名这里我们填入要打包的程序名 "foo";Application version 程序版本我们填入 foo 的版本,比如 "1.0";Application publisher 发布者我们填程序的开发公司名或者自己的名字,比如 "张三";Application website 程序主页我们填程序的主页名,没有主页的就可以不填。然后点击下一步进入安装文件选项页。

Setup icon 安装图标是您希望您的安装文件长什么样子,而不是您应用程序的图标,一般选择默认即可;Setup file 是您希望安装文件叫什么名字,比如 "foo-1.0-setup.exe";Setup lang 安装程序语言是安装过程中的提示所使用的语言,您可以根据您的需要选择,比如简体中文 "SimpChinese";GUI 是安装文件的对话框风格,随便您喜欢哪种;Compress 压缩格式是您希望使用什么格式将应用程序压缩存放在安装文件中。然后点击下一步进入应用程序默认安装目录和协议页。

Application default directory 里面填您的应用程序默认安装到哪里,比如 $PROGRAMFILESfoo 是默认安装到 C:Program Filesfoo 目录下,最好勾选上 Allow user to change the application directory,允许用户更改安装目录,这样您的程序显得更人性化一点儿;License file 是指应用程序所使用的协议文本,如果您在 foo 目录下已经准备了协议文件 license.txt,那么直接填 licence.txt 即可。这个协议就是您通常在 Windows 下安装软件时,第一个页面提示的“是否同意上述协议”的“上述协议”文本框里的内容;下面几个选项是让用户选择如何接受协议。点击下一步,进入应用程序文件选择页。

在应用程序文件选择页中会有三个文本框。这个页面的作用是分组添加应用程序所需要的程序文件,这样用户安装时就可以通过选择“最小安装”、“完全安装”、“自定义”等选择安装不同的组件。左上方的文本框是组件框,右下方的文本框是组件信息说明框,右方最大的文本框是每个组件所包含的可执行、dll 和资源文件。如果我们的程序很简单,不用分什么组件,我们就只用一个 MainSection 就行了。点中左上方文本框中的 MainSection,在右侧将所有程序文件添加进去。由于我们已经将所有文件都放置在了 D:foo 目录下,我们只需要点选第二个图标:Add directory tree,在对话框中将源目录选择为 F:Moviefoo,目标目录选择为 $INSTDIR,这样 foo 下所有的文件和目录都将会被安装到 $INSTDIR(默认是 C:Program Filesfoo)目录下。确定之后返回文件选择页,点击下一步进入应用程序图标页。

应用程序图标页的主要作用是选择将会被安装到“桌面”和“开始”菜单的快捷方式指向的可执行程序。如果您的程序名和项目名一样,或者 foo 目录下只有一个 exe 可执行文件,此处就使用默认设置即可。Create an Internet shortcut in the Start Menu folder 的意思是在“开始”菜单中添加一个到软件主页的快捷方式;Create an Uninstall shortcut in the Start Menu folder 的意思是在“开始”菜单中添加一个到卸载程序快捷方式。点击下一步进入安装后执行设置页。

安装后执行的意思是当安装程序安装完成后,用户选择安装后直接启动应用程序或者查看自述文件时,程序的行为。如果您有自述文件,就在 Readme 中填入自述文件的名字,比如 readme.txt,如果没有,就什么也不填,直接进入下一步程序卸载选项。

如果您选择了使用卸载程序 Use uninstaller,NSIS 将会为您自动生成一个卸载程序,其选项使用默认即可。点击下一步进入结束页。

最后结束时,HM NIS Edit 会询问您是否保存脚本。当然要保存了,保存了以后再需要生成安装文件时就不必使用 HM NIS Edit 重新生成脚本了。Convert file paths to relative paths 将脚本中的文件路径修改成相对于脚本文件的路径,这个选项也最好选上,这样在更改 foo 的目录时,我们只需要 NSIS 脚本与 foo 的相对位置不变就不影响脚本的使用。接下来保存脚本文件,最好将脚本文件保存在 foo 目录下,这样以后需要重新生成安装文件,只需要将 NSIS 拷贝到 foo 目录下就可以编译了。比如取名为 foo.nsi。

这样,整个脚本文件我们已经编写好了。现在我们到 D:foo 目录下,就能发现一个 foo.nsi 文件,右键点击 foo.nsi,在下拉菜单中选择 Compile NSIS script,不出错的话,就能在当前目录下生成一个名为 foo-1.0-setup.exe 安装文件了。您可以双击执行一下它,看看安装过程是否如您所料。

我们也可以使用命令行编译 NSIS 脚本,您可以使用这个命令:

C:Program FilesNSISmakensis.exe foo.nsi

如果您将 C:Program FilesNSIS 添加到了 PATH 环境变量中,就可以直接使用 makensis.exe foo.nsi 来编译了。

小技巧:

1. 当生成 NSIS 脚本之后,我们想修改设置,不需要重新执行一遍脚本生成向导。只需要用文本编辑器打开 foo.nsi,找到相应的域,更改设置即可。

2. NSIS 是一个相当强大的安装文件生成器,但是使用 HM NIS Edit 脚本生成向导生成的脚本并不具有很灵活的定制性。如果您需要更多特性,请阅读 NSIS 用户手册,您能从网上搜索到该手册的中文版本。然后直接去修改 NSIS 脚本。

3. 用 NSIS 产生的卸载程序有可能会产生卸载不干净的现象,主要原因是 NSIS 卸载程序不支持递归删除目录。如果您想要它把所有文件和目录都删除的话,就需要在 Section Uninstall 中将所有程序可能会生成的文件和目录都添加进去,这样生成的卸载程序就能卸载全部文件和目录了。

4. 您可以在这里找到更漂亮的图文教程。

APUE, A Great Book

这两周是选课试听期,还没有正式开始上课,所以有点空闲就翻了翻 UINX 环境高级编程(Advanced Programming in the UNIX Environment, 2e),看了七八章,发现这本书真的是无愧于“UNIX 编程圣经”的称号。书中对编程中可能遇到的问题讲解得非常系统和详细,尤其当看到自己以前遇到过问题的地方时,简直就有一种顿悟的感觉,就想感叹一句“哦,原来如此!”。

我平常在写程序时,遇到问题总是求助于 Google。对那些讲编程技巧的书向来不怎么感冒(尤其是中国人写的),总觉得那种书根本不适合花时间仔细看一遍。这种问题驱动式的学习方式固然在解决某一特定问题时显得快捷高效,但是也往往受限于一叶障目不见泰山的困境。在解决了某一问题之后,对其它同类问题没有足够关注,导致再遇到类似问题时仍需要去搜索答案。

问题驱动式的学习方式会导致对问题的了解不够系统和深入,但如果仅仅拿本大部头慢慢翻完的话,又会枯燥无味,而且体会也不深。我觉得读编程书的最好方法就是,先有一定量的实践,再去看书,而且要保持对书中习题和代码的练习量。有时候不妨先看实例代码再看正文解释,如果代码看得懂,看作者的解释是否和自己理解一样;如果代码看不懂,就会加深对正文的注意度。而且有时候读那些入门级的教科书,不妨只看代码。

当然,在编程的时候,桌子上应该有几本经典图书当作手册来参考,不时地重读一下某些章节会很有好处。像 APUE,我就觉得非常适合作为案头书,做 Linux/Unix 开发的程序员买一本看看绝对不会失望。

Something About Scanf() In C (1)

一. scanf 函数输入格式中的字符串

scanf 函数输入格式中也可以含有普通字符串,但他的含义是这些字符必须在输入中出现,例如:

int num;
scanf("hello %d", &num);

他的含义是首先要求输入一个hello字符串,然后再输入一个十进制数。 注意在等待输入时忽略hello与要输入的数之间的空格,制表符,回车。

因此这两种输入都是正确的: hello 1234 hello1234

二. scanf函数的返回值

程序:

{
  int num, result=0;
  printf("please input the student’s score: ");
  while(result==0) {
    /* 清空输入缓冲区。 */
    fflush(stdin);
    if(result!=1) printf("Please input a digital score: ");
    result=scanf("%d",&num);
  }
}

一切OK!

三. scanf函数中一个参数的应用

在 scanf 函数中,我们可以使用 %c 来读取一个字符,使用 %s 读取一个字符串。 但是读取字符串时不忽略空格,读字符串时忽略开始的空格,并且读到空格为止,因此我们只能读取一个单词,而不是整行字符串。因此一般使用 fgets 来读取一个字符串。

其实 scanf 函数也可完成这样的功能,而且还更强大。 这里主要介绍一个参数:%[] ,这个参数的意义是读入一个字符集合。 [] 是个集合的标志,因此 %[] 特指读入此集合所限定的那些字符, 比如 %[A-Z] 是输入大写字母,一旦遇到不在此集合的字符便停止。 如果集合的第一个字符是"^", 这说明读取不在 "^" 后面集合的字符,既遇到 "^" 后面集合的字符便停止。注意此时读入的字符串是可以含有空格的。 Eg: 输入一个字符串, 这个字符串只含有小写字符。遇到第一个不是小写字符时停止, scanf("%[a-z], str); Eg: 想输入一个字符串, 遇到 "." 停止,可设计如下: scanf("%[^.]", str); 使用这个参数,你可以完成许多强大的功能呦!

通常来讲,scanf 函数和他的一些参数并不是很常用,主要是因为:

1. 许多系统的 scanf 函数都有漏洞。 (典型的就是TC再输入浮点型时有时会出错)。

2. 用法复杂,容易出错。

3. 编译器作语法分析时会很困难,从而影响目标代码的质量和执行效率。

About "double" in C

昨天发现一个很有趣的现象,在 Turbo C 里 double 类型的变量无法用通常模式进行输入操作,即无法用 scanf() 进行赋值,程序举例:

void main()
{
  double a,b,c;
  scanf("%f%f%f",&a,&b,&c);
  printf("%f%f%f",a,b,c);
}

输出结果均是 0.000000,猜想 C 语言应该没有默认初始值的功能,之所以是 0.000000 可能是保留六位小数的原因,由于数值较小且跟地址有关,输入的数值没有传入到地址中。如果将 double 型改为 float 型,则能正常操作。是何道理?难道 C 里输入函数对 double 不兼容?还是另有其他输入方法?回去要查一下 MSDN,不知道 C 有没有专门的帮助文档。

还有要注意的是 TC2 和 TC3 之间对程序要求的变化,在 TC3 中如果不包含标准输入输出的头文件 stdio.h,程序中使用 scanf 和 printf 会报错,而且主函数也必须声明类型,不然会有警告。难道是 ANSI C 的标准变了?还是 TC 为了完善自己只适用于 TC?看来还是要注意编译软件的兼容性问题。 C 语言在好多地方还是不如 C++ 呀!

PS: 输出结果是 0.000000 的原因是因为 scanf() 如果取不到值就会把变量赋零。

2009年6月11日15:40
只因当时年纪小呀,这篇博客现在让我看都快要笑死了。为了不误导读者,特更正如下:
1. 之所以读取错误,是因为本文中程序是错的,%f 匹配浮点类型,%lf 才能匹配 double 类型,这不是 scanf 的错。
2. TC3 对程序的检查是正确的,C 标准确实有变化,我当时是受一本错误的 C 语言教科书误导。
3. C 虽然很多地方不如 C++,但是 C 有简洁性的长处,C++ 是不能比的。