Archive for the ‘C++’ Category

printf(“(char)48 = %c\n”, (char)48);
printf(“(int)’0′ = %d\n”, (int)’0′);
printf(“convert ’0′ to 0: %d\n”, b_atoi(’0′));
printf(“convert 0 to ’0′: %c\n”, b_itoa(0));

在不严谨的情况下,可以如上写。

有的时候,我们会希望能够把1、 2 、 3这样的数字作为字符直接存储到字符串中。如果直接(char)2的话,你得到的不会是字符’2′,它更像一些乱码。

由于字符’0′对应int型的48,所以可以利用这个特性来进行int和字符字面上的转换。

zz from: http://www.ibm.com/developerworks/cn/aix/library/au-unixtools.html

计算机编程的最新技术将一种特殊的人性与一组特殊的工具结合在一起,用以生产出对其他人非常有帮助的一种神奇的产品,即软件。计算机程序员是一群注重细节的人,他们可以处理计算机中各种各样的困难。计算机的要求非常苛刻,并且不能容忍其中存在任何的偏差。毫无疑问,无论您的个性如何以及在工作中使用了何种辅助工具,计算机程序的编写都是非常困难的。

在 UNIX® 和 Linux® 中,任何事物都是文件。您可以认为,UNIX 和 Linux 编程实际上是编写处理各种文件的代码。系统由许多类型的文件组成,但目标文件具有一种特殊的设计,提供了灵活和多样的用途。

目标文件是包含带有附加地址和值的助记符号的路线图。这些符号可以用来对各种代码段和数据段进行命名,包括经过初始化的和未初始化的。它们也可以用来定位嵌入的调试信息,就像语义 Web,非常适合由程序进行阅读。

计算机编程中使用的工具包括代码编辑器,如 vi 或 Emacs,您可以使用这些工具输入和编辑希望计算机在完成所需任务时执行的指令,以及编译器和连接器,它们可以生成真正实现这些目标的机器代码。

高级的工具,称为集成调试环境 (IDE),它以统一的外观集成了不同工具的功能。IDE 使得编辑器、编译器、连接器和调试器之间的界限变得很模糊。因此,为了更深入地研究和了解系统,在使用集成的套件之前,最好先单独地使用这些工具。(注意:IDE 也通常被称为集成开发环境。)

编译器可以将您在代码编辑器中创建的文本转换为目标文件。最初,目标文件被称为代码的中间表示形式,因为它用作连接编辑器(即连接器)的输入,而连接编辑器最终完成整个任务并生成可执行的程序作为输出。

从代码到可执行代码的转换过程经过了良好的定义并实现了自动化,而目标文件是这个链中有机的连接性环节。在这个转换过程中,目标文件作为连接编辑器所使用的映象,使得它们能够解析各种符号并将不同的代码和数据段连接在一起形成统一的整体。

计算机编程领域中存在许多著名的目标文件格式。DOS 系列包括 COMOBJ 和 EXE 格式。UNIX 和 Linux 使用 a.outCOFFELF。Microsoft® Windows® 使用可移植的执行文件 (PE) 格式,而 Macintosh 使用 PEFMach-O 和其他文件格式。

最初,各种类型的计算机具有自己独特的目标文件格式,但随着 UNIX 和其他在不同硬件平台上提供可移植性的操作系统的出现,一些常用的文件格式上升为通用的标准。其中包括 a.outCOFF 和 ELF 格式。

要了解目标文件,需要一组可以读取目标文件中不同部分并以更易于读取的格式显示这些内容的工具。本文将讨论这些工具中比较重要的方面。但首先,您必须创建一个工作台,并在其中建立一个研究对象。

启动一个 xterm 会话,让我们先创建一个空白的工作台,并开始对目标文件进行研究。下面的命令创建了一个目录,可以将目标文件放到该目录中进行研究:

cd
mkdir src
cd src
mkdir hw
cd hw

然后,使用您最喜欢的代码编辑器,在 $HOME/src/hw 目录中输入清单 1 中的程序,并命名为 hw.c

#include <stdio.h>

int main(void)
{
  printf("Hello World!\n");
  return 0;
}

要使用 UNIX 工具库中提供的各种工具,可以将这个简单的“Hello World”程序作为研究的对象。您将学习构建和查看目标文件的输出,而不是使用任何快捷方法直接创建可执行文件(的确有许多这样的快捷方法)。

C 编译器的正常输出是用于您所指定的目标处理器的汇编代码。汇编代码是汇编器的输入,在缺省情况下,汇编器将生成所有目标文件的祖先,即 a.out 文件。这个名称本身表示汇编输出 (Assembler Output)。要创建 a.out 文件,可以在 xterm 窗口中输入下面的命令:

cc hw.c

注意:如果出现了任何错误或者没有创建 a.out 文件,那么您可能需要检查自己的系统或源文件 (hw.c),以找出其中的错误。还需要检查是否已将 cc 定义为运行您的 C/C++ 编译器。

最新的 C 编译器将编译和汇编步骤组合成一个步骤。您可以指定不同开关选项以查看 C 编译器的汇编输出。通过输入下面的命令,您可以看到 C 编译器的汇编输出:

cc -S hw.c

这个命令生成了一个新的文件 hw.s,其中包含您通常无法看到的汇编输入文本,因为编译器在缺省情况下将生成 a.out 文件。正如所预期的,UNIX 汇编程序可以对这种输入文件进行汇编,以生成 a.out 文件。

假定编译过程一切顺利,那么在该目录中就有了一个 a.out 文件,下面让我们来对其进行研究。有许多可用于研究目标文件的有价值的工具,下面便是其中一组:

  • nm:列出目标文件中的符号。
  • objdump:显示目标文件中的详细信息。
  • readelf:显示关于 ELF 目标文件的信息。

列表中的第一个工具是 nm,它可以列出目标文件中的符号。如果您输入 nm 命令,您将注意到在缺省情况下,它会寻找一个名为a.out 的文件。如果没有找到该文件,这个工具会给出相应的提示。然而,如果该工具找到了编译器创建的 a.out 文件,它将显示类似清单 2 的清单。

08049594 A __bss_start
080482e4 t call_gmon_start
08049594 b completed.4463
08049498 d __CTOR_END__
08049494 d __CTOR_LIST__
08049588 D __data_start
08049588 W data_start
0804842c t __do_global_ctors_aux
0804830c t __do_global_dtors_aux
0804958c D __dso_handle
080494a0 d __DTOR_END__
0804949c d __DTOR_LIST__
080494a8 d _DYNAMIC
08049594 A _edata
08049598 A _end
08048458 T _fini
08049494 a __fini_array_end
08049494 a __fini_array_start
08048478 R _fp_hw
0804833b t frame_dummy
08048490 r __FRAME_END__
08049574 d _GLOBAL_OFFSET_TABLE_
         w __gmon_start__
08048308 T __i686.get_pc_thunk.bx
08048278 T _init
08049494 a __init_array_end
08049494 a __init_array_start
0804847c R _IO_stdin_used
080494a4 d __JCR_END__
080494a4 d __JCR_LIST__
         w _Jv_RegisterClasses
080483e1 T __libc_csu_fini
08048390 T __libc_csu_init
         U __libc_start_main@@GLIBC_2.0
08048360 T main
08049590 d p.4462
         U puts@@GLIBC_2.0
080482c0 T _start

这些包含可执行代码的段称为正文段。同样地,数据段包含了不可执行的信息或数据。另一种类型的段,称为 BSS 段,它包含以符号数据开头的块。

对于 nm 命令列出的每个符号,它们的值使用十六进制来表示(缺省行为),并且在该符号前面加上了一个表示符号类型的编码字符。常见的各种编码包括:A 表示绝对 (absolute),这意味着不能将该值更改为其他的连接;B 表示 BSS 段中的符号;而 C 表示引用未初始化的数据的一般符号。

可以将目标文件中所包含的不同的部分划分为段。段可以包含可执行代码、符号名称、初始数据值和许多其他类型的数据。有关这些类型的数据的详细信息,可以阅读 UNIX 中 nm 的 man 页面,其中按照该命令输出中的字符编码分别对每种类型进行了描述。

在目标文件阶段,即使是一个简单的 Hello World 程序,其中也包含了大量的细节信息。nm 程序可用于列举符号及其类型和值,但是,要更仔细地研究目标文件中这些命名段的内容,需要使用功能更强大的工具。

其中两种功能强大的工具是 objdump 和 readelf 程序。通过输入下面的命令,您可以看到目标文件中包含可执行代码的每个段的汇编清单。对于这么一个小的程序,编译器生成了这么多的代码,真的很令人惊异!

objdump -d a.out

这个命令生成的输出如清单 3 所示。每个可执行代码段将在需要特定的事件时执行,这些事件包括库的初始化和该程序本身主入口点。

对于那些着迷于底层编程细节的程序员来说,这是一个功能非常强大的工具,可用于研究编译器和汇编器的输出。细节信息,比如这段代码中所显示的这些信息,可以揭示有关本地处理器本身运行方式的很多内容。对该处理器制造商提供的技术文档进行深入的研究,您可以收集关于一些有价值的信息,通过这些信息可以深入地了解内部的运行机制,因为功能程序提供了清晰的输出。

类似地,readelf 程序也可以清楚地列出目标文件中的内容。输入下面的命令,您将可以看到这一点:

readelf -all a.out

这个命令生成的输出如清单 4 所示。ELF Header 为该文件中所有段入口显示了详细的摘要。在列举出这些 Header 中的内容之前,您可以看到 Header 的具体数目。在研究一个较大的目标文件时,该信息可能非常有用。

正如从该输出中看到的,简单的 a.out Hello World 文件中包含了大量有价值的细节信息,包括版本信息、柱状图、各种符号类型的表格,等等。通过使用本文中介绍的这几种工具分析目标文件,您可以慢慢地对可执行程序进行研究。

除了所有这些段之外,编译器可以将调试信息放入到目标文件中,并且还可以显示这些信息。输入下面的命令,仔细分析编译器的输出(假设您扮演了调试程序的角色):

readelf --debug-dump a.out | less

这个命令生成的输出如清单 5 所示。调试工具,如 GDB,可以读取这些调试信息,并且当程序在调试器中运行的同时,您可以使用该工具显示更具描述性的标记,而不是对代码进行反汇编时的原始地址值。

在 UNIX 中,可执行文件 目标文件,并且您可以像对 a.out 文件那样对它们进行分析。可以进行一次有益的练习,更改到 /bin 或 /local/bin 目录,然后针对一些您最常用的命令,如 pwdpscat 或 rm,运行 nmobjdump 和 readelf。通常,在您编写需要某种功能的程序时,如果标准的工具已经提供了这个功能,那么通过运行 objdump -d <command>,可以查看这些工具究竟如何完成这项任务。

如果您倾向于使用编译器和其他的语言工具,那么您可以对组成计算机系统的各种目标文件进行仔细研究,并且您将会发现这项工作是非常值得的。UNIX 操作系统具有许多层次,那些通过工具查看目标文件所公开的层次,非常接近底层硬件。通过这种方式,您可以真实地接触到系统。

另外,还有一篇文章也涉及到segfault调试的问题:

http://hi.baidu.com/liheng_2009/blog/item/c065e880cc09129ef603a6e8.html

error number是由三个字位组成的,从高到底分别为bit2 bit1和bit0,所以它的取值范围是0~7.
bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址

我的segfault log为:

Nov  8 15:37:37 tj1clnxweb0004 madservice[27711]: segfault at 0000000000000018 rip 00002b80ddfc6be5 rsp 00000000410010a8 error 4

最终定位到,是针对C++的map类型erase时出错导致的:

正确代码:

for (spi=mad_spammers.begin(); spi!=mad_spammers.end();) {
if (spi->second < stamp) {
adr.s_addr = ((client_t)spi->first).ip;
ret = blacklist_remove_item(adr.s_addr);
mad_spammers.erase(spi++);
} else {
spi++;
}

错误代码:

for (spi=mad_spammers.begin(); spi!=mad_spammers.end();spi++) {

if (spi->second < stamp) {

adr.s_addr = ((client_t)spi->first).ip;

ret = blacklist_remove_item(adr.s_addr);

mad_spammers.erase(spi);

}

}

但是很怪异的是,进行以下测试发现:将spi++放置在位置1,正确;放置在位置2,依然内存出错。怀疑可能是编译器做了优化。

for (spi=mad_spammers.begin(); spi!=mad_spammers.end();) {

if (spi->second < stamp) {

adr.s_addr = ((client_t)spi->first).ip;

ret = blacklist_remove_item(adr.s_addr);

mad_spammers.erase(spi);

// 位置1

log_msg(“spammer cleanup, remove %s from blacklist %d”, inet_ntoa(adr), ret);

// 位置2

} else {

spi++;

}

在网上没有找到合适的开源collaborative filtering的C语言实现,所以决定自己动手写一个。

目前网络上的cf资源请参考前文:Collaborative Filtering Resources在动手之前,试用了SUGGEST库,这是一个库文件,没有开放源代码,但是性能和推荐结果都挺不错的,我没有妄图超越它。

计划写的内容(不一定按照计划来):

窥探SUGGEST库

实现user-based cf算法:使用三元组存储稀疏矩阵

实现user-based cf算法:计算用户间距离

实现user-based cf算法:根据相似用户进行推荐

实现item-based cf算法

根据item相关性,改进cf算法

由于我目前只初步实现了user-based cf算法,所以只能保证前4条。之后,我希望可以把源码开放出来,给同学们做一个参考。也希望更多更好的cf算法能够开源。

SCWS是一款简单的开源中文分词系统,其网址为:http://www.ftphp.com/scws/

在代码中添加了一些输出,以便探究其分词的算法:

[yicheng@chengyi cli]$ ./scws -c utf-8 -d dict.utf8.xdb -r rules.utf8.ini -D -i ‘中国人民在中国人民大学上大学’ -t 10
No. WordString               Attr  Weight(times)
————————————————-
[0,0]Ox11(中)
[0,1]Ox83(中国)
[0,2]Ox81(中国人)
[1,1]Ox31(国)
[1,2]Ox81(国人)
[2,2]Ox31(人)
[2,3]Ox81(人民)
[3,3]Ox21(民)
[4,4]Ox1(在)
[5,5]Ox11(中)
[5,6]Ox83(中国)
[5,7]Ox83(中国人)
[5,10]Ox81(中国人民大学)
[6,6]Ox31(国)
[6,7]Ox81(国人)
[7,7]Ox31(人)
[7,8]Ox83(人民)
[7,10]Ox81(人民大学)
[8,8]Ox21(民)
[9,9]Ox31(大)
[9,10]Ox81(大学)
[10,10]Ox21(学)
[11,11]Ox1(上)
[12,12]Ox11(大)
[12,13]Ox81(大学)
[13,13]Ox21(学)
PATH by keyword = 中国人, (weight=8.9964):
中国人 民
PATH by keyword = 中国, (weight=219.7764):
中国 人民
PATH by keyword = 国人, (weight=0.0218):
中 国人 民
PATH by keyword = 人民, (weight=219.7764):
中国 人民
01. 中国人民大学       nt    10.64(1)
02. 中国                   ns    6.26(1)
03. 人民                   n     4.41(1)
04. 大学                   n     4.23(1)
+–[lt-scws(scws-cli/1.1.2)]———-+
| TextLen:   42                  |
| Prepare:   0.0074    (sec)     |
| Segment:   0.0010    (sec)     |
+——————————–+

以下简单记录今天看代码的收获。

typedef struct

{

xdict_t d;

rule_t r;

unsigned char *mblen;

unsigned int mode;

unsigned char *txt;

int zis;

int len;

int off;

int wend;

scws_res_t res0;

scws_res_t res1;

word_t **wmap;

struct scws_zchar *zmap;

}       scws_st, *scws_t;

是很核心的结构体。其中,使用txt保存用户输入的待分词字符串,使用off记录当前待处理的字节位置,res0为返回的分词结果,wmap就是上面输出的二维数组,zmap为wmap中每个词在txt中的index位置,d是字典,r是rule。

1、在scws_get_result函数中,首先按照最简单的规则,对文字进行分段,比如字母、数字、标点、换行符等,都被当做token来分段。

2、 对于每一段,首先初始化wmap二维数组,在它的对角线上存储每个字。

3、 然后依次访问wmap的对角线,向后匹配,在字典中查找,若找到匹配结果,则存储在wmap的行里。比如

[0,0]Ox11(中)
[0,1]Ox83(中国)
[0,2]Ox81(中国人)

就是从对角线的第一个元素,依次匹配第二个、第三个元素的结果。

4、 根据rule,进行规则过滤

5、 对wmap中的元素遍历,根据tf、idf计算weight,找出weight最大的path,作为最优分词结果。

6、 清理内存,返回。

scws_get_result的返回结果是二维的。每一次返回的是当前段的处理结果,是一个链表。该结果的next,是下一段的处理结果。

所以需要

while (res = cur = scws_get_result(s))

{

while (cur != NULL)

{

printf(“Word: %.*s/%s (IDF = %4.2f)\n”,

cur->len, text+cur->off, cur->attr, cur->idf);

cur = cur->next;

}

scws_free_result(res);

printf(“\n———————\n”);

}

当父类是一个抽象类,即它具备至少一个virtual函数的时候,如果没有virtual的析构函数,gcc在打开-Wall的时候,会给出警告:

abstract.cc:5: 警告:‘class Base’ 有虚函数却没有虚析构函数
abstract.cc:10: 警告:‘class Child’ 有虚函数却没有虚析构函数
为了规避这个警告,可以给父类增加一个virtual的析构函数。那么这个析构函数到底有什么用处呢?
下面的代码的运行结果为:
child call
child call
Child destroy
Base destroy
Child destroy
Base destroy
可以看到,子类析构的时候,编译器会自动调用父类的析构函数。
  #include 

  using namespace std;

  class Base {
      public:
          virtual void call() = 0;
          virtual ~Base(){
              cout << "Base destroy" << endl;
          }
  };

  class Child : public Base{
      public:
          void call(){
              cout << "child call" << endl;
          }
          ~Child(){
              cout << "Child destroy" << endl;
          }
  };

  int main()
  {
      Child c;
      c.call();

      Base* b = new Child();
      b->call();
      delete b;

      return 0;
  }

但是,如果把Base析构函数的virtual去掉,gcc仍然会给出警告,运行结果:

child call

child call

Base destroy

Child destroy

Base destroy

这代表着,当显示操作子类Child的时候,会先调用Child的析构函数,然后编译器自动调用父类Base的析构函数。但是,当使用父类的指针操作Child的时候,就仅调用了父类Base的析构函数!

所以,《C++大学教程》里有这样的两段话:

1、如果您打算让别人从您的类中派生子类,并希望能够通过父类指针多态的删除它们,就把父类的析构函数定义为虚函数。

2、如果您想创建一个父类,可以被派生,但是不要多态的删除它们,那么就把父类的析构函数定义为protected的非虚函数。这样,子类仍然可以隐式调用父类的析构函数,但是其他函数,由于无法访问父类的非public函数,就无法直接调用父类的析构函数了。

考虑下面的代码,父类P没有无参构造函数,当调用子类C的有参构造函数时,由于没有采用C(int i):P(i)的形式,先调用父类的有参构造函数,所以P的无参构造函数会首先被调用!这样就会引起编译器报错。

  class P{
      public:
          P(int i):val(i){}

      private:
          int val;
  };

  class C : public P{
      public:
          C(int i){
              P(i);
          }
  };

  int main(){
      C *c = new C(1);
      return 0;
  }

但是,如果子类C的有参构造函数,必须对它的参数先进行处理,然后才能初始化父类中的值,该怎么办呢?

答案是:为父类P添加一个无参构造函数,和一个接受参数的init函数。

  class P{
      public:
          P(int i):val(i){}
          P(){}

          void init(int i){
              val = i;
          }

      private:
          int val;
  };

  class C : public P{
      public:
          C(int i){
              init(i);
          }
  };

  int main(){
      C *c = new C(1);
      return 0;
  }

那么结论是不是c++里子类想调用父类的构造函数,是不是只能在初始化列表里调用呢?这样不会太麻烦了吗?为什么这样设计呢?

另外,记得在某个地方看到过,如果一个类有可能成为别的类的父类,那么就必须要有无参构造函数。现在明白为什么了!

在《C++大学教程》第三版的第四章《引用变量》看到这样一句话:

C++中的引用变量是C中没有的。

很疑惑,C中不也有&符号吗?不过那里是叫取地址符。这两个有什么区别呢?

在网上搜了搜,自己写了以下一段代码来区分它们:

#include 
using std::cout;
using std::endl;
void init(char ** ptr){
*ptr = "this is c style";
}
void init(char * &ptr){
ptr = "this is c++ style";
}
int main(){
char * ptr;
init(&ptr); // 取地址符
cout << ptr << endl;
init(ptr); // 引用调用
cout << ptr << endl;
return 0;
}

可以看出,C++中的引用变量,其实也是把变量所在地址给传递过去,但是使用起来,却不像指针那样还需要解引用(*)。这样就给编程带来了一定的便利。

那么引用和指针又有什么区别呢?

首先,引用必须在声明的同时初始化。而指针不必。

其次,引用变量不可改变,引用变量指向的对象内容可以改变。所以,没有这样的声明方式:int &const pi = 1; 因为pi本来就是const的了。

为了加深对c++的了解,做了习题:

// learning C++ 4.1

void swapByPointer (int * p1, int * p2){

*p2 = *p1 + *p2;

*p1 = *p2 - *p1;

*p2 = *p2 - *p1;

}

void swapByRef(int & p1, int & p2){

p2 = p1 + p2;

p1 = p2 - p1;

p2 = p2 - p1;

}

// learning C++ 4.2

void find(int const * array, std::size_t const dim, int &min, int &max){

if (dim <= 0){

return ;

}

min = array[0];

max = array[0];

for(std::size_t i = 1; i < dim; ++i){ if (array[i] > max){

max = array[i];

}

if (array[i] < min){

min = array[i];

}

}

}

那么int *& 与 int &的区别是什么呢?

int *i = new int(1);

int & ri = *i;

cout << ri << endl;

int* & rpi = i;

cout << *rpi << endl;

int j = 1;

int & rj = j;

//int* & rpj = &j; // error: 将类型为 ‘int*&’ 的非 const 引用初始化为类型为 ‘int*’ 的临时变量无效

int * p = &j;

可以看出,int & ri = *i; 声明的ri是一个指向int型的引用(即*i的值),如果i=new int(2),ri的内容也不会发生变化;而int* & rpi = i;声明的rpi是指向int指针的引用,所以如果i=new int(2),rpi的内容会随之变化。

同时,由上面的error语句也可以看出,引用与取地址是不同的!(不过这里还有一点迷糊)